From 0b1cd52df10e199de3dbcfb95a028e916a78a992 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 19 Feb 2026 00:55:27 +0200 Subject: [PATCH] ConfigTools & Encription manager --- ConfigTools.py | 49 + PKGBUILD | 22 + lib/jaraco.classes-3.4.0.dist-info/INSTALLER | 1 + lib/jaraco.classes-3.4.0.dist-info/LICENSE | 17 + lib/jaraco.classes-3.4.0.dist-info/METADATA | 60 + lib/jaraco.classes-3.4.0.dist-info/RECORD | 15 + lib/jaraco.classes-3.4.0.dist-info/WHEEL | 5 + .../top_level.txt | 1 + lib/jaraco/classes/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 161 bytes .../__pycache__/ancestry.cpython-314.pyc | Bin 0 -> 2859 bytes .../classes/__pycache__/meta.cpython-314.pyc | Bin 0 -> 3399 bytes .../__pycache__/properties.cpython-314.pyc | Bin 0 -> 10705 bytes lib/jaraco/classes/ancestry.py | 76 + lib/jaraco/classes/meta.py | 85 + lib/jaraco/classes/properties.py | 241 + lib/jaraco/classes/py.typed | 0 lib/jaraco/context/__init__.py | 367 ++ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 15456 bytes lib/jaraco/context/py.typed | 0 lib/jaraco/functools/__init__.py | 722 +++ lib/jaraco/functools/__init__.pyi | 123 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 24901 bytes lib/jaraco/functools/py.typed | 0 lib/jaraco_context-6.1.0.dist-info/INSTALLER | 1 + lib/jaraco_context-6.1.0.dist-info/METADATA | 82 + lib/jaraco_context-6.1.0.dist-info/RECORD | 9 + lib/jaraco_context-6.1.0.dist-info/WHEEL | 5 + .../licenses/LICENSE | 18 + .../top_level.txt | 1 + .../INSTALLER | 1 + lib/jaraco_functools-4.4.0.dist-info/METADATA | 69 + lib/jaraco_functools-4.4.0.dist-info/RECORD | 10 + lib/jaraco_functools-4.4.0.dist-info/WHEEL | 5 + .../licenses/LICENSE | 18 + .../top_level.txt | 1 + lib/jeepney-0.9.0.dist-info/INSTALLER | 1 + lib/jeepney-0.9.0.dist-info/METADATA | 35 + lib/jeepney-0.9.0.dist-info/RECORD | 64 + lib/jeepney-0.9.0.dist-info/WHEEL | 4 + lib/jeepney-0.9.0.dist-info/licenses/LICENSE | 21 + lib/jeepney/__init__.py | 13 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 716 bytes lib/jeepney/__pycache__/auth.cpython-314.pyc | Bin 0 -> 7938 bytes .../__pycache__/bindgen.cpython-314.pyc | Bin 0 -> 9689 bytes lib/jeepney/__pycache__/bus.cpython-314.pyc | Bin 0 -> 3151 bytes .../__pycache__/bus_messages.cpython-314.pyc | Bin 0 -> 12619 bytes lib/jeepney/__pycache__/fds.cpython-314.pyc | Bin 0 -> 7268 bytes .../__pycache__/low_level.cpython-314.pyc | Bin 0 -> 36681 bytes .../__pycache__/wrappers.cpython-314.pyc | Bin 0 -> 15807 bytes lib/jeepney/auth.py | 144 + lib/jeepney/bindgen.py | 170 + lib/jeepney/bus.py | 62 + lib/jeepney/bus_messages.py | 238 + lib/jeepney/fds.py | 158 + lib/jeepney/io/__init__.py | 1 + .../io/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 203 bytes .../io/__pycache__/asyncio.cpython-314.pyc | Bin 0 -> 15285 bytes .../io/__pycache__/blocking.cpython-314.pyc | Bin 0 -> 19190 bytes .../io/__pycache__/common.cpython-314.pyc | Bin 0 -> 5989 bytes .../io/__pycache__/threading.cpython-314.pyc | Bin 0 -> 16183 bytes .../io/__pycache__/trio.cpython-314.pyc | Bin 0 -> 25606 bytes lib/jeepney/io/asyncio.py | 233 + lib/jeepney/io/blocking.py | 337 ++ lib/jeepney/io/common.py | 88 + lib/jeepney/io/tests/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 163 bytes .../__pycache__/conftest.cpython-314.pyc | Bin 0 -> 4950 bytes .../__pycache__/test_asyncio.cpython-314.pyc | Bin 0 -> 5692 bytes .../__pycache__/test_blocking.cpython-314.pyc | Bin 0 -> 5083 bytes .../test_threading.cpython-314.pyc | Bin 0 -> 5058 bytes .../__pycache__/test_trio.cpython-314.pyc | Bin 0 -> 9214 bytes .../tests/__pycache__/utils.cpython-314.pyc | Bin 0 -> 332 bytes lib/jeepney/io/tests/conftest.py | 81 + lib/jeepney/io/tests/test_asyncio.py | 95 + lib/jeepney/io/tests/test_blocking.py | 84 + lib/jeepney/io/tests/test_threading.py | 83 + lib/jeepney/io/tests/test_trio.py | 114 + lib/jeepney/io/tests/utils.py | 3 + lib/jeepney/io/threading.py | 273 + lib/jeepney/io/trio.py | 424 ++ lib/jeepney/low_level.py | 608 ++ lib/jeepney/tests/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 160 bytes .../__pycache__/test_auth.cpython-314.pyc | Bin 0 -> 1795 bytes .../__pycache__/test_bindgen.cpython-314.pyc | Bin 0 -> 1785 bytes .../__pycache__/test_bus.cpython-314.pyc | Bin 0 -> 1785 bytes .../test_bus_messages.cpython-314.pyc | Bin 0 -> 4578 bytes .../__pycache__/test_fds.cpython-314.pyc | Bin 0 -> 4937 bytes .../test_low_level.cpython-314.pyc | Bin 0 -> 6499 bytes .../__pycache__/test_wrappers.cpython-314.pyc | Bin 0 -> 5277 bytes lib/jeepney/tests/secrets_introspect.xml | 116 + lib/jeepney/tests/test_auth.py | 24 + lib/jeepney/tests/test_bindgen.py | 28 + lib/jeepney/tests/test_bus.py | 24 + lib/jeepney/tests/test_bus_messages.py | 112 + lib/jeepney/tests/test_fds.py | 80 + lib/jeepney/tests/test_low_level.py | 101 + lib/jeepney/tests/test_wrappers.py | 74 + lib/jeepney/wrappers.py | 265 + lib/keyring-25.7.0.dist-info/INSTALLER | 1 + lib/keyring-25.7.0.dist-info/METADATA | 554 ++ lib/keyring-25.7.0.dist-info/RECORD | 68 + lib/keyring-25.7.0.dist-info/REQUESTED | 0 lib/keyring-25.7.0.dist-info/WHEEL | 5 + lib/keyring-25.7.0.dist-info/entry_points.txt | 13 + lib/keyring-25.7.0.dist-info/licenses/LICENSE | 18 + lib/keyring-25.7.0.dist-info/top_level.txt | 1 + lib/keyring/__init__.py | 17 + lib/keyring/__main__.py | 4 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 375 bytes .../__pycache__/__main__.cpython-314.pyc | Bin 0 -> 287 bytes .../__pycache__/backend.cpython-314.pyc | Bin 0 -> 15492 bytes lib/keyring/__pycache__/cli.cpython-314.pyc | Bin 0 -> 11552 bytes .../__pycache__/completion.cpython-314.pyc | Bin 0 -> 2984 bytes lib/keyring/__pycache__/core.cpython-314.pyc | Bin 0 -> 10896 bytes .../__pycache__/credentials.cpython-314.pyc | Bin 0 -> 6521 bytes .../__pycache__/devpi_client.cpython-314.pyc | Bin 0 -> 1215 bytes .../__pycache__/errors.cpython-314.pyc | Bin 0 -> 3756 bytes lib/keyring/__pycache__/http.cpython-314.pyc | Bin 0 -> 2120 bytes lib/keyring/backend.py | 300 + lib/keyring/backend_complete.bash | 14 + lib/keyring/backend_complete.zsh | 14 + lib/keyring/backends/SecretService.py | 120 + lib/keyring/backends/Windows.py | 168 + lib/keyring/backends/__init__.py | 0 .../__pycache__/SecretService.cpython-314.pyc | Bin 0 -> 7680 bytes .../__pycache__/Windows.cpython-314.pyc | Bin 0 -> 8383 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 163 bytes .../__pycache__/chainer.cpython-314.pyc | Bin 0 -> 3750 bytes .../backends/__pycache__/fail.cpython-314.pyc | Bin 0 -> 1668 bytes .../__pycache__/kwallet.cpython-314.pyc | Bin 0 -> 9310 bytes .../__pycache__/libsecret.cpython-314.pyc | Bin 0 -> 9104 bytes .../backends/__pycache__/null.cpython-314.pyc | Bin 0 -> 1260 bytes lib/keyring/backends/chainer.py | 71 + lib/keyring/backends/fail.py | 30 + lib/keyring/backends/kwallet.py | 164 + lib/keyring/backends/libsecret.py | 155 + lib/keyring/backends/macOS/__init__.py | 85 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 4442 bytes .../macOS/__pycache__/api.cpython-314.pyc | Bin 0 -> 7516 bytes lib/keyring/backends/macOS/api.py | 184 + lib/keyring/backends/null.py | 20 + lib/keyring/cli.py | 220 + lib/keyring/compat/__init__.py | 7 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 330 bytes .../__pycache__/properties.cpython-314.pyc | Bin 0 -> 5537 bytes .../compat/__pycache__/py312.cpython-314.pyc | Bin 0 -> 359 bytes lib/keyring/compat/properties.py | 169 + lib/keyring/compat/py312.py | 9 + lib/keyring/completion.py | 55 + lib/keyring/core.py | 202 + lib/keyring/credentials.py | 85 + lib/keyring/devpi_client.py | 29 + lib/keyring/errors.py | 67 + lib/keyring/http.py | 39 + lib/keyring/py.typed | 0 lib/keyring/testing/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 162 bytes .../__pycache__/backend.cpython-314.pyc | Bin 0 -> 12196 bytes .../testing/__pycache__/util.cpython-314.pyc | Bin 0 -> 3760 bytes lib/keyring/testing/backend.py | 200 + lib/keyring/testing/util.py | 68 + lib/keyring/util/__init__.py | 11 + .../util/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 705 bytes .../__pycache__/platform_.cpython-314.pyc | Bin 0 -> 2173 bytes lib/keyring/util/platform_.py | 40 + lib/more_itertools-10.8.0.dist-info/INSTALLER | 1 + lib/more_itertools-10.8.0.dist-info/METADATA | 283 + lib/more_itertools-10.8.0.dist-info/RECORD | 15 + lib/more_itertools-10.8.0.dist-info/WHEEL | 4 + .../licenses/LICENSE | 19 + lib/more_itertools/__init__.py | 6 + lib/more_itertools/__init__.pyi | 2 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 310 bytes .../__pycache__/more.cpython-314.pyc | Bin 0 -> 189675 bytes .../__pycache__/recipes.cpython-314.pyc | Bin 0 -> 50858 bytes lib/more_itertools/more.py | 5303 +++++++++++++++++ lib/more_itertools/more.pyi | 949 +++ lib/more_itertools/py.typed | 0 lib/more_itertools/recipes.py | 1471 +++++ lib/more_itertools/recipes.pyi | 205 + lib/secretstorage-3.5.0.dist-info/INSTALLER | 1 + lib/secretstorage-3.5.0.dist-info/METADATA | 109 + lib/secretstorage-3.5.0.dist-info/RECORD | 21 + lib/secretstorage-3.5.0.dist-info/WHEEL | 5 + .../licenses/LICENSE | 25 + .../top_level.txt | 1 + lib/secretstorage/__init__.py | 103 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 3842 bytes .../__pycache__/collection.cpython-314.pyc | Bin 0 -> 15384 bytes .../__pycache__/defines.cpython-314.pyc | Bin 0 -> 910 bytes .../__pycache__/dhcrypto.cpython-314.pyc | Bin 0 -> 3065 bytes .../__pycache__/exceptions.cpython-314.pyc | Bin 0 -> 2259 bytes .../__pycache__/item.cpython-314.pyc | Bin 0 -> 11877 bytes .../__pycache__/util.cpython-314.pyc | Bin 0 -> 11703 bytes lib/secretstorage/collection.py | 244 + lib/secretstorage/defines.py | 21 + lib/secretstorage/dhcrypto.py | 50 + lib/secretstorage/exceptions.py | 50 + lib/secretstorage/item.py | 159 + lib/secretstorage/py.typed | 0 lib/secretstorage/util.py | 227 + 203 files changed, 18643 insertions(+) create mode 100644 ConfigTools.py create mode 100644 PKGBUILD create mode 100644 lib/jaraco.classes-3.4.0.dist-info/INSTALLER create mode 100644 lib/jaraco.classes-3.4.0.dist-info/LICENSE create mode 100644 lib/jaraco.classes-3.4.0.dist-info/METADATA create mode 100644 lib/jaraco.classes-3.4.0.dist-info/RECORD create mode 100644 lib/jaraco.classes-3.4.0.dist-info/WHEEL create mode 100644 lib/jaraco.classes-3.4.0.dist-info/top_level.txt create mode 100644 lib/jaraco/classes/__init__.py create mode 100644 lib/jaraco/classes/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jaraco/classes/__pycache__/ancestry.cpython-314.pyc create mode 100644 lib/jaraco/classes/__pycache__/meta.cpython-314.pyc create mode 100644 lib/jaraco/classes/__pycache__/properties.cpython-314.pyc create mode 100644 lib/jaraco/classes/ancestry.py create mode 100644 lib/jaraco/classes/meta.py create mode 100644 lib/jaraco/classes/properties.py create mode 100644 lib/jaraco/classes/py.typed create mode 100644 lib/jaraco/context/__init__.py create mode 100644 lib/jaraco/context/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jaraco/context/py.typed create mode 100644 lib/jaraco/functools/__init__.py create mode 100644 lib/jaraco/functools/__init__.pyi create mode 100644 lib/jaraco/functools/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jaraco/functools/py.typed create mode 100644 lib/jaraco_context-6.1.0.dist-info/INSTALLER create mode 100644 lib/jaraco_context-6.1.0.dist-info/METADATA create mode 100644 lib/jaraco_context-6.1.0.dist-info/RECORD create mode 100644 lib/jaraco_context-6.1.0.dist-info/WHEEL create mode 100644 lib/jaraco_context-6.1.0.dist-info/licenses/LICENSE create mode 100644 lib/jaraco_context-6.1.0.dist-info/top_level.txt create mode 100644 lib/jaraco_functools-4.4.0.dist-info/INSTALLER create mode 100644 lib/jaraco_functools-4.4.0.dist-info/METADATA create mode 100644 lib/jaraco_functools-4.4.0.dist-info/RECORD create mode 100644 lib/jaraco_functools-4.4.0.dist-info/WHEEL create mode 100644 lib/jaraco_functools-4.4.0.dist-info/licenses/LICENSE create mode 100644 lib/jaraco_functools-4.4.0.dist-info/top_level.txt create mode 100644 lib/jeepney-0.9.0.dist-info/INSTALLER create mode 100644 lib/jeepney-0.9.0.dist-info/METADATA create mode 100644 lib/jeepney-0.9.0.dist-info/RECORD create mode 100644 lib/jeepney-0.9.0.dist-info/WHEEL create mode 100644 lib/jeepney-0.9.0.dist-info/licenses/LICENSE create mode 100644 lib/jeepney/__init__.py create mode 100644 lib/jeepney/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/auth.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/bindgen.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/bus.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/bus_messages.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/fds.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/low_level.cpython-314.pyc create mode 100644 lib/jeepney/__pycache__/wrappers.cpython-314.pyc create mode 100644 lib/jeepney/auth.py create mode 100644 lib/jeepney/bindgen.py create mode 100644 lib/jeepney/bus.py create mode 100644 lib/jeepney/bus_messages.py create mode 100644 lib/jeepney/fds.py create mode 100644 lib/jeepney/io/__init__.py create mode 100644 lib/jeepney/io/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jeepney/io/__pycache__/asyncio.cpython-314.pyc create mode 100644 lib/jeepney/io/__pycache__/blocking.cpython-314.pyc create mode 100644 lib/jeepney/io/__pycache__/common.cpython-314.pyc create mode 100644 lib/jeepney/io/__pycache__/threading.cpython-314.pyc create mode 100644 lib/jeepney/io/__pycache__/trio.cpython-314.pyc create mode 100644 lib/jeepney/io/asyncio.py create mode 100644 lib/jeepney/io/blocking.py create mode 100644 lib/jeepney/io/common.py create mode 100644 lib/jeepney/io/tests/__init__.py create mode 100644 lib/jeepney/io/tests/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/__pycache__/conftest.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/__pycache__/test_trio.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/__pycache__/utils.cpython-314.pyc create mode 100644 lib/jeepney/io/tests/conftest.py create mode 100644 lib/jeepney/io/tests/test_asyncio.py create mode 100644 lib/jeepney/io/tests/test_blocking.py create mode 100644 lib/jeepney/io/tests/test_threading.py create mode 100644 lib/jeepney/io/tests/test_trio.py create mode 100644 lib/jeepney/io/tests/utils.py create mode 100644 lib/jeepney/io/threading.py create mode 100644 lib/jeepney/io/trio.py create mode 100644 lib/jeepney/low_level.py create mode 100644 lib/jeepney/tests/__init__.py create mode 100644 lib/jeepney/tests/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_auth.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_bus.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_fds.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_low_level.cpython-314.pyc create mode 100644 lib/jeepney/tests/__pycache__/test_wrappers.cpython-314.pyc create mode 100644 lib/jeepney/tests/secrets_introspect.xml create mode 100644 lib/jeepney/tests/test_auth.py create mode 100644 lib/jeepney/tests/test_bindgen.py create mode 100644 lib/jeepney/tests/test_bus.py create mode 100644 lib/jeepney/tests/test_bus_messages.py create mode 100644 lib/jeepney/tests/test_fds.py create mode 100644 lib/jeepney/tests/test_low_level.py create mode 100644 lib/jeepney/tests/test_wrappers.py create mode 100644 lib/jeepney/wrappers.py create mode 100644 lib/keyring-25.7.0.dist-info/INSTALLER create mode 100644 lib/keyring-25.7.0.dist-info/METADATA create mode 100644 lib/keyring-25.7.0.dist-info/RECORD create mode 100644 lib/keyring-25.7.0.dist-info/REQUESTED create mode 100644 lib/keyring-25.7.0.dist-info/WHEEL create mode 100644 lib/keyring-25.7.0.dist-info/entry_points.txt create mode 100644 lib/keyring-25.7.0.dist-info/licenses/LICENSE create mode 100644 lib/keyring-25.7.0.dist-info/top_level.txt create mode 100644 lib/keyring/__init__.py create mode 100644 lib/keyring/__main__.py create mode 100644 lib/keyring/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/__main__.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/backend.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/cli.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/completion.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/core.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/credentials.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/devpi_client.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/errors.cpython-314.pyc create mode 100644 lib/keyring/__pycache__/http.cpython-314.pyc create mode 100644 lib/keyring/backend.py create mode 100644 lib/keyring/backend_complete.bash create mode 100644 lib/keyring/backend_complete.zsh create mode 100644 lib/keyring/backends/SecretService.py create mode 100644 lib/keyring/backends/Windows.py create mode 100644 lib/keyring/backends/__init__.py create mode 100644 lib/keyring/backends/__pycache__/SecretService.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/Windows.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/chainer.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/fail.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/kwallet.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/libsecret.cpython-314.pyc create mode 100644 lib/keyring/backends/__pycache__/null.cpython-314.pyc create mode 100644 lib/keyring/backends/chainer.py create mode 100644 lib/keyring/backends/fail.py create mode 100644 lib/keyring/backends/kwallet.py create mode 100644 lib/keyring/backends/libsecret.py create mode 100644 lib/keyring/backends/macOS/__init__.py create mode 100644 lib/keyring/backends/macOS/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/keyring/backends/macOS/__pycache__/api.cpython-314.pyc create mode 100644 lib/keyring/backends/macOS/api.py create mode 100644 lib/keyring/backends/null.py create mode 100644 lib/keyring/cli.py create mode 100644 lib/keyring/compat/__init__.py create mode 100644 lib/keyring/compat/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/keyring/compat/__pycache__/properties.cpython-314.pyc create mode 100644 lib/keyring/compat/__pycache__/py312.cpython-314.pyc create mode 100644 lib/keyring/compat/properties.py create mode 100644 lib/keyring/compat/py312.py create mode 100644 lib/keyring/completion.py create mode 100644 lib/keyring/core.py create mode 100644 lib/keyring/credentials.py create mode 100644 lib/keyring/devpi_client.py create mode 100644 lib/keyring/errors.py create mode 100644 lib/keyring/http.py create mode 100644 lib/keyring/py.typed create mode 100644 lib/keyring/testing/__init__.py create mode 100644 lib/keyring/testing/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/keyring/testing/__pycache__/backend.cpython-314.pyc create mode 100644 lib/keyring/testing/__pycache__/util.cpython-314.pyc create mode 100644 lib/keyring/testing/backend.py create mode 100644 lib/keyring/testing/util.py create mode 100644 lib/keyring/util/__init__.py create mode 100644 lib/keyring/util/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/keyring/util/__pycache__/platform_.cpython-314.pyc create mode 100644 lib/keyring/util/platform_.py create mode 100644 lib/more_itertools-10.8.0.dist-info/INSTALLER create mode 100644 lib/more_itertools-10.8.0.dist-info/METADATA create mode 100644 lib/more_itertools-10.8.0.dist-info/RECORD create mode 100644 lib/more_itertools-10.8.0.dist-info/WHEEL create mode 100644 lib/more_itertools-10.8.0.dist-info/licenses/LICENSE create mode 100644 lib/more_itertools/__init__.py create mode 100644 lib/more_itertools/__init__.pyi create mode 100644 lib/more_itertools/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/more_itertools/__pycache__/more.cpython-314.pyc create mode 100644 lib/more_itertools/__pycache__/recipes.cpython-314.pyc create mode 100755 lib/more_itertools/more.py create mode 100644 lib/more_itertools/more.pyi create mode 100644 lib/more_itertools/py.typed create mode 100644 lib/more_itertools/recipes.py create mode 100644 lib/more_itertools/recipes.pyi create mode 100644 lib/secretstorage-3.5.0.dist-info/INSTALLER create mode 100644 lib/secretstorage-3.5.0.dist-info/METADATA create mode 100644 lib/secretstorage-3.5.0.dist-info/RECORD create mode 100644 lib/secretstorage-3.5.0.dist-info/WHEEL create mode 100644 lib/secretstorage-3.5.0.dist-info/licenses/LICENSE create mode 100644 lib/secretstorage-3.5.0.dist-info/top_level.txt create mode 100644 lib/secretstorage/__init__.py create mode 100644 lib/secretstorage/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/secretstorage/__pycache__/collection.cpython-314.pyc create mode 100644 lib/secretstorage/__pycache__/defines.cpython-314.pyc create mode 100644 lib/secretstorage/__pycache__/dhcrypto.cpython-314.pyc create mode 100644 lib/secretstorage/__pycache__/exceptions.cpython-314.pyc create mode 100644 lib/secretstorage/__pycache__/item.cpython-314.pyc create mode 100644 lib/secretstorage/__pycache__/util.cpython-314.pyc create mode 100644 lib/secretstorage/collection.py create mode 100644 lib/secretstorage/defines.py create mode 100644 lib/secretstorage/dhcrypto.py create mode 100644 lib/secretstorage/exceptions.py create mode 100644 lib/secretstorage/item.py create mode 100644 lib/secretstorage/py.typed create mode 100644 lib/secretstorage/util.py diff --git a/ConfigTools.py b/ConfigTools.py new file mode 100644 index 0000000..2eabcb6 --- /dev/null +++ b/ConfigTools.py @@ -0,0 +1,49 @@ +from logging import Manager +import os +from lib.cryptography.fernet import Fernet + +class CredentialManager: + def __init__(self, key_file="secret_key"): + self.key_file = key_file + self.key = self._load_or_generate_key() + self.cipher = Fernet(self.key) + + def _load_or_generate_key(self): + if not os.path.exists(self.key_file): + key = Fernet.generate_key() + with open(self.key_file, "wb") as file: + file.write(key) + os.chmod(self.key_file, 0o600) + return key + with open(self.key_file, "rb") as file: + return file.read() + + def encrypt_password(self, password): + return self.cipher.encrypt(password.encode()).decode() + + def dencrypt_password(self, encrypted_password): + return self.cipher.decrypt(encrypted_password.encode()).decode() + + +manager = CredentialManager() + +password = "test1" +encrypted = manager.encrypt_password(password) +print(f"encrpted: {encrypted}") + + +decripted = manager.dencrypt_password(encrypted) +print(f"Decripted: {decripted}") + + + + + + + + + + + + + diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..e4d15c7 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,22 @@ +pkgname=ServerSync +pkgver=1.0.0 +pkgrel=1 +pkgdesc="A tool to simply manage & Sync files and directories to remotes with configs!" +arch=('any') +url="https://github.com/youruser/your-tool" +license=('MIT') +depends=('bash, python3') # Add runtime dependencies here +makedepends=('git' 'gcc') # Add build-time dependencies here +source=("https://github.com/youruser/$pkgname/archive/v$pkgver.tar.gz") +sha256sums=('SKIP') # We will fix this in the next step + +build() { + cd "$pkgname-$pkgver" + make # Or your specific build command +} + +package() { + cd "$pkgname-$pkgver" + # This installs the binary to /usr/bin inside the package + install -Dm755 your-binary-name "$pkgdir/usr/bin/your-binary-name" +} diff --git a/lib/jaraco.classes-3.4.0.dist-info/INSTALLER b/lib/jaraco.classes-3.4.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/jaraco.classes-3.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/jaraco.classes-3.4.0.dist-info/LICENSE b/lib/jaraco.classes-3.4.0.dist-info/LICENSE new file mode 100644 index 0000000..1bb5a44 --- /dev/null +++ b/lib/jaraco.classes-3.4.0.dist-info/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/lib/jaraco.classes-3.4.0.dist-info/METADATA b/lib/jaraco.classes-3.4.0.dist-info/METADATA new file mode 100644 index 0000000..6b11499 --- /dev/null +++ b/lib/jaraco.classes-3.4.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.1 +Name: jaraco.classes +Version: 3.4.0 +Summary: Utility functions for Python class constructs +Home-page: https://github.com/jaraco/jaraco.classes +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.8 +License-File: LICENSE +Requires-Dist: more-itertools +Provides-Extra: docs +Requires-Dist: sphinx >=3.5 ; extra == 'docs' +Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs' +Requires-Dist: rst.linker >=1.9 ; extra == 'docs' +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx-lint ; extra == 'docs' +Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest >=6 ; extra == 'testing' +Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-mypy ; extra == 'testing' +Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing' +Requires-Dist: pytest-ruff >=0.2.1 ; extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.classes.svg + :target: https://pypi.org/project/jaraco.classes + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.classes.svg + +.. image:: https://github.com/jaraco/jaraco.classes/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/jaraco.classes/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://readthedocs.org/projects/jaracoclasses/badge/?version=latest + :target: https://jaracoclasses.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2024-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/jaraco.classes + :target: https://tidelift.com/subscription/pkg/pypi-jaraco.classes?utm_source=pypi-jaraco.classes&utm_medium=readme + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/lib/jaraco.classes-3.4.0.dist-info/RECORD b/lib/jaraco.classes-3.4.0.dist-info/RECORD new file mode 100644 index 0000000..4d09383 --- /dev/null +++ b/lib/jaraco.classes-3.4.0.dist-info/RECORD @@ -0,0 +1,15 @@ +jaraco.classes-3.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.classes-3.4.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023 +jaraco.classes-3.4.0.dist-info/METADATA,sha256=LmsQIjLt1Frhu4prQJH9QM8yAaa7b9S8l8XozXZaRLg,2623 +jaraco.classes-3.4.0.dist-info/RECORD,, +jaraco.classes-3.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92 +jaraco.classes-3.4.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jaraco/classes/__pycache__/__init__.cpython-314.pyc,, +jaraco/classes/__pycache__/ancestry.cpython-314.pyc,, +jaraco/classes/__pycache__/meta.cpython-314.pyc,, +jaraco/classes/__pycache__/properties.cpython-314.pyc,, +jaraco/classes/ancestry.py,sha256=FkU7kyOO-TOMgwR3obcpqB93Ht-f0yxjGnTxcvfBLB0,1787 +jaraco/classes/meta.py,sha256=uz1zmtse_0n7cs2M2hfz8iIqoe2_2vZI-_JiFvQuDwE,2198 +jaraco/classes/properties.py,sha256=f-88KCSBeeCliwxfXOwe7Uqk9_elEmi9ZSwOh6_yBq4,6191 +jaraco/classes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/lib/jaraco.classes-3.4.0.dist-info/WHEEL b/lib/jaraco.classes-3.4.0.dist-info/WHEEL new file mode 100644 index 0000000..bab98d6 --- /dev/null +++ b/lib/jaraco.classes-3.4.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.43.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/jaraco.classes-3.4.0.dist-info/top_level.txt b/lib/jaraco.classes-3.4.0.dist-info/top_level.txt new file mode 100644 index 0000000..f6205a5 --- /dev/null +++ b/lib/jaraco.classes-3.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/lib/jaraco/classes/__init__.py b/lib/jaraco/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/jaraco/classes/__pycache__/__init__.cpython-314.pyc b/lib/jaraco/classes/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53e4f6a4acd5d7c91ef3b97da88aa91df9f6bee7 GIT binary patch literal 161 zcmdPq+klGC}lX5CH>>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%TB)}w?Mxj zvp}~bu_!&YM7K1(Brnl4$4EaXGfBUovLquvPd_WMC^0!-KRG9{xHz>~KR!M)FS8^* kUaz3?7Kcr4eoARhs$CH)&^VBV#UREfW=2NFB4!{90M=(FS^xk5 literal 0 HcmV?d00001 diff --git a/lib/jaraco/classes/__pycache__/ancestry.cpython-314.pyc b/lib/jaraco/classes/__pycache__/ancestry.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8611bc702cc9f8b659fbe0d5ba09b5069a7221b9 GIT binary patch literal 2859 zcmbVOO-vg{6rTOD!HdBpPD&L>nBWTRG}vj1X%!MAAT-cWnpza83S_mc7uaaob!W$E z;AB-fq^YWMsHz^hM(rV|-gEDPluC(K$*HQM^aLU(ZF=c@vvzRvBifGa**7z9X5Rb0 zd2i;;mS&AW`F-G9`>!Y=zha|SK*R_&>-Qfwxq19jE$#<3WW3B3r+)40}23eh%FXlNr&D{m?^f*ccayr6;H zB;@8o9OPypw-j1ZB$HVAKr47yxlKW+kZE#Be)k$7Md<4w zA4!W+K`O`vrJ&~I6uB9Bgn$Q<`83X#OV-8E>e(R5EM8{rO2V;yeo+j;Mzgaw3G5jK z|CbEz<)hryikElan`rxFzP@lvoqA1(4mW6~rL+Y#w_Z+5(CL zAXLa^oWz^I&VP~r@}1R_Uk$9Qud)3w@c+qYF__L;pvp)r0iTdiVTa&BRESd@8A*gc zxq2GONUnV-TDx|Dt|-sQ=z`{6WB~lq%EzyOzj|UQ_vmMfwz)`6I%heayWk7F5nEbC zyQ*4`H#-RXvo@&eF>XSPtJCL< z7Rzdas5#Gba2yFs+mOHX-Ida|%QMvo!5|VF(4lmgj54Pn!u*i#aAY_=^kV18%R0}d zhmViEs5_cITh}SgdGJ1aU?OoD&%oL ziMRjKp1Y-NwD)Z^<=55x?qRGQy8jeA>@Y06XH<-$owcZFkkVTOs_&dl{H*7OMRV5> zP!SiB~eLgZ7A0AXB8Ts`0Yx3r4G8mZZaE;(eP8 zD2ItHHIRw^ykbjmjcMYp#%gvlqXsQ{9kIF|#HL_mlrfJnq(ktSQ3T(C3P->X6s$2j zbW<8$1r!yr5@;7ecm#-T%1UJ5USj{<#L+v6qZ^6Tb#*&RS`S=T?zQZ@5xbx4T6=pV z*>ktC=jXD$Js8hM!A%l4Ph=g*d3i4zo5j5y}(KEqS zP1onk@QA@z1!5XR0Tp{x@W7iJ(^{z<67GfCQ#6-nLEh?IIrx_=|b9xAO5 d)bs=O&`uOyHaq*_M%_6C`TWUgIKE=dzX3H4hOPhr literal 0 HcmV?d00001 diff --git a/lib/jaraco/classes/__pycache__/meta.cpython-314.pyc b/lib/jaraco/classes/__pycache__/meta.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4e63fbf50cdef7e48c82bf0f1f4d1f4678eed1f GIT binary patch literal 3399 zcmb_e-ESL35Z|-!?DNHq(~>p?QgTV!+6LEF+kp5e5hWBFAZa*|E0nm$#oin+as6imvlmY!k<>tB%y4b2V#|2gp+f`QYMwtF&-O2IyM4>QkFXJJCx zFa_o~Un+zcCp)H@3qCU=BTc^KaPwtK8-dB0Q+?U=7fi=>ao|gIS?~>qQ7_BQc_*N) zg+;>{A0Ibokj2eM%^gZ}*{K!D6P9EPW-eba-?)(PURC#^^+nqSo9)OZ;|12FLT+NI z?$&O-xnQ;TmZ}O}D&*4t)DYQ!JGmzvH=y#QXTTloR<;O6+mbF5I(rL|9fy=#qYqu` zD@|6aig+QrP!d=@uRfU@+_+f+GAP_yuRt|_&QYPnO557rA4dw>P zbIOupDOSviTdJj52`g#oB^4w2*BFS0s^=`zFdlFgYIx1TU;BBp;ZmvFpFdBlyizcW z#bPDY9L`zr*X~-q%4cCk!HoD}f>GXNUZ_s{9{qwqd?z5*mK`3Tkt5;5CBED!HR_Ec z++mB9A89Uf&$-_{R&wj}rN#;Y;EmXfN9ebkT!7pAs817elT zwcHI3w#XPd4_XUa7W>fIb5Rl#GA7v|>qU};%mMt!*1bbREvqxB7N;|o$ylUuh?LdN z&gEEZzA(^{Zm5<5ml|WJV>+*e$t5Si>w>GLT+D_^aJ-yyYBdmvSKWXi#IiJYz0if} z?tBaoWe-C$SwAv$+pc?c4!5xXpSK`6;sz!_c$M5plfHqA!TOo2nZh@j!gX`>Q*CYH zO8UU{fqT}2PtIH(7+XtU)BCUL`@h!ruitS+Kk%d8|51GX)Q85m`pCO!hUa5@pqmDC z1LYX1pxm(SWxv*Rk%r)SrRlg4Cu7?Sbrx{9?orPNcG9+MeiaDdbOEw$vmrdtUfT{3 zqpLRhr9R)}Gyoo|l*;kgEYa#RKm>O}_cqy#?a}m&9O)mqtPbB)A%H}z$yK!IExlttOuMjT62aW zu#A&I3zsEu*Mly&2Z%PSj5?gw3JB^e0`E=7WMUV#VgmfZywSn;Ruzbn(41cZaq(le zT~vT&00FqI;DhVL)kKxyN#8#sU;{97o5i_on-!G>1d<$g79Y(n_?-A*B}Bt)UY_EIbt9o6#=`xn)yv#p?GQAaz1czfrLK;AfJ7+HYL ztT|6z|1~pM@|#f8=Bb&LKv#@>3<6CqN;BXf4fv57eH9@Q$kF4+A9_F(#9L^X&7$QM z9P1TpO9ok&9~8?tBuy zbq?Jm+F5l5QFceOl$O?pR@Pb?YT&{_a+b_sdEe2>8saP>zC`^n)lL9fAr-19#*V^7fCWa>1annhr$Ewu zJFiog196pSVa1=K9;ABTTV8+uYyIBO$G%Ek(I3B_*#`yUnX8%HH<{eE!F$#w-kZ9X z+4KH|cP^}{g8A|E2h*QFaXE8zP5q%Kb8Y0%`%~{stslIc9@>nPbYG`L0pcGdyZ5b5 z-c%vh{=l%52B7RTSb*BFJS{ZgH3a2}VT6SVc*fPeMFwF=7)DE|;=@kX6`^Z;!3p&f zvc8S8hd~+*3(&-kxS}YVF;!82PZMSIXJY;m@B3c8>qcTk8NFD$L14J?v@)z5zWBr+ I1O{Q;-{6Bz&j0`b literal 0 HcmV?d00001 diff --git a/lib/jaraco/classes/__pycache__/properties.cpython-314.pyc b/lib/jaraco/classes/__pycache__/properties.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d344ae35f722b877e660b6574536aeb8b3ddd58b GIT binary patch literal 10705 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 e=r6Ks5+2va list[type[Any]]: + """ + return a tuple of all base classes the class c has as a parent. + >>> object in all_bases(list) + True + """ + return c.mro()[1:] + + +def all_classes(c: type[object]) -> list[type[Any]]: + """ + return a tuple of all classes to which c belongs + >>> list in all_classes(list) + True + """ + return c.mro() + + +# borrowed from +# http://code.activestate.com/recipes/576949-find-all-subclasses-of-a-given-class/ + + +def iter_subclasses(cls: type[object]) -> Iterator[type[Any]]: + """ + Generator over all subclasses of a given class, in depth-first order. + + >>> bool in list(iter_subclasses(int)) + True + >>> class A(object): pass + >>> class B(A): pass + >>> class C(A): pass + >>> class D(B,C): pass + >>> class E(D): pass + >>> + >>> for cls in iter_subclasses(A): + ... print(cls.__name__) + B + D + E + C + >>> # get ALL classes currently defined + >>> res = [cls.__name__ for cls in iter_subclasses(object)] + >>> 'type' in res + True + >>> 'tuple' in res + True + >>> len(res) > 100 + True + """ + return unique_everseen(_iter_all_subclasses(cls)) + + +def _iter_all_subclasses(cls: type[object]) -> Iterator[type[Any]]: + try: + subs = cls.__subclasses__() + except TypeError: # fails only when cls is type + subs = cast('type[type]', cls).__subclasses__(cls) + for sub in subs: + yield sub + yield from iter_subclasses(sub) diff --git a/lib/jaraco/classes/meta.py b/lib/jaraco/classes/meta.py new file mode 100644 index 0000000..27d03a0 --- /dev/null +++ b/lib/jaraco/classes/meta.py @@ -0,0 +1,85 @@ +""" +meta.py + +Some useful metaclasses. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +class LeafClassesMeta(type): + """ + A metaclass for classes that keeps track of all of them that + aren't base classes. + + >>> Parent = LeafClassesMeta('MyParentClass', (), {}) + >>> Parent in Parent._leaf_classes + True + >>> Child = LeafClassesMeta('MyChildClass', (Parent,), {}) + >>> Child in Parent._leaf_classes + True + >>> Parent in Parent._leaf_classes + False + + >>> Other = LeafClassesMeta('OtherClass', (), {}) + >>> Parent in Other._leaf_classes + False + >>> len(Other._leaf_classes) + 1 + """ + + _leaf_classes: set[type[Any]] + + def __init__( + cls, + name: str, + bases: tuple[type[object], ...], + attrs: dict[str, object], + ) -> None: + if not hasattr(cls, '_leaf_classes'): + cls._leaf_classes = set() + leaf_classes = getattr(cls, '_leaf_classes') + leaf_classes.add(cls) + # remove any base classes + leaf_classes -= set(bases) + + +class TagRegistered(type): + """ + As classes of this metaclass are created, they keep a registry in the + base class of all classes by a class attribute, indicated by attr_name. + + >>> FooObject = TagRegistered('FooObject', (), dict(tag='foo')) + >>> FooObject._registry['foo'] is FooObject + True + >>> BarObject = TagRegistered('Barobject', (FooObject,), dict(tag='bar')) + >>> FooObject._registry is BarObject._registry + True + >>> len(FooObject._registry) + 2 + + '...' below should be 'jaraco.classes' but for pytest-dev/pytest#3396 + >>> FooObject._registry['bar'] + + """ + + attr_name = 'tag' + + def __init__( + cls, + name: str, + bases: tuple[type[object], ...], + namespace: dict[str, object], + ) -> None: + super(TagRegistered, cls).__init__(name, bases, namespace) + if not hasattr(cls, '_registry'): + cls._registry = {} + meta = cls.__class__ + attr = getattr(cls, meta.attr_name, None) + if attr: + cls._registry[attr] = cls diff --git a/lib/jaraco/classes/properties.py b/lib/jaraco/classes/properties.py new file mode 100644 index 0000000..2447041 --- /dev/null +++ b/lib/jaraco/classes/properties.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload + +_T = TypeVar('_T') +_U = TypeVar('_U') + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Protocol + + from typing_extensions import Self, TypeAlias + + # TODO(coherent-oss/granary#4): Migrate to PEP 695 by 2027-10. + _GetterCallable: TypeAlias = Callable[..., _T] + _GetterClassMethod: TypeAlias = classmethod[Any, [], _T] + + _SetterCallable: TypeAlias = Callable[[type[Any], _T], None] + _SetterClassMethod: TypeAlias = classmethod[Any, [_T], None] + + class _ClassPropertyAttribute(Protocol[_T]): + def __get__(self, obj: object, objtype: type[Any] | None = None) -> _T: ... + + def __set__(self, obj: object, value: _T) -> None: ... + + +class NonDataProperty(Generic[_T, _U]): + """Much like the property builtin, but only implements __get__, + making it a non-data property, and can be subsequently reset. + + See http://users.rcn.com/python/download/Descriptor.htm for more + information. + + >>> class X(object): + ... @NonDataProperty + ... def foo(self): + ... return 3 + >>> x = X() + >>> x.foo + 3 + >>> x.foo = 4 + >>> x.foo + 4 + + '...' below should be 'jaraco.classes' but for pytest-dev/pytest#3396 + >>> X.foo + <....properties.NonDataProperty object at ...> + """ + + def __init__(self, fget: Callable[[_T], _U]) -> None: + assert fget is not None, "fget cannot be none" + assert callable(fget), "fget must be callable" + self.fget = fget + + @overload + def __get__( + self, + obj: None, + objtype: None, + ) -> Self: ... + + @overload + def __get__( + self, + obj: _T, + objtype: type[_T] | None = None, + ) -> _U: ... + + def __get__( + self, + obj: _T | None, + objtype: type[_T] | None = None, + ) -> Self | _U: + if obj is None: + return self + return self.fget(obj) + + +class classproperty(Generic[_T]): + """ + Like @property but applies at the class level. + + + >>> class X(metaclass=classproperty.Meta): + ... val = None + ... @classproperty + ... def foo(cls): + ... return cls.val + ... @foo.setter + ... def foo(cls, val): + ... cls.val = val + >>> X.foo + >>> X.foo = 3 + >>> X.foo + 3 + >>> x = X() + >>> x.foo + 3 + >>> X.foo = 4 + >>> x.foo + 4 + + Setting the property on an instance affects the class. + + >>> x.foo = 5 + >>> x.foo + 5 + >>> X.foo + 5 + >>> vars(x) + {} + >>> X().foo + 5 + + Attempting to set an attribute where no setter was defined + results in an AttributeError: + + >>> class GetOnly(metaclass=classproperty.Meta): + ... @classproperty + ... def foo(cls): + ... return 'bar' + >>> GetOnly.foo = 3 + Traceback (most recent call last): + ... + AttributeError: can't set attribute + + It is also possible to wrap a classmethod or staticmethod in + a classproperty. + + >>> class Static(metaclass=classproperty.Meta): + ... @classproperty + ... @classmethod + ... def foo(cls): + ... return 'foo' + ... @classproperty + ... @staticmethod + ... def bar(): + ... return 'bar' + >>> Static.foo + 'foo' + >>> Static.bar + 'bar' + + *Legacy* + + For compatibility, if the metaclass isn't specified, the + legacy behavior will be invoked. + + >>> class X: + ... val = None + ... @classproperty + ... def foo(cls): + ... return cls.val + ... @foo.setter + ... def foo(cls, val): + ... cls.val = val + >>> X.foo + >>> X.foo = 3 + >>> X.foo + 3 + >>> x = X() + >>> x.foo + 3 + >>> X.foo = 4 + >>> x.foo + 4 + + Note, because the metaclass was not specified, setting + a value on an instance does not have the intended effect. + + >>> x.foo = 5 + >>> x.foo + 5 + >>> X.foo # should be 5 + 4 + >>> vars(x) # should be empty + {'foo': 5} + >>> X().foo # should be 5 + 4 + """ + + fget: _ClassPropertyAttribute[_GetterClassMethod[_T]] + fset: _ClassPropertyAttribute[_SetterClassMethod[_T] | None] + + class Meta(type): + def __setattr__(self, key: str, value: object) -> None: + obj = self.__dict__.get(key, None) + if type(obj) is classproperty: + return obj.__set__(self, value) + return super().__setattr__(key, value) + + def __init__( + self, + fget: _GetterCallable[_T] | _GetterClassMethod[_T], + fset: _SetterCallable[_T] | _SetterClassMethod[_T] | None = None, + ) -> None: + self.fget = self._ensure_method(fget) + self.fset = fset # type: ignore[assignment] # Corrected in the next line. + fset and self.setter(fset) + + def __get__(self, instance: object, owner: type[object] | None = None) -> _T: + return self.fget.__get__(None, owner)() + + def __set__(self, owner: object, value: _T) -> None: + if not self.fset: + raise AttributeError("can't set attribute") + if type(owner) is not classproperty.Meta: + owner = type(owner) + return self.fset.__get__(None, cast('type[object]', owner))(value) + + def setter(self, fset: _SetterCallable[_T] | _SetterClassMethod[_T]) -> Self: + self.fset = self._ensure_method(fset) + return self + + @overload + @classmethod + def _ensure_method( + cls, + fn: _GetterCallable[_T] | _GetterClassMethod[_T], + ) -> _GetterClassMethod[_T]: ... + + @overload + @classmethod + def _ensure_method( + cls, + fn: _SetterCallable[_T] | _SetterClassMethod[_T], + ) -> _SetterClassMethod[_T]: ... + + @classmethod + def _ensure_method( + cls, + fn: _GetterCallable[_T] + | _GetterClassMethod[_T] + | _SetterCallable[_T] + | _SetterClassMethod[_T], + ) -> _GetterClassMethod[_T] | _SetterClassMethod[_T]: + """ + Ensure fn is a classmethod or staticmethod. + """ + needs_method = not isinstance(fn, (classmethod, staticmethod)) + return classmethod(fn) if needs_method else fn # type: ignore[arg-type,return-value] diff --git a/lib/jaraco/classes/py.typed b/lib/jaraco/classes/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/lib/jaraco/context/__init__.py b/lib/jaraco/context/__init__.py new file mode 100644 index 0000000..41ad609 --- /dev/null +++ b/lib/jaraco/context/__init__.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import contextlib +import errno +import functools +import operator +import os +import platform +import shutil +import stat +import subprocess +import sys +import tempfile +import urllib.request +from collections.abc import Iterator + +if sys.version_info < (3, 12): + from backports import tarfile +else: + import tarfile + + +@contextlib.contextmanager +def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]: + """ + >>> tmp_path = getfixture('tmp_path') + >>> with pushd(tmp_path): + ... assert os.getcwd() == os.fspath(tmp_path) + >>> assert os.getcwd() != os.fspath(tmp_path) + """ + + orig = os.getcwd() + os.chdir(dir) + try: + yield dir + finally: + os.chdir(orig) + + +@contextlib.contextmanager +def tarball( + url, target_dir: str | os.PathLike | None = None +) -> Iterator[str | os.PathLike]: + """ + Get a URL to a tarball, download, extract, yield, then clean up. + + Assumes everything in the tarball is prefixed with a common + directory. That common path is stripped and the contents + are extracted to ``target_dir``, similar to passing + ``-C {target} --strip-components 1`` to the ``tar`` command. + + Uses the streaming protocol to extract the contents from a + stream in a single pass without loading the whole file into + memory. + + >>> import urllib.request + >>> url = getfixture('tarfile_served') + >>> target = getfixture('tmp_path') / 'out' + >>> tb = tarball(url, target_dir=target) + >>> import pathlib + >>> with tb as extracted: + ... contents = pathlib.Path(extracted, 'contents.txt').read_text(encoding='utf-8') + >>> assert not os.path.exists(extracted) + + If the target is not specified, contents are extracted to a + directory relative to the current working directory named after + the name of the file as extracted from the URL. + + >>> target = getfixture('tmp_path') + >>> with pushd(target), tarball(url): + ... target.joinpath('served').is_dir() + True + """ + if target_dir is None: + target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') + os.mkdir(target_dir) + try: + req = urllib.request.urlopen(url) + with tarfile.open(fileobj=req, mode='r|*') as tf: + tf.extractall(path=target_dir, filter=_default_filter) + yield target_dir + finally: + shutil.rmtree(target_dir) + + +def _compose_tarfile_filters(*filters): + def compose_two(f1, f2): + return lambda member, path: f1(f2(member, path), path) + + return functools.reduce(compose_two, filters, lambda member, path: member) + + +def strip_first_component( + member: tarfile.TarInfo, + path, +) -> tarfile.TarInfo: + _, member.name = member.name.split('/', 1) + return member + + +_default_filter = _compose_tarfile_filters(tarfile.data_filter, strip_first_component) + + +def _compose(*cmgrs): + """ + Compose any number of dependent context managers into a single one. + + The last, innermost context manager may take arbitrary arguments, but + each successive context manager should accept the result from the + previous as a single parameter. + + Like :func:`jaraco.functools.compose`, behavior works from right to + left, so the context manager should be indicated from outermost to + innermost. + + Example, to create a context manager to change to a temporary + directory: + + >>> temp_dir_as_cwd = _compose(pushd, temp_dir) + >>> with temp_dir_as_cwd() as dir: + ... assert os.path.samefile(os.getcwd(), dir) + """ + + def compose_two(inner, outer): + def composed(*args, **kwargs): + with inner(*args, **kwargs) as saved, outer(saved) as res: + yield res + + return contextlib.contextmanager(composed) + + return functools.reduce(compose_two, reversed(cmgrs)) + + +tarball_cwd = _compose(pushd, tarball) +""" +A tarball context with the current working directory pointing to the contents. +""" + + +def remove_readonly(func, path, exc_info): + """ + Add support for removing read-only files on Windows. + """ + _, exc, _ = exc_info + if func in (os.rmdir, os.remove, os.unlink) and exc.errno == errno.EACCES: + # change the file to be readable,writable,executable: 0777 + os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + # retry + func(path) + else: + raise + + +def robust_remover(): + return ( + functools.partial(shutil.rmtree, onerror=remove_readonly) + if platform.system() == 'Windows' + else shutil.rmtree + ) + + +@contextlib.contextmanager +def temp_dir(remover=shutil.rmtree): + """ + Create a temporary directory context. Pass a custom remover + to override the removal behavior. + + >>> import pathlib + >>> with temp_dir() as the_dir: + ... assert os.path.isdir(the_dir) + >>> assert not os.path.exists(the_dir) + """ + temp_dir = tempfile.mkdtemp() + try: + yield temp_dir + finally: + remover(temp_dir) + + +robust_temp_dir = functools.partial(temp_dir, remover=robust_remover()) + + +@contextlib.contextmanager +def repo_context( + url, branch: str | None = None, quiet: bool = True, dest_ctx=robust_temp_dir +): + """ + Check out the repo indicated by url. + + If dest_ctx is supplied, it should be a context manager + to yield the target directory for the check out. + + >>> getfixture('ensure_git') + >>> getfixture('needs_internet') + >>> repo = repo_context('https://github.com/jaraco/jaraco.context') + >>> with repo as dest: + ... listing = os.listdir(dest) + >>> 'README.rst' in listing + True + """ + exe = 'git' if 'git' in url else 'hg' + with dest_ctx() as repo_dir: + cmd = [exe, 'clone', url, repo_dir] + cmd.extend(['--branch', branch] * bool(branch)) + stream = subprocess.DEVNULL if quiet else None + subprocess.check_call(cmd, stdout=stream, stderr=stream) + yield repo_dir + + +class ExceptionTrap: + """ + A context manager that will catch certain exceptions and provide an + indication they occurred. + + >>> with ExceptionTrap() as trap: + ... raise Exception() + >>> bool(trap) + True + + >>> with ExceptionTrap() as trap: + ... pass + >>> bool(trap) + False + + >>> with ExceptionTrap(ValueError) as trap: + ... raise ValueError("1 + 1 is not 3") + >>> bool(trap) + True + >>> trap.value + ValueError('1 + 1 is not 3') + >>> trap.tb + + + >>> with ExceptionTrap(ValueError) as trap: + ... raise Exception() + Traceback (most recent call last): + ... + Exception + + >>> bool(trap) + False + """ + + exc_info = None, None, None + + def __init__(self, exceptions=(Exception,)): + self.exceptions = exceptions + + def __enter__(self): + return self + + @property + def type(self): + return self.exc_info[0] + + @property + def value(self): + return self.exc_info[1] + + @property + def tb(self): + return self.exc_info[2] + + def __exit__(self, *exc_info): + type = exc_info[0] + matches = type and issubclass(type, self.exceptions) + if matches: + self.exc_info = exc_info + return matches + + def __bool__(self): + return bool(self.type) + + def raises(self, func, *, _test=bool): + """ + Wrap func and replace the result with the truth + value of the trap (True if an exception occurred). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> raises = ExceptionTrap(ValueError).raises + + Now decorate a function that always fails. + + >>> @raises + ... def fail(): + ... raise ValueError('failed') + >>> fail() + True + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with ExceptionTrap(self.exceptions) as trap: + func(*args, **kwargs) + return _test(trap) + + return wrapper + + def passes(self, func): + """ + Wrap func and replace the result with the truth + value of the trap (True if no exception). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> passes = ExceptionTrap(ValueError).passes + + Now decorate a function that always fails. + + >>> @passes + ... def fail(): + ... raise ValueError('failed') + + >>> fail() + False + """ + return self.raises(func, _test=operator.not_) + + +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ + + +class on_interrupt(contextlib.ContextDecorator): + """ + Replace a KeyboardInterrupt with SystemExit(1). + + Useful in conjunction with console entry point functions. + + >>> def do_interrupt(): + ... raise KeyboardInterrupt() + >>> on_interrupt('error')(do_interrupt)() + Traceback (most recent call last): + ... + SystemExit: 1 + >>> on_interrupt('error', code=255)(do_interrupt)() + Traceback (most recent call last): + ... + SystemExit: 255 + >>> on_interrupt('suppress')(do_interrupt)() + >>> with __import__('pytest').raises(KeyboardInterrupt): + ... on_interrupt('ignore')(do_interrupt)() + """ + + def __init__(self, action='error', /, code=1): + self.action = action + self.code = code + + def __enter__(self): + return self + + def __exit__(self, exctype, excinst, exctb): + if exctype is not KeyboardInterrupt or self.action == 'ignore': + return + elif self.action == 'error': + raise SystemExit(self.code) from excinst + return self.action == 'suppress' diff --git a/lib/jaraco/context/__pycache__/__init__.cpython-314.pyc b/lib/jaraco/context/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f51726be08d1995a60c83788fee772d9fbc8646 GIT binary patch literal 15456 zcmb_jdvFxTnV*?`&CW`@dP@j_1_`8HBc$-KjWGs`hm9PNZ|AXXXEgbhddU2B1D)2Ke3LH1eJ3>X3SEHO&h zv(yOU>FNsYC^O0$xxPS8lY>&>8LeMm!Ep(Z;~EGXcUQ%ZN~5xo+hyT^a-Yx>F0^+_ zQBN%w_11E!vUPTqQGJ`1i=#fY6{&5C&$q6L`fE9(HY(v;fW8@ZeYMNDzRI38&KQMf zU)?&a9VMeaT7rIy`xc{Zbze=szF{#JEyb55(IE1r3**u!_H44?6z6OCXb2b-aFhGo zJzI)fSY?bT_bpw+$ES}D_&NNPvRbwiV%ku!RGk75k%OdeECucjI~E&bVIXVj(|fS`aYn@Y5TRjdGuGrF`vm%Em+TG<&XikiE~~JI5A-3Y0wf zHRCZejzSaHi0=ESo~0g}NSt?i7KkXz7qxU3x3S90u(a#!MQ`L54?IAx13G~&bYZX-XuWLV*4+`8 zDP(lhYk@UA@gdM!M9{$5IMbccl7l+!LyN4HQjLdUs8yp}i=X}gvX8NCH!HZ3;Hmpg z-uIpRN4H&dd%wNoxgE!MKfh~QaCvH{g5@XuzZa!b;z{vz)r-MTUl6R~cY~knivi)e zw$2v*r|!-G_wzcGE(Gd2n_U;0T`2!0hez`PwkcUe9-}i~a4;>8q9xG-8TL#;*Y zz5Kv~bnJ_0vbYC2Q0k#mGA9*iyNBS;NyI=L*0|tzcj5c2b3~gyC*d3w(f49*9~0Qr zsE1Z)PU@j)&q)Pbv%pR0jID4$PHOBj#dr0b*Dz_0&71!gKbVEiSNX3xs2Rx#+SfJqj|5kTeD`}b3OLY;xbE8&U3 zG0n$y%#l8!o2m@cG_zX9$iy=#>SV9dEG*>#Ei)`DlGTG&MFC#v!Ia8yVr!Af88WRi z!6WF0hcfs~LLBXlj5Mqc)AC4+%O-&;cydllL2R~b>eq9sZm=(?n#=4hRUif~en?H& zEZ>^^+?KYqXpP7naw`yOWgv}yw6p0U4EWf2CcBBPJzy0rIbvf)n+CL4U$)Btpnxna z@=+57(c#@Pl% z*iO+grfn+{=@zv11DRx+Rw&%+&_;Vwr(F$4q&-?rmAa4m+R?Us@Tkchb+;RXM~}L+ zXYPs!rjM}B^dB(Y!dK#60Hr@WG3l9M@EXo^fsOoD=zsKU9GNtW$#OS zC#zRntX^>@K2d$wWc8Yh)oae*_g?=*b?0Pt*Tw3t|7;nL?wP25{J7-k98X?$*G&sv zPt(<4)v5G}^ki`P#o+QYyC;H?HE-twCTCnbtSoeD|c&hYd>FEQ%5|@4AM>YEV-%XM8hpg%=;ZZ^=Xrl!3CbUEb*TTKF0eK1|DairoixqR_5pN#+Igf@R#^vM686InoX0f-pk#SbwNdiCzYLvTh zh)o)2-#?H`LwILWQ06r?k&6>UAPxampld{?Y|Ojt6Ige<;WSd|d#U*nHo^^o_iVoD z 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 knMWp<{9 literal 0 HcmV?d00001 diff --git a/lib/jaraco/context/py.typed b/lib/jaraco/context/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/lib/jaraco/functools/__init__.py b/lib/jaraco/functools/__init__.py new file mode 100644 index 0000000..df32e2e --- /dev/null +++ b/lib/jaraco/functools/__init__.py @@ -0,0 +1,722 @@ +from __future__ import annotations + +import collections.abc +import functools +import inspect +import itertools +import operator +import time +import types +import warnings +from typing import Callable, TypeVar + +import more_itertools + + +def compose(*funcs): + """ + Compose any number of unary functions into a single unary function. + + Comparable to + `function composition `_ + in mathematics: + + ``h = g ∘ f`` implies ``h(x) = g(f(x))``. + + In Python, ``h = compose(g, f)``. + + >>> import textwrap + >>> expected = str.strip(textwrap.dedent(compose.__doc__)) + >>> strip_and_dedent = compose(str.strip, textwrap.dedent) + >>> strip_and_dedent(compose.__doc__) == expected + True + + Compose also allows the innermost function to take arbitrary arguments. + + >>> round_three = lambda x: round(x, ndigits=3) + >>> f = compose(round_three, int.__truediv__) + >>> [f(3*x, x+1) for x in range(1,10)] + [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7] + """ + + def compose_two(f1, f2): + return lambda *args, **kwargs: f1(f2(*args, **kwargs)) + + return functools.reduce(compose_two, funcs) + + +def once(func): + """ + Decorate func so it's only ever called the first time. + + This decorator can ensure that an expensive or non-idempotent function + will not be expensive on subsequent calls and is idempotent. + + >>> add_three = once(lambda a: a+3) + >>> add_three(3) + 6 + >>> add_three(9) + 6 + >>> add_three('12') + 6 + + To reset the stored value, simply clear the property ``saved_result``. + + >>> del add_three.saved_result + >>> add_three(9) + 12 + >>> add_three(8) + 12 + + Or invoke 'reset()' on it. + + >>> add_three.reset() + >>> add_three(-3) + 0 + >>> add_three(0) + 0 + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not hasattr(wrapper, 'saved_result'): + wrapper.saved_result = func(*args, **kwargs) + return wrapper.saved_result + + wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result') + return wrapper + + +def method_cache(method, cache_wrapper=functools.lru_cache()): + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + + def wrapper(self, *args, **kwargs): + # it's the first call, replace the method with a cached, bound method + bound_method = types.MethodType(method, self) + cached_method = cache_wrapper(bound_method) + setattr(self, method.__name__, cached_method) + return cached_method(*args, **kwargs) + + # Support cache clear even before cache has been created. + wrapper.cache_clear = lambda: None + + return _special_method_cache(method, cache_wrapper) or wrapper + + +def _special_method_cache(method, cache_wrapper): + """ + Because Python treats special methods differently, it's not + possible to use instance attributes to implement the cached + methods. + + Instead, install the wrapper method under a different name + and return a simple proxy to that wrapper. + + https://github.com/jaraco/jaraco.functools/issues/5 + """ + name = method.__name__ + special_names = '__getattr__', '__getitem__' + + if name not in special_names: + return None + + wrapper_name = '__cached' + name + + def proxy(self, /, *args, **kwargs): + if wrapper_name not in vars(self): + bound = types.MethodType(method, self) + cache = cache_wrapper(bound) + setattr(self, wrapper_name, cache) + else: + cache = getattr(self, wrapper_name) + return cache(*args, **kwargs) + + return proxy + + +def apply(transform): + """ + Decorate a function with a transform function that is + invoked on results returned from the decorated function. + + >>> @apply(reversed) + ... def get_numbers(start): + ... "doc for get_numbers" + ... return range(start, start+3) + >>> list(get_numbers(4)) + [6, 5, 4] + >>> get_numbers.__doc__ + 'doc for get_numbers' + """ + + def wrap(func): + return functools.wraps(func)(compose(transform, func)) + + return wrap + + +def result_invoke(action): + r""" + Decorate a function with an action function that is + invoked on the results returned from the decorated + function (for its side effect), then return the original + result. + + >>> @result_invoke(print) + ... def add_two(a, b): + ... return a + b + >>> x = add_two(2, 3) + 5 + >>> x + 5 + """ + + def wrap(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + action(result) + return result + + return wrapper + + return wrap + + +def invoke(f, /, *args, **kwargs): + """ + Call a function for its side effect after initialization. + + The benefit of using the decorator instead of simply invoking a function + after defining it is that it makes explicit the author's intent for the + function to be called immediately. Whereas if one simply calls the + function immediately, it's less obvious if that was intentional or + incidental. It also avoids repeating the name - the two actions, defining + the function and calling it immediately are modeled separately, but linked + by the decorator construct. + + The benefit of having a function construct (opposed to just invoking some + behavior inline) is to serve as a scope in which the behavior occurs. It + avoids polluting the global namespace with local variables, provides an + anchor on which to attach documentation (docstring), keeps the behavior + logically separated (instead of conceptually separated or not separated at + all), and provides potential to re-use the behavior for testing or other + purposes. + + This function is named as a pithy way to communicate, "call this function + primarily for its side effect", or "while defining this function, also + take it aside and call it". It exists because there's no Python construct + for "define and call" (nor should there be, as decorators serve this need + just fine). The behavior happens immediately and synchronously. + + >>> @invoke + ... def func(): print("called") + called + >>> func() + called + + Use functools.partial to pass parameters to the initial call + + >>> @functools.partial(invoke, name='bingo') + ... def func(name): print('called with', name) + called with bingo + """ + f(*args, **kwargs) + return f + + +_T = TypeVar('_T') + + +def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]: + """ + Wrap the function to always return the first parameter. + + >>> passthrough(print)('3') + 3 + '3' + """ + + @functools.wraps(func) + def wrapper(first: _T, *args, **kwargs) -> _T: + func(first, *args, **kwargs) + return first + + return wrapper + + +class Throttler: + """Rate-limit a function (or other callable).""" + + def __init__(self, func, max_rate=float('Inf')): + if isinstance(func, Throttler): + func = func.func + self.func = func + self.max_rate = max_rate + self.reset() + + def reset(self): + self.last_called = 0 + + def __call__(self, *args, **kwargs): + self._wait() + return self.func(*args, **kwargs) + + def _wait(self): + """Ensure at least 1/max_rate seconds from last call.""" + elapsed = time.time() - self.last_called + must_wait = 1 / self.max_rate - elapsed + time.sleep(max(0, must_wait)) + self.last_called = time.time() + + def __get__(self, obj, owner=None): + return first_invoke(self._wait, functools.partial(self.func, obj)) + + +def first_invoke(func1, func2): + """ + Return a function that when invoked will invoke func1 without + any parameters (for its side effect) and then invoke func2 + with whatever parameters were passed, returning its result. + """ + + def wrapper(*args, **kwargs): + func1() + return func2(*args, **kwargs) + + return wrapper + + +method_caller = first_invoke( + lambda: warnings.warn( + '`jaraco.functools.method_caller` is deprecated, ' + 'use `operator.methodcaller` instead', + DeprecationWarning, + stacklevel=3, + ), + operator.methodcaller, +) + + +def retry_call(func, cleanup=lambda: None, retries=0, trap=()): + """ + Given a callable func, trap the indicated exceptions + for up to 'retries' times, invoking cleanup on the + exception. On the final attempt, allow any exceptions + to propagate. + """ + attempts = itertools.count() if retries == float('inf') else range(retries) + for _ in attempts: + try: + return func() + except trap: + cleanup() + + return func() + + +def retry(*r_args, **r_kwargs): + """ + Decorator wrapper for retry_call. Accepts arguments to retry_call + except func and then returns a decorator for the decorated function. + + Ex: + + >>> @retry(retries=3) + ... def my_func(a, b): + ... "this is my funk" + ... print(a, b) + >>> my_func.__doc__ + 'this is my funk' + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*f_args, **f_kwargs): + bound = functools.partial(func, *f_args, **f_kwargs) + return retry_call(bound, *r_args, **r_kwargs) + + return wrapper + + return decorate + + +def print_yielded(func): + """ + Convert a generator into a function that prints all yielded elements. + + >>> @print_yielded + ... def x(): + ... yield 3; yield None + >>> x() + 3 + None + """ + print_all = functools.partial(map, print) + print_results = compose(more_itertools.consume, print_all, func) + return functools.wraps(func)(print_results) + + +def pass_none(func): + """ + Wrap func so it's not called if its first param is None. + + >>> print_text = pass_none(print) + >>> print_text('text') + text + >>> print_text(None) + """ + + @functools.wraps(func) + def wrapper(param, /, *args, **kwargs): + if param is not None: + return func(param, *args, **kwargs) + return None + + return wrapper + + +def none_as(value, replacement=None): + """ + >>> none_as(None, 'foo') + 'foo' + >>> none_as('bar', 'foo') + 'bar' + """ + return replacement if value is None else value + + +def assign_params(func, namespace): + """ + Assign parameters from namespace where func solicits. + + >>> def func(x, y=3): + ... print(x, y) + >>> assigned = assign_params(func, dict(x=2, z=4)) + >>> assigned() + 2 3 + + The usual errors are raised if a function doesn't receive + its required parameters: + + >>> assigned = assign_params(func, dict(y=3, z=4)) + >>> assigned() + Traceback (most recent call last): + TypeError: func() ...argument... + + It even works on methods: + + >>> class Handler: + ... def meth(self, arg): + ... print(arg) + >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))() + crystal + """ + sig = inspect.signature(func) + params = sig.parameters.keys() + call_ns = {k: namespace[k] for k in params if k in namespace} + return functools.partial(func, **call_ns) + + +def save_method_args(method): + """ + Wrap a method such that when it is called, the args and kwargs are + saved on the method. + + >>> class MyClass: + ... @save_method_args + ... def method(self, a, b): + ... print(a, b) + >>> my_ob = MyClass() + >>> my_ob.method(1, 2) + 1 2 + >>> my_ob._saved_method.args + (1, 2) + >>> my_ob._saved_method.kwargs + {} + >>> my_ob.method(a=3, b='foo') + 3 foo + >>> my_ob._saved_method.args + () + >>> my_ob._saved_method.kwargs == dict(a=3, b='foo') + True + + The arguments are stored on the instance, allowing for + different instance to save different args. + + >>> your_ob = MyClass() + >>> your_ob.method({str('x'): 3}, b=[4]) + {'x': 3} [4] + >>> your_ob._saved_method.args + ({'x': 3},) + >>> my_ob._saved_method.args + () + """ + args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') # noqa: PYI024 # Internal; stubs used for typing + + @functools.wraps(method) + def wrapper(self, /, *args, **kwargs): + attr_name = '_saved_' + method.__name__ + attr = args_and_kwargs(args, kwargs) + setattr(self, attr_name, attr) + return method(self, *args, **kwargs) + + return wrapper + + +def except_(*exceptions, replace=None, use=None): + """ + Replace the indicated exceptions, if raised, with the indicated + literal replacement or evaluated expression (if present). + + >>> safe_int = except_(ValueError)(int) + >>> safe_int('five') + >>> safe_int('5') + 5 + + Specify a literal replacement with ``replace``. + + >>> safe_int_r = except_(ValueError, replace=0)(int) + >>> safe_int_r('five') + 0 + + Provide an expression to ``use`` to pass through particular parameters. + + >>> safe_int_pt = except_(ValueError, use='args[0]')(int) + >>> safe_int_pt('five') + 'five' + + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except exceptions: + try: + return eval(use) + except TypeError: + return replace + + return wrapper + + return decorate + + +def identity(x): + """ + Return the argument. + + >>> o = object() + >>> identity(o) is o + True + """ + return x + + +def bypass_when(check, *, _op=identity): + """ + Decorate a function to return its parameter when ``check``. + + >>> bypassed = [] # False + + >>> @bypass_when(bypassed) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> bypassed[:] = [object()] # True + >>> double(2) + 2 + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(param, /): + return param if _op(check) else func(param) + + return wrapper + + return decorate + + +def bypass_unless(check): + """ + Decorate a function to return its parameter unless ``check``. + + >>> enabled = [object()] # True + + >>> @bypass_unless(enabled) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> del enabled[:] # False + >>> double(2) + 2 + """ + return bypass_when(check, _op=operator.not_) + + +@functools.singledispatch +def _splat_inner(args, func): + """Splat args to func.""" + return func(*args) + + +@_splat_inner.register +def _(args: collections.abc.Mapping, func): + """Splat kargs to func as kwargs.""" + return func(**args) + + +def splat(func): + """ + Wrap func to expect its parameters to be passed positionally in a tuple. + + Has a similar effect to that of ``itertools.starmap`` over + simple ``map``. + + >>> pairs = [(-1, 1), (0, 2)] + >>> more_itertools.consume(itertools.starmap(print, pairs)) + -1 1 + 0 2 + >>> more_itertools.consume(map(splat(print), pairs)) + -1 1 + 0 2 + + The approach generalizes to other iterators that don't have a "star" + equivalent, such as a "starfilter". + + >>> list(filter(splat(operator.add), pairs)) + [(0, 2)] + + Splat also accepts a mapping argument. + + >>> def is_nice(msg, code): + ... return "smile" in msg or code == 0 + >>> msgs = [ + ... dict(msg='smile!', code=20), + ... dict(msg='error :(', code=1), + ... dict(msg='unknown', code=0), + ... ] + >>> for msg in filter(splat(is_nice), msgs): + ... print(msg) + {'msg': 'smile!', 'code': 20} + {'msg': 'unknown', 'code': 0} + """ + return functools.wraps(func)(functools.partial(_splat_inner, func=func)) + + +_T = TypeVar('_T') + + +def chainable(method: Callable[[_T, ...], None]) -> Callable[[_T, ...], _T]: + """ + Wrap an instance method to always return self. + + + >>> class Dingus: + ... @chainable + ... def set_attr(self, name, val): + ... setattr(self, name, val) + >>> d = Dingus().set_attr('a', 'eh!') + >>> d.a + 'eh!' + >>> d2 = Dingus().set_attr('a', 'eh!').set_attr('b', 'bee!') + >>> d2.a + d2.b + 'eh!bee!' + + Enforces that the return value is null. + + >>> class BorkedDingus: + ... @chainable + ... def set_attr(self, name, val): + ... setattr(self, name, val) + ... return len(name) + >>> BorkedDingus().set_attr('a', 'eh!') + Traceback (most recent call last): + ... + AssertionError + """ + + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + assert method(self, *args, **kwargs) is None + return self + + return wrapper + + +def noop(*args, **kwargs): + """ + A no-operation function that does nothing. + + >>> noop(1, 2, three=3) + """ diff --git a/lib/jaraco/functools/__init__.pyi b/lib/jaraco/functools/__init__.pyi new file mode 100644 index 0000000..6f834bf --- /dev/null +++ b/lib/jaraco/functools/__init__.pyi @@ -0,0 +1,123 @@ +from collections.abc import Callable, Hashable, Iterator +from functools import partial +from operator import methodcaller +from typing import ( + Any, + Generic, + Protocol, + TypeVar, + overload, +) + +from typing_extensions import Concatenate, ParamSpec, TypeVarTuple, Unpack + +_P = ParamSpec('_P') +_R = TypeVar('_R') +_T = TypeVar('_T') +_Ts = TypeVarTuple('_Ts') +_R1 = TypeVar('_R1') +_R2 = TypeVar('_R2') +_V = TypeVar('_V') +_S = TypeVar('_S') +_R_co = TypeVar('_R_co', covariant=True) + +class _OnceCallable(Protocol[_P, _R]): + saved_result: _R + reset: Callable[[], None] + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + +class _ProxyMethodCacheWrapper(Protocol[_R_co]): + cache_clear: Callable[[], None] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +class _MethodCacheWrapper(Protocol[_R_co]): + def cache_clear(self) -> None: ... + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +# `compose()` overloads below will cover most use cases. + +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[_P, _R], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R1], _R], + __func3: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R2], _R], + __func3: Callable[[_R1], _R2], + __func4: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +def once(func: Callable[_P, _R]) -> _OnceCallable[_P, _R]: ... +def method_cache( + method: Callable[..., _R], + cache_wrapper: Callable[[Callable[..., _R]], _MethodCacheWrapper[_R]] = ..., +) -> _MethodCacheWrapper[_R] | _ProxyMethodCacheWrapper[_R]: ... +def apply( + transform: Callable[[_R], _T], +) -> Callable[[Callable[_P, _R]], Callable[_P, _T]]: ... +def result_invoke( + action: Callable[[_R], Any], +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +def invoke( + f: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... + +class Throttler(Generic[_R]): + last_called: float + func: Callable[..., _R] + max_rate: float + def __init__( + self, func: Callable[..., _R] | Throttler[_R], max_rate: float = ... + ) -> None: ... + def reset(self) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> _R: ... + def __get__(self, obj: Any, owner: type[Any] | None = ...) -> Callable[..., _R]: ... + +def first_invoke( + func1: Callable[..., Any], func2: Callable[_P, _R] +) -> Callable[_P, _R]: ... + +method_caller: Callable[..., methodcaller] + +def retry_call( + func: Callable[..., _R], + cleanup: Callable[..., None] = ..., + retries: float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> _R: ... +def retry( + cleanup: Callable[..., None] = ..., + retries: float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... +def print_yielded(func: Callable[_P, Iterator[Any]]) -> Callable[_P, None]: ... +def pass_none( + func: Callable[Concatenate[_T, _P], _R], +) -> Callable[Concatenate[_T, _P], _R]: ... +def assign_params( + func: Callable[..., _R], namespace: dict[str, Any] +) -> partial[_R]: ... +def save_method_args( + method: Callable[Concatenate[_S, _P], _R], +) -> Callable[Concatenate[_S, _P], _R]: ... +def except_( + *exceptions: type[BaseException], replace: Any = ..., use: Any = ... +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: ... +def identity(x: _T) -> _T: ... +def bypass_when( + check: _V, *, _op: Callable[[_V], Any] = ... +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... +def bypass_unless( + check: Any, +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... +def splat(func: Callable[[Unpack[_Ts]], _R]) -> Callable[[tuple[Unpack[_Ts]]], _R]: ... diff --git a/lib/jaraco/functools/__pycache__/__init__.cpython-314.pyc b/lib/jaraco/functools/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8a9443da99c81db07e9014ed9592cb9e761083e GIT binary patch literal 24901 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 literal 0 HcmV?d00001 diff --git a/lib/jaraco/functools/py.typed b/lib/jaraco/functools/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/lib/jaraco_context-6.1.0.dist-info/INSTALLER b/lib/jaraco_context-6.1.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/jaraco_context-6.1.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/jaraco_context-6.1.0.dist-info/METADATA b/lib/jaraco_context-6.1.0.dist-info/METADATA new file mode 100644 index 0000000..8fb5e53 --- /dev/null +++ b/lib/jaraco_context-6.1.0.dist-info/METADATA @@ -0,0 +1,82 @@ +Metadata-Version: 2.4 +Name: jaraco.context +Version: 6.1.0 +Summary: Useful decorators and context managers +Author-email: "Jason R. Coombs" +License-Expression: MIT +Project-URL: Source, https://github.com/jaraco/jaraco.context +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: backports.tarfile; python_version < "3.12" +Provides-Extra: test +Requires-Dist: pytest!=8.1.*,>=6; extra == "test" +Requires-Dist: jaraco.test>=5.6.0; extra == "test" +Requires-Dist: portend; extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx>=3.5; extra == "doc" +Requires-Dist: jaraco.packaging>=9.3; extra == "doc" +Requires-Dist: rst.linker>=1.9; extra == "doc" +Requires-Dist: furo; extra == "doc" +Requires-Dist: sphinx-lint; extra == "doc" +Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" +Provides-Extra: check +Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" +Provides-Extra: cover +Requires-Dist: pytest-cov; extra == "cover" +Provides-Extra: enabler +Requires-Dist: pytest-enabler>=3.4; extra == "enabler" +Provides-Extra: type +Requires-Dist: pytest-mypy>=1.0.1; extra == "type" +Requires-Dist: mypy<1.19; platform_python_implementation == "PyPy" and extra == "type" +Dynamic: license-file + +.. image:: https://img.shields.io/pypi/v/jaraco.context.svg + :target: https://pypi.org/project/jaraco.context + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg + +.. image:: https://github.com/jaraco/jaraco.context/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest + :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2025-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/jaraco.context + :target: https://tidelift.com/subscription/pkg/pypi-jaraco.context?utm_source=pypi-jaraco.context&utm_medium=readme + + +Highlights +========== + +See the docs linked from the badge above for the full details, but here are some features that may be of interest. + +- ``ExceptionTrap`` provides a general-purpose wrapper for trapping exceptions and then acting on the outcome. Includes ``passes`` and ``raises`` decorators to replace the result of a wrapped function by a boolean indicating the outcome of the exception trap. See `this keyring commit `_ for an example of it in production. +- ``suppress`` simply enables ``contextlib.suppress`` as a decorator. +- ``on_interrupt`` is a decorator used by CLI entry points to affect the handling of a ``KeyboardInterrupt``. Inspired by `Lucretiel/autocommand#18 `_. +- ``pushd`` is similar to pytest's ``monkeypatch.chdir`` or path's `default context `_, changes the current working directory for the duration of the context. +- ``tarball`` will download a tarball, extract it, change directory, yield, then clean up after. Convenient when working with web assets. +- ``null`` is there for those times when one code branch needs a context and the other doesn't; this null context provides symmetry across those branches. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/lib/jaraco_context-6.1.0.dist-info/RECORD b/lib/jaraco_context-6.1.0.dist-info/RECORD new file mode 100644 index 0000000..c82da66 --- /dev/null +++ b/lib/jaraco_context-6.1.0.dist-info/RECORD @@ -0,0 +1,9 @@ +jaraco/context/__init__.py,sha256=br1ydYGo1Xr_Pu1anuEdd-QrjUiz_EY5L_5E4C03L4w,9809 +jaraco/context/__pycache__/__init__.cpython-314.pyc,, +jaraco/context/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jaraco_context-6.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco_context-6.1.0.dist-info/METADATA,sha256=BDXr_FIFXFqZdO0gwXG2RUOD6vnbsVCIFLp62XxZ1xI,4270 +jaraco_context-6.1.0.dist-info/RECORD,, +jaraco_context-6.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +jaraco_context-6.1.0.dist-info/licenses/LICENSE,sha256=l1WhhRlmbl8PTK49qtPXASvK5IpgCzEjfXXp_hNOZoM,1076 +jaraco_context-6.1.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 diff --git a/lib/jaraco_context-6.1.0.dist-info/WHEEL b/lib/jaraco_context-6.1.0.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/lib/jaraco_context-6.1.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/jaraco_context-6.1.0.dist-info/licenses/LICENSE b/lib/jaraco_context-6.1.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..c891f41 --- /dev/null +++ b/lib/jaraco_context-6.1.0.dist-info/licenses/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/jaraco_context-6.1.0.dist-info/top_level.txt b/lib/jaraco_context-6.1.0.dist-info/top_level.txt new file mode 100644 index 0000000..f6205a5 --- /dev/null +++ b/lib/jaraco_context-6.1.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/lib/jaraco_functools-4.4.0.dist-info/INSTALLER b/lib/jaraco_functools-4.4.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/jaraco_functools-4.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/jaraco_functools-4.4.0.dist-info/METADATA b/lib/jaraco_functools-4.4.0.dist-info/METADATA new file mode 100644 index 0000000..f2150dd --- /dev/null +++ b/lib/jaraco_functools-4.4.0.dist-info/METADATA @@ -0,0 +1,69 @@ +Metadata-Version: 2.4 +Name: jaraco.functools +Version: 4.4.0 +Summary: Functools like those found in stdlib +Author-email: "Jason R. Coombs" +License-Expression: MIT +Project-URL: Source, https://github.com/jaraco/jaraco.functools +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: more_itertools +Provides-Extra: test +Requires-Dist: pytest!=8.1.*,>=6; extra == "test" +Requires-Dist: jaraco.classes; extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx>=3.5; extra == "doc" +Requires-Dist: jaraco.packaging>=9.3; extra == "doc" +Requires-Dist: rst.linker>=1.9; extra == "doc" +Requires-Dist: furo; extra == "doc" +Requires-Dist: sphinx-lint; extra == "doc" +Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" +Provides-Extra: check +Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" +Provides-Extra: cover +Requires-Dist: pytest-cov; extra == "cover" +Provides-Extra: enabler +Requires-Dist: pytest-enabler>=3.4; extra == "enabler" +Provides-Extra: type +Requires-Dist: pytest-mypy>=1.0.1; extra == "type" +Requires-Dist: mypy<1.19; platform_python_implementation == "PyPy" and extra == "type" +Dynamic: license-file + +.. image:: https://img.shields.io/pypi/v/jaraco.functools.svg + :target: https://pypi.org/project/jaraco.functools + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.functools.svg + +.. image:: https://github.com/jaraco/jaraco.functools/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/jaraco.functools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://readthedocs.org/projects/jaracofunctools/badge/?version=latest + :target: https://jaracofunctools.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2025-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/jaraco.functools + :target: https://tidelift.com/subscription/pkg/pypi-jaraco.functools?utm_source=pypi-jaraco.functools&utm_medium=readme + +Additional functools in the spirit of stdlib's functools. + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/lib/jaraco_functools-4.4.0.dist-info/RECORD b/lib/jaraco_functools-4.4.0.dist-info/RECORD new file mode 100644 index 0000000..2b53e4d --- /dev/null +++ b/lib/jaraco_functools-4.4.0.dist-info/RECORD @@ -0,0 +1,10 @@ +jaraco/functools/__init__.py,sha256=ZJx9cMs2Nvk2xGUl8OjVGkpjdOaNlSzJrN4dGglgX2g,18599 +jaraco/functools/__init__.pyi,sha256=K4DcbnYIHE5QlMxqf9-cVp-WhycrhuTao4J7O7TMq4Y,3907 +jaraco/functools/__pycache__/__init__.cpython-314.pyc,, +jaraco/functools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jaraco_functools-4.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco_functools-4.4.0.dist-info/METADATA,sha256=LnnajcNGmSSr46yLIqP-tWkqeb-fR7vIa2U11hhkGEk,2960 +jaraco_functools-4.4.0.dist-info/RECORD,, +jaraco_functools-4.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +jaraco_functools-4.4.0.dist-info/licenses/LICENSE,sha256=WlfLTbheKi3YjCkGKJCK3VfjRRRJ4KmnH9-zh3b9dZ0,1076 +jaraco_functools-4.4.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 diff --git a/lib/jaraco_functools-4.4.0.dist-info/WHEEL b/lib/jaraco_functools-4.4.0.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/lib/jaraco_functools-4.4.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/jaraco_functools-4.4.0.dist-info/licenses/LICENSE b/lib/jaraco_functools-4.4.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..f60bd57 --- /dev/null +++ b/lib/jaraco_functools-4.4.0.dist-info/licenses/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/jaraco_functools-4.4.0.dist-info/top_level.txt b/lib/jaraco_functools-4.4.0.dist-info/top_level.txt new file mode 100644 index 0000000..f6205a5 --- /dev/null +++ b/lib/jaraco_functools-4.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/lib/jeepney-0.9.0.dist-info/INSTALLER b/lib/jeepney-0.9.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/jeepney-0.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/jeepney-0.9.0.dist-info/METADATA b/lib/jeepney-0.9.0.dist-info/METADATA new file mode 100644 index 0000000..c9a9e6c --- /dev/null +++ b/lib/jeepney-0.9.0.dist-info/METADATA @@ -0,0 +1,35 @@ +Metadata-Version: 2.4 +Name: jeepney +Version: 0.9.0 +Summary: Low-level, pure Python DBus protocol wrapper. +Author-email: Thomas Kluyver +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-Expression: MIT +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Desktop Environment +License-File: LICENSE +Requires-Dist: pytest ; extra == "test" +Requires-Dist: pytest-trio ; extra == "test" +Requires-Dist: pytest-asyncio >=0.17 ; extra == "test" +Requires-Dist: testpath ; extra == "test" +Requires-Dist: trio ; extra == "test" +Requires-Dist: async-timeout ; extra == "test" and ( python_version < '3.11') +Requires-Dist: trio ; extra == "trio" +Project-URL: Documentation, https://jeepney.readthedocs.io/en/latest/ +Project-URL: Source, https://gitlab.com/takluyver/jeepney +Provides-Extra: test +Provides-Extra: trio + +Jeepney is a pure Python implementation of D-Bus messaging. It has an `I/O-free +`__ core, and integration modules for different +event loops. + +D-Bus is an inter-process communication system, mainly used in Linux. + +To install Jeepney:: + + pip install jeepney + +`Jeepney docs on Readthedocs `__ + diff --git a/lib/jeepney-0.9.0.dist-info/RECORD b/lib/jeepney-0.9.0.dist-info/RECORD new file mode 100644 index 0000000..74db428 --- /dev/null +++ b/lib/jeepney-0.9.0.dist-info/RECORD @@ -0,0 +1,64 @@ +jeepney-0.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jeepney-0.9.0.dist-info/METADATA,sha256=uObDU-mq7Q7QFEApVWQX_aI7ZPNE5xgzGQcFS6BbjGI,1230 +jeepney-0.9.0.dist-info/RECORD,, +jeepney-0.9.0.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82 +jeepney-0.9.0.dist-info/licenses/LICENSE,sha256=GyKwSbUmfW38I6Z79KhNjsBLn9-xpR02DkK0NCyLQVQ,1081 +jeepney/__init__.py,sha256=ULhIr444tY81PUkEHRWNlTXlqRbFmqMTbWBsktKgmoI,408 +jeepney/__pycache__/__init__.cpython-314.pyc,, +jeepney/__pycache__/auth.cpython-314.pyc,, +jeepney/__pycache__/bindgen.cpython-314.pyc,, +jeepney/__pycache__/bus.cpython-314.pyc,, +jeepney/__pycache__/bus_messages.cpython-314.pyc,, +jeepney/__pycache__/fds.cpython-314.pyc,, +jeepney/__pycache__/low_level.cpython-314.pyc,, +jeepney/__pycache__/wrappers.cpython-314.pyc,, +jeepney/auth.py,sha256=ZW0HMX6Vfwx28P-jNrzVVgEn1ipjO-KJrNJ2SG90V3U,5409 +jeepney/bindgen.py,sha256=yPDJFt_WjKoFUp08r-_upsqu0L8Rmv8gNKr-MA4T4bI,6085 +jeepney/bus.py,sha256=KUiSr3ECzdbe-S9tNKm6kvf3oZi4RYnJWkZUXK7tE2k,1817 +jeepney/bus_messages.py,sha256=uUCc_1Xllzth4F95aghpDLmlv5Gz0are2FpKg7D_gqc,8239 +jeepney/fds.py,sha256=ZYzN_c_7rkBT0wU7dYUmQRijpSzCv-DATCYEklpXxUU,5056 +jeepney/io/__init__.py,sha256=inJI_1U-ATymLcAVYs-LD2aUwgl-tihW8-oVFUxYRgA,33 +jeepney/io/__pycache__/__init__.cpython-314.pyc,, +jeepney/io/__pycache__/asyncio.cpython-314.pyc,, +jeepney/io/__pycache__/blocking.cpython-314.pyc,, +jeepney/io/__pycache__/common.cpython-314.pyc,, +jeepney/io/__pycache__/threading.cpython-314.pyc,, +jeepney/io/__pycache__/trio.cpython-314.pyc,, +jeepney/io/asyncio.py,sha256=qfWi_1pWCXSP1LNRafHBuvrxHx4tX96b52KBa4sUFMc,7622 +jeepney/io/blocking.py,sha256=I_rw90IY_EesBZmkfUqk7UniyVkQAngz7jyQmzju680,11940 +jeepney/io/common.py,sha256=l8lbFUgQmBxfqSC-hqHYmPUYCVFMKbOGB1k5ZWPKXfs,2696 +jeepney/io/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jeepney/io/tests/__pycache__/__init__.cpython-314.pyc,, +jeepney/io/tests/__pycache__/conftest.cpython-314.pyc,, +jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc,, +jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc,, +jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc,, +jeepney/io/tests/__pycache__/test_trio.cpython-314.pyc,, +jeepney/io/tests/__pycache__/utils.cpython-314.pyc,, +jeepney/io/tests/conftest.py,sha256=o7JrYypYE-0jNFUndsQ4Ek5dNYM0ofh1sYcIVeCZMj0,2730 +jeepney/io/tests/test_asyncio.py,sha256=JJtnX5HiRRZjjuGIDoI8LvzfbaSNg-ljiX95yUvd9xk,2720 +jeepney/io/tests/test_blocking.py,sha256=ETLnoivenN8Dzp0JB4wPOb9PNbpSuiocuP_IDeNRlI4,2804 +jeepney/io/tests/test_threading.py,sha256=RALwy-aI64TBoFmBnSU63HLcwRnStLVtnewOtoaBl3o,2699 +jeepney/io/tests/test_trio.py,sha256=DPY1V_K2qLTyBTrbrxZeLTA5dmca3Ye3e6pz08UxbO8,3892 +jeepney/io/tests/utils.py,sha256=i7VJYT-axefzS8mWcvv-9DeHEB6LdP9M82H3Hx6fyC4,79 +jeepney/io/threading.py,sha256=mwGCNlun_baX8Y4eienCGDKdZD4SKdTMvBTkIE0EMKo,9391 +jeepney/io/trio.py,sha256=IdZIJnQcPjVOBA9KooFn0nTBEz3BuBDkz56qLYhGR1M,15088 +jeepney/low_level.py,sha256=m4wGY-quPnzylgKlBdBccmkuOXF_hQ1gbtT25qPX2GM,19949 +jeepney/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jeepney/tests/__pycache__/__init__.cpython-314.pyc,, +jeepney/tests/__pycache__/test_auth.cpython-314.pyc,, +jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc,, +jeepney/tests/__pycache__/test_bus.cpython-314.pyc,, +jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc,, +jeepney/tests/__pycache__/test_fds.cpython-314.pyc,, +jeepney/tests/__pycache__/test_low_level.cpython-314.pyc,, +jeepney/tests/__pycache__/test_wrappers.cpython-314.pyc,, +jeepney/tests/secrets_introspect.xml,sha256=9cfNs1aGLtIAykcQVsycwIwCLmEeorKkFjqJCLAknRQ,4575 +jeepney/tests/test_auth.py,sha256=Ee79vsedCwveukudAZTwqYTXHWV3PYnXkmMl0MBMZEE,611 +jeepney/tests/test_bindgen.py,sha256=Ez99zr9TIV3mlZdH-2A_dz4LbvxCqzWDIadhOCbbaoc,1098 +jeepney/tests/test_bus.py,sha256=ApOxd3AcYQB14G1XsiFGBYtQ4xSKw52y9YvmPz700gc,847 +jeepney/tests/test_bus_messages.py,sha256=elwS7odY9RDsjg9jL4tN0O7uCxUqSYHsWShWXn_WPOQ,3338 +jeepney/tests/test_fds.py,sha256=-gyvQpfsXtPaIEeqbwhrNPOcIAN0DsrQ7MXZu4nMvvQ,1821 +jeepney/tests/test_low_level.py,sha256=2SC-wKKGr0yfEguswfHzCojSTwsYlTVLPyuzQbGS3L4,3000 +jeepney/tests/test_wrappers.py,sha256=NSY6LblWeU2kToISjpi9YHgrd_Y6PVyFwXqnbY93ygU,2202 +jeepney/wrappers.py,sha256=5zM_v1jFqEGDSaPh0f06SDxCF6JmWVhyXjfYR6KHum4,9605 diff --git a/lib/jeepney-0.9.0.dist-info/WHEEL b/lib/jeepney-0.9.0.dist-info/WHEEL new file mode 100644 index 0000000..23d2d7e --- /dev/null +++ b/lib/jeepney-0.9.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.11.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/jeepney-0.9.0.dist-info/licenses/LICENSE b/lib/jeepney-0.9.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..b0ae9db --- /dev/null +++ b/lib/jeepney-0.9.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Thomas Kluyver + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/jeepney/__init__.py b/lib/jeepney/__init__.py new file mode 100644 index 0000000..b314820 --- /dev/null +++ b/lib/jeepney/__init__.py @@ -0,0 +1,13 @@ +"""Low-level, pure Python DBus protocol wrapper. +""" +from .auth import AuthenticationError, FDNegotiationError +from .low_level import ( + Endianness, Header, HeaderFields, Message, MessageFlag, MessageType, + Parser, SizeLimitError, +) +from .bus import find_session_bus, find_system_bus +from .bus_messages import * +from .fds import FileDescriptor, NoFDError +from .wrappers import * + +__version__ = '0.9.0' diff --git a/lib/jeepney/__pycache__/__init__.cpython-314.pyc b/lib/jeepney/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1440bcda3f1a9dee798c8d1e0eeda73fa2804b64 GIT binary patch literal 716 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 literal 0 HcmV?d00001 diff --git a/lib/jeepney/__pycache__/bindgen.cpython-314.pyc b/lib/jeepney/__pycache__/bindgen.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07af7a357606b11503cab9f22d757351eaad55b9 GIT binary patch literal 9689 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 N&-x(d%GT1k{XbAN(j@=@ literal 0 HcmV?d00001 diff --git a/lib/jeepney/__pycache__/bus.cpython-314.pyc b/lib/jeepney/__pycache__/bus.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ae5ff8eabccb1aeb45b5d7973c03b1009d61c98 GIT binary patch literal 3151 zcma)8-A^0Y6~EUr;}2|Oldya^e3&?;cmtTPl!WXOkp-j(mC)8cRR*aJ9$@#xN& z2BK7GpG@~5Nuwr?R_*ebmG+_kLi-nZ*WRr6)>2pMs#5wypwepJdhU#8APA|w@*Ll} z_uO;N{hi-EcP=*i8xXXAUwxi!cOmpo{$e$$-Z}gdIy>ktBKQW<&3~-pCA=V7sz?7l zh&aZu`K}8@Xhph6M355k=}sa+&qbUd-NXgbL);)6mb}Ds`PT?#x)JI?OFppDu;l4M zV|Bx04zts3&Uu&Ir_B)@ffyqw>VL!4=eL`F*7uvCfh$u3v#EiVrH3PrqUm~QhB+R- z0MFqFh#f>wD^l^IF~!9KpF7d+1g%iO4SvTOhwCgnhYKKf(4y@rajK=e*n;V9kl^w9 z2Eo4tJ4)CZ;LGub+lyi*E~hj(Dlj3d=uBFrxxC7R^csyin6#17(`yVX8n@OPW+O3u zLyf6fbwE$iRaqa%uj)$b()vhjJ-ZxJxAe7~61ykMsv>X2mh;-6y2V5)tLv;KugF?D zrOJaJtmo3Hb!~Exk2}Gx204QL)qH^`(hKiXZ*l5b;~uH_`U99`^+BJxEq9b7$Pv)mK^8(nE64I%9!U>zJi#Ej)E-S7(v%L`O90(8ULU%5VIv$`MsT<9GY>p5y zf-VVJ9F-Wp%J2lk{S1FN&-1UT>sj4^FzRMlV`3_kq0FPxl%lCQsxvXGtmK&E{wBi@ z7`{)>fG5ojqBm?dGaUgnMKw9eSIe3vYqSScyj*t$#2Wy&8f<#udg>|)&wT~48tVG| z{g?0W#b5Q5L;bsff~V>a6dcv2_g;jahKe6O?=DPL&o;jpe>z?q-NVnW?M@e_YRw(R zUp<>ET>o_Dt%T0>>^o7gy^yQ8I}iP+sq-hz39dewicJl_d(8N-k!g;wndzgWX4MN7 z6Qnwhp?f^*Nra4yjwOUe5v&>Q3RufD^Xdb9fJ%6>B?K!!kO}ag5D4ZKYZWrsf#&)V z)V+jw1(s!6`3MmbB60MGj_iPn-OP*+eqOh> z8s`2MJ(h9eT;QoX(GvJ*uNJSZ9j#&Fs&mRc%f!5reZbtQWes?f))_{(m0ZF_k7 zdR`0LY|_Gg4tK3CCX64Fa*|`0a&-o8;Eabn+IN znve5-oC!cP<|BDU&u+*wl;$XNuH-1xvZzGQ!8Uj?vK=Ik+%stXM~`7;Pk=vrs^v)d&xRP{CO%+%WZUNygNAKdi= zmA%cCj-iTosOmfat*`ehU+>=5Yv1tq{+43zYk#;JXfKjVpu5@J@D zi`BM{KSLV zxnto_zyX5y5%v)LRsf}+G~g5ZVcMohthd@i%(XorH zUdL=r=n!p_gDX3XgUk1Z2ePe>|}} zQED0b#yh+-UGoMi!RV_CH9K20>cm_m|>yx1K_G+-iXo1hb*z%{kYAZ$P%Pwq~ZLZg2n zW$)!55%!G(*?i+w?=SWRBzb-~9cG?5tJyFsFv9Oyf{brBs5(%GY)z{cN4SPjD>J4B z=cv(+H2!b)Ihd1&az2x%`wzC-QXf~c?(+=S6eO6 zT@Lh=+&xu~|4CcL(_Ra7l>_0DJN!f7Y^8bNRi@lLS`Lhr++zmum^+!=NM)5|a=S5Z zKpqC`;gmkH9f+Hxwg#e@M!;9pZ={krsLWjH+=iN6muV0tj0@YSkhG7#8p-9+nS0^^ zb8kv&Lrx}{q*D0u`vMtJn0s@7u#wB;*X2n%1G+o`+6IVy0b~4kbbM10}E=fYk4XHZ*D2{j*xO_<<=UI!jNXb`w?;!7|o`U=|>q5V72Q)&pk dgaS2C5eJR8b6<51lsjXu#UZ%9#gGxse*uAqYmNW_ literal 0 HcmV?d00001 diff --git a/lib/jeepney/__pycache__/bus_messages.cpython-314.pyc b/lib/jeepney/__pycache__/bus_messages.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a9d27bc7ff2ac1ec0c6504c34f77c98fc437870 GIT binary patch literal 12619 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 literal 0 HcmV?d00001 diff --git a/lib/jeepney/__pycache__/low_level.cpython-314.pyc b/lib/jeepney/__pycache__/low_level.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..385239c4bf4a8b9ad7d1e8349d8a32b347dfb467 GIT binary patch literal 36681 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 literal 0 HcmV?d00001 diff --git a/lib/jeepney/__pycache__/wrappers.cpython-314.pyc b/lib/jeepney/__pycache__/wrappers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8661918fb794576dd3f0342b4a1ed18b5e010100 GIT binary patch literal 15807 zcmd^Gd2Afld7s&XJKW_iDUuRNogfM zApL!Bj-6etBsWJ3G!NpN**EX}-gkeWx+38qhw{@W{w)26TRHCE>4#nX*30rMJjcy( z=ePtv#A)>O4Dm-jnr9QedxyM7eVPyNe8M;6--Xr~mk^KhTFqH*Bw|lidT!BCKr{+8WPB`n^_b zeWD5d8xorn%`IGp*H+bS|sP`Oocmc7M<3q@H}T=dAVC+w(YnbnZA4yWgj1*B=a)b?z#4 zHb-|wqa7*h?iut6$LJoG{~pyGcgFcP%GvjM?w12sIBtA1$F1i^frH&^*+^C;Jl&-q z+%$j2x>F8#Im!=u>bQh&r`Miol0W0KyN?9yx6-q;=60*`!YW#|Y*voV>UYnYsKM<7 zKKdmF{oU|K0v^M^@Wx`Xd=PLIv(l6-?)Zfm71OGy<#J*sm%SkF*dy-TwM$G*N{W=y zWJQhsBEouPbDDTj%B07Kqdvo%k+Vi{RLacA?0$^8u^BZ8D-1ns~EY>$&KJdm1^ILyZ zzv)%qQlxc$@ap)x&lGx&721XuBF78D@ug5pF|H; zJi(_l(vK!oMmTMEBqUK*(}y*vEX+{as8hXp%T8BiBU(XYoO%Y;gRi-;uIt*N>jQ

`_EXUB?rY80g+lL0 zq3y&%BvB9&OQE&Jkf?{mtIfsGwws}CM8B-pjc}oD&q8ExLD);zx}9BX3hOwRnlBMZc$r-FCw!2?H3>iJK%zzyMxhrZJm9Q?QbQB; zKsKD31ccJ#a#l-EK$3~tq%7vfJ`X`AO4)JI!Ddksr%VY*Af3o5v0zY+U5MGO#E8Z5 zv^t%UX2o$?eNM|w6XY>#O41-(Rq;}8CNnNhN*86Uf=MJ(v>PYQID&qm~~bj^;$hc_SB<}JjR^8S-M+R z5?niXoga<{jL30zr6ZCyd1`iAHi9Nnk4u_lgv{$E&Nj(JlOfo^WGu|6bsz-!=~Ly* zghJ9>*+Eqll}dZthGH^8A(>2P(^@iVG`sFJX16{;Ble;KOBB{D2(3k-T^HJ~e)g8o zySQ%KEur;;P}9xOrlq>I^8>f)Iu={S*TWx#ns0{0rMh+VL$~U-Ew*iWGh&hlP}nJS zaCRZD<7JLZa3k)oR=1MsQBqDu9N;>0K`|O}09n>b+DJt~U#9ezH}_k!g5dc4_7`8; z@#1FXrEbv%?r!s^16=ky8m%#EKwVHn44+0<1!ywuBv}FU>%78v5FN1uctxI8?ghNr zdIUYy0aSC`V*Nm|ey?7?_aA-VJN1KLaorysdXkHECn+d+39_qZU#J6)w2bO|#Ch4lg zsdA7p>R});agr%*7GNTm(%K}D(vtSZS~p&nfBP(ERB1-S&X)PwTf&wlC}oFU&(C)h z*LLb_JFgx3#^UCZ}@aGZ`88QkP6V zKO<$#3pMJK$%(Y0YMFFa&gSqrkW7x}Qpu#!Md#W{)h?Q1buy`H;G(Idq-jceY(|q+ zbYGQBrZSSMj;B*vO3q}G$?Lokb~R!cp~pT(myGALiY8V!qx$5_IOfNmfM2-V5cId- zY4!LI^DBbK{}gVV>U%|y&C-@1#dwL6c$W}5&V&#TLxJn#6Fh{F0OQY>@M!)~7_Xz; z6TB7xHTaAmU4Y4x%2=v{PX{pjlbD-SC$pUNjdJJl{d6PPowbWi+!-3dN`gD*N$`(Z z*trQNHCFQGqlZ8dGDMI(F2V+o)NXM`mEFPyGEUWGX&gph5KvA%mys{Znc=9{@T)UW z;myk+T#foo{BjVEVVY5EK^)Mg-4G+3Ga*jbXf&11>hQo3d{8%_`h?Id{8~U4TIbi@ z0JaS-33Y|=!1qoRg(D35*E!`ebcD<%Y*L7RmB*$fHh>-u(^$1iI zWfXo5cr!c+PaHbRV*&FsU64ZKqJG0ak&!OI%$`1yA!Ez^X`uW_HF4vsCT`A-XN01+4q{_F4u2f|Vt#rEiArK<- zyP~iidSp=LdhT?`L&mSI7>u7Z{tVGHE2j_IJ}O|jN4g3=d=N^orp%CLEdu+XIm2Tx zE~cbR#@3>PK~o=xax5xx1}q<}XcqpBOFfJ$Sz}`2P*$9hva=Yh%3x_spT3mFL}N0W zP#zCXD7h(2Gsr~5;Q31b)p=2!8DrnXOOxr;qzKBp2t$CDp2{hhCYQY^XVb6@g8N5? zpzaS&!?Q9~#yS;wDyPXwXpn>AsgmUYQ-Ga{YzB75T-F{92w08g2oi;2Zi3kzR%dIp zdxV<4kd?FzO&oc!&|QfXsC4khw&Xx|ur4FfhZYGkMih zjDZDnwv4LW2zaHCP8qujYcIv(^ zHe#I~Sa&n9eX)12*n3FtJ@jhLTjAHjS2D#l-8%jWJ&Vz}z0y{_ajWjzwisx*8CbvA z`*=mq4?~TYA6*EE1)pd#XfF<97l%P#0VTQ_^n(SL!-}EPRI>-FvS;r@m`4>CzQrDE z?CqAG)#e%@kzG+-?4KN06$MP)63`GTK8wj6tTwl6p8K(J-WuQZkn#6n&S%J7#>(lP z_O6IY=D%IZA>nyv&T~*4bG|z~$RYp{)Q{+`k1T|D zf~ez<7vuZ%_`YKNh#o&uBGfLuu}k-LFNRx+;f;EDV==r{4{t4oqk1^{_3mQlZoPB& z``SYI5atMkZwA^HcMjM@8PnIqbRm8}VqK~vRw7WlIK=udzZ$WU-c*g0euKKyLp6*l zn%4i2$Q_N$1`G6yl8#q}I9){UAN5;Adg@mrQuw1johEc2P3jE!q**D~gz`VFByu1< zWa@8`)Y$^eiE^*X!M^?W_(H_R7yN9kYWzTv0VpF_+NUys28GDGk{{G4lhm4_isZa< zo~jZzAgm_?)Mf{@#171br|{_&{HcG93T&XkVgpgVHM$V){1j}U=P_FnNAIgbGDrGpNtS6>7{cB?EzJ z2+HqMFY>Lk0HpFPy)k;Xw1R-Os*-cQoToS91hz`B%~A=NtHWC6d3?V@ef|O!V*=%h zey85rxe)FmML$;DKB#XWENKJe5Mhdbq!?(`1Fbg$B2)B_EJThLgrkeyeZ}tGdbi6o z*r7M>(0$QQufpG>6=0W6u@qQtc^{g}qF>is(Jwd~DgK4H7e&7U%oT|USVll*OpR8H z7+8?G1_KItq4IB>w5?#KFH#Uoo`65rWspLJyeOqJl-MEWvd|})`RQCYEMqI^9~(<& z$1liPLI6>k(Q@$OBl?wr@|jX(xF8*q8&>v=N~nxkth^Bzd@%%=+%^ln<4;)rI$qp1 zOFw>7d02p{Bir&@Dn<}4y%Qc-Mshy}06+sVplu{`Kx`sf47R(w^6 zwXnF7iOV*T4q8KF~zDkFN0CPD|+bnJ0NxgC0sNX}%U4G$~ zATEaL-`exqo|~bq6g@ihwc&S$Z`E~uzyeJOExj2wZAHivr_i|=+%Mwge>09``?X;W z@%fhO5u+%)&@t@GjD0Ylumery5qxlYIavt7s0KeXBJ@6Wr)U60gmx6`<9dDkTfQ5o z{x(?bJ*f8{TcWPM60b1LV7h)7LZb zB_tZ5bv<{2pXB4*N&Z>-D@!?g0)Bf!60!k5A|hvq67?9u@oDmdz~AeI$p&vWJGbM_ z-ZE!;GZ0i^J{k}hf+8ELU`y%qHB7IP{6gsC74G98*ULSsAs+ zWP4%u09=XrAMPhV;0v=9$xelrIp32}^{vXTiX$>5Td)|g`rf*lSm80#^36R`tky^mpEqUoq4=bN>?DY*8g zVEZ#q8G2?#n+d0Lv0%!U6$B}I2MP+yoyLBe zIX-TF)aZcAVMIT`Y%1L;2&rYmn|0<*xi{hPlz(NtM)_*7>4cM_M&G%DN&`2XJ5_RjYs>xNPY8j*Oj(73Hox9!8Gwe$U7d-9zp zuQgxO7Ml8B6_!FxmyaU5?q+DiVpB`8sY7q-xcbaOQ>;)I`!KS89;sPd3c{9qZB34bvSs5yQWdO9$Qh9@pMwn&G z$UnxrvBm`kmt(g;og=6qAS0U#TP#(L9YA>*?RJ#9GOoxN9OG?oqL)HzJb;^hfj(39 zUHqvcDvO)>{KB0e@85W*M)0?z{+wb{%<@q*m8p0OC4RJ0{m zCPXNEJrr!PzD`Q2I5m^e(iB*5G(ZGkWC;-zrvXV$U`ZN=UCIVkS)&L=X<{ghjJqKY zZRT5nYO>>!GG3uris#Q)<}#7y8N*duTReY0$fC2h81Uhkgo+Ok!3o|L!FYQ5Jnr1z zI8P#2c>yyRUYr=zNVA!V1H|3%F-8rQB3OBmhP0G62FB>f!sL(fQQd;-6NH#KFXY+B ztbXV4yIp#GZ(+;cAFbbKHr-2Eq=RA;!t!~%lr!&AI2GKKj$TpFN~0@Ef&$QVER<2M zlvZWs3v7A_$N>VT9FT1`3{~t;@iYNM%#?jI1iEWAnVzs|mO7QuEK!!xR0c0IWs1S7 z9bdt?n%2B7EVZ^@J^0@7caOiH`iIPSGK;a}h(foW;9vFGimOdB7{k#W;b?Ws=hzIc zbR2HP-H-aBBoL8@=%@m^YJeN%`#i3cCU3su2r!#$aU@sII6*u*JF?7AFZ$u~|R zap~=&UpZQA-ljKiyXGr2F-Id1-b}Tq*crgKN1N9<<7?Q%6ElIeNc(?) zTqUu^WY+>-)NWLkT&?j7D-q(Pw;3nB6ZHDyR6V+aigre|dBqsy6q^i|2~K6=^=BpV znt*5}bAb}JkT>juOzAwNHZ z(6=RS%iJR3Gsk|9nFkW01e9-JNn;~eS~|*`+@;b0CeMk{R_aGvjFo=g!DqFeZI0dh z)iVqAI}5^220G;kJ`CR{@rsB|q1hjBeFQWHxu4-hC9f;HzQu-Jb3-dWkH3$S?D`&X zO=R7dUDMC+y(U~1TwUbog4W{)Fl&?`hUNDrq_hgna%|QznOw0^V3wM%GILhwucF_*%^!VUZ-n_Ng9MzjyV06FUxqqSgz^lI3YpDl>69jgxdDAu#VmxON zh_F1&W~-jDTwN|z7u>FRI-d_b`{GMy#TQ?K9>>%+cK#b!>0a4F38s`d+(sH^<*C`H z|H3$yvWP?1w-)O+>Ghkg`rfO3xAt29Hy*!^bl6=5VHZ0h0LxaP^@gE+6B^&&!1xw@ zy}ze~keOlh&+($tl#Gb}%{O!h>53ax1l~^)&2LFGg02l=x>Lg3bO5us zlwakWXaYxJzIL9$j`?o3n7$iQcA3UY3#k;PCB3$mZ~mC7D@WMl-n+JF$hZ6o8-e@7 zv$9m9p$8`m4gkL`F9GozR!W)eEQOCiSW&?Db!Nd8U!~CTNpNQ>X6(f?!x|8Ne*m`>0*4R=OY=kkCW!I>=iuE>j^dT22#1H>2yX`NOYJk^bPdJT<9XbZKjIYDa+I~!%Fwv8?(}xNaI2c$4t_OQSU0lJaAGce$LHq{ z@n~*Z{dVV9I|~~gTWB1ZtEc9D{7S<*{wY|ARNo!+@<;j0>+W*&yc6{B19yd1e)HY_ z2r97wRmUoR^U97!o(xN>S2~;dA-q$)(!G{Hh*4*2=L!s@MeIXv%10{o-%wRbr@ LR+>S$?AZSehSk;> literal 0 HcmV?d00001 diff --git a/lib/jeepney/auth.py b/lib/jeepney/auth.py new file mode 100644 index 0000000..2c4153f --- /dev/null +++ b/lib/jeepney/auth.py @@ -0,0 +1,144 @@ +from binascii import hexlify +from enum import Enum +import os +from typing import Optional + +def make_auth_external() -> bytes: + """Prepare an AUTH command line with the current effective user ID. + + This is the preferred authentication method for typical D-Bus connections + over a Unix domain socket. + """ + hex_uid = hexlify(str(os.geteuid()).encode('ascii')) + return b'AUTH EXTERNAL %b\r\n' % hex_uid + +def make_auth_anonymous() -> bytes: + """Format an AUTH command line for the ANONYMOUS mechanism + + Jeepney's higher-level wrappers don't currently use this mechanism, + but third-party code may choose to. + + See for details. + """ + from . import __version__ + trace = hexlify(('Jeepney %s' % __version__).encode('ascii')) + return b'AUTH ANONYMOUS %s\r\n' % trace + +BEGIN = b'BEGIN\r\n' +NEGOTIATE_UNIX_FD = b'NEGOTIATE_UNIX_FD\r\n' + +class ClientState(Enum): + # States from the D-Bus spec (plus 'Success'). Not all used in Jeepney. + WaitingForData = 1 + WaitingForOk = 2 + WaitingForReject = 3 + WaitingForAgreeUnixFD = 4 + Success = 5 + +class AuthenticationError(ValueError): + """Raised when DBus authentication fails""" + def __init__(self, data, msg="Authentication failed"): + self.msg = msg + self.data = data + + def __str__(self): + return f"{self.msg}. Bus sent: {self.data!r}" + +class FDNegotiationError(AuthenticationError): + """Raised when file descriptor support is requested but not available""" + def __init__(self, data): + super().__init__(data, msg="File descriptor support not available") + + +class Authenticator: + """Process data for the SASL authentication conversation + + If enable_fds is True, this includes negotiating support for passing + file descriptors. If inc_null_byte is True, sends the '\0' byte + at the beginning of the negotiations, which was the past behavior, + but which prevents sending the SCM_CREDS ancillary data over the socket, + breaking authentication on *BSD; the caller should rather send that + null byte and ancillary data and pass inc_null_byte=False to prevent + it being done here. + """ + def __init__(self, enable_fds=False, inc_null_byte=True): + self.enable_fds = enable_fds + self.buffer = bytearray() + if inc_null_byte: + self._to_send = b'\0' + make_auth_external() + else: + self._to_send = make_auth_external() + self.state = ClientState.WaitingForOk + self.error = None + + @property + def authenticated(self): + return self.state is ClientState.Success + + def __iter__(self): + return iter(self.data_to_send, None) + + def data_to_send(self) -> Optional[bytes]: + """Get a line of data to send to the server + + The data returned should be sent before waiting to receive data. + Returns empty bytes if waiting for more data from the server, and None + if authentication is finished (success or error). + + Iterating over the Authenticator object will also yield these lines; + :meth:`feed` should be called with received data inside the loop. + """ + if self.authenticated or self.error: + return None + self._to_send, to_send = b'', self._to_send + return to_send + + def process_line(self, line): + if self.state is ClientState.WaitingForOk: + if line.startswith(b'OK '): + if self.enable_fds: + return NEGOTIATE_UNIX_FD, ClientState.WaitingForAgreeUnixFD + else: + return BEGIN, ClientState.Success + # We only support EXTERNAL authentication, but if we allow others, + # 'REJECTED ' would tell us to try another one. + + elif self.state is ClientState.WaitingForAgreeUnixFD: + if line.startswith(b'AGREE_UNIX_FD'): + return BEGIN, ClientState.Success + # The protocol allows us to continue if FD passing is rejected, + # but Jeepney assumes that if you enable FD support you need it, + # so we fail rather + self.error = line + raise FDNegotiationError(line) + + self.error = line + raise AuthenticationError(line) + + def feed(self, data: bytes): + """Process received data + + Raises AuthenticationError if the incoming data is not as expected for + successful authentication. The connection should then be abandoned. + """ + self.buffer += data + if b'\r\n' in self.buffer: + line, self.buffer = self.buffer.split(b'\r\n', 1) + if self.buffer: + # We only expect one line before we reply + raise AuthenticationError(self.buffer, "Unexpected data received") + + self._to_send, self.state = self.process_line(line) + + # Avoid consuming lots of memory if the server is not sending what we + # expect. There doesn't appear to be a specified maximum line length, + # but 8192 bytes leaves a sizeable margin over all the examples in the + # spec (all < 100 bytes per line). + elif len(self.buffer) > 8192: + raise AuthenticationError( + self.buffer, "Too much data received without line ending" + ) + + +# Old name (behaviour on errors has changed, but should work for standard case) +SASLParser = Authenticator diff --git a/lib/jeepney/bindgen.py b/lib/jeepney/bindgen.py new file mode 100644 index 0000000..4695eb6 --- /dev/null +++ b/lib/jeepney/bindgen.py @@ -0,0 +1,170 @@ +"""Generate a wrapper class from DBus introspection data""" +import argparse +import os.path +import sys +import xml.etree.ElementTree as ET +from textwrap import indent + +from jeepney.wrappers import Introspectable +from jeepney.io.blocking import open_dbus_connection, Proxy +from jeepney import __version__ + +class Method: + def __init__(self, xml_node): + self.name = xml_node.attrib['name'] + self.in_args = [] + self.signature = [] + for arg in xml_node.findall("arg[@direction='in']"): + try: + name = arg.attrib['name'] + except KeyError: + name = 'arg{}'.format(len(self.in_args)) + self.in_args.append(name) + self.signature.append(arg.attrib['type']) + + def _make_code_noargs(self): + return ("def {name}(self):\n" + " return new_method_call(self, '{name}')\n").format( + name=self.name) + + def make_code(self): + if not self.in_args: + return self._make_code_noargs() + + args = ', '.join(self.in_args) + signature = ''.join(self.signature) + tuple = ('({},)' if len(self.in_args) == 1 else '({})').format(args) + return ("def {name}(self, {args}):\n" + " return new_method_call(self, '{name}', '{signature}',\n" + " {tuple})\n").format( + name=self.name, args=args, signature=signature, tuple=tuple + ) + +INTERFACE_CLASS_TEMPLATE = """ +class {cls_name}(MessageGenerator): + interface = {interface!r} + + def __init__(self, object_path{path_default}, + bus_name{name_default}): + super().__init__(object_path=object_path, bus_name=bus_name) +""" + +class Interface: + def __init__(self, xml_node, path, bus_name): + self.name = xml_node.attrib['name'] + self.path = path + self.bus_name = bus_name + self.methods = [Method(node) for node in xml_node.findall('method')] + + def make_code(self): + cls_name = self.name.split('.')[-1] + chunks = [INTERFACE_CLASS_TEMPLATE.format( + cls_name=cls_name, + interface=self.name, + path_default='' if self.path is None else f'={self.path!r}', + name_default='' if self.bus_name is None else f'={self.bus_name!r}' + )] + for method in self.methods: + chunks.append(indent(method.make_code(), ' ' * 4)) + return '\n'.join(chunks) + +MODULE_TEMPLATE = '''\ +"""Auto-generated DBus bindings + +Generated by jeepney version {version} + +Object path: {path} +Bus name : {bus_name} +""" + +from jeepney.wrappers import MessageGenerator, new_method_call + +''' + +# Jeepney already includes bindings for these common interfaces +IGNORE_INTERFACES = { + 'org.freedesktop.DBus.Introspectable', + 'org.freedesktop.DBus.Properties', + 'org.freedesktop.DBus.Peer', +} + +def code_from_xml(xml, path, bus_name, fh): + if isinstance(fh, (bytes, str)): + with open(fh, 'w') as f: + return code_from_xml(xml, path, bus_name, f) + + root = ET.fromstring(xml) + fh.write(MODULE_TEMPLATE.format(version=__version__, path=path, + bus_name=bus_name)) + + i = 0 + for interface_node in root.findall('interface'): + if interface_node.attrib['name'] in IGNORE_INTERFACES: + continue + fh.write(Interface(interface_node, path, bus_name).make_code()) + i += 1 + + return i + +def generate_from_introspection(path, name, output_file, bus='SESSION'): + # Many D-Bus services have a main object at a predictable name, e.g. + # org.freedesktop.Notifications -> /org/freedesktop/Notifications + if not path: + path = '/' + name.replace('.', '/') + + conn = open_dbus_connection(bus) + introspectable = Proxy(Introspectable(path, name), conn) + xml, = introspectable.Introspect() + # print(xml) + + n_interfaces = code_from_xml(xml, path, name, output_file) + print("Written {} interface wrappers to {}".format(n_interfaces, output_file)) + +def generate_from_file(input_file, path, name, output_file): + with open(input_file, encoding='utf-8') as f: + xml = f.read() + + n_interfaces = code_from_xml(xml, path, name, output_file) + print("Written {} interface wrappers to {}".format(n_interfaces, output_file)) + +def main(): + ap = argparse.ArgumentParser( + description="Generate a simple wrapper module to call D-Bus methods.", + epilog="If you don't use --file, this will connect to D-Bus and introspect the " + "given name and path. --name and --path can also be used with --file, " + "to give defaults for the generated class." + ) + ap.add_argument('-n', '--name', + help='Bus name to introspect, required unless using file') + ap.add_argument('-p', '--path', + help='Object path to introspect. If not specified, a path matching ' + 'the name will be used, e.g. /org/freedesktop/Notifications for org.freedesktop.Notifications') + ap.add_argument('--bus', default='SESSION', + help='Bus to connect to for introspection (SESSION/SYSTEM), default SESSION') + ap.add_argument('-f', '--file', + help='XML file to use instead of D-Bus introspection') + ap.add_argument('-o', '--output', + help='Output filename') + args = ap.parse_args() + + if not (args.file or args.name): + sys.exit("Either --name or --file is required") + + # If no --output, guess a (hopefully) reasonable name. + if args.output: + output = args.output + elif args.file: + output = os.path.splitext(os.path.basename(args.file))[0] + '.py' + elif args.path and len(args.path) > 1: + output = args.path[1:].replace('/', '_') + '.py' + else: # e.g. path is '/' + output = args.name.replace('.', '_') + '.py' + + if args.file: + generate_from_file(args.file, args.path, args.name, output) + else: + generate_from_introspection(args.path, args.name, output, args.bus) + + +if __name__ == '__main__': + main() diff --git a/lib/jeepney/bus.py b/lib/jeepney/bus.py new file mode 100644 index 0000000..dfc10ee --- /dev/null +++ b/lib/jeepney/bus.py @@ -0,0 +1,62 @@ +import os +import re + +_escape_pat = re.compile(r'%([0-9A-Fa-f]{2})') +def unescape(v): + def repl(match): + n = int(match.group(1), base=16) + return chr(n) + return _escape_pat.sub(repl, v) + +def parse_addresses(s): + for addr in s.split(';'): + transport, info = addr.split(':', 1) + kv = {} + for x in info.split(','): + k, v = x.split('=', 1) + kv[k] = unescape(v) + yield (transport, kv) + +SUPPORTED_TRANSPORTS = ('unix',) + +def get_connectable_addresses(addr): + unsupported_transports = set() + found = False + for transport, kv in parse_addresses(addr): + if transport not in SUPPORTED_TRANSPORTS: + unsupported_transports.add(transport) + + elif transport == 'unix': + if 'abstract' in kv: + yield '\0' + kv['abstract'] + found = True + elif 'path' in kv: + yield kv['path'] + found = True + + if not found: + raise RuntimeError("DBus transports ({}) not supported. Supported: {}" + .format(unsupported_transports, SUPPORTED_TRANSPORTS)) + +def find_session_bus(): + addr = os.environ['DBUS_SESSION_BUS_ADDRESS'] + return next(get_connectable_addresses(addr)) + # TODO: fallbacks to X, filesystem + +def find_system_bus(): + addr = os.environ.get('DBUS_SYSTEM_BUS_ADDRESS', '') \ + or 'unix:path=/var/run/dbus/system_bus_socket' + return next(get_connectable_addresses(addr)) + +def get_bus(addr): + if addr == 'SESSION': + return find_session_bus() + elif addr == 'SYSTEM': + return find_system_bus() + else: + return next(get_connectable_addresses(addr)) + + +if __name__ == '__main__': + print('System bus at:', find_system_bus()) + print('Session bus at:', find_session_bus()) diff --git a/lib/jeepney/bus_messages.py b/lib/jeepney/bus_messages.py new file mode 100644 index 0000000..67fdf7a --- /dev/null +++ b/lib/jeepney/bus_messages.py @@ -0,0 +1,238 @@ +"""Messages for talking to the DBus daemon itself + +Generated by jeepney.bindgen and modified by hand. +""" +from .low_level import Message, MessageType, HeaderFields +from .wrappers import MessageGenerator, new_method_call + +__all__ = [ + 'DBusNameFlags', + 'DBus', + 'message_bus', + 'Monitoring', + 'Stats', + 'MatchRule', +] + +class DBusNameFlags: + allow_replacement = 1 + replace_existing = 2 + do_not_queue = 4 + +class DBus(MessageGenerator): + """Messages to talk to the message bus + """ + interface = 'org.freedesktop.DBus' + + def __init__(self, object_path='/org/freedesktop/DBus', + bus_name='org.freedesktop.DBus'): + super().__init__(object_path=object_path, bus_name=bus_name) + + def Hello(self): + return new_method_call(self, 'Hello') + + def RequestName(self, name, flags=0): + return new_method_call(self, 'RequestName', 'su', (name, flags)) + + def ReleaseName(self, name): + return new_method_call(self, 'ReleaseName', 's', (name,)) + + def StartServiceByName(self, name): + return new_method_call(self, 'StartServiceByName', 'su', + (name, 0)) + + def UpdateActivationEnvironment(self, env): + return new_method_call(self, 'UpdateActivationEnvironment', 'a{ss}', + (env,)) + + def NameHasOwner(self, name): + return new_method_call(self, 'NameHasOwner', 's', (name,)) + + def ListNames(self): + return new_method_call(self, 'ListNames') + + def ListActivatableNames(self): + return new_method_call(self, 'ListActivatableNames') + + def AddMatch(self, rule): + """*rule* can be a str or a :class:`MatchRule` instance""" + if isinstance(rule, MatchRule): + rule = rule.serialise() + return new_method_call(self, 'AddMatch', 's', (rule,)) + + def RemoveMatch(self, rule): + if isinstance(rule, MatchRule): + rule = rule.serialise() + return new_method_call(self, 'RemoveMatch', 's', (rule,)) + + def GetNameOwner(self, name): + return new_method_call(self, 'GetNameOwner', 's', (name,)) + + def ListQueuedOwners(self, name): + return new_method_call(self, 'ListQueuedOwners', 's', (name,)) + + def GetConnectionUnixUser(self, name): + return new_method_call(self, 'GetConnectionUnixUser', 's', (name,)) + + def GetConnectionUnixProcessID(self, name): + return new_method_call(self, 'GetConnectionUnixProcessID', 's', (name,)) + + def GetAdtAuditSessionData(self, name): + return new_method_call(self, 'GetAdtAuditSessionData', 's', (name,)) + + def GetConnectionSELinuxSecurityContext(self, name): + return new_method_call(self, 'GetConnectionSELinuxSecurityContext', 's', + (name,)) + + def ReloadConfig(self): + return new_method_call(self, 'ReloadConfig') + + def GetId(self): + return new_method_call(self, 'GetId') + + def GetConnectionCredentials(self, name): + return new_method_call(self, 'GetConnectionCredentials', 's', (name,)) + +message_bus = DBus() + +class Monitoring(MessageGenerator): + interface = 'org.freedesktop.DBus.Monitoring' + + def __init__(self, object_path='/org/freedesktop/DBus', + bus_name='org.freedesktop.DBus'): + super().__init__(object_path=object_path, bus_name=bus_name) + + def BecomeMonitor(self, rules): + """Convert this connection to a monitor connection (advanced)""" + return new_method_call(self, 'BecomeMonitor', 'asu', (rules, 0)) + +class Stats(MessageGenerator): + interface = 'org.freedesktop.DBus.Debug.Stats' + + def __init__(self, object_path='/org/freedesktop/DBus', + bus_name='org.freedesktop.DBus'): + super().__init__(object_path=object_path, bus_name=bus_name) + + def GetStats(self): + return new_method_call(self, 'GetStats') + + def GetConnectionStats(self, arg0): + return new_method_call(self, 'GetConnectionStats', 's', + (arg0,)) + + def GetAllMatchRules(self): + return new_method_call(self, 'GetAllMatchRules') + + +class MatchRule: + """Construct a match rule to subscribe to DBus messages. + + e.g.:: + + mr = MatchRule( + interface='org.freedesktop.DBus', + member='NameOwnerChanged', + type='signal' + ) + msg = message_bus.AddMatch(mr) + # Send this message to subscribe to the signal + """ + def __init__(self, *, type=None, sender=None, interface=None, member=None, + path=None, path_namespace=None, destination=None, + eavesdrop=False): + if isinstance(type, str): + type = MessageType[type] + self.message_type = type + fields = { + 'sender': sender, + 'interface': interface, + 'member': member, + 'path': path, + 'destination': destination, + } + self.header_fields = { + k: v for (k, v) in fields.items() if (v is not None) + } + self.path_namespace = path_namespace + self.eavesdrop = eavesdrop + self.arg_conditions = {} + + def add_arg_condition(self, argno: int, value: str, kind='string'): + """Add a condition for a particular argument + + argno: int, 0-63 + kind: 'string', 'path', 'namespace' + """ + if kind not in {'string', 'path', 'namespace'}: + raise ValueError("kind={!r}".format(kind)) + if kind == 'namespace' and argno != 0: + raise ValueError("argno must be 0 for kind='namespace'") + self.arg_conditions[argno] = (value, kind) + + def serialise(self) -> str: + """Convert to a string to use in an AddMatch call to the message bus""" + pairs = list(self.header_fields.items()) + + if self.message_type: + pairs.append(('type', self.message_type.name)) + + if self.path_namespace: + pairs.append(('path_namespace', self.path_namespace)) + + if self.eavesdrop: + pairs.append(('eavesdrop', 'true')) + + for argno, (val, kind) in self.arg_conditions.items(): + if kind == 'string': + kind = '' + pairs.append((f'arg{argno}{kind}', val)) + + # Quoting rules: single quotes ('') needed if the value contains a comma. + # A literal ' can only be represented outside single quotes, by + # backslash-escaping it. No escaping inside the quotes. + # The simplest way to handle this is to use '' around every value, and + # use '\'' (end quote, escaped ', restart quote) for literal ' . + return ','.join( + "{}='{}'".format(k, v.replace("'", r"'\''")) for (k, v) in pairs + ) + + def matches(self, msg: Message) -> bool: + """Returns True if msg matches this rule""" + h = msg.header + if (self.message_type is not None) and h.message_type != self.message_type: + return False + + for field, expected in self.header_fields.items(): + if h.fields.get(HeaderFields[field], None) != expected: + return False + + if self.path_namespace is not None: + path = h.fields.get(HeaderFields.path, '\0') + path_ns = self.path_namespace.rstrip('/') + if not ((path == path_ns) or path.startswith(path_ns + '/')): + return False + + for argno, (expected, kind) in self.arg_conditions.items(): + if argno >= len(msg.body): + return False + arg = msg.body[argno] + if not isinstance(arg, str): + return False + if kind == 'string': + if arg != expected: + return False + elif kind == 'path': + if not ( + (arg == expected) + or (expected.endswith('/') and arg.startswith(expected)) + or (arg.endswith('/') and expected.startswith(arg)) + ): + return False + elif kind == 'namespace': + if not ( + (arg == expected) + or arg.startswith(expected + '.') + ): + return False + + return True diff --git a/lib/jeepney/fds.py b/lib/jeepney/fds.py new file mode 100644 index 0000000..233c3aa --- /dev/null +++ b/lib/jeepney/fds.py @@ -0,0 +1,158 @@ +import array +import os +import socket +from warnings import warn + + +class NoFDError(RuntimeError): + """Raised by :class:`FileDescriptor` methods if it was already closed/converted + """ + pass + + +class FileDescriptor: + """A file descriptor received in a D-Bus message + + This wrapper helps ensure that the file descriptor is closed exactly once. + If you don't explicitly convert or close the FileDescriptor object, it will + close its file descriptor when it goes out of scope, and emit a + ResourceWarning. + """ + __slots__ = ('_fd',) + _CLOSED = -1 + _CONVERTED = -2 + + def __init__(self, fd): + self._fd = fd + + def __repr__(self): + detail = self._fd + if self._fd == self._CLOSED: + detail = 'closed' + elif self._fd == self._CONVERTED: + detail = 'converted' + return f"" + + def close(self): + """Close the file descriptor + + This can safely be called multiple times, but will raise RuntimeError + if called after converting it with one of the ``to_*`` methods. + + This object can also be used in a ``with`` block, to close it on + leaving the block. + """ + if self._fd == self._CLOSED: + pass + elif self._fd == self._CONVERTED: + raise NoFDError("Can't close FileDescriptor after converting it") + else: + self._fd, fd = self._CLOSED, self._fd + os.close(fd) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def __del__(self): + if self._fd >= 0: + warn( + f'FileDescriptor ({self._fd}) was neither closed nor converted', + ResourceWarning, stacklevel=2, source=self + ) + self.close() + + def _check(self): + if self._fd < 0: + detail = 'closed' if self._fd == self._CLOSED else 'converted' + raise NoFDError(f'FileDescriptor object was already {detail}') + + def fileno(self): + """Get the integer file descriptor + + This does not change the state of the :class:`FileDescriptor` object, + unlike the ``to_*`` methods. + """ + self._check() + return self._fd + + def to_raw_fd(self): + """Convert to the low-level integer file descriptor:: + + raw_fd = fd.to_raw_fd() + os.write(raw_fd, b'xyz') + os.close(raw_fd) + + The :class:`FileDescriptor` can't be used after calling this. The caller + is responsible for closing the file descriptor. + """ + self._check() + self._fd, fd = self._CONVERTED, self._fd + return fd + + def to_file(self, mode, buffering=-1, encoding=None, errors=None, newline=None): + """Convert to a Python file object:: + + with fd.to_file('w') as f: + f.write('xyz') + + The arguments are the same as for the builtin :func:`open` function. + + The :class:`FileDescriptor` can't be used after calling this. Closing + the file object will also close the file descriptor. + """ + self._check() + f = open( + self._fd, mode, buffering=buffering, + encoding=encoding, errors=errors, newline=newline + ) + self._fd = self._CONVERTED + return f + + def to_socket(self): + """Convert to a socket object + + This returns a standard library :func:`socket.socket` object:: + + with fd.to_socket() as sock: + b = sock.sendall(b'xyz') + + The wrapper object can't be used after calling this. Closing the socket + object will also close the file descriptor. + """ + from socket import socket + + self._check() + s = socket(fileno=self._fd) + self._fd = self._CONVERTED + return s + + @classmethod + def from_ancdata(cls, ancdata) -> ['FileDescriptor']: + """Make a list of FileDescriptor from received file descriptors + + ancdata is a list of ancillary data tuples as returned by socket.recvmsg() + """ + fds = array.array("i") # Array of ints + for cmsg_level, cmsg_type, data in ancdata: + if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: + # Append data, ignoring any truncated integers at the end. + fds.frombytes(data[:len(data) - (len(data) % fds.itemsize)]) + return [cls(i) for i in fds] + + +_fds_buf_size_cache = None + +def fds_buf_size(): + # If there may be file descriptors, we try to read 1 message at a time. + # The reference implementation of D-Bus defaults to allowing 16 FDs per + # message, and the Linux kernel currently allows 253 FDs per sendmsg() + # call. So hopefully allowing 256 FDs per recvmsg() will always suffice. + global _fds_buf_size_cache + if _fds_buf_size_cache is None: + maxfds = 256 + fd_size = array.array('i').itemsize + _fds_buf_size_cache = socket.CMSG_SPACE(maxfds * fd_size) + return _fds_buf_size_cache diff --git a/lib/jeepney/io/__init__.py b/lib/jeepney/io/__init__.py new file mode 100644 index 0000000..d346b6c --- /dev/null +++ b/lib/jeepney/io/__init__.py @@ -0,0 +1 @@ +from .common import RouterClosed diff --git a/lib/jeepney/io/__pycache__/__init__.cpython-314.pyc b/lib/jeepney/io/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84b23129649f41411dbd6650d17ef95fb8bdc705 GIT binary patch literal 203 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 PE;L^}_zw&hKIH!ouAOH< literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/__pycache__/blocking.cpython-314.pyc b/lib/jeepney/io/__pycache__/blocking.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b3de04f2ccecd51f4c19f6a816ba254de9bbf6e GIT binary patch literal 19190 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 ANdN!< literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/__pycache__/common.cpython-314.pyc b/lib/jeepney/io/__pycache__/common.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be2e62377041c94fd809aad6727b37295283ef6a GIT binary patch literal 5989 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 HBgFp*^T7WN literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/__pycache__/trio.cpython-314.pyc b/lib/jeepney/io/__pycache__/trio.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1a2538e2a77d4e4458a989ddee15841d93974fe GIT binary patch literal 25606 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 literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/asyncio.py b/lib/jeepney/io/asyncio.py new file mode 100644 index 0000000..2c6ade6 --- /dev/null +++ b/lib/jeepney/io/asyncio.py @@ -0,0 +1,233 @@ +import asyncio +import contextlib +from itertools import count +from typing import Optional + +from jeepney.auth import Authenticator, BEGIN +from jeepney.bus import get_bus +from jeepney import Message, MessageType, Parser +from jeepney.wrappers import ProxyBase, unwrap_msg +from jeepney.bus_messages import message_bus +from .common import ( + MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable, +) + + +class DBusConnection: + """A plain D-Bus connection with no matching of replies. + + This doesn't run any separate tasks: sending and receiving are done in + the task that calls those methods. It's suitable for implementing servers: + several worker tasks can receive requests and send replies. + For a typical client pattern, see :class:`DBusRouter`. + """ + def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + self.reader = reader + self.writer = writer + self.parser = Parser() + self.outgoing_serial = count(start=1) + self.unique_name = None + self.send_lock = asyncio.Lock() + + async def send(self, message: Message, *, serial=None): + """Serialise and send a :class:`~.Message` object""" + async with self.send_lock: + if serial is None: + serial = next(self.outgoing_serial) + self.writer.write(message.serialise(serial)) + await self.writer.drain() + + async def receive(self) -> Message: + """Return the next available message from the connection""" + while True: + msg = self.parser.get_next_message() + if msg is not None: + return msg + + b = await self.reader.read(4096) + if not b: + raise EOFError + self.parser.add_data(b) + + async def close(self): + """Close the D-Bus connection""" + self.writer.close() + await self.writer.wait_closed() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + + +async def open_dbus_connection(bus='SESSION'): + """Open a plain D-Bus connection + + :return: :class:`DBusConnection` + """ + bus_addr = get_bus(bus) + reader, writer = await asyncio.open_unix_connection(bus_addr) + + # Authentication flow + authr = Authenticator() + for req_data in authr: + writer.write(req_data) + await writer.drain() + b = await reader.read(1024) + if not b: + raise EOFError("Socket closed before authentication") + authr.feed(b) + + writer.write(BEGIN) + await writer.drain() + # Authentication finished + + conn = DBusConnection(reader, writer) + + # Say *Hello* to the message bus - this must be the first message, and the + # reply gives us our unique name. + async with DBusRouter(conn) as router: + reply_body = await asyncio.wait_for(Proxy(message_bus, router).Hello(), 10) + conn.unique_name = reply_body[0] + + return conn + +class DBusRouter: + """A 'client' D-Bus connection which can wait for a specific reply. + + This runs a background receiver task, and makes it possible to send a + request and wait for the relevant reply. + """ + _nursery_mgr = None + _send_cancel_scope = None + _rcv_cancel_scope = None + + def __init__(self, conn: DBusConnection): + self._conn = conn + self._replies = ReplyMatcher() + self._filters = MessageFilters() + self._rcv_task = asyncio.create_task(self._receiver()) + + @property + def unique_name(self): + return self._conn.unique_name + + async def send(self, message, *, serial=None): + """Send a message, don't wait for a reply""" + await self._conn.send(message, serial=serial) + + async def send_and_get_reply(self, message) -> Message: + """Send a method call message and wait for the reply + + Returns the reply message (method return or error message type). + """ + check_replyable(message) + if self._rcv_task.done(): + raise RouterClosed("This DBusRouter has stopped") + + serial = next(self._conn.outgoing_serial) + + with self._replies.catch(serial, asyncio.Future()) as reply_fut: + await self.send(message, serial=serial) + return (await reply_fut) + + def filter(self, rule, *, queue: Optional[asyncio.Queue] =None, bufsize=1): + """Create a filter for incoming messages + + Usage:: + + with router.filter(rule) as queue: + matching_msg = await queue.get() + + :param MatchRule rule: Catch messages matching this rule + :param asyncio.Queue queue: Send matching messages here + :param int bufsize: If no queue is passed in, create one with this size + """ + return FilterHandle(self._filters, rule, queue or asyncio.Queue(bufsize)) + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self._rcv_task.done(): + self._rcv_task.result() # Throw exception if receive task failed + else: + self._rcv_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._rcv_task + return False + + # Code to run in receiver task ------------------------------------ + + def _dispatch(self, msg: Message): + """Handle one received message""" + if self._replies.dispatch(msg): + return + + for filter in list(self._filters.matches(msg)): + try: + filter.queue.put_nowait(msg) + except asyncio.QueueFull: + pass + + async def _receiver(self): + """Receiver loop - runs in a separate task""" + try: + while True: + msg = await self._conn.receive() + self._dispatch(msg) + finally: + # Send errors to any tasks still waiting for a message. + self._replies.drop_all() + +class open_dbus_router: + """Open a D-Bus 'router' to send and receive messages + + Use as an async context manager:: + + async with open_dbus_router() as router: + ... + """ + conn = None + req_ctx = None + + def __init__(self, bus='SESSION'): + self.bus = bus + + async def __aenter__(self): + self.conn = await open_dbus_connection(self.bus) + self.req_ctx = DBusRouter(self.conn) + return await self.req_ctx.__aenter__() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.req_ctx.__aexit__(exc_type, exc_val, exc_tb) + await self.conn.close() + + +class Proxy(ProxyBase): + """An asyncio proxy for calling D-Bus methods + + You can call methods on the proxy object, such as ``await bus_proxy.Hello()`` + to make a method call over D-Bus and wait for a reply. It will either + return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. + The methods available are defined by the message generator you wrap. + + :param msggen: A message generator object. + :param ~asyncio.DBusRouter router: Router to send and receive messages. + """ + def __init__(self, msggen, router): + super().__init__(msggen) + self._router = router + + def __repr__(self): + return 'Proxy({}, {})'.format(self._msggen, self._router) + + def _method_call(self, make_msg): + async def inner(*args, **kwargs): + msg = make_msg(*args, **kwargs) + assert msg.header.message_type is MessageType.method_call + reply = await self._router.send_and_get_reply(msg) + return unwrap_msg(reply) + + return inner diff --git a/lib/jeepney/io/blocking.py b/lib/jeepney/io/blocking.py new file mode 100644 index 0000000..d2d9b54 --- /dev/null +++ b/lib/jeepney/io/blocking.py @@ -0,0 +1,337 @@ +"""Synchronous IO wrappers around jeepney +""" +import array +from collections import deque +from errno import ECONNRESET +import functools +from itertools import count +import os +from selectors import DefaultSelector, EVENT_READ +import socket +import time +from typing import Optional + +from jeepney import Parser, Message, MessageType, HeaderFields +from jeepney.auth import Authenticator, BEGIN +from jeepney.bus import get_bus +from jeepney.fds import FileDescriptor, fds_buf_size +from jeepney.wrappers import ProxyBase, unwrap_msg +from jeepney.bus_messages import message_bus +from .common import MessageFilters, FilterHandle, check_replyable + +__all__ = [ + 'open_dbus_connection', + 'DBusConnection', + 'Proxy', +] + + +class _Future: + def __init__(self): + self._result = None + + def done(self): + return bool(self._result) + + def set_exception(self, exception): + self._result = (False, exception) + + def set_result(self, result): + self._result = (True, result) + + def result(self): + success, value = self._result + if success: + return value + raise value + + +def timeout_to_deadline(timeout): + if timeout is not None: + return time.monotonic() + timeout + return None + +def deadline_to_timeout(deadline): + if deadline is not None: + return max(deadline - time.monotonic(), 0.) + return None + + +class DBusConnectionBase: + """Connection machinery shared by this module and threading""" + def __init__(self, sock: socket.socket, enable_fds=False): + self.sock = sock + self.enable_fds = enable_fds + self.parser = Parser() + self.outgoing_serial = count(start=1) + self.selector = DefaultSelector() + self.select_key = self.selector.register(sock, EVENT_READ) + self.unique_name = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def _serialise(self, message: Message, serial) -> (bytes, Optional[array.array]): + if serial is None: + serial = next(self.outgoing_serial) + fds = array.array('i') if self.enable_fds else None + data = message.serialise(serial=serial, fds=fds) + return data, fds + + def _send_with_fds(self, data, fds): + bytes_sent = self.sock.sendmsg( + [data], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)] + ) + # If sendmsg succeeds, I think ancillary data has been sent atomically? + # So now we just need to send any leftover normal data. + if bytes_sent < len(data): + self.sock.sendall(data[bytes_sent:]) + + def _receive(self, deadline): + while True: + msg = self.parser.get_next_message() + if msg is not None: + return msg + + b, fds = self._read_some_data(timeout=deadline_to_timeout(deadline)) + self.parser.add_data(b, fds=fds) + + def _read_some_data(self, timeout=None): + for key, ev in self.selector.select(timeout): + if key == self.select_key: + if self.enable_fds: + return self._read_with_fds() + else: + return unwrap_read(self.sock.recv(4096)), [] + + raise TimeoutError + + def _read_with_fds(self): + nbytes = self.parser.bytes_desired() + data, ancdata, flags, _ = self.sock.recvmsg(nbytes, fds_buf_size()) + if flags & getattr(socket, 'MSG_CTRUNC', 0): + self.close() + raise RuntimeError("Unable to receive all file descriptors") + return unwrap_read(data), FileDescriptor.from_ancdata(ancdata) + + def close(self): + """Close the connection""" + self.selector.close() + self.sock.close() + + +class DBusConnection(DBusConnectionBase): + def __init__(self, sock: socket.socket, enable_fds=False): + super().__init__(sock, enable_fds) + + # Message routing machinery + self._filters = MessageFilters() + + # Say Hello, get our unique name + self.bus_proxy = Proxy(message_bus, self) + hello_reply = self.bus_proxy.Hello() + self.unique_name = hello_reply[0] + + def send(self, message: Message, serial=None): + """Serialise and send a :class:`~.Message` object""" + data, fds = self._serialise(message, serial) + if fds: + self._send_with_fds(data, fds) + else: + self.sock.sendall(data) + + send_message = send # Backwards compatibility + + def receive(self, *, timeout=None) -> Message: + """Return the next available message from the connection + + If the data is ready, this will return immediately, even if timeout<=0. + Otherwise, it will wait for up to timeout seconds, or indefinitely if + timeout is None. If no message comes in time, it raises TimeoutError. + """ + return self._receive(timeout_to_deadline(timeout)) + + def recv_messages(self, *, timeout=None): + """Receive one message and apply filters + + See :meth:`filter`. Returns nothing. + """ + msg = self.receive(timeout=timeout) + for filter in self._filters.matches(msg): + filter.queue.append(msg) + + def send_and_get_reply(self, message, *, timeout=None): + """Send a message, wait for the reply and return it + + Filters are applied to other messages received before the reply - + see :meth:`add_filter`. + """ + check_replyable(message) + deadline = timeout_to_deadline(timeout) + + serial = next(self.outgoing_serial) + self.send_message(message, serial=serial) + while True: + msg_in = self.receive(timeout=deadline_to_timeout(deadline)) + reply_to = msg_in.header.fields.get(HeaderFields.reply_serial, -1) + if reply_to == serial: + return msg_in + + # Not the reply + for filter in self._filters.matches(msg_in): + filter.queue.append(msg_in) + + def filter(self, rule, *, queue: Optional[deque] =None, bufsize=1): + """Create a filter for incoming messages + + Usage:: + + with conn.filter(rule) as matches: + # matches is a deque containing matched messages + matching_msg = conn.recv_until_filtered(matches) + + :param jeepney.MatchRule rule: Catch messages matching this rule + :param collections.deque queue: Matched messages will be added to this + :param int bufsize: If no deque is passed in, create one with this size + """ + if queue is None: + queue = deque(maxlen=bufsize) + return FilterHandle(self._filters, rule, queue) + + def recv_until_filtered(self, queue, *, timeout=None) -> Message: + """Process incoming messages until one is filtered into queue + + Pops the message from queue and returns it, or raises TimeoutError if + the optional timeout expires. Without a timeout, this is equivalent to:: + + while len(queue) == 0: + conn.recv_messages() + return queue.popleft() + + In the other I/O modules, there is no need for this, because messages + are placed in queues by a separate task. + + :param collections.deque queue: A deque connected by :meth:`filter` + :param float timeout: Maximum time to wait in seconds + """ + deadline = timeout_to_deadline(timeout) + while len(queue) == 0: + self.recv_messages(timeout=deadline_to_timeout(deadline)) + return queue.popleft() + + +class Proxy(ProxyBase): + """A blocking proxy for calling D-Bus methods + + You can call methods on the proxy object, such as ``bus_proxy.Hello()`` + to make a method call over D-Bus and wait for a reply. It will either + return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. + The methods available are defined by the message generator you wrap. + + You can set a time limit on a call by passing ``_timeout=`` in the method + call, or set a default when creating the proxy. The ``_timeout`` argument + is not passed to the message generator. + All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it + expires before a reply arrives. + + :param msggen: A message generator object + :param ~blocking.DBusConnection connection: Connection to send and receive messages + :param float timeout: Default seconds to wait for a reply, or None for no limit + """ + def __init__(self, msggen, connection, *, timeout=None): + super().__init__(msggen) + self._connection = connection + self._timeout = timeout + + def __repr__(self): + extra = '' if (self._timeout is None) else f', timeout={self._timeout}' + return f"Proxy({self._msggen}, {self._connection}{extra})" + + def _method_call(self, make_msg): + @functools.wraps(make_msg) + def inner(*args, **kwargs): + timeout = kwargs.pop('_timeout', self._timeout) + msg = make_msg(*args, **kwargs) + assert msg.header.message_type is MessageType.method_call + return unwrap_msg(self._connection.send_and_get_reply( + msg, timeout=timeout + )) + + return inner + + +def unwrap_read(b): + """Raise ConnectionResetError from an empty read. + + Sometimes the socket raises an error itself, sometimes it gives no data. + I haven't worked out when it behaves each way. + """ + if not b: + raise ConnectionResetError(ECONNRESET, os.strerror(ECONNRESET)) + return b + + +def prep_socket(addr, enable_fds=False, timeout=2.0) -> socket.socket: + """Create a socket and authenticate ready to send D-Bus messages""" + sock = socket.socket(family=socket.AF_UNIX) + + # To impose the overall auth timeout, we'll update the timeout on the socket + # before each send/receive. This is ugly, but we can't use the socket for + # anything else until this has succeeded, so this should be safe. + deadline = timeout_to_deadline(timeout) + def with_sock_deadline(meth, *args): + sock.settimeout(deadline_to_timeout(deadline)) + return meth(*args) + + try: + with_sock_deadline(sock.connect, addr) + authr = Authenticator(enable_fds=enable_fds, inc_null_byte=False) + if hasattr(socket, 'SCM_CREDS'): + # BSD: send credentials message to authenticate (kernel fills in data) + sock.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_CREDS, bytes(512))]) + else: + # Linux: no ancillary data needed, bus checks with SO_PEERCRED + sock.send(b'\0') + for req_data in authr: + with_sock_deadline(sock.sendall, req_data) + authr.feed(unwrap_read(with_sock_deadline(sock.recv, 1024))) + with_sock_deadline(sock.sendall, BEGIN) + except socket.timeout as e: + sock.close() + raise TimeoutError(f"Did not authenticate in {timeout} seconds") from e + except: + sock.close() + raise + + sock.settimeout(None) # Put the socket back in blocking mode + return sock + + +def open_dbus_connection( + bus='SESSION', enable_fds=False, auth_timeout=1., +) -> DBusConnection: + """Connect to a D-Bus message bus + + Pass ``enable_fds=True`` to allow sending & receiving file descriptors. + An error will be raised if the bus does not allow this. For simplicity, + it's advisable to leave this disabled unless you need it. + + D-Bus has an authentication step before sending or receiving messages. + This takes < 1 ms in normal operation, but there is a timeout so that client + code won't get stuck if the server doesn't reply. *auth_timeout* configures + this timeout in seconds. + """ + bus_addr = get_bus(bus) + sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout) + + conn = DBusConnection(sock, enable_fds) + return conn + + +if __name__ == '__main__': + conn = open_dbus_connection() + print("Unique name:", conn.unique_name) diff --git a/lib/jeepney/io/common.py b/lib/jeepney/io/common.py new file mode 100644 index 0000000..f74d460 --- /dev/null +++ b/lib/jeepney/io/common.py @@ -0,0 +1,88 @@ +from contextlib import contextmanager +from itertools import count + +from jeepney import HeaderFields, Message, MessageFlag, MessageType + +class MessageFilters: + def __init__(self): + self.filters = {} + self.filter_ids = count() + + def matches(self, message): + for handle in self.filters.values(): + if handle.rule.matches(message): + yield handle + + +class FilterHandle: + def __init__(self, filters: MessageFilters, rule, queue): + self._filters = filters + self._filter_id = next(filters.filter_ids) + self.rule = rule + self.queue = queue + + self._filters.filters[self._filter_id] = self + + def close(self): + del self._filters.filters[self._filter_id] + + def __enter__(self): + return self.queue + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + +class ReplyMatcher: + def __init__(self): + self._futures = {} + + @contextmanager + def catch(self, serial, future): + """Context manager to capture a reply for the given serial number""" + self._futures[serial] = future + + try: + yield future + finally: + del self._futures[serial] + + def dispatch(self, msg): + """Dispatch an incoming message which may be a reply + + Returns True if a task was waiting for it, otherwise False. + """ + rep_serial = msg.header.fields.get(HeaderFields.reply_serial, -1) + if rep_serial in self._futures: + self._futures[rep_serial].set_result(msg) + return True + else: + return False + + def drop_all(self, exc: Exception = None): + """Throw an error in any task still waiting for a reply""" + if exc is None: + exc = RouterClosed("D-Bus router closed before reply arrived") + futures, self._futures = self._futures, {} + for fut in futures.values(): + fut.set_exception(exc) + + +class RouterClosed(Exception): + """Raised in tasks waiting for a reply when the router is closed + + This will also be raised if the receiver task crashes, so tasks are not + stuck waiting for a reply that can never come. The router object will not + be usable after this is raised. + """ + pass + + +def check_replyable(msg: Message): + """Raise an error if we wouldn't expect a reply for msg""" + if msg.header.message_type != MessageType.method_call: + raise TypeError("Only method call messages have replies " + f"(not {msg.header.message_type})") + if MessageFlag.no_reply_expected & msg.header.flags: + raise ValueError("This message has the no_reply_expected flag set") diff --git a/lib/jeepney/io/tests/__init__.py b/lib/jeepney/io/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/jeepney/io/tests/__pycache__/__init__.cpython-314.pyc b/lib/jeepney/io/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b0c690115f137596e84b21423b4d9a1ad5e339c GIT binary patch literal 163 zcmdPq+klGC}lX5CH>>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%R#>+w?Mxj zvp}~bu_!&YM7K1(Brnl4$4EaXGfBUovLquvPd_U)wIDCGQa>|aza+J|q*y;bJ~J<~ mBtBlRpz;=nO>TZlX-=wL5i8I@kd?(C#wTV*M#ds$APWFAuO}z~ literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/tests/__pycache__/conftest.cpython-314.pyc b/lib/jeepney/io/tests/__pycache__/conftest.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9c3d808609f3b0a625d698b853960d905ff7edc GIT binary patch literal 4950 zcmeHLTWnLw8J=_XId*Jv!w$xFoMaK0EKXPgyCN5DNC@`ArmhpEk(y%J@kwIvt#ghy z1YV-;Lt%wfP^FScRHSO9Lc0(2p;8|Dz(wlwW-)7bN~K8Khi;$Jrd76-hyHWyW1A2z zQq`AL=Oi=#nfd3RIrCq>f81%anh})0_WmUCE1i|8y0GeHz!G^Ul( zY>?Ghz2!6xS}w>31WgEtniw!?rhueL0a=p+X3ebg^Fd3XPOA%8HEY18*;>)}xRA$K zs@k@pp7Kjra8zim{ z>?uopkuS?mnGC)u$)G0Kp7NR@i_uqYCy*g;9^_`Zut=Yt^4?m}BzKm1ZxW$d=y9QM zqd8_mJi>%QO-U)sPt_TcLN*NgC)!^goP{rTp#DDjVB#VDa4YA zodXMsWTV;~F^!!Z8w-AGbR1YXYL{ju9gQUwn5T|TSHBA(sz&<3o6kTgeas@-i^jN6 zS@E@u#t~%E1PU2(p@t#E*SwU;DbeT)RgA>wLFj|Bs!#W#m&w#4Q*{QVUTp|(JFDDb z-JIf4s-UTgF@#ye40kYVW`fyC{bA0SLk%aiySj!!ovj#mE1Gf`z2J!a1apKjj6BQ2 z+3qn$t1UQA`NUh{{KEo$j5%PULI>5!4)dn=8Zuxd(Qv-1tkZu%=+$O;`d;kbs9L^>eBAN8GLf+cuT>idf26nFw_-nZAu(oA#YTJ4?!1w_ zp8Rq88dq$x-#C8#c%kXwTGPQ#&lmhB*Ze0RH=QgxoHs6BzgTb_SaTfs)K&2Ht@-*M zJNoZA@{Yh0_rcpe1$TGe-F^2&(dD@nSPraA=3RbDpISb(GI;ZYC+$b>4y`t=;#K8Q z`|#3G(dE4rTn?^G-yAKv+P5sob9l|&yEGG~gCPDh zclT4(3cG%Q2Dzbw!o!1>p=5^BF3XiDwDor#`JVCbvaDKH>MF6Qx7JVu=4#J zoZBc3G_p2FZ6lGMCE~bF5n(f43uVIkq_bXW6~t zTjdLK|6{rTUh@;HV~H68W-~9#e*g6E#V9lo>);s_^dA3O-=MVM`wK2Dwhit;{+E4V#_dTEs zoA98`3EdBREkhRTLs5dBhc?TQM}D|Rg8t{?^#5W1n+W^pyk0U!-U1LQNl(S zFwLNRyD&{Hu!~l7IUU$By{zF@T2VB|UZ@Bk%fuImNy|iL;I4$*^*#y^xP$W0xK&1U z{1zQ!W(X52#e4(J^?+YOhBi9WF!vo%_$UZbe+$)rkHRf^N3iJj-kMyVT#5ely8xB| zrgxhQ?qhlPvDLPs%YAEPd1U4A&0x{n_Dk_+;?Jd}!7TxK-rW$(h?~~|y7?ZnssTXf zTTcF=#k1bxdI5~BFbU*7RVji!aI_Eg3lIA(!&Vu0LI?KKidxZ&I0#r-JrR5gc<78; z5bXWmfS?6H(D^1H=%XO`_H!WExg7*C z9aB%#D|gu{o8hYZINigxQk%$sr~U|Az9q6@c&ctqe=c}+Vof<}Zg`wZ6RBuy`tUnP zE^_mou~8qdXTi7)`iU@;%&Bv_^Y~c8$D@!>?Slc8UbMX4QbuIg;8zLV%OX*@2SPHd zDQQi`r-3I(nJH1qi<-eCWc#&TL3ZaMCu%H8<{wE{q-)JrZDsPRnG&WerfUq4w))F+ zpV+o!WcI)m`1@RTKU1mqe{3K0GfVFcI)(f7W+3l7ErXr%{e3dfz9u5kJ?V~5wSl{# zA|j;UkSr2YIsY!@m3qXWhfoS7G|4X|{msTT7qGCBT5d~2iD@Qr0e&@6^ej;K0{ye^ zpiPj2O6Mc>r84ncQW?Nw&_^#+Y8I*uj$xQTq4qzb`Y(}ni{}`23(-nKOpsYafpy08 wL(j*aM@+-t*u6~4CWqL@4N7hFAy%pkIV(dzZ5*#15*dd+>+dX3I1G#a2Y$1um;e9( literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc b/lib/jeepney/io/tests/__pycache__/test_asyncio.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88062b704dbd9e48d84611c927d7c2ec00eb6e86 GIT binary patch literal 5692 zcmcIoU2Gf25#GHY^2p<#C{dI|Nt7sAHZ57SV>`0!q_+MNDRC6`Y!F2unV`k9M4KX+ zyOV9{C5rn{yD4Djr?r6usL-aU+@fd-6h(smppyL1N6}J1=4`Y9nxb%@B-@3Xzz?0> zBPmKzoD@Zuyt&!k+1P0G5MK7AIRI+}@T6S^glf`>g58ZCLpQ>Rm7iF9XXTDNYPhf+i4g-h)Mau0U%)9T&x)2r3p zIalGo?CSm?o_Dxkw?1M~#o}0g;eWGd#SIS*dqqMO#S98Oh7HqQQ~c0F$u3}2 z0>aexoc_6?oU;@DA2~bUZ_7KXUblhOxk5%`i9S|)VaTQ0o*MiP?h@W*f%%c?2>(+N znD=ov_iHmSA7UWCGxc~Z#Nar`0%Ik-ogvg|fzD0l#lw`;ynYMJ)@>gF7d8qQQi0Fl z5u!g@!o}cQY=C<+07{s6@s<+7MN>S*17Hj1a~T+$0ld}C1su?Y3$cV2J(E^+w<@RN(HLBSC{abuB&T)m zbUHpAWT}dDO9rN-@ki%rw@RjOYicq)z)L;gi<2n3L23_N>dQcge($>c?+Np5E3SRB zLwDV^Id|Z;JCGL~IiWc#G|vTBgpT)P4_T(t^awF_)9k>S3E3*H9=>w;hI>`?Epxux zqHo;{U7x8=nDf$+o?83|+~ei`SO?rDZ4%8;ZQcUr%e5IP)24)A&vBU8!IlzXiW&ng z;qp+BVI!=>c7yzeMqm!}mIyYpUCT!>ZWxZsrEk$C= z`nUD+uEI*h!ov0~!=ZYLSX2Q!{0MB0c*K?xVax26E4N!1fx;DA#G$$|0)h*s2~#Nr z1{=lU%5BD!d&j0#sAcmJ{uJJfStC`&S;EytZsQKP$%Sh->P3#26e$ufnaWUe(~22W z#1yVGXv;IvDCO65Ar&vO=o0~0N|Dt z#RV^-n?=MqVoVXQ?f!- zs9TLUi%zQJDnTNOB1Ie#%l1Hd4tk%3OML)@0*Tf6ws<45Vr?aW(L3A!X+>S$?z!u% z%Q;)J&X&9GhMaq6*1a?DY`$x+$=O@8_ST1@$!(f_VaG?ynRUgNJ}$YR3H@VJXlG{4KKxGPf!*;XRak#r|EE zTLBBSkHL(kLV$;Yps)=HKIt@-K|WLlU?kXXDa~l4loExv0l*?KeGz8-XoLhQT27U+ z_OH%$DWq)!lttLsnqYmMQ(pNgkj}pi$UbR)3LG~8G$125ei*Kf|2DHe-e-WxlR@YB zuptF2Jllj$qV0mgrso8WZJbm6C{YGkM)oey^QUftLnY)0aPE&K;7b7Ye%*G`_*^ld zC}~9@%Gb?`JSHd3gQJfn)2gg^ps_6B*Nr3*gw97}X8_(xC#IJ`@FWp_2MBTThgRfn zn-8v59LR|WX8YF|p7-6gS6&_c-srnsIbSI23+3#4KeF$=>utR8%JrG+Ge4NgSJz!n zzL9(@m2zTz#+G{oqLM|?nXynD_Y=c`keeiJ?`COTWDYj z-&EDR-?DH3_l9f>2RXubi_pHv;og11BF_=tBEZ%t?E0m#&v1E&hZw38^B3X4>)oqY1(l0=X6dVVrHtjSZMGIf0ltCDxI}A%Gen`;G zg~U@Kd5Uq&s4C{7`glmHMCLy&P~5uTY$ z$000zLm7oOk|3)e16gA+#&?k84zhm2nsRJqmaV*YVU=zC3+Mkkx8n}7-$9;#puK-Z zU5^+BGwTS*zf1_XLL}kE@4ooji#g7l<-B>$YE-JTTy@@7zbe!(?-*EP5$}C~7~ZtT z^A2X#y6!+WZ{FULckRqOf)CA{jhPi7Ofjw;_TI+c6r_5q5mUEDxcdUePr+ha gBs?Vv&wYW%_4gfS+;HFRfv+$RJ4NhMM&RxK17o*-@&Et; literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc b/lib/jeepney/io/tests/__pycache__/test_blocking.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0495445d617fdb3f24714fbdb94d2e8bb4714bff GIT binary patch literal 5083 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 A_y7O^ literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc b/lib/jeepney/io/tests/__pycache__/test_threading.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ba2ab906dfa2d050f51a49e1a2aefab0dfcd4c6 GIT binary patch literal 5058 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 ONat6L&?ujUYx^JGA?9oV literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/tests/__pycache__/utils.cpython-314.pyc b/lib/jeepney/io/tests/__pycache__/utils.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..493eec80b28de44266abac04547ae152cf72b386 GIT binary patch literal 332 zcmdPq+klGW~({V-N=hSfPy10YF9rLmWd8qbEZNLoA~rLo7o83rrb;4q!*H zVXPn~6GjLdMwhSzF)A}?GQR`~``waoaS9EN4|WX>_Vo9Q2eBPpT!MgHP1ajX`Ng+b zlJfI&Zn3B4m1P#?=iOpXPc6A6kdauH8eg1RT%4Jo7oSvGyprKF$h2Fw`X#vq`URN< zx+RH4K$W_s=_PrIra4CXIhjfN1(hWk`FZ+Tsi_5dsg?Sf`T8ZP#U;i1r6rj;#d-ym zw>WHa^HWN5QtgU(fTn@mU91QsJ}@&fGTvt3d?+kFgK0k3Os@HSGx;v4J6@J>x-9H` Ki9w(U6o>#7a!-c< literal 0 HcmV?d00001 diff --git a/lib/jeepney/io/tests/conftest.py b/lib/jeepney/io/tests/conftest.py new file mode 100644 index 0000000..1087467 --- /dev/null +++ b/lib/jeepney/io/tests/conftest.py @@ -0,0 +1,81 @@ +from tempfile import TemporaryFile +import threading + +import pytest + +from jeepney import ( + DBusAddress, HeaderFields, message_bus, MessageType, new_error, + new_method_return, +) +from jeepney.io.threading import open_dbus_connection, DBusRouter, Proxy + +@pytest.fixture() +def respond_with_fd(): + name = "io.gitlab.takluyver.jeepney.tests.respond_with_fd" + addr = DBusAddress(bus_name=name, object_path='/') + + with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: + with DBusRouter(conn) as router: + status, = Proxy(message_bus, router).RequestName(name) + assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER + + def _reply_once(): + while True: + msg = conn.receive() + if msg.header.message_type is MessageType.method_call: + if msg.header.fields[HeaderFields.member] == 'GetFD': + with TemporaryFile('w+') as tf: + tf.write('readme') + tf.seek(0) + rep = new_method_return(msg, 'h', (tf,)) + conn.send(rep) + return + else: + conn.send(new_error(msg, 'NoMethod')) + + reply_thread = threading.Thread(target=_reply_once, daemon=True) + reply_thread.start() + yield addr + + reply_thread.join() + + +@pytest.fixture() +def read_from_fd(): + name = "io.gitlab.takluyver.jeepney.tests.read_from_fd" + addr = DBusAddress(bus_name=name, object_path='/') + + with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: + with DBusRouter(conn) as router: + status, = Proxy(message_bus, router).RequestName(name) + assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER + + def _reply_once(): + while True: + msg = conn.receive() + if msg.header.message_type is MessageType.method_call: + if msg.header.fields[HeaderFields.member] == 'ReadFD': + with msg.body[0].to_file('rb') as f: + f.seek(0) + b = f.read() + conn.send(new_method_return(msg, 'ay', (b,))) + return + else: + conn.send(new_error(msg, 'NoMethod')) + + reply_thread = threading.Thread(target=_reply_once, daemon=True) + reply_thread.start() + yield addr + + reply_thread.join() + + +@pytest.fixture() +def temp_file_and_contents(): + data = b'abc123' + with TemporaryFile('w+b') as tf: + tf.write(data) + tf.flush() + tf.seek(0) + yield tf, data + diff --git a/lib/jeepney/io/tests/test_asyncio.py b/lib/jeepney/io/tests/test_asyncio.py new file mode 100644 index 0000000..c738105 --- /dev/null +++ b/lib/jeepney/io/tests/test_asyncio.py @@ -0,0 +1,95 @@ +import asyncio +import sys + +if sys.version_info >= (3, 11): + from asyncio import timeout +else: + from async_timeout import timeout +import pytest +import pytest_asyncio + +from jeepney import DBusAddress, new_method_call +from jeepney.bus_messages import message_bus, MatchRule +from jeepney.io.asyncio import ( + open_dbus_connection, open_dbus_router, Proxy +) +from .utils import have_session_bus + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.skipif( + not have_session_bus, reason="Tests require DBus session bus" + ), +] + +bus_peer = DBusAddress( + bus_name='org.freedesktop.DBus', + object_path='/org/freedesktop/DBus', + interface='org.freedesktop.DBus.Peer' +) + + +@pytest_asyncio.fixture() +async def connection(): + async with (await open_dbus_connection(bus='SESSION')) as conn: + yield conn + +async def test_connect(connection): + assert connection.unique_name.startswith(':') + +@pytest_asyncio.fixture() +async def router(): + async with open_dbus_router(bus='SESSION') as router: + yield router + +async def test_send_and_get_reply(router): + ping_call = new_method_call(bus_peer, 'Ping') + reply = await asyncio.wait_for( + router.send_and_get_reply(ping_call), timeout=5 + ) + assert reply.body == () + +async def test_proxy(router): + proxy = Proxy(message_bus, router) + name = "io.gitlab.takluyver.jeepney.examples.Server" + res = await proxy.RequestName(name) + assert res in {(1,), (2,)} # 1: got the name, 2: queued + + has_owner, = await proxy.NameHasOwner(name) + assert has_owner is True + +async def test_filter(router): + bus = Proxy(message_bus, router) + name = "io.gitlab.takluyver.jeepney.tests.asyncio_test_filter" + + match_rule = MatchRule( + type="signal", + sender=message_bus.bus_name, + interface=message_bus.interface, + member="NameOwnerChanged", + path=message_bus.object_path, + ) + match_rule.add_arg_condition(0, name) + + # Ask the message bus to subscribe us to this signal + await bus.AddMatch(match_rule) + + with router.filter(match_rule) as queue: + res, = await bus.RequestName(name) + assert res == 1 # 1: got the name + + signal_msg = await asyncio.wait_for(queue.get(), timeout=2.0) + assert signal_msg.body == (name, '', router.unique_name) + +async def test_recv_after_connect(): + # Can't use here: + # 1. 'connection' fixture + # 2. asyncio.wait_for() + # If (1) and/or (2) is used, the error won't be triggered. + conn = await open_dbus_connection(bus='SESSION') + try: + with pytest.raises(asyncio.TimeoutError): + async with timeout(0): + await conn.receive() + finally: + await conn.close() diff --git a/lib/jeepney/io/tests/test_blocking.py b/lib/jeepney/io/tests/test_blocking.py new file mode 100644 index 0000000..fedd95e --- /dev/null +++ b/lib/jeepney/io/tests/test_blocking.py @@ -0,0 +1,84 @@ +import pytest + +from jeepney import new_method_call, MessageType, DBusAddress +from jeepney.bus_messages import message_bus, MatchRule +from jeepney.io.blocking import open_dbus_connection, Proxy +from .utils import have_session_bus + +pytestmark = pytest.mark.skipif( + not have_session_bus, reason="Tests require DBus session bus" +) + +@pytest.fixture +def session_conn(): + with open_dbus_connection(bus='SESSION') as conn: + yield conn + + +def test_connect(session_conn): + assert session_conn.unique_name.startswith(':') + +bus_peer = DBusAddress( + bus_name='org.freedesktop.DBus', + object_path='/org/freedesktop/DBus', + interface='org.freedesktop.DBus.Peer' +) + +def test_send_and_get_reply(session_conn): + ping_call = new_method_call(bus_peer, 'Ping') + reply = session_conn.send_and_get_reply(ping_call, timeout=5) + assert reply.header.message_type == MessageType.method_return + assert reply.body == () + +def test_proxy(session_conn): + proxy = Proxy(message_bus, session_conn, timeout=5) + name = "io.gitlab.takluyver.jeepney.examples.Server" + res = proxy.RequestName(name) + assert res in {(1,), (2,)} # 1: got the name, 2: queued + + has_owner, = proxy.NameHasOwner(name, _timeout=3) + assert has_owner is True + +def test_filter(session_conn): + bus = Proxy(message_bus, session_conn) + name = "io.gitlab.takluyver.jeepney.tests.blocking_test_filter" + + match_rule = MatchRule( + type="signal", + sender=message_bus.bus_name, + interface=message_bus.interface, + member="NameOwnerChanged", + path=message_bus.object_path, + ) + match_rule.add_arg_condition(0, name) + + # Ask the message bus to subscribe us to this signal + bus.AddMatch(match_rule) + + with session_conn.filter(match_rule) as matches: + res, = bus.RequestName(name) + assert res == 1 # 1: got the name + + signal_msg = session_conn.recv_until_filtered(matches, timeout=2) + + assert signal_msg.body == (name, '', session_conn.unique_name) + + +def test_recv_fd(respond_with_fd): + getfd_call = new_method_call(respond_with_fd, 'GetFD') + with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: + reply = conn.send_and_get_reply(getfd_call, timeout=5) + + assert reply.header.message_type is MessageType.method_return + with reply.body[0].to_file('w+') as f: + assert f.read() == 'readme' + + +def test_send_fd(temp_file_and_contents, read_from_fd): + temp_file, data = temp_file_and_contents + readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,)) + with open_dbus_connection(bus='SESSION', enable_fds=True) as conn: + reply = conn.send_and_get_reply(readfd_call, timeout=5) + + assert reply.header.message_type is MessageType.method_return + assert reply.body[0] == data diff --git a/lib/jeepney/io/tests/test_threading.py b/lib/jeepney/io/tests/test_threading.py new file mode 100644 index 0000000..d408497 --- /dev/null +++ b/lib/jeepney/io/tests/test_threading.py @@ -0,0 +1,83 @@ +import pytest + +from jeepney import new_method_call, MessageType, DBusAddress +from jeepney.bus_messages import message_bus, MatchRule +from jeepney.io.threading import open_dbus_router, Proxy +from .utils import have_session_bus + +pytestmark = pytest.mark.skipif( + not have_session_bus, reason="Tests require DBus session bus" +) + +@pytest.fixture +def router(): + with open_dbus_router(bus='SESSION') as conn: + yield conn + + +def test_connect(router): + assert router.unique_name.startswith(':') + +bus_peer = DBusAddress( + bus_name='org.freedesktop.DBus', + object_path='/org/freedesktop/DBus', + interface='org.freedesktop.DBus.Peer' +) + +def test_send_and_get_reply(router): + ping_call = new_method_call(bus_peer, 'Ping') + reply = router.send_and_get_reply(ping_call, timeout=5) + assert reply.header.message_type == MessageType.method_return + assert reply.body == () + +def test_proxy(router): + proxy = Proxy(message_bus, router, timeout=5) + name = "io.gitlab.takluyver.jeepney.examples.Server" + res = proxy.RequestName(name) + assert res in {(1,), (2,)} # 1: got the name, 2: queued + + has_owner, = proxy.NameHasOwner(name, _timeout=3) + assert has_owner is True + +def test_filter(router): + bus = Proxy(message_bus, router) + name = "io.gitlab.takluyver.jeepney.tests.threading_test_filter" + + match_rule = MatchRule( + type="signal", + sender=message_bus.bus_name, + interface=message_bus.interface, + member="NameOwnerChanged", + path=message_bus.object_path, + ) + match_rule.add_arg_condition(0, name) + + # Ask the message bus to subscribe us to this signal + bus.AddMatch(match_rule) + + with router.filter(match_rule) as queue: + res, = bus.RequestName(name) + assert res == 1 # 1: got the name + + signal_msg = queue.get(timeout=2.0) + assert signal_msg.body == (name, '', router.unique_name) + + +def test_recv_fd(respond_with_fd): + getfd_call = new_method_call(respond_with_fd, 'GetFD') + with open_dbus_router(bus='SESSION', enable_fds=True) as router: + reply = router.send_and_get_reply(getfd_call, timeout=5) + + assert reply.header.message_type is MessageType.method_return + with reply.body[0].to_file('w+') as f: + assert f.read() == 'readme' + + +def test_send_fd(temp_file_and_contents, read_from_fd): + temp_file, data = temp_file_and_contents + readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,)) + with open_dbus_router(bus='SESSION', enable_fds=True) as router: + reply = router.send_and_get_reply(readfd_call, timeout=5) + + assert reply.header.message_type is MessageType.method_return + assert reply.body[0] == data diff --git a/lib/jeepney/io/tests/test_trio.py b/lib/jeepney/io/tests/test_trio.py new file mode 100644 index 0000000..d426993 --- /dev/null +++ b/lib/jeepney/io/tests/test_trio.py @@ -0,0 +1,114 @@ +import trio +import pytest + +from jeepney import DBusAddress, DBusErrorResponse, MessageType, new_method_call +from jeepney.bus_messages import message_bus, MatchRule +from jeepney.io.trio import ( + open_dbus_connection, open_dbus_router, Proxy, +) +from .utils import have_session_bus + +pytestmark = [ + pytest.mark.trio, + pytest.mark.skipif( + not have_session_bus, reason="Tests require DBus session bus" + ), +] + +# Can't use any async fixtures here, because pytest-asyncio tries to handle +# all of them: https://github.com/pytest-dev/pytest-asyncio/issues/124 + +async def test_connect(): + conn = await open_dbus_connection(bus='SESSION') + async with conn: + assert conn.unique_name.startswith(':') + +bus_peer = DBusAddress( + bus_name='org.freedesktop.DBus', + object_path='/org/freedesktop/DBus', + interface='org.freedesktop.DBus.Peer' +) + +async def test_send_and_get_reply(): + ping_call = new_method_call(bus_peer, 'Ping') + async with open_dbus_router(bus='SESSION') as req: + with trio.fail_after(5): + reply = await req.send_and_get_reply(ping_call) + + assert reply.header.message_type == MessageType.method_return + assert reply.body == () + + +async def test_send_and_get_reply_error(): + ping_call = new_method_call(bus_peer, 'Snart') # No such method + async with open_dbus_router(bus='SESSION') as req: + with trio.fail_after(5): + reply = await req.send_and_get_reply(ping_call) + + assert reply.header.message_type == MessageType.error + + +async def test_proxy(): + async with open_dbus_router(bus='SESSION') as req: + proxy = Proxy(message_bus, req) + name = "io.gitlab.takluyver.jeepney.examples.Server" + res = await proxy.RequestName(name) + assert res in {(1,), (2,)} # 1: got the name, 2: queued + + has_owner, = await proxy.NameHasOwner(name) + assert has_owner is True + + +async def test_proxy_error(): + async with open_dbus_router(bus='SESSION') as req: + proxy = Proxy(message_bus, req) + with pytest.raises(DBusErrorResponse): + await proxy.RequestName(":123") # Invalid name + + +async def test_filter(): + name = "io.gitlab.takluyver.jeepney.tests.trio_test_filter" + async with open_dbus_router(bus='SESSION') as router: + bus = Proxy(message_bus, router) + + match_rule = MatchRule( + type="signal", + sender=message_bus.bus_name, + interface=message_bus.interface, + member="NameOwnerChanged", + path=message_bus.object_path, + ) + match_rule.add_arg_condition(0, name) + + # Ask the message bus to subscribe us to this signal + await bus.AddMatch(match_rule) + + async with router.filter(match_rule) as chan: + res, = await bus.RequestName(name) + assert res == 1 # 1: got the name + + with trio.fail_after(2.0): + signal_msg = await chan.receive() + assert signal_msg.body == (name, '', router.unique_name) + + +async def test_recv_fd(respond_with_fd): + getfd_call = new_method_call(respond_with_fd, 'GetFD') + with trio.fail_after(5): + async with open_dbus_router(bus='SESSION', enable_fds=True) as router: + reply = await router.send_and_get_reply(getfd_call) + + assert reply.header.message_type is MessageType.method_return + with reply.body[0].to_file('w+') as f: + assert f.read() == 'readme' + + +async def test_send_fd(temp_file_and_contents, read_from_fd): + temp_file, data = temp_file_and_contents + readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,)) + with trio.fail_after(5): + async with open_dbus_router(bus='SESSION', enable_fds=True) as router: + reply = await router.send_and_get_reply(readfd_call) + + assert reply.header.message_type is MessageType.method_return + assert reply.body[0] == data diff --git a/lib/jeepney/io/tests/utils.py b/lib/jeepney/io/tests/utils.py new file mode 100644 index 0000000..6db0f86 --- /dev/null +++ b/lib/jeepney/io/tests/utils.py @@ -0,0 +1,3 @@ +import os + +have_session_bus = bool(os.environ.get('DBUS_SESSION_BUS_ADDRESS')) diff --git a/lib/jeepney/io/threading.py b/lib/jeepney/io/threading.py new file mode 100644 index 0000000..5649299 --- /dev/null +++ b/lib/jeepney/io/threading.py @@ -0,0 +1,273 @@ +"""Synchronous IO wrappers with thread safety +""" +from concurrent.futures import Future +from contextlib import contextmanager +import functools +import os +from selectors import EVENT_READ +import socket +from queue import Queue, Full as QueueFull +from threading import Lock, Thread +from typing import Optional + +from jeepney import Message, MessageType +from jeepney.bus import get_bus +from jeepney.bus_messages import message_bus +from jeepney.wrappers import ProxyBase, unwrap_msg +from .blocking import ( + unwrap_read, prep_socket, DBusConnectionBase, timeout_to_deadline, +) +from .common import ( + MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable, +) + +__all__ = [ + 'open_dbus_connection', + 'open_dbus_router', + 'DBusConnection', + 'DBusRouter', + 'Proxy', + 'ReceiveStopped', +] + + +class ReceiveStopped(Exception): + pass + + +class DBusConnection(DBusConnectionBase): + def __init__(self, sock: socket.socket, enable_fds=False): + super().__init__(sock, enable_fds=enable_fds) + self._stop_r, self._stop_w = os.pipe() + self.stop_key = self.selector.register(self._stop_r, EVENT_READ) + self.send_lock = Lock() + self.rcv_lock = Lock() + + def send(self, message: Message, serial=None): + """Serialise and send a :class:`~.Message` object""" + data, fds = self._serialise(message, serial) + with self.send_lock: + if fds: + self._send_with_fds(data, fds) + else: + self.sock.sendall(data) + + def receive(self, *, timeout=None) -> Message: + """Return the next available message from the connection + + If the data is ready, this will return immediately, even if timeout<=0. + Otherwise, it will wait for up to timeout seconds, or indefinitely if + timeout is None. If no message comes in time, it raises TimeoutError. + + If the connection is closed from another thread, this will raise + ReceiveStopped. + """ + deadline = timeout_to_deadline(timeout) + + if not self.rcv_lock.acquire(timeout=(timeout or -1)): + raise TimeoutError(f"Did not get receive lock in {timeout} seconds") + try: + return self._receive(deadline) + finally: + self.rcv_lock.release() + + def _read_some_data(self, timeout=None): + # Wait for data or a signal on the stop pipe + for key, ev in self.selector.select(timeout): + if key == self.select_key: + if self.enable_fds: + return self._read_with_fds() + else: + return unwrap_read(self.sock.recv(4096)), [] + elif key == self.stop_key: + raise ReceiveStopped("DBus receive stopped from another thread") + + raise TimeoutError + + def interrupt(self): + """Make any threads waiting for a message raise ReceiveStopped""" + os.write(self._stop_w, b'a') + + def reset_interrupt(self): + """Allow calls to .receive() again after .interrupt() + + To avoid race conditions, you should typically wait for threads to + respond (e.g. by joining them) between interrupting and resetting. + """ + # Clear any data on the stop pipe + while (self.stop_key, EVENT_READ) in self.selector.select(timeout=0): + os.read(self._stop_r, 1024) + + def close(self): + """Close the connection""" + self.interrupt() + super().close() + + +def open_dbus_connection(bus='SESSION', enable_fds=False, auth_timeout=1.): + """Open a plain D-Bus connection + + D-Bus has an authentication step before sending or receiving messages. + This takes < 1 ms in normal operation, but there is a timeout so that client + code won't get stuck if the server doesn't reply. *auth_timeout* configures + this timeout in seconds. + + :return: :class:`DBusConnection` + """ + bus_addr = get_bus(bus) + sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout) + + conn = DBusConnection(sock, enable_fds) + + with DBusRouter(conn) as router: + reply_body = Proxy(message_bus, router, timeout=10).Hello() + conn.unique_name = reply_body[0] + + return conn + + +class DBusRouter: + """A client D-Bus connection which can wait for replies. + + This runs a separate receiver thread and dispatches received messages. + + It's possible to wrap a :class:`DBusConnection` in a router temporarily. + Using the connection directly while it is wrapped is not supported, + but you can use it again after the router is closed. + """ + def __init__(self, conn: DBusConnection): + self.conn = conn + self._replies = ReplyMatcher() + self._filters = MessageFilters() + self._rcv_thread = Thread(target=self._receiver, daemon=True) + self._rcv_thread.start() + + @property + def unique_name(self): + return self.conn.unique_name + + def send(self, message, *, serial=None): + """Serialise and send a :class:`~.Message` object""" + self.conn.send(message, serial=serial) + + def send_and_get_reply(self, msg: Message, *, timeout=None) -> Message: + """Send a method call message, wait for and return a reply""" + check_replyable(msg) + if not self._rcv_thread.is_alive(): + raise RouterClosed("This D-Bus router has stopped") + + serial = next(self.conn.outgoing_serial) + + with self._replies.catch(serial, Future()) as reply_fut: + self.conn.send(msg, serial=serial) + return reply_fut.result(timeout=timeout) + + def close(self): + """Close this router + + This does not close the underlying connection. + """ + self.conn.interrupt() + self._rcv_thread.join(timeout=10) + self.conn.reset_interrupt() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def filter(self, rule, *, queue: Optional[Queue] =None, bufsize=1): + """Create a filter for incoming messages + + Usage:: + + with router.filter(rule) as queue: + matching_msg = queue.get() + + :param jeepney.MatchRule rule: Catch messages matching this rule + :param queue.Queue queue: Matched messages will be added to this + :param int bufsize: If no queue is passed in, create one with this size + """ + return FilterHandle(self._filters, rule, queue or Queue(maxsize=bufsize)) + + # Code to run in receiver thread ------------------------------------ + + def _dispatch(self, msg: Message): + if self._replies.dispatch(msg): + return + + for filter in self._filters.matches(msg): + try: + filter.queue.put_nowait(msg) + except QueueFull: + pass + + def _receiver(self): + try: + while True: + msg = self.conn.receive() + self._dispatch(msg) + except ReceiveStopped: + pass + finally: + # Send errors to any tasks still waiting for a message. + self._replies.drop_all() + +class Proxy(ProxyBase): + """A blocking proxy for calling D-Bus methods via a :class:`DBusRouter`. + + You can call methods on the proxy object, such as ``bus_proxy.Hello()`` + to make a method call over D-Bus and wait for a reply. It will either + return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. + The methods available are defined by the message generator you wrap. + + You can set a time limit on a call by passing ``_timeout=`` in the method + call, or set a default when creating the proxy. The ``_timeout`` argument + is not passed to the message generator. + All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it + expires before a reply arrives. + + :param msggen: A message generator object + :param ~threading.DBusRouter router: Router to send and receive messages + :param float timeout: Default seconds to wait for a reply, or None for no limit + """ + def __init__(self, msggen, router, *, timeout=None): + super().__init__(msggen) + self._router = router + self._timeout = timeout + + def __repr__(self): + extra = '' if (self._timeout is None) else f', timeout={self._timeout}' + return f"Proxy({self._msggen}, {self._router}{extra})" + + def _method_call(self, make_msg): + @functools.wraps(make_msg) + def inner(*args, **kwargs): + timeout = kwargs.pop('_timeout', self._timeout) + msg = make_msg(*args, **kwargs) + assert msg.header.message_type is MessageType.method_call + reply = self._router.send_and_get_reply(msg, timeout=timeout) + return unwrap_msg(reply) + + return inner + +@contextmanager +def open_dbus_router(bus='SESSION', enable_fds=False): + """Open a D-Bus 'router' to send and receive messages. + + Use as a context manager:: + + with open_dbus_router() as router: + ... + + On leaving the ``with`` block, the connection will be closed. + + :param str bus: 'SESSION' or 'SYSTEM' or a supported address. + :param bool enable_fds: Whether to enable passing file descriptors. + :return: :class:`DBusRouter` + """ + with open_dbus_connection(bus=bus, enable_fds=enable_fds) as conn: + with DBusRouter(conn) as router: + yield router diff --git a/lib/jeepney/io/trio.py b/lib/jeepney/io/trio.py new file mode 100644 index 0000000..99acb91 --- /dev/null +++ b/lib/jeepney/io/trio.py @@ -0,0 +1,424 @@ +import array +import errno +import logging +import socket +from contextlib import asynccontextmanager, contextmanager +from itertools import count +from typing import Optional + +from outcome import Value, Error +import trio +from trio.abc import Channel + +from jeepney.auth import Authenticator, BEGIN +from jeepney.bus import get_bus +from jeepney.fds import FileDescriptor, fds_buf_size +from jeepney.low_level import Parser, MessageType, Message +from jeepney.wrappers import ProxyBase, unwrap_msg +from jeepney.bus_messages import message_bus +from .common import ( + MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable, +) + +log = logging.getLogger(__name__) + +__all__ = [ + 'open_dbus_connection', + 'open_dbus_router', + 'Proxy', +] + + +# The function below is copied from trio, which is under the MIT license: + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +@contextmanager +def _translate_socket_errors_to_stream_errors(): + try: + yield + except OSError as exc: + if exc.errno in {errno.EBADF, errno.ENOTSOCK}: + # EBADF on Unix, ENOTSOCK on Windows + raise trio.ClosedResourceError("this socket was already closed") from None + else: + raise trio.BrokenResourceError( + "socket connection broken: {}".format(exc) + ) from exc + + + +class DBusConnection(Channel): + """A plain D-Bus connection with no matching of replies. + + This doesn't run any separate tasks: sending and receiving are done in + the task that calls those methods. It's suitable for implementing servers: + several worker tasks can receive requests and send replies. + For a typical client pattern, see :class:`DBusRouter`. + + Implements trio's channel interface for Message objects. + """ + def __init__(self, socket, enable_fds=False): + self.socket = socket + self.enable_fds = enable_fds + self.parser = Parser() + self.outgoing_serial = count(start=1) + self.unique_name = None + self.send_lock = trio.Lock() + self.recv_lock = trio.Lock() + self._leftover_to_send = None # type: Optional[memoryview] + + async def send(self, message: Message, *, serial=None): + """Serialise and send a :class:`~.Message` object""" + async with self.send_lock: + if serial is None: + serial = next(self.outgoing_serial) + fds = array.array('i') if self.enable_fds else None + data = message.serialise(serial, fds=fds) + await self._send_data(data, fds) + + # _send_data is copied & modified from trio's SocketStream.send_all() . + # See above for the MIT license. + async def _send_data(self, data: bytes, fds): + if self.socket.did_shutdown_SHUT_WR: + raise trio.ClosedResourceError("can't send data after sending EOF") + + with _translate_socket_errors_to_stream_errors(): + if self._leftover_to_send: + # A previous message was partly sent - finish sending it now. + await self._send_remainder(self._leftover_to_send) + + with memoryview(data) as data: + if fds: + sent = await self.socket.sendmsg([data], [( + trio.socket.SOL_SOCKET, trio.socket.SCM_RIGHTS, fds + )]) + else: + sent = await self.socket.send(data) + + await self._send_remainder(data, sent) + + async def _send_remainder(self, data: memoryview, already_sent=0): + try: + while already_sent < len(data): + with data[already_sent:] as remaining: + sent = await self.socket.send(remaining) + already_sent += sent + self._leftover_to_send = None + except trio.Cancelled: + # Sending cancelled mid-message. Keep track of the remaining data + # so it can be sent before the next message, otherwise the next + # message won't be recognised. + self._leftover_to_send = data[already_sent:] + raise + + async def receive(self) -> Message: + """Return the next available message from the connection""" + async with self.recv_lock: + while True: + msg = self.parser.get_next_message() + if msg is not None: + return msg + + # Once data is read, it must be given to the parser with no + # checkpoints (where the task could be cancelled). + b, fds = await self._read_data() + if not b: + raise trio.EndOfChannel("Socket closed at the other end") + self.parser.add_data(b, fds) + + async def _read_data(self): + if self.enable_fds: + nbytes = self.parser.bytes_desired() + with _translate_socket_errors_to_stream_errors(): + data, ancdata, flags, _ = await self.socket.recvmsg( + nbytes, fds_buf_size() + ) + if flags & getattr(trio.socket, 'MSG_CTRUNC', 0): + self._close() + raise RuntimeError("Unable to receive all file descriptors") + return data, FileDescriptor.from_ancdata(ancdata) + + else: # not self.enable_fds + with _translate_socket_errors_to_stream_errors(): + data = await self.socket.recv(4096) + return data, [] + + def _close(self): + self.socket.close() + self._leftover_to_send = None + + # Our closing is currently sync, but AsyncResource objects must have aclose + async def aclose(self): + """Close the D-Bus connection""" + self._close() + + @asynccontextmanager + async def router(self): + """Temporarily wrap this connection as a :class:`DBusRouter` + + To be used like:: + + async with conn.router() as req: + reply = await req.send_and_get_reply(msg) + + While the router is running, you shouldn't use :meth:`receive`. + Once the router is closed, you can use the plain connection again. + """ + async with trio.open_nursery() as nursery: + router = DBusRouter(self) + await router.start(nursery) + try: + yield router + finally: + await router.aclose() + + +async def open_dbus_connection(bus='SESSION', *, enable_fds=False) -> DBusConnection: + """Open a plain D-Bus connection + + :return: :class:`DBusConnection` + """ + bus_addr = get_bus(bus) + sock : trio.SocketStream = await trio.open_unix_socket(bus_addr) + + # Authentication + authr = Authenticator(enable_fds=enable_fds, inc_null_byte=False) + if hasattr(socket, 'SCM_CREDS'): + # BSD: send credentials message to authenticate (kernel fills in data) + await sock.socket.sendmsg( + [b'\0'], [(socket.SOL_SOCKET, socket.SCM_CREDS, bytes(512))] + ) + else: + # Linux: no ancillary data needed, bus checks with SO_PEERCRED + await sock.send_all(b'\0') + for req_data in authr: + await sock.send_all(req_data) + authr.feed(await sock.receive_some()) + await sock.send_all(BEGIN) + + conn = DBusConnection(sock.socket, enable_fds=enable_fds) + + # Say *Hello* to the message bus - this must be the first message, and the + # reply gives us our unique name. + async with conn.router() as router: + reply = await router.send_and_get_reply(message_bus.Hello()) + conn.unique_name = reply.body[0] + + return conn + + +class TrioFilterHandle(FilterHandle): + def __init__(self, filters: MessageFilters, rule, send_chn, recv_chn): + super().__init__(filters, rule, recv_chn) + self.send_channel = send_chn + + @property + def receive_channel(self): + return self.queue + + async def aclose(self): + self.close() + await self.send_channel.aclose() + + async def __aenter__(self): + return self.queue + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.aclose() + + +class Future: + """A very simple Future for trio based on `trio.Event`.""" + def __init__(self): + self._outcome = None + self._event = trio.Event() + + def set_result(self, result): + self._outcome = Value(result) + self._event.set() + + def set_exception(self, exc): + self._outcome = Error(exc) + self._event.set() + + async def get(self): + await self._event.wait() + return self._outcome.unwrap() + + +class DBusRouter: + """A client D-Bus connection which can wait for replies. + + This runs a separate receiver task and dispatches received messages. + """ + _nursery_mgr = None + _rcv_cancel_scope = None + + def __init__(self, conn: DBusConnection): + self._conn = conn + self._replies = ReplyMatcher() + self._filters = MessageFilters() + + @property + def unique_name(self): + return self._conn.unique_name + + async def send(self, message, *, serial=None): + """Send a message, don't wait for a reply + """ + await self._conn.send(message, serial=serial) + + async def send_and_get_reply(self, message) -> Message: + """Send a method call message and wait for the reply + + Returns the reply message (method return or error message type). + """ + check_replyable(message) + if self._rcv_cancel_scope is None: + raise RouterClosed("This DBusRouter has stopped") + + serial = next(self._conn.outgoing_serial) + + with self._replies.catch(serial, Future()) as reply_fut: + await self.send(message, serial=serial) + return (await reply_fut.get()) + + def filter(self, rule, *, channel: Optional[trio.MemorySendChannel]=None, bufsize=1): + """Create a filter for incoming messages + + Usage:: + + async with router.filter(rule) as receive_channel: + matching_msg = await receive_channel.receive() + + # OR: + send_chan, recv_chan = trio.open_memory_channel(1) + async with router.filter(rule, channel=send_chan): + matching_msg = await recv_chan.receive() + + If the channel fills up, + The sending end of the channel is closed when leaving the ``async with`` + block, whether or not it was passed in. + + :param jeepney.MatchRule rule: Catch messages matching this rule + :param trio.MemorySendChannel channel: Send matching messages here + :param int bufsize: If no channel is passed in, create one with this size + """ + if channel is None: + channel, recv_channel = trio.open_memory_channel(bufsize) + else: + recv_channel = None + return TrioFilterHandle(self._filters, rule, channel, recv_channel) + + # Task management ------------------------------------------- + + async def start(self, nursery: trio.Nursery): + if self._rcv_cancel_scope is not None: + raise RuntimeError("DBusRouter receiver task is already running") + self._rcv_cancel_scope = await nursery.start(self._receiver) + + async def aclose(self): + """Stop the sender & receiver tasks""" + # It doesn't matter if we receive a partial message - the connection + # should ensure that whatever is received is fed to the parser. + if self._rcv_cancel_scope is not None: + self._rcv_cancel_scope.cancel() + self._rcv_cancel_scope = None + + # Ensure trio checkpoint + await trio.sleep(0) + + # Code to run in receiver task ------------------------------------ + + def _dispatch(self, msg: Message): + """Handle one received message""" + if self._replies.dispatch(msg): + return + + for filter in self._filters.matches(msg): + try: + filter.send_channel.send_nowait(msg) + except trio.WouldBlock: + pass + + async def _receiver(self, task_status=trio.TASK_STATUS_IGNORED): + """Receiver loop - runs in a separate task""" + with trio.CancelScope() as cscope: + self.is_running = True + task_status.started(cscope) + try: + while True: + msg = await self._conn.receive() + self._dispatch(msg) + finally: + self.is_running = False + # Send errors to any tasks still waiting for a message. + self._replies.drop_all() + + # Closing a memory channel can't block, but it only has an + # async close method, so we need to shield it from cancellation. + with trio.move_on_after(3) as cleanup_scope: + for filter in self._filters.filters.values(): + cleanup_scope.shield = True + await filter.send_channel.aclose() + + +class Proxy(ProxyBase): + """A trio proxy for calling D-Bus methods + + You can call methods on the proxy object, such as ``await bus_proxy.Hello()`` + to make a method call over D-Bus and wait for a reply. It will either + return a tuple of returned data, or raise :exc:`.DBusErrorResponse`. + The methods available are defined by the message generator you wrap. + + :param msggen: A message generator object. + :param ~trio.DBusRouter router: Router to send and receive messages. + """ + def __init__(self, msggen, router): + super().__init__(msggen) + if not isinstance(router, DBusRouter): + raise TypeError("Proxy can only be used with DBusRequester") + self._router = router + + def _method_call(self, make_msg): + async def inner(*args, **kwargs): + msg = make_msg(*args, **kwargs) + assert msg.header.message_type is MessageType.method_call + reply = await self._router.send_and_get_reply(msg) + return unwrap_msg(reply) + + return inner + + +@asynccontextmanager +async def open_dbus_router(bus='SESSION', *, enable_fds=False): + """Open a D-Bus 'router' to send and receive messages. + + Use as an async context manager:: + + async with open_dbus_router() as req: + ... + + :param str bus: 'SESSION' or 'SYSTEM' or a supported address. + :return: :class:`DBusRouter` + + This is a shortcut for:: + + conn = await open_dbus_connection() + async with conn: + async with conn.router() as req: + ... + """ + conn = await open_dbus_connection(bus, enable_fds=enable_fds) + async with conn: + async with conn.router() as rtr: + yield rtr diff --git a/lib/jeepney/low_level.py b/lib/jeepney/low_level.py new file mode 100644 index 0000000..1b1463d --- /dev/null +++ b/lib/jeepney/low_level.py @@ -0,0 +1,608 @@ +import string +import struct +from collections import deque +from enum import Enum, IntEnum, IntFlag +from typing import Optional + +class SizeLimitError(ValueError): + """Raised when trying to (de-)serialise data exceeding D-Bus' size limit. + + This is currently only implemented for arrays, where the maximum size is + 64 MiB. + """ + pass + +class Endianness(Enum): + little = 1 + big = 2 + + def struct_code(self): + return '<' if (self is Endianness.little) else '>' + + def dbus_code(self): + return b'l' if (self is Endianness.little) else b'B' + + +endian_map = {b'l': Endianness.little, b'B': Endianness.big} + + +class MessageType(Enum): + method_call = 1 + method_return = 2 + error = 3 + signal = 4 + + +class MessageFlag(IntFlag): + no_reply_expected = 1 + no_auto_start = 2 + allow_interactive_authorization = 4 + + +class HeaderFields(IntEnum): + path = 1 + interface = 2 + member = 3 + error_name = 4 + reply_serial = 5 + destination = 6 + sender = 7 + signature = 8 + unix_fds = 9 + + +def padding(pos, step): + pad = step - (pos % step) + if pad == step: + return 0 + return pad + + +class FixedType: + def __init__(self, size, struct_code): + self.size = self.alignment = size + self.struct_code = struct_code + + def parse_data(self, buf, pos, endianness, fds=()): + pos += padding(pos, self.alignment) + code = endianness.struct_code() + self.struct_code + val = struct.unpack(code, buf[pos:pos + self.size])[0] + return val, pos + self.size + + def serialise(self, data, pos, endianness, fds=None): + pad = b'\0' * padding(pos, self.alignment) + code = endianness.struct_code() + self.struct_code + return pad + struct.pack(code, data) + + def __repr__(self): + return 'FixedType({!r}, {!r})'.format(self.size, self.struct_code) + + def __eq__(self, other): + return (type(other) is FixedType) and (self.size == other.size) \ + and (self.struct_code == other.struct_code) + + +class Boolean(FixedType): + def __init__(self): + super().__init__(4, 'I') # D-Bus booleans take 4 bytes + + def parse_data(self, buf, pos, endianness, fds=()): + val, new_pos = super().parse_data(buf, pos, endianness) + return bool(val), new_pos + + def __repr__(self): + return 'Boolean()' + + def __eq__(self, other): + return type(other) is Boolean + + +class FileDescriptor(FixedType): + def __init__(self): + super().__init__(4, 'I') + + def parse_data(self, buf, pos, endianness, fds=()): + idx, new_pos = super().parse_data(buf, pos, endianness) + return fds[idx], new_pos + + def serialise(self, data, pos, endianness, fds=None): + if fds is None: + raise RuntimeError("Sending FDs is not supported or not enabled") + + if hasattr(data, 'fileno'): + data = data.fileno() + if isinstance(data, bool) or not isinstance(data, int): + raise TypeError("Cannot use {data!r} as file descriptor. Expected " + "an int or an object with fileno() method") + + if data < 0: + raise ValueError(f"File descriptor can't be negative ({data})") + + fds.append(data) + return super().serialise(len(fds) - 1, pos, endianness) + + def __repr__(self): + return 'FileDescriptor()' + + def __eq__(self, other): + return type(other) is FileDescriptor + + +simple_types = { + 'y': FixedType(1, 'B'), # unsigned 8 bit + 'n': FixedType(2, 'h'), # signed 16 bit + 'q': FixedType(2, 'H'), # unsigned 16 bit + 'b': Boolean(), # bool (32-bit) + 'i': FixedType(4, 'i'), # signed 32-bit + 'u': FixedType(4, 'I'), # unsigned 32-bit + 'x': FixedType(8, 'q'), # signed 64-bit + 't': FixedType(8, 'Q'), # unsigned 64-bit + 'd': FixedType(8, 'd'), # double + 'h': FileDescriptor(), # file descriptor (uint32 index in a separate list) +} + + +class StringType: + def __init__(self, length_type): + self.length_type = length_type + + @property + def alignment(self): + return self.length_type.size + + def parse_data(self, buf, pos, endianness, fds=()): + length, pos = self.length_type.parse_data(buf, pos, endianness) + end = pos + length + val = buf[pos:end].decode('utf-8') + assert buf[end:end + 1] == b'\0' + return val, end + 1 + + def check_data(self, data): + if not isinstance(data, str): + raise TypeError("Expected str, not {!r}".format(data)) + + def serialise(self, data, pos, endianness, fds=None): + self.check_data(data) + encoded = data.encode('utf-8') + len_data = self.length_type.serialise(len(encoded), pos, endianness) + return len_data + encoded + b'\0' + + def __repr__(self): + return 'StringType({!r})'.format(self.length_type) + + def __eq__(self, other): + return (type(other) is StringType) \ + and (self.length_type == other.length_type) + + +class ObjectPathType(StringType): + def __init__(self): + super().__init__(simple_types['u']) + + def check_data(self, data): + super().check_data(data) + if not data.startswith('/'): + raise ValueError(f"Object path ({data!r}) must start with /") + if data.endswith('/') and len(data) > 1: + raise ValueError(f"Object path ({data!r}) cannot end with /") + if '//' in data: + raise ValueError(f"Object path ({data!r}) cannot contain double /") + valid_chars = string.ascii_letters + string.digits + '/_' + if any(c not in valid_chars for c in data): + raise ValueError( + f"Object path ({data!r}) can only contain A-Z, a-z, 0-9, / and _" + ) + + +simple_types.update({ + 's': StringType(simple_types['u']), # String + 'o': ObjectPathType(), # Object path + 'g': StringType(simple_types['y']), # Signature +}) + + +class Struct: + alignment = 8 + + def __init__(self, fields): + if any(isinstance(f, DictEntry) for f in fields): + raise TypeError("Found dict entry outside array") + self.fields = fields + + def parse_data(self, buf, pos, endianness, fds=()): + pos += padding(pos, 8) + res = [] + for field in self.fields: + v, pos = field.parse_data(buf, pos, endianness, fds=fds) + res.append(v) + return tuple(res), pos + + def serialise(self, data, pos, endianness, fds=None): + if not isinstance(data, tuple): + raise TypeError("Expected tuple, not {!r}".format(data)) + if len(data) != len(self.fields): + raise ValueError("{} entries for {} fields".format( + len(data), len(self.fields) + )) + pad = b'\0' * padding(pos, self.alignment) + pos += len(pad) + res_pieces = [] + for item, field in zip(data, self.fields): + res_pieces.append(field.serialise(item, pos, endianness, fds=fds)) + pos += len(res_pieces[-1]) + return pad + b''.join(res_pieces) + + def __repr__(self): + return "{}({!r})".format(type(self).__name__, self.fields) + + def __eq__(self, other): + return (type(other) is type(self)) and (self.fields == other.fields) + + +class DictEntry(Struct): + def __init__(self, fields): + if len(fields) != 2: + raise TypeError( + "Dict entry must have 2 fields, not %d" % len(fields)) + if not isinstance(fields[0], (FixedType, StringType)): + raise TypeError( + "First field in dict entry must be simple type, not {}" + .format(type(fields[0]))) + super().__init__(fields) + +class Array: + alignment = 4 + length_type = FixedType(4, 'I') + + def __init__(self, elt_type): + self.elt_type = elt_type + + def parse_data(self, buf, pos, endianness, fds=()): + # print('Array start', pos) + length, pos = self.length_type.parse_data(buf, pos, endianness) + pos += padding(pos, self.elt_type.alignment) + end = pos + length + if self.elt_type == simple_types['y']: # Array of bytes + return buf[pos:end], end + + res = [] + while pos < end: + # print('Array elem', pos) + v, pos = self.elt_type.parse_data(buf, pos, endianness, fds=fds) + res.append(v) + if isinstance(self.elt_type, DictEntry): + # Convert list of 2-tuples to dict + res = dict(res) + return res, pos + + def serialise(self, data, pos, endianness, fds=None): + data_is_bytes = False + if isinstance(self.elt_type, DictEntry) and isinstance(data, dict): + data = data.items() + elif (self.elt_type == simple_types['y']) and isinstance(data, bytes): + data_is_bytes = True + elif not isinstance(data, list): + raise TypeError("Not suitable for array: {!r}".format(data)) + + # Fail fast if we know in advance that the data is too big: + if isinstance(self.elt_type, FixedType): + if (self.elt_type.size * len(data)) > 2**26: + raise SizeLimitError("Array size exceeds 64 MiB limit") + + pad1 = padding(pos, self.alignment) + pos_after_length = pos + pad1 + 4 + pad2 = padding(pos_after_length, self.elt_type.alignment) + + if data_is_bytes: + buf = data + else: + data_pos = pos_after_length + pad2 + limit_pos = data_pos + 2 ** 26 + chunks = [] + for item in data: + chunks.append(self.elt_type.serialise( + item, data_pos, endianness, fds=fds + )) + data_pos += len(chunks[-1]) + if data_pos > limit_pos: + raise SizeLimitError("Array size exceeds 64 MiB limit") + buf = b''.join(chunks) + + len_data = self.length_type.serialise(len(buf), pos+pad1, endianness) + # print('Array ser: pad1={!r}, len_data={!r}, pad2={!r}, buf={!r}'.format( + # pad1, len_data, pad2, buf)) + return (b'\0' * pad1) + len_data + (b'\0' * pad2) + buf + + def __repr__(self): + return 'Array({!r})'.format(self.elt_type) + + def __eq__(self, other): + return (type(other) is Array) and (self.elt_type == other.elt_type) + + +class Variant: + alignment = 1 + + def parse_data(self, buf, pos, endianness, fds=()): + # print('variant', pos) + sig, pos = simple_types['g'].parse_data(buf, pos, endianness) + # print('variant sig:', repr(sig), pos) + valtype = parse_signature(list(sig)) + val, pos = valtype.parse_data(buf, pos, endianness, fds=fds) + # print('variant done', (sig, val), pos) + return (sig, val), pos + + def serialise(self, data, pos, endianness, fds=None): + sig, data = data + valtype = parse_signature(list(sig)) + sig_buf = simple_types['g'].serialise(sig, pos, endianness) + return sig_buf + valtype.serialise( + data, pos + len(sig_buf), endianness, fds=fds + ) + + def __repr__(self): + return 'Variant()' + + def __eq__(self, other): + return type(other) is Variant + +def parse_signature(sig): + """Parse a symbolic signature into objects. + """ + # Based on http://norvig.com/lispy.html + token = sig.pop(0) + if token == 'a': + return Array(parse_signature(sig)) + if token == 'v': + return Variant() + elif token == '(': + fields = [] + while sig[0] != ')': + fields.append(parse_signature(sig)) + sig.pop(0) # ) + return Struct(fields) + elif token == '{': + de = [] + while sig[0] != '}': + de.append(parse_signature(sig)) + sig.pop(0) # } + return DictEntry(de) + elif token in ')}': + raise ValueError('Unexpected end of struct') + else: + return simple_types[token] + + +def calc_msg_size(buf): + endian, = struct.unpack('c', buf[:1]) + endian = endian_map[endian] + body_length, = struct.unpack(endian.struct_code() + 'I', buf[4:8]) + fields_array_len, = struct.unpack(endian.struct_code() + 'I', buf[12:16]) + header_len = 16 + fields_array_len + return header_len + padding(header_len, 8) + body_length + + +_header_fields_type = Array(Struct([simple_types['y'], Variant()])) + + +def parse_header_fields(buf, endianness): + l, pos = _header_fields_type.parse_data(buf, 12, endianness) + return {HeaderFields(k): v[1] for (k, v) in l}, pos + + +header_field_codes = { + 1: 'o', + 2: 's', + 3: 's', + 4: 's', + 5: 'u', + 6: 's', + 7: 's', + 8: 'g', + 9: 'u', +} + + +def serialise_header_fields(d, endianness): + l = [(i.value, (header_field_codes[i], v)) for (i, v) in sorted(d.items())] + return _header_fields_type.serialise(l, 12, endianness) + + +class Header: + def __init__(self, endianness, message_type, flags, protocol_version, + body_length, serial, fields): + """A D-Bus message header + + It's not normally necessary to construct this directly: use higher level + functions and methods instead. + """ + self.endianness = endianness + self.message_type = MessageType(message_type) + self.flags = MessageFlag(flags) + self.protocol_version = protocol_version + self.body_length = body_length + self.serial = serial + self.fields = fields + + def __repr__(self): + return 'Header({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, fields={!r})'.format( + self.endianness, self.message_type, self.flags, + self.protocol_version, self.body_length, self.serial, self.fields) + + def serialise(self, serial=None): + s = self.endianness.struct_code() + 'cBBBII' + if serial is None: + serial = self.serial + return struct.pack(s, self.endianness.dbus_code(), + self.message_type.value, self.flags, + self.protocol_version, + self.body_length, serial) \ + + serialise_header_fields(self.fields, self.endianness) + + @classmethod + def from_buffer(cls, buf): + endian, msgtype, flags, pv = struct.unpack('cBBB', buf[:4]) + endian = endian_map[endian] + bodylen, serial = struct.unpack(endian.struct_code() + 'II', buf[4:12]) + fields, pos = parse_header_fields(buf, endian) + return cls(endian, msgtype, flags, pv, bodylen, serial, fields), pos + + +class Message: + """Object representing a DBus message. + + It's not normally necessary to construct this directly: use higher level + functions and methods instead. + """ + def __init__(self, header, body): + self.header = header + self.body = body + + def __repr__(self): + return "{}({!r}, {!r})".format(type(self).__name__, self.header, self.body) + + @classmethod + def from_buffer(cls, buf: bytes, fds=()) -> 'Message': + header, pos = Header.from_buffer(buf) + n_fds = header.fields.get(HeaderFields.unix_fds, 0) + if n_fds > len(fds): + raise ValueError( + f"Message expects {n_fds} FDs, but only {len(fds)} were received" + ) + fds = fds[:n_fds] + body = () + if HeaderFields.signature in header.fields: + sig = header.fields[HeaderFields.signature] + body_type = parse_signature(list('(%s)' % sig)) + body = body_type.parse_data(buf, pos, header.endianness, fds=fds)[0] + return Message(header, body) + + def serialise(self, serial=None, fds=None) -> bytes: + """Convert this message to bytes. + + Specifying *serial* overrides the ``msg.header.serial`` field, so a + connection can use its own serial number without modifying the message. + + If file-descriptor support is in use, *fds* should be a + :class:`array.array` object with type ``'i'``. Any file descriptors in + the message will be added to the array. If the message contains FDs, + it can't be serialised without this array. + """ + endian = self.header.endianness + + if HeaderFields.signature in self.header.fields: + sig = self.header.fields[HeaderFields.signature] + body_type = parse_signature(list('(%s)' % sig)) + body_buf = body_type.serialise(self.body, 0, endian, fds=fds) + else: + body_buf = b'' + + self.header.body_length = len(body_buf) + if fds: + self.header.fields[HeaderFields.unix_fds] = len(fds) + + header_buf = self.header.serialise(serial=serial) + pad = b'\0' * padding(len(header_buf), 8) + return header_buf + pad + body_buf + + +class Parser: + """Parse DBus messages from a stream of incoming data. + """ + def __init__(self): + self.buf = BufferPipe() + self.fds = [] + self.next_msg_size = None + + def add_data(self, data: bytes, fds=()): + """Provide newly received data to the parser""" + self.buf.write(data) + self.fds.extend(fds) + + def feed(self, data): + """Feed the parser newly read data. + + Returns a list of messages completed by the new data. + """ + self.add_data(data) + return list(iter(self.get_next_message, None)) + + def bytes_desired(self): + """How many bytes can be received without going beyond the next message? + + This is only used with file-descriptor passing, so we don't get too many + FDs in a single recvmsg call. + """ + got = self.buf.bytes_buffered + if got < 16: # The first 16 bytes tell us the message size + return 16 - got + + if self.next_msg_size is None: + self.next_msg_size = calc_msg_size(self.buf.peek(16)) + return self.next_msg_size - got + + def get_next_message(self) -> Optional[Message]: + """Parse one message, if there is enough data. + + Returns None if it doesn't have a complete message. + """ + if self.next_msg_size is None: + if self.buf.bytes_buffered >= 16: + self.next_msg_size = calc_msg_size(self.buf.peek(16)) + nms = self.next_msg_size + if (nms is not None) and self.buf.bytes_buffered >= nms: + raw_msg = self.buf.read(nms) + msg = Message.from_buffer(raw_msg, fds=self.fds) + self.next_msg_size = None + fds_consumed = msg.header.fields.get(HeaderFields.unix_fds, 0) + self.fds = self.fds[fds_consumed:] + return msg + + +class BufferPipe: + """A place to store received data until we can parse a complete message + + The main difference from io.BytesIO is that read & write operate at + opposite ends, like a pipe. + """ + def __init__(self): + self.chunks = deque() + self.bytes_buffered = 0 + + def write(self, b: bytes): + self.chunks.append(b) + self.bytes_buffered += len(b) + + def _peek_iter(self, nbytes: int): + assert nbytes <= self.bytes_buffered + for chunk in self.chunks: + chunk = chunk[:nbytes] + nbytes -= len(chunk) + yield chunk + if nbytes <= 0: + break + + def peek(self, nbytes: int) -> bytes: + """Get exactly nbytes bytes from the front without removing them""" + return b''.join(self._peek_iter(nbytes)) + + def _read_iter(self, nbytes: int): + assert nbytes <= self.bytes_buffered + while True: + chunk = self.chunks.popleft() + self.bytes_buffered -= len(chunk) + if nbytes <= len(chunk): + break + nbytes -= len(chunk) + yield chunk + + # Final chunk + chunk, rem = chunk[:nbytes], chunk[nbytes:] + if rem: + self.chunks.appendleft(rem) + self.bytes_buffered += len(rem) + yield chunk + + def read(self, nbytes: int) -> bytes: + """Take & return exactly nbytes bytes from the front""" + return b''.join(self._read_iter(nbytes)) diff --git a/lib/jeepney/tests/__init__.py b/lib/jeepney/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/jeepney/tests/__pycache__/__init__.cpython-314.pyc b/lib/jeepney/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41f4a852037071a117e3279cedbceadb85db977a GIT binary patch literal 160 zcmdPq+klGC}lX5CH>>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%T~W6w?Mxj zvp}~bu_!&YM7K1(Brnl4$4EaXGfBUovLquvPd_U)wIDCGQokg%xTIJ=K0Y%qvm`!V jub}c4hfQvNN@-52T@fqLG?0D8AjT(VMn=XWW*`dyu_z`U literal 0 HcmV?d00001 diff --git a/lib/jeepney/tests/__pycache__/test_auth.cpython-314.pyc b/lib/jeepney/tests/__pycache__/test_auth.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48c99f94dc1c76b7da41fcb53fcae2317ff6edba GIT binary patch literal 1795 zcmcgsU2hvj6rI^!J8N$om!!y{)V0G$TPG?`j9XeIYGlAw3&9DS6oFJ>?b<`KC9zjC zYeAk8o@nyo7sP+)%?FiDW3r4LAmhxQ32NR@cv-tpHqDMCnyv3zEC=FXj&d(NJz zkyw;Kdpi5QQ&9-{6<>M{-eo(^LAOmlCnmi^JnWS_^tR$DDPpP#;;E(vsF@+akQoLH zoAh;(8G3@tOBw2GR>NDX$Yfnmp~wHR4X{n_b%zOA>h0PeA!LH&yKgS}ge-*uQ{6|C zt(vY#g(lQk;OqKFtw6z;CvP8UC2DVn9iLMfwiJc2*EVSUNUc*HXM+GD(C zt@}~oS=Qh@_`m}#D>utDM*Kx1F^eXeNG*-Qub#*C_WYG0q&E*u)e&AFYq*7)VN zk=ZjcEe-J)B1r|I*YGy@|Nn|Q0=vng7D|hy&pukbyWFqp*x;5{tzO&Qs5b-;Lp&b> z=+u)(;L#sHQItVArpJ(ARnmKUdS8DnP?b#EnA|faTN)xaApXTw`2lABOR1$$jCi9H zttY`MaM6+A--wp>MEg&f0%BI+!&1ddu;t>1>34G1tJiXNCCBWyZ{)>U7@c9ZZBN_z z+>NW(?HM**y%vj>GFnIJ(dEMO!ji=Wz)QHWqT1N6G4a3dy#{_N5m{G(}vPe%YDa@)S#wL zw1MM*3xda(G>_+oCRYr1o`ZN=7U)cAGDUhbtKfa-i37fxge>@5xHzjOi9<7r7=v79 z@q^-fE5-Nw9){cwNpATWnD@hALEtccm|Kou!XGOP=ymF~BIk9^Q9OJU0dofFJ{zFB z%-MZb0a5UC5QWkbivZ8_{x}kECU=wHdF@2@$3(Urnf@s85Gl~61#*Z|GP9EtBojTG(^wmE`G@6nL{sK$AKJEYj literal 0 HcmV?d00001 diff --git a/lib/jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc b/lib/jeepney/tests/__pycache__/test_bindgen.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0e33d496ecb42aec574cbb69b8a0eb82a5f6822 GIT binary patch literal 1785 zcmZ`(%}*Og6rcU@=Nc0*4TKm>Ax-2EY&GH|h0>6cv>2ttT}M_9PF9OOwl}PISF>Xp zIQbYvC2o}?RZ0$3mEL;nAJ9L*6e80pk*c;*)t-=u6sf1qti6~<&8&9bZ{BXTNLj$orv5t~HW9lY9%|F4@};a9HNYK-~8iQ^q;s z&tRk(rPQ63I8UU{plz!qcz45sB(wz80z{u^P%qfjvny{Fpi)F;MR`=wH_zHIk07=c z&A`aCEttm%s?WC;YxO=V1o|Iep?j!-TzgLdImxn*&J`m6AmSVFB7}*tC-Eireg` zhQujDqHTx^M6^^~CjQl`tr%(r624d_z6?a`S{`l=zd0}ClyIVLCqBF7NQ9zk60u5$ zL|9c({0tFI3mQbS)rzH45@OL&chD#C?P- z5`mJ92)1UDkij_ETFNHDRm~_+Mk|JmSRE6gVwY^jjFDC&HOFG0Lt6Mb*s~pP24lOy*t2ubZ+$=g!&GDB#$NE_+Twu}-kIH+ZAzoN(&#s9 z&5QH97w7k+`Gz>(kmgUUEaKO>=Frsc(9{cQx-pyCmog18^OKZ${T@99J>5Sx%Wl2r zrr`NekZwi-@ovIvrc)5aXW9v;5_!`mJ?&g3bn^>EhwD&xZo74{-LA2nU8k0ipN1yl zLsK(|ub?5Pl>LfRcZ!18GvOrtJ8(=PK%>mv|6#=}RCV|fO3)#DYV8kbcO-Be_Y(B~ z0>XcSnU^5`YxvAgX{)rOZ|U``PiLOYJiY$p`txgh=O-J}D|_KPHR+WY;qG#1g3kUI DKirR~ literal 0 HcmV?d00001 diff --git a/lib/jeepney/tests/__pycache__/test_bus.cpython-314.pyc b/lib/jeepney/tests/__pycache__/test_bus.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a0956aca04a9b0475e282d07e7e0b6b06d9edee GIT binary patch literal 1785 zcmb7F-EZ4e6hDp~$F7sQ3|IzBo0fh+R&6d-i^f6*Ev>Zhkq!qCNEMcwxUC^|qHBAi z8Auh66;k(r(EbJfjPVzEP0%Vco){1CG|{0-ed64kIBnTRwVULed(X%3ex2XVWPhp; zu>F<)(O%*Key4+|#k!sSY3yvl7oc-R@TiUzxoa^mmZg2Z$X^pYAq%>Y1~0D1QHnY@ z3OVsl3Q)>%VREBhwO4JcYFN$<9P=x_Tj`=Rl|YHp!~QBqr&>nYY0J?f;JOgmj`RrV zFw>2VbOS!cgKv8*12auj73P1W{CV_h+?#k>r&J^(j&0U4whOXoIGc6 ze$3|$nCS*)pJk^)gcE5zay)K5K0{9q8y%0rFigU$9RsZ4>1DviiyA~P{m_U!KL|FL z6Av@KV}E_6VS4KYjrfk{yB5)2pPHJ|@O}=@JZ@{AWf|s*>k+f!6*O<7p{>^IO&NOK z0TzOes!doxDJO6kbERe(X0=Kz*R|YGY;1a#>xCjQZ8}cs zzT?>&)*>Nwf@Q;*F1_sGurqPPZd~+Cf_NAGHP10;YtvfIUeOR`z3ymVT2{lcHZ=l zU2RKmAMnsOj&A(!%43Ia5|?pjc6dG;|2eD7PsvZFP6p5BICXTSfgaO&tyj=bEWpzU z)+;+b{vS%6mMKPsswYlno(El`pQFM(mF98}tM4S1E}d5LA86fzSHg1(^PiNB@?yDM zTq+r~Kew=;a|S#c9gL=1DFxTaG7`_Z?CtXfb2pPq2?RApLamWUtK8L(ZXT z0DtbcD6pvI7x$!Bg3-A>X(SjeJ&f~d@c{U~#6MI#l3?KZotxV?Z@<}6$AesKSIg+)~Ga*NSbm7)ZstYVkm!Y5kB?ck<{9 literal 0 HcmV?d00001 diff --git a/lib/jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc b/lib/jeepney/tests/__pycache__/test_bus_messages.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9dc1cf4acc9328fb8323f44a454bdfe83528c32 GIT binary patch literal 4578 zcmcIn-EY%Y6u(X!+liAV&;q6F2c=9j8zg*`!A8ec=od&7W@*#b6(T3OZ39Uhu3fQK zXzDAQG~N&oJnWIa@yO%;goH*avMRBOC-&5}m5_MaIZhm>Aubfbm3(~dd;IhD_3!-7 zxu3U$0tDK>XYXeMAwpi@h24DB&c?6MStQqq!d)ceoWYHI49~dN@b=(6&qaRRXZXei zL+BybxL%@o6<+b3_EerW#B1W!T{fh1R)hhr>fu+!eZ&YT5?ERBgAFPHupvbT+oA;f zNHp{kough;9zH*(52e$L>be=o)7uF>Gm}@dW*GZ9YRqcsL`uzOqaM?LSv69#%3PM3 zat{2}8JbAW=?gt^jm^ZT8Kr5e-!!yByr3~d&Bm*j3*9gts~eA1#$&@3cYv0)o@7xt%d)6Sem)aVF1zYC8VM z1LkPl$N{f1g`j?^r!)pQwM(z#X9efxl z_nmw^x!nHkiahjG9xC!fMR^E@BW?HY+`aSgO8Lm(6S~|vyb>OH8XhU~BSm?{8NReC zwX8^8C8?{}eSTTG@Ym#q*V84uBA$r2j%JN`d}xB=e|Wh8vmq%ocEg25`fdc9(EN0j z^zTe?j~gW&f-Bywp;UWEjpFvy2pOn$zl5`cQ1?8qaJA?0D;iqUAatE$9hH4RWnp2T zix5RQvjvsK5*59G)C#;}cPzMP`PgYZ004hMwsMqG!}>MyE@J?Z>fw zc)PoiUk&f=2B!`<^xwf)JS!ghx-6etkw;7N=&H24+}ii&Ht!XTRJx*VO#A->fF~_LMC$;qaM3iF(VH9{P zfM-%=OBE8DyVP_#0RosvY58=<$Y}WrFL5a>Ua~#Vt7vA0kgbh`gs0hO;M9+f_rX{! zd~mcZ_b(1VyDe5eA)_6 z)`wn)z?j?|M&%LcM!5xl^^sHxE_l3vvlq^A5-MX+?qv`k#xQ#$DBGI^rB(Z~6X1RZ z4jpyHEiyBYri*+>nLp)}CA9#Zau3jeUBlFzukXb9r|q2WXtqNib;2d|-%}@SGm*WwZV<(%NCP~)A6jFgaO$HSL=GbQKR9B*4e`CAo1n729Z?Y> z4ufW70ycVAZs#exFstTgXd07Vj^>h-y>3fsxfuOL#R@%^R8u!;K7HKuJ< zD=~TudKjQzz!`$Dv)Pj|$fW?oV0=%r@e>fUKoMeBd{IPq%04Vt-iYL`W|8bn5DVc+ zIP@1_D&<~htSra3WZGi)z>~99K#w^A9axoGi+c_~YAwdbV2|LEb88`8=YC~7_iCKN zKEy^Jcin+FNxJ6#DDCR)MBhX-Y>L&r7{kb`l+D&|udRANR=+W;6xi1KwxW9*8mzLd zZ5hLw*(zBqb!51vtP71(c1wdfEe+ebb8HX>@F(h*!K`^Xj(bi9OJwjlIsK2n?OEjb zv(V8^pEuxH6rf^h>sV>+FSYg;qZgN3FBM5-T?lgGdbi98>zyHPlv_X0!-<>2++I$! GUiUxaIZ}D2FMICTGvfq9 z*oSJ5HTT}RbHC=^^E>B^_qm*Q1oDr8uVr5YLO+ljL+EN@?blG4LFZA7`T(hwoKh+M zo>pl+XH*7q`UCb9r*fxks?CpLj2FdNFPdyr8)BRnseH@^sSs;`)E?s@6=PI43JE`v ziAF-S<~k`)N)bs(VL7K}u_laUPex8)oW&^$nvjbKSr`M?+HOc@P#m47kNy^+G}LvX zPpAnhMvcOgiu92w=$}X*jUG>jD2oVmz(%=B{XtL?W5VWYwf9B@$SK zAqBV;4Zp zA2sWuF00lk9X*TCh04JoaxmQ}fcBxD>f;OWcAZG3{m2J%lHW&z1B?&JREWNMEIc8l zE(X+zqzZ`?xFBDaGJ(w0r7;NyWF-LG9mwXODyamLfpJisK-$!ukr1!h;Mm}#G#gIJ zilk@_AD`BhNu#rh#wu`F8h1H4IVEYFgmEUTaVLg{A}2#MCdZ-i(r2lJoEgt5GjS#G&_vda8of z{`p3#5=Sw#pNv*;>7FI72IRZLJb^8UKSDMO*^s3RSxqc!U= z`WWgOr#7of$4e?1qeq=aud}o<)4Nb*DcGo~g3(5!t(5~f&BmS4|K*kX(d$?GGAyI5 z#;Dc~kXG9&`==FU-v){PDiVxFB}X({YBH-x>5zzggz$=zcUHiA~e;WIW$BfxaPlG*l-NK?3@lecy0Rz)p9;Yx zUM$%2_L~mUVb=>A;+p4n{grK9Z-V|X!N$5B>Y?q1vJ;l}pmk@sk$d=yrf@em-zbIy z!hE+Fe&-b+z4#eGs=>pA#!`+Pfx*NE7F?BA!SH&8tcQ+;O8n; z(_*}}m4i<*O$h0`EkJJ>0Btw!nVNBBWB`TRU~WxFWiyw-rc3FODpj}x>TxGY2n)DN zZ>DAwI5|zMzMoWX=1Jo!hy>u#W`JBeKvw6spamIFxd%yIh!%wXGsDaFoyFz_d*@7e zNpuz7%fENySn*(4^p#j&N%XBi-C%z3#{0!FlbKbP<64*8?RV05-}y!^yL(D}Ps!c0 z?DQ1Q&7QmS?xJ(gQ|F!~hr94${==Ie0ZOjDfD%B=&+>E4UpUqqpaxp~r>6FnV?)^u z3w_XYDC|cMJDS1{?%{qO%JU8}+$qd^^|F6w_y|+qPmO@5*gmBB|M)2##5!IHLZMd& zR!p3Unw|)@rYfN3)jno;1IhytDFi;?%^32tK!~xU#1`xFN<>W`7B&5r(FY;$W+p<$ z%&+ifBud#LhN%I}wybQFK%q-5j9bqqN)Xv1#+WgBvoDV^+k$-E0Qt4S;Ybx+t^zJK ztue{5)Ck@K3kkV&hggHI4{9rpumR{JzEI<)v8+n<_|+NLouVdSDVa{F(r0Rj#X&L% zG2wU@BpNlYu#kgythz^sK{pv6_rtRTa4CO*q%PZ=9i#>Mir^@85F?wzZYfb#4 zEk;`_2Sc#KxHI~{{C*JDGEoz4qdhYBkp!%pzX3;0#4Q5_qo61OYl|5BN=Dfh_N#9M z`!9`FXSSKq^5;66U=h+)li=2DN;Y*-QZ;)e&n0E7H7HUhot&K1Y-veNrY0&jLI4|4 zZ4B{p>N0eJzXDWcT87xTj>ESM1}4WGvT3yPHe3faMu8APw-ziJP@~2a@-YP_1Elg5 zX~qPV8S!y=J_wgmh6L<})3fO8dgAO_a0Y&Ja%Omyc5|L3_>_bgc(6Hg~ZLoU3RIl<@dsuLUGqu&g(;e^RyJbi*5TJx9$6W zXtAyLNn3Bp(OdHLmfFZyr^^Q+cw@)^8aM2iOO^Td6<8LZ=fAM8x57Y$1PR3}QN*fU zk1g{2E&GNt3@`SFAvO|14}AR*hI{CuN!oM_YUZ6yksaK8mj}v^7%}1z9=Ui@-XTWz z3Xg(%In*2(U_uT|0H|2{gQP^FFs-5b&rBs+BL-Tx(^b literal 0 HcmV?d00001 diff --git a/lib/jeepney/tests/__pycache__/test_low_level.cpython-314.pyc b/lib/jeepney/tests/__pycache__/test_low_level.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac4a1a737626db848be2bab1f26c36e8d60252c0 GIT binary patch literal 6499 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/jeepney/tests/test_auth.py b/lib/jeepney/tests/test_auth.py new file mode 100644 index 0000000..62d9228 --- /dev/null +++ b/lib/jeepney/tests/test_auth.py @@ -0,0 +1,24 @@ +import pytest + +from jeepney import auth + +def test_make_auth_external(): + b = auth.make_auth_external() + assert b.startswith(b'AUTH EXTERNAL') + +def test_make_auth_anonymous(): + b = auth.make_auth_anonymous() + assert b.startswith(b'AUTH ANONYMOUS') + +def test_parser(): + p = auth.SASLParser() + p.feed(b'OK 728d62bc2eb394') + assert not p.authenticated + p.feed(b'1ebbb0b42958b1e0d6\r\n') + assert p.authenticated + +def test_parser_rejected(): + p = auth.SASLParser() + with pytest.raises(auth.AuthenticationError): + p.feed(b'REJECTED EXTERNAL\r\n') + assert not p.authenticated diff --git a/lib/jeepney/tests/test_bindgen.py b/lib/jeepney/tests/test_bindgen.py new file mode 100644 index 0000000..ef9571b --- /dev/null +++ b/lib/jeepney/tests/test_bindgen.py @@ -0,0 +1,28 @@ +from io import StringIO +import os.path + +from jeepney.low_level import MessageType, HeaderFields +from jeepney.bindgen import code_from_xml + +sample_file = os.path.join(os.path.dirname(__file__), 'secrets_introspect.xml') + +def test_bindgen(): + with open(sample_file) as f: + xml = f.read() + sio = StringIO() + n_interfaces = code_from_xml(xml, path='/org/freedesktop/secrets', + bus_name='org.freedesktop.secrets', + fh=sio) + # 5 interfaces defined, but we ignore Properties, Introspectable, Peer + assert n_interfaces == 2 + + # Run the generated code, defining the message generator classes. + binding_ns = {} + exec(sio.getvalue(), binding_ns) + Service = binding_ns['Service'] + + # Check basic functionality of the Service class + assert Service.interface == 'org.freedesktop.Secret.Service' + msg = Service().SearchItems({"service": "foo", "user": "bar"}) + assert msg.header.message_type is MessageType.method_call + assert msg.header.fields[HeaderFields.destination] == 'org.freedesktop.secrets' diff --git a/lib/jeepney/tests/test_bus.py b/lib/jeepney/tests/test_bus.py new file mode 100644 index 0000000..70dfa36 --- /dev/null +++ b/lib/jeepney/tests/test_bus.py @@ -0,0 +1,24 @@ +import pytest +from testpath import modified_env + +from jeepney import bus + +def test_get_connectable_addresses(): + a = list(bus.get_connectable_addresses('unix:path=/run/user/1000/bus')) + assert a == ['/run/user/1000/bus'] + + a = list(bus.get_connectable_addresses('unix:abstract=/tmp/foo')) + assert a == ['\0/tmp/foo'] + + with pytest.raises(RuntimeError): + list(bus.get_connectable_addresses('unix:tmpdir=/tmp')) + +def test_get_bus(): + with modified_env({ + 'DBUS_SESSION_BUS_ADDRESS':'unix:path=/run/user/1000/bus', + 'DBUS_SYSTEM_BUS_ADDRESS': 'unix:path=/var/run/dbus/system_bus_socket' + }): + assert bus.get_bus('SESSION') == '/run/user/1000/bus' + assert bus.get_bus('SYSTEM') == '/var/run/dbus/system_bus_socket' + + assert bus.get_bus('unix:path=/run/user/1002/bus') == '/run/user/1002/bus' diff --git a/lib/jeepney/tests/test_bus_messages.py b/lib/jeepney/tests/test_bus_messages.py new file mode 100644 index 0000000..50069b5 --- /dev/null +++ b/lib/jeepney/tests/test_bus_messages.py @@ -0,0 +1,112 @@ +from jeepney import DBusAddress, new_signal, new_method_call +from jeepney.bus_messages import MatchRule, message_bus + +portal = DBusAddress( + object_path='/org/freedesktop/portal/desktop', + bus_name='org.freedesktop.portal.Desktop', +) +portal_req_iface = portal.with_interface('org.freedesktop.portal.Request') + + +def test_match_rule_simple(): + rule = MatchRule( + type='signal', interface='org.freedesktop.portal.Request', + ) + assert rule.matches(new_signal(portal_req_iface, 'Response')) + + # Wrong message type + assert not rule.matches(new_method_call(portal_req_iface, 'Boo')) + + # Wrong interface + assert not rule.matches(new_signal( + portal.with_interface('org.freedesktop.portal.FileChooser'), 'Response' + )) + + +def test_match_rule_path_namespace(): + assert MatchRule(path_namespace='/org/freedesktop/portal').matches( + new_signal(portal_req_iface, 'Response') + ) + assert "/freedesktop/" in ( + MatchRule(path_namespace='/org/freedesktop/portal').serialise() + ) + + # Prefix but not a parent in the path hierarchy + assert not MatchRule(path_namespace='/org/freedesktop/por').matches( + new_signal(portal_req_iface, 'Response') + ) + + +def test_match_rule_arg(): + rule = MatchRule(type='method_call') + rule.add_arg_condition(0, 'foo') + + assert rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('foo',) + )) + + assert not rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('foobar',) + )) + + # No such argument + assert not rule.matches(new_method_call(portal_req_iface, 'Boo')) + + +def test_match_rule_arg_path(): + rule = MatchRule(type='method_call') + rule.add_arg_condition(0, '/aa/bb/', kind='path') + + # Exact match + assert rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('/aa/bb/',) + )) + + # Match a prefix + assert rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('/aa/bb/cc',) + )) + + # Argument is a prefix, ending with / + assert rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('/aa/',) + )) + + # Argument is a prefix, but NOT ending with / + assert not rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('/aa',) + )) + + assert not rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='s', body=('/aa/bb',) + )) + + # Not a string + assert not rule.matches(new_method_call( + portal_req_iface, 'Boo', signature='u', body=(12,) + )) + + +def test_match_rule_arg_namespace(): + rule = MatchRule(member='NameOwnerChanged') + rule.add_arg_condition(0, 'com.example.backend1', kind='namespace') + + # Exact match + assert rule.matches(new_signal( + message_bus, 'NameOwnerChanged', 's', ('com.example.backend1',) + )) + + # Parent of the name + assert rule.matches(new_signal( + message_bus, 'NameOwnerChanged', 's', ('com.example.backend1.foo.bar',) + )) + + # Prefix but not a parent in the namespace + assert not rule.matches(new_signal( + message_bus, 'NameOwnerChanged', 's', ('com.example.backend12',) + )) + + # Not a string + assert not rule.matches(new_signal( + message_bus, 'NameOwnerChanged', 'u', (1,) + )) diff --git a/lib/jeepney/tests/test_fds.py b/lib/jeepney/tests/test_fds.py new file mode 100644 index 0000000..5d197fb --- /dev/null +++ b/lib/jeepney/tests/test_fds.py @@ -0,0 +1,80 @@ +import errno +import os +import socket + +import pytest + +from jeepney import FileDescriptor, NoFDError + +def assert_not_fd(fd: int): + """Check that the given number is not open as a file descriptor""" + with pytest.raises(OSError) as exc_info: + os.stat(fd) + assert exc_info.value.errno == errno.EBADF + + +def test_close(tmp_path): + fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR) + + with FileDescriptor(fd) as wfd: + assert wfd.fileno() == fd + # Leaving the with block is equivalent to calling .close() + + assert 'closed' in repr(wfd) + with pytest.raises(NoFDError): + wfd.fileno() + + assert_not_fd(fd) + + +def test_to_raw_fd(tmp_path): + fd = os.open(tmp_path / 'a', os.O_CREAT) + wfd = FileDescriptor(fd) + assert wfd.fileno() == fd + + assert wfd.to_raw_fd() == fd + + try: + assert 'converted' in repr(wfd) + with pytest.raises(NoFDError): + wfd.fileno() + finally: + os.close(fd) + + +def test_to_file(tmp_path): + fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR) + wfd = FileDescriptor(fd) + + with wfd.to_file('w') as f: + assert f.write('abc') + + assert 'converted' in repr(wfd) + with pytest.raises(NoFDError): + wfd.fileno() + + assert_not_fd(fd) # Check FD was closed by file object + + assert (tmp_path / 'a').read_text() == 'abc' + + +def test_to_socket(): + s1, s2 = socket.socketpair() + try: + s1.sendall(b'abcd') + sfd = s2.detach() + wfd = FileDescriptor(sfd) + + with wfd.to_socket() as sock: + b = sock.recv(16) + assert b and b'abcd'.startswith(b) + + assert 'converted' in repr(wfd) + with pytest.raises(NoFDError): + wfd.fileno() + + assert_not_fd(sfd) # Check FD was closed by socket object + finally: + s1.close() + + diff --git a/lib/jeepney/tests/test_low_level.py b/lib/jeepney/tests/test_low_level.py new file mode 100644 index 0000000..ce8b4ee --- /dev/null +++ b/lib/jeepney/tests/test_low_level.py @@ -0,0 +1,101 @@ +import pytest +from jeepney.low_level import * + +HELLO_METHOD_CALL = ( + b'l\x01\x00\x01\x00\x00\x00\x00\x01\x00\x00\x00m\x00\x00\x00\x01\x01o\x00\x15' + b'\x00\x00\x00/org/freedesktop/DBus\x00\x00\x00\x02\x01s\x00\x14\x00\x00\x00' + b'org.freedesktop.DBus\x00\x00\x00\x00\x03\x01s\x00\x05\x00\x00\x00Hello\x00' + b'\x00\x00\x06\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00') + + +def test_parser_simple(): + msg = Parser().feed(HELLO_METHOD_CALL)[0] + assert msg.header.fields[HeaderFields.member] == 'Hello' + +def chunks(src, size): + pos = 0 + while pos < len(src): + end = pos + size + yield src[pos:end] + pos = end + +def test_parser_chunks(): + p = Parser() + chunked = list(chunks(HELLO_METHOD_CALL, 16)) + for c in chunked[:-1]: + assert p.feed(c) == [] + msg = p.feed(chunked[-1])[0] + assert msg.header.fields[HeaderFields.member] == 'Hello' + +def test_multiple(): + msgs = Parser().feed(HELLO_METHOD_CALL * 6) + assert len(msgs) == 6 + for msg in msgs: + assert msg.header.fields[HeaderFields.member] == 'Hello' + +def test_roundtrip(): + msg = Parser().feed(HELLO_METHOD_CALL)[0] + assert msg.serialise() == HELLO_METHOD_CALL + +def test_serialise_dict(): + data = { + 'a': 'b', + 'de': 'f', + } + string_type = simple_types['s'] + sig = Array(DictEntry([string_type, string_type])) + print(sig.serialise(data, 0, Endianness.little)) + assert sig.serialise(data, 0, Endianness.little) == ( + b'\x1e\0\0\0' + # Length + b'\0\0\0\0' + # Padding + b'\x01\0\0\0a\0\0\0' + + b'\x01\0\0\0b\0\0\0' + + b'\x02\0\0\0de\0\0' + + b'\x01\0\0\0f\0' + ) + +def test_parse_signature(): + sig = parse_signature(list('(a{sv}(oayays)b)')) + print(sig) + assert sig == Struct([ + Array(DictEntry([simple_types['s'], Variant()])), + Struct([ + simple_types['o'], + Array(simple_types['y']), + Array(simple_types['y']), + simple_types['s'] + ]), + simple_types['b'], + ]) + +class fake_list(list): + def __init__(self, n): + super().__init__() + self._n = n + + def __len__(self): + return self._n + + def __iter__(self): + return iter(range(self._n)) + +def test_array_limit(): + # The spec limits arrays to 64 MiB + a = Array(FixedType(8, 'Q')) # 'at' - array of uint64 + a.serialise(fake_list(100), 0, Endianness.little) + with pytest.raises(SizeLimitError): + a.serialise(fake_list(2**23 + 1), 0, Endianness.little) + + +def test_bad_object_path(): + with pytest.raises(ValueError): + ObjectPathType().check_data('org/freedesktop/DBus') + + with pytest.raises(ValueError): + ObjectPathType().check_data('/org/freedesktop/DBus/') + + with pytest.raises(ValueError): + ObjectPathType().check_data('/org//freedesktop/DBus') + + with pytest.raises(ValueError): + ObjectPathType().check_data('/org/freedesktop/DBüs') # Non-ASCII character diff --git a/lib/jeepney/tests/test_wrappers.py b/lib/jeepney/tests/test_wrappers.py new file mode 100644 index 0000000..636feef --- /dev/null +++ b/lib/jeepney/tests/test_wrappers.py @@ -0,0 +1,74 @@ +import pytest + +from jeepney.wrappers import * + +def test_bad_bus_name(): + obj = '/com/example/foo' + DBusAddress(obj, 'com.example.a') # Valid (well known name) + DBusAddress(obj, 'com.example.a-b') # Valid but discouraged + DBusAddress(obj, ':1.13') # Valid (unique name) + + with pytest.raises(ValueError, match='too long'): + DBusAddress(obj, 'com.example.' + ('a' * 256)) + + with pytest.raises(ValueError): + DBusAddress(obj, '.com.example.a') + + with pytest.raises(ValueError): + DBusAddress(obj, 'com..example.a') + + with pytest.raises(ValueError): + DBusAddress(obj, 'com.2example.a') + + with pytest.raises(ValueError): + DBusAddress(obj, 'cöm.example.a') # Non-ASCII character + + with pytest.raises(ValueError): + DBusAddress(obj, 'com') + +def test_bad_interface(): + obj = '/com/example/foo' + busname = 'com.example.foo' + DBusAddress(obj, 'com.example.a', 'com.example.a_b') # Valid + + with pytest.raises(ValueError, match='too long'): + DBusAddress(obj, 'com.example.a', 'com.example.' + ('a' * 256)) + + with pytest.raises(ValueError): + DBusAddress(obj, 'com.example.a', 'com.example.a-b') # No hyphens + + with pytest.raises(ValueError): + DBusAddress(obj, busname, '.com.example.a') + + with pytest.raises(ValueError): + DBusAddress(obj, busname, 'com..example.a') + + with pytest.raises(ValueError): + DBusAddress(obj, busname, 'com.2example.a') + + with pytest.raises(ValueError): + DBusAddress(obj, busname, 'cöm.example.a') # Non-ASCII character + + with pytest.raises(ValueError): + DBusAddress(obj, busname, 'com') + + +def test_bad_member_name(): + addr = DBusAddress( + '/org/freedesktop/DBus', + bus_name='org.freedesktop.DBus', + interface='org.freedesktop.DBus', + ) + new_method_call(addr, 'Hello') + + with pytest.raises(ValueError, match='too long'): + new_method_call(addr, 'Hell' + ('o' * 256)) + + with pytest.raises(ValueError): + new_method_call(addr, 'org.Hello') + + with pytest.raises(ValueError): + new_method_call(addr, '9Hello') + + with pytest.raises(ValueError): + new_method_call(addr, '') diff --git a/lib/jeepney/wrappers.py b/lib/jeepney/wrappers.py new file mode 100644 index 0000000..a3b62c5 --- /dev/null +++ b/lib/jeepney/wrappers.py @@ -0,0 +1,265 @@ +import re +from typing import Union +from warnings import warn + +from .low_level import * + +__all__ = [ + 'DBusAddress', + 'new_method_call', + 'new_method_return', + 'new_error', + 'new_signal', + 'MessageGenerator', + 'Properties', + 'Introspectable', + 'DBusErrorResponse', +] + +bus_name_pat = re.compile( + r'([A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+' # Well known name + r'|:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+))$', # Unique name +) + +def check_bus_name(name): + if len(name) > 255: + abbr = name[:8] + '...' + raise ValueError(f"Bus name ({abbr!r}) is too long (> 255 characters)") + if not bus_name_pat.match(name): + raise ValueError(f"Bus name ({name!r}) is not valid") + +interface_pat = re.compile(r'[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$') + +def check_interface(name): + if len(name) > 255: + abbr = name[:8] + '...' + raise ValueError(f"Interface name ({abbr!r}) is too long (> 255 characters)") + if not interface_pat.match(name): + raise ValueError(f"Interface name ({name!r}) is not valid") + +member_name_pat = re.compile(r'[A-Za-z_][A-Za-z0-9_]*$') + +def check_member_name(name): + if len(name) > 255: + abbr = name[:8] + '...' + raise ValueError(f"Member name ({abbr!r}) is too long (> 255 characters)") + if not member_name_pat.match(name): + raise ValueError(f"Member name ({name!r} is not valid") + + +class DBusAddress: + """This identifies the object and interface a message is for. + + e.g. messages to display desktop notifications would have this address:: + + DBusAddress('/org/freedesktop/Notifications', + bus_name='org.freedesktop.Notifications', + interface='org.freedesktop.Notifications') + """ + def __init__(self, object_path, bus_name=None, interface=None): + ObjectPathType().check_data(object_path) + self.object_path = object_path + + if bus_name is not None: + check_bus_name(bus_name) + self.bus_name = bus_name + + if interface is not None: + check_interface(interface) + self.interface = interface + + def __repr__(self): + return '{}({!r}, bus_name={!r}, interface={!r})'.format(type(self).__name__, + self.object_path, self.bus_name, self.interface) + + def with_interface(self, interface): + check_interface(interface) + return type(self)(self.object_path, self.bus_name, interface) + +class DBusObject(DBusAddress): + def __init__(self, object_path, bus_name=None, interface=None): + super().__init__(object_path, bus_name, interface) + warn('Deprecated alias, use DBusAddress instead', stacklevel=2) + +def new_header(msg_type): + return Header(Endianness.little, msg_type, flags=0, protocol_version=1, + body_length=-1, serial=-1, fields={}) + +def new_method_call(remote_obj, method, signature=None, body=()): + """Construct a new method call message + + This is a relatively low-level method. In many cases, this will be called + from a :class:`MessageGenerator` subclass which provides a more convenient + API. + + :param DBusAddress remote_obj: The object to call a method on + :param str method: The name of the method to call + :param str signature: The DBus signature of the body data + :param tuple body: Body data (i.e. method parameters) + """ + check_member_name(method) + header = new_header(MessageType.method_call) + header.fields[HeaderFields.path] = remote_obj.object_path + if remote_obj.bus_name is None: + raise ValueError("remote_obj.bus_name cannot be None for method calls") + header.fields[HeaderFields.destination] = remote_obj.bus_name + if remote_obj.interface is not None: + header.fields[HeaderFields.interface] = remote_obj.interface + header.fields[HeaderFields.member] = method + if signature is not None: + header.fields[HeaderFields.signature] = signature + + return Message(header, body) + +def new_method_return(parent_msg, signature=None, body=()): + """Construct a new response message + + :param Message parent_msg: The method call this is a reply to + :param str signature: The DBus signature of the body data + :param tuple body: Body data + """ + header = new_header(MessageType.method_return) + header.fields[HeaderFields.reply_serial] = parent_msg.header.serial + sender = parent_msg.header.fields.get(HeaderFields.sender, None) + if sender is not None: + header.fields[HeaderFields.destination] = sender + if signature is not None: + header.fields[HeaderFields.signature] = signature + return Message(header, body) + +def new_error(parent_msg, error_name, signature=None, body=()): + """Construct a new error response message + + :param Message parent_msg: The method call this is a reply to + :param str error_name: The name of the error + :param str signature: The DBus signature of the body data + :param tuple body: Body data + """ + header = new_header(MessageType.error) + header.fields[HeaderFields.reply_serial] = parent_msg.header.serial + header.fields[HeaderFields.error_name] = error_name + sender = parent_msg.header.fields.get(HeaderFields.sender, None) + if sender is not None: + header.fields[HeaderFields.destination] = sender + if signature is not None: + header.fields[HeaderFields.signature] = signature + return Message(header, body) + +def new_signal(emitter, signal, signature=None, body=()): + """Construct a new signal message + + :param DBusAddress emitter: The object sending the signal + :param str signal: The name of the signal + :param str signature: The DBus signature of the body data + :param tuple body: Body data + """ + check_member_name(signal) + header = new_header(MessageType.signal) + header.fields[HeaderFields.path] = emitter.object_path + if emitter.interface is None: + raise ValueError("emitter.interface cannot be None for signals") + header.fields[HeaderFields.interface] = emitter.interface + header.fields[HeaderFields.member] = signal + if signature is not None: + header.fields[HeaderFields.signature] = signature + return Message(header, body) + + +class MessageGenerator: + """Subclass this to define the methods available on a DBus interface. + + jeepney.bindgen can automatically create subclasses using introspection. + """ + interface: Optional[str] = None + + def __init__(self, object_path, bus_name): + ObjectPathType().check_data(object_path) + check_bus_name(bus_name) + if self.interface is not None: + check_interface(self.interface) + + self.object_path = object_path + self.bus_name = bus_name + + def __repr__(self): + return "{}({!r}, bus_name={!r})".format(type(self).__name__, + self.object_path, self.bus_name) + + +class ProxyBase: + """A proxy is an IO-aware wrapper around a MessageGenerator + + Calling methods on a proxy object will send a message and wait for the + reply. This is a base class for proxy implementations in jeepney.io. + """ + def __init__(self, msggen): + self._msggen = msggen + + def __getattr__(self, item): + if item.startswith('__'): + raise AttributeError(item) + + make_msg = getattr(self._msggen, item, None) + if callable(make_msg): + return self._method_call(make_msg) + + raise AttributeError(item) + + def _method_call(self, make_msg): + raise NotImplementedError("Needs to be implemented in subclass") + +class Properties: + """Build messages for accessing object properties + + If a D-Bus object has multiple interfaces, each interface has its own + set of properties. + + This uses the standard DBus interface ``org.freedesktop.DBus.Properties`` + """ + def __init__(self, obj: Union[DBusAddress, MessageGenerator]): + self.obj = obj + self.props_if = DBusAddress(obj.object_path, bus_name=obj.bus_name, + interface='org.freedesktop.DBus.Properties') + + def get(self, name): + """Get the value of the property *name*""" + return new_method_call(self.props_if, 'Get', 'ss', + (self.obj.interface, name)) + + def get_all(self): + """Get all property values for this interface""" + return new_method_call(self.props_if, 'GetAll', 's', + (self.obj.interface,)) + + def set(self, name, signature, value): + """Set the property *name* to *value* (with appropriate signature)""" + return new_method_call(self.props_if, 'Set', 'ssv', + (self.obj.interface, name, (signature, value))) + +class Introspectable(MessageGenerator): + interface = 'org.freedesktop.DBus.Introspectable' + + def Introspect(self): + """Request D-Bus introspection XML for a remote object""" + return new_method_call(self, 'Introspect') + +class DBusErrorResponse(Exception): + """Raised by proxy method calls when the reply is an error message""" + def __init__(self, msg): + self.name = msg.header.fields.get(HeaderFields.error_name) + self.data = msg.body + + def __str__(self): + return '[{}] {}'.format(self.name, self.data) + + +def unwrap_msg(msg: Message): + """Get the body of a message, raising DBusErrorResponse for error messages + + This is to be used with replies to method_call messages, which may be + method_return or error. + """ + if msg.header.message_type == MessageType.error: + raise DBusErrorResponse(msg) + + return msg.body diff --git a/lib/keyring-25.7.0.dist-info/INSTALLER b/lib/keyring-25.7.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/keyring-25.7.0.dist-info/METADATA b/lib/keyring-25.7.0.dist-info/METADATA new file mode 100644 index 0000000..76689bd --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/METADATA @@ -0,0 +1,554 @@ +Metadata-Version: 2.4 +Name: keyring +Version: 25.7.0 +Summary: Store and access your passwords safely. +Author-email: Kang Zhang +Maintainer-email: "Jason R. Coombs" +License-Expression: MIT +Project-URL: Source, https://github.com/jaraco/keyring +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: pywin32-ctypes>=0.2.0; sys_platform == "win32" +Requires-Dist: SecretStorage>=3.2; sys_platform == "linux" +Requires-Dist: jeepney>=0.4.2; sys_platform == "linux" +Requires-Dist: importlib_metadata>=4.11.4; python_version < "3.12" +Requires-Dist: jaraco.classes +Requires-Dist: jaraco.functools +Requires-Dist: jaraco.context +Provides-Extra: test +Requires-Dist: pytest!=8.1.*,>=6; extra == "test" +Requires-Dist: pyfakefs; extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx>=3.5; extra == "doc" +Requires-Dist: jaraco.packaging>=9.3; extra == "doc" +Requires-Dist: rst.linker>=1.9; extra == "doc" +Requires-Dist: furo; extra == "doc" +Requires-Dist: sphinx-lint; extra == "doc" +Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" +Provides-Extra: check +Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" +Provides-Extra: cover +Requires-Dist: pytest-cov; extra == "cover" +Provides-Extra: enabler +Requires-Dist: pytest-enabler>=3.4; extra == "enabler" +Provides-Extra: type +Requires-Dist: pytest-mypy>=1.0.1; extra == "type" +Requires-Dist: pygobject-stubs; extra == "type" +Requires-Dist: shtab; extra == "type" +Requires-Dist: types-pywin32; extra == "type" +Provides-Extra: completion +Requires-Dist: shtab>=1.1.0; extra == "completion" +Dynamic: license-file + +.. image:: https://img.shields.io/pypi/v/keyring.svg + :target: https://pypi.org/project/keyring + +.. image:: https://img.shields.io/pypi/pyversions/keyring.svg + +.. image:: https://github.com/jaraco/keyring/actions/workflows/main.yml/badge.svg + :target: https://github.com/jaraco/keyring/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://readthedocs.org/projects/keyring/badge/?version=latest + :target: https://keyring.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2025-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/keyring + :target: https://tidelift.com/subscription/pkg/pypi-keyring?utm_source=pypi-keyring&utm_medium=readme + +.. image:: https://badges.gitter.im/jaraco/keyring.svg + :alt: Join the chat at https://gitter.im/jaraco/keyring + :target: https://gitter.im/jaraco/keyring?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + +The Python keyring library provides an easy way to access the +system keyring service from python. It can be used in any +application that needs safe password storage. + +These recommended keyring backends are supported: + +* macOS `Keychain + `_ +* Freedesktop `Secret Service + `_ supports many DE including + GNOME (requires `secretstorage `_) +* KDE4 & KDE5 `KWallet `_ + (requires `dbus `_) +* `Windows Credential Locker + `_ + +Other keyring implementations are available through `Third-Party Backends`_. + +Installation - Linux +==================== + +On Linux, the KWallet backend relies on dbus-python_, which does not always +install correctly when using pip (compilation is needed). For best results, +install dbus-python as a system package. + +.. _dbus-python: https://gitlab.freedesktop.org/dbus/dbus-python + +Compatibility - macOS +===================== + +macOS keychain supports macOS 11 (Big Sur) and later requires Python 3.8.7 +or later with the "universal2" binary. See +`#525 `_ for details. + +Using Keyring +============= + +The basic usage of keyring is pretty simple: just call +``keyring.set_password`` and ``keyring.get_password``:: + + >>> import keyring + >>> keyring.set_password("system", "username", "password") + >>> keyring.get_password("system", "username") + 'password' + +Command-line Utility +-------------------- + +Keyring supplies a ``keyring`` command which is installed with the +package. After installing keyring in most environments, the +command should be available for setting, getting, and deleting +passwords. For more usage information, invoke with no arguments +or with ``--help`` as so:: + + $ keyring --help + $ keyring set system username + Password for 'username' in 'system': + $ keyring get system username + password + +The command-line functionality is also exposed as an executable +package, suitable for invoking from Python like so:: + + $ python -m keyring --help + $ python -m keyring set system username + Password for 'username' in 'system': + $ python -m keyring get system username + password + +Tab Completion +-------------- + +If installed via a package manager (apt, pacman, nix, homebrew, etc), +these shell completions may already have been distributed with the package +(no action required). + +Keyring provides tab completion if the ``completion`` extra is installed:: + + $ pip install 'keyring[completion]' + +Then, generate shell completions, something like:: + + $ keyring --print-completion bash | sudo tee /usr/share/bash-completion/completions/keyring + $ keyring --print-completion zsh | sudo tee /usr/share/zsh/site-functions/_keyring + $ keyring --print-completion tcsh | sudo tee /etc/profile.d/keyring.csh + +**Note**: the path of `/usr/share` is mainly for GNU/Linux. For other OSs, +consider: + +- macOS (Homebrew x86): /usr/local/share +- macOS (Homebrew ARM): /opt/homebrew/share +- Android (Termux): /data/data/com.termux/files/usr/share +- Windows (mingw64 of msys2): /mingw64/share +- ... + +After installing the shell completions, enable them following your shell's +recommended instructions. e.g.: + +- bash: install `bash-completion `_, + and ensure ``. /usr/share/bash-completion/bash_completion`` in ``~/.bashrc``. +- zsh: ensure ``autoload -Uz compinit && compinit`` appears in ``~/.zshrc``, + then ``grep -w keyring ~/.zcompdump`` to verify keyring appears, indicating + it was installed correctly. + +Configuring +=========== + +The python keyring lib contains implementations for several backends. The +library will attempt to +automatically choose the most suitable backend for the current +environment. Users may also specify the preferred keyring in a +config file or by calling the ``set_keyring()`` function. + +Config file path +---------------- + +The configuration is stored in a file named "keyringrc.cfg" +found in a platform-specific location. To determine +where the config file is stored, run ``keyring diagnose``. + +Config file content +------------------- + +To specify a keyring backend, set the **default-keyring** option to the +full path of the class for that backend, such as +``keyring.backends.macOS.Keyring``. + +If **keyring-path** is indicated, keyring will add that path to the Python +module search path before loading the backend. + +For example, this config might be used to load the +``SimpleKeyring`` from the ``simplekeyring`` module in +the ``./demo`` directory (not implemented):: + + [backend] + default-keyring=simplekeyring.SimpleKeyring + keyring-path=demo + +Third-Party Backends +==================== + +In addition to the backends provided by the core keyring package for +the most common and secure use cases, there +are additional keyring backend implementations available for other +use cases. Simply install them to make them available: + +- `keyrings.cryptfile `_ + - Encrypted text file storage. +- `keyrings.alt `_ - "alternate", + possibly-insecure backends, originally part of the core package, but + available for opt-in. +- `gsheet-keyring `_ + - a backend that stores secrets in a Google Sheet. For use with + `ipython-secrets `_. +- `bitwarden-keyring `_ + - a backend that stores secrets in the `BitWarden `_ + password manager. +- `onepassword-keyring `_ + - a backend that stores secrets in the `1Password `_ password manager. +- `sagecipher `_ - an encryption + backend which uses the ssh agent protocol's signature operation to + derive the cipher key. +- `keyrings.osx_keychain_keys `_ + - OSX keychain key-management, for private, public, and symmetric keys. +- `keyring_pass.PasswordStoreBackend `_ + - Password Store (pass) backend for python's keyring +- `keyring_jeepney `__ - a + pure Python backend using the secret service DBus API for desktop + Linux (requires ``keyring<24``). + + +Write your own keyring backend +============================== + +The interface for the backend is defined by ``keyring.backend.KeyringBackend``. +Every backend should derive from that base class and define a ``priority`` +attribute and three functions: ``get_password()``, ``set_password()``, and +``delete_password()``. The ``get_credential()`` function may be defined if +desired. + +See the ``backend`` module for more detail on the interface of this class. + +Keyring employs entry points to allow any third-party package to implement +backends without any modification to the keyring itself. Those interested in +creating new backends are encouraged to create new, third-party packages +in the ``keyrings`` namespace, in a manner modeled by the `keyrings.alt +package `_. See the +``setup.cfg`` file +in that project for hints on how to create the requisite entry points. +Backends that prove essential may be considered for inclusion in the core +library, although the ease of installing these third-party packages should +mean that extensions may be readily available. + +To create an extension for Keyring, please submit a pull request to +have your extension mentioned as an available extension. + +Runtime Configuration +===================== + +Keyring additionally allows programmatic configuration of the +backend calling the api ``set_keyring()``. The indicated backend +will subsequently be used to store and retrieve passwords. + +To invoke ``set_keyring``:: + + # define a new keyring class which extends the KeyringBackend + import keyring.backend + + class TestKeyring(keyring.backend.KeyringBackend): + """A test keyring which always outputs the same password + """ + priority = 1 + + def set_password(self, servicename, username, password): + pass + + def get_password(self, servicename, username): + return "password from TestKeyring" + + def delete_password(self, servicename, username): + pass + + # set the keyring for keyring lib + keyring.set_keyring(TestKeyring()) + + # invoke the keyring lib + try: + keyring.set_password("demo-service", "tarek", "passexample") + print("password stored successfully") + except keyring.errors.PasswordSetError: + print("failed to store password") + print("password", keyring.get_password("demo-service", "tarek")) + + +Disabling Keyring +================= + +In many cases, uninstalling keyring will never be necessary. +Especially on Windows and macOS, the behavior of keyring is +usually degenerate, meaning it will return empty values to +the caller, allowing the caller to fall back to some other +behavior. + +In some cases, the default behavior of keyring is undesirable and +it would be preferable to disable the keyring behavior altogether. +There are several mechanisms to disable keyring: + +- Uninstall keyring. Most applications are tolerant to keyring + not being installed. Uninstalling keyring should cause those + applications to fall back to the behavior without keyring. + This approach affects the Python environment where keyring + would otherwise have been installed. + +- Configure the Null keyring in the environment. Set + ``PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring`` + in the environment, and the ``Null`` (degenerate) backend + will be used. This approach affects all uses of Keyring where + that variable is set. + +- Permanently configure the Null keyring for the user by running + ``keyring --disable`` or ``python -m keyring --disable``. + This approach affects all uses of keyring for that user. + + +Altering Keyring Behavior +========================= + +Keyring provides a mechanism to alter the keyring's behavior through +environment variables. Each backend implements a +``KeyringBackend.set_properties_from_env``, which +when invoked will find all environment variables beginning with +``KEYRING_PROPERTY_{NAME}`` and will set a property for each +``{NAME.lower()}`` on the keyring. This method is invoked during +initialization for the default/configured keyring. + +This mechanism may be used to set some useful values on various +keyrings, including: + +- keychain; macOS, path to an alternate keychain file +- appid; Linux/SecretService, alternate ID for the application + + +Using Keyring on Ubuntu 16.04 +============================= + +The following is a complete transcript for installing keyring in a +virtual environment on Ubuntu 16.04. No config file was used:: + + $ sudo apt install python3-venv libdbus-glib-1-dev + $ cd /tmp + $ pyvenv py3 + $ source py3/bin/activate + $ pip install -U pip + $ pip install secretstorage dbus-python + $ pip install keyring + $ python + >>> import keyring + >>> keyring.get_keyring() + + >>> keyring.set_password("system", "username", "password") + >>> keyring.get_password("system", "username") + 'password' + + +Using Keyring on headless Linux systems +======================================= + +It is possible to use the SecretService backend on Linux systems without +X11 server available (only D-Bus is required). In this case: + +* Install the `GNOME Keyring`_ daemon. +* Start a D-Bus session, e.g. run ``dbus-run-session -- sh`` and run + the following commands inside that shell. +* Run ``gnome-keyring-daemon`` with ``--unlock`` option. The description of + that option says: + + Read a password from stdin, and use it to unlock the login keyring + or create it if the login keyring does not exist. + + When that command is started, enter a password into stdin and + press Ctrl+D (end of data). After that, the daemon will fork into + the background (use ``--foreground`` option to block). +* Now you can use the SecretService backend of Keyring. Remember to + run your application in the same D-Bus session as the daemon. + +.. _GNOME Keyring: https://wiki.gnome.org/Projects/GnomeKeyring + +Using Keyring on headless Linux systems in a Docker container +============================================================= + +It is possible to use keyring with the SecretService backend in Docker containers as well. +All you need to do is install the necessary dependencies and add the `--privileged` flag +to avoid any `Operation not permitted` errors when attempting to unlock the system's keyring. + +The following is a complete transcript for installing keyring on a Ubuntu 18:04 container:: + + docker run -it -d --privileged ubuntu:18.04 + + $ apt-get update + $ apt install -y gnome-keyring python3-venv python3-dev + $ python3 -m venv venv + $ source venv/bin/activate # source a virtual environment to avoid polluting your system + $ pip3 install --upgrade pip + $ pip3 install keyring + $ dbus-run-session -- sh # this will drop you into a new D-bus shell + $ echo 'somecredstorepass' | gnome-keyring-daemon --unlock # unlock the system's keyring + + $ python + >>> import keyring + >>> keyring.get_keyring() + + >>> keyring.set_password("system", "username", "password") + >>> keyring.get_password("system", "username") + 'password' + +Using Keyring with tox +====================== + +Some backends rely on environment variables to operate correctly, and ``tox`` filters most environment variables by default. + +For example, when using Keyring to store credentials for pip, one may encounter the following error when +running tests under ``tox`` when using a backend reliant on D-Bus: + + RuntimeError: No recommended backend was available. Install the keyrings.alt package if you want to use the non-recommended backends. See README.rst for details. + +This error is caused by Keyring KWallet backend not able to resolve the backing service. + +To work around the issue, add ``DBUS_SESSION_BUS_ADDRESS`` to ``pass_env`` in the +``tox`` configuration. Consider adding other necessary variables, such as ``DISPLAY`` and ``WAYLAND_DISPLAY`` (if using ``pinentry``). + +Integration +=========== + +API +--- + +The keyring lib has a few functions: + +* ``get_keyring()``: Return the currently-loaded keyring implementation. +* ``get_password(service, username)``: Returns the password stored in the + active keyring. If the password does not exist, it will return None. +* ``get_credential(service, username)``: Return a credential object stored + in the active keyring. This object contains at least ``username`` and + ``password`` attributes for the specified service, where the returned + ``username`` may be different from the argument. +* ``set_password(service, username, password)``: Store the password in the + keyring. +* ``delete_password(service, username)``: Delete the password stored in + keyring. If the password does not exist, it will raise an exception. + +In all cases, the parameters (``service``, ``username``, ``password``) +should be Unicode text. + + +Exceptions +---------- + +The keyring lib raises the following exceptions: + +* ``keyring.errors.KeyringError``: Base Error class for all exceptions in keyring lib. +* ``keyring.errors.InitError``: Raised when the keyring cannot be initialized. +* ``keyring.errors.PasswordSetError``: Raised when the password cannot be set in the keyring. +* ``keyring.errors.PasswordDeleteError``: Raised when the password cannot be deleted in the keyring. + +Get Involved +============ + +Python keyring lib is an open community project and eagerly +welcomes contributors. + +* Repository: https://github.com/jaraco/keyring/ +* Bug Tracker: https://github.com/jaraco/keyring/issues/ +* Mailing list: http://groups.google.com/group/python-keyring + +Security Considerations +======================= + +Each built-in backend may have security considerations to understand +before using this library. Authors of tools or libraries utilizing +``keyring`` are encouraged to consider these concerns. + +As with any list of known security concerns, this list is not exhaustive. +Additional issues can be added as needed. + +- macOS Keychain + - Any Python script or application can access secrets created by + ``keyring`` from that same Python executable without the operating + system prompting the user for a password. To cause any specific + secret to prompt for a password every time it is accessed, locate + the credential using the ``Keychain Access`` application, and in + the ``Access Control`` settings, remove ``Python`` from the list + of allowed applications. + +- Freedesktop Secret Service + - No analysis has been performed + +- KDE4 & KDE5 KWallet + - No analysis has been performed + +- Windows Credential Locker + - No analysis has been performed + +Making Releases +=============== + +This project makes use of automated releases and continuous +integration. The +simple workflow is to tag a commit and push it to Github. If it +passes tests in CI, it will be automatically deployed to PyPI. + +Other things to consider when making a release: + +- Check that the changelog is current for the intended release. + +Running Tests +============= + +Tests are continuously run in Github Actions. + +To run the tests locally, install and invoke +`tox `_. + +Background +========== + +The project was based on Tarek Ziade's idea in `this post`_. Kang Zhang +initially carried it out as a `Google Summer of Code`_ project, and Tarek +mentored Kang on this project. + +.. _this post: http://tarekziade.wordpress.com/2009/03/27/pycon-hallway-session-1-a-keyring-library-for-python/ +.. _Google Summer of Code: http://socghop.appspot.com/ + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/lib/keyring-25.7.0.dist-info/RECORD b/lib/keyring-25.7.0.dist-info/RECORD new file mode 100644 index 0000000..f0863be --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/RECORD @@ -0,0 +1,68 @@ +../../bin/keyring,sha256=k2jzr6sNOFrQzL2whxOi7SHxOw1QaRbmHwC8EU32iQ4,157 +keyring-25.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +keyring-25.7.0.dist-info/METADATA,sha256=vVTemP7ebcPh882JtON8ldiEKlI727nlKrQO3_GDcWM,21447 +keyring-25.7.0.dist-info/RECORD,, +keyring-25.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +keyring-25.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +keyring-25.7.0.dist-info/entry_points.txt,sha256=8ibyc9zH2ST1JDZHWlQZHEUPx9kVaXfVy8z5af_6OUk,334 +keyring-25.7.0.dist-info/licenses/LICENSE,sha256=WlfLTbheKi3YjCkGKJCK3VfjRRRJ4KmnH9-zh3b9dZ0,1076 +keyring-25.7.0.dist-info/top_level.txt,sha256=ohh1dke28_NdSNkZ6nkVSwIKkLJTOwIfEwnXKva3pkg,8 +keyring/__init__.py,sha256=4bk66hxOsw5JRhyy4I9U8c_VXK-pLusB-YB-aS86ot0,271 +keyring/__main__.py,sha256=vB_vOSk4pIZrkevBQeHXy6GYv7Nd0_vieKe44Xf1i9g,71 +keyring/__pycache__/__init__.cpython-314.pyc,, +keyring/__pycache__/__main__.cpython-314.pyc,, +keyring/__pycache__/backend.cpython-314.pyc,, +keyring/__pycache__/cli.cpython-314.pyc,, +keyring/__pycache__/completion.cpython-314.pyc,, +keyring/__pycache__/core.cpython-314.pyc,, +keyring/__pycache__/credentials.cpython-314.pyc,, +keyring/__pycache__/devpi_client.cpython-314.pyc,, +keyring/__pycache__/errors.cpython-314.pyc,, +keyring/__pycache__/http.cpython-314.pyc,, +keyring/backend.py,sha256=hg5qqlLy2K_KSh2sZ6BM_nFbgIKjFhjz5iJwwsdqIHs,9069 +keyring/backend_complete.bash,sha256=I3bRA3fGR_duzLrJyki94CaxxnelhiiXYyXLvUmlbec,397 +keyring/backend_complete.zsh,sha256=Je9QAn0CbF8_8ssGSkroa4HMcJDB3g20yL8XhhW50fI,451 +keyring/backends/SecretService.py,sha256=qt9lQpa8h6rGnjzTOE8GMIDH2e2J40RIhV3yc1TXSsc,4712 +keyring/backends/Windows.py,sha256=2pi3LSV2RCwXrLYeNplIUVJgPLH5uMnyYcSBgo-6kmw,5727 +keyring/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +keyring/backends/__pycache__/SecretService.cpython-314.pyc,, +keyring/backends/__pycache__/Windows.cpython-314.pyc,, +keyring/backends/__pycache__/__init__.cpython-314.pyc,, +keyring/backends/__pycache__/chainer.cpython-314.pyc,, +keyring/backends/__pycache__/fail.cpython-314.pyc,, +keyring/backends/__pycache__/kwallet.cpython-314.pyc,, +keyring/backends/__pycache__/libsecret.cpython-314.pyc,, +keyring/backends/__pycache__/null.cpython-314.pyc,, +keyring/backends/chainer.py,sha256=-hhe-UWbCn0PAUK-00cWjHz_JJNQf_N4OyHUn89yCOw,2175 +keyring/backends/fail.py,sha256=ef5uP3Ddj2apq2pe08LXI2lLgpkmN0UrKZmOx58UHIU,914 +keyring/backends/kwallet.py,sha256=Le-bwfJVN7dNUiMLYLE66e0HzM5gmJZpXnmLQkDlCEo,5824 +keyring/backends/libsecret.py,sha256=gWeUveE44wZH0j7t2w2L-leYMpJOEHV0OqSUiC-sHQE,5942 +keyring/backends/macOS/__init__.py,sha256=-CIONvwrJFbeuj60opbCMZw4wWtiGyHuGCshocd4Ndg,2589 +keyring/backends/macOS/__pycache__/__init__.cpython-314.pyc,, +keyring/backends/macOS/__pycache__/api.cpython-314.pyc,, +keyring/backends/macOS/api.py,sha256=eikiBaGcYCQpqDsNdLy8wNoB_nFBYfY41j_38vsMKpo,4576 +keyring/backends/null.py,sha256=HW-Ovygh78UebL-ICPTilmCOk37h5WFPvVlMnNP8ElA,438 +keyring/cli.py,sha256=B9084Rmlt4atfQCw2qugMmovVQzeFjkeLRf6vTNcMTI,6605 +keyring/compat/__init__.py,sha256=WXWOxJd1wdBdrTNjKqjt8jOmfIahcIipDahbqdlQ6g8,169 +keyring/compat/__pycache__/__init__.cpython-314.pyc,, +keyring/compat/__pycache__/properties.cpython-314.pyc,, +keyring/compat/__pycache__/py312.cpython-314.pyc,, +keyring/compat/properties.py,sha256=JTlR3v7A5AgK93grI2nIW1sj0efYePgWQURDsWHwzj4,3886 +keyring/compat/py312.py,sha256=euMz5d91tbdrG2JkpoqDu3bBg3Pjzd3pEyWVxSK4IkA,159 +keyring/completion.py,sha256=MSj0qPtLAhhN9kSk34LRzGSYIhS19aG05wlYl_RHG_Q,1450 +keyring/core.py,sha256=2zEOVKitYardvqPDHzMFCRfIB812cuXLbIVh9udbxc0,5848 +keyring/credentials.py,sha256=PWFUzeAEX9FqjYonSIST4y6WHqQ2lKceLcvicKSaipY,2092 +keyring/devpi_client.py,sha256=IpkyYAso0BH9tXpsZ3K1UjJG_Obtj6kTflrpDatNzoQ,603 +keyring/errors.py,sha256=hiHZxG3e1WABMDw80iT0Yg6qrccaVuVUpTNFK7iVmnY,1625 +keyring/http.py,sha256=udH83q5BIrfKYm-4AOuefQ3Avb-J9UbpXBYu49Ik_iA,1214 +keyring/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +keyring/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +keyring/testing/__pycache__/__init__.cpython-314.pyc,, +keyring/testing/__pycache__/backend.cpython-314.pyc,, +keyring/testing/__pycache__/util.cpython-314.pyc,, +keyring/testing/backend.py,sha256=HuCE8NL1rXMIZBrFELce2aO-N5pY3UEtQLDsNdCgvyA,7551 +keyring/testing/util.py,sha256=O15JsfcLIBcnsF1O8LfnbWkeEuiEfbovzQ1h8oN7XUA,1884 +keyring/util/__init__.py,sha256=ilEB7cz4cWl7acmrubGF9142ZeBer1mFqaL0U-7UXAc,302 +keyring/util/__pycache__/__init__.cpython-314.pyc,, +keyring/util/__pycache__/platform_.cpython-314.pyc,, +keyring/util/platform_.py,sha256=lhsGKWZobEvsztNOkotUoNqiHUhJ7G4ENCfdDwp2wVA,1092 diff --git a/lib/keyring-25.7.0.dist-info/REQUESTED b/lib/keyring-25.7.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/lib/keyring-25.7.0.dist-info/WHEEL b/lib/keyring-25.7.0.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/keyring-25.7.0.dist-info/entry_points.txt b/lib/keyring-25.7.0.dist-info/entry_points.txt new file mode 100644 index 0000000..802929d --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/entry_points.txt @@ -0,0 +1,13 @@ +[console_scripts] +keyring = keyring.cli:main + +[devpi_client] +keyring = keyring.devpi_client + +[keyring.backends] +KWallet = keyring.backends.kwallet +SecretService = keyring.backends.SecretService +Windows = keyring.backends.Windows +chainer = keyring.backends.chainer +libsecret = keyring.backends.libsecret +macOS = keyring.backends.macOS diff --git a/lib/keyring-25.7.0.dist-info/licenses/LICENSE b/lib/keyring-25.7.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..f60bd57 --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/licenses/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/keyring-25.7.0.dist-info/top_level.txt b/lib/keyring-25.7.0.dist-info/top_level.txt new file mode 100644 index 0000000..d6fa9c2 --- /dev/null +++ b/lib/keyring-25.7.0.dist-info/top_level.txt @@ -0,0 +1 @@ +keyring diff --git a/lib/keyring/__init__.py b/lib/keyring/__init__.py new file mode 100644 index 0000000..e1ee7a8 --- /dev/null +++ b/lib/keyring/__init__.py @@ -0,0 +1,17 @@ +from .core import ( + delete_password, + get_credential, + get_keyring, + get_password, + set_keyring, + set_password, +) + +__all__ = ( + 'set_keyring', + 'get_keyring', + 'set_password', + 'get_password', + 'delete_password', + 'get_credential', +) diff --git a/lib/keyring/__main__.py b/lib/keyring/__main__.py new file mode 100644 index 0000000..5dd75f4 --- /dev/null +++ b/lib/keyring/__main__.py @@ -0,0 +1,4 @@ +if __name__ == '__main__': + from keyring import cli + + cli.main() diff --git a/lib/keyring/__pycache__/__init__.cpython-314.pyc b/lib/keyring/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1ba56cccd3e0691e008d9313adbfaa09795544c GIT binary patch literal 375 zcmYk1yH3L}6ozf*;)s-1Vqj%p3aD5@LJS~gvoPcVutH-63pcTxQ=x2#hu|G}C9h0~ zrBa8k*rf%|@ag>i&&{Xf{XOL4>*`HSFhUQu3wT{pkI(#pj(nyl%86mTbf>ymuQ+}q@RPO4oI$ghk*Tr3A9J}@&fGTvb3?x?!LEP0nj X`T+}jyGN5po7YEn1{SFz4xkbM-_k<6 literal 0 HcmV?d00001 diff --git a/lib/keyring/__pycache__/backend.cpython-314.pyc b/lib/keyring/__pycache__/backend.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..265cc6f4a4b87eaa7eee0ea5f59c50e06ca1a932 GIT binary patch literal 15492 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 bZOyYaKOKE(^y1{JQ+L=#xT9d(=-&B%lX(We literal 0 HcmV?d00001 diff --git a/lib/keyring/__pycache__/completion.cpython-314.pyc b/lib/keyring/__pycache__/completion.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42268114d57a5144271a33117e0c160942172d82 GIT binary patch literal 2984 zcmb7GO>7&-6@J6rC3pEFO0;F!sb$f2M4PrrHMJ7ikl@xy+gSN=j7=iDi0s8`xstY~ zxZ9awD)SQd2dI5%j3Q`N^w7UU>)v`wj_t8PfjU(Zt78=jkWCRFCu7ez9#DRZeX8up6I z0^4y#@^!o3beYKcfi&M@T)vt~%S&>IvlDzJm~ZlC}0Ku7xV4n{&s>Xt^J?zCmC__bzT&a;;Fou*xnIC;xj^eE$Xui-X)%c2E5 zE`}{M(&nCH*&K~sXmXd~(IG@qw;8K>4tHFM8hx9KZQ?Qc#lA*|kW%TOkYsSe@d9-pn$>%A=#e{w=Fo`HTcyxjVpt0G5m*f$ z{qItlveKLEIFdGeq^9qwBk!ss8~Q;pcx*F&Y$JDS%K&ZvHfUOMD+}2|`^C_)&TjhQ zNjaA_rgH{~P0*|4G?su7sO#uo+t&PF+7}f?H!lH2$o>V+?hsaVk@NmMR za}(7s(=pT_l3f;)7)r%<*M-K+lG|`s1A0!3Vj5cAFH@bV`W{ilFQr5B7|cU!=zgBGMzBtgM6|ke))m z&{k~jEO)Ui7Q0x|eE{?c6q0$%_9NNB?9E^K)!gr_-&pJUE8n}&esv==*nXk=&Ekj&RI@nVgJbzOid(tRbaLy^rFdbH?Y-h4mxd8eXuS z>uzH=Gu^68@0Ql&inB3~NHZX=Na zzGpL6cx!q+H};`1bZh_JLV4Xd@u5+;bztq;n+Mj7$2YS>Z=GDv9*Wq<*Nt+Vy`4)9 zCfgUb2SLMnci`{pfwj{6>g49+*|%r^IQzrrZoYnZ*~q()X6P9Rt=*FM|{{?@Gm{qxq_sP_q2YL%`;t%RbL zHucBWp87yPva^}lyA4EcjIJ%-)5o_ITztml#o+syJiNKDto)vom2y_-*lxQ0O=g1F zB)Mb8?V&@8DhE7X%W;SGp_(ZaM%j>ppi6RV)(2Nir9hQ4x>z27E^qKD6 z;v9Vzm(tDbB_xQAko%Co59xox_?D^=#fYS$AofIDIOOT-l r={zZHKdzF}&epdG=zGvoV*EqZr6S|A@cwXHor1Sh=ZW%XAV~iMwELD@ literal 0 HcmV?d00001 diff --git a/lib/keyring/__pycache__/core.cpython-314.pyc b/lib/keyring/__pycache__/core.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9f8c4ce6b36fd3bf6f49e1a75b081d4fb59f4ff GIT binary patch literal 10896 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 literal 0 HcmV?d00001 diff --git a/lib/keyring/__pycache__/credentials.cpython-314.pyc b/lib/keyring/__pycache__/credentials.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad5cc1127f77f2e4977ec58a3d0afb4c3f2fea00 GIT binary patch literal 6521 zcmcIo&2JmW6`xshsU;!JbDOVEo9O7*qx&2T>`ZTy8~0KOf* zc{}sw&70r+W)Eh!WC)c19{*E$L?Pr`{NtCDsLwOP+H^4$Xgjzz1KQ4rt)fYT zB;WNNc9^GLr(s#PYq(|GawbaAkz1Y9<^pIJNs+izBj9w0o6xA6EXtR=qUNHmNiZBt zrP6)0)#!qwv6e}&l-n#7jZX}9Zdy~^jFtvhGhXI2GiOZ8EgO}EedE)P%Z!q%mMVth zsIIM64a>M@s-?ilDNz_4e^?~=!CYvw4=RgfihL*?`z;|epz9;=(K)J7O)AhqayRiE z+IlHwx^-rG2^i=u#waywv#Q}b*yDircEPRI3bk_WkZZ7OrhBM<&9#i5Rt^^`<>^9g z-kr0p!f(uZR<^DcB2JCg=DjXmZ^hfxb#F&FuhD2jCydkF!o7Wz9JrqxSQ-S;1O?$U z!wMGk2WF9OVy-O`jldk}1jE$7F%xRtVHS~fIy==zLZf=a@eE_vNXYp>UsT{D?G zA8~pw_~C8WbqBGlq#Lfw%F}h%baY+h39h%uIgBVnfr;t-9K>z%b#n9@S$QP)-j{nH z!`@1e)UK7}K%DMrGF&z>MeGY;Azd|HgQHLh6hVQ-!$O-H$Ovw36Sx^7q$?Lw+GZ5& z0BwMS0g5Vshv^m`c3&x1YZWtGpM||b6U9K4+f)HIIk>m&Wy~0GlQvcEVmu^j34ldB zE0QApc?uy)@H~1};(PrY9c+U)Od)3%ANeVfKuhKU9>x3F?hFz(W)h20y5XCQT8BZ*o0#w z!^_Le`7ULL;iXM;#!1D@+0ULm_zofnA02R&cY@KI$YZF!k)Zsnng;!A)j z>;(EF2>?Ma9ek?=9b%FD;=K~U!9Bu5hy%xuj2Z8uujpe)KE^ru7^ea?#;J&czfr*! zAw$3hQl=>R(8$r2-5kwRVB!9vM~)nI&Tv%AR(*hgBQi5(RO;qg#%#7}tD#?BY`pvq z4uFM&L~)V)C%d!J`-=zJQw`-*=(g{AYw?~>@fn^T&pNsohHV7L4Krs1Q)Wakwvljf zFybW%65^I_jzT3Gvv|S=)U4$;r)E^JGKs11wm4ZgFkH45TD;yM;^8(M<+O}@i(nX` zWe;-)8_GfMD^J>qd^#4L5SZ9W6#R6@kysoiox%nb`{3swrrjp%06qf&_>9v4k;duM z6a#skqu&=LWnvCSMQWJqp=7s0I(00;mn-3C%=(DDm!n>M3$xt-b;hGhT9m$Eps#1{XcG z?5H#5tjXXKs@CDET2YyKy>2?LIRn;L>b089Tt_Y0)f)8T*p@myuY!e9DZBGvaq{Fz z)qGQZN8RM#fnj5KL>-)jXV;WOXy>8#G5U=ZuPDX4+sp z2`dF@{!*OL>%y(9LkuxfIi`D^wVN}6=2-y0ds8IZ;C&qYiLl6FcIBC1_a2y?gm8KK=^V8+d$gK({n&BA_5eiM#V(mi^Z7ITZxCCQV6*&{(d)r{+D>i;kW2P~q8dfawE%aY1I}YL<#06or4Hw_eCq2n_c+jxX zOTgzbb{#v_Y&A>aeuTR+cymiavUS$65> zgYMx^J^a*mL5mt6yDg^q+gM^?al2{7P=yCtaI!;l&99hVHpbj_?p63}d)! z#ste63kL@yUJy3mmu?<|3Vx8h>~DS1&SgA#@Yl`1xk;=9)&iq8(gx12VxsA@E5tEJ ze9po9+sB`Nyfpc!fAoI;=!0CLp%g;LefNIgs7;~+AR1K>YAg!&TeF zO8EsaCTji-@CrJ;DQbuA_aAzY8*M0~JZb=HymJikM7}GQdf^u3hX(sK*7!PxOpM=w z;+WVaG)6H7KL=m9O`b?uSy@-%;9FM!wbxSsF{|0ut#B;hWn%$ZD{vj&6h)wUhj5pJ zFLkqZ`1TE7>KMkGAp>VR2(M%O-P4!gdl&zZ#;>RB6gF~y+HbI)azcFXJIShGg4yFB zSg$51rT-!W-;&HfNqQ}np=apQkuQ&baeSR%eQnnky8F(;8iB`}nx-R-U2m)rc&_c- OPYX+#{}Fib`2HVA-nujZ literal 0 HcmV?d00001 diff --git a/lib/keyring/__pycache__/devpi_client.cpython-314.pyc b/lib/keyring/__pycache__/devpi_client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bb826cc1d48176eeb5d1a2c3a69e2add57d55b3 GIT binary patch literal 1215 zcmah|%}*0S6rb7c$I>lDs04!{v=G{Egxy5(1nI?SAV$&@f(NY4T9#5-x|^9PSWk@} z)RUY%bM+ta??8G$$CzmFV!VLm;L$g`T>*)4l6~*Z%=`Gg-@Lt+=#3#*KW5(4RwRTD z#U@zcuF-i1MhiVeMLds~uQ9{0m*%B8naQL6S%`&8xDXG*Pjwm<)ZihLdy@lV<8Ga&fhDI7Eu6Y8Qs7|G=yv=0N~J=)mWil;3W3Wxv zaBJ=qGfCBAQ;jOCn>X$2hFyDPxJ|b1)Qt_RNow_~;WbE(r0a_V%jvXcYS)HAWeYy)k0tUk~_EBePesdSO-p!Ch;DDiaCx=d00eH zO(J5Ph?oH?dRj{n6%_BkP>b}#xwxYfARzy@aN=8G8vznEB`7*2EkPf z3Kd2RoeTgC2kPj)I=U;jRedj{dz5H#(gzMa;gkKgrY>9z>PG_gp5jp-|UdUTO-a3+rWtxzc zK-QMZnhmLkl(+qbDwB4k7*Q#HsXqqH5}77kG6>HcS9sQtxNKy&Vr0Qq4hwJ41?G4s z4>9Jm#V2300;=1t6$JXM&vZI((<*m-FVG!NKNG30kja&%P&G}@I!jG6)J*fNU#Yps z7fkb9&2rwGvF3?Y}4YLIWsj*1Jh&*z^xK<;fla%kNo#dW1>JAOM&dgwSfuHid!5GjWMexBub%9|B1;Ld9Bp;FAaJO%Q zTa1h5uy3ECmBpv-2YJ?VTu70c=Yk>>*#n^NK&VKzH$okXprU+BP#@g{wPn(e!A5z| zcNbPhbtizRCdIAKSm2ZQps02rR3wi!LfscZMfnD(aRVduahS0ho6#`uaS2DDqQ8Vb)(5w00c(5!OdjzdX5io7`Bdz{Ym#W%HAz4czThcl~M zg>Xa4hl&_Y-9))S$iEXI%O|btlV8sLol;gOCKFj9j?wV7wso_IIMG zu$@-#uK^KH^`^+t08=K1$@q&f1vlV%_qcarPD$NFhBUm7O`44BtBpy0@k`}tLOQ|r7<@Y# zTVDk<)x>K3nOtIe2*Z>pD#}@ZfA*^WY(1OlLxsF1HVMG#u&w|%S#D++Cx&h_Jlq-HM_DtFFrOxd@RdPe z2}4}0fYWWJDLjT&#ck@krnw@8#RehVn=s>E5w0`5-U+e`m;i(K-^bwGKbfqeLOJyh zuaTUx>-M&cvg?j2D*zZs)Tq(ahF?13Dj{`5v!PM%PZ~9}WVky5Ri+he)I0~xSv6$n zUJ5IN*@kjWbM*wwERm@+n@+-c*7?vzIs#TWs?wA+2Zamen>O6WQm$qUDBLKEFw8}AY#}Wjux!&v}ZjBbAb+IiTu&pezSeY zx8q-qfA!LZgR4D#S5+O#HNQOa^$S0o+gz&n4fqx6Sn6gAwrh;hiiTZz#`j%@zOsHGB01JOGfqM`T0b{q=OX!wXn?o2 zC9Ej#|gaIh?KnzDf`rIO;&c_DvxT8cRB0h@$2BKA?gKIqc*YXlO028=7fdk}LR+6L}M7u#+*P0}0 zaQXQ)0*^ISlJ+fsfc!q>N56Xp`BCJ@mJ7&_AwRr)82Mr3hnA0B9>=$#JL*nJ`-cFz NGg6QquM;2w%Kv-x2!8+o literal 0 HcmV?d00001 diff --git a/lib/keyring/__pycache__/http.cpython-314.pyc b/lib/keyring/__pycache__/http.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58ed5c266783d854310dae9d502229fa24ad9c24 GIT binary patch literal 2120 zcmbVNL2nyH6rNr0+FQq|)jDpfk|qIWzZiu4fl7aXu104LrX5ltlB|)6c0-fenpV+o=nE;CS}>%sU`lh0 z6v;TLq=rewnAN)%WxAqG5ZiRW(g?}6xbJ)GmrD!F%S%hH5Vu2K|9FGbaQ!n@le8tg zV1vpgqo1=a4(l1(2svysdfSstJUQm#16O#p_gk{L;0AS{@lrP167B|@%TgHLW)yO8 z+Ks8Unq50V-=m)<-}0iP>BQrvZTqIW+`r%jLb|^1gb@oEpYHp#j~Rh6o==xbB|Dp4 zZh8Vz@O>Kba5IuBEtf_;2&mVfVZbODadxZaaaO1BQqY6fC3Sty-1=753zE>3=yH<< zGt>ivdR^cw!=!1)#$tp zlM8gXNu&mp(AS1!>I?~i7{f#w6&<9BG=Sn8ATt$HT2&%XRMJ3&OgxZ4x0dQT#R)kB zy49J1#SU4;JvEkkZ0hSckCWM+IzjGhi?$wTAP-DREg)SE$r^(Bjo1VP{mnSz0#{nx z6Bu-AYAU``mYY#I@}f)9<#78_YeNR^oBri8(6k(F$z~Xodt59xrHo3^Ru?XGAGn(= zKAT9Ugh2LBiV#EOLF|x&{F!gJzuDf)(}x!A;Hzr}%HA+xR%y?}Vuw`8QXlD_0ZuEY z`z;k2tB0Vi3{_LJJ#7rybAoUSjBTA`WR6j6lMmoJhhh*!Y#mnKz{d)V4`@tLeiDy! zz^5ZR9Lq0xau#gG2#6iBpD+BLr;qZqUEIsRv1=SyL*ISUo_=CoflO6(kjCXqWUgZBYAhYD`a8&-syW*O8Q{b> zsj*V18b@%h%wEy)+qHOSu#cE(s#u%9W2)61eU92j7i+8nKY;=R+UXZi=RCV z9|4gD1!1Q>;7>Qapsx6H1pjD~UN4^`{sQ~t6-6kFwavZ4%&xURQ21@&wf*4>?dA6M zd*k;$y7$(D$)7V1wtm$g7C+p*elR@xQFX5-YYhG(j;+OmiA}hM#GF^X$dMvSAbZ3p zh=0C@T0he@bNpFaH)oz%8FTdcY2BQBN float: + """ + Each backend class must supply a priority, a number (float or integer) + indicating the priority of the backend relative to all other backends. + The priority need not be static -- it may (and should) vary based + attributes of the environment in which is runs (platform, available + packages, etc.). + + A higher number indicates a higher priority. The priority should raise + a RuntimeError with a message indicating the underlying cause if the + backend is not suitable for the current environment. + + As a rule of thumb, a priority between zero but less than one is + suitable, but a priority of one or greater is recommended. + """ + raise NotImplementedError + + # Python 3.8 compatibility + passes = ExceptionTrap().passes + + @properties.classproperty + @passes + def viable(cls): + cls.priority # noqa: B018 + + @classmethod + def get_viable_backends( + cls: type[KeyringBackend], + ) -> filter[type[KeyringBackend]]: + """ + Return all subclasses deemed viable. + """ + return filter(operator.attrgetter('viable'), cls._classes) + + @properties.classproperty + def name(cls) -> str: + """ + The keyring name, suitable for display. + + The name is derived from module and class name. + """ + parent, sep, mod_name = cls.__module__.rpartition('.') + mod_name = mod_name.replace('_', ' ') + # mypy doesn't see `cls` is `type[Self]`, might be fixable in jaraco.classes + return ' '.join([mod_name, cls.__name__]) # type: ignore[attr-defined] + + def __str__(self) -> str: + keyring_class = type(self) + return f"{keyring_class.__module__}.{keyring_class.__name__} (priority: {keyring_class.priority:g})" + + @abc.abstractmethod + def get_password(self, service: str, username: str) -> str | None: + """Get password of the username for the service""" + return None + + def _validate_username(self, username: str) -> None: + """ + Ensure the username is not empty. + """ + if not username: + warnings.warn( + "Empty usernames are deprecated. See #668", + DeprecationWarning, + stacklevel=3, + ) + # raise ValueError("Username cannot be empty") + + @abc.abstractmethod + def set_password(self, service: str, username: str, password: str) -> None: + """Set password for the username of the service. + + If the backend cannot store passwords, raise + PasswordSetError. + """ + raise errors.PasswordSetError("reason") + + # for backward-compatibility, don't require a backend to implement + # delete_password + # @abc.abstractmethod + def delete_password(self, service: str, username: str) -> None: + """Delete the password for the username of the service. + + If the backend cannot delete passwords, raise + PasswordDeleteError. + """ + raise errors.PasswordDeleteError("reason") + + # for backward-compatibility, don't require a backend to implement + # get_credential + # @abc.abstractmethod + def get_credential( + self, + service: str, + username: str | None, + ) -> credentials.Credential | None: + """Gets the username and password for the service. + Returns a Credential instance. + + The *username* argument is optional and may be omitted by + the caller or ignored by the backend. Callers must use the + returned username. + """ + # The default implementation requires a username here. + if username is not None: + password = self.get_password(service, username) + if password is not None: + return credentials.SimpleCredential(username, password) + return None + + def set_properties_from_env(self) -> None: + """For all KEYRING_PROPERTY_* env var, set that property.""" + + def parse(item: tuple[str, str]): + key, value = item + pre, sep, name = key.partition('KEYRING_PROPERTY_') + return sep and (name.lower(), value) + + props: filter[tuple[str, str]] = filter(None, map(parse, os.environ.items())) + for name, value in props: + setattr(self, name, value) + + def with_properties(self, **kwargs: typing.Any) -> KeyringBackend: + alt = copy.copy(self) + vars(alt).update(kwargs) + return alt + + +class Crypter: + """Base class providing encryption and decryption""" + + @abc.abstractmethod + def encrypt(self, value): + """Encrypt the value.""" + pass + + @abc.abstractmethod + def decrypt(self, value): + """Decrypt the value.""" + pass + + +class NullCrypter(Crypter): + """A crypter that does nothing""" + + def encrypt(self, value): + return value + + def decrypt(self, value): + return value + + +def _load_plugins() -> None: + """ + Locate all setuptools entry points by the name 'keyring backends' + and initialize them. + Any third-party library may register an entry point by adding the + following to their setup.cfg:: + + [options.entry_points] + keyring.backends = + plugin_name = mylib.mymodule:initialize_func + + `plugin_name` can be anything, and is only used to display the name + of the plugin at initialization time. + + `initialize_func` is optional, but will be invoked if callable. + """ + for ep in metadata.entry_points(group='keyring.backends'): + try: + log.debug('Loading %s', ep.name) + init_func = ep.load() + if callable(init_func): + init_func() + except Exception: + log.exception(f"Error initializing plugin {ep}.") + + +@once +def get_all_keyring() -> list[KeyringBackend]: + """ + Return a list of all implemented keyrings that can be constructed without + parameters. + """ + _load_plugins() + viable_classes = KeyringBackend.get_viable_backends() + rings = util.suppress_exceptions(viable_classes, exceptions=TypeError) + return list(rings) + + +class SchemeSelectable: + """ + Allow a backend to select different "schemes" for the + username and service. + + >>> backend = SchemeSelectable() + >>> backend._query('contoso', 'alice') + {'username': 'alice', 'service': 'contoso'} + >>> backend._query('contoso') + {'service': 'contoso'} + >>> backend.scheme = 'KeePassXC' + >>> backend._query('contoso', 'alice') + {'UserName': 'alice', 'Title': 'contoso'} + >>> backend._query('contoso', 'alice', foo='bar') + {'UserName': 'alice', 'Title': 'contoso', 'foo': 'bar'} + """ + + scheme = 'default' + schemes = dict( + default=dict(username='username', service='service'), + KeePassXC=dict(username='UserName', service='Title'), + ) + + def _query( + self, service: str, username: str | None = None, **base: typing.Any + ) -> dict[str, str]: + scheme = self.schemes[self.scheme] + return dict( + { + scheme['username']: username, + scheme['service']: service, + } + if username is not None + else { + scheme['service']: service, + }, + **base, + ) diff --git a/lib/keyring/backend_complete.bash b/lib/keyring/backend_complete.bash new file mode 100644 index 0000000..1248d95 --- /dev/null +++ b/lib/keyring/backend_complete.bash @@ -0,0 +1,14 @@ +# Complete keyring backends for `keyring -b` from `keyring --list-backends` +# # keyring -b +# keyring.backends.chainer.ChainerBackend keyring.backends.fail.Keyring ... + +_keyring_backends() { + local choices + choices=$( + "${COMP_WORDS[0]}" --list-backends 2>/dev/null | + while IFS=$' \t' read -r backend rest; do + printf "%s\n" "$backend" + done + ) + compgen -W "${choices[*]}" -- "$1" +} diff --git a/lib/keyring/backend_complete.zsh b/lib/keyring/backend_complete.zsh new file mode 100644 index 0000000..eba76c6 --- /dev/null +++ b/lib/keyring/backend_complete.zsh @@ -0,0 +1,14 @@ +# Complete keyring backends for `keyring -b` from `keyring --list-backends` +# % keyring -b +# keyring priority +# keyring.backends.chainer.ChainerBackend 10 +# keyring.backends.fail.Keyring 0 +# ... ... + +backend_complete() { + local line + while read -r line; do + choices+=(${${line/ \(priority: /\\\\:}/)/}) + done <<< "$($words[1] --list-backends)" + _arguments "*:keyring priority:(($choices))" +} diff --git a/lib/keyring/backends/SecretService.py b/lib/keyring/backends/SecretService.py new file mode 100644 index 0000000..41aa788 --- /dev/null +++ b/lib/keyring/backends/SecretService.py @@ -0,0 +1,120 @@ +import logging +from contextlib import closing + +from jaraco.context import ExceptionTrap + +from .. import backend +from ..backend import KeyringBackend +from ..compat import properties +from ..credentials import SimpleCredential +from ..errors import ( + InitError, + KeyringLocked, + PasswordDeleteError, +) + +try: + import secretstorage + import secretstorage.exceptions as exceptions +except ImportError: + pass +except AttributeError: + # See https://github.com/jaraco/keyring/issues/296 + pass + +log = logging.getLogger(__name__) + + +class Keyring(backend.SchemeSelectable, KeyringBackend): + """Secret Service Keyring""" + + appid = 'Python keyring library' + + @properties.classproperty + def priority(cls) -> float: + with ExceptionTrap() as exc: + secretstorage.__name__ # noqa: B018 + if exc: + raise RuntimeError("SecretStorage required") + if secretstorage.__version_tuple__ < (3, 2): + raise RuntimeError("SecretStorage 3.2 or newer required") + try: + with closing(secretstorage.dbus_init()) as connection: + if not secretstorage.check_service_availability(connection): + raise RuntimeError( + "The Secret Service daemon is neither running nor " + "activatable through D-Bus" + ) + except exceptions.SecretStorageException as e: + raise RuntimeError(f"Unable to initialize SecretService: {e}") from e + return 5 + + def get_preferred_collection(self): + """If self.preferred_collection contains a D-Bus path, + the collection at that address is returned. Otherwise, + the default collection is returned. + """ + bus = secretstorage.dbus_init() + try: + if hasattr(self, 'preferred_collection'): + collection = secretstorage.Collection(bus, self.preferred_collection) + else: + collection = secretstorage.get_default_collection(bus) + except exceptions.SecretStorageException as e: + raise InitError(f"Failed to create the collection: {e}.") from e + if collection.is_locked(): + collection.unlock() + if collection.is_locked(): # User dismissed the prompt + raise KeyringLocked("Failed to unlock the collection!") + return collection + + def unlock(self, item): + if hasattr(item, 'unlock'): + item.unlock() + if item.is_locked(): # User dismissed the prompt + raise KeyringLocked('Failed to unlock the item!') + + def get_password(self, service, username): + """Get password of the username for the service""" + collection = self.get_preferred_collection() + with closing(collection.connection): + items = collection.search_items(self._query(service, username)) + for item in items: + self.unlock(item) + return item.get_secret().decode('utf-8') + + def set_password(self, service, username, password): + """Set password for the username of the service""" + collection = self.get_preferred_collection() + attributes = self._query(service, username, application=self.appid) + label = f"Password for '{username}' on '{service}'" + with closing(collection.connection): + collection.create_item(label, attributes, password, replace=True) + + def delete_password(self, service, username): + """Delete the stored password (only the first one)""" + collection = self.get_preferred_collection() + with closing(collection.connection): + items = collection.search_items(self._query(service, username)) + for item in items: + return item.delete() + raise PasswordDeleteError("No such password!") + + def get_credential(self, service, username): + """Gets the first username and password for a service. + Returns a Credential instance + + The username can be omitted, but if there is one, it will use get_password + and return a SimpleCredential containing the username and password + Otherwise, it will return the first username and password combo that it finds. + """ + scheme = self.schemes[self.scheme] + query = self._query(service, username) + collection = self.get_preferred_collection() + + with closing(collection.connection): + items = collection.search_items(query) + for item in items: + self.unlock(item) + username = item.get_attributes().get(scheme['username']) + return SimpleCredential(username, item.get_secret().decode('utf-8')) diff --git a/lib/keyring/backends/Windows.py b/lib/keyring/backends/Windows.py new file mode 100644 index 0000000..110075b --- /dev/null +++ b/lib/keyring/backends/Windows.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import logging + +from jaraco.context import ExceptionTrap + +from ..backend import KeyringBackend +from ..compat import properties +from ..credentials import SimpleCredential +from ..errors import PasswordDeleteError + +with ExceptionTrap() as missing_deps: + try: + # prefer pywin32-ctypes + from win32ctypes.pywin32 import pywintypes, win32cred + + # force demand import to raise ImportError + win32cred.__name__ # noqa: B018 + except ImportError: + # fallback to pywin32 + import pywintypes + import win32cred + + # force demand import to raise ImportError + win32cred.__name__ # noqa: B018 + +log = logging.getLogger(__name__) + + +class Persistence: + def __get__(self, keyring, type=None): + return getattr(keyring, '_persist', win32cred.CRED_PERSIST_ENTERPRISE) + + def __set__(self, keyring, value): + """ + Set the persistence value on the Keyring. Value may be + one of the win32cred.CRED_PERSIST_* constants or a + string representing one of those constants. For example, + 'local machine' or 'session'. + """ + if isinstance(value, str): + attr = 'CRED_PERSIST_' + value.replace(' ', '_').upper() + value = getattr(win32cred, attr) + keyring._persist = value + + +class DecodingCredential(dict): + @property + def value(self): + """ + Attempt to decode the credential blob as UTF-16 then UTF-8. + """ + cred = self['CredentialBlob'] + try: + return cred.decode('utf-16') + except UnicodeDecodeError: + decoded_cred_utf8 = cred.decode('utf-8') + log.warning( + "Retrieved a UTF-8 encoded credential. Please be aware that " + "this library only writes credentials in UTF-16." + ) + return decoded_cred_utf8 + + +class WinVaultKeyring(KeyringBackend): + """ + WinVaultKeyring stores encrypted passwords using the Windows Credential + Manager. + + Requires pywin32 + + This backend does some gymnastics to simulate multi-user support, + which WinVault doesn't support natively. See + https://github.com/jaraco/keyring/issues/47#issuecomment-75763152 + for details on the implementation, but here's the gist: + + Passwords are stored under the service name unless there is a collision + (another password with the same service name but different user name), + in which case the previous password is moved into a compound name: + {username}@{service} + """ + + persist = Persistence() + + @properties.classproperty + def priority(cls) -> float: + """ + If available, the preferred backend on Windows. + """ + if missing_deps: + raise RuntimeError("Requires Windows and pywin32") + return 5 + + @staticmethod + def _compound_name(username, service): + return f'{username}@{service}' + + def get_password(self, service, username): + res = self._resolve_credential(service, username) + return res and res.value + + def _resolve_credential( + self, service: str, username: str | None + ) -> DecodingCredential | None: + # first attempt to get the password under the service name + res = self._read_credential(service) + if not res or username and res['UserName'] != username: + # It wasn't found so attempt to get it with the compound name + res = self._read_credential(self._compound_name(username, service)) + return res + + def _read_credential(self, target): + try: + res = win32cred.CredRead( + Type=win32cred.CRED_TYPE_GENERIC, TargetName=target + ) + except pywintypes.error as e: + if e.winerror == 1168 and e.funcname == 'CredRead': # not found + return None + raise + return DecodingCredential(res) + + def set_password(self, service, username, password): + existing_pw = self._read_credential(service) + if existing_pw: + # resave the existing password using a compound target + existing_username = existing_pw['UserName'] + target = self._compound_name(existing_username, service) + self._set_password( + target, + existing_username, + existing_pw.value, + ) + self._set_password(service, username, str(password)) + + def _set_password(self, target, username, password): + credential = dict( + Type=win32cred.CRED_TYPE_GENERIC, + TargetName=target, + UserName=username, + CredentialBlob=password, + Comment="Stored using python-keyring", + Persist=self.persist, + ) + win32cred.CredWrite(credential, 0) + + def delete_password(self, service, username): + compound = self._compound_name(username, service) + deleted = False + for target in service, compound: + existing_pw = self._read_credential(target) + if existing_pw and existing_pw['UserName'] == username: + deleted = True + self._delete_password(target) + if not deleted: + raise PasswordDeleteError(service) + + def _delete_password(self, target): + try: + win32cred.CredDelete(Type=win32cred.CRED_TYPE_GENERIC, TargetName=target) + except pywintypes.error as e: + if e.winerror == 1168 and e.funcname == 'CredDelete': # not found + return + raise + + def get_credential(self, service, username): + res = self._resolve_credential(service, username) + return res and SimpleCredential(res['UserName'], res.value) diff --git a/lib/keyring/backends/__init__.py b/lib/keyring/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/keyring/backends/__pycache__/SecretService.cpython-314.pyc b/lib/keyring/backends/__pycache__/SecretService.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f4d3957ceae1723d65a8cc6665712f55e6e2150 GIT binary patch literal 7680 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 literal 0 HcmV?d00001 diff --git a/lib/keyring/backends/__pycache__/chainer.cpython-314.pyc b/lib/keyring/backends/__pycache__/chainer.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa3fb6272e6b0b561477ddc1591783d37dce2868 GIT binary patch literal 3750 zcmcInO>7&-6@L3i(v(C}HtDUZzm;Xnreu?K<3w((s)?GlyxAqNAXiZZCuO@5|5n(W|xNeJpRuEqu-AHQ8nPMK@Osw^t6Ea_Io zB+3QFq9)O)=_<}G{C4OP%3GE`Z=1@zZoFw$%C174WZ7~JU!$tK?y5tu)v73_ZY&j& zdCrT6x>!wCiBmNRwM-Yk#0A~5je|V_gqwoK2iU5kxi^7wXfi+7Mun5$fXY!(<*9Tt zHj`=|7}QJf96}(2onWThnMpQ?G^DbnR?v1(N>P@OEA^uo#_t?19&-z?hJ ze6hMlmz+v5w5J%^Q;h7Xu6ga6rdKKsMY3p`*L~Q+LUZRyT+x+LJgLDR#H81_N51?h z7rYzT1wI6f9*Qyw%;;f)!p-br0aw^K|l8KJ9T*aXly4I4ZxcV}#WVl5eW-pwqxkFJ6`bhPUEcZG>RI0 zmhJv<`n~CTcKAVd_-=Xq+TXK%cUFIw+RTn_Wk&aa@67Ko@z-qEowM&x?+H=E7uoK5 z_Vh;f^l#tV$e!DYiybMSYp>%@1>C6@Cnma-YobTQ7AzYts=(MZhA$1bp+)AzT!&Dz z?6u9WX%XA=0%388C@8$RziG4VGU5y=fysnX;3;p34DQ2D_c;_bc-$U4mfFq0iLN`j zyYad*`cN6&RKB%&{KfV7XX)I#i+4xsgD-3hzVK&pD}C)j;#%M`AxUux0CvOUccL+e z6&B%VSZhnkz(fx~RVWEF&0|BYH;zZFs){9X0X2GH>gxa{aRqaPK{y6|m^lo>eQr9> z2e!2{prQ*KQ50R%Uhv|sNp4w&=_OXMR>9yyn6VgGqN|A`Ke|5xco9KLoYz(~YFQ3=CJ zqHzmb{tHHUtW~ia?LHc%k`U!RhjCGw{5A$~{HY5b_$!-j?XLo_?$a$IUj)3ofyDSTQuQSizZ@Vmg!d;e( zW+LsAkowC!lZ1FtR0Rb1D`@yX!kd2vyy>!Oo0v;PtRpN-%Zsgu`d~7eFhT~m+$er| zeJz2iWj2PS`yz@X;Vf*(g?p=;^2-l1FMkD?nREQTwo&P0ZuA*o{{KBXU}fmjDdudCw+y1$GpA?yzMTf-c{V_D*isc*>!dO#&)`E zD}82L9(b4;IB@9uFrCkM2~A^R1=GZ&rY$?=6`R#DfqZL4x5J*arY%^+r8fSDR2=M% zYg*Ycuo4S?KM13)*WstK;V;FSf06m#N!a#$uOf^N#J4Jk=`QA{mZ9mC5^H`1|7&o< z&ZdOeF!A2d@*H8F;-^k81*G!tNhaD@$B$7w`6b%_h!>>9P6|@TYZG5c>AKXrA@%O@ zEU9e>5bN9Vt2-HpWrDs(d`g;#`dIa`B1+2Rc1g-TPNCRKiPEVZK;cI~|9+m%C&*P? z(Sz_dH=O0FPJIM|>@PJH{ApYben^5_{o6Psn1m&NUSceOdYs(APnH$BEdRjBz;S2> jC;l@DocuZTe-6iYV`JQ@ci*VzMmBOI|H8kHU^o8(#t$%C literal 0 HcmV?d00001 diff --git a/lib/keyring/backends/__pycache__/fail.cpython-314.pyc b/lib/keyring/backends/__pycache__/fail.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ecd33d99662347ac890ce6a5b8c67a76fcc4426 GIT binary patch literal 1668 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 literal 0 HcmV?d00001 diff --git a/lib/keyring/backends/__pycache__/libsecret.cpython-314.pyc b/lib/keyring/backends/__pycache__/libsecret.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa4ed7f82ae4fdba84f285be36a74d3c2496a64a GIT binary patch literal 9104 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 U|EYU?1nrF2C#=kU3kC9j0QEi!&Hw-a literal 0 HcmV?d00001 diff --git a/lib/keyring/backends/__pycache__/null.cpython-314.pyc b/lib/keyring/backends/__pycache__/null.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..362406fd9f0d777eee03f423cf7e97b13cf9bf4c GIT binary patch literal 1260 zcmZ8gQE%Kt5T5m&eeU%xmq0^!AjD9V^i-M?p_Pyt0^+F^C;}tp1*I#;x0hJ)*=Bbx z=yD>~@e}$7_#N9&F7ulrAu zp9R35yjh4_8}qwl%;1p|iihw(px9#DdFVW7U}Fo`;T;GY8xS^+#ob1A2AiRK6Z+nz zL;Jq4E1#>WPO{U_qWD~8L-Mo>ofk@DqD)-pDVO>Jn2%^1QQBib5du1oTx`4nIfMe6 z$L^P%F@B=1}Bl{gcj_*#4e13_3F z&Hr1kLPm{rM`<2m-?82(*W(CnSIRi0#KR;;DGmSr?#%8Fa9j+Eq_~5TCTw>mrd8M8D&|H5h^L|jk?&~W$y~*F*iuPn!!sD z?rYCk{j^>#bMix3H5)8kJ=N#WJu+s*D%W<&-phYTp7PW*;?x*+ZEL36`h`w%o#522 zE)Xm{U5c9)sTusyTYG#>_sBkso1_==|BP@Go{B^LI>q}<+cYX2*%tA0k;KaS%v2VQ z$y$(T__A%13QT92;Qv~(>|LV+hF{SPe)l^iq%xg;lXG^hFD0j-j<%&7=fg?LHud`3 zNtD)+o|L0R8%&c-WjTdgQV#Q2O3iR=x1_p$OH;j0KE26-j#-*N+Z(D>5qFDP>^e;b zv6;j&Le$C01eKx7*Oq1{R%t3_U+C*}UrEEKW#yU~D;1Y!?vZ-+BbE5vab5pv1-#YY z+|6Iz&6f`GKW~8d_7z(%JmGe)JlNj*rTM|^izlB{{out)(GpG3}STD}UMTBLI tRX(PvI#G?ne$}r7eVYPIt=S-T)eu5FgZG}n)(fvE_8w<{11;sje*u{{Kf3?` literal 0 HcmV?d00001 diff --git a/lib/keyring/backends/chainer.py b/lib/keyring/backends/chainer.py new file mode 100644 index 0000000..6bc711f --- /dev/null +++ b/lib/keyring/backends/chainer.py @@ -0,0 +1,71 @@ +""" +Keyring Chainer - iterates over other viable backends to +discover passwords in each. +""" + +from .. import backend +from ..compat import properties +from . import fail + + +class ChainerBackend(backend.KeyringBackend): + """ + >>> ChainerBackend() + + """ + + # override viability as 'priority' cannot be determined + # until other backends have been constructed + viable = True + + @properties.classproperty + def priority(cls) -> float: + """ + If there are backends to chain, high priority + Otherwise very low priority since our operation when empty + is the same as null. + """ + return 10 if len(cls.backends) > 1 else (fail.Keyring.priority - 1) + + @properties.classproperty + def backends(cls): + """ + Discover all keyrings for chaining. + """ + + def allow(keyring): + limit = backend._limit or bool + return ( + not isinstance(keyring, ChainerBackend) + and limit(keyring) + and keyring.priority > 0 + ) + + allowed = filter(allow, backend.get_all_keyring()) + return sorted(allowed, key=backend.by_priority, reverse=True) + + def get_password(self, service, username): + for keyring in self.backends: + password = keyring.get_password(service, username) + if password is not None: + return password + + def set_password(self, service, username, password): + for keyring in self.backends: + try: + return keyring.set_password(service, username, password) + except NotImplementedError: + pass + + def delete_password(self, service, username): + for keyring in self.backends: + try: + return keyring.delete_password(service, username) + except NotImplementedError: + pass + + def get_credential(self, service, username): + for keyring in self.backends: + credential = keyring.get_credential(service, username) + if credential is not None: + return credential diff --git a/lib/keyring/backends/fail.py b/lib/keyring/backends/fail.py new file mode 100644 index 0000000..5007ab9 --- /dev/null +++ b/lib/keyring/backends/fail.py @@ -0,0 +1,30 @@ +from ..backend import KeyringBackend +from ..compat import properties +from ..errors import NoKeyringError + + +class Keyring(KeyringBackend): + """ + Keyring that raises error on every operation. + + >>> kr = Keyring() + >>> kr.get_password('svc', 'user') + Traceback (most recent call last): + ... + keyring.errors.NoKeyringError: ...No recommended backend... + """ + + @properties.classproperty + def priority(cls) -> float: + return 0 + + def get_password(self, service, username, password=None): + msg = ( + "No recommended backend was available. Install a recommended 3rd " + "party backend package; or, install the keyrings.alt package if " + "you want to use the non-recommended backends. See " + "https://pypi.org/project/keyring for details." + ) + raise NoKeyringError(msg) + + set_password = delete_password = get_password diff --git a/lib/keyring/backends/kwallet.py b/lib/keyring/backends/kwallet.py new file mode 100644 index 0000000..b1d9c8e --- /dev/null +++ b/lib/keyring/backends/kwallet.py @@ -0,0 +1,164 @@ +import contextlib +import os +import sys + +from ..backend import KeyringBackend +from ..compat import properties +from ..credentials import SimpleCredential +from ..errors import InitError, KeyringLocked, PasswordDeleteError, PasswordSetError + +try: + import dbus + from dbus.mainloop.glib import DBusGMainLoop +except ImportError: + pass +except AttributeError: + # See https://github.com/jaraco/keyring/issues/296 + pass + + +def _id_from_argv(): + """ + Safely infer an app id from sys.argv. + """ + allowed = AttributeError, IndexError, TypeError + with contextlib.suppress(allowed): + return sys.argv[0] + + +class DBusKeyring(KeyringBackend): + """ + KDE KWallet 5 via D-Bus + """ + + appid = _id_from_argv() or 'Python keyring library' + wallet = None + bus_name = 'org.kde.kwalletd5' + object_path = '/modules/kwalletd5' + + @properties.classproperty + def priority(cls) -> float: + if 'dbus' not in globals(): + raise RuntimeError('python-dbus not installed') + try: + bus = dbus.SessionBus(mainloop=DBusGMainLoop()) + except dbus.DBusException as exc: + raise RuntimeError(exc.get_dbus_message()) from exc + if not ( + bus.name_has_owner(cls.bus_name) + and cls.bus_name in bus.list_activatable_names() + ): + raise RuntimeError( + "The KWallet daemon is neither running nor activatable through D-Bus" + ) + if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":"): + return 5.1 + return 4.9 + + def __init__(self, *arg, **kw): + super().__init__(*arg, **kw) + self.handle = -1 + + def _migrate(self, service): + old_folder = 'Python' + entry_list = [] + if self.iface.hasFolder(self.handle, old_folder, self.appid): + entry_list = self.iface.readPasswordList( + self.handle, old_folder, '*@*', self.appid + ) + + for entry in entry_list.items(): + key = entry[0] + password = entry[1] + + username, service = key.rsplit('@', 1) + ret = self.iface.writePassword( + self.handle, service, username, password, self.appid + ) + if ret == 0: + self.iface.removeEntry(self.handle, old_folder, key, self.appid) + + entry_list = self.iface.readPasswordList( + self.handle, old_folder, '*', self.appid + ) + if not entry_list: + self.iface.removeFolder(self.handle, old_folder, self.appid) + + def connected(self, service): + if self.handle >= 0: + if self.iface.isOpen(self.handle): + return True + + bus = dbus.SessionBus(mainloop=DBusGMainLoop()) + wId = 0 + try: + remote_obj = bus.get_object(self.bus_name, self.object_path) + self.iface = dbus.Interface(remote_obj, 'org.kde.KWallet') + self.handle = self.iface.open(self.iface.networkWallet(), wId, self.appid) + except dbus.DBusException as e: + raise InitError(f'Failed to open keyring: {e}.') from e + + if self.handle < 0: + return False + self._migrate(service) + return True + + def get_password(self, service, username): + """Get password of the username for the service""" + if not self.connected(service): + # the user pressed "cancel" when prompted to unlock their keyring. + raise KeyringLocked("Failed to unlock the keyring!") + if not self.iface.hasEntry(self.handle, service, username, self.appid): + return None + password = self.iface.readPassword(self.handle, service, username, self.appid) + return str(password) + + def get_credential(self, service, username): + """Gets the first username and password for a service. + Returns a Credential instance + + The username can be omitted, but if there is one, it will forward to + get_password. + Otherwise, it will return the first username and password combo that it finds. + """ + if username is not None: + return super().get_credential(service, username) + + if not self.connected(service): + # the user pressed "cancel" when prompted to unlock their keyring. + raise KeyringLocked("Failed to unlock the keyring!") + + for username in self.iface.entryList(self.handle, service, self.appid): + password = self.iface.readPassword( + self.handle, service, username, self.appid + ) + return SimpleCredential(str(username), str(password)) + + def set_password(self, service, username, password): + """Set password for the username of the service""" + if not self.connected(service): + # the user pressed "cancel" when prompted to unlock their keyring. + raise PasswordSetError("Cancelled by user") + self.iface.writePassword(self.handle, service, username, password, self.appid) + + def delete_password(self, service, username): + """Delete the password for the username of the service.""" + if not self.connected(service): + # the user pressed "cancel" when prompted to unlock their keyring. + raise PasswordDeleteError("Cancelled by user") + if not self.iface.hasEntry(self.handle, service, username, self.appid): + raise PasswordDeleteError("Password not found") + self.iface.removeEntry(self.handle, service, username, self.appid) + + +class DBusKeyringKWallet4(DBusKeyring): + """ + KDE KWallet 4 via D-Bus + """ + + bus_name = 'org.kde.kwalletd' + object_path = '/modules/kwalletd' + + @properties.classproperty + def priority(cls): + return super().priority - 1 diff --git a/lib/keyring/backends/libsecret.py b/lib/keyring/backends/libsecret.py new file mode 100644 index 0000000..b92b3c2 --- /dev/null +++ b/lib/keyring/backends/libsecret.py @@ -0,0 +1,155 @@ +import logging + +from .. import backend +from ..backend import KeyringBackend +from ..compat import properties +from ..credentials import SimpleCredential +from ..errors import ( + KeyringLocked, + PasswordDeleteError, + PasswordSetError, +) + +available = False +try: + import gi + from gi.repository import Gio, GLib + + gi.require_version('Secret', '1') + from gi.repository import Secret + + available = True +except (AttributeError, ImportError, ValueError): + pass + +log = logging.getLogger(__name__) + + +class Keyring(backend.SchemeSelectable, KeyringBackend): + """libsecret Keyring""" + + appid = 'Python keyring library' + + @property + def schema(self): + return Secret.Schema.new( + "org.freedesktop.Secret.Generic", + Secret.SchemaFlags.NONE, + self._query( + Secret.SchemaAttributeType.STRING, + Secret.SchemaAttributeType.STRING, + application=Secret.SchemaAttributeType.STRING, + ), + ) + + @properties.NonDataProperty + def collection(self): + return Secret.COLLECTION_DEFAULT + + @properties.classproperty + def priority(cls) -> float: + if not available: + raise RuntimeError("libsecret required") + + # Make sure there is actually a secret service running + try: + Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION, None) + except GLib.Error as error: + raise RuntimeError("Can't open a session to the secret service") from error + + return 4.8 + + def get_password(self, service, username): + """Get password of the username for the service""" + attributes = self._query(service, username, application=self.appid) + try: + items = Secret.password_search_sync( + self.schema, attributes, Secret.SearchFlags.UNLOCK, None + ) + except GLib.Error as error: + quark = GLib.quark_try_string('g-io-error-quark') + if error.matches(quark, Gio.IOErrorEnum.FAILED): + raise KeyringLocked('Failed to unlock the item!') from error + raise + for item in items: + try: + return item.retrieve_secret_sync().get_text() + except GLib.Error as error: + quark = GLib.quark_try_string('secret-error') + if error.matches(quark, Secret.Error.IS_LOCKED): + raise KeyringLocked('Failed to unlock the item!') from error + raise + + def set_password(self, service, username, password): + """Set password for the username of the service""" + attributes = self._query(service, username, application=self.appid) + label = f"Password for '{username}' on '{service}'" + try: + stored = Secret.password_store_sync( + self.schema, attributes, self.collection, label, password, None + ) + except GLib.Error as error: + quark = GLib.quark_try_string('secret-error') + if error.matches(quark, Secret.Error.IS_LOCKED): + raise KeyringLocked("Failed to unlock the collection!") from error + quark = GLib.quark_try_string('g-io-error-quark') + if error.matches(quark, Gio.IOErrorEnum.FAILED): + raise KeyringLocked("Failed to unlock the collection!") from error + raise + if not stored: + raise PasswordSetError("Failed to store password!") + + def delete_password(self, service, username): + """Delete the stored password (only the first one)""" + attributes = self._query(service, username, application=self.appid) + try: + items = Secret.password_search_sync( + self.schema, attributes, Secret.SearchFlags.UNLOCK, None + ) + except GLib.Error as error: + quark = GLib.quark_try_string('g-io-error-quark') + if error.matches(quark, Gio.IOErrorEnum.FAILED): + raise KeyringLocked('Failed to unlock the item!') from error + raise + for item in items: + try: + removed = Secret.password_clear_sync( + self.schema, item.get_attributes(), None + ) + except GLib.Error as error: + quark = GLib.quark_try_string('secret-error') + if error.matches(quark, Secret.Error.IS_LOCKED): + raise KeyringLocked('Failed to unlock the item!') from error + raise + return removed + raise PasswordDeleteError("No such password!") + + def get_credential(self, service, username): + """Get the first username and password for a service. + Return a Credential instance + + The username can be omitted, but if there is one, it will use get_password + and return a SimpleCredential containing the username and password + Otherwise, it will return the first username and password combo that it finds. + """ + query = self._query(service, username) + try: + items = Secret.password_search_sync( + self.schema, query, Secret.SearchFlags.UNLOCK, None + ) + except GLib.Error as error: + quark = GLib.quark_try_string('g-io-error-quark') + if error.matches(quark, Gio.IOErrorEnum.FAILED): + raise KeyringLocked('Failed to unlock the item!') from error + raise + for item in items: + username = item.get_attributes().get("username") + try: + return SimpleCredential( + username, item.retrieve_secret_sync().get_text() + ) + except GLib.Error as error: + quark = GLib.quark_try_string('secret-error') + if error.matches(quark, Secret.Error.IS_LOCKED): + raise KeyringLocked('Failed to unlock the item!') from error + raise diff --git a/lib/keyring/backends/macOS/__init__.py b/lib/keyring/backends/macOS/__init__.py new file mode 100644 index 0000000..c3734e2 --- /dev/null +++ b/lib/keyring/backends/macOS/__init__.py @@ -0,0 +1,85 @@ +import functools +import os +import platform +import warnings + +from ...backend import KeyringBackend +from ...compat import properties +from ...errors import KeyringError, KeyringLocked, PasswordDeleteError, PasswordSetError + +try: + from . import api +except Exception: + pass + + +def warn_keychain(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if self.keychain: + warnings.warn("Specified keychain is ignored. See #623", stacklevel=2) + return func(self, *args, **kwargs) + + return wrapper + + +class Keyring(KeyringBackend): + """macOS Keychain""" + + keychain = os.environ.get('KEYCHAIN_PATH') + "Path to keychain file, overriding default" + + @properties.classproperty + def priority(cls): + """ + Preferred for all macOS environments. + """ + if platform.system() != 'Darwin': + raise RuntimeError("macOS required") + if 'api' not in globals(): + raise RuntimeError("Security API unavailable") + return 5 + + @warn_keychain + def set_password(self, service, username, password): + if username is None: + username = '' + + try: + api.set_generic_password(self.keychain, service, username, password) + except api.KeychainDenied as e: + raise KeyringLocked(f"Can't store password on keychain: {e}") from e + except api.Error as e: + raise PasswordSetError(f"Can't store password on keychain: {e}") from e + + @warn_keychain + def get_password(self, service, username): + if username is None: + username = '' + + try: + return api.find_generic_password(self.keychain, service, username) + except api.NotFound: + pass + except api.KeychainDenied as e: + raise KeyringLocked(f"Can't get password from keychain: {e}") from e + except api.Error as e: + raise KeyringError(f"Can't get password from keychain: {e}") from e + + @warn_keychain + def delete_password(self, service, username): + if username is None: + username = '' + + try: + return api.delete_generic_password(self.keychain, service, username) + except api.Error as e: + raise PasswordDeleteError(f"Can't delete password in keychain: {e}") from e + + def with_keychain(self, keychain): + warnings.warn( + "macOS.Keyring.with_keychain is deprecated. Use with_properties instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.with_properties(keychain=keychain) diff --git a/lib/keyring/backends/macOS/__pycache__/__init__.cpython-314.pyc b/lib/keyring/backends/macOS/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9eb476f8e52101bec7e9c332f55c43088ecb753 GIT binary patch literal 4442 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 fhU~zE9&? literal 0 HcmV?d00001 diff --git a/lib/keyring/backends/macOS/__pycache__/api.cpython-314.pyc b/lib/keyring/backends/macOS/__pycache__/api.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e961635b38b96fa022ee952d8d34ed9a9e56f76 GIT binary patch literal 7516 zcmbU_?{6DNcC*|ix%??n5=s5ED9M&hS(cnw*XUy31(IyVsvTKgJ4%X5-cr;`T8JW< z*`Xczf;*<|-8s1yNzkAWiYuV@0@Ox7B*h)j`_K>Ve?T796K|se0`9&9{US#y4CF)8 z_hz|V%5lVC-RX1~A^)Sy>FY{iNnauH}8Q)bu^Y@ZP4+}&|A5q+K zqIkw!)+#KxD9&}-OFMNRS@Z+0cTBMQ?3#tY5ns0P?3y)wBfihZvuoD8*gV(o@L{)D zWKmv>WLxZS_`*LA!V4Z4X|0bm&z-T`PR-f?v+XH*?}!L}jg&(Uh23INpo|`)^unH+ z?Qy4u*`f`zh3AHyF}uZLi-E=1vDxGcF6eQ%2+xh!*iOyjKo|cCUHll`c?aKav38(q zZ=|~*z_+tn^W5_`u2ZuP*mK8IR2|2t#vE+B#X3PAoljA9I#h~3PFUBXG&gQjIJFXp z6MIV5v)D8D9cRpLE%w@4@-^cLdt3>Ej)&qT(`?Ua*Q^g{FWECXwGsx3rbY_Zr!)h< z{YnJj0JoeLz-e7PIRdrI?<}8na2sbT4gEZN>DaRXxitJa5WrwNP)} z&FRZ(9;kvVdUjdO=u0$3H`6$kU1}yR9HO=t$s%EbLcpm+n5c+MQY7Y9+{~kRm{;*K zSrM3zGbEdCjnW)_L}vBjzy0z{z4#@73)mX^;=jMtPeLo{H)U1LrZzNHHT|l(ky|cg zupLy@HwvjtZKO$6S9EHyjGoo9IT#M>OxsXlo9argkX<&L*0s&_T1wBV%UV{~pu^D8 zYO282)RmN;fsRl@!&<#V+u9{sq4yT~eR2ol;&y~t#a`yl?s;UMaAk4rR)t3rP zGgOrZVE>1BD(1u}+sKdR_53-OqN^G^S6F4))VDIvkHP_t<~P||E;|Y=QCMnpDV1K= zvdhNkMk;;d)@UlPkK{KgI&==epA(}Sz>i2Z+Etvb#yX2Ps2tjcK!@%VRX&gsg>ncq^;|lx?bQ?vo z7ifL&rFUQY>DYFp5*R2+1AH^bc*F@fj~L8~WS$HVibuka9M*8 zo~Q&SOVXrs()M}&9~6O}0tP5<*c1&$YZ^dLBX2mJC4Me&4*m!SY^l41+aPs>4_d9- z;RWz`L78x=e-|1>q7aap%_k&rpapg5S_B9cmo1lhp+PdDP6Q&)no@zSj3-@Y$JF#K z@LbtdO9-#&Y;DTwFg;o}om^oV{53j#3HSDoMAj-6F$wK@EeSaCiy!2yV#PDOo_N@ZxB# zqld$FP3GzGgYmPA)Fk7akFY7#ZLBO`eY&mEV?9voBQhDx(O$p+V!TqG^ z0k2koP~oXX%^r~bZ7@z}o@?6X6hy&E2w8^N`<3B&q^~1@WA?1yg8*nnfZ>`ytU2d5 z3L4#{ldyq1@H18cU@YjV%E3Kf-+jrhgLHlf%36a9JMajghiEav1UkJqN_=_I!h>QO?Ex z8Uvptyt?8!0<{G`hYtLyqSPHSI8Ncd8w~T@c+yQVSvP&)P{7h4y)c_Wj476SS!0u% zOq*lWY^wKMw;ZfzS>qNfm=eVIRAVFzrdMDpmE|B|p%eq54nG5(C?0lXH_-K~K-W(9 z@Wb1c?&p7T<6kdV#%3ykKim!6EC+5@<)%Z?g{S7SPYnp`rDm4GA-d~^#4pFhMM)7F zGH|!zf&@^6okF_F!$7)j(?Bc14}6*LFve|vUbsWX!IxO!SAnn#5vsoGv(gfd9pVws z5enaM#BtsVa*GQ_%$=|>vki(HNz_S_wM7|)_sG6-LO{OTfkaU z!&;DNL66(%NqQ)r*zEoA3}m(m$ZQilv%Mb!ljNCgVj>M`o{_lB6Wse(?KChk0am@HyUkLfiV`J45vI}Mv zBIYSB?yHv?Uayxdpds@zU*`f8%5Aoor_4eP#a$^~fQ^MVacO$3DkyAU~Qy&liMh^c@^6g5UWeL39j@19VKzvK8 z1bRwR4>y@(`im@c{S8|B3!hebR$Oho?%SGhQBZ|~t01cN3>tK!uC1>L^Dg_|(F=DL z{Byn?O?GaS$}!8B7ZukFHG2cg2-)1tOE6C@X8`N?X05I3Cvq@A667qgHGdW6=q9?W zm-LWc{&)S_MX`tILQwvEt#@W z$mb~pUhY`U__CJK;OXHjDlm!DC2%@_O+hD?mmNifJF|q^Mwby!Lp-^zKC9Tr6%I2O zOW~|!y3NqoSJ!$2u>KBy2F?TO=4;*cb^prO{nOWXeS`n*8{Ct7x21<~?#Sa$MB+bv zVFaka+vcA13l}{LK7F<|8^lT zDf-f&V)cSdbCqMzXuR4rxW{@nJP!(_2rty=^P=M%=S5ouK{4tn+_Q4)dCA5E1+bzX zm#p}dWM?7YcH|PO$>lPaV1sDfGzW}3lA(?~(1&yT=i;Cnh=<@4_!-{FSr0V0 z9eNHy7Qqq%1Hlyp(+Cm(%rHKn!jr0Qy8x)jIrFaCjC0kIv{sDXO-0%&2irx?swetheFs(zjs%d>b(?xd=HB1wr@^ z(*Ii$`ILk{CCyJ 31: + raise OverflowError(val) + int32 = 0x9 + return CFNumberCreate(None, int32, ctypes.byref(c_int32(val))) + + +@create_cf.register +def _(s: str): + kCFStringEncodingUTF8 = 0x08000100 + return CFStringCreateWithCString(None, s.encode('utf8'), kCFStringEncodingUTF8) + + +def create_query(**kwargs): + return CFDictionaryCreate( + None, + (c_void_p * len(kwargs))(*map(k_, kwargs.keys())), + (c_void_p * len(kwargs))(*map(create_cf, kwargs.values())), + len(kwargs), + _found.kCFTypeDictionaryKeyCallBacks, + _found.kCFTypeDictionaryValueCallBacks, + ) + + +def cfstr_to_str(data): + return ctypes.string_at(CFDataGetBytePtr(data), CFDataGetLength(data)).decode( + 'utf-8' + ) + + +class Error(Exception): + @classmethod + def raise_for_status(cls, status): + if status == 0: + return + if status == error.item_not_found: + raise NotFound(status, "Item not found") + if status == error.keychain_denied: + raise KeychainDenied(status, "Keychain Access Denied") + if status == error.sec_auth_failed or status == error.plist_missing: + raise SecAuthFailure( + status, + "Security Auth Failure: make sure " + "executable is signed with codesign util", + ) + raise cls(status, "Unknown Error") + + +class NotFound(Error): + pass + + +class KeychainDenied(Error): + pass + + +class SecAuthFailure(Error): + pass + + +def find_generic_password(kc_name, service, username, not_found_ok=False): + q = create_query( + kSecClass=k_('kSecClassGenericPassword'), + kSecMatchLimit=k_('kSecMatchLimitOne'), + kSecAttrService=service, + kSecAttrAccount=username, + kSecReturnData=True, + ) + + data = c_void_p() + status = SecItemCopyMatching(q, byref(data)) + + if status == error.item_not_found and not_found_ok: + return + + Error.raise_for_status(status) + + return cfstr_to_str(data) + + +def set_generic_password(name, service, username, password): + with contextlib.suppress(NotFound): + delete_generic_password(name, service, username) + + q = create_query( + kSecClass=k_('kSecClassGenericPassword'), + kSecAttrService=service, + kSecAttrAccount=username, + kSecValueData=password, + ) + + status = SecItemAdd(q, None) + Error.raise_for_status(status) + + +def delete_generic_password(name, service, username): + q = create_query( + kSecClass=k_('kSecClassGenericPassword'), + kSecAttrService=service, + kSecAttrAccount=username, + ) + + status = SecItemDelete(q) + Error.raise_for_status(status) diff --git a/lib/keyring/backends/null.py b/lib/keyring/backends/null.py new file mode 100644 index 0000000..6b4c3b0 --- /dev/null +++ b/lib/keyring/backends/null.py @@ -0,0 +1,20 @@ +from ..backend import KeyringBackend +from ..compat import properties + + +class Keyring(KeyringBackend): + """ + Keyring that return None on every operation. + + >>> kr = Keyring() + >>> kr.get_password('svc', 'user') + """ + + @properties.classproperty + def priority(cls) -> float: + return -1 + + def get_password(self, service, username, password=None): + pass + + set_password = delete_password = get_password diff --git a/lib/keyring/cli.py b/lib/keyring/cli.py new file mode 100644 index 0000000..2c0ba4d --- /dev/null +++ b/lib/keyring/cli.py @@ -0,0 +1,220 @@ +"""Simple command line interface to get/set password from a keyring""" + +from __future__ import annotations + +import argparse +import getpass +import json +import sys + +from . import ( + backend, + completion, + core, + credentials, + delete_password, + get_credential, + get_password, + set_keyring, + set_password, +) +from .util import platform_ + + +class CommandLineTool: + # Attributes set dynamically by the ArgumentParser + keyring_path: str | None + keyring_backend: str | None + get_mode: str + output_format: str + operation: str + service: str + username: str + + def __init__(self): + self.parser = argparse.ArgumentParser() + self.parser.add_argument( + "-p", + "--keyring-path", + dest="keyring_path", + default=None, + help="Path to the keyring backend", + ) + self.parser.add_argument( + "-b", + "--keyring-backend", + dest="keyring_backend", + default=None, + help="Name of the keyring backend", + ) + self.parser.add_argument( + "--list-backends", + action="store_true", + help="List keyring backends and exit", + ) + self.parser.add_argument( + "--disable", action="store_true", help="Disable keyring and exit" + ) + self.parser._get_modes = ["password", "creds"] + self.parser.add_argument( + "--mode", + choices=self.parser._get_modes, + dest="get_mode", + default="password", + help=""" + Mode for 'get' operation. + 'password' requires a username and will return only the password. + 'creds' does not require a username and will return both the username and password separated by a newline. + + Default is 'password' + """, + ) + self.parser._output_formats = ["plain", "json"] + self.parser.add_argument( + "--output", + choices=self.parser._output_formats, + dest="output_format", + default="plain", + help=""" + Output format for 'get' operation. + + Default is 'plain' + """, + ) + self.parser._operations = ["get", "set", "del", "diagnose"] + self.parser.add_argument( + 'operation', + choices=self.parser._operations, + nargs="?", + ) + self.parser.add_argument( + 'service', + nargs="?", + ) + self.parser.add_argument( + 'username', + nargs="?", + ) + completion.install(self.parser) + + def run(self, argv): + args = self.parser.parse_args(argv) + vars(self).update(vars(args)) + + if args.list_backends: + for k in backend.get_all_keyring(): + print(k) + return + + if args.disable: + core.disable() + return + + if args.operation == 'diagnose': + self.diagnose() + return + + self._check_args() + self._load_spec_backend() + method = getattr(self, f'do_{self.operation}', self.invalid_op) + return method() + + def _check_args(self): + needs_username = self.operation != 'get' or self.get_mode != 'creds' + required = (['service'] + ['username'] * needs_username) * bool(self.operation) + if any(getattr(self, param) is None for param in required): + self.parser.error(f"{self.operation} requires {' and '.join(required)}") + + def do_get(self): + credential = getattr(self, f'_get_{self.get_mode}')() + if credential is None: + raise SystemExit(1) + getattr(self, f'_emit_{self.output_format}')(credential) + + def _emit_json(self, credential: credentials.Credential): + print(json.dumps(credential._vars())) + + def _emit_plain(self, credential: credentials.Credential): + for val in credential._vars().values(): + print(val) + + def _get_creds(self) -> credentials.Credential | None: + return get_credential(self.service, self.username) + + def _get_password(self) -> credentials.Credential | None: + password = get_password(self.service, self.username) + return ( + credentials.AnonymousCredential(password) if password is not None else None + ) + + def do_set(self): + password = self.input_password( + f"Password for '{self.username}' in '{self.service}': " + ) + set_password(self.service, self.username, password) + + def do_del(self): + delete_password(self.service, self.username) + + def diagnose(self): + config_root = core._config_path() + if config_root.exists(): + print("config path:", config_root) + else: + print("config path:", config_root, "(absent)") + print("data root:", platform_.data_root()) + + def invalid_op(self): + self.parser.error(f"Specify operation ({', '.join(self.parser._operations)}).") + + def _load_spec_backend(self): + if self.keyring_backend is None: + return + + try: + if self.keyring_path: + sys.path.insert(0, self.keyring_path) + set_keyring(core.load_keyring(self.keyring_backend)) + except Exception as exc: + # Tons of things can go wrong here: + # ImportError when using "fjkljfljkl" + # AttributeError when using "os.path.bar" + # TypeError when using "__builtins__.str" + # So, we play on the safe side, and catch everything. + self.parser.error(f"Unable to load specified keyring: {exc}") + + def input_password(self, prompt): + """Retrieve password from input.""" + return self.pass_from_pipe() or getpass.getpass(prompt) + + @classmethod + def pass_from_pipe(cls): + """Return password from pipe if not on TTY, else False.""" + is_pipe = not sys.stdin.isatty() + return is_pipe and cls.strip_last_newline(sys.stdin.read()) + + @staticmethod + def strip_last_newline(str): + r"""Strip one last newline, if present. + + >>> CommandLineTool.strip_last_newline('foo') + 'foo' + >>> CommandLineTool.strip_last_newline('foo\n') + 'foo' + """ + slc = slice(-1 if str.endswith('\n') else None) + return str[slc] + + +def main(argv=None): + """Main command line interface.""" + + if argv is None: + argv = sys.argv[1:] + + cli = CommandLineTool() + return cli.run(argv) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/lib/keyring/compat/__init__.py b/lib/keyring/compat/__init__.py new file mode 100644 index 0000000..22f1e1c --- /dev/null +++ b/lib/keyring/compat/__init__.py @@ -0,0 +1,7 @@ +__all__ = ['properties'] + + +try: + from jaraco.classes import properties +except ImportError: # pragma: no cover + from . import properties # type: ignore[no-redef] diff --git a/lib/keyring/compat/__pycache__/__init__.cpython-314.pyc b/lib/keyring/compat/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4eb69a5b8d898263f1d39cba0aa59b91213a1810 GIT binary patch literal 330 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 literal 0 HcmV?d00001 diff --git a/lib/keyring/compat/__pycache__/py312.cpython-314.pyc b/lib/keyring/compat/__pycache__/py312.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8dac9c733f20abdba93923fe28d0018bdc518bf GIT binary patch literal 359 zcmdPqF&9@B-(ruCPt3`QkH5uJmReMtnV%P*nU|J-ODHq9Ait<2Co@S8rn?BF7fB)> zCb5#?Gf;-%mbHFKZh?M5W`S-=Vo`c(iEe3nNnWC9j*&jl82y6El8pR3{p{4rqRhN> z{p9@Ig2WP_sIj4uUP0w84x8Nkl+v73yCOcIS&TqjtO_JPFf%eT-e%x>z%AOL)4=|L dje&*zI>> class X(object): + ... @NonDataProperty + ... def foo(self): + ... return 3 + >>> x = X() + >>> x.foo + 3 + >>> x.foo = 4 + >>> x.foo + 4 + """ + + def __init__(self, fget): + assert fget is not None, "fget cannot be none" + assert callable(fget), "fget must be callable" + self.fget = fget + + def __get__(self, obj, objtype=None): + if obj is None: + return self + return self.fget(obj) + + +class classproperty: + """ + Like @property but applies at the class level. + + + >>> class X(metaclass=classproperty.Meta): + ... val = None + ... @classproperty + ... def foo(cls): + ... return cls.val + ... @foo.setter + ... def foo(cls, val): + ... cls.val = val + >>> X.foo + >>> X.foo = 3 + >>> X.foo + 3 + >>> x = X() + >>> x.foo + 3 + >>> X.foo = 4 + >>> x.foo + 4 + + Setting the property on an instance affects the class. + + >>> x.foo = 5 + >>> x.foo + 5 + >>> X.foo + 5 + >>> vars(x) + {} + >>> X().foo + 5 + + Attempting to set an attribute where no setter was defined + results in an AttributeError: + + >>> class GetOnly(metaclass=classproperty.Meta): + ... @classproperty + ... def foo(cls): + ... return 'bar' + >>> GetOnly.foo = 3 + Traceback (most recent call last): + ... + AttributeError: can't set attribute + + It is also possible to wrap a classmethod or staticmethod in + a classproperty. + + >>> class Static(metaclass=classproperty.Meta): + ... @classproperty + ... @classmethod + ... def foo(cls): + ... return 'foo' + ... @classproperty + ... @staticmethod + ... def bar(): + ... return 'bar' + >>> Static.foo + 'foo' + >>> Static.bar + 'bar' + + *Legacy* + + For compatibility, if the metaclass isn't specified, the + legacy behavior will be invoked. + + >>> class X: + ... val = None + ... @classproperty + ... def foo(cls): + ... return cls.val + ... @foo.setter + ... def foo(cls, val): + ... cls.val = val + >>> X.foo + >>> X.foo = 3 + >>> X.foo + 3 + >>> x = X() + >>> x.foo + 3 + >>> X.foo = 4 + >>> x.foo + 4 + + Note, because the metaclass was not specified, setting + a value on an instance does not have the intended effect. + + >>> x.foo = 5 + >>> x.foo + 5 + >>> X.foo # should be 5 + 4 + >>> vars(x) # should be empty + {'foo': 5} + >>> X().foo # should be 5 + 4 + """ + + class Meta(type): + def __setattr__(self, key, value): + obj = self.__dict__.get(key, None) + if type(obj) is classproperty: + return obj.__set__(self, value) + return super().__setattr__(key, value) + + def __init__(self, fget, fset=None): + self.fget = self._ensure_method(fget) + self.fset = fset + fset and self.setter(fset) + + def __get__(self, instance, owner=None): + return self.fget.__get__(None, owner)() + + def __set__(self, owner, value): + if not self.fset: + raise AttributeError("can't set attribute") + if type(owner) is not classproperty.Meta: + owner = type(owner) + return self.fset.__get__(None, owner)(value) + + def setter(self, fset): + self.fset = self._ensure_method(fset) + return self + + @classmethod + def _ensure_method(cls, fn): + """ + Ensure fn is a classmethod or staticmethod. + """ + needs_method = not isinstance(fn, (classmethod, staticmethod)) + return classmethod(fn) if needs_method else fn diff --git a/lib/keyring/compat/py312.py b/lib/keyring/compat/py312.py new file mode 100644 index 0000000..f14044a --- /dev/null +++ b/lib/keyring/compat/py312.py @@ -0,0 +1,9 @@ +import sys + +__all__ = ['metadata'] + + +if sys.version_info >= (3, 12): + import importlib.metadata as metadata +else: + import importlib_metadata as metadata diff --git a/lib/keyring/completion.py b/lib/keyring/completion.py new file mode 100644 index 0000000..7b0cd39 --- /dev/null +++ b/lib/keyring/completion.py @@ -0,0 +1,55 @@ +import argparse +import sys +from importlib.resources import files + +try: + import shtab +except ImportError: + pass + + +class _MissingCompletionAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string): + print("Install keyring[completion] for completion support.", file=sys.stderr) + parser.exit(1) + + +def add_completion_notice(parser): + """Add completion argument to parser.""" + parser.add_argument( + "--print-completion", + choices=["bash", "zsh", "tcsh"], + action=_MissingCompletionAction, + help="print shell completion script", + ) + return parser + + +def get_action(parser, option): + (match,) = (action for action in parser._actions if option in action.option_strings) + return match + + +def install_completion(parser): + preamble = dict( + bash=files(__package__) + .joinpath('backend_complete.bash') + .read_text(encoding='utf-8'), + zsh=files(__package__) + .joinpath('backend_complete.zsh') + .read_text(encoding='utf-8'), + ) + shtab.add_argument_to(parser, preamble=preamble) + get_action(parser, '--keyring-path').complete = shtab.DIR + get_action(parser, '--keyring-backend').complete = dict( + bash='_keyring_backends', + zsh='backend_complete', + ) + return parser + + +def install(parser): + try: + install_completion(parser) + except NameError: + add_completion_notice(parser) diff --git a/lib/keyring/core.py b/lib/keyring/core.py new file mode 100644 index 0000000..b108845 --- /dev/null +++ b/lib/keyring/core.py @@ -0,0 +1,202 @@ +""" +Core API functions and initialization routines. +""" + +from __future__ import annotations + +import configparser +import logging +import os +import sys +import typing + +from . import backend, credentials +from .backends import fail +from .util import platform_ as platform + +LimitCallable = typing.Callable[[backend.KeyringBackend], bool] + +log = logging.getLogger(__name__) + +_keyring_backend = None + + +def set_keyring(keyring: backend.KeyringBackend) -> None: + """Set current keyring backend.""" + global _keyring_backend + if not isinstance(keyring, backend.KeyringBackend): + raise TypeError("The keyring must be an instance of KeyringBackend") + _keyring_backend = keyring + + +def get_keyring() -> backend.KeyringBackend: + """Get current keyring backend.""" + if _keyring_backend is None: + init_backend() + return typing.cast(backend.KeyringBackend, _keyring_backend) + + +def disable() -> None: + """ + Configure the null keyring as the default. + + >>> fs = getfixture('fs') + >>> disable() + >>> disable() + Traceback (most recent call last): + ... + RuntimeError: Refusing to overwrite... + """ + root = platform.config_root() + try: + os.makedirs(root) + except OSError: + pass + filename = os.path.join(root, 'keyringrc.cfg') + if os.path.exists(filename): + msg = f"Refusing to overwrite {filename}" + raise RuntimeError(msg) + with open(filename, 'w', encoding='utf-8') as file: + file.write('[backend]\ndefault-keyring=keyring.backends.null.Keyring') + + +def get_password(service_name: str, username: str) -> str | None: + """Get password from the specified service.""" + return get_keyring().get_password(service_name, username) + + +def set_password(service_name: str, username: str, password: str) -> None: + """Set password for the user in the specified service.""" + get_keyring().set_password(service_name, username, password) + + +def delete_password(service_name: str, username: str) -> None: + """Delete the password for the user in the specified service.""" + get_keyring().delete_password(service_name, username) + + +def get_credential( + service_name: str, username: str | None +) -> credentials.Credential | None: + """Get a Credential for the specified service.""" + return get_keyring().get_credential(service_name, username) + + +def recommended(backend) -> bool: + return backend.priority >= 1 + + +def init_backend(limit: LimitCallable | None = None): + """ + Load a detected backend. + """ + set_keyring(_detect_backend(limit)) + + +def _detect_backend(limit: LimitCallable | None = None): + """ + Return a keyring specified in the config file or infer the best available. + + Limit, if supplied, should be a callable taking a backend and returning + True if that backend should be included for consideration. + """ + + # save the limit for the chainer to honor + backend._limit = limit + return ( + load_env() + or load_config() + or max( + # all keyrings passing the limit filter + filter(limit, backend.get_all_keyring()), + default=fail.Keyring(), + key=backend.by_priority, + ) + ) + + +def _load_keyring_class(keyring_name: str) -> type[backend.KeyringBackend]: + """ + Load the keyring class indicated by name. + + These popular names are tested to ensure their presence. + + >>> popular_names = [ + ... 'keyring.backends.Windows.WinVaultKeyring', + ... 'keyring.backends.macOS.Keyring', + ... 'keyring.backends.kwallet.DBusKeyring', + ... 'keyring.backends.SecretService.Keyring', + ... ] + >>> list(map(_load_keyring_class, popular_names)) + [...] + """ + module_name, sep, class_name = keyring_name.rpartition('.') + __import__(module_name) + module = sys.modules[module_name] + return getattr(module, class_name) + + +def load_keyring(keyring_name: str) -> backend.KeyringBackend: + """ + Load the specified keyring by name (a fully-qualified name to the + keyring, such as 'keyring.backends.file.PlaintextKeyring') + """ + class_ = _load_keyring_class(keyring_name) + # invoke the priority to ensure it is viable, or raise a RuntimeError + class_.priority # noqa: B018 + return class_() + + +def load_env() -> backend.KeyringBackend | None: + """Load a keyring configured in the environment variable.""" + try: + return load_keyring(os.environ['PYTHON_KEYRING_BACKEND']) + except KeyError: + return None + + +def _config_path(): + return platform.config_root() / 'keyringrc.cfg' + + +def _ensure_path(path): + if not path.exists(): + raise FileNotFoundError(path) + return path + + +def load_config() -> backend.KeyringBackend | None: + """Load a keyring using the config file in the config root.""" + + config = configparser.RawConfigParser() + try: + config.read(_ensure_path(_config_path()), encoding='utf-8') + except FileNotFoundError: + return None + _load_keyring_path(config) + + # load the keyring class name, and then load this keyring + try: + if config.has_section("backend"): + keyring_name = config.get("backend", "default-keyring").strip() + else: + return None + + except (configparser.NoOptionError, ImportError): + logger = logging.getLogger('keyring') + logger.warning( + "Keyring config file contains incorrect values.\n" + + f"Config file: {_config_path()}" + ) + return None + + return load_keyring(keyring_name) + + +def _load_keyring_path(config: configparser.RawConfigParser) -> None: + "load the keyring-path option (if present)" + try: + path = config.get("backend", "keyring-path").strip() + sys.path.insert(0, os.path.expanduser(path)) + except (configparser.NoOptionError, configparser.NoSectionError): + pass diff --git a/lib/keyring/credentials.py b/lib/keyring/credentials.py new file mode 100644 index 0000000..6a2cecd --- /dev/null +++ b/lib/keyring/credentials.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import abc +import os + + +class Credential(metaclass=abc.ABCMeta): + """Abstract class to manage credentials""" + + @abc.abstractproperty + def username(self) -> str: ... + + @abc.abstractproperty + def password(self) -> str: ... + + def _vars(self) -> dict[str, str]: + return dict(username=self.username, password=self.password) + + +class SimpleCredential(Credential): + """Simple credentials implementation""" + + def __init__(self, username: str, password: str): + self._username = username + self._password = password + + @property + def username(self) -> str: + return self._username + + @property + def password(self) -> str: + return self._password + + +class AnonymousCredential(SimpleCredential): + def __init__(self, password: str): + self._password = password + + @property + def username(self) -> str: + raise ValueError("Anonymous credential has no username") + + def _vars(self) -> dict[str, str]: + return dict(password=self.password) + + +class EnvironCredential(Credential): + """ + Source credentials from environment variables. + + Actual sourcing is deferred until requested. + + Supports comparison by equality. + + >>> e1 = EnvironCredential('a', 'b') + >>> e2 = EnvironCredential('a', 'b') + >>> e3 = EnvironCredential('a', 'c') + >>> e1 == e2 + True + >>> e2 == e3 + False + """ + + def __init__(self, user_env_var: str, pwd_env_var: str): + self.user_env_var = user_env_var + self.pwd_env_var = pwd_env_var + + def __eq__(self, other: object) -> bool: + return vars(self) == vars(other) + + def _get_env(self, env_var: str) -> str: + """Helper to read an environment variable""" + value = os.environ.get(env_var) + if not value: + raise ValueError(f'Missing environment variable:{env_var}') + return value + + @property + def username(self) -> str: + return self._get_env(self.user_env_var) + + @property + def password(self) -> str: + return self._get_env(self.pwd_env_var) diff --git a/lib/keyring/devpi_client.py b/lib/keyring/devpi_client.py new file mode 100644 index 0000000..dd4b09d --- /dev/null +++ b/lib/keyring/devpi_client.py @@ -0,0 +1,29 @@ +import functools + +import pluggy +from jaraco.context import suppress + +import keyring.errors + +hookimpl = pluggy.HookimplMarker("devpiclient") + + +def restore_signature(func): + # workaround for pytest-dev/pluggy#358 + @functools.wraps(func) + def wrapper(url, username): + return func(url, username) + + return wrapper + + +@hookimpl() +@restore_signature +@suppress(keyring.errors.KeyringError) +def devpiclient_get_password(url, username): + """ + >>> pluggy._hooks.varnames(devpiclient_get_password) + (('url', 'username'), ()) + >>> + """ + return keyring.get_password(url, username) diff --git a/lib/keyring/errors.py b/lib/keyring/errors.py new file mode 100644 index 0000000..ed97cf9 --- /dev/null +++ b/lib/keyring/errors.py @@ -0,0 +1,67 @@ +import sys +import warnings + + +class KeyringError(Exception): + """Base class for exceptions in keyring""" + + +class PasswordSetError(KeyringError): + """Raised when the password can't be set.""" + + +class PasswordDeleteError(KeyringError): + """Raised when the password can't be deleted.""" + + +class InitError(KeyringError): + """Raised when the keyring could not be initialised""" + + +class KeyringLocked(KeyringError): + """Raised when the keyring failed unlocking""" + + +class NoKeyringError(KeyringError, RuntimeError): + """Raised when there is no keyring backend""" + + +class ExceptionRaisedContext: + """ + An exception-trapping context that indicates whether an exception was + raised. + """ + + def __init__(self, ExpectedException=Exception): + warnings.warn( + "ExceptionRaisedContext is deprecated; use `jaraco.context.ExceptionTrap`", + DeprecationWarning, + stacklevel=2, + ) + self.ExpectedException = ExpectedException + self.exc_info = None + + def __enter__(self): + self.exc_info = object.__new__(ExceptionInfo) + return self.exc_info + + def __exit__(self, *exc_info): + self.exc_info.__init__(*exc_info) + return self.exc_info.type and issubclass( + self.exc_info.type, self.ExpectedException + ) + + +class ExceptionInfo: + def __init__(self, *info): + if not info: + info = sys.exc_info() + self.type, self.value, _ = info + + def __bool__(self): + """ + Return True if an exception occurred + """ + return bool(self.type) + + __nonzero__ = __bool__ diff --git a/lib/keyring/http.py b/lib/keyring/http.py new file mode 100644 index 0000000..2561535 --- /dev/null +++ b/lib/keyring/http.py @@ -0,0 +1,39 @@ +""" +urllib2.HTTPPasswordMgr object using the keyring, for use with the +urllib2.HTTPBasicAuthHandler. + +usage: + import urllib2 + handlers = [urllib2.HTTPBasicAuthHandler(PasswordMgr())] + urllib2.install_opener(handlers) + urllib2.urlopen(...) + +This will prompt for a password if one is required and isn't already +in the keyring. Then, it adds it to the keyring for subsequent use. +""" + +import getpass + +from . import delete_password, get_password, set_password + + +class PasswordMgr: + def get_username(self, realm, authuri): + return getpass.getuser() + + def add_password(self, realm, authuri, password): + user = self.get_username(realm, authuri) + set_password(realm, user, password) + + def find_user_password(self, realm, authuri): + user = self.get_username(realm, authuri) + password = get_password(realm, user) + if password is None: + prompt = f'password for {user}@{realm} for {authuri}: ' + password = getpass.getpass(prompt) + set_password(realm, user, password) + return user, password + + def clear_password(self, realm, authuri): + user = self.get_username(realm, authuri) + delete_password(realm, user) diff --git a/lib/keyring/py.typed b/lib/keyring/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/lib/keyring/testing/__init__.py b/lib/keyring/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/keyring/testing/__pycache__/__init__.cpython-314.pyc b/lib/keyring/testing/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..add4385696d56aeb3e5790cf648a34ffb6c5495d GIT binary patch literal 162 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%U-`Ew?Mxj zvp}~bu_!&YM7K1(Brnl4$4EaXGfBUovLquvPd_`gvM4hzUB4u?xCBJU$7kkcmc+;F j6;$5hu*uC&Da}c>D`Ewj2ePpk#Q4O_$jDg43}gWS5l|-e literal 0 HcmV?d00001 diff --git a/lib/keyring/testing/__pycache__/backend.cpython-314.pyc b/lib/keyring/testing/__pycache__/backend.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d50f0d516ad7264a2537c8cac34e62933d219b57 GIT binary patch literal 12196 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 literal 0 HcmV?d00001 diff --git a/lib/keyring/testing/__pycache__/util.cpython-314.pyc b/lib/keyring/testing/__pycache__/util.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad4e3741ac4b820178433129aea5c3812dfa6f3c GIT binary patch literal 3760 zcma)9-ER}w6~E&d+Y^7pA>SJU8Dip%A&Jo@gqBSZ=4)B9q+S;}(kd_-?6Ey%JmcQE zW=Wo`R%(-~Dq@8;L8=5i0dG~Gwkm%_(xk*4c&OBUS@k90RhND1xp!>G$%byPtaH!Y z&vWkioev*x4f+tYe}DL;Ht$8~U-YBigc?)*GcaXz10}hS5pjBsaO~SfT&(8_&w7Cf zUFZl(x_VHOpKv)R5chSI2-J_=>61bany7seZ_?d^hRj=Xd&%9TpM) zb^kb*^zuv=9m3jZDyy2vmCFPPS0_ z;4}}D%uCNEV$ut=B`FeL3hqquaF5Rpd|W7+n9OLpuHwbB=S_pCcZgI_3?-*xiI~9N zQYFQZG`3JOvyvj^wA-p78A?HwOw4*q$|{;Zmgbs7rV@i?#43H3GMZ~M8k0#BM$fp} zXPoV(r&v*!Em*3a#Ud=R=*Gw~`@r_u$C@Lgx(Muwc2&MqG z{aMY($OSV~(p9^6m#j@Qt+3SU1_I*xJ60C&EpGacR62T=W?u6sFo=UNm99escq+If z{1h<%OZXfU{!^6a=II{RQrtw%ka>=&6Lm4m*nUU8ix`_2#;A5U1jI2}T_fF8-l0*` z3Untj!RgJ=(3&{378-ih`rhjBMr(LY3_FT7qsk3iKv1!|$@a$J-FI-6 zLW$-_fEBd0p-U$aDsuCJ^Nmn~+GtNpP;K_K6nI!Y7QoqiX6a{&ivXSZ0HGFi7&YC% zqSNjT32LQ1?FPtSzhPhpKT)qOnpn$ehN3%9&sz(Y?R9Ws*?iF~+ARf@C^ChV?a>TN z#iV8vTVP(r{77tg@vr;mge;?NIm zN8g`Cgy-y>Z=kP2zoZ_<#_%jd)Qhbvn z``9hzVm6i>8#jDB>cQ`Ue7GNo?ZIlnyshG1`r+1yTCi{*@KM2u0Rn_DFdKv`+kIQn zOR7a5BXUft*$3j4dS~BwP|dbR7VuWgEhmA=CdM<0xC^^LCgjlSrMEnitW zUkPmGjSac573ZFVFL*9$+dt4d_b0eSyEW| zg0GM)XEkgQT{Bd}gaNNCXUw!LV;WHL0ZO_k8KC5SAa<85TSOsRT2=_b+RYM#YS?%{ zmeaaoSs5)&(yFe@a+I@!O@pwF+WIWRFQ%`>2+I)E9&djOZ}AV|(i?$uAN8)CIo7}&ZB6)r#Le(C z0qARlOJ^0ODZY-+xiFP6C1kgl6EkkKZm9kJ4ki05oSXKt(o5lzf5gEyXfes)QnS5Sb4LgXQZ2|eifgL|Ab2e*JxB3@Gqe{2}~KyH!wQi$aV>e z=sj(mquKEsCxD(&ZgDccpQ#~IK?SH_O<$0lw`7(xtufWOtzpxEDuzCl?J^<%)W&h?8Kv7oFvpBKl{r!JTKK$&#XY2m)r>B6d3FA&?XvhwWsp34<@DpGZ zt_;=eejbqGpi+h!R~MU#l}1izYj|qi3qfywar7hAP%$7(QX~u@Gz*difyl^7pKBy9 z>8g=~Zc00IYbvIlvC$6##W=`RrOAJab3(;6bP3!E3JMr=+I|V_2>Q9JE{NmSGf?l) zGJ&QL>V&o03X%sC%l4%6rj}NtyzLr00im^Ilq@x4dn~hr)2dT9O66KYpUQz+cZ&Fo zUDFD}EY)gkN;lJrZk-uxthfL|vRdmc#rax${4e6ux%-nV(_c(K>fY)YUh5eCtN1<3 zNWTt8AU4>-FHFs_-5^&^g-`Alrn$!{^RkWrK}YD?t@tCDKS$TlO!c;m{^<|iJ+*vl zvpxRY{gv`u+-Q$)_~UCr+{r&d+gFP_P|V{~um?{A0bQ9APG+T;wp(lKYX3L<| fVBJ6PDDq_V@#x?Ev9bsu;n2{3kc&IVvHSlEL|zsC literal 0 HcmV?d00001 diff --git a/lib/keyring/testing/backend.py b/lib/keyring/testing/backend.py new file mode 100644 index 0000000..89a414b --- /dev/null +++ b/lib/keyring/testing/backend.py @@ -0,0 +1,200 @@ +""" +Common test functionality for backends. +""" + +import os +import string + +import pytest + +from keyring import errors + +from .util import random_string + +# unicode only characters +# Sourced from The Quick Brown Fox... Pangrams +# http://www.columbia.edu/~fdc/utf8/ +UNICODE_CHARS = ( + "זהכיףסתםלשמועאיךתנצחקרפדעץטובבגן" + "ξεσκεπάζωτηνψυχοφθόραβδελυγμία" + "Съешьжеещёэтихмягкихфранцузскихбулокдавыпейчаю" + "Жълтатадюлябешещастливачепухъткойтоцъфназамръзнакатогьон" +) + +# ensure no-ascii chars slip by - watch your editor! +assert min(ord(char) for char in UNICODE_CHARS) > 127 + + +def is_ascii_printable(s): + return all(32 <= ord(c) < 127 for c in s) + + +class BackendBasicTests: + """Test for the keyring's basic functions. password_set and password_get""" + + DIFFICULT_CHARS = string.whitespace + string.punctuation + + @pytest.fixture(autouse=True) + def _init_properties(self, request): + self.keyring = self.init_keyring() + self.credentials_created = set() + request.addfinalizer(self.cleanup) + + def cleanup(self): + for item in self.credentials_created: + self.keyring.delete_password(*item) + + def set_password(self, service, username, password): + # set the password and save the result so the test runner can clean + # up after if necessary. + self.keyring.set_password(service, username, password) + self.credentials_created.add((service, username)) + + def check_set_get(self, service, username, password): + keyring = self.keyring + + # for the non-existent password + assert keyring.get_password(service, username) is None + + # common usage + self.set_password(service, username, password) + assert keyring.get_password(service, username) == password + + # for the empty password + self.set_password(service, username, "") + assert keyring.get_password(service, username) == "" + + def test_password_set_get(self): + password = random_string(20) + username = random_string(20) + service = random_string(20) + self.check_set_get(service, username, password) + + def test_set_after_set_blank(self): + service = random_string(20) + username = random_string(20) + self.keyring.set_password(service, username, "") + self.keyring.set_password(service, username, "non-blank") + + def test_difficult_chars(self): + password = random_string(20, self.DIFFICULT_CHARS) + username = random_string(20, self.DIFFICULT_CHARS) + service = random_string(20, self.DIFFICULT_CHARS) + self.check_set_get(service, username, password) + + def test_delete_present(self): + password = random_string(20, self.DIFFICULT_CHARS) + username = random_string(20, self.DIFFICULT_CHARS) + service = random_string(20, self.DIFFICULT_CHARS) + self.keyring.set_password(service, username, password) + self.keyring.delete_password(service, username) + assert self.keyring.get_password(service, username) is None + + def test_delete_not_present(self): + username = random_string(20, self.DIFFICULT_CHARS) + service = random_string(20, self.DIFFICULT_CHARS) + with pytest.raises(errors.PasswordDeleteError): + self.keyring.delete_password(service, username) + + def test_delete_one_in_group(self): + username1 = random_string(20, self.DIFFICULT_CHARS) + username2 = random_string(20, self.DIFFICULT_CHARS) + password = random_string(20, self.DIFFICULT_CHARS) + service = random_string(20, self.DIFFICULT_CHARS) + self.keyring.set_password(service, username1, password) + self.set_password(service, username2, password) + self.keyring.delete_password(service, username1) + assert self.keyring.get_password(service, username2) == password + + def test_name_property(self): + assert is_ascii_printable(self.keyring.name) + + def test_unicode_chars(self): + password = random_string(20, UNICODE_CHARS) + username = random_string(20, UNICODE_CHARS) + service = random_string(20, UNICODE_CHARS) + self.check_set_get(service, username, password) + + def test_unicode_and_ascii_chars(self): + source = ( + random_string(10, UNICODE_CHARS) + + random_string(10) + + random_string(10, self.DIFFICULT_CHARS) + ) + password = random_string(20, source) + username = random_string(20, source) + service = random_string(20, source) + self.check_set_get(service, username, password) + + def test_different_user(self): + """ + Issue #47 reports that WinVault isn't storing passwords for + multiple users. This test exercises that test for each of the + backends. + """ + + keyring = self.keyring + self.set_password('service1', 'user1', 'password1') + self.set_password('service1', 'user2', 'password2') + assert keyring.get_password('service1', 'user1') == 'password1' + assert keyring.get_password('service1', 'user2') == 'password2' + self.set_password('service2', 'user3', 'password3') + assert keyring.get_password('service1', 'user1') == 'password1' + + def test_credential(self): + keyring = self.keyring + + cred = keyring.get_credential('service', None) + assert cred is None + + self.set_password('service1', 'user1', 'password1') + self.set_password('service1', 'user2', 'password2') + + cred = keyring.get_credential('service1', None) + assert cred is None or (cred.username, cred.password) in ( + ('user1', 'password1'), + ('user2', 'password2'), + ) + + cred = keyring.get_credential('service1', 'user2') + assert cred is not None + assert (cred.username, cred.password) in ( + ('user1', 'password1'), + ('user2', 'password2'), + ) + + @pytest.mark.xfail("platform.system() == 'Windows'", reason="#668") + def test_empty_username(self): + with pytest.deprecated_call(): + self.set_password('service1', '', 'password1') + assert self.keyring.get_password('service1', '') == 'password1' + + def test_set_properties(self, monkeypatch): + env = dict(KEYRING_PROPERTY_FOO_BAR='fizz buzz', OTHER_SETTING='ignore me') + monkeypatch.setattr(os, 'environ', env) + self.keyring.set_properties_from_env() + assert self.keyring.foo_bar == 'fizz buzz' + + def test_new_with_properties(self): + alt = self.keyring.with_properties(foo='bar') + assert alt is not self.keyring + assert alt.foo == 'bar' + with pytest.raises(AttributeError): + self.keyring.foo # noqa: B018 + + def test_wrong_username_returns_none(self): + keyring = self.keyring + service = 'test_wrong_username_returns_none' + cred = keyring.get_credential(service, None) + assert cred is None + + password_1 = 'password1' + password_2 = 'password2' + self.set_password(service, 'user1', password_1) + self.set_password(service, 'user2', password_2) + + assert keyring.get_credential(service, "user1").password == password_1 + assert keyring.get_credential(service, "user2").password == password_2 + + # Missing/wrong username should not return a cred + assert keyring.get_credential(service, "nobody!") is None diff --git a/lib/keyring/testing/util.py b/lib/keyring/testing/util.py new file mode 100644 index 0000000..b8ef4c6 --- /dev/null +++ b/lib/keyring/testing/util.py @@ -0,0 +1,68 @@ +import contextlib +import os +import random +import string +import sys + + +class ImportKiller: + "Context manager to make an import of a given name or names fail." + + def __init__(self, *names): + self.names = names + + def find_module(self, fullname, path=None): + if fullname in self.names: + return self + + def load_module(self, fullname): + assert fullname in self.names + raise ImportError(fullname) + + def __enter__(self): + self.original = {} + for name in self.names: + self.original[name] = sys.modules.pop(name, None) + sys.meta_path.insert(0, self) + + def __exit__(self, *args): + sys.meta_path.remove(self) + for key, value in self.original.items(): + if value is not None: + sys.modules[key] = value + + +@contextlib.contextmanager +def NoNoneDictMutator(destination, **changes): + """Helper context manager to make and unmake changes to a dict. + + A None is not a valid value for the destination, and so means that the + associated name should be removed.""" + original = {} + for key, value in changes.items(): + original[key] = destination.get(key) + if value is None: + if key in destination: + del destination[key] + else: + destination[key] = value + yield + for key, value in original.items(): + if value is None: + if key in destination: + del destination[key] + else: + destination[key] = value + + +def Environ(**changes): + """A context manager to temporarily change the os.environ""" + return NoNoneDictMutator(os.environ, **changes) + + +ALPHABET = string.ascii_letters + string.digits + + +def random_string(k, source=ALPHABET): + """Generate a random string with length k""" + return ''.join(random.choice(source) for _unused in range(k)) diff --git a/lib/keyring/util/__init__.py b/lib/keyring/util/__init__.py new file mode 100644 index 0000000..097a943 --- /dev/null +++ b/lib/keyring/util/__init__.py @@ -0,0 +1,11 @@ +import contextlib + + +def suppress_exceptions(callables, exceptions=Exception): + """ + yield the results of calling each element of callables, suppressing + any indicated exceptions. + """ + for callable in callables: + with contextlib.suppress(exceptions): + yield callable() diff --git a/lib/keyring/util/__pycache__/__init__.cpython-314.pyc b/lib/keyring/util/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f8fd4a1a9971ee6f2048f4f1601532fb78d57bb GIT binary patch literal 705 zcmYjPL2DC16n?W?Yea>z+twwo~-oNSieSx7t; zPu>O3oOWv1u3qD9y}E_$JM^zF~&SL+S06M&zp z$U>VXlOiU2@D$qQ0Z4d4)=67i1VN~3Chsx10cF&8;4<7_ z2MD#tiZP8x|7&L03Uo*V&!VD5T#{&s?n2`fz)l@)<*XXP9x3Cw)M)6Zlrn5L0Ss71 zyaE)~;MPprL$@X1S{X{%M~&@==%JT@lcvr|ReRx@W6ngE%08nJixVMZI($ZbPY53L zDD(V2Wr7V@AT!+C6)d)CoP;4NVx*Z~Fs3}{a^I7zOWDY0q2$9LuA4hmEj9gNAlXO? zzMJMUSyfLB<jJGv0;o9=6+@ZC2V9|YxzAYYD z_BYG^UMRl4y?=4(j}H0TSIeI0$UCxZ-2GzNf8*<@e|dAOK;D^~^B?n=d|KGLwD8$5 zFsp<`$T9VcPxnCMBl=brQ3bI;xP zbC;{}SOk#r=g9YZh6CU?>i8wtXq@~`8WnIKC`=Y0&oYQ{eHO6_dl8_35+F6V6hJ|R zBdyR9-v@>o0pxf5kS(hOmsq*0<@>GHtZRN3u}j<#>$xcMzmY@diX3W278E84Qv4ru zq1n8*oL6@c6Cl6FNh~(M$&IX<9i-9P!3H@|$iUmca`g?6!#*1xn_c$eRO=zt|PJ+YH>A8vV>2X=kj4zC1L55}#s-=vIRLrEe z;_@b1HOt~%Z4>I{Vv5IM%TUpZ2}`(RLoeUBo_nA{%v&m2HS}f7$)tf<)5g43exO6M zi~~gtrGl7;nxUz-hIul<*6D}5UBE#LZCV=UplKqT(kRk`Z%L?RNtSL6AQh4T1M5Xp zR)>x25?MvE+@z#6FDYpq>4xN|l}lTjxGPs68_3b1xi9r{!Q8N6HwjLqu@zEPz+v+0 zescJAa@a|J@`@8HlaKRDH62r`u1qUzUEFcP?k0 z@ST$Y8x2)5MK- z5BK*HH!4$yLacVPdb2iG9ovz1ha6#WUl^?mqc5i_nFc#MT>XQzBS>{lf)_}%Hv{q7 zUFYm0KMxQJO9VVO6hzXqpkU+uy3yM2$4MpB8ujs{gCit}1hZ7WsiMiR&vy&bW zu!o!4wx@IWL6w5O`vXWB{FG&ywnmVt>7tNLQwQiCn5o zv@4$6L5}dzzA#u926w~%sW=we_`Jt>T>fzPsgeGpC6YmyFB;~u zYS=E^IAm|yNF&d0^NGVk+cuv87d(&k@My^_tQ*=GOp!qJkHFSQbrfJ2<^XgYfcODO z9EXBT^0<>{29LTy&y_mpJ`RPM!M}Pytou>6HdCG18QA^!h4fr +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-Expression: MIT +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries +License-File: LICENSE +Project-URL: Documentation, https://more-itertools.readthedocs.io/en/stable/ +Project-URL: Homepage, https://github.com/more-itertools/more-itertools + +============== +More Itertools +============== + +.. image:: https://readthedocs.org/projects/more-itertools/badge/?version=latest + :target: https://more-itertools.readthedocs.io/en/stable/ + +Python's ``itertools`` library is a gem - you can compose elegant solutions +for a variety of problems with the functions it provides. In ``more-itertools`` +we collect additional building blocks, recipes, and routines for working with +Python iterables. + ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Grouping | `chunked `_, | +| | `ichunked `_, | +| | `chunked_even `_, | +| | `sliced `_, | +| | `constrained_batches `_, | +| | `distribute `_, | +| | `divide `_, | +| | `split_at `_, | +| | `split_before `_, | +| | `split_after `_, | +| | `split_into `_, | +| | `split_when `_, | +| | `bucket `_, | +| | `unzip `_, | +| | `batched `_, | +| | `grouper `_, | +| | `partition `_, | +| | `transpose `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Lookahead and lookback | `spy `_, | +| | `peekable `_, | +| | `seekable `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Windowing | `windowed `_, | +| | `substrings `_, | +| | `substrings_indexes `_, | +| | `stagger `_, | +| | `windowed_complete `_, | +| | `pairwise `_, | +| | `triplewise `_, | +| | `sliding_window `_, | +| | `subslices `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Augmenting | `count_cycle `_, | +| | `intersperse `_, | +| | `padded `_, | +| | `repeat_each `_, | +| | `mark_ends `_, | +| | `repeat_last `_, | +| | `adjacent `_, | +| | `groupby_transform `_, | +| | `pad_none `_, | +| | `ncycles `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Combining | `collapse `_, | +| | `sort_together `_, | +| | `interleave `_, | +| | `interleave_longest `_, | +| | `interleave_evenly `_, | +| | `interleave_randomly `_, | +| | `zip_offset `_, | +| | `zip_equal `_, | +| | `zip_broadcast `_, | +| | `flatten `_, | +| | `roundrobin `_, | +| | `prepend `_, | +| | `value_chain `_, | +| | `partial_product `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Summarizing | `ilen `_, | +| | `unique_to_each `_, | +| | `sample `_, | +| | `consecutive_groups `_, | +| | `run_length `_, | +| | `map_reduce `_, | +| | `join_mappings `_, | +| | `exactly_n `_, | +| | `is_sorted `_, | +| | `all_equal `_, | +| | `all_unique `_, | +| | `argmin `_, | +| | `argmax `_, | +| | `minmax `_, | +| | `first_true `_, | +| | `quantify `_, | +| | `iequals `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Selecting | `islice_extended `_, | +| | `first `_, | +| | `last `_, | +| | `one `_, | +| | `only `_, | +| | `strictly_n `_, | +| | `strip `_, | +| | `lstrip `_, | +| | `rstrip `_, | +| | `filter_except `_, | +| | `map_except `_, | +| | `filter_map `_, | +| | `iter_suppress `_, | +| | `nth_or_last `_, | +| | `extract `_, | +| | `unique_in_window `_, | +| | `before_and_after `_, | +| | `nth `_, | +| | `take `_, | +| | `tail `_, | +| | `unique_everseen `_, | +| | `unique_justseen `_, | +| | `unique `_, | +| | `duplicates_everseen `_, | +| | `duplicates_justseen `_, | +| | `classify_unique `_, | +| | `longest_common_prefix `_, | +| | `takewhile_inclusive `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Math | `dft `_, | +| | `idft `_, | +| | `convolve `_, | +| | `dotproduct `_, | +| | `matmul `_, | +| | `polynomial_from_roots `_, | +| | `polynomial_derivative `_, | +| | `polynomial_eval `_, | +| | `sum_of_squares `_, | +| | `running_median `_, | +| | `totient `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Integer math | `factor `_, | +| | `is_prime `_, | +| | `multinomial `_, | +| | `nth_prime `_, | +| | `sieve `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Combinatorics | `circular_shifts `_, | +| | `derangements `_, | +| | `gray_product `_, | +| | `outer_product `_, | +| | `partitions `_, | +| | `set_partitions `_, | +| | `powerset `_, | +| | `powerset_of_sets `_ | +| +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| | `distinct_combinations `_, | +| | `distinct_permutations `_ | +| +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| | `combination_index `_, | +| | `combination_with_replacement_index `_, | +| | `permutation_index `_, | +| | `product_index `_ | +| +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| | `nth_combination `_, | +| | `nth_combination_with_replacement `_, | +| | `nth_permutation `_, | +| | `nth_product `_ | +| +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| | `random_combination `_, | +| | `random_combination_with_replacement `_, | +| | `random_permutation `_, | +| | `random_product `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Wrapping | `always_iterable `_, | +| | `always_reversible `_, | +| | `countable `_, | +| | `consumer `_, | +| | `with_iter `_, | +| | `iter_except `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Others | `locate `_, | +| | `rlocate `_, | +| | `replace `_, | +| | `numeric_range `_, | +| | `side_effect `_, | +| | `iterate `_, | +| | `loops `_, | +| | `difference `_, | +| | `make_decorator `_, | +| | `SequenceView `_, | +| | `time_limited `_, | +| | `map_if `_, | +| | `iter_index `_, | +| | `consume `_, | +| | `tabulate `_, | +| | `repeatfunc `_, | +| | `reshape `_, | +| | `doublestarmap `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + +Getting started +=============== + +To get started, install the library with `pip `_: + +.. code-block:: shell + + pip install more-itertools + +The recipes from the `itertools docs `_ +are included in the top-level package: + +.. code-block:: python + + >>> from more_itertools import flatten + >>> iterable = [(0, 1), (2, 3)] + >>> list(flatten(iterable)) + [0, 1, 2, 3] + +Several new recipes are available as well: + +.. code-block:: python + + >>> from more_itertools import chunked + >>> iterable = [0, 1, 2, 3, 4, 5, 6, 7, 8] + >>> list(chunked(iterable, 3)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + >>> from more_itertools import spy + >>> iterable = (x * x for x in range(1, 6)) + >>> head, iterable = spy(iterable, n=3) + >>> list(head) + [1, 4, 9] + >>> list(iterable) + [1, 4, 9, 16, 25] + + + +For the full listing of functions, see the `API documentation `_. + + +Links elsewhere +=============== + +Blog posts about ``more-itertools``: + +* `Yo, I heard you like decorators `__ +* `Tour of Python Itertools `__ (`Alternate `__) +* `Real-World Python More Itertools `_ + + +Development +=========== + +``more-itertools`` is maintained by `@erikrose `_ +and `@bbayles `_, with help from `many others `_. +If you have a problem or suggestion, please file a bug or pull request in this +repository. Thanks for contributing! + + +Version History +=============== + +The version history can be found in `documentation `_. + diff --git a/lib/more_itertools-10.8.0.dist-info/RECORD b/lib/more_itertools-10.8.0.dist-info/RECORD new file mode 100644 index 0000000..03ae95c --- /dev/null +++ b/lib/more_itertools-10.8.0.dist-info/RECORD @@ -0,0 +1,15 @@ +more_itertools-10.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +more_itertools-10.8.0.dist-info/METADATA,sha256=arNRUUWr5YsGfwh8hnYxz0z11lP-2BuWQu4SCGw5BLg,39413 +more_itertools-10.8.0.dist-info/RECORD,, +more_itertools-10.8.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +more_itertools-10.8.0.dist-info/licenses/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053 +more_itertools/__init__.py,sha256=5F7E_zpoGcEBW_T_3WE0WYYt8j-gJodIuiBcOJxrOv8,149 +more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 +more_itertools/__pycache__/__init__.cpython-314.pyc,, +more_itertools/__pycache__/more.cpython-314.pyc,, +more_itertools/__pycache__/recipes.cpython-314.pyc,, +more_itertools/more.py,sha256=mNPKKu5UI7lRL460vgm0QTCWFiGMVCMosSPxVSdibos,163690 +more_itertools/more.pyi,sha256=fpEgNX3O66wY5cnT-s5VYDKNUpAcaCyU3iP84It3OOM,27119 +more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +more_itertools/recipes.py,sha256=Ma-kuBNZDFhaQDbIJgRmnrG86WzaupbOyUV3v8je3xw,41811 +more_itertools/recipes.pyi,sha256=LNRwN-OL3nkMfQAqx-PPc1fBaetUObb_Z6mdePyzh1c,6226 diff --git a/lib/more_itertools-10.8.0.dist-info/WHEEL b/lib/more_itertools-10.8.0.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/lib/more_itertools-10.8.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/more_itertools-10.8.0.dist-info/licenses/LICENSE b/lib/more_itertools-10.8.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..0a523be --- /dev/null +++ b/lib/more_itertools-10.8.0.dist-info/licenses/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/more_itertools/__init__.py b/lib/more_itertools/__init__.py new file mode 100644 index 0000000..24216c5 --- /dev/null +++ b/lib/more_itertools/__init__.py @@ -0,0 +1,6 @@ +"""More routines for operating on iterables, beyond itertools""" + +from .more import * # noqa +from .recipes import * # noqa + +__version__ = '10.8.0' diff --git a/lib/more_itertools/__init__.pyi b/lib/more_itertools/__init__.pyi new file mode 100644 index 0000000..96f6e36 --- /dev/null +++ b/lib/more_itertools/__init__.pyi @@ -0,0 +1,2 @@ +from .more import * +from .recipes import * diff --git a/lib/more_itertools/__pycache__/__init__.cpython-314.pyc b/lib/more_itertools/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a601ec0e965181999d2954a2d9ee0d3761c8db60 GIT binary patch literal 310 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 DOI;34 literal 0 HcmV?d00001 diff --git a/lib/more_itertools/more.py b/lib/more_itertools/more.py new file mode 100755 index 0000000..bf50195 --- /dev/null +++ b/lib/more_itertools/more.py @@ -0,0 +1,5303 @@ +import math +import warnings + +from collections import Counter, defaultdict, deque, abc +from collections.abc import Sequence +from contextlib import suppress +from functools import cached_property, partial, reduce, wraps +from heapq import heapify, heapreplace +from itertools import ( + chain, + combinations, + compress, + count, + cycle, + dropwhile, + groupby, + islice, + permutations, + repeat, + starmap, + takewhile, + tee, + zip_longest, + product, +) +from math import comb, e, exp, factorial, floor, fsum, log, log1p, perm, tau +from math import ceil +from queue import Empty, Queue +from random import random, randrange, shuffle, uniform +from operator import ( + attrgetter, + is_not, + itemgetter, + lt, + mul, + neg, + sub, + gt, +) +from sys import hexversion, maxsize +from time import monotonic + +from .recipes import ( + _marker, + _zip_equal, + UnequalIterablesError, + consume, + first_true, + flatten, + is_prime, + nth, + powerset, + sieve, + take, + unique_everseen, + all_equal, + batched, +) + +__all__ = [ + 'AbortThread', + 'SequenceView', + 'UnequalIterablesError', + 'adjacent', + 'all_unique', + 'always_iterable', + 'always_reversible', + 'argmax', + 'argmin', + 'bucket', + 'callback_iter', + 'chunked', + 'chunked_even', + 'circular_shifts', + 'collapse', + 'combination_index', + 'combination_with_replacement_index', + 'consecutive_groups', + 'constrained_batches', + 'consumer', + 'count_cycle', + 'countable', + 'derangements', + 'dft', + 'difference', + 'distinct_combinations', + 'distinct_permutations', + 'distribute', + 'divide', + 'doublestarmap', + 'duplicates_everseen', + 'duplicates_justseen', + 'classify_unique', + 'exactly_n', + 'extract', + 'filter_except', + 'filter_map', + 'first', + 'gray_product', + 'groupby_transform', + 'ichunked', + 'iequals', + 'idft', + 'ilen', + 'interleave', + 'interleave_evenly', + 'interleave_longest', + 'interleave_randomly', + 'intersperse', + 'is_sorted', + 'islice_extended', + 'iterate', + 'iter_suppress', + 'join_mappings', + 'last', + 'locate', + 'longest_common_prefix', + 'lstrip', + 'make_decorator', + 'map_except', + 'map_if', + 'map_reduce', + 'mark_ends', + 'minmax', + 'nth_or_last', + 'nth_permutation', + 'nth_prime', + 'nth_product', + 'nth_combination_with_replacement', + 'numeric_range', + 'one', + 'only', + 'outer_product', + 'padded', + 'partial_product', + 'partitions', + 'peekable', + 'permutation_index', + 'powerset_of_sets', + 'product_index', + 'raise_', + 'repeat_each', + 'repeat_last', + 'replace', + 'rlocate', + 'rstrip', + 'run_length', + 'sample', + 'seekable', + 'set_partitions', + 'side_effect', + 'sliced', + 'sort_together', + 'split_after', + 'split_at', + 'split_before', + 'split_into', + 'split_when', + 'spy', + 'stagger', + 'strip', + 'strictly_n', + 'substrings', + 'substrings_indexes', + 'takewhile_inclusive', + 'time_limited', + 'unique_in_window', + 'unique_to_each', + 'unzip', + 'value_chain', + 'windowed', + 'windowed_complete', + 'with_iter', + 'zip_broadcast', + 'zip_equal', + 'zip_offset', +] + +# math.sumprod is available for Python 3.12+ +try: + from math import sumprod as _fsumprod + +except ImportError: # pragma: no cover + # Extended precision algorithms from T. J. Dekker, + # "A Floating-Point Technique for Extending the Available Precision" + # https://csclub.uwaterloo.ca/~pbarfuss/dekker1971.pdf + # Formulas: (5.5) (5.6) and (5.8). Code: mul12() + + def dl_split(x: float): + "Split a float into two half-precision components." + t = x * 134217729.0 # Veltkamp constant = 2.0 ** 27 + 1 + hi = t - (t - x) + lo = x - hi + return hi, lo + + def dl_mul(x, y): + "Lossless multiplication." + xx_hi, xx_lo = dl_split(x) + yy_hi, yy_lo = dl_split(y) + p = xx_hi * yy_hi + q = xx_hi * yy_lo + xx_lo * yy_hi + z = p + q + zz = p - z + q + xx_lo * yy_lo + return z, zz + + def _fsumprod(p, q): + return fsum(chain.from_iterable(map(dl_mul, p, q))) + + +def chunked(iterable, n, strict=False): + """Break *iterable* into lists of length *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) + [[1, 2, 3], [4, 5, 6]] + + By the default, the last yielded list will have fewer than *n* elements + if the length of *iterable* is not divisible by *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) + [[1, 2, 3], [4, 5, 6], [7, 8]] + + To use a fill-in value instead, see the :func:`grouper` recipe. + + If the length of *iterable* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + list is yielded. + + """ + iterator = iter(partial(take, n, iter(iterable)), []) + if strict: + if n is None: + raise ValueError('n must not be None when using strict mode.') + + def ret(): + for chunk in iterator: + if len(chunk) != n: + raise ValueError('iterable is not divisible by n.') + yield chunk + + return ret() + else: + return iterator + + +def first(iterable, default=_marker): + """Return the first item of *iterable*, or *default* if *iterable* is + empty. + + >>> first([0, 1, 2, 3]) + 0 + >>> first([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + + :func:`first` is useful when you have a generator of expensive-to-retrieve + values and want any arbitrary one. It is marginally shorter than + ``next(iter(iterable), default)``. + + """ + for item in iterable: + return item + if default is _marker: + raise ValueError( + 'first() was called on an empty iterable, ' + 'and no default value was provided.' + ) + return default + + +def last(iterable, default=_marker): + """Return the last item of *iterable*, or *default* if *iterable* is + empty. + + >>> last([0, 1, 2, 3]) + 3 + >>> last([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + try: + if isinstance(iterable, Sequence): + return iterable[-1] + # Work around https://bugs.python.org/issue38525 + if getattr(iterable, '__reversed__', None): + return next(reversed(iterable)) + return deque(iterable, maxlen=1)[-1] + except (IndexError, TypeError, StopIteration): + if default is _marker: + raise ValueError( + 'last() was called on an empty iterable, ' + 'and no default value was provided.' + ) + return default + + +def nth_or_last(iterable, n, default=_marker): + """Return the nth or the last item of *iterable*, + or *default* if *iterable* is empty. + + >>> nth_or_last([0, 1, 2, 3], 2) + 2 + >>> nth_or_last([0, 1], 2) + 1 + >>> nth_or_last([], 0, 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + return last(islice(iterable, n + 1), default=default) + + +class peekable: + """Wrap an iterator to allow lookahead and prepending elements. + + Call :meth:`peek` on the result to get the value that will be returned + by :func:`next`. This won't advance the iterator: + + >>> p = peekable(['a', 'b']) + >>> p.peek() + 'a' + >>> next(p) + 'a' + + Pass :meth:`peek` a default value to return that instead of raising + ``StopIteration`` when the iterator is exhausted. + + >>> p = peekable([]) + >>> p.peek('hi') + 'hi' + + peekables also offer a :meth:`prepend` method, which "inserts" items + at the head of the iterable: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> p.peek() + 11 + >>> list(p) + [11, 12, 1, 2, 3] + + peekables can be indexed. Index 0 is the item that will be returned by + :func:`next`, index 1 is the item after that, and so on: + The values up to the given index will be cached. + + >>> p = peekable(['a', 'b', 'c', 'd']) + >>> p[0] + 'a' + >>> p[1] + 'b' + >>> next(p) + 'a' + + Negative indexes are supported, but be aware that they will cache the + remaining items in the source iterator, which may require significant + storage. + + To check whether a peekable is exhausted, check its truth value: + + >>> p = peekable(['a', 'b']) + >>> if p: # peekable has items + ... list(p) + ['a', 'b'] + >>> if not p: # peekable is exhausted + ... list(p) + [] + + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self._cache = deque() + + def __iter__(self): + return self + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + """Return the item that will be next returned from ``next()``. + + Return ``default`` if there are no items left. If ``default`` is not + provided, raise ``StopIteration``. + + """ + if not self._cache: + try: + self._cache.append(next(self._it)) + except StopIteration: + if default is _marker: + raise + return default + return self._cache[0] + + def prepend(self, *items): + """Stack up items to be the next ones returned from ``next()`` or + ``self.peek()``. The items will be returned in + first in, first out order:: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> list(p) + [11, 12, 1, 2, 3] + + It is possible, by prepending items, to "resurrect" a peekable that + previously raised ``StopIteration``. + + >>> p = peekable([]) + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + >>> p.prepend(1) + >>> next(p) + 1 + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + + """ + self._cache.extendleft(reversed(items)) + + def __next__(self): + if self._cache: + return self._cache.popleft() + + return next(self._it) + + def _get_slice(self, index): + # Normalize the slice's arguments + step = 1 if (index.step is None) else index.step + if step > 0: + start = 0 if (index.start is None) else index.start + stop = maxsize if (index.stop is None) else index.stop + elif step < 0: + start = -1 if (index.start is None) else index.start + stop = (-maxsize - 1) if (index.stop is None) else index.stop + else: + raise ValueError('slice step cannot be zero') + + # If either the start or stop index is negative, we'll need to cache + # the rest of the iterable in order to slice from the right side. + if (start < 0) or (stop < 0): + self._cache.extend(self._it) + # Otherwise we'll need to find the rightmost index and cache to that + # point. + else: + n = min(max(start, stop) + 1, maxsize) + cache_len = len(self._cache) + if n >= cache_len: + self._cache.extend(islice(self._it, n - cache_len)) + + return list(self._cache)[index] + + def __getitem__(self, index): + if isinstance(index, slice): + return self._get_slice(index) + + cache_len = len(self._cache) + if index < 0: + self._cache.extend(self._it) + elif index >= cache_len: + self._cache.extend(islice(self._it, index + 1 - cache_len)) + + return self._cache[index] + + +def consumer(func): + """Decorator that automatically advances a PEP-342-style "reverse iterator" + to its first yield point so you don't have to call ``next()`` on it + manually. + + >>> @consumer + ... def tally(): + ... i = 0 + ... while True: + ... print('Thing number %s is %s.' % (i, (yield))) + ... i += 1 + ... + >>> t = tally() + >>> t.send('red') + Thing number 0 is red. + >>> t.send('fish') + Thing number 1 is fish. + + Without the decorator, you would have to call ``next(t)`` before + ``t.send()`` could be used. + + """ + + @wraps(func) + def wrapper(*args, **kwargs): + gen = func(*args, **kwargs) + next(gen) + return gen + + return wrapper + + +def ilen(iterable): + """Return the number of items in *iterable*. + + For example, there are 168 prime numbers below 1,000: + + >>> ilen(sieve(1000)) + 168 + + Equivalent to, but faster than:: + + def ilen(iterable): + count = 0 + for _ in iterable: + count += 1 + return count + + This fully consumes the iterable, so handle with care. + + """ + # This is the "most beautiful of the fast variants" of this function. + # If you think you can improve on it, please ensure that your version + # is both 10x faster and 10x more beautiful. + return sum(compress(repeat(1), zip(iterable))) + + +def iterate(func, start): + """Return ``start``, ``func(start)``, ``func(func(start))``, ... + + Produces an infinite iterator. To add a stopping condition, + use :func:`take`, ``takewhile``, or :func:`takewhile_inclusive`:. + + >>> take(10, iterate(lambda x: 2*x, 1)) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + + >>> collatz = lambda x: 3*x + 1 if x%2==1 else x // 2 + >>> list(takewhile_inclusive(lambda x: x!=1, iterate(collatz, 10))) + [10, 5, 16, 8, 4, 2, 1] + + """ + with suppress(StopIteration): + while True: + yield start + start = func(start) + + +def with_iter(context_manager): + """Wrap an iterable in a ``with`` statement, so it closes once exhausted. + + For example, this will close the file when the iterator is exhausted:: + + upper_lines = (line.upper() for line in with_iter(open('foo'))) + + Any context manager which returns an iterable is a candidate for + ``with_iter``. + + """ + with context_manager as iterable: + yield from iterable + + +def one(iterable, too_short=None, too_long=None): + """Return the first item from *iterable*, which is expected to contain only + that item. Raise an exception if *iterable* is empty or has more than one + item. + + :func:`one` is useful for ensuring that an iterable contains only one item. + For example, it can be used to retrieve the result of a database query + that is expected to return a single row. + + If *iterable* is empty, ``ValueError`` will be raised. You may specify a + different exception with the *too_short* keyword: + + >>> it = [] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too few items in iterable (expected 1)' + >>> too_short = IndexError('too few items') + >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + IndexError: too few items + + Similarly, if *iterable* contains more than one item, ``ValueError`` will + be raised. You may specify a different exception with the *too_long* + keyword: + + >>> it = ['too', 'many'] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 'too', + 'many', and perhaps more. + >>> too_long = RuntimeError + >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + Note that :func:`one` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check iterable + contents less destructively. + + """ + iterator = iter(iterable) + for first in iterator: + for second in iterator: + msg = ( + f'Expected exactly one item in iterable, but got {first!r}, ' + f'{second!r}, and perhaps more.' + ) + raise too_long or ValueError(msg) + return first + raise too_short or ValueError('too few items in iterable (expected 1)') + + +def raise_(exception, *args): + raise exception(*args) + + +def strictly_n(iterable, n, too_short=None, too_long=None): + """Validate that *iterable* has exactly *n* items and return them if + it does. If it has fewer than *n* items, call function *too_short* + with the actual number of items. If it has more than *n* items, call function + *too_long* with the number ``n + 1``. + + >>> iterable = ['a', 'b', 'c', 'd'] + >>> n = 4 + >>> list(strictly_n(iterable, n)) + ['a', 'b', 'c', 'd'] + + Note that the returned iterable must be consumed in order for the check to + be made. + + By default, *too_short* and *too_long* are functions that raise + ``ValueError``. + + >>> list(strictly_n('ab', 3)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too few items in iterable (got 2) + + >>> list(strictly_n('abc', 2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (got at least 3) + + You can instead supply functions that do something else. + *too_short* will be called with the number of items in *iterable*. + *too_long* will be called with `n + 1`. + + >>> def too_short(item_count): + ... raise RuntimeError + >>> it = strictly_n('abcd', 6, too_short=too_short) + >>> list(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + >>> def too_long(item_count): + ... print('The boss is going to hear about this') + >>> it = strictly_n('abcdef', 4, too_long=too_long) + >>> list(it) + The boss is going to hear about this + ['a', 'b', 'c', 'd'] + + """ + if too_short is None: + too_short = lambda item_count: raise_( + ValueError, + f'Too few items in iterable (got {item_count})', + ) + + if too_long is None: + too_long = lambda item_count: raise_( + ValueError, + f'Too many items in iterable (got at least {item_count})', + ) + + it = iter(iterable) + + sent = 0 + for item in islice(it, n): + yield item + sent += 1 + + if sent < n: + too_short(sent) + return + + for item in it: + too_long(n + 1) + return + + +def distinct_permutations(iterable, r=None): + """Yield successive distinct permutations of the elements in *iterable*. + + >>> sorted(distinct_permutations([1, 0, 1])) + [(0, 1, 1), (1, 0, 1), (1, 1, 0)] + + Equivalent to yielding from ``set(permutations(iterable))``, except + duplicates are not generated and thrown away. For larger input sequences + this is much more efficient. + + Duplicate permutations arise when there are duplicated elements in the + input iterable. The number of items returned is + `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of + items input, and each `x_i` is the count of a distinct item in the input + sequence. The function :func:`multinomial` computes this directly. + + If *r* is given, only the *r*-length permutations are yielded. + + >>> sorted(distinct_permutations([1, 0, 1], r=2)) + [(0, 1), (1, 0), (1, 1)] + >>> sorted(distinct_permutations(range(3), r=2)) + [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + + *iterable* need not be sortable, but note that using equal (``x == y``) + but non-identical (``id(x) != id(y)``) elements may produce surprising + behavior. For example, ``1`` and ``True`` are equal but non-identical: + + >>> list(distinct_permutations([1, True, '3'])) # doctest: +SKIP + [ + (1, True, '3'), + (1, '3', True), + ('3', 1, True) + ] + >>> list(distinct_permutations([1, 2, '3'])) # doctest: +SKIP + [ + (1, 2, '3'), + (1, '3', 2), + (2, 1, '3'), + (2, '3', 1), + ('3', 1, 2), + ('3', 2, 1) + ] + """ + + # Algorithm: https://w.wiki/Qai + def _full(A): + while True: + # Yield the permutation we have + yield tuple(A) + + # Find the largest index i such that A[i] < A[i + 1] + for i in range(size - 2, -1, -1): + if A[i] < A[i + 1]: + break + # If no such index exists, this permutation is the last one + else: + return + + # Find the largest index j greater than j such that A[i] < A[j] + for j in range(size - 1, i, -1): + if A[i] < A[j]: + break + + # Swap the value of A[i] with that of A[j], then reverse the + # sequence from A[i + 1] to form the new permutation + A[i], A[j] = A[j], A[i] + A[i + 1 :] = A[: i - size : -1] # A[i + 1:][::-1] + + # Algorithm: modified from the above + def _partial(A, r): + # Split A into the first r items and the last r items + head, tail = A[:r], A[r:] + right_head_indexes = range(r - 1, -1, -1) + left_tail_indexes = range(len(tail)) + + while True: + # Yield the permutation we have + yield tuple(head) + + # Starting from the right, find the first index of the head with + # value smaller than the maximum value of the tail - call it i. + pivot = tail[-1] + for i in right_head_indexes: + if head[i] < pivot: + break + pivot = head[i] + else: + return + + # Starting from the left, find the first value of the tail + # with a value greater than head[i] and swap. + for j in left_tail_indexes: + if tail[j] > head[i]: + head[i], tail[j] = tail[j], head[i] + break + # If we didn't find one, start from the right and find the first + # index of the head with a value greater than head[i] and swap. + else: + for j in right_head_indexes: + if head[j] > head[i]: + head[i], head[j] = head[j], head[i] + break + + # Reverse head[i + 1:] and swap it with tail[:r - (i + 1)] + tail += head[: i - r : -1] # head[i + 1:][::-1] + i += 1 + head[i:], tail[:] = tail[: r - i], tail[r - i :] + + items = list(iterable) + + try: + items.sort() + sortable = True + except TypeError: + sortable = False + + indices_dict = defaultdict(list) + + for item in items: + indices_dict[items.index(item)].append(item) + + indices = [items.index(item) for item in items] + indices.sort() + + equivalent_items = {k: cycle(v) for k, v in indices_dict.items()} + + def permuted_items(permuted_indices): + return tuple( + next(equivalent_items[index]) for index in permuted_indices + ) + + size = len(items) + if r is None: + r = size + + # functools.partial(_partial, ... ) + algorithm = _full if (r == size) else partial(_partial, r=r) + + if 0 < r <= size: + if sortable: + return algorithm(items) + else: + return ( + permuted_items(permuted_indices) + for permuted_indices in algorithm(indices) + ) + + return iter(() if r else ((),)) + + +def derangements(iterable, r=None): + """Yield successive derangements of the elements in *iterable*. + + A derangement is a permutation in which no element appears at its original + index. In other words, a derangement is a permutation that has no fixed points. + + Suppose Alice, Bob, Carol, and Dave are playing Secret Santa. + The code below outputs all of the different ways to assign gift recipients + such that nobody is assigned to himself or herself: + + >>> for d in derangements(['Alice', 'Bob', 'Carol', 'Dave']): + ... print(', '.join(d)) + Bob, Alice, Dave, Carol + Bob, Carol, Dave, Alice + Bob, Dave, Alice, Carol + Carol, Alice, Dave, Bob + Carol, Dave, Alice, Bob + Carol, Dave, Bob, Alice + Dave, Alice, Bob, Carol + Dave, Carol, Alice, Bob + Dave, Carol, Bob, Alice + + If *r* is given, only the *r*-length derangements are yielded. + + >>> sorted(derangements(range(3), 2)) + [(1, 0), (1, 2), (2, 0)] + >>> sorted(derangements([0, 2, 3], 2)) + [(2, 0), (2, 3), (3, 0)] + + Elements are treated as unique based on their position, not on their value. + + Consider the Secret Santa example with two *different* people who have + the *same* name. Then there are two valid gift assignments even though + it might appear that a person is assigned to themselves: + + >>> names = ['Alice', 'Bob', 'Bob'] + >>> list(derangements(names)) + [('Bob', 'Bob', 'Alice'), ('Bob', 'Alice', 'Bob')] + + To avoid confusion, make the inputs distinct: + + >>> deduped = [f'{name}{index}' for index, name in enumerate(names)] + >>> list(derangements(deduped)) + [('Bob1', 'Bob2', 'Alice0'), ('Bob2', 'Alice0', 'Bob1')] + + The number of derangements of a set of size *n* is known as the + "subfactorial of n". For n > 0, the subfactorial is: + ``round(math.factorial(n) / math.e)``. + + References: + + * Article: https://www.numberanalytics.com/blog/ultimate-guide-to-derangements-in-combinatorics + * Sizes: https://oeis.org/A000166 + """ + xs = tuple(iterable) + ys = tuple(range(len(xs))) + return compress( + permutations(xs, r=r), + map(all, map(map, repeat(is_not), repeat(ys), permutations(ys, r=r))), + ) + + +def intersperse(e, iterable, n=1): + """Intersperse filler element *e* among the items in *iterable*, leaving + *n* items between each filler element. + + >>> list(intersperse('!', [1, 2, 3, 4, 5])) + [1, '!', 2, '!', 3, '!', 4, '!', 5] + + >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) + [1, 2, None, 3, 4, None, 5] + + """ + if n == 0: + raise ValueError('n must be > 0') + elif n == 1: + # interleave(repeat(e), iterable) -> e, x_0, e, x_1, e, x_2... + # islice(..., 1, None) -> x_0, e, x_1, e, x_2... + return islice(interleave(repeat(e), iterable), 1, None) + else: + # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... + # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... + # flatten(...) -> x_0, x_1, e, x_2, x_3... + filler = repeat([e]) + chunks = chunked(iterable, n) + return flatten(islice(interleave(filler, chunks), 1, None)) + + +def unique_to_each(*iterables): + """Return the elements from each of the input iterables that aren't in the + other input iterables. + + For example, suppose you have a set of packages, each with a set of + dependencies:: + + {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} + + If you remove one package, which dependencies can also be removed? + + If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not + associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for + ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: + + >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) + [['A'], ['C'], ['D']] + + If there are duplicates in one input iterable that aren't in the others + they will be duplicated in the output. Input order is preserved:: + + >>> unique_to_each("mississippi", "missouri") + [['p', 'p'], ['o', 'u', 'r']] + + It is assumed that the elements of each iterable are hashable. + + """ + pool = [list(it) for it in iterables] + counts = Counter(chain.from_iterable(map(set, pool))) + uniques = {element for element in counts if counts[element] == 1} + return [list(filter(uniques.__contains__, it)) for it in pool] + + +def windowed(seq, n, fillvalue=None, step=1): + """Return a sliding window of width *n* over the given iterable. + + >>> all_windows = windowed([1, 2, 3, 4, 5], 3) + >>> list(all_windows) + [(1, 2, 3), (2, 3, 4), (3, 4, 5)] + + When the window is larger than the iterable, *fillvalue* is used in place + of missing values: + + >>> list(windowed([1, 2, 3], 4)) + [(1, 2, 3, None)] + + Each window will advance in increments of *step*: + + >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) + [(1, 2, 3), (3, 4, 5), (5, 6, '!')] + + To slide into the iterable's items, use :func:`chain` to add filler items + to the left: + + >>> iterable = [1, 2, 3, 4] + >>> n = 3 + >>> padding = [None] * (n - 1) + >>> list(windowed(chain(padding, iterable), 3)) + [(None, None, 1), (None, 1, 2), (1, 2, 3), (2, 3, 4)] + """ + if n < 0: + raise ValueError('n must be >= 0') + if n == 0: + yield () + return + if step < 1: + raise ValueError('step must be >= 1') + + iterator = iter(seq) + + # Generate first window + window = deque(islice(iterator, n), maxlen=n) + + # Deal with the first window not being full + if not window: + return + if len(window) < n: + yield tuple(window) + ((fillvalue,) * (n - len(window))) + return + yield tuple(window) + + # Create the filler for the next windows. The padding ensures + # we have just enough elements to fill the last window. + padding = (fillvalue,) * (n - 1 if step >= n else step - 1) + filler = map(window.append, chain(iterator, padding)) + + # Generate the rest of the windows + for _ in islice(filler, step - 1, None, step): + yield tuple(window) + + +def substrings(iterable): + """Yield all of the substrings of *iterable*. + + >>> [''.join(s) for s in substrings('more')] + ['m', 'o', 'r', 'e', 'mo', 'or', 're', 'mor', 'ore', 'more'] + + Note that non-string iterables can also be subdivided. + + >>> list(substrings([0, 1, 2])) + [(0,), (1,), (2,), (0, 1), (1, 2), (0, 1, 2)] + + """ + # The length-1 substrings + seq = [] + for item in iterable: + seq.append(item) + yield (item,) + seq = tuple(seq) + item_count = len(seq) + + # And the rest + for n in range(2, item_count + 1): + for i in range(item_count - n + 1): + yield seq[i : i + n] + + +def substrings_indexes(seq, reverse=False): + """Yield all substrings and their positions in *seq* + + The items yielded will be a tuple of the form ``(substr, i, j)``, where + ``substr == seq[i:j]``. + + This function only works for iterables that support slicing, such as + ``str`` objects. + + >>> for item in substrings_indexes('more'): + ... print(item) + ('m', 0, 1) + ('o', 1, 2) + ('r', 2, 3) + ('e', 3, 4) + ('mo', 0, 2) + ('or', 1, 3) + ('re', 2, 4) + ('mor', 0, 3) + ('ore', 1, 4) + ('more', 0, 4) + + Set *reverse* to ``True`` to yield the same items in the opposite order. + + + """ + r = range(1, len(seq) + 1) + if reverse: + r = reversed(r) + return ( + (seq[i : i + L], i, i + L) for L in r for i in range(len(seq) - L + 1) + ) + + +class bucket: + """Wrap *iterable* and return an object that buckets the iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character + >>> sorted(list(s)) # Get the keys + ['a', 'b', 'c'] + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __iter__(self): + for item in self._it: + item_value = self._key(item) + if self._validator(item_value): + self._cache[item_value].append(item) + + return iter(self._cache) + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) + + +def spy(iterable, n=1): + """Return a 2-tuple with a list containing the first *n* elements of + *iterable*, and an iterator with the same items as *iterable*. + This allows you to "look ahead" at the items in the iterable without + advancing it. + + There is one item in the list by default: + + >>> iterable = 'abcdefg' + >>> head, iterable = spy(iterable) + >>> head + ['a'] + >>> list(iterable) + ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + + You may use unpacking to retrieve items instead of lists: + + >>> (head,), iterable = spy('abcdefg') + >>> head + 'a' + >>> (first, second), iterable = spy('abcdefg', 2) + >>> first + 'a' + >>> second + 'b' + + The number of items requested can be larger than the number of items in + the iterable: + + >>> iterable = [1, 2, 3, 4, 5] + >>> head, iterable = spy(iterable, 10) + >>> head + [1, 2, 3, 4, 5] + >>> list(iterable) + [1, 2, 3, 4, 5] + + """ + p, q = tee(iterable) + return take(n, q), p + + +def interleave(*iterables): + """Return a new iterable yielding from each iterable in turn, + until the shortest is exhausted. + + >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7] + + For a version that doesn't terminate after the shortest iterable is + exhausted, see :func:`interleave_longest`. + + """ + return chain.from_iterable(zip(*iterables)) + + +def interleave_longest(*iterables): + """Return a new iterable yielding from each iterable in turn, + skipping any that are exhausted. + + >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7, 3, 8] + + This function produces the same output as :func:`roundrobin`, but may + perform better for some inputs (in particular when the number of iterables + is large). + + """ + for xs in zip_longest(*iterables, fillvalue=_marker): + for x in xs: + if x is not _marker: + yield x + + +def interleave_evenly(iterables, lengths=None): + """ + Interleave multiple iterables so that their elements are evenly distributed + throughout the output sequence. + + >>> iterables = [1, 2, 3, 4, 5], ['a', 'b'] + >>> list(interleave_evenly(iterables)) + [1, 2, 'a', 3, 4, 'b', 5] + + >>> iterables = [[1, 2, 3], [4, 5], [6, 7, 8]] + >>> list(interleave_evenly(iterables)) + [1, 6, 4, 2, 7, 3, 8, 5] + + This function requires iterables of known length. Iterables without + ``__len__()`` can be used by manually specifying lengths with *lengths*: + + >>> from itertools import combinations, repeat + >>> iterables = [combinations(range(4), 2), ['a', 'b', 'c']] + >>> lengths = [4 * (4 - 1) // 2, 3] + >>> list(interleave_evenly(iterables, lengths=lengths)) + [(0, 1), (0, 2), 'a', (0, 3), (1, 2), 'b', (1, 3), (2, 3), 'c'] + + Based on Bresenham's algorithm. + """ + if lengths is None: + try: + lengths = [len(it) for it in iterables] + except TypeError: + raise ValueError( + 'Iterable lengths could not be determined automatically. ' + 'Specify them with the lengths keyword.' + ) + elif len(iterables) != len(lengths): + raise ValueError('Mismatching number of iterables and lengths.') + + dims = len(lengths) + + # sort iterables by length, descending + lengths_permute = sorted( + range(dims), key=lambda i: lengths[i], reverse=True + ) + lengths_desc = [lengths[i] for i in lengths_permute] + iters_desc = [iter(iterables[i]) for i in lengths_permute] + + # the longest iterable is the primary one (Bresenham: the longest + # distance along an axis) + delta_primary, deltas_secondary = lengths_desc[0], lengths_desc[1:] + iter_primary, iters_secondary = iters_desc[0], iters_desc[1:] + errors = [delta_primary // dims] * len(deltas_secondary) + + to_yield = sum(lengths) + while to_yield: + yield next(iter_primary) + to_yield -= 1 + # update errors for each secondary iterable + errors = [e - delta for e, delta in zip(errors, deltas_secondary)] + + # those iterables for which the error is negative are yielded + # ("diagonal step" in Bresenham) + for i, e_ in enumerate(errors): + if e_ < 0: + yield next(iters_secondary[i]) + to_yield -= 1 + errors[i] += delta_primary + + +def interleave_randomly(*iterables): + """Repeatedly select one of the input *iterables* at random and yield the next + item from it. + + >>> iterables = [1, 2, 3], 'abc', (True, False, None) + >>> list(interleave_randomly(*iterables)) # doctest: +SKIP + ['a', 'b', 1, 'c', True, False, None, 2, 3] + + The relative order of the items in each input iterable will preserved. Note the + sequences of items with this property are not equally likely to be generated. + + """ + iterators = [iter(e) for e in iterables] + while iterators: + idx = randrange(len(iterators)) + try: + yield next(iterators[idx]) + except StopIteration: + # equivalent to `list.pop` but slightly faster + iterators[idx] = iterators[-1] + del iterators[-1] + + +def collapse(iterable, base_type=None, levels=None): + """Flatten an iterable with multiple levels of nesting (e.g., a list of + lists of tuples) into non-iterable types. + + >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] + >>> list(collapse(iterable)) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and + will not be collapsed. + + To avoid collapsing other types, specify *base_type*: + + >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] + >>> list(collapse(iterable, base_type=tuple)) + ['ab', ('cd', 'ef'), 'gh', 'ij'] + + Specify *levels* to stop flattening after a certain level: + + >>> iterable = [('a', ['b']), ('c', ['d'])] + >>> list(collapse(iterable)) # Fully flattened + ['a', 'b', 'c', 'd'] + >>> list(collapse(iterable, levels=1)) # Only one level flattened + ['a', ['b'], 'c', ['d']] + + """ + stack = deque() + # Add our first node group, treat the iterable as a single node + stack.appendleft((0, repeat(iterable, 1))) + + while stack: + node_group = stack.popleft() + level, nodes = node_group + + # Check if beyond max level + if levels is not None and level > levels: + yield from nodes + continue + + for node in nodes: + # Check if done iterating + if isinstance(node, (str, bytes)) or ( + (base_type is not None) and isinstance(node, base_type) + ): + yield node + # Otherwise try to create child nodes + else: + try: + tree = iter(node) + except TypeError: + yield node + else: + # Save our current location + stack.appendleft(node_group) + # Append the new child node + stack.appendleft((level + 1, tree)) + # Break to process child node + break + + +def side_effect(func, iterable, chunk_size=None, before=None, after=None): + """Invoke *func* on each item in *iterable* (or on each *chunk_size* group + of items) before yielding the item. + + `func` must be a function that takes a single argument. Its return value + will be discarded. + + *before* and *after* are optional functions that take no arguments. They + will be executed before iteration starts and after it ends, respectively. + + `side_effect` can be used for logging, updating progress bars, or anything + that is not functionally "pure." + + Emitting a status message: + + >>> from more_itertools import consume + >>> func = lambda item: print('Received {}'.format(item)) + >>> consume(side_effect(func, range(2))) + Received 0 + Received 1 + + Operating on chunks of items: + + >>> pair_sums = [] + >>> func = lambda chunk: pair_sums.append(sum(chunk)) + >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) + [0, 1, 2, 3, 4, 5] + >>> list(pair_sums) + [1, 5, 9] + + Writing to a file-like object: + + >>> from io import StringIO + >>> from more_itertools import consume + >>> f = StringIO() + >>> func = lambda x: print(x, file=f) + >>> before = lambda: print(u'HEADER', file=f) + >>> after = f.close + >>> it = [u'a', u'b', u'c'] + >>> consume(side_effect(func, it, before=before, after=after)) + >>> f.closed + True + + """ + try: + if before is not None: + before() + + if chunk_size is None: + for item in iterable: + func(item) + yield item + else: + for chunk in chunked(iterable, chunk_size): + func(chunk) + yield from chunk + finally: + if after is not None: + after() + + +def sliced(seq, n, strict=False): + """Yield slices of length *n* from the sequence *seq*. + + >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) + [(1, 2, 3), (4, 5, 6)] + + By the default, the last yielded slice will have fewer than *n* elements + if the length of *seq* is not divisible by *n*: + + >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) + [(1, 2, 3), (4, 5, 6), (7, 8)] + + If the length of *seq* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + slice is yielded. + + This function will only work for iterables that support slicing. + For non-sliceable iterables, see :func:`chunked`. + + """ + iterator = takewhile(len, (seq[i : i + n] for i in count(0, n))) + if strict: + + def ret(): + for _slice in iterator: + if len(_slice) != n: + raise ValueError("seq is not divisible by n.") + yield _slice + + return ret() + else: + return iterator + + +def split_at(iterable, pred, maxsplit=-1, keep_separator=False): + """Yield lists of items from *iterable*, where each list is delimited by + an item where callable *pred* returns ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b')) + [['a'], ['c', 'd', 'c'], ['a']] + + >>> list(split_at(range(10), lambda n: n % 2 == 1)) + [[0], [2], [4], [6], [8], []] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_at(range(10), lambda n: n % 2 == 1, maxsplit=2)) + [[0], [2], [4, 5, 6, 7, 8, 9]] + + By default, the delimiting items are not included in the output. + To include them, set *keep_separator* to ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b', keep_separator=True)) + [['a'], ['b'], ['c', 'd', 'c'], ['b'], ['a']] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item): + yield buf + if keep_separator: + yield [item] + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + else: + buf.append(item) + yield buf + + +def split_before(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends just before + an item for which callable *pred* returns ``True``: + + >>> list(split_before('OneTwo', lambda s: s.isupper())) + [['O', 'n', 'e'], ['T', 'w', 'o']] + + >>> list(split_before(range(10), lambda n: n % 3 == 0)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_before(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8, 9]] + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield [item, *it] + return + buf = [] + maxsplit -= 1 + buf.append(item) + if buf: + yield buf + + +def split_after(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends with an + item where callable *pred* returns ``True``: + + >>> list(split_after('one1two2', lambda s: s.isdigit())) + [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] + + >>> list(split_after(range(10), lambda n: n % 3 == 0)) + [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_after(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0], [1, 2, 3], [4, 5, 6, 7, 8, 9]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + buf.append(item) + if pred(item) and buf: + yield buf + if maxsplit == 1: + buf = list(it) + if buf: + yield buf + return + buf = [] + maxsplit -= 1 + if buf: + yield buf + + +def split_when(iterable, pred, maxsplit=-1): + """Split *iterable* into pieces based on the output of *pred*. + *pred* should be a function that takes successive pairs of items and + returns ``True`` if the iterable should be split in between them. + + For example, to find runs of increasing numbers, split the iterable when + element ``i`` is larger than element ``i + 1``: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], lambda x, y: x > y)) + [[1, 2, 3, 3], [2, 5], [2, 4], [2]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], + ... lambda x, y: x > y, maxsplit=2)) + [[1, 2, 3, 3], [2, 5], [2, 4, 2]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + it = iter(iterable) + try: + cur_item = next(it) + except StopIteration: + return + + buf = [cur_item] + for next_item in it: + if pred(cur_item, next_item): + yield buf + if maxsplit == 1: + yield [next_item, *it] + return + buf = [] + maxsplit -= 1 + + buf.append(next_item) + cur_item = next_item + + yield buf + + +def split_into(iterable, sizes): + """Yield a list of sequential items from *iterable* of length 'n' for each + integer 'n' in *sizes*. + + >>> list(split_into([1,2,3,4,5,6], [1,2,3])) + [[1], [2, 3], [4, 5, 6]] + + If the sum of *sizes* is smaller than the length of *iterable*, then the + remaining items of *iterable* will not be returned. + + >>> list(split_into([1,2,3,4,5,6], [2,3])) + [[1, 2], [3, 4, 5]] + + If the sum of *sizes* is larger than the length of *iterable*, fewer items + will be returned in the iteration that overruns the *iterable* and further + lists will be empty: + + >>> list(split_into([1,2,3,4], [1,2,3,4])) + [[1], [2, 3], [4], []] + + When a ``None`` object is encountered in *sizes*, the returned list will + contain items up to the end of *iterable* the same way that + :func:`itertools.slice` does: + + >>> list(split_into([1,2,3,4,5,6,7,8,9,0], [2,3,None])) + [[1, 2], [3, 4, 5], [6, 7, 8, 9, 0]] + + :func:`split_into` can be useful for grouping a series of items where the + sizes of the groups are not uniform. An example would be where in a row + from a table, multiple columns represent elements of the same feature + (e.g. a point represented by x,y,z) but, the format is not the same for + all columns. + """ + # convert the iterable argument into an iterator so its contents can + # be consumed by islice in case it is a generator + it = iter(iterable) + + for size in sizes: + if size is None: + yield list(it) + return + else: + yield list(islice(it, size)) + + +def padded(iterable, fillvalue=None, n=None, next_multiple=False): + """Yield the elements from *iterable*, followed by *fillvalue*, such that + at least *n* items are emitted. + + >>> list(padded([1, 2, 3], '?', 5)) + [1, 2, 3, '?', '?'] + + If *next_multiple* is ``True``, *fillvalue* will be emitted until the + number of items emitted is a multiple of *n*: + + >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) + [1, 2, 3, 4, None, None] + + If *n* is ``None``, *fillvalue* will be emitted indefinitely. + + To create an *iterable* of exactly size *n*, you can truncate with + :func:`islice`. + + >>> list(islice(padded([1, 2, 3], '?'), 5)) + [1, 2, 3, '?', '?'] + >>> list(islice(padded([1, 2, 3, 4, 5, 6, 7, 8], '?'), 5)) + [1, 2, 3, 4, 5] + + """ + iterator = iter(iterable) + iterator_with_repeat = chain(iterator, repeat(fillvalue)) + + if n is None: + return iterator_with_repeat + elif n < 1: + raise ValueError('n must be at least 1') + elif next_multiple: + + def slice_generator(): + for first in iterator: + yield (first,) + yield islice(iterator_with_repeat, n - 1) + + # While elements exist produce slices of size n + return chain.from_iterable(slice_generator()) + else: + # Ensure the first batch is at least size n then iterate + return chain(islice(iterator_with_repeat, n), iterator) + + +def repeat_each(iterable, n=2): + """Repeat each element in *iterable* *n* times. + + >>> list(repeat_each('ABC', 3)) + ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'] + """ + return chain.from_iterable(map(repeat, iterable, repeat(n))) + + +def repeat_last(iterable, default=None): + """After the *iterable* is exhausted, keep yielding its last element. + + >>> list(islice(repeat_last(range(3)), 5)) + [0, 1, 2, 2, 2] + + If the iterable is empty, yield *default* forever:: + + >>> list(islice(repeat_last(range(0), 42), 5)) + [42, 42, 42, 42, 42] + + """ + item = _marker + for item in iterable: + yield item + final = default if item is _marker else item + yield from repeat(final) + + +def distribute(n, iterable): + """Distribute the items from *iterable* among *n* smaller iterables. + + >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 3, 5] + >>> list(group_2) + [2, 4, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 4, 7], [2, 5], [3, 6]] + + If the length of *iterable* is smaller than *n*, then the last returned + iterables will be empty: + + >>> children = distribute(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function uses :func:`itertools.tee` and may require significant + storage. + + If you need the order items in the smaller iterables to match the + original iterable, see :func:`divide`. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + children = tee(iterable, n) + return [islice(it, index, None, n) for index, it in enumerate(children)] + + +def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): + """Yield tuples whose elements are offset from *iterable*. + The amount by which the `i`-th item in each tuple is offset is given by + the `i`-th item in *offsets*. + + >>> list(stagger([0, 1, 2, 3])) + [(None, 0, 1), (0, 1, 2), (1, 2, 3)] + >>> list(stagger(range(8), offsets=(0, 2, 4))) + [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] + + By default, the sequence will end when the final element of a tuple is the + last item in the iterable. To continue until the first element of a tuple + is the last item in the iterable, set *longest* to ``True``:: + + >>> list(stagger([0, 1, 2, 3], longest=True)) + [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + children = tee(iterable, len(offsets)) + + return zip_offset( + *children, offsets=offsets, longest=longest, fillvalue=fillvalue + ) + + +def zip_equal(*iterables): + """``zip`` the input *iterables* together but raise + ``UnequalIterablesError`` if they aren't all the same length. + + >>> it_1 = range(3) + >>> it_2 = iter('abc') + >>> list(zip_equal(it_1, it_2)) + [(0, 'a'), (1, 'b'), (2, 'c')] + + >>> it_1 = range(3) + >>> it_2 = iter('abcd') + >>> list(zip_equal(it_1, it_2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + more_itertools.more.UnequalIterablesError: Iterables have different + lengths + + """ + if hexversion >= 0x30A00A6: + warnings.warn( + ( + 'zip_equal will be removed in a future version of ' + 'more-itertools. Use the builtin zip function with ' + 'strict=True instead.' + ), + DeprecationWarning, + ) + + return _zip_equal(*iterables) + + +def zip_offset(*iterables, offsets, longest=False, fillvalue=None): + """``zip`` the input *iterables* together, but offset the `i`-th iterable + by the `i`-th item in *offsets*. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] + + This can be used as a lightweight alternative to SciPy or pandas to analyze + data sets in which some series have a lead or lag relationship. + + By default, the sequence will end when the shortest iterable is exhausted. + To continue until the longest iterable is exhausted, set *longest* to + ``True``. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + if len(iterables) != len(offsets): + raise ValueError("Number of iterables and offsets didn't match") + + staggered = [] + for it, n in zip(iterables, offsets): + if n < 0: + staggered.append(chain(repeat(fillvalue, -n), it)) + elif n > 0: + staggered.append(islice(it, n, None)) + else: + staggered.append(it) + + if longest: + return zip_longest(*staggered, fillvalue=fillvalue) + + return zip(*staggered) + + +def sort_together( + iterables, key_list=(0,), key=None, reverse=False, strict=False +): + """Return the input iterables sorted together, with *key_list* as the + priority for sorting. All iterables are trimmed to the length of the + shortest one. + + This can be used like the sorting function in a spreadsheet. If each + iterable represents a column of data, the key list determines which + columns are used for sorting. + + By default, all iterables are sorted using the ``0``-th iterable:: + + >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] + >>> sort_together(iterables) + [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] + + Set a different key list to sort according to another iterable. + Specifying multiple keys dictates how ties are broken:: + + >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] + >>> sort_together(iterables, key_list=(1, 2)) + [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] + + To sort by a function of the elements of the iterable, pass a *key* + function. Its arguments are the elements of the iterables corresponding to + the key list:: + + >>> names = ('a', 'b', 'c') + >>> lengths = (1, 2, 3) + >>> widths = (5, 2, 1) + >>> def area(length, width): + ... return length * width + >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) + [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] + + Set *reverse* to ``True`` to sort in descending order. + + >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) + [(3, 2, 1), ('a', 'b', 'c')] + + If the *strict* keyword argument is ``True``, then + ``UnequalIterablesError`` will be raised if any of the iterables have + different lengths. + + """ + if key is None: + # if there is no key function, the key argument to sorted is an + # itemgetter + key_argument = itemgetter(*key_list) + else: + # if there is a key function, call it with the items at the offsets + # specified by the key function as arguments + key_list = list(key_list) + if len(key_list) == 1: + # if key_list contains a single item, pass the item at that offset + # as the only argument to the key function + key_offset = key_list[0] + key_argument = lambda zipped_items: key(zipped_items[key_offset]) + else: + # if key_list contains multiple items, use itemgetter to return a + # tuple of items, which we pass as *args to the key function + get_key_items = itemgetter(*key_list) + key_argument = lambda zipped_items: key( + *get_key_items(zipped_items) + ) + + zipper = zip_equal if strict else zip + return list( + zipper(*sorted(zipper(*iterables), key=key_argument, reverse=reverse)) + ) + + +def unzip(iterable): + """The inverse of :func:`zip`, this function disaggregates the elements + of the zipped *iterable*. + + The ``i``-th iterable contains the ``i``-th element from each element + of the zipped iterable. The first element is used to determine the + length of the remaining elements. + + >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> letters, numbers = unzip(iterable) + >>> list(letters) + ['a', 'b', 'c', 'd'] + >>> list(numbers) + [1, 2, 3, 4] + + This is similar to using ``zip(*iterable)``, but it avoids reading + *iterable* into memory. Note, however, that this function uses + :func:`itertools.tee` and thus may require significant storage. + + """ + head, iterable = spy(iterable) + if not head: + # empty iterable, e.g. zip([], [], []) + return () + # spy returns a one-length iterable as head + head = head[0] + iterables = tee(iterable, len(head)) + + # If we have an iterable like iter([(1, 2, 3), (4, 5), (6,)]), + # the second unzipped iterable fails at the third tuple since + # it tries to access (6,)[1]. + # Same with the third unzipped iterable and the second tuple. + # To support these "improperly zipped" iterables, we suppress + # the IndexError, which just stops the unzipped iterables at + # first length mismatch. + return tuple( + iter_suppress(map(itemgetter(i), it), IndexError) + for i, it in enumerate(iterables) + ) + + +def divide(n, iterable): + """Divide the elements from *iterable* into *n* parts, maintaining + order. + + >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 2, 3] + >>> list(group_2) + [4, 5, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 2, 3], [4, 5], [6, 7]] + + If the length of the iterable is smaller than n, then the last returned + iterables will be empty: + + >>> children = divide(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function will exhaust the iterable before returning. + If order is not important, see :func:`distribute`, which does not first + pull the iterable into memory. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + try: + iterable[:0] + except TypeError: + seq = tuple(iterable) + else: + seq = iterable + + q, r = divmod(len(seq), n) + + ret = [] + stop = 0 + for i in range(1, n + 1): + start = stop + stop += q + 1 if i <= r else q + ret.append(iter(seq[start:stop])) + + return ret + + +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) + + +def adjacent(predicate, iterable, distance=1): + """Return an iterable over `(bool, item)` tuples where the `item` is + drawn from *iterable* and the `bool` indicates whether + that item satisfies the *predicate* or is adjacent to an item that does. + + For example, to find whether items are adjacent to a ``3``:: + + >>> list(adjacent(lambda x: x == 3, range(6))) + [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] + + Set *distance* to change what counts as adjacent. For example, to find + whether items are two places away from a ``3``: + + >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) + [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] + + This is useful for contextualizing the results of a search function. + For example, a code comparison tool might want to identify lines that + have changed, but also surrounding lines to give the viewer of the diff + context. + + The predicate function will only be called once for each item in the + iterable. + + See also :func:`groupby_transform`, which can be used with this function + to group ranges of items with the same `bool` value. + + """ + # Allow distance=0 mainly for testing that it reproduces results with map() + if distance < 0: + raise ValueError('distance must be at least 0') + + i1, i2 = tee(iterable) + padding = [False] * distance + selected = chain(padding, map(predicate, i1), padding) + adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) + return zip(adjacent_to_selected, i2) + + +def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): + """An extension of :func:`itertools.groupby` that can apply transformations + to the grouped data. + + * *keyfunc* is a function computing a key value for each item in *iterable* + * *valuefunc* is a function that transforms the individual items from + *iterable* after grouping + * *reducefunc* is a function that transforms each group of items + + >>> iterable = 'aAAbBBcCC' + >>> keyfunc = lambda k: k.upper() + >>> valuefunc = lambda v: v.lower() + >>> reducefunc = lambda g: ''.join(g) + >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) + [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] + + Each optional argument defaults to an identity function if not specified. + + :func:`groupby_transform` is useful when grouping elements of an iterable + using a separate iterable as the key. To do this, :func:`zip` the iterables + and pass a *keyfunc* that extracts the first element and a *valuefunc* + that extracts the second element:: + + >>> from operator import itemgetter + >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] + >>> values = 'abcdefghi' + >>> iterable = zip(keys, values) + >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) + >>> [(k, ''.join(g)) for k, g in grouper] + [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] + + Note that the order of items in the iterable is significant. + Only adjacent items are grouped together, so if you don't want any + duplicate groups, you should sort the iterable by the key function. + + """ + ret = groupby(iterable, keyfunc) + if valuefunc: + ret = ((k, map(valuefunc, g)) for k, g in ret) + if reducefunc: + ret = ((k, reducefunc(g)) for k, g in ret) + + return ret + + +class numeric_range(abc.Sequence, abc.Hashable): + """An extension of the built-in ``range()`` function whose arguments can + be any orderable numeric type. + + With only *stop* specified, *start* defaults to ``0`` and *step* + defaults to ``1``. The output items will match the type of *stop*: + + >>> list(numeric_range(3.5)) + [0.0, 1.0, 2.0, 3.0] + + With only *start* and *stop* specified, *step* defaults to ``1``. The + output items will match the type of *start*: + + >>> from decimal import Decimal + >>> start = Decimal('2.1') + >>> stop = Decimal('5.1') + >>> list(numeric_range(start, stop)) + [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] + + With *start*, *stop*, and *step* specified the output items will match + the type of ``start + step``: + + >>> from fractions import Fraction + >>> start = Fraction(1, 2) # Start at 1/2 + >>> stop = Fraction(5, 2) # End at 5/2 + >>> step = Fraction(1, 2) # Count by 1/2 + >>> list(numeric_range(start, stop, step)) + [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] + + If *step* is zero, ``ValueError`` is raised. Negative steps are supported: + + >>> list(numeric_range(3, -1, -1.0)) + [3.0, 2.0, 1.0, 0.0] + + Be aware of the limitations of floating-point numbers; the representation + of the yielded numbers may be surprising. + + ``datetime.datetime`` objects can be used for *start* and *stop*, if *step* + is a ``datetime.timedelta`` object: + + >>> import datetime + >>> start = datetime.datetime(2019, 1, 1) + >>> stop = datetime.datetime(2019, 1, 3) + >>> step = datetime.timedelta(days=1) + >>> items = iter(numeric_range(start, stop, step)) + >>> next(items) + datetime.datetime(2019, 1, 1, 0, 0) + >>> next(items) + datetime.datetime(2019, 1, 2, 0, 0) + + """ + + _EMPTY_HASH = hash(range(0, 0)) + + def __init__(self, *args): + argc = len(args) + if argc == 1: + (self._stop,) = args + self._start = type(self._stop)(0) + self._step = type(self._stop - self._start)(1) + elif argc == 2: + self._start, self._stop = args + self._step = type(self._stop - self._start)(1) + elif argc == 3: + self._start, self._stop, self._step = args + elif argc == 0: + raise TypeError( + f'numeric_range expected at least 1 argument, got {argc}' + ) + else: + raise TypeError( + f'numeric_range expected at most 3 arguments, got {argc}' + ) + + self._zero = type(self._step)(0) + if self._step == self._zero: + raise ValueError('numeric_range() arg 3 must not be zero') + self._growing = self._step > self._zero + + def __bool__(self): + if self._growing: + return self._start < self._stop + else: + return self._start > self._stop + + def __contains__(self, elem): + if self._growing: + if self._start <= elem < self._stop: + return (elem - self._start) % self._step == self._zero + else: + if self._start >= elem > self._stop: + return (self._start - elem) % (-self._step) == self._zero + + return False + + def __eq__(self, other): + if isinstance(other, numeric_range): + empty_self = not bool(self) + empty_other = not bool(other) + if empty_self or empty_other: + return empty_self and empty_other # True if both empty + else: + return ( + self._start == other._start + and self._step == other._step + and self._get_by_index(-1) == other._get_by_index(-1) + ) + else: + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self._get_by_index(key) + elif isinstance(key, slice): + step = self._step if key.step is None else key.step * self._step + + if key.start is None or key.start <= -self._len: + start = self._start + elif key.start >= self._len: + start = self._stop + else: # -self._len < key.start < self._len + start = self._get_by_index(key.start) + + if key.stop is None or key.stop >= self._len: + stop = self._stop + elif key.stop <= -self._len: + stop = self._start + else: # -self._len < key.stop < self._len + stop = self._get_by_index(key.stop) + + return numeric_range(start, stop, step) + else: + raise TypeError( + 'numeric range indices must be ' + f'integers or slices, not {type(key).__name__}' + ) + + def __hash__(self): + if self: + return hash((self._start, self._get_by_index(-1), self._step)) + else: + return self._EMPTY_HASH + + def __iter__(self): + values = (self._start + (n * self._step) for n in count()) + if self._growing: + return takewhile(partial(gt, self._stop), values) + else: + return takewhile(partial(lt, self._stop), values) + + def __len__(self): + return self._len + + @cached_property + def _len(self): + if self._growing: + start = self._start + stop = self._stop + step = self._step + else: + start = self._stop + stop = self._start + step = -self._step + distance = stop - start + if distance <= self._zero: + return 0 + else: # distance > 0 and step > 0: regular euclidean division + q, r = divmod(distance, step) + return int(q) + int(r != self._zero) + + def __reduce__(self): + return numeric_range, (self._start, self._stop, self._step) + + def __repr__(self): + if self._step == 1: + return f"numeric_range({self._start!r}, {self._stop!r})" + return ( + f"numeric_range({self._start!r}, {self._stop!r}, {self._step!r})" + ) + + def __reversed__(self): + return iter( + numeric_range( + self._get_by_index(-1), self._start - self._step, -self._step + ) + ) + + def count(self, value): + return int(value in self) + + def index(self, value): + if self._growing: + if self._start <= value < self._stop: + q, r = divmod(value - self._start, self._step) + if r == self._zero: + return int(q) + else: + if self._start >= value > self._stop: + q, r = divmod(self._start - value, -self._step) + if r == self._zero: + return int(q) + + raise ValueError(f"{value} is not in numeric range") + + def _get_by_index(self, i): + if i < 0: + i += self._len + if i < 0 or i >= self._len: + raise IndexError("numeric range object index out of range") + return self._start + i * self._step + + +def count_cycle(iterable, n=None): + """Cycle through the items from *iterable* up to *n* times, yielding + the number of completed cycles along with each item. If *n* is omitted the + process repeats indefinitely. + + >>> list(count_cycle('AB', 3)) + [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] + + """ + seq = tuple(iterable) + if not seq: + return iter(()) + counter = count() if n is None else range(n) + return zip(repeat_each(counter, len(seq)), cycle(seq)) + + +def mark_ends(iterable): + """Yield 3-tuples of the form ``(is_first, is_last, item)``. + + >>> list(mark_ends('ABC')) + [(True, False, 'A'), (False, False, 'B'), (False, True, 'C')] + + Use this when looping over an iterable to take special action on its first + and/or last items: + + >>> iterable = ['Header', 100, 200, 'Footer'] + >>> total = 0 + >>> for is_first, is_last, item in mark_ends(iterable): + ... if is_first: + ... continue # Skip the header + ... if is_last: + ... continue # Skip the footer + ... total += item + >>> print(total) + 300 + """ + it = iter(iterable) + for a in it: + first = True + for b in it: + yield first, False, a + a = b + first = False + yield first, True, a + + +def locate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(locate([0, 1, 1, 0, 1, 0, 0])) + [1, 2, 4] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item. + + >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) + [1, 3] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(locate(iterable, pred=pred, window_size=3)) + [1, 5, 9] + + Use with :func:`seekable` to find indexes and then retrieve the associated + items: + + >>> from itertools import count + >>> from more_itertools import seekable + >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) + >>> it = seekable(source) + >>> pred = lambda x: x > 100 + >>> indexes = locate(it, pred=pred) + >>> i = next(indexes) + >>> it.seek(i) + >>> next(it) + 106 + + """ + if window_size is None: + return compress(count(), map(pred, iterable)) + + if window_size < 1: + raise ValueError('window size must be at least 1') + + it = windowed(iterable, window_size, fillvalue=_marker) + return compress(count(), starmap(pred, it)) + + +def longest_common_prefix(iterables): + """Yield elements of the longest common prefix among given *iterables*. + + >>> ''.join(longest_common_prefix(['abcd', 'abc', 'abf'])) + 'ab' + + """ + return (c[0] for c in takewhile(all_equal, zip(*iterables))) + + +def lstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the beginning + for which *pred* returns ``True``. + + For example, to remove a set of items from the start of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(lstrip(iterable, pred)) + [1, 2, None, 3, False, None] + + This function is analogous to to :func:`str.lstrip`, and is essentially + an wrapper for :func:`itertools.dropwhile`. + + """ + return dropwhile(pred, iterable) + + +def rstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the end + for which *pred* returns ``True``. + + For example, to remove a set of items from the end of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(rstrip(iterable, pred)) + [None, False, None, 1, 2, None, 3] + + This function is analogous to :func:`str.rstrip`. + + """ + cache = [] + cache_append = cache.append + cache_clear = cache.clear + for x in iterable: + if pred(x): + cache_append(x) + else: + yield from cache + cache_clear() + yield x + + +def strip(iterable, pred): + """Yield the items from *iterable*, but strip any from the + beginning and end for which *pred* returns ``True``. + + For example, to remove a set of items from both ends of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(strip(iterable, pred)) + [1, 2, None, 3] + + This function is analogous to :func:`str.strip`. + + """ + return rstrip(lstrip(iterable, pred), pred) + + +class islice_extended: + """An extension of :func:`itertools.islice` that supports negative values + for *stop*, *start*, and *step*. + + >>> iterator = iter('abcdefgh') + >>> list(islice_extended(iterator, -4, -1)) + ['e', 'f', 'g'] + + Slices with negative values require some caching of *iterable*, but this + function takes care to minimize the amount of memory required. + + For example, you can use a negative step with an infinite iterator: + + >>> from itertools import count + >>> list(islice_extended(count(), 110, 99, -2)) + [110, 108, 106, 104, 102, 100] + + You can also use slice notation directly: + + >>> iterator = map(str, count()) + >>> it = islice_extended(iterator)[10:20:2] + >>> list(it) + ['10', '12', '14', '16', '18'] + + """ + + def __init__(self, iterable, *args): + it = iter(iterable) + if args: + self._iterator = _islice_helper(it, slice(*args)) + else: + self._iterator = it + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterator) + + def __getitem__(self, key): + if isinstance(key, slice): + return islice_extended(_islice_helper(self._iterator, key)) + + raise TypeError('islice_extended.__getitem__ argument must be a slice') + + +def _islice_helper(it, s): + start = s.start + stop = s.stop + if s.step == 0: + raise ValueError('step argument must be a non-zero integer or None.') + step = s.step or 1 + + if step > 0: + start = 0 if (start is None) else start + + if start < 0: + # Consume all but the last -start items + cache = deque(enumerate(it, 1), maxlen=-start) + len_iter = cache[-1][0] if cache else 0 + + # Adjust start to be positive + i = max(len_iter + start, 0) + + # Adjust stop to be positive + if stop is None: + j = len_iter + elif stop >= 0: + j = min(stop, len_iter) + else: + j = max(len_iter + stop, 0) + + # Slice the cache + n = j - i + if n <= 0: + return + + for index in range(n): + if index % step == 0: + # pop and yield the item. + # We don't want to use an intermediate variable + # it would extend the lifetime of the current item + yield cache.popleft()[1] + else: + # just pop and discard the item + cache.popleft() + elif (stop is not None) and (stop < 0): + # Advance to the start position + next(islice(it, start, start), None) + + # When stop is negative, we have to carry -stop items while + # iterating + cache = deque(islice(it, -stop), maxlen=-stop) + + for index, item in enumerate(it): + if index % step == 0: + # pop and yield the item. + # We don't want to use an intermediate variable + # it would extend the lifetime of the current item + yield cache.popleft() + else: + # just pop and discard the item + cache.popleft() + cache.append(item) + else: + # When both start and stop are positive we have the normal case + yield from islice(it, start, stop, step) + else: + start = -1 if (start is None) else start + + if (stop is not None) and (stop < 0): + # Consume all but the last items + n = -stop - 1 + cache = deque(enumerate(it, 1), maxlen=n) + len_iter = cache[-1][0] if cache else 0 + + # If start and stop are both negative they are comparable and + # we can just slice. Otherwise we can adjust start to be negative + # and then slice. + if start < 0: + i, j = start, stop + else: + i, j = min(start - len_iter, -1), None + + for index, item in list(cache)[i:j:step]: + yield item + else: + # Advance to the stop position + if stop is not None: + m = stop + 1 + next(islice(it, m, m), None) + + # stop is positive, so if start is negative they are not comparable + # and we need the rest of the items. + if start < 0: + i = start + n = None + # stop is None and start is positive, so we just need items up to + # the start index. + elif stop is None: + i = None + n = start + 1 + # Both stop and start are positive, so they are comparable. + else: + i = None + n = start - stop + if n <= 0: + return + + cache = list(islice(it, n)) + + yield from cache[i::step] + + +def always_reversible(iterable): + """An extension of :func:`reversed` that supports all iterables, not + just those which implement the ``Reversible`` or ``Sequence`` protocols. + + >>> print(*always_reversible(x for x in range(3))) + 2 1 0 + + If the iterable is already reversible, this function returns the + result of :func:`reversed()`. If the iterable is not reversible, + this function will cache the remaining items in the iterable and + yield them in reverse order, which may require significant storage. + """ + try: + return reversed(iterable) + except TypeError: + return reversed(list(iterable)) + + +def consecutive_groups(iterable, ordering=None): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + By default, the ordering function is the identity function. This is + suitable for finding runs of numbers: + + >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] + >>> for group in consecutive_groups(iterable): + ... print(list(group)) + [1] + [10, 11, 12] + [20] + [30, 31, 32, 33] + [40] + + To find runs of adjacent letters, apply :func:`ord` function + to convert letters to ordinals. + + >>> iterable = 'abcdfgilmnop' + >>> ordering = ord + >>> for group in consecutive_groups(iterable, ordering): + ... print(list(group)) + ['a', 'b', 'c', 'd'] + ['f', 'g'] + ['i'] + ['l', 'm', 'n', 'o', 'p'] + + Each group of consecutive items is an iterator that shares it source with + *iterable*. When an an output group is advanced, the previous group is + no longer available unless its elements are copied (e.g., into a ``list``). + + >>> iterable = [1, 2, 11, 12, 21, 22] + >>> saved_groups = [] + >>> for group in consecutive_groups(iterable): + ... saved_groups.append(list(group)) # Copy group elements + >>> saved_groups + [[1, 2], [11, 12], [21, 22]] + + """ + if ordering is None: + key = lambda x: x[0] - x[1] + else: + key = lambda x: x[0] - ordering(x[1]) + + for k, g in groupby(enumerate(iterable), key=key): + yield map(itemgetter(1), g) + + +def difference(iterable, func=sub, *, initial=None): + """This function is the inverse of :func:`itertools.accumulate`. By default + it will compute the first difference of *iterable* using + :func:`operator.sub`: + + >>> from itertools import accumulate + >>> iterable = accumulate([0, 1, 2, 3, 4]) # produces 0, 1, 3, 6, 10 + >>> list(difference(iterable)) + [0, 1, 2, 3, 4] + + *func* defaults to :func:`operator.sub`, but other functions can be + specified. They will be applied as follows:: + + A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... + + For example, to do progressive division: + + >>> iterable = [1, 2, 6, 24, 120] + >>> func = lambda x, y: x // y + >>> list(difference(iterable, func)) + [1, 2, 3, 4, 5] + + If the *initial* keyword is set, the first element will be skipped when + computing successive differences. + + >>> it = [10, 11, 13, 16] # from accumulate([1, 2, 3], initial=10) + >>> list(difference(it, initial=10)) + [1, 2, 3] + + """ + a, b = tee(iterable) + try: + first = [next(b)] + except StopIteration: + return iter([]) + + if initial is not None: + first = [] + + return chain(first, map(func, b, a)) + + +class SequenceView(Sequence): + """Return a read-only view of the sequence object *target*. + + :class:`SequenceView` objects are analogous to Python's built-in + "dictionary view" types. They provide a dynamic view of a sequence's items, + meaning that when the sequence updates, so does the view. + + >>> seq = ['0', '1', '2'] + >>> view = SequenceView(seq) + >>> view + SequenceView(['0', '1', '2']) + >>> seq.append('3') + >>> view + SequenceView(['0', '1', '2', '3']) + + Sequence views support indexing, slicing, and length queries. They act + like the underlying sequence, except they don't allow assignment: + + >>> view[1] + '1' + >>> view[1:-1] + ['1', '2'] + >>> len(view) + 4 + + Sequence views are useful as an alternative to copying, as they don't + require (much) extra storage. + + """ + + def __init__(self, target): + if not isinstance(target, Sequence): + raise TypeError + self._target = target + + def __getitem__(self, index): + return self._target[index] + + def __len__(self): + return len(self._target) + + def __repr__(self): + return f'{self.__class__.__name__}({self._target!r})' + + +class seekable: + """Wrap an iterator to allow for seeking backward and forward. This + progressively caches the items in the source iterable so they can be + re-visited. + + Call :meth:`seek` with an index to seek to that position in the source + iterable. + + To "reset" an iterator, seek to ``0``: + + >>> from itertools import count + >>> it = seekable((str(n) for n in count())) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> it.seek(0) + >>> next(it), next(it), next(it) + ('0', '1', '2') + + You can also seek forward: + + >>> it = seekable((str(n) for n in range(20))) + >>> it.seek(10) + >>> next(it) + '10' + >>> it.seek(20) # Seeking past the end of the source isn't a problem + >>> list(it) + [] + >>> it.seek(0) # Resetting works even after hitting the end + >>> next(it) + '0' + + Call :meth:`relative_seek` to seek relative to the source iterator's + current position. + + >>> it = seekable((str(n) for n in range(20))) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> it.relative_seek(2) + >>> next(it) + '5' + >>> it.relative_seek(-3) # Source is at '6', we move back to '3' + >>> next(it) + '3' + >>> it.relative_seek(-3) # Source is at '4', we move back to '1' + >>> next(it) + '1' + + + Call :meth:`peek` to look ahead one item without advancing the iterator: + + >>> it = seekable('1234') + >>> it.peek() + '1' + >>> list(it) + ['1', '2', '3', '4'] + >>> it.peek(default='empty') + 'empty' + + Before the iterator is at its end, calling :func:`bool` on it will return + ``True``. After it will return ``False``: + + >>> it = seekable('5678') + >>> bool(it) + True + >>> list(it) + ['5', '6', '7', '8'] + >>> bool(it) + False + + You may view the contents of the cache with the :meth:`elements` method. + That returns a :class:`SequenceView`, a view that updates automatically: + + >>> it = seekable((str(n) for n in range(10))) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> elements = it.elements() + >>> elements + SequenceView(['0', '1', '2']) + >>> next(it) + '3' + >>> elements + SequenceView(['0', '1', '2', '3']) + + By default, the cache grows as the source iterable progresses, so beware of + wrapping very large or infinite iterables. Supply *maxlen* to limit the + size of the cache (this of course limits how far back you can seek). + + >>> from itertools import count + >>> it = seekable((str(n) for n in count()), maxlen=2) + >>> next(it), next(it), next(it), next(it) + ('0', '1', '2', '3') + >>> list(it.elements()) + ['2', '3'] + >>> it.seek(0) + >>> next(it), next(it), next(it), next(it) + ('2', '3', '4', '5') + >>> next(it) + '6' + + """ + + def __init__(self, iterable, maxlen=None): + self._source = iter(iterable) + if maxlen is None: + self._cache = [] + else: + self._cache = deque([], maxlen) + self._index = None + + def __iter__(self): + return self + + def __next__(self): + if self._index is not None: + try: + item = self._cache[self._index] + except IndexError: + self._index = None + else: + self._index += 1 + return item + + item = next(self._source) + self._cache.append(item) + return item + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + try: + peeked = next(self) + except StopIteration: + if default is _marker: + raise + return default + if self._index is None: + self._index = len(self._cache) + self._index -= 1 + return peeked + + def elements(self): + return SequenceView(self._cache) + + def seek(self, index): + self._index = index + remainder = index - len(self._cache) + if remainder > 0: + consume(self, remainder) + + def relative_seek(self, count): + if self._index is None: + self._index = len(self._cache) + + self.seek(max(self._index + count, 0)) + + +class run_length: + """ + :func:`run_length.encode` compresses an iterable with run-length encoding. + It yields groups of repeated items with the count of how many times they + were repeated: + + >>> uncompressed = 'abbcccdddd' + >>> list(run_length.encode(uncompressed)) + [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + + :func:`run_length.decode` decompresses an iterable that was previously + compressed with run-length encoding. It yields the items of the + decompressed iterable: + + >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> list(run_length.decode(compressed)) + ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] + + """ + + @staticmethod + def encode(iterable): + return ((k, ilen(g)) for k, g in groupby(iterable)) + + @staticmethod + def decode(iterable): + return chain.from_iterable(starmap(repeat, iterable)) + + +def exactly_n(iterable, n, predicate=bool): + """Return ``True`` if exactly ``n`` items in the iterable are ``True`` + according to the *predicate* function. + + >>> exactly_n([True, True, False], 2) + True + >>> exactly_n([True, True, False], 1) + False + >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) + True + + The iterable will be advanced until ``n + 1`` truthy items are encountered, + so avoid calling it on infinite iterables. + + """ + return ilen(islice(filter(predicate, iterable), n + 1)) == n + + +def circular_shifts(iterable, steps=1): + """Yield the circular shifts of *iterable*. + + >>> list(circular_shifts(range(4))) + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + + Set *steps* to the number of places to rotate to the left + (or to the right if negative). Defaults to 1. + + >>> list(circular_shifts(range(4), 2)) + [(0, 1, 2, 3), (2, 3, 0, 1)] + + >>> list(circular_shifts(range(4), -1)) + [(0, 1, 2, 3), (3, 0, 1, 2), (2, 3, 0, 1), (1, 2, 3, 0)] + + """ + buffer = deque(iterable) + if steps == 0: + raise ValueError('Steps should be a non-zero integer') + + buffer.rotate(steps) + steps = -steps + n = len(buffer) + n //= math.gcd(n, steps) + + for _ in repeat(None, n): + buffer.rotate(steps) + yield tuple(buffer) + + +def make_decorator(wrapping_func, result_index=0): + """Return a decorator version of *wrapping_func*, which is a function that + modifies an iterable. *result_index* is the position in that function's + signature where the iterable goes. + + This lets you use itertools on the "production end," i.e. at function + definition. This can augment what the function returns without changing the + function's code. + + For example, to produce a decorator version of :func:`chunked`: + + >>> from more_itertools import chunked + >>> chunker = make_decorator(chunked, result_index=0) + >>> @chunker(3) + ... def iter_range(n): + ... return iter(range(n)) + ... + >>> list(iter_range(9)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + To only allow truthy items to be returned: + + >>> truth_serum = make_decorator(filter, result_index=1) + >>> @truth_serum(bool) + ... def boolean_test(): + ... return [0, 1, '', ' ', False, True] + ... + >>> list(boolean_test()) + [1, ' ', True] + + The :func:`peekable` and :func:`seekable` wrappers make for practical + decorators: + + >>> from more_itertools import peekable + >>> peekable_function = make_decorator(peekable) + >>> @peekable_function() + ... def str_range(*args): + ... return (str(x) for x in range(*args)) + ... + >>> it = str_range(1, 20, 2) + >>> next(it), next(it), next(it) + ('1', '3', '5') + >>> it.peek() + '7' + >>> next(it) + '7' + + """ + + # See https://sites.google.com/site/bbayles/index/decorator_factory for + # notes on how this works. + def decorator(*wrapping_args, **wrapping_kwargs): + def outer_wrapper(f): + def inner_wrapper(*args, **kwargs): + result = f(*args, **kwargs) + wrapping_args_ = list(wrapping_args) + wrapping_args_.insert(result_index, result) + return wrapping_func(*wrapping_args_, **wrapping_kwargs) + + return inner_wrapper + + return outer_wrapper + + return decorator + + +def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): + """Return a dictionary that maps the items in *iterable* to categories + defined by *keyfunc*, transforms them with *valuefunc*, and + then summarizes them by category with *reducefunc*. + + *valuefunc* defaults to the identity function if it is unspecified. + If *reducefunc* is unspecified, no summarization takes place: + + >>> keyfunc = lambda x: x.upper() + >>> result = map_reduce('abbccc', keyfunc) + >>> sorted(result.items()) + [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] + + Specifying *valuefunc* transforms the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> result = map_reduce('abbccc', keyfunc, valuefunc) + >>> sorted(result.items()) + [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] + + Specifying *reducefunc* summarizes the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> reducefunc = sum + >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) + >>> sorted(result.items()) + [('A', 1), ('B', 2), ('C', 3)] + + You may want to filter the input iterable before applying the map/reduce + procedure: + + >>> all_items = range(30) + >>> items = [x for x in all_items if 10 <= x <= 20] # Filter + >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 + >>> categories = map_reduce(items, keyfunc=keyfunc) + >>> sorted(categories.items()) + [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] + >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) + >>> sorted(summaries.items()) + [(0, 90), (1, 75)] + + Note that all items in the iterable are gathered into a list before the + summarization step, which may require significant storage. + + The returned object is a :obj:`collections.defaultdict` with the + ``default_factory`` set to ``None``, such that it behaves like a normal + dictionary. + + """ + + ret = defaultdict(list) + + if valuefunc is None: + for item in iterable: + key = keyfunc(item) + ret[key].append(item) + + else: + for item in iterable: + key = keyfunc(item) + value = valuefunc(item) + ret[key].append(value) + + if reducefunc is not None: + for key, value_list in ret.items(): + ret[key] = reducefunc(value_list) + + ret.default_factory = None + return ret + + +def rlocate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``, starting from the right and moving left. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 + [4, 2, 1] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item: + + >>> iterator = iter('abcb') + >>> pred = lambda x: x == 'b' + >>> list(rlocate(iterator, pred)) + [3, 1] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(rlocate(iterable, pred=pred, window_size=3)) + [9, 5, 1] + + Beware, this function won't return anything for infinite iterables. + If *iterable* is reversible, ``rlocate`` will reverse it and search from + the right. Otherwise, it will search from the left and return the results + in reverse order. + + See :func:`locate` to for other example applications. + + """ + if window_size is None: + try: + len_iter = len(iterable) + return (len_iter - i - 1 for i in locate(reversed(iterable), pred)) + except TypeError: + pass + + return reversed(list(locate(iterable, pred, window_size))) + + +def replace(iterable, pred, substitutes, count=None, window_size=1): + """Yield the items from *iterable*, replacing the items for which *pred* + returns ``True`` with the items from the iterable *substitutes*. + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] + >>> pred = lambda x: x == 0 + >>> substitutes = (2, 3) + >>> list(replace(iterable, pred, substitutes)) + [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] + + If *count* is given, the number of replacements will be limited: + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] + >>> pred = lambda x: x == 0 + >>> substitutes = [None] + >>> list(replace(iterable, pred, substitutes, count=2)) + [1, 1, None, 1, 1, None, 1, 1, 0] + + Use *window_size* to control the number of items passed as arguments to + *pred*. This allows for locating and replacing subsequences. + + >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] + >>> window_size = 3 + >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred + >>> substitutes = [3, 4] # Splice in these items + >>> list(replace(iterable, pred, substitutes, window_size=window_size)) + [3, 4, 5, 3, 4, 5] + + """ + if window_size < 1: + raise ValueError('window_size must be at least 1') + + # Save the substitutes iterable, since it's used more than once + substitutes = tuple(substitutes) + + # Add padding such that the number of windows matches the length of the + # iterable + it = chain(iterable, repeat(_marker, window_size - 1)) + windows = windowed(it, window_size) + + n = 0 + for w in windows: + # If the current window matches our predicate (and we haven't hit + # our maximum number of replacements), splice in the substitutes + # and then consume the following windows that overlap with this one. + # For example, if the iterable is (0, 1, 2, 3, 4...) + # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... + # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) + if pred(*w): + if (count is None) or (n < count): + n += 1 + yield from substitutes + consume(windows, window_size - 1) + continue + + # If there was no match (or we've reached the replacement limit), + # yield the first item from the window. + if w and (w[0] is not _marker): + yield w[0] + + +def partitions(iterable): + """Yield all possible order-preserving partitions of *iterable*. + + >>> iterable = 'abc' + >>> for part in partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['a', 'b', 'c'] + + This is unrelated to :func:`partition`. + + """ + sequence = list(iterable) + n = len(sequence) + for i in powerset(range(1, n)): + yield [sequence[i:j] for i, j in zip((0,) + i, i + (n,))] + + +def set_partitions(iterable, k=None, min_size=None, max_size=None): + """ + Yield the set partitions of *iterable* into *k* parts. Set partitions are + not order-preserving. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable, 2): + ... print([''.join(p) for p in part]) + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + + + If *k* is not given, every set partition is generated. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + ['a', 'b', 'c'] + + if *min_size* and/or *max_size* are given, the minimum and/or maximum size + per block in partition is set. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable, min_size=2): + ... print([''.join(p) for p in part]) + ['abc'] + >>> for part in set_partitions(iterable, max_size=2): + ... print([''.join(p) for p in part]) + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + ['a', 'b', 'c'] + + """ + L = list(iterable) + n = len(L) + if k is not None: + if k < 1: + raise ValueError( + "Can't partition in a negative or zero number of groups" + ) + elif k > n: + return + + min_size = min_size if min_size is not None else 0 + max_size = max_size if max_size is not None else n + if min_size > max_size: + return + + def set_partitions_helper(L, k): + n = len(L) + if k == 1: + yield [L] + elif n == k: + yield [[s] for s in L] + else: + e, *M = L + for p in set_partitions_helper(M, k - 1): + yield [[e], *p] + for p in set_partitions_helper(M, k): + for i in range(len(p)): + yield p[:i] + [[e] + p[i]] + p[i + 1 :] + + if k is None: + for k in range(1, n + 1): + yield from filter( + lambda z: all(min_size <= len(bk) <= max_size for bk in z), + set_partitions_helper(L, k), + ) + else: + yield from filter( + lambda z: all(min_size <= len(bk) <= max_size for bk in z), + set_partitions_helper(L, k), + ) + + +class time_limited: + """ + Yield items from *iterable* until *limit_seconds* have passed. + If the time limit expires before all items have been yielded, the + ``timed_out`` parameter will be set to ``True``. + + >>> from time import sleep + >>> def generator(): + ... yield 1 + ... yield 2 + ... sleep(0.2) + ... yield 3 + >>> iterable = time_limited(0.1, generator()) + >>> list(iterable) + [1, 2] + >>> iterable.timed_out + True + + Note that the time is checked before each item is yielded, and iteration + stops if the time elapsed is greater than *limit_seconds*. If your time + limit is 1 second, but it takes 2 seconds to generate the first item from + the iterable, the function will run for 2 seconds and not yield anything. + As a special case, when *limit_seconds* is zero, the iterator never + returns anything. + + """ + + def __init__(self, limit_seconds, iterable): + if limit_seconds < 0: + raise ValueError('limit_seconds must be positive') + self.limit_seconds = limit_seconds + self._iterator = iter(iterable) + self._start_time = monotonic() + self.timed_out = False + + def __iter__(self): + return self + + def __next__(self): + if self.limit_seconds == 0: + self.timed_out = True + raise StopIteration + item = next(self._iterator) + if monotonic() - self._start_time > self.limit_seconds: + self.timed_out = True + raise StopIteration + + return item + + +def only(iterable, default=None, too_long=None): + """If *iterable* has only one item, return it. + If it has zero items, return *default*. + If it has more than one item, raise the exception given by *too_long*, + which is ``ValueError`` by default. + + >>> only([], default='missing') + 'missing' + >>> only([1]) + 1 + >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 1, 2, + and perhaps more.' + >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError + + Note that :func:`only` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check + iterable contents less destructively. + + """ + iterator = iter(iterable) + for first in iterator: + for second in iterator: + msg = ( + f'Expected exactly one item in iterable, but got {first!r}, ' + f'{second!r}, and perhaps more.' + ) + raise too_long or ValueError(msg) + return first + return default + + +def _ichunk(iterator, n): + cache = deque() + chunk = islice(iterator, n) + + def generator(): + with suppress(StopIteration): + while True: + if cache: + yield cache.popleft() + else: + yield next(chunk) + + def materialize_next(n=1): + # if n not specified materialize everything + if n is None: + cache.extend(chunk) + return len(cache) + + to_cache = n - len(cache) + + # materialize up to n + if to_cache > 0: + cache.extend(islice(chunk, to_cache)) + + # return number materialized up to n + return min(n, len(cache)) + + return (generator(), materialize_next) + + +def ichunked(iterable, n): + """Break *iterable* into sub-iterables with *n* elements each. + :func:`ichunked` is like :func:`chunked`, but it yields iterables + instead of lists. + + If the sub-iterables are read in order, the elements of *iterable* + won't be stored in memory. + If they are read out of order, :func:`itertools.tee` is used to cache + elements as necessary. + + >>> from itertools import count + >>> all_chunks = ichunked(count(), 4) + >>> c_1, c_2, c_3 = next(all_chunks), next(all_chunks), next(all_chunks) + >>> list(c_2) # c_1's elements have been cached; c_3's haven't been + [4, 5, 6, 7] + >>> list(c_1) + [0, 1, 2, 3] + >>> list(c_3) + [8, 9, 10, 11] + + """ + iterator = iter(iterable) + while True: + # Create new chunk + chunk, materialize_next = _ichunk(iterator, n) + + # Check to see whether we're at the end of the source iterable + if not materialize_next(): + return + + yield chunk + + # Fill previous chunk's cache + materialize_next(None) + + +def iequals(*iterables): + """Return ``True`` if all given *iterables* are equal to each other, + which means that they contain the same elements in the same order. + + The function is useful for comparing iterables of different data types + or iterables that do not support equality checks. + + >>> iequals("abc", ['a', 'b', 'c'], ('a', 'b', 'c'), iter("abc")) + True + + >>> iequals("abc", "acb") + False + + Not to be confused with :func:`all_equal`, which checks whether all + elements of iterable are equal to each other. + + """ + return all(map(all_equal, zip_longest(*iterables, fillvalue=object()))) + + +def distinct_combinations(iterable, r): + """Yield the distinct combinations of *r* items taken from *iterable*. + + >>> list(distinct_combinations([0, 0, 1], 2)) + [(0, 0), (0, 1)] + + Equivalent to ``set(combinations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + """ + if r < 0: + raise ValueError('r must be non-negative') + elif r == 0: + yield () + return + pool = tuple(iterable) + generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] + current_combo = [None] * r + level = 0 + while generators: + try: + cur_idx, p = next(generators[-1]) + except StopIteration: + generators.pop() + level -= 1 + continue + current_combo[level] = p + if level + 1 == r: + yield tuple(current_combo) + else: + generators.append( + unique_everseen( + enumerate(pool[cur_idx + 1 :], cur_idx + 1), + key=itemgetter(1), + ) + ) + level += 1 + + +def filter_except(validator, iterable, *exceptions): + """Yield the items from *iterable* for which the *validator* function does + not raise one of the specified *exceptions*. + + *validator* is called for each item in *iterable*. + It should be a function that accepts one argument and raises an exception + if that item is not valid. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(filter_except(int, iterable, ValueError, TypeError)) + ['1', '2', '4'] + + If an exception other than one given by *exceptions* is raised by + *validator*, it is raised like normal. + """ + for item in iterable: + try: + validator(item) + except exceptions: + pass + else: + yield item + + +def map_except(function, iterable, *exceptions): + """Transform each item from *iterable* with *function* and yield the + result, unless *function* raises one of the specified *exceptions*. + + *function* is called to transform each item in *iterable*. + It should accept one argument. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(map_except(int, iterable, ValueError, TypeError)) + [1, 2, 4] + + If an exception other than one given by *exceptions* is raised by + *function*, it is raised like normal. + """ + for item in iterable: + try: + yield function(item) + except exceptions: + pass + + +def map_if(iterable, pred, func, func_else=None): + """Evaluate each item from *iterable* using *pred*. If the result is + equivalent to ``True``, transform the item with *func* and yield it. + Otherwise, transform the item with *func_else* and yield it. + + *pred*, *func*, and *func_else* should each be functions that accept + one argument. By default, *func_else* is the identity function. + + >>> from math import sqrt + >>> iterable = list(range(-5, 5)) + >>> iterable + [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4] + >>> list(map_if(iterable, lambda x: x > 3, lambda x: 'toobig')) + [-5, -4, -3, -2, -1, 0, 1, 2, 3, 'toobig'] + >>> list(map_if(iterable, lambda x: x >= 0, + ... lambda x: f'{sqrt(x):.2f}', lambda x: None)) + [None, None, None, None, None, '0.00', '1.00', '1.41', '1.73', '2.00'] + """ + + if func_else is None: + for item in iterable: + yield func(item) if pred(item) else item + + else: + for item in iterable: + yield func(item) if pred(item) else func_else(item) + + +def _sample_unweighted(iterator, k, strict): + # Algorithm L in the 1994 paper by Kim-Hung Li: + # "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". + + reservoir = list(islice(iterator, k)) + if strict and len(reservoir) < k: + raise ValueError('Sample larger than population') + W = 1.0 + + with suppress(StopIteration): + while True: + W *= random() ** (1 / k) + skip = floor(log(random()) / log1p(-W)) + element = next(islice(iterator, skip, None)) + reservoir[randrange(k)] = element + + shuffle(reservoir) + return reservoir + + +def _sample_weighted(iterator, k, weights, strict): + # Implementation of "A-ExpJ" from the 2006 paper by Efraimidis et al. : + # "Weighted random sampling with a reservoir". + + # Log-transform for numerical stability for weights that are small/large + weight_keys = (log(random()) / weight for weight in weights) + + # Fill up the reservoir (collection of samples) with the first `k` + # weight-keys and elements, then heapify the list. + reservoir = take(k, zip(weight_keys, iterator)) + if strict and len(reservoir) < k: + raise ValueError('Sample larger than population') + + heapify(reservoir) + + # The number of jumps before changing the reservoir is a random variable + # with an exponential distribution. Sample it using random() and logs. + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + + for weight, element in zip(weights, iterator): + if weight >= weights_to_skip: + # The notation here is consistent with the paper, but we store + # the weight-keys in log-space for better numerical stability. + smallest_weight_key, _ = reservoir[0] + t_w = exp(weight * smallest_weight_key) + r_2 = uniform(t_w, 1) # generate U(t_w, 1) + weight_key = log(r_2) / weight + heapreplace(reservoir, (weight_key, element)) + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + else: + weights_to_skip -= weight + + ret = [element for weight_key, element in reservoir] + shuffle(ret) + return ret + + +def _sample_counted(population, k, counts, strict): + element = None + remaining = 0 + + def feed(i): + # Advance *i* steps ahead and consume an element + nonlocal element, remaining + + while i + 1 > remaining: + i = i - remaining + element = next(population) + remaining = next(counts) + remaining -= i + 1 + return element + + with suppress(StopIteration): + reservoir = [] + for _ in range(k): + reservoir.append(feed(0)) + + if strict and len(reservoir) < k: + raise ValueError('Sample larger than population') + + with suppress(StopIteration): + W = 1.0 + while True: + W *= random() ** (1 / k) + skip = floor(log(random()) / log1p(-W)) + element = feed(skip) + reservoir[randrange(k)] = element + + shuffle(reservoir) + return reservoir + + +def sample(iterable, k, weights=None, *, counts=None, strict=False): + """Return a *k*-length list of elements chosen (without replacement) + from the *iterable*. + + Similar to :func:`random.sample`, but works on inputs that aren't + indexable (such as sets and dictionaries) and on inputs where the + size isn't known in advance (such as generators). + + >>> iterable = range(100) + >>> sample(iterable, 5) # doctest: +SKIP + [81, 60, 96, 16, 4] + + For iterables with repeated elements, you may supply *counts* to + indicate the repeats. + + >>> iterable = ['a', 'b'] + >>> counts = [3, 4] # Equivalent to 'a', 'a', 'a', 'b', 'b', 'b', 'b' + >>> sample(iterable, k=3, counts=counts) # doctest: +SKIP + ['a', 'a', 'b'] + + An iterable with *weights* may be given: + + >>> iterable = range(100) + >>> weights = (i * i + 1 for i in range(100)) + >>> sampled = sample(iterable, 5, weights=weights) # doctest: +SKIP + [79, 67, 74, 66, 78] + + Weighted selections are made without replacement. + After an element is selected, it is removed from the pool and the + relative weights of the other elements increase (this + does not match the behavior of :func:`random.sample`'s *counts* + parameter). Note that *weights* may not be used with *counts*. + + If the length of *iterable* is less than *k*, + ``ValueError`` is raised if *strict* is ``True`` and + all elements are returned (in shuffled order) if *strict* is ``False``. + + By default, the `Algorithm L `__ reservoir sampling + technique is used. When *weights* are provided, + `Algorithm A-ExpJ `__ is used instead. + + Notes on reproducibility: + + * The algorithms rely on inexact floating-point functions provided + by the underlying math library (e.g. ``log``, ``log1p``, and ``pow``). + Those functions can `produce slightly different results + `_ on + different builds. Accordingly, selections can vary across builds + even for the same seed. + + * The algorithms loop over the input and make selections based on + ordinal position, so selections from unordered collections (such as + sets) won't reproduce across sessions on the same platform using the + same seed. For example, this won't reproduce:: + + >> seed(8675309) + >> sample(set('abcdefghijklmnopqrstuvwxyz'), 10) + ['c', 'p', 'e', 'w', 's', 'a', 'j', 'd', 'n', 't'] + + """ + iterator = iter(iterable) + + if k < 0: + raise ValueError('k must be non-negative') + + if k == 0: + return [] + + if weights is not None and counts is not None: + raise TypeError('weights and counts are mutually exclusive') + + elif weights is not None: + weights = iter(weights) + return _sample_weighted(iterator, k, weights, strict) + + elif counts is not None: + counts = iter(counts) + return _sample_counted(iterator, k, counts, strict) + + else: + return _sample_unweighted(iterator, k, strict) + + +def is_sorted(iterable, key=None, reverse=False, strict=False): + """Returns ``True`` if the items of iterable are in sorted order, and + ``False`` otherwise. *key* and *reverse* have the same meaning that they do + in the built-in :func:`sorted` function. + + >>> is_sorted(['1', '2', '3', '4', '5'], key=int) + True + >>> is_sorted([5, 4, 3, 1, 2], reverse=True) + False + + If *strict*, tests for strict sorting, that is, returns ``False`` if equal + elements are found: + + >>> is_sorted([1, 2, 2]) + True + >>> is_sorted([1, 2, 2], strict=True) + False + + The function returns ``False`` after encountering the first out-of-order + item, which means it may produce results that differ from the built-in + :func:`sorted` function for objects with unusual comparison dynamics + (like ``math.nan``). If there are no out-of-order items, the iterable is + exhausted. + """ + it = iterable if (key is None) else map(key, iterable) + a, b = tee(it) + next(b, None) + if reverse: + b, a = a, b + return all(map(lt, a, b)) if strict else not any(map(lt, b, a)) + + +class AbortThread(BaseException): + pass + + +class callback_iter: + """Convert a function that uses callbacks to an iterator. + + Let *func* be a function that takes a `callback` keyword argument. + For example: + + >>> def func(callback=None): + ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: + ... if callback: + ... callback(i, c) + ... return 4 + + + Use ``with callback_iter(func)`` to get an iterator over the parameters + that are delivered to the callback. + + >>> with callback_iter(func) as it: + ... for args, kwargs in it: + ... print(args) + (1, 'a') + (2, 'b') + (3, 'c') + + The function will be called in a background thread. The ``done`` property + indicates whether it has completed execution. + + >>> it.done + True + + If it completes successfully, its return value will be available + in the ``result`` property. + + >>> it.result + 4 + + Notes: + + * If the function uses some keyword argument besides ``callback``, supply + *callback_kwd*. + * If it finished executing, but raised an exception, accessing the + ``result`` property will raise the same exception. + * If it hasn't finished executing, accessing the ``result`` + property from within the ``with`` block will raise ``RuntimeError``. + * If it hasn't finished executing, accessing the ``result`` property from + outside the ``with`` block will raise a + ``more_itertools.AbortThread`` exception. + * Provide *wait_seconds* to adjust how frequently the it is polled for + output. + + """ + + def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): + self._func = func + self._callback_kwd = callback_kwd + self._aborted = False + self._future = None + self._wait_seconds = wait_seconds + # Lazily import concurrent.future + self._executor = __import__( + 'concurrent.futures' + ).futures.ThreadPoolExecutor(max_workers=1) + self._iterator = self._reader() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._aborted = True + self._executor.shutdown() + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterator) + + @property + def done(self): + if self._future is None: + return False + return self._future.done() + + @property + def result(self): + if not self.done: + raise RuntimeError('Function has not yet completed') + + return self._future.result() + + def _reader(self): + q = Queue() + + def callback(*args, **kwargs): + if self._aborted: + raise AbortThread('canceled by user') + + q.put((args, kwargs)) + + self._future = self._executor.submit( + self._func, **{self._callback_kwd: callback} + ) + + while True: + try: + item = q.get(timeout=self._wait_seconds) + except Empty: + pass + else: + q.task_done() + yield item + + if self._future.done(): + break + + remaining = [] + while True: + try: + item = q.get_nowait() + except Empty: + break + else: + q.task_done() + remaining.append(item) + q.join() + yield from remaining + + +def windowed_complete(iterable, n): + """ + Yield ``(beginning, middle, end)`` tuples, where: + + * Each ``middle`` has *n* items from *iterable* + * Each ``beginning`` has the items before the ones in ``middle`` + * Each ``end`` has the items after the ones in ``middle`` + + >>> iterable = range(7) + >>> n = 3 + >>> for beginning, middle, end in windowed_complete(iterable, n): + ... print(beginning, middle, end) + () (0, 1, 2) (3, 4, 5, 6) + (0,) (1, 2, 3) (4, 5, 6) + (0, 1) (2, 3, 4) (5, 6) + (0, 1, 2) (3, 4, 5) (6,) + (0, 1, 2, 3) (4, 5, 6) () + + Note that *n* must be at least 0 and most equal to the length of + *iterable*. + + This function will exhaust the iterable and may require significant + storage. + """ + if n < 0: + raise ValueError('n must be >= 0') + + seq = tuple(iterable) + size = len(seq) + + if n > size: + raise ValueError('n must be <= len(seq)') + + for i in range(size - n + 1): + beginning = seq[:i] + middle = seq[i : i + n] + end = seq[i + n :] + yield beginning, middle, end + + +def all_unique(iterable, key=None): + """ + Returns ``True`` if all the elements of *iterable* are unique (no two + elements are equal). + + >>> all_unique('ABCB') + False + + If a *key* function is specified, it will be used to make comparisons. + + >>> all_unique('ABCb') + True + >>> all_unique('ABCb', str.lower) + False + + The function returns as soon as the first non-unique element is + encountered. Iterables with a mix of hashable and unhashable items can + be used, but the function will be slower for unhashable items. + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + for element in map(key, iterable) if key else iterable: + try: + if element in seenset: + return False + seenset_add(element) + except TypeError: + if element in seenlist: + return False + seenlist_add(element) + return True + + +def nth_product(index, *args): + """Equivalent to ``list(product(*args))[index]``. + + The products of *args* can be ordered lexicographically. + :func:`nth_product` computes the product at sort position *index* without + computing the previous products. + + >>> nth_product(8, range(2), range(2), range(2), range(2)) + (1, 0, 0, 0) + + ``IndexError`` will be raised if the given *index* is invalid. + """ + pools = list(map(tuple, reversed(args))) + ns = list(map(len, pools)) + + c = reduce(mul, ns) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + result = [] + for pool, n in zip(pools, ns): + result.append(pool[index % n]) + index //= n + + return tuple(reversed(result)) + + +def nth_permutation(iterable, r, index): + """Equivalent to ``list(permutations(iterable, r))[index]``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`nth_permutation` + computes the subsequence at sort position *index* directly, without + computing the previous subsequences. + + >>> nth_permutation('ghijk', 2, 5) + ('h', 'i') + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = list(iterable) + n = len(pool) + + if r is None or r == n: + r, c = n, factorial(n) + elif not 0 <= r < n: + raise ValueError + else: + c = perm(n, r) + assert c > 0 # factorial(n)>0, and r>> nth_combination_with_replacement(range(5), 3, 5) + (0, 1, 1) + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = tuple(iterable) + n = len(pool) + if (r < 0) or (r > n): + raise ValueError + + c = comb(n + r - 1, r) + + if index < 0: + index += c + + if (index < 0) or (index >= c): + raise IndexError + + result = [] + i = 0 + while r: + r -= 1 + while n >= 0: + num_combs = comb(n + r - 1, r) + if index < num_combs: + break + n -= 1 + i += 1 + index -= num_combs + result.append(pool[i]) + + return tuple(result) + + +def value_chain(*args): + """Yield all arguments passed to the function in the same order in which + they were passed. If an argument itself is iterable then iterate over its + values. + + >>> list(value_chain(1, 2, 3, [4, 5, 6])) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and are emitted + as-is: + + >>> list(value_chain('12', '34', ['56', '78'])) + ['12', '34', '56', '78'] + + Pre- or postpend a single element to an iterable: + + >>> list(value_chain(1, [2, 3, 4, 5, 6])) + [1, 2, 3, 4, 5, 6] + >>> list(value_chain([1, 2, 3, 4, 5], 6)) + [1, 2, 3, 4, 5, 6] + + Multiple levels of nesting are not flattened. + + """ + for value in args: + if isinstance(value, (str, bytes)): + yield value + continue + try: + yield from value + except TypeError: + yield value + + +def product_index(element, *args): + """Equivalent to ``list(product(*args)).index(element)`` + + The products of *args* can be ordered lexicographically. + :func:`product_index` computes the first index of *element* without + computing the previous products. + + >>> product_index([8, 2], range(10), range(5)) + 42 + + ``ValueError`` will be raised if the given *element* isn't in the product + of *args*. + """ + index = 0 + + for x, pool in zip_longest(element, args, fillvalue=_marker): + if x is _marker or pool is _marker: + raise ValueError('element is not a product of args') + + pool = tuple(pool) + index = index * len(pool) + pool.index(x) + + return index + + +def combination_index(element, iterable): + """Equivalent to ``list(combinations(iterable, r)).index(element)`` + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`combination_index` computes the index of the + first *element*, without computing the previous combinations. + + >>> combination_index('adf', 'abcdefg') + 10 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations of *iterable*. + """ + element = enumerate(element) + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = enumerate(iterable) + for n, x in pool: + if x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + else: + raise ValueError('element is not a combination of iterable') + + n, _ = last(pool, default=(n, None)) + + # Python versions below 3.8 don't have math.comb + index = 1 + for i, j in enumerate(reversed(indexes), start=1): + j = n - j + if i <= j: + index += comb(j, i) + + return comb(n + 1, k + 1) - index + + +def combination_with_replacement_index(element, iterable): + """Equivalent to + ``list(combinations_with_replacement(iterable, r)).index(element)`` + + The subsequences with repetition of *iterable* that are of length *r* can + be ordered lexicographically. :func:`combination_with_replacement_index` + computes the index of the first *element*, without computing the previous + combinations with replacement. + + >>> combination_with_replacement_index('adf', 'abcdefg') + 20 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations with replacement of *iterable*. + """ + element = tuple(element) + l = len(element) + element = enumerate(element) + + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = tuple(iterable) + for n, x in enumerate(pool): + while x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + if y is None: + break + else: + raise ValueError( + 'element is not a combination with replacement of iterable' + ) + + n = len(pool) + occupations = [0] * n + for p in indexes: + occupations[p] += 1 + + index = 0 + cumulative_sum = 0 + for k in range(1, n): + cumulative_sum += occupations[k - 1] + j = l + n - 1 - k - cumulative_sum + i = n - k + if i <= j: + index += comb(j, i) + + return index + + +def permutation_index(element, iterable): + """Equivalent to ``list(permutations(iterable, r)).index(element)``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`permutation_index` + computes the index of the first *element* directly, without computing + the previous permutations. + + >>> permutation_index([1, 3, 2], range(5)) + 19 + + ``ValueError`` will be raised if the given *element* isn't one of the + permutations of *iterable*. + """ + index = 0 + pool = list(iterable) + for i, x in zip(range(len(pool), -1, -1), element): + r = pool.index(x) + index = index * i + r + del pool[r] + + return index + + +class countable: + """Wrap *iterable* and keep a count of how many items have been consumed. + + The ``items_seen`` attribute starts at ``0`` and increments as the iterable + is consumed: + + >>> iterable = map(str, range(10)) + >>> it = countable(iterable) + >>> it.items_seen + 0 + >>> next(it), next(it) + ('0', '1') + >>> list(it) + ['2', '3', '4', '5', '6', '7', '8', '9'] + >>> it.items_seen + 10 + """ + + def __init__(self, iterable): + self._iterator = iter(iterable) + self.items_seen = 0 + + def __iter__(self): + return self + + def __next__(self): + item = next(self._iterator) + self.items_seen += 1 + + return item + + +def chunked_even(iterable, n): + """Break *iterable* into lists of approximately length *n*. + Items are distributed such the lengths of the lists differ by at most + 1 item. + + >>> iterable = [1, 2, 3, 4, 5, 6, 7] + >>> n = 3 + >>> list(chunked_even(iterable, n)) # List lengths: 3, 2, 2 + [[1, 2, 3], [4, 5], [6, 7]] + >>> list(chunked(iterable, n)) # List lengths: 3, 3, 1 + [[1, 2, 3], [4, 5, 6], [7]] + + """ + iterator = iter(iterable) + + # Initialize a buffer to process the chunks while keeping + # some back to fill any underfilled chunks + min_buffer = (n - 1) * (n - 2) + buffer = list(islice(iterator, min_buffer)) + + # Append items until we have a completed chunk + for _ in islice(map(buffer.append, iterator), n, None, n): + yield buffer[:n] + del buffer[:n] + + # Check if any chunks need addition processing + if not buffer: + return + length = len(buffer) + + # Chunks are either size `full_size <= n` or `partial_size = full_size - 1` + q, r = divmod(length, n) + num_lists = q + (1 if r > 0 else 0) + q, r = divmod(length, num_lists) + full_size = q + (1 if r > 0 else 0) + partial_size = full_size - 1 + num_full = length - partial_size * num_lists + + # Yield chunks of full size + partial_start_idx = num_full * full_size + if full_size > 0: + for i in range(0, partial_start_idx, full_size): + yield buffer[i : i + full_size] + + # Yield chunks of partial size + if partial_size > 0: + for i in range(partial_start_idx, length, partial_size): + yield buffer[i : i + partial_size] + + +def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): + """A version of :func:`zip` that "broadcasts" any scalar + (i.e., non-iterable) items into output tuples. + + >>> iterable_1 = [1, 2, 3] + >>> iterable_2 = ['a', 'b', 'c'] + >>> scalar = '_' + >>> list(zip_broadcast(iterable_1, iterable_2, scalar)) + [(1, 'a', '_'), (2, 'b', '_'), (3, 'c', '_')] + + The *scalar_types* keyword argument determines what types are considered + scalar. It is set to ``(str, bytes)`` by default. Set it to ``None`` to + treat strings and byte strings as iterable: + + >>> list(zip_broadcast('abc', 0, 'xyz', scalar_types=None)) + [('a', 0, 'x'), ('b', 0, 'y'), ('c', 0, 'z')] + + If the *strict* keyword argument is ``True``, then + ``UnequalIterablesError`` will be raised if any of the iterables have + different lengths. + """ + + def is_scalar(obj): + if scalar_types and isinstance(obj, scalar_types): + return True + try: + iter(obj) + except TypeError: + return True + else: + return False + + size = len(objects) + if not size: + return + + new_item = [None] * size + iterables, iterable_positions = [], [] + for i, obj in enumerate(objects): + if is_scalar(obj): + new_item[i] = obj + else: + iterables.append(iter(obj)) + iterable_positions.append(i) + + if not iterables: + yield tuple(objects) + return + + zipper = _zip_equal if strict else zip + for item in zipper(*iterables): + for i, new_item[i] in zip(iterable_positions, item): + pass + yield tuple(new_item) + + +def unique_in_window(iterable, n, key=None): + """Yield the items from *iterable* that haven't been seen recently. + *n* is the size of the lookback window. + + >>> iterable = [0, 1, 0, 2, 3, 0] + >>> n = 3 + >>> list(unique_in_window(iterable, n)) + [0, 1, 2, 3, 0] + + The *key* function, if provided, will be used to determine uniqueness: + + >>> list(unique_in_window('abAcda', 3, key=lambda x: x.lower())) + ['a', 'b', 'c', 'd', 'a'] + + The items in *iterable* must be hashable. + + """ + if n <= 0: + raise ValueError('n must be greater than 0') + + window = deque(maxlen=n) + counts = defaultdict(int) + use_key = key is not None + + for item in iterable: + if len(window) == n: + to_discard = window[0] + if counts[to_discard] == 1: + del counts[to_discard] + else: + counts[to_discard] -= 1 + + k = key(item) if use_key else item + if k not in counts: + yield item + counts[k] += 1 + window.append(k) + + +def duplicates_everseen(iterable, key=None): + """Yield duplicate elements after their first appearance. + + >>> list(duplicates_everseen('mississippi')) + ['s', 'i', 's', 's', 'i', 'p', 'i'] + >>> list(duplicates_everseen('AaaBbbCccAaa', str.lower)) + ['a', 'a', 'b', 'b', 'c', 'c', 'A', 'a', 'a'] + + This function is analogous to :func:`unique_everseen` and is subject to + the same performance considerations. + + """ + seen_set = set() + seen_list = [] + use_key = key is not None + + for element in iterable: + k = key(element) if use_key else element + try: + if k not in seen_set: + seen_set.add(k) + else: + yield element + except TypeError: + if k not in seen_list: + seen_list.append(k) + else: + yield element + + +def duplicates_justseen(iterable, key=None): + """Yields serially-duplicate elements after their first appearance. + + >>> list(duplicates_justseen('mississippi')) + ['s', 's', 'p'] + >>> list(duplicates_justseen('AaaBbbCccAaa', str.lower)) + ['a', 'a', 'b', 'b', 'c', 'c', 'a', 'a'] + + This function is analogous to :func:`unique_justseen`. + + """ + return flatten(g for _, g in groupby(iterable, key) for _ in g) + + +def classify_unique(iterable, key=None): + """Classify each element in terms of its uniqueness. + + For each element in the input iterable, return a 3-tuple consisting of: + + 1. The element itself + 2. ``False`` if the element is equal to the one preceding it in the input, + ``True`` otherwise (i.e. the equivalent of :func:`unique_justseen`) + 3. ``False`` if this element has been seen anywhere in the input before, + ``True`` otherwise (i.e. the equivalent of :func:`unique_everseen`) + + >>> list(classify_unique('otto')) # doctest: +NORMALIZE_WHITESPACE + [('o', True, True), + ('t', True, True), + ('t', False, False), + ('o', True, False)] + + This function is analogous to :func:`unique_everseen` and is subject to + the same performance considerations. + + """ + seen_set = set() + seen_list = [] + use_key = key is not None + previous = None + + for i, element in enumerate(iterable): + k = key(element) if use_key else element + is_unique_justseen = not i or previous != k + previous = k + is_unique_everseen = False + try: + if k not in seen_set: + seen_set.add(k) + is_unique_everseen = True + except TypeError: + if k not in seen_list: + seen_list.append(k) + is_unique_everseen = True + yield element, is_unique_justseen, is_unique_everseen + + +def minmax(iterable_or_value, *others, key=None, default=_marker): + """Returns both the smallest and largest items from an iterable + or from two or more arguments. + + >>> minmax([3, 1, 5]) + (1, 5) + + >>> minmax(4, 2, 6) + (2, 6) + + If a *key* function is provided, it will be used to transform the input + items for comparison. + + >>> minmax([5, 30], key=str) # '30' sorts before '5' + (30, 5) + + If a *default* value is provided, it will be returned if there are no + input items. + + >>> minmax([], default=(0, 0)) + (0, 0) + + Otherwise ``ValueError`` is raised. + + This function makes a single pass over the input elements and takes care to + minimize the number of comparisons made during processing. + + Note that unlike the builtin ``max`` function, which always returns the first + item with the maximum value, this function may return another item when there are + ties. + + This function is based on the + `recipe `__ by + Raymond Hettinger. + """ + iterable = (iterable_or_value, *others) if others else iterable_or_value + + it = iter(iterable) + + try: + lo = hi = next(it) + except StopIteration as exc: + if default is _marker: + raise ValueError( + '`minmax()` argument is an empty iterable. ' + 'Provide a `default` value to suppress this error.' + ) from exc + return default + + # Different branches depending on the presence of key. This saves a lot + # of unimportant copies which would slow the "key=None" branch + # significantly down. + if key is None: + for x, y in zip_longest(it, it, fillvalue=lo): + if y < x: + x, y = y, x + if x < lo: + lo = x + if hi < y: + hi = y + + else: + lo_key = hi_key = key(lo) + + for x, y in zip_longest(it, it, fillvalue=lo): + x_key, y_key = key(x), key(y) + + if y_key < x_key: + x, y, x_key, y_key = y, x, y_key, x_key + if x_key < lo_key: + lo, lo_key = x, x_key + if hi_key < y_key: + hi, hi_key = y, y_key + + return lo, hi + + +def constrained_batches( + iterable, max_size, max_count=None, get_len=len, strict=True +): + """Yield batches of items from *iterable* with a combined size limited by + *max_size*. + + >>> iterable = [b'12345', b'123', b'12345678', b'1', b'1', b'12', b'1'] + >>> list(constrained_batches(iterable, 10)) + [(b'12345', b'123'), (b'12345678', b'1', b'1'), (b'12', b'1')] + + If a *max_count* is supplied, the number of items per batch is also + limited: + + >>> iterable = [b'12345', b'123', b'12345678', b'1', b'1', b'12', b'1'] + >>> list(constrained_batches(iterable, 10, max_count = 2)) + [(b'12345', b'123'), (b'12345678', b'1'), (b'1', b'12'), (b'1',)] + + If a *get_len* function is supplied, use that instead of :func:`len` to + determine item size. + + If *strict* is ``True``, raise ``ValueError`` if any single item is bigger + than *max_size*. Otherwise, allow single items to exceed *max_size*. + """ + if max_size <= 0: + raise ValueError('maximum size must be greater than zero') + + batch = [] + batch_size = 0 + batch_count = 0 + for item in iterable: + item_len = get_len(item) + if strict and item_len > max_size: + raise ValueError('item size exceeds maximum size') + + reached_count = batch_count == max_count + reached_size = item_len + batch_size > max_size + if batch_count and (reached_size or reached_count): + yield tuple(batch) + batch.clear() + batch_size = 0 + batch_count = 0 + + batch.append(item) + batch_size += item_len + batch_count += 1 + + if batch: + yield tuple(batch) + + +def gray_product(*iterables): + """Like :func:`itertools.product`, but return tuples in an order such + that only one element in the generated tuple changes from one iteration + to the next. + + >>> list(gray_product('AB','CD')) + [('A', 'C'), ('B', 'C'), ('B', 'D'), ('A', 'D')] + + This function consumes all of the input iterables before producing output. + If any of the input iterables have fewer than two items, ``ValueError`` + is raised. + + For information on the algorithm, see + `this section `__ + of Donald Knuth's *The Art of Computer Programming*. + """ + all_iterables = tuple(tuple(x) for x in iterables) + iterable_count = len(all_iterables) + for iterable in all_iterables: + if len(iterable) < 2: + raise ValueError("each iterable must have two or more items") + + # This is based on "Algorithm H" from section 7.2.1.1, page 20. + # a holds the indexes of the source iterables for the n-tuple to be yielded + # f is the array of "focus pointers" + # o is the array of "directions" + a = [0] * iterable_count + f = list(range(iterable_count + 1)) + o = [1] * iterable_count + while True: + yield tuple(all_iterables[i][a[i]] for i in range(iterable_count)) + j = f[0] + f[0] = 0 + if j == iterable_count: + break + a[j] = a[j] + o[j] + if a[j] == 0 or a[j] == len(all_iterables[j]) - 1: + o[j] = -o[j] + f[j] = f[j + 1] + f[j + 1] = j + 1 + + +def partial_product(*iterables): + """Yields tuples containing one item from each iterator, with subsequent + tuples changing a single item at a time by advancing each iterator until it + is exhausted. This sequence guarantees every value in each iterable is + output at least once without generating all possible combinations. + + This may be useful, for example, when testing an expensive function. + + >>> list(partial_product('AB', 'C', 'DEF')) + [('A', 'C', 'D'), ('B', 'C', 'D'), ('B', 'C', 'E'), ('B', 'C', 'F')] + """ + + iterators = list(map(iter, iterables)) + + try: + prod = [next(it) for it in iterators] + except StopIteration: + return + yield tuple(prod) + + for i, it in enumerate(iterators): + for prod[i] in it: + yield tuple(prod) + + +def takewhile_inclusive(predicate, iterable): + """A variant of :func:`takewhile` that yields one additional element. + + >>> list(takewhile_inclusive(lambda x: x < 5, [1, 4, 6, 4, 1])) + [1, 4, 6] + + :func:`takewhile` would return ``[1, 4]``. + """ + for x in iterable: + yield x + if not predicate(x): + break + + +def outer_product(func, xs, ys, *args, **kwargs): + """A generalized outer product that applies a binary function to all + pairs of items. Returns a 2D matrix with ``len(xs)`` rows and ``len(ys)`` + columns. + Also accepts ``*args`` and ``**kwargs`` that are passed to ``func``. + + Multiplication table: + + >>> list(outer_product(mul, range(1, 4), range(1, 6))) + [(1, 2, 3, 4, 5), (2, 4, 6, 8, 10), (3, 6, 9, 12, 15)] + + Cross tabulation: + + >>> xs = ['A', 'B', 'A', 'A', 'B', 'B', 'A', 'A', 'B', 'B'] + >>> ys = ['X', 'X', 'X', 'Y', 'Z', 'Z', 'Y', 'Y', 'Z', 'Z'] + >>> pair_counts = Counter(zip(xs, ys)) + >>> count_rows = lambda x, y: pair_counts[x, y] + >>> list(outer_product(count_rows, sorted(set(xs)), sorted(set(ys)))) + [(2, 3, 0), (1, 0, 4)] + + Usage with ``*args`` and ``**kwargs``: + + >>> animals = ['cat', 'wolf', 'mouse'] + >>> list(outer_product(min, animals, animals, key=len)) + [('cat', 'cat', 'cat'), ('cat', 'wolf', 'wolf'), ('cat', 'wolf', 'mouse')] + """ + ys = tuple(ys) + return batched( + starmap(lambda x, y: func(x, y, *args, **kwargs), product(xs, ys)), + n=len(ys), + ) + + +def iter_suppress(iterable, *exceptions): + """Yield each of the items from *iterable*. If the iteration raises one of + the specified *exceptions*, that exception will be suppressed and iteration + will stop. + + >>> from itertools import chain + >>> def breaks_at_five(x): + ... while True: + ... if x >= 5: + ... raise RuntimeError + ... yield x + ... x += 1 + >>> it_1 = iter_suppress(breaks_at_five(1), RuntimeError) + >>> it_2 = iter_suppress(breaks_at_five(2), RuntimeError) + >>> list(chain(it_1, it_2)) + [1, 2, 3, 4, 2, 3, 4] + """ + try: + yield from iterable + except exceptions: + return + + +def filter_map(func, iterable): + """Apply *func* to every element of *iterable*, yielding only those which + are not ``None``. + + >>> elems = ['1', 'a', '2', 'b', '3'] + >>> list(filter_map(lambda s: int(s) if s.isnumeric() else None, elems)) + [1, 2, 3] + """ + for x in iterable: + y = func(x) + if y is not None: + yield y + + +def powerset_of_sets(iterable): + """Yields all possible subsets of the iterable. + + >>> list(powerset_of_sets([1, 2, 3])) # doctest: +SKIP + [set(), {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}] + >>> list(powerset_of_sets([1, 1, 0])) # doctest: +SKIP + [set(), {1}, {0}, {0, 1}] + + :func:`powerset_of_sets` takes care to minimize the number + of hash operations performed. + """ + sets = tuple(dict.fromkeys(map(frozenset, zip(iterable)))) + return chain.from_iterable( + starmap(set().union, combinations(sets, r)) + for r in range(len(sets) + 1) + ) + + +def join_mappings(**field_to_map): + """ + Joins multiple mappings together using their common keys. + + >>> user_scores = {'elliot': 50, 'claris': 60} + >>> user_times = {'elliot': 30, 'claris': 40} + >>> join_mappings(score=user_scores, time=user_times) + {'elliot': {'score': 50, 'time': 30}, 'claris': {'score': 60, 'time': 40}} + """ + ret = defaultdict(dict) + + for field_name, mapping in field_to_map.items(): + for key, value in mapping.items(): + ret[key][field_name] = value + + return dict(ret) + + +def _complex_sumprod(v1, v2): + """High precision sumprod() for complex numbers. + Used by :func:`dft` and :func:`idft`. + """ + + real = attrgetter('real') + imag = attrgetter('imag') + r1 = chain(map(real, v1), map(neg, map(imag, v1))) + r2 = chain(map(real, v2), map(imag, v2)) + i1 = chain(map(real, v1), map(imag, v1)) + i2 = chain(map(imag, v2), map(real, v2)) + return complex(_fsumprod(r1, r2), _fsumprod(i1, i2)) + + +def dft(xarr): + """Discrete Fourier Transform. *xarr* is a sequence of complex numbers. + Yields the components of the corresponding transformed output vector. + + >>> import cmath + >>> xarr = [1, 2-1j, -1j, -1+2j] # time domain + >>> Xarr = [2, -2-2j, -2j, 4+4j] # frequency domain + >>> magnitudes, phases = zip(*map(cmath.polar, Xarr)) + >>> all(map(cmath.isclose, dft(xarr), Xarr)) + True + + Inputs are restricted to numeric types that can add and multiply + with a complex number. This includes int, float, complex, and + Fraction, but excludes Decimal. + + See :func:`idft` for the inverse Discrete Fourier Transform. + """ + N = len(xarr) + roots_of_unity = [e ** (n / N * tau * -1j) for n in range(N)] + for k in range(N): + coeffs = [roots_of_unity[k * n % N] for n in range(N)] + yield _complex_sumprod(xarr, coeffs) + + +def idft(Xarr): + """Inverse Discrete Fourier Transform. *Xarr* is a sequence of + complex numbers. Yields the components of the corresponding + inverse-transformed output vector. + + >>> import cmath + >>> xarr = [1, 2-1j, -1j, -1+2j] # time domain + >>> Xarr = [2, -2-2j, -2j, 4+4j] # frequency domain + >>> all(map(cmath.isclose, idft(Xarr), xarr)) + True + + Inputs are restricted to numeric types that can add and multiply + with a complex number. This includes int, float, complex, and + Fraction, but excludes Decimal. + + See :func:`dft` for the Discrete Fourier Transform. + """ + N = len(Xarr) + roots_of_unity = [e ** (n / N * tau * 1j) for n in range(N)] + for k in range(N): + coeffs = [roots_of_unity[k * n % N] for n in range(N)] + yield _complex_sumprod(Xarr, coeffs) / N + + +def doublestarmap(func, iterable): + """Apply *func* to every item of *iterable* by dictionary unpacking + the item into *func*. + + The difference between :func:`itertools.starmap` and :func:`doublestarmap` + parallels the distinction between ``func(*a)`` and ``func(**a)``. + + >>> iterable = [{'a': 1, 'b': 2}, {'a': 40, 'b': 60}] + >>> list(doublestarmap(lambda a, b: a + b, iterable)) + [3, 100] + + ``TypeError`` will be raised if *func*'s signature doesn't match the + mapping contained in *iterable* or if *iterable* does not contain mappings. + """ + for item in iterable: + yield func(**item) + + +def _nth_prime_bounds(n): + """Bounds for the nth prime (counting from 1): lb < p_n < ub.""" + # At and above 688,383, the lb/ub spread is under 0.003 * p_n. + + if n < 1: + raise ValueError + + if n < 6: + return (n, 2.25 * n) + + # https://en.wikipedia.org/wiki/Prime-counting_function#Inequalities + upper_bound = n * log(n * log(n)) + lower_bound = upper_bound - n + if n >= 688_383: + upper_bound -= n * (1.0 - (log(log(n)) - 2.0) / log(n)) + + return lower_bound, upper_bound + + +def nth_prime(n, *, approximate=False): + """Return the nth prime (counting from 0). + + >>> nth_prime(0) + 2 + >>> nth_prime(100) + 547 + + If *approximate* is set to True, will return a prime close + to the nth prime. The estimation is much faster than computing + an exact result. + + >>> nth_prime(200_000_000, approximate=True) # Exact result is 4222234763 + 4217820427 + + """ + lb, ub = _nth_prime_bounds(n + 1) + + if not approximate or n <= 1_000_000: + return nth(sieve(ceil(ub)), n) + + # Search from the midpoint and return the first odd prime + odd = floor((lb + ub) / 2) | 1 + return first_true(count(odd, step=2), pred=is_prime) + + +def argmin(iterable, *, key=None): + """ + Index of the first occurrence of a minimum value in an iterable. + + >>> argmin('efghabcdijkl') + 4 + >>> argmin([3, 2, 1, 0, 4, 2, 1, 0]) + 3 + + For example, look up a label corresponding to the position + of a value that minimizes a cost function:: + + >>> def cost(x): + ... "Days for a wound to heal given a subject's age." + ... return x**2 - 20*x + 150 + ... + >>> labels = ['homer', 'marge', 'bart', 'lisa', 'maggie'] + >>> ages = [ 35, 30, 10, 9, 1 ] + + # Fastest healing family member + >>> labels[argmin(ages, key=cost)] + 'bart' + + # Age with fastest healing + >>> min(ages, key=cost) + 10 + + """ + if key is not None: + iterable = map(key, iterable) + return min(enumerate(iterable), key=itemgetter(1))[0] + + +def argmax(iterable, *, key=None): + """ + Index of the first occurrence of a maximum value in an iterable. + + >>> argmax('abcdefghabcd') + 7 + >>> argmax([0, 1, 2, 3, 3, 2, 1, 0]) + 3 + + For example, identify the best machine learning model:: + + >>> models = ['svm', 'random forest', 'knn', 'naïve bayes'] + >>> accuracy = [ 68, 61, 84, 72 ] + + # Most accurate model + >>> models[argmax(accuracy)] + 'knn' + + # Best accuracy + >>> max(accuracy) + 84 + + """ + if key is not None: + iterable = map(key, iterable) + return max(enumerate(iterable), key=itemgetter(1))[0] + + +def extract(iterable, indices): + """Yield values at the specified indices. + + Example: + + >>> data = 'abcdefghijklmnopqrstuvwxyz' + >>> list(extract(data, [7, 4, 11, 11, 14])) + ['h', 'e', 'l', 'l', 'o'] + + The *iterable* is consumed lazily and can be infinite. + The *indices* are consumed immediately and must be finite. + + Raises ``IndexError`` if an index lies beyond the iterable. + Raises ``ValueError`` for negative indices. + """ + + iterator = iter(iterable) + index_and_position = sorted(zip(indices, count())) + + if index_and_position and index_and_position[0][0] < 0: + raise ValueError('Indices must be non-negative') + + buffer = {} + iterator_position = -1 + next_to_emit = 0 + + for index, order in index_and_position: + advance = index - iterator_position + if advance: + try: + value = next(islice(iterator, advance - 1, None)) + except StopIteration: + raise IndexError(index) + iterator_position = index + + buffer[order] = value + + while next_to_emit in buffer: + yield buffer.pop(next_to_emit) + next_to_emit += 1 diff --git a/lib/more_itertools/more.pyi b/lib/more_itertools/more.pyi new file mode 100644 index 0000000..b5e33f8 --- /dev/null +++ b/lib/more_itertools/more.pyi @@ -0,0 +1,949 @@ +"""Stubs for more_itertools.more""" + +from __future__ import annotations + +import sys +import types + +from collections.abc import ( + Container, + Hashable, + Iterable, + Iterator, + Mapping, + Reversible, + Sequence, + Sized, +) +from contextlib import AbstractContextManager +from typing import ( + Any, + Callable, + Generic, + TypeVar, + overload, + type_check_only, +) +from typing_extensions import Protocol + +__all__ = [ + 'AbortThread', + 'SequenceView', + 'UnequalIterablesError', + 'adjacent', + 'all_unique', + 'always_iterable', + 'always_reversible', + 'argmax', + 'argmin', + 'bucket', + 'callback_iter', + 'chunked', + 'chunked_even', + 'circular_shifts', + 'collapse', + 'combination_index', + 'combination_with_replacement_index', + 'consecutive_groups', + 'constrained_batches', + 'consumer', + 'count_cycle', + 'countable', + 'derangements', + 'dft', + 'difference', + 'distinct_combinations', + 'distinct_permutations', + 'distribute', + 'divide', + 'doublestarmap', + 'duplicates_everseen', + 'duplicates_justseen', + 'classify_unique', + 'exactly_n', + 'extract', + 'filter_except', + 'filter_map', + 'first', + 'gray_product', + 'groupby_transform', + 'ichunked', + 'iequals', + 'idft', + 'ilen', + 'interleave', + 'interleave_evenly', + 'interleave_longest', + 'interleave_randomly', + 'intersperse', + 'is_sorted', + 'islice_extended', + 'iterate', + 'iter_suppress', + 'join_mappings', + 'last', + 'locate', + 'longest_common_prefix', + 'lstrip', + 'make_decorator', + 'map_except', + 'map_if', + 'map_reduce', + 'mark_ends', + 'minmax', + 'nth_or_last', + 'nth_permutation', + 'nth_prime', + 'nth_product', + 'nth_combination_with_replacement', + 'numeric_range', + 'one', + 'only', + 'outer_product', + 'padded', + 'partial_product', + 'partitions', + 'peekable', + 'permutation_index', + 'powerset_of_sets', + 'product_index', + 'raise_', + 'repeat_each', + 'repeat_last', + 'replace', + 'rlocate', + 'rstrip', + 'run_length', + 'sample', + 'seekable', + 'set_partitions', + 'side_effect', + 'sliced', + 'sort_together', + 'split_after', + 'split_at', + 'split_before', + 'split_into', + 'split_when', + 'spy', + 'stagger', + 'strip', + 'strictly_n', + 'substrings', + 'substrings_indexes', + 'takewhile_inclusive', + 'time_limited', + 'unique_in_window', + 'unique_to_each', + 'unzip', + 'value_chain', + 'windowed', + 'windowed_complete', + 'with_iter', + 'zip_broadcast', + 'zip_equal', + 'zip_offset', +] + +# Type and type variable definitions +_T = TypeVar('_T') +_T1 = TypeVar('_T1') +_T2 = TypeVar('_T2') +_T3 = TypeVar('_T3') +_T4 = TypeVar('_T4') +_T5 = TypeVar('_T5') +_U = TypeVar('_U') +_V = TypeVar('_V') +_W = TypeVar('_W') +_T_co = TypeVar('_T_co', covariant=True) +_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[Any]]) +_Raisable = BaseException | type[BaseException] + +# The type of isinstance's second argument (from typeshed builtins) +if sys.version_info >= (3, 10): + _ClassInfo = type | types.UnionType | tuple[_ClassInfo, ...] +else: + _ClassInfo = type | tuple[_ClassInfo, ...] + +@type_check_only +class _SizedIterable(Protocol[_T_co], Sized, Iterable[_T_co]): ... + +@type_check_only +class _SizedReversible(Protocol[_T_co], Sized, Reversible[_T_co]): ... + +@type_check_only +class _SupportsSlicing(Protocol[_T_co]): + def __getitem__(self, __k: slice) -> _T_co: ... + +def chunked( + iterable: Iterable[_T], n: int | None, strict: bool = ... +) -> Iterator[list[_T]]: ... +@overload +def first(iterable: Iterable[_T]) -> _T: ... +@overload +def first(iterable: Iterable[_T], default: _U) -> _T | _U: ... +@overload +def last(iterable: Iterable[_T]) -> _T: ... +@overload +def last(iterable: Iterable[_T], default: _U) -> _T | _U: ... +@overload +def nth_or_last(iterable: Iterable[_T], n: int) -> _T: ... +@overload +def nth_or_last(iterable: Iterable[_T], n: int, default: _U) -> _T | _U: ... + +class peekable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> peekable[_T]: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> _T | _U: ... + def prepend(self, *items: _T) -> None: ... + def __next__(self) -> _T: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> list[_T]: ... + +def consumer(func: _GenFn) -> _GenFn: ... +def ilen(iterable: Iterable[_T]) -> int: ... +def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... +def with_iter( + context_manager: AbstractContextManager[Iterable[_T]], +) -> Iterator[_T]: ... +def one( + iterable: Iterable[_T], + too_short: _Raisable | None = ..., + too_long: _Raisable | None = ..., +) -> _T: ... +def raise_(exception: _Raisable, *args: Any) -> None: ... +def strictly_n( + iterable: Iterable[_T], + n: int, + too_short: _GenFn | None = ..., + too_long: _GenFn | None = ..., +) -> list[_T]: ... +def distinct_permutations( + iterable: Iterable[_T], r: int | None = ... +) -> Iterator[tuple[_T, ...]]: ... +def derangements( + iterable: Iterable[_T], r: int | None = None +) -> Iterator[tuple[_T, ...]]: ... +def intersperse( + e: _U, iterable: Iterable[_T], n: int = ... +) -> Iterator[_T | _U]: ... +def unique_to_each(*iterables: Iterable[_T]) -> list[list[_T]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, *, step: int = ... +) -> Iterator[tuple[_T | None, ...]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, fillvalue: _U, step: int = ... +) -> Iterator[tuple[_T | _U, ...]]: ... +def substrings(iterable: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def substrings_indexes( + seq: Sequence[_T], reverse: bool = ... +) -> Iterator[tuple[Sequence[_T], int, int]]: ... + +class bucket(Generic[_T, _U], Container[_U]): + def __init__( + self, + iterable: Iterable[_T], + key: Callable[[_T], _U], + validator: Callable[[_U], object] | None = ..., + ) -> None: ... + def __contains__(self, value: object) -> bool: ... + def __iter__(self) -> Iterator[_U]: ... + def __getitem__(self, value: object) -> Iterator[_T]: ... + +def spy( + iterable: Iterable[_T], n: int = ... +) -> tuple[list[_T], Iterator[_T]]: ... +def interleave(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_longest(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_evenly( + iterables: list[Iterable[_T]], lengths: list[int] | None = ... +) -> Iterator[_T]: ... +def interleave_randomly(*iterables: Iterable[_T]) -> Iterable[_T]: ... +def collapse( + iterable: Iterable[Any], + base_type: _ClassInfo | None = ..., + levels: int | None = ..., +) -> Iterator[Any]: ... +@overload +def side_effect( + func: Callable[[_T], object], + iterable: Iterable[_T], + chunk_size: None = ..., + before: Callable[[], object] | None = ..., + after: Callable[[], object] | None = ..., +) -> Iterator[_T]: ... +@overload +def side_effect( + func: Callable[[list[_T]], object], + iterable: Iterable[_T], + chunk_size: int, + before: Callable[[], object] | None = ..., + after: Callable[[], object] | None = ..., +) -> Iterator[_T]: ... +def sliced( + seq: _SupportsSlicing[_T], n: int, strict: bool = ... +) -> Iterator[_T]: ... +def split_at( + iterable: Iterable[_T], + pred: Callable[[_T], object], + maxsplit: int = ..., + keep_separator: bool = ..., +) -> Iterator[list[_T]]: ... +def split_before( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[list[_T]]: ... +def split_after( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[list[_T]]: ... +def split_when( + iterable: Iterable[_T], + pred: Callable[[_T, _T], object], + maxsplit: int = ..., +) -> Iterator[list[_T]]: ... +def split_into( + iterable: Iterable[_T], sizes: Iterable[int | None] +) -> Iterator[list[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + *, + n: int | None = ..., + next_multiple: bool = ..., +) -> Iterator[_T | None]: ... +@overload +def padded( + iterable: Iterable[_T], + fillvalue: _U, + n: int | None = ..., + next_multiple: bool = ..., +) -> Iterator[_T | _U]: ... +@overload +def repeat_last(iterable: Iterable[_T]) -> Iterator[_T]: ... +@overload +def repeat_last(iterable: Iterable[_T], default: _U) -> Iterator[_T | _U]: ... +def distribute(n: int, iterable: Iterable[_T]) -> list[Iterator[_T]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., +) -> Iterator[tuple[_T | None, ...]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., + fillvalue: _U = ..., +) -> Iterator[tuple[_T | _U, ...]]: ... + +class UnequalIterablesError(ValueError): + def __init__(self, details: tuple[int, int, int] | None = ...) -> None: ... + +# zip_equal +@overload +def zip_equal(__iter1: Iterable[_T1]) -> Iterator[tuple[_T1]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T1], __iter2: Iterable[_T2] +) -> Iterator[tuple[_T1, _T2]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T1], __iter2: Iterable[_T2], __iter3: Iterable[_T3] +) -> Iterator[tuple[_T1, _T2, _T3]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + __iter3: Iterable[_T3], + __iter4: Iterable[_T4], +) -> Iterator[tuple[_T1, _T2, _T3, _T4]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + __iter3: Iterable[_T3], + __iter4: Iterable[_T4], + __iter5: Iterable[_T5], +) -> Iterator[tuple[_T1, _T2, _T3, _T4, _T5]]: ... +@overload +def zip_equal( + __iter1: Iterable[Any], + __iter2: Iterable[Any], + __iter3: Iterable[Any], + __iter4: Iterable[Any], + __iter5: Iterable[Any], + __iter6: Iterable[Any], + *iterables: Iterable[Any], +) -> Iterator[tuple[Any, ...]]: ... + +# zip_offset +@overload +def zip_offset( + __iter1: Iterable[_T1], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None, +) -> Iterator[tuple[_T1 | None]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None, +) -> Iterator[tuple[_T1 | None, _T2 | None]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None, +) -> Iterator[tuple[_T | None, ...]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[tuple[_T1 | _U]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[tuple[_T1 | _U, _T2 | _U]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[tuple[_T | _U, ...]]: ... +def sort_together( + iterables: Iterable[Iterable[_T]], + key_list: Iterable[int] = ..., + key: Callable[..., Any] | None = ..., + reverse: bool = ..., + strict: bool = ..., +) -> list[tuple[_T, ...]]: ... +def unzip(iterable: Iterable[Sequence[_T]]) -> tuple[Iterator[_T], ...]: ... +def divide(n: int, iterable: Iterable[_T]) -> list[Iterator[_T]]: ... +def always_iterable( + obj: object, + base_type: _ClassInfo | None = ..., +) -> Iterator[Any]: ... +def adjacent( + predicate: Callable[[_T], bool], + iterable: Iterable[_T], + distance: int = ..., +) -> Iterator[tuple[bool, _T]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None = None, + valuefunc: None = None, + reducefunc: None = None, +) -> Iterator[tuple[_T, Iterator[_T]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None, + reducefunc: None, +) -> Iterator[tuple[_U, Iterator[_T]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: Callable[[_T], _V], + reducefunc: None, +) -> Iterator[tuple[_T, Iterator[_V]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None, +) -> Iterator[tuple[_U, Iterator[_V]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: None, + reducefunc: Callable[[Iterator[_T]], _W], +) -> Iterator[tuple[_T, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None, + reducefunc: Callable[[Iterator[_T]], _W], +) -> Iterator[tuple[_U, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[Iterator[_V]], _W], +) -> Iterator[tuple[_T, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[Iterator[_V]], _W], +) -> Iterator[tuple[_U, _W]]: ... + +class numeric_range(Generic[_T, _U], Sequence[_T], Hashable, Reversible[_T]): + @overload + def __init__(self, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T, __step: _U) -> None: ... + def __bool__(self) -> bool: ... + def __contains__(self, elem: object) -> bool: ... + def __eq__(self, other: object) -> bool: ... + @overload + def __getitem__(self, key: int) -> _T: ... + @overload + def __getitem__(self, key: slice) -> numeric_range[_T, _U]: ... + def __hash__(self) -> int: ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def __reduce__( + self, + ) -> tuple[type[numeric_range[_T, _U]], tuple[_T, _T, _U]]: ... + def __repr__(self) -> str: ... + def __reversed__(self) -> Iterator[_T]: ... + def count(self, value: _T) -> int: ... + def index(self, value: _T) -> int: ... # type: ignore + +def count_cycle( + iterable: Iterable[_T], n: int | None = ... +) -> Iterable[tuple[int, _T]]: ... +def mark_ends( + iterable: Iterable[_T], +) -> Iterable[tuple[bool, bool, _T]]: ... +def locate( + iterable: Iterable[_T], + pred: Callable[..., Any] = ..., + window_size: int | None = ..., +) -> Iterator[int]: ... +def lstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def rstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def strip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... + +class islice_extended(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T], *args: int | None) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + def __getitem__(self, index: slice) -> islice_extended[_T]: ... + +def always_reversible(iterable: Iterable[_T]) -> Iterator[_T]: ... +def consecutive_groups( + iterable: Iterable[_T], ordering: None | Callable[[_T], int] = ... +) -> Iterator[Iterator[_T]]: ... +@overload +def difference( + iterable: Iterable[_T], + func: Callable[[_T, _T], _U] = ..., + *, + initial: None = ..., +) -> Iterator[_T | _U]: ... +@overload +def difference( + iterable: Iterable[_T], func: Callable[[_T, _T], _U] = ..., *, initial: _U +) -> Iterator[_U]: ... + +class SequenceView(Generic[_T], Sequence[_T]): + def __init__(self, target: Sequence[_T]) -> None: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[_T]: ... + def __len__(self) -> int: ... + +class seekable(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], maxlen: int | None = ... + ) -> None: ... + def __iter__(self) -> seekable[_T]: ... + def __next__(self) -> _T: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> _T | _U: ... + def elements(self) -> SequenceView[_T]: ... + def seek(self, index: int) -> None: ... + def relative_seek(self, count: int) -> None: ... + +class run_length: + @staticmethod + def encode(iterable: Iterable[_T]) -> Iterator[tuple[_T, int]]: ... + @staticmethod + def decode(iterable: Iterable[tuple[_T, int]]) -> Iterator[_T]: ... + +def exactly_n( + iterable: Iterable[_T], n: int, predicate: Callable[[_T], object] = ... +) -> bool: ... +def circular_shifts( + iterable: Iterable[_T], steps: int = 1 +) -> list[tuple[_T, ...]]: ... +def make_decorator( + wrapping_func: Callable[..., _U], result_index: int = ... +) -> Callable[..., Callable[[Callable[..., Any]], Callable[..., _U]]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: None = ..., +) -> dict[_U, list[_T]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None = ..., +) -> dict[_U, list[_V]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: Callable[[list[_T]], _W] = ..., +) -> dict[_U, _W]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[list[_V]], _W], +) -> dict[_U, _W]: ... +def rlocate( + iterable: Iterable[_T], + pred: Callable[..., object] = ..., + window_size: int | None = ..., +) -> Iterator[int]: ... +def replace( + iterable: Iterable[_T], + pred: Callable[..., object], + substitutes: Iterable[_U], + count: int | None = ..., + window_size: int = ..., +) -> Iterator[_T | _U]: ... +def partitions(iterable: Iterable[_T]) -> Iterator[list[list[_T]]]: ... +def set_partitions( + iterable: Iterable[_T], + k: int | None = ..., + min_size: int | None = ..., + max_size: int | None = ..., +) -> Iterator[list[list[_T]]]: ... + +class time_limited(Generic[_T], Iterator[_T]): + def __init__( + self, limit_seconds: float, iterable: Iterable[_T] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + +@overload +def only( + iterable: Iterable[_T], *, too_long: _Raisable | None = ... +) -> _T | None: ... +@overload +def only( + iterable: Iterable[_T], default: _U, too_long: _Raisable | None = ... +) -> _T | _U: ... +def ichunked(iterable: Iterable[_T], n: int) -> Iterator[Iterator[_T]]: ... +def distinct_combinations( + iterable: Iterable[_T], r: int +) -> Iterator[tuple[_T, ...]]: ... +def filter_except( + validator: Callable[[Any], object], + iterable: Iterable[_T], + *exceptions: type[BaseException], +) -> Iterator[_T]: ... +def map_except( + function: Callable[[Any], _U], + iterable: Iterable[_T], + *exceptions: type[BaseException], +) -> Iterator[_U]: ... +def map_if( + iterable: Iterable[Any], + pred: Callable[[Any], bool], + func: Callable[[Any], Any], + func_else: Callable[[Any], Any] | None = ..., +) -> Iterator[Any]: ... +def _sample_unweighted( + iterator: Iterator[_T], k: int, strict: bool +) -> list[_T]: ... +def _sample_counted( + population: Iterator[_T], k: int, counts: Iterable[int], strict: bool +) -> list[_T]: ... +def _sample_weighted( + iterator: Iterator[_T], k: int, weights: Iterator[float], strict: bool +) -> list[_T]: ... +def sample( + iterable: Iterable[_T], + k: int, + weights: Iterable[float] | None = ..., + *, + counts: Iterable[int] | None = ..., + strict: bool = False, +) -> list[_T]: ... +def is_sorted( + iterable: Iterable[_T], + key: Callable[[_T], _U] | None = ..., + reverse: bool = False, + strict: bool = False, +) -> bool: ... + +class AbortThread(BaseException): + pass + +class callback_iter(Generic[_T], Iterator[_T]): + def __init__( + self, + func: Callable[..., Any], + callback_kwd: str = ..., + wait_seconds: float = ..., + ) -> None: ... + def __enter__(self) -> callback_iter[_T]: ... + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: types.TracebackType | None, + ) -> bool | None: ... + def __iter__(self) -> callback_iter[_T]: ... + def __next__(self) -> _T: ... + def _reader(self) -> Iterator[_T]: ... + @property + def done(self) -> bool: ... + @property + def result(self) -> Any: ... + +def windowed_complete( + iterable: Iterable[_T], n: int +) -> Iterator[tuple[tuple[_T, ...], tuple[_T, ...], tuple[_T, ...]]]: ... +def all_unique( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> bool: ... +def nth_product(index: int, *args: Iterable[_T]) -> tuple[_T, ...]: ... +def nth_combination_with_replacement( + iterable: Iterable[_T], r: int, index: int +) -> tuple[_T, ...]: ... +def nth_permutation( + iterable: Iterable[_T], r: int, index: int +) -> tuple[_T, ...]: ... +def value_chain(*args: _T | Iterable[_T]) -> Iterable[_T]: ... +def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... +def combination_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def combination_with_replacement_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def permutation_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def repeat_each(iterable: Iterable[_T], n: int = ...) -> Iterator[_T]: ... + +class countable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> countable[_T]: ... + def __next__(self) -> _T: ... + items_seen: int + +def chunked_even(iterable: Iterable[_T], n: int) -> Iterator[list[_T]]: ... +@overload +def zip_broadcast( + __obj1: _T | Iterable[_T], + *, + scalar_types: _ClassInfo | None = ..., + strict: bool = ..., +) -> Iterable[tuple[_T, ...]]: ... +@overload +def zip_broadcast( + __obj1: _T | Iterable[_T], + __obj2: _T | Iterable[_T], + *, + scalar_types: _ClassInfo | None = ..., + strict: bool = ..., +) -> Iterable[tuple[_T, ...]]: ... +@overload +def zip_broadcast( + __obj1: _T | Iterable[_T], + __obj2: _T | Iterable[_T], + __obj3: _T | Iterable[_T], + *, + scalar_types: _ClassInfo | None = ..., + strict: bool = ..., +) -> Iterable[tuple[_T, ...]]: ... +@overload +def zip_broadcast( + __obj1: _T | Iterable[_T], + __obj2: _T | Iterable[_T], + __obj3: _T | Iterable[_T], + __obj4: _T | Iterable[_T], + *, + scalar_types: _ClassInfo | None = ..., + strict: bool = ..., +) -> Iterable[tuple[_T, ...]]: ... +@overload +def zip_broadcast( + __obj1: _T | Iterable[_T], + __obj2: _T | Iterable[_T], + __obj3: _T | Iterable[_T], + __obj4: _T | Iterable[_T], + __obj5: _T | Iterable[_T], + *, + scalar_types: _ClassInfo | None = ..., + strict: bool = ..., +) -> Iterable[tuple[_T, ...]]: ... +@overload +def zip_broadcast( + __obj1: _T | Iterable[_T], + __obj2: _T | Iterable[_T], + __obj3: _T | Iterable[_T], + __obj4: _T | Iterable[_T], + __obj5: _T | Iterable[_T], + __obj6: _T | Iterable[_T], + *objects: _T | Iterable[_T], + scalar_types: _ClassInfo | None = ..., + strict: bool = ..., +) -> Iterable[tuple[_T, ...]]: ... +def unique_in_window( + iterable: Iterable[_T], n: int, key: Callable[[_T], _U] | None = ... +) -> Iterator[_T]: ... +def duplicates_everseen( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> Iterator[_T]: ... +def duplicates_justseen( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> Iterator[_T]: ... +def classify_unique( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> Iterator[tuple[_T, bool, bool]]: ... + +class _SupportsLessThan(Protocol): + def __lt__(self, __other: Any) -> bool: ... + +_SupportsLessThanT = TypeVar("_SupportsLessThanT", bound=_SupportsLessThan) + +@overload +def minmax( + iterable_or_value: Iterable[_SupportsLessThanT], *, key: None = None +) -> tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: Iterable[_T], *, key: Callable[[_T], _SupportsLessThan] +) -> tuple[_T, _T]: ... +@overload +def minmax( + iterable_or_value: Iterable[_SupportsLessThanT], + *, + key: None = None, + default: _U, +) -> _U | tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: Iterable[_T], + *, + key: Callable[[_T], _SupportsLessThan], + default: _U, +) -> _U | tuple[_T, _T]: ... +@overload +def minmax( + iterable_or_value: _SupportsLessThanT, + __other: _SupportsLessThanT, + *others: _SupportsLessThanT, +) -> tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: _T, + __other: _T, + *others: _T, + key: Callable[[_T], _SupportsLessThan], +) -> tuple[_T, _T]: ... +def longest_common_prefix( + iterables: Iterable[Iterable[_T]], +) -> Iterator[_T]: ... +def iequals(*iterables: Iterable[Any]) -> bool: ... +def constrained_batches( + iterable: Iterable[_T], + max_size: int, + max_count: int | None = ..., + get_len: Callable[[_T], object] = ..., + strict: bool = ..., +) -> Iterator[tuple[_T]]: ... +def gray_product(*iterables: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def partial_product(*iterables: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def takewhile_inclusive( + predicate: Callable[[_T], bool], iterable: Iterable[_T] +) -> Iterator[_T]: ... +def outer_product( + func: Callable[[_T, _U], _V], + xs: Iterable[_T], + ys: Iterable[_U], + *args: Any, + **kwargs: Any, +) -> Iterator[tuple[_V, ...]]: ... +def iter_suppress( + iterable: Iterable[_T], + *exceptions: type[BaseException], +) -> Iterator[_T]: ... +def filter_map( + func: Callable[[_T], _V | None], + iterable: Iterable[_T], +) -> Iterator[_V]: ... +def powerset_of_sets(iterable: Iterable[_T]) -> Iterator[set[_T]]: ... +def join_mappings( + **field_to_map: Mapping[_T, _V], +) -> dict[_T, dict[str, _V]]: ... +def doublestarmap( + func: Callable[..., _T], + iterable: Iterable[Mapping[str, Any]], +) -> Iterator[_T]: ... +def dft(xarr: Sequence[complex]) -> Iterator[complex]: ... +def idft(Xarr: Sequence[complex]) -> Iterator[complex]: ... +def _nth_prime_ub(n: int) -> float: ... +def nth_prime(n: int, *, approximate: bool = ...) -> int: ... +def argmin( + iterable: Iterable[_T], *, key: Callable[[_T], _U] | None = ... +) -> int: ... +def argmax( + iterable: Iterable[_T], *, key: Callable[[_T], _U] | None = ... +) -> int: ... +def extract( + iterable: Iterable[_T], indices: Iterable[int] +) -> Iterator[_T]: ... diff --git a/lib/more_itertools/py.typed b/lib/more_itertools/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/lib/more_itertools/recipes.py b/lib/more_itertools/recipes.py new file mode 100644 index 0000000..dacf614 --- /dev/null +++ b/lib/more_itertools/recipes.py @@ -0,0 +1,1471 @@ +"""Imported from the recipes section of the itertools documentation. + +All functions taken from the recipes section of the itertools library docs +[1]_. +Some backward-compatible usability improvements have been made. + +.. [1] http://docs.python.org/library/itertools.html#recipes + +""" + +import random + +from bisect import bisect_left, insort +from collections import deque +from contextlib import suppress +from functools import lru_cache, partial, reduce +from heapq import heappush, heappushpop +from itertools import ( + accumulate, + chain, + combinations, + compress, + count, + cycle, + groupby, + islice, + product, + repeat, + starmap, + takewhile, + tee, + zip_longest, +) +from math import prod, comb, isqrt, gcd +from operator import mul, not_, itemgetter, getitem, index +from random import randrange, sample, choice +from sys import hexversion + +__all__ = [ + 'all_equal', + 'batched', + 'before_and_after', + 'consume', + 'convolve', + 'dotproduct', + 'first_true', + 'factor', + 'flatten', + 'grouper', + 'is_prime', + 'iter_except', + 'iter_index', + 'loops', + 'matmul', + 'multinomial', + 'ncycles', + 'nth', + 'nth_combination', + 'padnone', + 'pad_none', + 'pairwise', + 'partition', + 'polynomial_eval', + 'polynomial_from_roots', + 'polynomial_derivative', + 'powerset', + 'prepend', + 'quantify', + 'reshape', + 'random_combination_with_replacement', + 'random_combination', + 'random_permutation', + 'random_product', + 'repeatfunc', + 'roundrobin', + 'running_median', + 'sieve', + 'sliding_window', + 'subslices', + 'sum_of_squares', + 'tabulate', + 'tail', + 'take', + 'totient', + 'transpose', + 'triplewise', + 'unique', + 'unique_everseen', + 'unique_justseen', +] + +_marker = object() + + +# zip with strict is available for Python 3.10+ +try: + zip(strict=True) +except TypeError: # pragma: no cover + _zip_strict = zip +else: # pragma: no cover + _zip_strict = partial(zip, strict=True) + + +# math.sumprod is available for Python 3.12+ +try: + from math import sumprod as _sumprod +except ImportError: # pragma: no cover + _sumprod = lambda x, y: dotproduct(x, y) + + +# heapq max-heap functions are available for Python 3.14+ +try: + from heapq import heappush_max, heappushpop_max +except ImportError: # pragma: no cover + _max_heap_available = False +else: # pragma: no cover + _max_heap_available = True + + +def take(n, iterable): + """Return first *n* items of the *iterable* as a list. + + >>> take(3, range(10)) + [0, 1, 2] + + If there are fewer than *n* items in the iterable, all of them are + returned. + + >>> take(10, range(3)) + [0, 1, 2] + + """ + return list(islice(iterable, n)) + + +def tabulate(function, start=0): + """Return an iterator over the results of ``func(start)``, + ``func(start + 1)``, ``func(start + 2)``... + + *func* should be a function that accepts one integer argument. + + If *start* is not specified it defaults to 0. It will be incremented each + time the iterator is advanced. + + >>> square = lambda x: x ** 2 + >>> iterator = tabulate(square, -3) + >>> take(4, iterator) + [9, 4, 1, 0] + + """ + return map(function, count(start)) + + +def tail(n, iterable): + """Return an iterator over the last *n* items of *iterable*. + + >>> t = tail(3, 'ABCDEFG') + >>> list(t) + ['E', 'F', 'G'] + + """ + try: + size = len(iterable) + except TypeError: + return iter(deque(iterable, maxlen=n)) + else: + return islice(iterable, max(0, size - n), None) + + +def consume(iterator, n=None): + """Advance *iterable* by *n* steps. If *n* is ``None``, consume it + entirely. + + Efficiently exhausts an iterator without returning values. Defaults to + consuming the whole iterator, but an optional second argument may be + provided to limit consumption. + + >>> i = (x for x in range(10)) + >>> next(i) + 0 + >>> consume(i, 3) + >>> next(i) + 4 + >>> consume(i) + >>> next(i) + Traceback (most recent call last): + File "", line 1, in + StopIteration + + If the iterator has fewer items remaining than the provided limit, the + whole iterator will be consumed. + + >>> i = (x for x in range(3)) + >>> consume(i, 5) + >>> next(i) + Traceback (most recent call last): + File "", line 1, in + StopIteration + + """ + # Use functions that consume iterators at C speed. + if n is None: + # feed the entire iterator into a zero-length deque + deque(iterator, maxlen=0) + else: + # advance to the empty slice starting at position n + next(islice(iterator, n, n), None) + + +def nth(iterable, n, default=None): + """Returns the nth item or a default value. + + >>> l = range(10) + >>> nth(l, 3) + 3 + >>> nth(l, 20, "zebra") + 'zebra' + + """ + return next(islice(iterable, n, None), default) + + +def all_equal(iterable, key=None): + """ + Returns ``True`` if all the elements are equal to each other. + + >>> all_equal('aaaa') + True + >>> all_equal('aaab') + False + + A function that accepts a single argument and returns a transformed version + of each input item can be specified with *key*: + + >>> all_equal('AaaA', key=str.casefold) + True + >>> all_equal([1, 2, 3], key=lambda x: x < 10) + True + + """ + iterator = groupby(iterable, key) + for first in iterator: + for second in iterator: + return False + return True + return True + + +def quantify(iterable, pred=bool): + """Return the how many times the predicate is true. + + >>> quantify([True, False, True]) + 2 + + """ + return sum(map(pred, iterable)) + + +def pad_none(iterable): + """Returns the sequence of elements and then returns ``None`` indefinitely. + + >>> take(5, pad_none(range(3))) + [0, 1, 2, None, None] + + Useful for emulating the behavior of the built-in :func:`map` function. + + See also :func:`padded`. + + """ + return chain(iterable, repeat(None)) + + +padnone = pad_none + + +def ncycles(iterable, n): + """Returns the sequence elements *n* times + + >>> list(ncycles(["a", "b"], 3)) + ['a', 'b', 'a', 'b', 'a', 'b'] + + """ + return chain.from_iterable(repeat(tuple(iterable), n)) + + +def dotproduct(vec1, vec2): + """Returns the dot product of the two iterables. + + >>> dotproduct([10, 15, 12], [0.65, 0.80, 1.25]) + 33.5 + >>> 10 * 0.65 + 15 * 0.80 + 12 * 1.25 + 33.5 + + In Python 3.12 and later, use ``math.sumprod()`` instead. + """ + return sum(map(mul, vec1, vec2)) + + +def flatten(listOfLists): + """Return an iterator flattening one level of nesting in a list of lists. + + >>> list(flatten([[0, 1], [2, 3]])) + [0, 1, 2, 3] + + See also :func:`collapse`, which can flatten multiple levels of nesting. + + """ + return chain.from_iterable(listOfLists) + + +def repeatfunc(func, times=None, *args): + """Call *func* with *args* repeatedly, returning an iterable over the + results. + + If *times* is specified, the iterable will terminate after that many + repetitions: + + >>> from operator import add + >>> times = 4 + >>> args = 3, 5 + >>> list(repeatfunc(add, times, *args)) + [8, 8, 8, 8] + + If *times* is ``None`` the iterable will not terminate: + + >>> from random import randrange + >>> times = None + >>> args = 1, 11 + >>> take(6, repeatfunc(randrange, times, *args)) # doctest:+SKIP + [2, 4, 8, 1, 8, 4] + + """ + if times is None: + return starmap(func, repeat(args)) + return starmap(func, repeat(args, times)) + + +def _pairwise(iterable): + """Returns an iterator of paired items, overlapping, from the original + + >>> take(4, pairwise(count())) + [(0, 1), (1, 2), (2, 3), (3, 4)] + + On Python 3.10 and above, this is an alias for :func:`itertools.pairwise`. + + """ + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +try: + from itertools import pairwise as itertools_pairwise +except ImportError: # pragma: no cover + pairwise = _pairwise +else: # pragma: no cover + + def pairwise(iterable): + return itertools_pairwise(iterable) + + pairwise.__doc__ = _pairwise.__doc__ + + +class UnequalIterablesError(ValueError): + def __init__(self, details=None): + msg = 'Iterables have different lengths' + if details is not None: + msg += (': index 0 has length {}; index {} has length {}').format( + *details + ) + + super().__init__(msg) + + +def _zip_equal_generator(iterables): + for combo in zip_longest(*iterables, fillvalue=_marker): + for val in combo: + if val is _marker: + raise UnequalIterablesError() + yield combo + + +def _zip_equal(*iterables): + # Check whether the iterables are all the same size. + try: + first_size = len(iterables[0]) + for i, it in enumerate(iterables[1:], 1): + size = len(it) + if size != first_size: + raise UnequalIterablesError(details=(first_size, i, size)) + # All sizes are equal, we can use the built-in zip. + return zip(*iterables) + # If any one of the iterables didn't have a length, start reading + # them until one runs out. + except TypeError: + return _zip_equal_generator(iterables) + + +def grouper(iterable, n, incomplete='fill', fillvalue=None): + """Group elements from *iterable* into fixed-length groups of length *n*. + + >>> list(grouper('ABCDEF', 3)) + [('A', 'B', 'C'), ('D', 'E', 'F')] + + The keyword arguments *incomplete* and *fillvalue* control what happens for + iterables whose length is not a multiple of *n*. + + When *incomplete* is `'fill'`, the last group will contain instances of + *fillvalue*. + + >>> list(grouper('ABCDEFG', 3, incomplete='fill', fillvalue='x')) + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')] + + When *incomplete* is `'ignore'`, the last group will not be emitted. + + >>> list(grouper('ABCDEFG', 3, incomplete='ignore', fillvalue='x')) + [('A', 'B', 'C'), ('D', 'E', 'F')] + + When *incomplete* is `'strict'`, a subclass of `ValueError` will be raised. + + >>> iterator = grouper('ABCDEFG', 3, incomplete='strict') + >>> list(iterator) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + UnequalIterablesError + + """ + iterators = [iter(iterable)] * n + if incomplete == 'fill': + return zip_longest(*iterators, fillvalue=fillvalue) + if incomplete == 'strict': + return _zip_equal(*iterators) + if incomplete == 'ignore': + return zip(*iterators) + else: + raise ValueError('Expected fill, strict, or ignore') + + +def roundrobin(*iterables): + """Visit input iterables in a cycle until each is exhausted. + + >>> list(roundrobin('ABC', 'D', 'EF')) + ['A', 'D', 'E', 'B', 'F', 'C'] + + This function produces the same output as :func:`interleave_longest`, but + may perform better for some inputs (in particular when the number of + iterables is small). + + """ + # Algorithm credited to George Sakkis + iterators = map(iter, iterables) + for num_active in range(len(iterables), 0, -1): + iterators = cycle(islice(iterators, num_active)) + yield from map(next, iterators) + + +def partition(pred, iterable): + """ + Returns a 2-tuple of iterables derived from the input iterable. + The first yields the items that have ``pred(item) == False``. + The second yields the items that have ``pred(item) == True``. + + >>> is_odd = lambda x: x % 2 != 0 + >>> iterable = range(10) + >>> even_items, odd_items = partition(is_odd, iterable) + >>> list(even_items), list(odd_items) + ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]) + + If *pred* is None, :func:`bool` is used. + + >>> iterable = [0, 1, False, True, '', ' '] + >>> false_items, true_items = partition(None, iterable) + >>> list(false_items), list(true_items) + ([0, False, ''], [1, True, ' ']) + + """ + if pred is None: + pred = bool + + t1, t2, p = tee(iterable, 3) + p1, p2 = tee(map(pred, p)) + return (compress(t1, map(not_, p1)), compress(t2, p2)) + + +def powerset(iterable): + """Yields all possible subsets of the iterable. + + >>> list(powerset([1, 2, 3])) + [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] + + :func:`powerset` will operate on iterables that aren't :class:`set` + instances, so repeated elements in the input will produce repeated elements + in the output. + + >>> seq = [1, 1, 0] + >>> list(powerset(seq)) + [(), (1,), (1,), (0,), (1, 1), (1, 0), (1, 0), (1, 1, 0)] + + For a variant that efficiently yields actual :class:`set` instances, see + :func:`powerset_of_sets`. + """ + s = list(iterable) + return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) + + +def unique_everseen(iterable, key=None): + """ + Yield unique elements, preserving order. + + >>> list(unique_everseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D'] + >>> list(unique_everseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'D'] + + Sequences with a mix of hashable and unhashable items can be used. + The function will be slower (i.e., `O(n^2)`) for unhashable items. + + Remember that ``list`` objects are unhashable - you can use the *key* + parameter to transform the list to a tuple (which is hashable) to + avoid a slowdown. + + >>> iterable = ([1, 2], [2, 3], [1, 2]) + >>> list(unique_everseen(iterable)) # Slow + [[1, 2], [2, 3]] + >>> list(unique_everseen(iterable, key=tuple)) # Faster + [[1, 2], [2, 3]] + + Similarly, you may want to convert unhashable ``set`` objects with + ``key=frozenset``. For ``dict`` objects, + ``key=lambda x: frozenset(x.items())`` can be used. + + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + use_key = key is not None + + for element in iterable: + k = key(element) if use_key else element + try: + if k not in seenset: + seenset_add(k) + yield element + except TypeError: + if k not in seenlist: + seenlist_add(k) + yield element + + +def unique_justseen(iterable, key=None): + """Yields elements in order, ignoring serial duplicates + + >>> list(unique_justseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D', 'A', 'B'] + >>> list(unique_justseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'A', 'D'] + + """ + if key is None: + return map(itemgetter(0), groupby(iterable)) + + return map(next, map(itemgetter(1), groupby(iterable, key))) + + +def unique(iterable, key=None, reverse=False): + """Yields unique elements in sorted order. + + >>> list(unique([[1, 2], [3, 4], [1, 2]])) + [[1, 2], [3, 4]] + + *key* and *reverse* are passed to :func:`sorted`. + + >>> list(unique('ABBcCAD', str.casefold)) + ['A', 'B', 'c', 'D'] + >>> list(unique('ABBcCAD', str.casefold, reverse=True)) + ['D', 'c', 'B', 'A'] + + The elements in *iterable* need not be hashable, but they must be + comparable for sorting to work. + """ + sequenced = sorted(iterable, key=key, reverse=reverse) + return unique_justseen(sequenced, key=key) + + +def iter_except(func, exception, first=None): + """Yields results from a function repeatedly until an exception is raised. + + Converts a call-until-exception interface to an iterator interface. + Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel + to end the loop. + + >>> l = [0, 1, 2] + >>> list(iter_except(l.pop, IndexError)) + [2, 1, 0] + + Multiple exceptions can be specified as a stopping condition: + + >>> l = [1, 2, 3, '...', 4, 5, 6] + >>> list(iter_except(lambda: 1 + l.pop(), (IndexError, TypeError))) + [7, 6, 5] + >>> list(iter_except(lambda: 1 + l.pop(), (IndexError, TypeError))) + [4, 3, 2] + >>> list(iter_except(lambda: 1 + l.pop(), (IndexError, TypeError))) + [] + + """ + with suppress(exception): + if first is not None: + yield first() + while True: + yield func() + + +def first_true(iterable, default=None, pred=None): + """ + Returns the first true value in the iterable. + + If no true value is found, returns *default* + + If *pred* is not None, returns the first item for which + ``pred(item) == True`` . + + >>> first_true(range(10)) + 1 + >>> first_true(range(10), pred=lambda x: x > 5) + 6 + >>> first_true(range(10), default='missing', pred=lambda x: x > 9) + 'missing' + + """ + return next(filter(pred, iterable), default) + + +def random_product(*args, repeat=1): + """Draw an item at random from each of the input iterables. + + >>> random_product('abc', range(4), 'XYZ') # doctest:+SKIP + ('c', 3, 'Z') + + If *repeat* is provided as a keyword argument, that many items will be + drawn from each iterable. + + >>> random_product('abcd', range(4), repeat=2) # doctest:+SKIP + ('a', 2, 'd', 3) + + This equivalent to taking a random selection from + ``itertools.product(*args, repeat=repeat)``. + + """ + pools = [tuple(pool) for pool in args] * repeat + return tuple(choice(pool) for pool in pools) + + +def random_permutation(iterable, r=None): + """Return a random *r* length permutation of the elements in *iterable*. + + If *r* is not specified or is ``None``, then *r* defaults to the length of + *iterable*. + + >>> random_permutation(range(5)) # doctest:+SKIP + (3, 4, 0, 1, 2) + + This equivalent to taking a random selection from + ``itertools.permutations(iterable, r)``. + + """ + pool = tuple(iterable) + r = len(pool) if r is None else r + return tuple(sample(pool, r)) + + +def random_combination(iterable, r): + """Return a random *r* length subsequence of the elements in *iterable*. + + >>> random_combination(range(5), 3) # doctest:+SKIP + (2, 3, 4) + + This equivalent to taking a random selection from + ``itertools.combinations(iterable, r)``. + + """ + pool = tuple(iterable) + n = len(pool) + indices = sorted(sample(range(n), r)) + return tuple(pool[i] for i in indices) + + +def random_combination_with_replacement(iterable, r): + """Return a random *r* length subsequence of elements in *iterable*, + allowing individual elements to be repeated. + + >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP + (0, 0, 1, 2, 2) + + This equivalent to taking a random selection from + ``itertools.combinations_with_replacement(iterable, r)``. + + """ + pool = tuple(iterable) + n = len(pool) + indices = sorted(randrange(n) for i in range(r)) + return tuple(pool[i] for i in indices) + + +def nth_combination(iterable, r, index): + """Equivalent to ``list(combinations(iterable, r))[index]``. + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`nth_combination` computes the subsequence at + sort position *index* directly, without computing the previous + subsequences. + + >>> nth_combination(range(5), 3, 5) + (0, 3, 4) + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = tuple(iterable) + n = len(pool) + if (r < 0) or (r > n): + raise ValueError + + c = 1 + k = min(r, n - r) + for i in range(1, k + 1): + c = c * (n - k + i) // i + + if index < 0: + index += c + + if (index < 0) or (index >= c): + raise IndexError + + result = [] + while r: + c, n, r = c * r // n, n - 1, r - 1 + while index >= c: + index -= c + c, n = c * (n - r) // n, n - 1 + result.append(pool[-1 - n]) + + return tuple(result) + + +def prepend(value, iterator): + """Yield *value*, followed by the elements in *iterator*. + + >>> value = '0' + >>> iterator = ['1', '2', '3'] + >>> list(prepend(value, iterator)) + ['0', '1', '2', '3'] + + To prepend multiple values, see :func:`itertools.chain` + or :func:`value_chain`. + + """ + return chain([value], iterator) + + +def convolve(signal, kernel): + """Discrete linear convolution of two iterables. + Equivalent to polynomial multiplication. + + For example, multiplying ``(x² -x - 20)`` by ``(x - 3)`` + gives ``(x³ -4x² -17x + 60)``. + + >>> list(convolve([1, -1, -20], [1, -3])) + [1, -4, -17, 60] + + Examples of popular kinds of kernels: + + * The kernel ``[0.25, 0.25, 0.25, 0.25]`` computes a moving average. + For image data, this blurs the image and reduces noise. + * The kernel ``[1/2, 0, -1/2]`` estimates the first derivative of + a function evaluated at evenly spaced inputs. + * The kernel ``[1, -2, 1]`` estimates the second derivative of a + function evaluated at evenly spaced inputs. + + Convolutions are mathematically commutative; however, the inputs are + evaluated differently. The signal is consumed lazily and can be + infinite. The kernel is fully consumed before the calculations begin. + + Supports all numeric types: int, float, complex, Decimal, Fraction. + + References: + + * Article: https://betterexplained.com/articles/intuitive-convolution/ + * Video by 3Blue1Brown: https://www.youtube.com/watch?v=KuXjwB4LzSA + + """ + # This implementation comes from an older version of the itertools + # documentation. While the newer implementation is a bit clearer, + # this one was kept because the inlined window logic is faster + # and it avoids an unnecessary deque-to-tuple conversion. + kernel = tuple(kernel)[::-1] + n = len(kernel) + window = deque([0], maxlen=n) * n + for x in chain(signal, repeat(0, n - 1)): + window.append(x) + yield _sumprod(kernel, window) + + +def before_and_after(predicate, it): + """A variant of :func:`takewhile` that allows complete access to the + remainder of the iterator. + + >>> it = iter('ABCdEfGhI') + >>> all_upper, remainder = before_and_after(str.isupper, it) + >>> ''.join(all_upper) + 'ABC' + >>> ''.join(remainder) # takewhile() would lose the 'd' + 'dEfGhI' + + Note that the first iterator must be fully consumed before the second + iterator can generate valid results. + """ + trues, after = tee(it) + trues = compress(takewhile(predicate, trues), zip(after)) + return trues, after + + +def triplewise(iterable): + """Return overlapping triplets from *iterable*. + + >>> list(triplewise('ABCDE')) + [('A', 'B', 'C'), ('B', 'C', 'D'), ('C', 'D', 'E')] + + """ + # This deviates from the itertools documentation recipe - see + # https://github.com/more-itertools/more-itertools/issues/889 + t1, t2, t3 = tee(iterable, 3) + next(t3, None) + next(t3, None) + next(t2, None) + return zip(t1, t2, t3) + + +def _sliding_window_islice(iterable, n): + # Fast path for small, non-zero values of n. + iterators = tee(iterable, n) + for i, iterator in enumerate(iterators): + next(islice(iterator, i, i), None) + return zip(*iterators) + + +def _sliding_window_deque(iterable, n): + # Normal path for other values of n. + iterator = iter(iterable) + window = deque(islice(iterator, n - 1), maxlen=n) + for x in iterator: + window.append(x) + yield tuple(window) + + +def sliding_window(iterable, n): + """Return a sliding window of width *n* over *iterable*. + + >>> list(sliding_window(range(6), 4)) + [(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5)] + + If *iterable* has fewer than *n* items, then nothing is yielded: + + >>> list(sliding_window(range(3), 4)) + [] + + For a variant with more features, see :func:`windowed`. + """ + if n > 20: + return _sliding_window_deque(iterable, n) + elif n > 2: + return _sliding_window_islice(iterable, n) + elif n == 2: + return pairwise(iterable) + elif n == 1: + return zip(iterable) + else: + raise ValueError(f'n should be at least one, not {n}') + + +def subslices(iterable): + """Return all contiguous non-empty subslices of *iterable*. + + >>> list(subslices('ABC')) + [['A'], ['A', 'B'], ['A', 'B', 'C'], ['B'], ['B', 'C'], ['C']] + + This is similar to :func:`substrings`, but emits items in a different + order. + """ + seq = list(iterable) + slices = starmap(slice, combinations(range(len(seq) + 1), 2)) + return map(getitem, repeat(seq), slices) + + +def polynomial_from_roots(roots): + """Compute a polynomial's coefficients from its roots. + + >>> roots = [5, -4, 3] # (x - 5) * (x + 4) * (x - 3) + >>> polynomial_from_roots(roots) # x³ - 4 x² - 17 x + 60 + [1, -4, -17, 60] + + Note that polynomial coefficients are specified in descending power order. + + Supports all numeric types: int, float, complex, Decimal, Fraction. + """ + + # This recipe differs from the one in itertools docs in that it + # applies list() after each call to convolve(). This avoids + # hitting stack limits with nested generators. + + poly = [1] + for root in roots: + poly = list(convolve(poly, (1, -root))) + return poly + + +def iter_index(iterable, value, start=0, stop=None): + """Yield the index of each place in *iterable* that *value* occurs, + beginning with index *start* and ending before index *stop*. + + + >>> list(iter_index('AABCADEAF', 'A')) + [0, 1, 4, 7] + >>> list(iter_index('AABCADEAF', 'A', 1)) # start index is inclusive + [1, 4, 7] + >>> list(iter_index('AABCADEAF', 'A', 1, 7)) # stop index is not inclusive + [1, 4] + + The behavior for non-scalar *values* matches the built-in Python types. + + >>> list(iter_index('ABCDABCD', 'AB')) + [0, 4] + >>> list(iter_index([0, 1, 2, 3, 0, 1, 2, 3], [0, 1])) + [] + >>> list(iter_index([[0, 1], [2, 3], [0, 1], [2, 3]], [0, 1])) + [0, 2] + + See :func:`locate` for a more general means of finding the indexes + associated with particular values. + + """ + seq_index = getattr(iterable, 'index', None) + if seq_index is None: + # Slow path for general iterables + iterator = islice(iterable, start, stop) + for i, element in enumerate(iterator, start): + if element is value or element == value: + yield i + else: + # Fast path for sequences + stop = len(iterable) if stop is None else stop + i = start - 1 + with suppress(ValueError): + while True: + yield (i := seq_index(value, i + 1, stop)) + + +def sieve(n): + """Yield the primes less than n. + + >>> list(sieve(30)) + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] + + """ + # This implementation comes from an older version of the itertools + # documentation. The newer implementation is easier to read but is + # less lazy. + if n > 2: + yield 2 + start = 3 + data = bytearray((0, 1)) * (n // 2) + for p in iter_index(data, 1, start, stop=isqrt(n) + 1): + yield from iter_index(data, 1, start, p * p) + data[p * p : n : p + p] = bytes(len(range(p * p, n, p + p))) + start = p * p + yield from iter_index(data, 1, start) + + +def _batched(iterable, n, *, strict=False): # pragma: no cover + """Batch data into tuples of length *n*. If the number of items in + *iterable* is not divisible by *n*: + * The last batch will be shorter if *strict* is ``False``. + * :exc:`ValueError` will be raised if *strict* is ``True``. + + >>> list(batched('ABCDEFG', 3)) + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)] + + On Python 3.13 and above, this is an alias for :func:`itertools.batched`. + """ + if n < 1: + raise ValueError('n must be at least one') + iterator = iter(iterable) + while batch := tuple(islice(iterator, n)): + if strict and len(batch) != n: + raise ValueError('batched(): incomplete batch') + yield batch + + +if hexversion >= 0x30D00A2: # pragma: no cover + from itertools import batched as itertools_batched + + def batched(iterable, n, *, strict=False): + return itertools_batched(iterable, n, strict=strict) + + batched.__doc__ = _batched.__doc__ +else: # pragma: no cover + batched = _batched + + +def transpose(it): + """Swap the rows and columns of the input matrix. + + >>> list(transpose([(1, 2, 3), (11, 22, 33)])) + [(1, 11), (2, 22), (3, 33)] + + The caller should ensure that the dimensions of the input are compatible. + If the input is empty, no output will be produced. + """ + return _zip_strict(*it) + + +def _is_scalar(value, stringlike=(str, bytes)): + "Scalars are bytes, strings, and non-iterables." + try: + iter(value) + except TypeError: + return True + return isinstance(value, stringlike) + + +def _flatten_tensor(tensor): + "Depth-first iterator over scalars in a tensor." + iterator = iter(tensor) + while True: + try: + value = next(iterator) + except StopIteration: + return iterator + iterator = chain((value,), iterator) + if _is_scalar(value): + return iterator + iterator = chain.from_iterable(iterator) + + +def reshape(matrix, shape): + """Change the shape of a *matrix*. + + If *shape* is an integer, the matrix must be two dimensional + and the shape is interpreted as the desired number of columns: + + >>> matrix = [(0, 1), (2, 3), (4, 5)] + >>> cols = 3 + >>> list(reshape(matrix, cols)) + [(0, 1, 2), (3, 4, 5)] + + If *shape* is a tuple (or other iterable), the input matrix can have + any number of dimensions. It will first be flattened and then rebuilt + to the desired shape which can also be multidimensional: + + >>> matrix = [(0, 1), (2, 3), (4, 5)] # Start with a 3 x 2 matrix + + >>> list(reshape(matrix, (2, 3))) # Make a 2 x 3 matrix + [(0, 1, 2), (3, 4, 5)] + + >>> list(reshape(matrix, (6,))) # Make a vector of length six + [0, 1, 2, 3, 4, 5] + + >>> list(reshape(matrix, (2, 1, 3, 1))) # Make 2 x 1 x 3 x 1 tensor + [(((0,), (1,), (2,)),), (((3,), (4,), (5,)),)] + + Each dimension is assumed to be uniform, either all arrays or all scalars. + Flattening stops when the first value in a dimension is a scalar. + Scalars are bytes, strings, and non-iterables. + The reshape iterator stops when the requested shape is complete + or when the input is exhausted, whichever comes first. + + """ + if isinstance(shape, int): + return batched(chain.from_iterable(matrix), shape) + first_dim, *dims = shape + scalar_stream = _flatten_tensor(matrix) + reshaped = reduce(batched, reversed(dims), scalar_stream) + return islice(reshaped, first_dim) + + +def matmul(m1, m2): + """Multiply two matrices. + + >>> list(matmul([(7, 5), (3, 5)], [(2, 5), (7, 9)])) + [(49, 80), (41, 60)] + + The caller should ensure that the dimensions of the input matrices are + compatible with each other. + + Supports all numeric types: int, float, complex, Decimal, Fraction. + """ + n = len(m2[0]) + return batched(starmap(_sumprod, product(m1, transpose(m2))), n) + + +def _factor_pollard(n): + # Return a factor of n using Pollard's rho algorithm. + # Efficient when n is odd and composite. + for b in range(1, n): + x = y = 2 + d = 1 + while d == 1: + x = (x * x + b) % n + y = (y * y + b) % n + y = (y * y + b) % n + d = gcd(x - y, n) + if d != n: + return d + raise ValueError('prime or under 5') # pragma: no cover + + +_primes_below_211 = tuple(sieve(211)) + + +def factor(n): + """Yield the prime factors of n. + + >>> list(factor(360)) + [2, 2, 2, 3, 3, 5] + + Finds small factors with trial division. Larger factors are + either verified as prime with ``is_prime`` or split into + smaller factors with Pollard's rho algorithm. + """ + + # Corner case reduction + if n < 2: + return + + # Trial division reduction + for prime in _primes_below_211: + while not n % prime: + yield prime + n //= prime + + # Pollard's rho reduction + primes = [] + todo = [n] if n > 1 else [] + for n in todo: + if n < 211**2 or is_prime(n): + primes.append(n) + else: + fact = _factor_pollard(n) + todo += (fact, n // fact) + yield from sorted(primes) + + +def polynomial_eval(coefficients, x): + """Evaluate a polynomial at a specific value. + + Computes with better numeric stability than Horner's method. + + Evaluate ``x^3 - 4 * x^2 - 17 * x + 60`` at ``x = 2.5``: + + >>> coefficients = [1, -4, -17, 60] + >>> x = 2.5 + >>> polynomial_eval(coefficients, x) + 8.125 + + Note that polynomial coefficients are specified in descending power order. + + Supports all numeric types: int, float, complex, Decimal, Fraction. + """ + n = len(coefficients) + if n == 0: + return type(x)(0) + powers = map(pow, repeat(x), reversed(range(n))) + return _sumprod(coefficients, powers) + + +def sum_of_squares(it): + """Return the sum of the squares of the input values. + + >>> sum_of_squares([10, 20, 30]) + 1400 + + Supports all numeric types: int, float, complex, Decimal, Fraction. + """ + return _sumprod(*tee(it)) + + +def polynomial_derivative(coefficients): + """Compute the first derivative of a polynomial. + + Evaluate the derivative of ``x³ - 4 x² - 17 x + 60``: + + >>> coefficients = [1, -4, -17, 60] + >>> derivative_coefficients = polynomial_derivative(coefficients) + >>> derivative_coefficients + [3, -8, -17] + + Note that polynomial coefficients are specified in descending power order. + + Supports all numeric types: int, float, complex, Decimal, Fraction. + """ + n = len(coefficients) + powers = reversed(range(1, n)) + return list(map(mul, coefficients, powers)) + + +def totient(n): + """Return the count of natural numbers up to *n* that are coprime with *n*. + + Euler's totient function φ(n) gives the number of totatives. + Totative are integers k in the range 1 ≤ k ≤ n such that gcd(n, k) = 1. + + >>> n = 9 + >>> totient(n) + 6 + + >>> totatives = [x for x in range(1, n) if gcd(n, x) == 1] + >>> totatives + [1, 2, 4, 5, 7, 8] + >>> len(totatives) + 6 + + Reference: https://en.wikipedia.org/wiki/Euler%27s_totient_function + + """ + for prime in set(factor(n)): + n -= n // prime + return n + + +# Miller–Rabin primality test: https://oeis.org/A014233 +_perfect_tests = [ + (2047, (2,)), + (9080191, (31, 73)), + (4759123141, (2, 7, 61)), + (1122004669633, (2, 13, 23, 1662803)), + (2152302898747, (2, 3, 5, 7, 11)), + (3474749660383, (2, 3, 5, 7, 11, 13)), + (18446744073709551616, (2, 325, 9375, 28178, 450775, 9780504, 1795265022)), + ( + 3317044064679887385961981, + (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41), + ), +] + + +@lru_cache +def _shift_to_odd(n): + 'Return s, d such that 2**s * d == n' + s = ((n - 1) ^ n).bit_length() - 1 + d = n >> s + assert (1 << s) * d == n and d & 1 and s >= 0 + return s, d + + +def _strong_probable_prime(n, base): + assert (n > 2) and (n & 1) and (2 <= base < n) + + s, d = _shift_to_odd(n - 1) + + x = pow(base, d, n) + if x == 1 or x == n - 1: + return True + + for _ in range(s - 1): + x = x * x % n + if x == n - 1: + return True + + return False + + +# Separate instance of Random() that doesn't share state +# with the default user instance of Random(). +_private_randrange = random.Random().randrange + + +def is_prime(n): + """Return ``True`` if *n* is prime and ``False`` otherwise. + + Basic examples: + + >>> is_prime(37) + True + >>> is_prime(3 * 13) + False + >>> is_prime(18_446_744_073_709_551_557) + True + + Find the next prime over one billion: + + >>> next(filter(is_prime, count(10**9))) + 1000000007 + + Generate random primes up to 200 bits and up to 60 decimal digits: + + >>> from random import seed, randrange, getrandbits + >>> seed(18675309) + + >>> next(filter(is_prime, map(getrandbits, repeat(200)))) + 893303929355758292373272075469392561129886005037663238028407 + + >>> next(filter(is_prime, map(randrange, repeat(10**60)))) + 269638077304026462407872868003560484232362454342414618963649 + + This function is exact for values of *n* below 10**24. For larger inputs, + the probabilistic Miller-Rabin primality test has a less than 1 in 2**128 + chance of a false positive. + """ + + if n < 17: + return n in {2, 3, 5, 7, 11, 13} + + if not (n & 1 and n % 3 and n % 5 and n % 7 and n % 11 and n % 13): + return False + + for limit, bases in _perfect_tests: + if n < limit: + break + else: + bases = (_private_randrange(2, n - 1) for i in range(64)) + + return all(_strong_probable_prime(n, base) for base in bases) + + +def loops(n): + """Returns an iterable with *n* elements for efficient looping. + Like ``range(n)`` but doesn't create integers. + + >>> i = 0 + >>> for _ in loops(5): + ... i += 1 + >>> i + 5 + + """ + return repeat(None, n) + + +def multinomial(*counts): + """Number of distinct arrangements of a multiset. + + The expression ``multinomial(3, 4, 2)`` has several equivalent + interpretations: + + * In the expansion of ``(a + b + c)⁹``, the coefficient of the + ``a³b⁴c²`` term is 1260. + + * There are 1260 distinct ways to arrange 9 balls consisting of 3 reds, 4 + greens, and 2 blues. + + * There are 1260 unique ways to place 9 distinct objects into three bins + with sizes 3, 4, and 2. + + The :func:`multinomial` function computes the length of + :func:`distinct_permutations`. For example, there are 83,160 distinct + anagrams of the word "abracadabra": + + >>> from more_itertools import distinct_permutations, ilen + >>> ilen(distinct_permutations('abracadabra')) + 83160 + + This can be computed directly from the letter counts, 5a 2b 2r 1c 1d: + + >>> from collections import Counter + >>> list(Counter('abracadabra').values()) + [5, 2, 2, 1, 1] + >>> multinomial(5, 2, 2, 1, 1) + 83160 + + A binomial coefficient is a special case of multinomial where there are + only two categories. For example, the number of ways to arrange 12 balls + with 5 reds and 7 blues is ``multinomial(5, 7)`` or ``math.comb(12, 5)``. + + Likewise, factorial is a special case of multinomial where + the multiplicities are all just 1 so that + ``multinomial(1, 1, 1, 1, 1, 1, 1) == math.factorial(7)``. + + Reference: https://en.wikipedia.org/wiki/Multinomial_theorem + + """ + return prod(map(comb, accumulate(counts), counts)) + + +def _running_median_minheap_and_maxheap(iterator): # pragma: no cover + "Non-windowed running_median() for Python 3.14+" + + read = iterator.__next__ + lo = [] # max-heap + hi = [] # min-heap (same size as or one smaller than lo) + + with suppress(StopIteration): + while True: + heappush_max(lo, heappushpop(hi, read())) + yield lo[0] + + heappush(hi, heappushpop_max(lo, read())) + yield (lo[0] + hi[0]) / 2 + + +def _running_median_minheap_only(iterator): # pragma: no cover + "Backport of non-windowed running_median() for Python 3.13 and prior." + + read = iterator.__next__ + lo = [] # max-heap (actually a minheap with negated values) + hi = [] # min-heap (same size as or one smaller than lo) + + with suppress(StopIteration): + while True: + heappush(lo, -heappushpop(hi, read())) + yield -lo[0] + + heappush(hi, -heappushpop(lo, -read())) + yield (hi[0] - lo[0]) / 2 + + +def _running_median_windowed(iterator, maxlen): + "Yield median of values in a sliding window." + + window = deque() + ordered = [] + + for x in iterator: + window.append(x) + insort(ordered, x) + + if len(ordered) > maxlen: + i = bisect_left(ordered, window.popleft()) + del ordered[i] + + n = len(ordered) + m = n // 2 + yield ordered[m] if n & 1 else (ordered[m - 1] + ordered[m]) / 2 + + +def running_median(iterable, *, maxlen=None): + """Cumulative median of values seen so far or values in a sliding window. + + Set *maxlen* to a positive integer to specify the maximum size + of the sliding window. The default of *None* is equivalent to + an unbounded window. + + For example: + + >>> list(running_median([5.0, 9.0, 4.0, 12.0, 8.0, 9.0])) + [5.0, 7.0, 5.0, 7.0, 8.0, 8.5] + >>> list(running_median([5.0, 9.0, 4.0, 12.0, 8.0, 9.0], maxlen=3)) + [5.0, 7.0, 5.0, 9.0, 8.0, 9.0] + + Supports numeric types such as int, float, Decimal, and Fraction, + but not complex numbers which are unorderable. + + On version Python 3.13 and prior, max-heaps are simulated with + negative values. The negation causes Decimal inputs to apply context + rounding, making the results slightly different than that obtained + by statistics.median(). + """ + + iterator = iter(iterable) + + if maxlen is not None: + maxlen = index(maxlen) + if maxlen <= 0: + raise ValueError('Window size should be positive') + return _running_median_windowed(iterator, maxlen) + + if not _max_heap_available: + return _running_median_minheap_only(iterator) # pragma: no cover + + return _running_median_minheap_and_maxheap(iterator) # pragma: no cover diff --git a/lib/more_itertools/recipes.pyi b/lib/more_itertools/recipes.pyi new file mode 100644 index 0000000..de3d0a1 --- /dev/null +++ b/lib/more_itertools/recipes.pyi @@ -0,0 +1,205 @@ +"""Stubs for more_itertools.recipes""" + +from __future__ import annotations + +from collections.abc import Iterable, Iterator, Sequence +from decimal import Decimal +from fractions import Fraction +from typing import ( + Any, + Callable, + TypeVar, + overload, +) + +__all__ = [ + 'all_equal', + 'batched', + 'before_and_after', + 'consume', + 'convolve', + 'dotproduct', + 'first_true', + 'factor', + 'flatten', + 'grouper', + 'is_prime', + 'iter_except', + 'iter_index', + 'loops', + 'matmul', + 'multinomial', + 'ncycles', + 'nth', + 'nth_combination', + 'padnone', + 'pad_none', + 'pairwise', + 'partition', + 'polynomial_eval', + 'polynomial_from_roots', + 'polynomial_derivative', + 'powerset', + 'prepend', + 'quantify', + 'reshape', + 'random_combination_with_replacement', + 'random_combination', + 'random_permutation', + 'random_product', + 'repeatfunc', + 'roundrobin', + 'running_median', + 'sieve', + 'sliding_window', + 'subslices', + 'sum_of_squares', + 'tabulate', + 'tail', + 'take', + 'totient', + 'transpose', + 'triplewise', + 'unique', + 'unique_everseen', + 'unique_justseen', +] + +# Type and type variable definitions +_T = TypeVar('_T') +_T1 = TypeVar('_T1') +_T2 = TypeVar('_T2') +_U = TypeVar('_U') +_NumberT = TypeVar("_NumberT", float, Decimal, Fraction) + +def take(n: int, iterable: Iterable[_T]) -> list[_T]: ... +def tabulate( + function: Callable[[int], _T], start: int = ... +) -> Iterator[_T]: ... +def tail(n: int, iterable: Iterable[_T]) -> Iterator[_T]: ... +def consume(iterator: Iterable[_T], n: int | None = ...) -> None: ... +@overload +def nth(iterable: Iterable[_T], n: int) -> _T | None: ... +@overload +def nth(iterable: Iterable[_T], n: int, default: _U) -> _T | _U: ... +def all_equal( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> bool: ... +def quantify( + iterable: Iterable[_T], pred: Callable[[_T], bool] = ... +) -> int: ... +def pad_none(iterable: Iterable[_T]) -> Iterator[_T | None]: ... +def padnone(iterable: Iterable[_T]) -> Iterator[_T | None]: ... +def ncycles(iterable: Iterable[_T], n: int) -> Iterator[_T]: ... +def dotproduct(vec1: Iterable[_T1], vec2: Iterable[_T2]) -> Any: ... +def flatten(listOfLists: Iterable[Iterable[_T]]) -> Iterator[_T]: ... +def repeatfunc( + func: Callable[..., _U], times: int | None = ..., *args: Any +) -> Iterator[_U]: ... +def pairwise(iterable: Iterable[_T]) -> Iterator[tuple[_T, _T]]: ... +def grouper( + iterable: Iterable[_T], + n: int, + incomplete: str = ..., + fillvalue: _U = ..., +) -> Iterator[tuple[_T | _U, ...]]: ... +def roundrobin(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def partition( + pred: Callable[[_T], object] | None, iterable: Iterable[_T] +) -> tuple[Iterator[_T], Iterator[_T]]: ... +def powerset(iterable: Iterable[_T]) -> Iterator[tuple[_T, ...]]: ... +def unique_everseen( + iterable: Iterable[_T], key: Callable[[_T], _U] | None = ... +) -> Iterator[_T]: ... +def unique_justseen( + iterable: Iterable[_T], key: Callable[[_T], object] | None = ... +) -> Iterator[_T]: ... +def unique( + iterable: Iterable[_T], + key: Callable[[_T], object] | None = ..., + reverse: bool = False, +) -> Iterator[_T]: ... +@overload +def iter_except( + func: Callable[[], _T], + exception: type[BaseException] | tuple[type[BaseException], ...], + first: None = ..., +) -> Iterator[_T]: ... +@overload +def iter_except( + func: Callable[[], _T], + exception: type[BaseException] | tuple[type[BaseException], ...], + first: Callable[[], _U], +) -> Iterator[_T | _U]: ... +@overload +def first_true( + iterable: Iterable[_T], *, pred: Callable[[_T], object] | None = ... +) -> _T | None: ... +@overload +def first_true( + iterable: Iterable[_T], + default: _U, + pred: Callable[[_T], object] | None = ..., +) -> _T | _U: ... +def random_product( + *args: Iterable[_T], repeat: int = ... +) -> tuple[_T, ...]: ... +def random_permutation( + iterable: Iterable[_T], r: int | None = ... +) -> tuple[_T, ...]: ... +def random_combination(iterable: Iterable[_T], r: int) -> tuple[_T, ...]: ... +def random_combination_with_replacement( + iterable: Iterable[_T], r: int +) -> tuple[_T, ...]: ... +def nth_combination( + iterable: Iterable[_T], r: int, index: int +) -> tuple[_T, ...]: ... +def prepend(value: _T, iterator: Iterable[_U]) -> Iterator[_T | _U]: ... +def convolve(signal: Iterable[_T], kernel: Iterable[_T]) -> Iterator[_T]: ... +def before_and_after( + predicate: Callable[[_T], bool], it: Iterable[_T] +) -> tuple[Iterator[_T], Iterator[_T]]: ... +def triplewise(iterable: Iterable[_T]) -> Iterator[tuple[_T, _T, _T]]: ... +def sliding_window( + iterable: Iterable[_T], n: int +) -> Iterator[tuple[_T, ...]]: ... +def subslices(iterable: Iterable[_T]) -> Iterator[list[_T]]: ... +def polynomial_from_roots(roots: Sequence[_T]) -> list[_T]: ... +def iter_index( + iterable: Iterable[_T], + value: Any, + start: int | None = ..., + stop: int | None = ..., +) -> Iterator[int]: ... +def sieve(n: int) -> Iterator[int]: ... +def _batched( + iterable: Iterable[_T], n: int, *, strict: bool = False +) -> Iterator[tuple[_T, ...]]: ... + +batched = _batched + +def transpose( + it: Iterable[Iterable[_T]], +) -> Iterator[tuple[_T, ...]]: ... +@overload +def reshape( + matrix: Iterable[Iterable[_T]], shape: int +) -> Iterator[tuple[_T, ...]]: ... +@overload +def reshape(matrix: Iterable[Any], shape: Iterable[int]) -> Iterator[Any]: ... +def matmul(m1: Sequence[_T], m2: Sequence[_T]) -> Iterator[tuple[_T]]: ... +def _factor_trial(n: int) -> Iterator[int]: ... +def _factor_pollard(n: int) -> int: ... +def factor(n: int) -> Iterator[int]: ... +def polynomial_eval(coefficients: Sequence[_T], x: _U) -> _U: ... +def sum_of_squares(it: Iterable[_T]) -> _T: ... +def polynomial_derivative(coefficients: Sequence[_T]) -> list[_T]: ... +def totient(n: int) -> int: ... +def _shift_to_odd(n: int) -> tuple[int, int]: ... +def _strong_probable_prime(n: int, base: int) -> bool: ... +def is_prime(n: int) -> bool: ... +def loops(n: int) -> Iterator[None]: ... +def multinomial(*counts: int) -> int: ... +def running_median( + iterable: Iterable[_NumberT], *, maxlen: int | None = ... +) -> Iterator[_NumberT]: ... diff --git a/lib/secretstorage-3.5.0.dist-info/INSTALLER b/lib/secretstorage-3.5.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/secretstorage-3.5.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/secretstorage-3.5.0.dist-info/METADATA b/lib/secretstorage-3.5.0.dist-info/METADATA new file mode 100644 index 0000000..2a2d24f --- /dev/null +++ b/lib/secretstorage-3.5.0.dist-info/METADATA @@ -0,0 +1,109 @@ +Metadata-Version: 2.4 +Name: SecretStorage +Version: 3.5.0 +Summary: Python bindings to FreeDesktop.org Secret Service API +Author-email: Dmitry Shachnev +License-Expression: BSD-3-Clause +Project-URL: Homepage, https://github.com/mitya57/secretstorage +Project-URL: Documentation, https://secretstorage.readthedocs.io/en/latest/ +Project-URL: Issue Tracker, https://github.com/mitya57/secretstorage/issues/ +Platform: Linux +Classifier: Development Status :: 5 - Production/Stable +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Security +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: cryptography>=2.0 +Requires-Dist: jeepney>=0.6 +Dynamic: license-file + +.. image:: https://github.com/mitya57/secretstorage/workflows/tests/badge.svg + :target: https://github.com/mitya57/secretstorage/actions + :alt: GitHub Actions status +.. image:: https://codecov.io/gh/mitya57/secretstorage/branch/master/graph/badge.svg + :target: https://codecov.io/gh/mitya57/secretstorage + :alt: Coverage status +.. image:: https://readthedocs.org/projects/secretstorage/badge/?version=latest + :target: https://secretstorage.readthedocs.io/en/latest/ + :alt: ReadTheDocs status + +Module description +================== + +This module provides a way for securely storing passwords and other secrets. + +It uses D-Bus `Secret Service`_ API that is supported by GNOME Keyring, +KWallet (since version 5.97) and KeePassXC. + +The main classes provided are ``secretstorage.Item``, representing a secret +item (that has a *label*, a *secret* and some *attributes*) and +``secretstorage.Collection``, a place items are stored in. + +SecretStorage supports most of the functions provided by Secret Service, +including creating and deleting items and collections, editing items, +locking and unlocking collections. + +The documentation can be found on `secretstorage.readthedocs.io`_. + +.. _`Secret Service`: https://specifications.freedesktop.org/secret-service/ +.. _`secretstorage.readthedocs.io`: https://secretstorage.readthedocs.io/en/latest/ + +Building the module +=================== + +SecretStorage requires Python ≥ 3.10 and these packages to work: + +* Jeepney_ +* `python-cryptography`_ + +To build SecretStorage, use this command:: + + python3 -m build + +If you have Sphinx_ installed, you can also build the documentation:: + + python3 -m sphinx docs build/sphinx/html + +.. _Jeepney: https://pypi.org/project/jeepney/ +.. _`python-cryptography`: https://pypi.org/project/cryptography/ +.. _Sphinx: https://www.sphinx-doc.org/en/master/ + +Testing the module +================== + +First, make sure that you have the Secret Service daemon installed. +The `GNOME Keyring`_ is the reference server-side implementation for the +Secret Service specification. + +.. _`GNOME Keyring`: https://download.gnome.org/sources/gnome-keyring/ + +Then, start the daemon and unlock the ``default`` collection, if needed. +The testsuite will fail to run if the ``default`` collection exists and is +locked. If it does not exist, the testsuite can also use the temporary +``session`` collection, as provided by the GNOME Keyring. + +Then, run the Python unittest module:: + + python3 -m unittest discover -s tests + +If you want to run the tests in an isolated or headless environment, run +this command in a D-Bus session:: + + dbus-run-session -- python3 -m unittest discover -s tests + +Get the code +============ + +SecretStorage is available under BSD license. The source code can be found +on GitHub_. + +.. _GitHub: https://github.com/mitya57/secretstorage diff --git a/lib/secretstorage-3.5.0.dist-info/RECORD b/lib/secretstorage-3.5.0.dist-info/RECORD new file mode 100644 index 0000000..f1c9798 --- /dev/null +++ b/lib/secretstorage-3.5.0.dist-info/RECORD @@ -0,0 +1,21 @@ +secretstorage-3.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +secretstorage-3.5.0.dist-info/METADATA,sha256=uY49xC_36bG6zOYNrCZizZll-tbkRp_JSDyeuZmYw2c,3974 +secretstorage-3.5.0.dist-info/RECORD,, +secretstorage-3.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +secretstorage-3.5.0.dist-info/licenses/LICENSE,sha256=5wA1dZK7k3BMsEDOSmZjqFKScWpAQbaiZ_WY8hZYRm8,1504 +secretstorage-3.5.0.dist-info/top_level.txt,sha256=hveSi1OWGaEt3kEVbjmZ0M-ASPxi6y-nTPVa-d3c0B4,14 +secretstorage/__init__.py,sha256=_Y34co2i9G-0ljfDUZE2CdLMYa1HV8jQRc6Xtrf-dEQ,3402 +secretstorage/__pycache__/__init__.cpython-314.pyc,, +secretstorage/__pycache__/collection.cpython-314.pyc,, +secretstorage/__pycache__/defines.cpython-314.pyc,, +secretstorage/__pycache__/dhcrypto.cpython-314.pyc,, +secretstorage/__pycache__/exceptions.cpython-314.pyc,, +secretstorage/__pycache__/item.cpython-314.pyc,, +secretstorage/__pycache__/util.cpython-314.pyc,, +secretstorage/collection.py,sha256=5g-aqHnPbHoaX41J839oaRFaUd8Mr4Bf7KSatfCykoo,9829 +secretstorage/defines.py,sha256=OacsZ_i7F7E-BBqKOdSTBJJUmPB7CHRNLgNjIQnjVM0,872 +secretstorage/dhcrypto.py,sha256=VdTb-rxwJcOlsWFT4a0vrFudDMdhgasTSD4qTr3bHn8,2274 +secretstorage/exceptions.py,sha256=1uUZXTua4jRZf4PKDIT2SVWcSKP2lP97s8r3eJZudio,1655 +secretstorage/item.py,sha256=b7FlGcWEmDC1zfECiM55rJ1we2h684NsVZkWUmV_zDc,6150 +secretstorage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +secretstorage/util.py,sha256=J_tZYXJTVv75ESFuvKYjltidNrt7fbRBhwM0C08qbLg,7487 diff --git a/lib/secretstorage-3.5.0.dist-info/WHEEL b/lib/secretstorage-3.5.0.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/lib/secretstorage-3.5.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/secretstorage-3.5.0.dist-info/licenses/LICENSE b/lib/secretstorage-3.5.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..076e642 --- /dev/null +++ b/lib/secretstorage-3.5.0.dist-info/licenses/LICENSE @@ -0,0 +1,25 @@ +Copyright 2012-2025 Dmitry Shachnev +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the University nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/secretstorage-3.5.0.dist-info/top_level.txt b/lib/secretstorage-3.5.0.dist-info/top_level.txt new file mode 100644 index 0000000..0ec6ae8 --- /dev/null +++ b/lib/secretstorage-3.5.0.dist-info/top_level.txt @@ -0,0 +1 @@ +secretstorage diff --git a/lib/secretstorage/__init__.py b/lib/secretstorage/__init__.py new file mode 100644 index 0000000..b0e51ae --- /dev/null +++ b/lib/secretstorage/__init__.py @@ -0,0 +1,103 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2013-2020 +# License: 3-clause BSD, see LICENSE file + +"""This file provides quick access to all SecretStorage API. Please +refer to documentation of individual modules for API details. +""" + +from jeepney.bus_messages import message_bus +from jeepney.io.blocking import DBusConnection, Proxy, open_dbus_connection + +from secretstorage.collection import ( + Collection, + create_collection, + get_all_collections, + get_any_collection, + get_collection_by_alias, + get_default_collection, + search_items, +) +from secretstorage.exceptions import ( + ItemNotFoundException, + LockedException, + PromptDismissedException, + SecretServiceNotAvailableException, + SecretStorageException, +) +from secretstorage.item import Item +from secretstorage.util import add_match_rules + +__version_tuple__ = (3, 5, 0) +__version__ = '.'.join(map(str, __version_tuple__)) + +__all__ = [ + 'Collection', + 'Item', + 'ItemNotFoundException', + 'LockedException', + 'PromptDismissedException', + 'SecretServiceNotAvailableException', + 'SecretStorageException', + 'check_service_availability', + 'create_collection', + 'dbus_init', + 'get_all_collections', + 'get_any_collection', + 'get_collection_by_alias', + 'get_default_collection', + 'search_items', +] + + +def dbus_init() -> DBusConnection: + """Returns a new connection to the session bus, instance of + jeepney's :class:`DBusConnection` class. This connection can + then be passed to various SecretStorage functions, such as + :func:`~secretstorage.collection.get_default_collection`. + + .. warning:: + The D-Bus socket will not be closed automatically. You can + close it manually using the :meth:`DBusConnection.close` method, + or you can use the :class:`contextlib.closing` context manager: + + .. code-block:: python + + from contextlib import closing + with closing(dbus_init()) as conn: + collection = secretstorage.get_default_collection(conn) + items = collection.search_items({'application': 'myapp'}) + + However, you will not be able to call any methods on the objects + created within the context after you leave it. + + .. versionchanged:: 3.0 + Before the port to Jeepney, this function returned an + instance of :class:`dbus.SessionBus` class. + + .. versionchanged:: 3.1 + This function no longer accepts any arguments. + """ + try: + connection = open_dbus_connection() + add_match_rules(connection) + return connection + except KeyError as ex: + # os.environ['DBUS_SESSION_BUS_ADDRESS'] may raise it + reason = f"Environment variable {ex.args[0]} is unset" + raise SecretServiceNotAvailableException(reason) from ex + except (ConnectionError, ValueError) as ex: + raise SecretServiceNotAvailableException(str(ex)) from ex + + +def check_service_availability(connection: DBusConnection) -> bool: + """Returns True if the Secret Service daemon is either running or + available for activation via D-Bus, False otherwise. + + .. versionadded:: 3.2 + """ + from secretstorage.util import BUS_NAME + proxy = Proxy(message_bus, connection) + return (proxy.NameHasOwner(BUS_NAME)[0] == 1 + or BUS_NAME in proxy.ListActivatableNames()[0]) diff --git a/lib/secretstorage/__pycache__/__init__.cpython-314.pyc b/lib/secretstorage/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4eacfa0d3807199fd927dbd3aefce6adcfc3f4b2 GIT binary patch literal 3842 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 Y^Y6sIOl-CD#WC!ow`Mf}6=lZ%0CF!CumAu6 literal 0 HcmV?d00001 diff --git a/lib/secretstorage/__pycache__/collection.cpython-314.pyc b/lib/secretstorage/__pycache__/collection.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01401600d4e2f811f738941a7065083fbf1a29a9 GIT binary patch literal 15384 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 KxLRYd?|%cO!28|+ literal 0 HcmV?d00001 diff --git a/lib/secretstorage/__pycache__/defines.cpython-314.pyc b/lib/secretstorage/__pycache__/defines.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b194efe03bd2f5e316f43245a34c6b30b23ed6e GIT binary patch literal 910 zcma)*-)`D46vjg-CYKMIkYeL6ayk>&O+_ zL+l;)He8}yRodllD)j|sLeQpaLi2@n{GFdaA6v&q4{{lbj9+iQo5MIo{ah~ABNhdB zrzDtBkiry<;TYV)TQH8}a2s#K1Wv#myaSUs2~#))(>M(?I0JX_F3cWM<(z+Dy91{0 zB4i+Mgl&hBk?tbQ_>WkUVgJgoUYPOx7x%aDwTZ_7*R@@yYmF@X+G-;_vS)&%1HK}?p_yai`(y(w+7LP8>Bb^O8_N^Nv0ePH zBBQ5T-CVTgi9VEipOKFL=ZRKikZ{JDY59l7kTwzSAHS?rX>X`iPEKhJdB>I4wBFNc zXQX?t#Bw%B%d*;mpdnla$z{kX&7j2By0Y38-;2^mQEdaIDe*xu6uEjG5R3<+0Qg`p z)MNlZ2z9XD5EMccT!OmV;Di?8a-kwhDg>R@r^rf_6jknaIucX}vO1F>a<|@8rP>9k zEB6YC#5~t}CqY4Jw$)CH6T~3r6>IS?dPGwyO;gYoNYqA&+sULF(+vHYQ_q?iPvJ@?&@xWV(&VVzJu-m3uZzFEUSO x$(#MRbE>p>cs{3!i-X#n+FO)}QdktKvs@(ab4w%yZ;Fp+`9;3^E$bGD-ERe3Dt!O| literal 0 HcmV?d00001 diff --git a/lib/secretstorage/__pycache__/dhcrypto.cpython-314.pyc b/lib/secretstorage/__pycache__/dhcrypto.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e40054c7053056be0d14a96df9787dec0bd557c3 GIT binary patch literal 3065 zcmb7`U2Ggz702()e%i4eyGgoEvQEaWFYickCUq zJ2Tw5o5l#CMSw6Zsss|Wg@;5)Ac3kj6@um=>O+N)klHHN0*(e(3Q!@9_;Of8ka&Re zpWRs}s3_t}`@83U&fIhV=gf}v^>#BVe|+TI=I@e>{T_|5MB7HQKt`RZOt1x}N+%^D z1+^?>s^ycB>8ObAWolf+&cP>QxCbklMb%P>ktNhwxz*EQS+mBpG>6sAohx<%b&2;CBHx3Cqn>Qs!X zVF{&F&YFf;-2c${MAj|q;|Cwk>V~_2{7|-7C}y2=MR!thjUqRMVpv7K>|n=ky|QF; zQTP~$kempE;>F1f{{L z$qJn>86sO-5|)0jGM=lLg`Bf2%C?njIms0^*O!c3sT}yq-sdcPJ({LlmMwH)XqvaZ z{r>xI?;WNGxhcXw>+BYb$EKy50bdzu)lC{=!HHp@#OOz7OE%vp1EQN}7W#t{v_jvI z6lpBH3YL$Dvu3+OYqQP=GDmy1XR|l7hS@@WqP?#whq9#I6XA^fKxi$<^K7;+Y;D&0 zdqR5xH%5ilcb=*;+|bM%$5>{Ix5X}e#-O`!bW!#aq)hFcvFvrVrD&AAh-nG0dy(5! zt*|T%*OP76OVl_?-mZFm)n(1$=6Sdt3nPyGyw`htTANi*K0T#D^Ktd(n%_%V1wP-{qsbj{j*u7YEZ>$4W6$-^-ae0} zqU3SDpoEfedA)hOLp+^^GUp!z1fB^{pe`>CFpaTTiWRx(|D zC(Yr50G{I|h20K_Xt}Bvy@+Kz&*6J7UNV;qS8&8(Cc)EKqF`X4XjiMYrL{5{NuKiI z9bFU|dPR5xj%(CPw$_TVAg+4sns91DD^%>_ITsfbaQyOtE3wAl(UsmCTPf() z)5^u^?@V8syE@uPKfacpUQJIo(x+&_dTRH}7ruFc_PRWI^+Y3eY%O(aHFc_ydTOQT zM(X}cQ@=`mVkP=gPoU^bEN~IuMz7FvEaHG8!OdinND7u-MMm$|wAQDXY|8bYt?8Ba z$W~2TG`TA(re#>Rrtu-{u#e#FpwA0^5h&^CLU6NCqufR72X6ZS6^%+o(w?>`dMNH&#A;SNOl6DL&GW?*ZQf zhJiBh4uF91e+K*fhpia;48pSfb#_YIIIQW3urzD>+ir*z!QK^;CKc% z(fl)T4$Y?l+?$^Veu^d>=Hp}TzlG)nSbqTyp&7X3Uud2Iz6M+bz7M=k;9r6@0zlLL z=Yc1Iy}%!UlfVPOB7x(S{-0pI2I#$u3eTjAGrr1TRt6-7@`? zYNe-bcYK|-Cq6YdbvpQ1@f}!!2+aKviTS0h~`Rwr!-@!1kB>zR`DW@A&1ZM*rb@@2&Vf(pZxbg1!F(DE+Gb literal 0 HcmV?d00001 diff --git a/lib/secretstorage/__pycache__/exceptions.cpython-314.pyc b/lib/secretstorage/__pycache__/exceptions.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b05a840212595c8fe962c85df2b7a341249ebe92 GIT binary patch literal 2259 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 literal 0 HcmV?d00001 diff --git a/lib/secretstorage/__pycache__/util.cpython-314.pyc b/lib/secretstorage/__pycache__/util.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56a14b2c4db548965c3b04bcd7752f4a88808db2 GIT binary patch literal 11703 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 literal 0 HcmV?d00001 diff --git a/lib/secretstorage/collection.py b/lib/secretstorage/collection.py new file mode 100644 index 0000000..4db2b41 --- /dev/null +++ b/lib/secretstorage/collection.py @@ -0,0 +1,244 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2013-2025 +# License: 3-clause BSD, see LICENSE file + +"""Collection is a place where secret items are stored. Normally, only +the default collection should be used, but this module allows to use any +registered collection. Use :func:`get_default_collection` to get the +default collection (and create it, if necessary). + +Collections are usually automatically unlocked when user logs in, but +collections can also be locked and unlocked using :meth:`Collection.lock` +and :meth:`Collection.unlock` methods (unlocking requires showing the +unlocking prompt to user and blocks until user accepts or declines it). +Creating new items and editing existing ones is possible only in unlocked +collections. +""" + +from collections.abc import Iterator + +from jeepney.io.blocking import DBusConnection + +from secretstorage.defines import SS_PATH, SS_PREFIX +from secretstorage.dhcrypto import Session +from secretstorage.exceptions import ( + ItemNotFoundException, + LockedException, + PromptDismissedException, +) +from secretstorage.item import Item +from secretstorage.util import ( + DBusAddressWrapper, + exec_prompt, + format_secret, + open_session, + unlock_objects, +) + +COLLECTION_IFACE = SS_PREFIX + 'Collection' +SERVICE_IFACE = SS_PREFIX + 'Service' +DEFAULT_COLLECTION = '/org/freedesktop/secrets/aliases/default' +SESSION_COLLECTION = '/org/freedesktop/secrets/collection/session' + + +class Collection: + """Represents a collection.""" + + def __init__(self, connection: DBusConnection, + collection_path: str = DEFAULT_COLLECTION, + session: Session | None = None) -> None: + self.connection = connection + self.session = session + self.collection_path = collection_path + self._collection = DBusAddressWrapper( + collection_path, COLLECTION_IFACE, connection) + self._collection.get_property('Label') + + def is_locked(self) -> bool: + """Returns :const:`True` if item is locked, otherwise + :const:`False`.""" + return bool(self._collection.get_property('Locked')) + + def ensure_not_locked(self) -> None: + """If collection is locked, raises + :exc:`~secretstorage.exceptions.LockedException`.""" + if self.is_locked(): + raise LockedException('Collection is locked!') + + def unlock(self, timeout: float | None = None) -> bool: + """Requests unlocking the collection. + + Returns a boolean representing whether the prompt has been + dismissed; that means :const:`False` on successful unlocking + and :const:`True` if it has been dismissed. + + :raises: ``TimeoutError`` if `timeout` (in seconds) passed + and the prompt was neither accepted nor dismissed. + + .. versionchanged:: 3.0 + No longer accepts the ``callback`` argument. + + .. versionchanged:: 3.5 + Added ``timeout`` argument. + """ + return unlock_objects(self.connection, [self.collection_path], timeout=timeout) + + def lock(self) -> None: + """Locks the collection.""" + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, self.connection) + service.call('Lock', 'ao', [self.collection_path]) + + def delete(self) -> None: + """Deletes the collection and all items inside it.""" + self.ensure_not_locked() + prompt, = self._collection.call('Delete', '') + if prompt != "/": + dismissed, _result = exec_prompt(self.connection, prompt) + if dismissed: + raise PromptDismissedException('Prompt dismissed.') + + def get_all_items(self) -> Iterator[Item]: + """Returns a generator of all items in the collection.""" + for item_path in self._collection.get_property('Items'): + yield Item(self.connection, item_path, self.session) + + def search_items(self, attributes: dict[str, str]) -> Iterator[Item]: + """Returns a generator of items with the given attributes. + `attributes` should be a dictionary.""" + result, = self._collection.call('SearchItems', 'a{ss}', attributes) + for item_path in result: + yield Item(self.connection, item_path, self.session) + + def get_label(self) -> str: + """Returns the collection label.""" + label = self._collection.get_property('Label') + assert isinstance(label, str) + return label + + def set_label(self, label: str) -> None: + """Sets collection label to `label`.""" + self.ensure_not_locked() + self._collection.set_property('Label', 's', label) + + def create_item(self, label: str, attributes: dict[str, str], + secret: bytes, replace: bool = False, + content_type: str = 'text/plain') -> Item: + """Creates a new :class:`~secretstorage.item.Item` with given + `label` (unicode string), `attributes` (dictionary) and `secret` + (bytestring). If `replace` is :const:`True`, replaces the existing + item with the same attributes. If `content_type` is given, also + sets the content type of the secret (``text/plain`` by default). + Returns the created item.""" + self.ensure_not_locked() + if not self.session: + self.session = open_session(self.connection) + _secret = format_secret(self.session, secret, content_type) + properties = { + SS_PREFIX + 'Item.Label': ('s', label), + SS_PREFIX + 'Item.Attributes': ('a{ss}', attributes), + } + item_path, prompt = self._collection.call( + 'CreateItem', + 'a{sv}(oayays)b', + properties, + _secret, + replace + ) + if len(item_path) > 1: + return Item(self.connection, item_path, self.session) + dismissed, result = exec_prompt(self.connection, prompt) + if dismissed: + raise PromptDismissedException('Prompt dismissed.') + signature, item_path = result + assert signature == 'o' + return Item(self.connection, item_path, self.session) + + def __repr__(self) -> str: + return f"" + + +def create_collection(connection: DBusConnection, label: str, alias: str = '', + session: Session | None = None) -> Collection: + """Creates a new :class:`Collection` with the given `label` and `alias` + and returns it. This action requires prompting. + + :raises: :exc:`~secretstorage.exceptions.PromptDismissedException` + if the prompt is dismissed. + """ + if not session: + session = open_session(connection) + properties = {SS_PREFIX + 'Collection.Label': ('s', label)} + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) + collection_path, prompt = service.call('CreateCollection', 'a{sv}s', + properties, alias) + if len(collection_path) > 1: + return Collection(connection, collection_path, session=session) + dismissed, result = exec_prompt(connection, prompt) + if dismissed: + raise PromptDismissedException('Prompt dismissed.') + signature, collection_path = result + assert signature == 'o' + return Collection(connection, collection_path, session=session) + + +def get_all_collections(connection: DBusConnection) -> Iterator[Collection]: + """Returns a generator of all available collections.""" + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) + for collection_path in service.get_property('Collections'): + yield Collection(connection, collection_path) + + +def get_default_collection(connection: DBusConnection, + session: Session | None = None) -> Collection: + """Returns the default collection. If it doesn't exist, + creates it.""" + try: + return Collection(connection) + except ItemNotFoundException: + return create_collection(connection, 'Default', 'default', session) + + +def get_any_collection(connection: DBusConnection) -> Collection: + """Returns any collection, in the following order of preference: + + - The default collection; + - The "session" collection (usually temporary); + - The first collection in the collections list.""" + try: + return Collection(connection) + except ItemNotFoundException: + pass + try: + # GNOME Keyring provides session collection where items + # are stored in process memory. + return Collection(connection, SESSION_COLLECTION) + except ItemNotFoundException: + pass + collections = list(get_all_collections(connection)) + if collections: + return collections[0] + else: + raise ItemNotFoundException('No collections found.') + + +def get_collection_by_alias(connection: DBusConnection, + alias: str) -> Collection: + """Returns the collection with the given `alias`. If there is no + such collection, raises + :exc:`~secretstorage.exceptions.ItemNotFoundException`.""" + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) + collection_path, = service.call('ReadAlias', 's', alias) + if len(collection_path) <= 1: + raise ItemNotFoundException('No collection with such alias.') + return Collection(connection, collection_path) + + +def search_items(connection: DBusConnection, + attributes: dict[str, str]) -> Iterator[Item]: + """Returns a generator of items in all collections with the given + attributes. `attributes` should be a dictionary.""" + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) + locked, unlocked = service.call('SearchItems', 'a{ss}', attributes) + for item_path in locked + unlocked: + yield Item(connection, item_path) diff --git a/lib/secretstorage/defines.py b/lib/secretstorage/defines.py new file mode 100644 index 0000000..59c7286 --- /dev/null +++ b/lib/secretstorage/defines.py @@ -0,0 +1,21 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2013-2016 +# License: 3-clause BSD, see LICENSE file + +# This file contains some common defines. + +SS_PREFIX = 'org.freedesktop.Secret.' +SS_PATH = '/org/freedesktop/secrets' + +DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' +DBUS_ACCESS_DENIED = 'org.freedesktop.DBus.Error.AccessDenied' +DBUS_SERVICE_UNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown' +DBUS_EXEC_FAILED = 'org.freedesktop.DBus.Error.Spawn.ExecFailed' +DBUS_NO_REPLY = 'org.freedesktop.DBus.Error.NoReply' +DBUS_NOT_SUPPORTED = 'org.freedesktop.DBus.Error.NotSupported' +DBUS_NO_SUCH_OBJECT = 'org.freedesktop.Secret.Error.NoSuchObject' +DBUS_UNKNOWN_OBJECT = 'org.freedesktop.DBus.Error.UnknownObject' + +ALGORITHM_PLAIN = 'plain' +ALGORITHM_DH = 'dh-ietf1024-sha256-aes128-cbc-pkcs7' diff --git a/lib/secretstorage/dhcrypto.py b/lib/secretstorage/dhcrypto.py new file mode 100644 index 0000000..31516cb --- /dev/null +++ b/lib/secretstorage/dhcrypto.py @@ -0,0 +1,50 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2014-2025 +# License: 3-clause BSD, see LICENSE file + +'''This module contains needed classes, functions and constants +to implement dh-ietf1024-sha256-aes128-cbc-pkcs7 secret encryption +algorithm.''' + +import hmac +import os +from hashlib import sha256 + +# A standard 1024 bits (128 bytes) prime number for use in Diffie-Hellman exchange +DH_PRIME_1024_BYTES = ( + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, + 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08, + 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, + 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, + 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, + 0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, + 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38, + 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, + 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF +) + + +DH_PRIME_1024 = int.from_bytes(DH_PRIME_1024_BYTES, 'big') + + +class Session: + def __init__(self) -> None: + self.object_path: str | None = None + self.aes_key: bytes | None = None + self.encrypted = True + # 128-bytes-long strong random number + self.my_private_key = int.from_bytes(os.urandom(0x80), 'big') + self.my_public_key = pow(2, self.my_private_key, DH_PRIME_1024) + + def set_server_public_key(self, server_public_key: int) -> None: + common_secret_int = pow(server_public_key, self.my_private_key, + DH_PRIME_1024) + common_secret = common_secret_int.to_bytes(128, 'big') + # HKDF with null salt, empty info and SHA-256 hash + salt = b'\x00' * 0x20 + pseudo_random_key = hmac.new(salt, common_secret, sha256).digest() + output_block = hmac.new(pseudo_random_key, b'\x01', sha256).digest() + # Resulting AES key should be 128-bit + self.aes_key = output_block[:0x10] diff --git a/lib/secretstorage/exceptions.py b/lib/secretstorage/exceptions.py new file mode 100644 index 0000000..c8c19d6 --- /dev/null +++ b/lib/secretstorage/exceptions.py @@ -0,0 +1,50 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2012-2018 +# License: 3-clause BSD, see LICENSE file + +"""All secretstorage functions may raise various exceptions when +something goes wrong. All exceptions derive from base +:exc:`SecretStorageException` class.""" + + +class SecretStorageException(Exception): + """All exceptions derive from this class.""" + + +class SecretServiceNotAvailableException(SecretStorageException): + """Raised by :class:`~secretstorage.item.Item` or + :class:`~secretstorage.collection.Collection` constructors, or by + other functions in the :mod:`secretstorage.collection` module, when + the Secret Service API is not available.""" + + +class LockedException(SecretStorageException): + """Raised when an action cannot be performed because the collection + is locked. Use :meth:`~secretstorage.collection.Collection.is_locked` + to check if the collection is locked, and + :meth:`~secretstorage.collection.Collection.unlock` to unlock it. + """ + + +class ItemNotFoundException(SecretStorageException): + """Raised when an item does not exist or has been deleted. Example of + handling: + + >>> import secretstorage + >>> connection = secretstorage.dbus_init() + >>> item_path = '/not/existing/path' + >>> try: + ... item = secretstorage.Item(connection, item_path) + ... except secretstorage.ItemNotFoundException: + ... print('Item not found!') + ... + Item not found! + """ + + +class PromptDismissedException(ItemNotFoundException): + """Raised when a prompt was dismissed by the user. + + .. versionadded:: 3.1 + """ diff --git a/lib/secretstorage/item.py b/lib/secretstorage/item.py new file mode 100644 index 0000000..ad3a8b0 --- /dev/null +++ b/lib/secretstorage/item.py @@ -0,0 +1,159 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2013-2025 +# License: 3-clause BSD, see LICENSE file + +"""SecretStorage item contains a *secret*, some *attributes* and a +*label* visible to user. Editing all these properties and reading the +secret is possible only when the :doc:`collection ` storing +the item is unlocked. The collection can be unlocked using collection's +:meth:`~secretstorage.collection.Collection.unlock` method.""" + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from jeepney.io.blocking import DBusConnection + +from secretstorage.defines import SS_PREFIX +from secretstorage.dhcrypto import Session +from secretstorage.exceptions import LockedException, PromptDismissedException +from secretstorage.util import ( + DBusAddressWrapper, + exec_prompt, + format_secret, + open_session, + unlock_objects, +) + +ITEM_IFACE = SS_PREFIX + 'Item' + + +class Item: + """Represents a secret item.""" + + def __init__(self, connection: DBusConnection, + item_path: str, session: Session | None = None) -> None: + self.item_path = item_path + self._item = DBusAddressWrapper(item_path, ITEM_IFACE, connection) + self._item.get_property('Label') + self.session = session + self.connection = connection + + def __eq__(self, other: "DBusConnection") -> bool: + assert isinstance(other.item_path, str) + return self.item_path == other.item_path + + def is_locked(self) -> bool: + """Returns :const:`True` if item is locked, otherwise + :const:`False`.""" + return bool(self._item.get_property('Locked')) + + def ensure_not_locked(self) -> None: + """If collection is locked, raises + :exc:`~secretstorage.exceptions.LockedException`.""" + if self.is_locked(): + raise LockedException('Item is locked!') + + def unlock(self, timeout: float | None = None) -> bool: + """Requests unlocking the item. Usually, this means that the + whole collection containing this item will be unlocked. + + Returns a boolean representing whether the prompt has been + dismissed; that means :const:`False` on successful unlocking + and :const:`True` if it has been dismissed. + + :raises: ``TimeoutError`` if `timeout` (in seconds) passed + and the prompt was neither accepted nor dismissed. + + .. versionadded:: 2.1.2 + + .. versionchanged:: 3.0 + No longer accepts the ``callback`` argument. + + .. versionchanged:: 3.5 + Added ``timeout`` argument. + """ + return unlock_objects(self.connection, [self.item_path], timeout=timeout) + + def get_attributes(self) -> dict[str, str]: + """Returns item attributes (dictionary).""" + attrs = self._item.get_property('Attributes') + return dict(attrs) + + def set_attributes(self, attributes: dict[str, str]) -> None: + """Sets item attributes to `attributes` (dictionary).""" + self._item.set_property('Attributes', 'a{ss}', attributes) + + def get_label(self) -> str: + """Returns item label (unicode string).""" + label = self._item.get_property('Label') + assert isinstance(label, str) + return label + + def set_label(self, label: str) -> None: + """Sets item label to `label`.""" + self.ensure_not_locked() + self._item.set_property('Label', 's', label) + + def delete(self) -> None: + """Deletes the item.""" + self.ensure_not_locked() + prompt, = self._item.call('Delete', '') + if prompt != "/": + dismissed, _result = exec_prompt(self.connection, prompt) + if dismissed: + raise PromptDismissedException('Prompt dismissed.') + + def get_secret(self) -> bytes: + """Returns item secret (bytestring).""" + self.ensure_not_locked() + if not self.session: + self.session = open_session(self.connection) + secret, = self._item.call('GetSecret', 'o', self.session.object_path) + if not self.session.encrypted: + return bytes(secret[2]) + assert self.session.aes_key is not None + aes = algorithms.AES(self.session.aes_key) + aes_iv = bytes(secret[1]) + decryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).decryptor() + encrypted_secret = secret[2] + padded_secret = decryptor.update(bytes(encrypted_secret)) + decryptor.finalize() + assert isinstance(padded_secret, bytes) + return padded_secret[:-padded_secret[-1]] + + def get_secret_content_type(self) -> str: + """Returns content type of item secret (string).""" + self.ensure_not_locked() + if not self.session: + self.session = open_session(self.connection) + secret, = self._item.call('GetSecret', 'o', self.session.object_path) + return str(secret[3]) + + def set_secret(self, secret: bytes, + content_type: str = 'text/plain') -> None: + """Sets item secret to `secret`. If `content_type` is given, + also sets the content type of the secret (``text/plain`` by + default).""" + self.ensure_not_locked() + if not self.session: + self.session = open_session(self.connection) + _secret = format_secret(self.session, secret, content_type) + self._item.call('SetSecret', '(oayays)', _secret) + + def get_created(self) -> int: + """Returns UNIX timestamp (integer) representing the time + when the item was created. + + .. versionadded:: 1.1""" + created = self._item.get_property('Created') + assert isinstance(created, int) + return created + + def get_modified(self) -> int: + """Returns UNIX timestamp (integer) representing the time + when the item was last modified.""" + modified = self._item.get_property('Modified') + assert isinstance(modified, int) + return modified + + def __repr__(self) -> str: + return f"" diff --git a/lib/secretstorage/py.typed b/lib/secretstorage/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/lib/secretstorage/util.py b/lib/secretstorage/util.py new file mode 100644 index 0000000..a7caf2c --- /dev/null +++ b/lib/secretstorage/util.py @@ -0,0 +1,227 @@ +# SecretStorage module for Python +# Access passwords using the SecretService DBus API +# Author: Dmitry Shachnev, 2013-2025 +# License: 3-clause BSD, see LICENSE file + +"""This module provides some utility functions, but these shouldn't +normally be used by external applications.""" + +import os +from typing import Any + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from jeepney import ( + DBusAddress, + DBusErrorResponse, + MatchRule, + Message, + MessageType, + Properties, + new_method_call, +) +from jeepney.io.blocking import DBusConnection + +from secretstorage.defines import ( + ALGORITHM_DH, + ALGORITHM_PLAIN, + DBUS_EXEC_FAILED, + DBUS_NO_REPLY, + DBUS_NO_SUCH_OBJECT, + DBUS_NOT_SUPPORTED, + DBUS_SERVICE_UNKNOWN, + DBUS_UNKNOWN_METHOD, + DBUS_UNKNOWN_OBJECT, + SS_PATH, + SS_PREFIX, +) +from secretstorage.dhcrypto import Session +from secretstorage.exceptions import ( + ItemNotFoundException, + SecretServiceNotAvailableException, +) + +BUS_NAME = 'org.freedesktop.secrets' +SERVICE_IFACE = SS_PREFIX + 'Service' +PROMPT_IFACE = SS_PREFIX + 'Prompt' + + +class DBusAddressWrapper(DBusAddress): # type: ignore + """A wrapper class around :class:`jeepney.wrappers.DBusAddress` + that adds some additional methods for calling and working with + properties, and converts error responses to SecretStorage + exceptions. + + .. versionadded:: 3.0 + """ + def __init__(self, path: str, interface: str, + connection: DBusConnection) -> None: + DBusAddress.__init__(self, path, BUS_NAME, interface) + self._connection = connection + + def send_and_get_reply(self, msg: Message) -> Any: + try: + resp_msg: Message = self._connection.send_and_get_reply(msg) + if resp_msg.header.message_type == MessageType.error: + raise DBusErrorResponse(resp_msg) + return resp_msg.body + except DBusErrorResponse as resp: + if resp.name in ( + DBUS_UNKNOWN_METHOD, + DBUS_NO_SUCH_OBJECT, + DBUS_UNKNOWN_OBJECT, + ): + raise ItemNotFoundException('Item does not exist!') from resp + elif resp.name in (DBUS_SERVICE_UNKNOWN, DBUS_EXEC_FAILED, + DBUS_NO_REPLY): + data = resp.data + if isinstance(data, tuple): + data = data[0] + raise SecretServiceNotAvailableException(data) from resp + raise + + def call(self, method: str, signature: str, *body: Any) -> Any: + msg = new_method_call(self, method, signature, body) + return self.send_and_get_reply(msg) + + def get_property(self, name: str) -> Any: + msg = Properties(self).get(name) + (signature, value), = self.send_and_get_reply(msg) + return value + + def set_property(self, name: str, signature: str, value: Any) -> None: + msg = Properties(self).set(name, signature, value) + self.send_and_get_reply(msg) + + +def open_session(connection: DBusConnection) -> Session: + """Returns a new Secret Service session.""" + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) + session = Session() + try: + output, result = service.call( + 'OpenSession', 'sv', + ALGORITHM_DH, + ('ay', session.my_public_key.to_bytes(128, 'big'))) + except DBusErrorResponse as resp: + if resp.name != DBUS_NOT_SUPPORTED: + raise + output, result = service.call( + 'OpenSession', 'sv', + ALGORITHM_PLAIN, + ('s', '')) + session.encrypted = False + else: + signature, value = output + assert signature == 'ay' + key = int.from_bytes(value, 'big') + session.set_server_public_key(key) + session.object_path = result + return session + + +def format_secret(session: Session, secret: bytes, + content_type: str) -> tuple[str, bytes, bytes, str]: + """Formats `secret` to make possible to pass it to the + Secret Service API.""" + if isinstance(secret, str): + secret = secret.encode('utf-8') + elif not isinstance(secret, bytes): + raise TypeError('secret must be bytes') + assert session.object_path is not None + if not session.encrypted: + return (session.object_path, b'', secret, content_type) + assert session.aes_key is not None + # PKCS-7 style padding + padding = 0x10 - (len(secret) & 0xf) + secret += bytes((padding,) * padding) + aes_iv = os.urandom(0x10) + aes = algorithms.AES(session.aes_key) + encryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).encryptor() + encrypted_secret = encryptor.update(secret) + encryptor.finalize() + return ( + session.object_path, + aes_iv, + encrypted_secret, + content_type + ) + + +def exec_prompt( + connection: DBusConnection, + prompt_path: str, + *, + timeout: float | None = None, +) -> tuple[bool, tuple[str, Any]]: + """Executes the prompt in a blocking mode. + + :returns: a two-element tuple: + + - The first element is a boolean value indicating whether the operation was + dismissed. + - The second element is a (signature, result) tuple. For creating items and + collections, ``signature`` is ``'o'`` and ``result`` is a single object + path. For unlocking, ``signature`` is ``'ao'`` and ``result`` is a list of + object paths. + + .. versionchanged:: 3.5 + Added ``timeout`` keyword argument. + """ + prompt = DBusAddressWrapper(prompt_path, PROMPT_IFACE, connection) + rule = MatchRule( + path=prompt_path, + interface=PROMPT_IFACE, + member='Completed', + type=MessageType.signal, + ) + with connection.filter(rule) as signals: + prompt.call('Prompt', 's', '') + message = connection.recv_until_filtered(signals, timeout=timeout) + dismissed, result = message.body + assert dismissed is not None + assert result is not None + return dismissed, result + + +def unlock_objects( + connection: DBusConnection, + paths: list[str], + *, + timeout: float | None = None, +) -> bool: + """Requests unlocking objects specified in `paths`. + Returns a boolean representing whether the operation was dismissed. + + .. versionadded:: 2.1.2 + + .. versionchanged:: 3.5 + Added ``timeout`` keyword argument. + """ + service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) + unlocked_paths, prompt = service.call('Unlock', 'ao', paths) + if len(prompt) > 1: + dismissed, (signature, unlocked) = exec_prompt( + connection, + prompt, + timeout=timeout, + ) + assert signature == 'ao' + return dismissed + return False + + +def add_match_rules(connection: DBusConnection) -> None: + """Adds match rules for the given connection. + + Currently it matches all messages from the Prompt interface, as the + mock service (unlike GNOME Keyring) does not specify the signal + destination. + + .. versionadded:: 3.1 + """ + rule = MatchRule(sender=BUS_NAME, interface=PROMPT_IFACE) + dbus = DBusAddressWrapper(path='/org/freedesktop/DBus', + interface='org.freedesktop.DBus', + connection=connection) + dbus.bus_name = 'org.freedesktop.DBus' + dbus.call('AddMatch', 's', rule.serialise())