├── .bumpversion.cfg ├── .circleci └── config.yml ├── .coveragerc ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── constant_sorrow ├── __about__.py ├── __init__.py ├── constants.py └── utilities.py ├── setup.py └── tests ├── __init__.py ├── _just_import_for_testing.py ├── docs_for_testing ├── Makefile ├── conf.py ├── index.rst └── make.bat └── test_the_whole_story.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0-alpha.9 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[^.]*)\.(?P\d+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{stage}.{devnum} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:part:stage] 11 | optional_value = stable 12 | first_value = stable 13 | values = 14 | alpha 15 | beta 16 | rc 17 | stable 18 | 19 | [bumpversion:part:devnum] 20 | 21 | [bumpversion:file:constant_sorrow/__about__.py] 22 | 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | workflows: 4 | version: 2 5 | test: 6 | jobs: 7 | - python-35: 8 | filters: 9 | tags: 10 | only: /.*/ 11 | - python-36: 12 | filters: 13 | tags: 14 | only: /.*/ 15 | - python-37: 16 | filters: 17 | tags: 18 | only: /.*/ 19 | - deploy: 20 | context: "NuCypher PyPI" 21 | requires: 22 | - python-35 23 | - python-36 24 | - python-37 25 | filters: 26 | tags: 27 | only: /v[0-9]+.*/ 28 | branches: 29 | ignore: /.*/ 30 | 31 | base_test_steps: &base_test_steps 32 | steps: 33 | - checkout 34 | - restore_cache: 35 | keys: 36 | - v1-dependencies-{{ checksum "Pipfile" }} 37 | - v1-dependencies- 38 | - run: 39 | name: Install dependencies 40 | command: | 41 | pip3 install --user pip==18.0 42 | pip install pipenv 43 | pipenv install --dev --skip-lock --three 44 | - save_cache: 45 | paths: 46 | - "~/.local/share/virtualenvs/" 47 | key: v2-dependencies-{{ checksum "Pipfile" }} 48 | - run: 49 | name: Run Tests 50 | command: pipenv run pytest tests --junitxml=./reports/pytest/results.xml 51 | - store_artifacts: 52 | path: ./htmlcov 53 | - store_test_results: 54 | path: /reports/pytest 55 | 56 | jobs: 57 | python-35: 58 | docker: 59 | - image: circleci/python:3.5 60 | working_directory: ~/repo 61 | <<: *base_test_steps 62 | 63 | python-36: 64 | docker: 65 | - image: circleci/python:3.6 66 | working_directory: ~/repo 67 | <<: *base_test_steps 68 | 69 | python-37: 70 | docker: 71 | - image: circleci/python:3.7 72 | working_directory: ~/repo 73 | <<: *base_test_steps 74 | 75 | deploy: 76 | docker: 77 | - image: circleci/python:3.7 78 | working_directory: ~/repo 79 | steps: 80 | - checkout 81 | - run: 82 | name: Install dependencies 83 | command: | 84 | pipenv install --three --dev --skip-lock 85 | pipenv install --dev --skip-lock twine 86 | - run: 87 | name: verify git tag == version 88 | command: pipenv run python setup.py verify 89 | - run: 90 | name: init .pypirc 91 | command: | 92 | echo -e "[pypi]" >> ~/.pypirc 93 | echo -e "username = $PYPI_USERNAME" >> ~/.pypirc 94 | echo -e "password = $PYPI_PASSWORD" >> ~/.pypirc 95 | - run: 96 | name: create packages 97 | command: | 98 | pipenv run python setup.py sdist 99 | pipenv run python setup.py bdist_wheel 100 | - run: 101 | name: upload to pypi 102 | command: pipenv run twine upload dist/* 103 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py, 4 | *__init__.py 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .pytest_cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | bytestring-splitter = "*" 8 | sphinx = "*" 9 | pytest = "*" 10 | 11 | [dev-packages] 12 | pytest = "*" 13 | bumpversion = "*" 14 | 15 | [pipenv] 16 | allow_prereleases = true 17 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "549b9f9feadcf2c4ad45d0dcd4655798e04409fa6d2efef72178cc3c0f22d3c6" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "alabaster": { 18 | "hashes": [ 19 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 20 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 21 | ], 22 | "version": "==0.7.12" 23 | }, 24 | "attrs": { 25 | "hashes": [ 26 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 27 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 28 | ], 29 | "version": "==19.3.0" 30 | }, 31 | "babel": { 32 | "hashes": [ 33 | "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", 34 | "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" 35 | ], 36 | "version": "==2.8.0" 37 | }, 38 | "bytestring-splitter": { 39 | "hashes": [ 40 | "sha256:03edf1391523b97dc3762705d15088ee109f3fd3e7597d48ae113f93da6d51bc", 41 | "sha256:d1606cefdd85ed94541b415a1677d5ebb278bea9fb7e8061fbfc225b75962f57" 42 | ], 43 | "index": "pypi", 44 | "version": "==2.0.2" 45 | }, 46 | "certifi": { 47 | "hashes": [ 48 | "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", 49 | "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" 50 | ], 51 | "version": "==2020.4.5.1" 52 | }, 53 | "chardet": { 54 | "hashes": [ 55 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 56 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 57 | ], 58 | "version": "==3.0.4" 59 | }, 60 | "docutils": { 61 | "hashes": [ 62 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 63 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 64 | ], 65 | "version": "==0.16" 66 | }, 67 | "idna": { 68 | "hashes": [ 69 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 70 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 71 | ], 72 | "version": "==2.9" 73 | }, 74 | "imagesize": { 75 | "hashes": [ 76 | "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", 77 | "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" 78 | ], 79 | "version": "==1.2.0" 80 | }, 81 | "importlib-metadata": { 82 | "hashes": [ 83 | "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", 84 | "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" 85 | ], 86 | "markers": "python_version < '3.8'", 87 | "version": "==1.6.0" 88 | }, 89 | "jinja2": { 90 | "hashes": [ 91 | "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", 92 | "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" 93 | ], 94 | "version": "==3.0.0a1" 95 | }, 96 | "markupsafe": { 97 | "hashes": [ 98 | "sha256:06358015a4dee8ee23ae426bf885616ab3963622defd829eb45b44e3dee3515f", 99 | "sha256:0b0c4fc852c5f02c6277ef3b33d23fcbe89b1b227460423e3335374da046b6db", 100 | "sha256:267677fc42afed5094fc5ea1c4236bbe4b6a00fe4b08e93451e65ae9048139c7", 101 | "sha256:303cb70893e2c345588fb5d5b86e0ca369f9bb56942f03064c5e3e75fa7a238a", 102 | "sha256:3c9b624a0d9ed5a5093ac4edc4e823e6b125441e60ef35d36e6f4a6fdacd5054", 103 | "sha256:42033e14cae1f6c86fc0c3e90d04d08ce73ac8e46ba420a0d22d545c2abd4977", 104 | "sha256:4e4a99b6af7bdc0856b50020c095848ec050356a001e1f751510aef6ab14d0e0", 105 | "sha256:4eb07faad54bb07427d848f31030a65a49ebb0cec0b30674f91cf1ddd456bfe4", 106 | "sha256:63a7161cd8c2bc563feeda45df62f42c860dd0675e2b8da2667f25bb3c95eaba", 107 | "sha256:68e0fd039b68d2945b4beb947d4023ca7f8e95b708031c345762efba214ea761", 108 | "sha256:8092a63397025c2f655acd42784b2a1528339b90b987beb9253f22e8cdbb36c3", 109 | "sha256:841218860683c0f2223e24756843d84cc49cccdae6765e04962607754a52d3e0", 110 | "sha256:94076b2314bd2f6cfae508ad65b4d493e3a58a50112b7a2cbb6287bdbc404ae8", 111 | "sha256:9d22aff1c5322e402adfb3ce40839a5056c353e711c033798cf4f02eb9f5124d", 112 | "sha256:b0e4584f62b3e5f5c1a7bcefd2b52f236505e6ef032cc508caa4f4c8dc8d3af1", 113 | "sha256:b1163ffc1384d242964426a8164da12dbcdbc0de18ea36e2c34b898ed38c3b45", 114 | "sha256:beac28ed60c8e838301226a7a85841d0af2068eba2dcb1a58c2d32d6c05e440e", 115 | "sha256:c29f096ce79c03054a1101d6e5fe6bf04b0bb489165d5e0e9653fb4fe8048ee1", 116 | "sha256:c58779966d53e5f14ba393d64e2402a7926601d1ac8adeb4e83893def79d0428", 117 | "sha256:cfe14b37908eaf7d5506302987228bff69e1b8e7071ccd4e70fd0283b1b47f0b", 118 | "sha256:e834249c45aa9837d0753351cdca61a4b8b383cc9ad0ff2325c97ff7b69e72a6", 119 | "sha256:eed1b234c4499811ee85bcefa22ef5e466e75d132502226ed29740d593316c1f" 120 | ], 121 | "version": "==2.0.0a1" 122 | }, 123 | "more-itertools": { 124 | "hashes": [ 125 | "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", 126 | "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" 127 | ], 128 | "version": "==8.2.0" 129 | }, 130 | "msgpack-python": { 131 | "hashes": [ 132 | "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" 133 | ], 134 | "version": "==0.5.6" 135 | }, 136 | "packaging": { 137 | "hashes": [ 138 | "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", 139 | "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" 140 | ], 141 | "version": "==20.3" 142 | }, 143 | "pluggy": { 144 | "hashes": [ 145 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 146 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 147 | ], 148 | "version": "==0.13.1" 149 | }, 150 | "py": { 151 | "hashes": [ 152 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 153 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 154 | ], 155 | "version": "==1.8.1" 156 | }, 157 | "pygments": { 158 | "hashes": [ 159 | "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", 160 | "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" 161 | ], 162 | "version": "==2.6.1" 163 | }, 164 | "pyparsing": { 165 | "hashes": [ 166 | "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", 167 | "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" 168 | ], 169 | "version": "==3.0.0a1" 170 | }, 171 | "pytest": { 172 | "hashes": [ 173 | "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", 174 | "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" 175 | ], 176 | "index": "pypi", 177 | "version": "==5.4.1" 178 | }, 179 | "pytz": { 180 | "hashes": [ 181 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 182 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 183 | ], 184 | "version": "==2019.3" 185 | }, 186 | "requests": { 187 | "hashes": [ 188 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 189 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 190 | ], 191 | "version": "==2.23.0" 192 | }, 193 | "six": { 194 | "hashes": [ 195 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 196 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 197 | ], 198 | "version": "==1.14.0" 199 | }, 200 | "snowballstemmer": { 201 | "hashes": [ 202 | "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", 203 | "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" 204 | ], 205 | "version": "==2.0.0" 206 | }, 207 | "sphinx": { 208 | "hashes": [ 209 | "sha256:3145d87d0962366d4c5264c39094eae3f5788d01d4b1a12294051bfe4271d91b", 210 | "sha256:d7c6e72c6aa229caf96af82f60a0d286a1521d42496c226fe37f5a75dcfe2941" 211 | ], 212 | "index": "pypi", 213 | "version": "==3.0.2" 214 | }, 215 | "sphinxcontrib-applehelp": { 216 | "hashes": [ 217 | "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", 218 | "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" 219 | ], 220 | "version": "==1.0.2" 221 | }, 222 | "sphinxcontrib-devhelp": { 223 | "hashes": [ 224 | "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", 225 | "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" 226 | ], 227 | "version": "==1.0.2" 228 | }, 229 | "sphinxcontrib-htmlhelp": { 230 | "hashes": [ 231 | "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", 232 | "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" 233 | ], 234 | "version": "==1.0.3" 235 | }, 236 | "sphinxcontrib-jsmath": { 237 | "hashes": [ 238 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 239 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 240 | ], 241 | "version": "==1.0.1" 242 | }, 243 | "sphinxcontrib-qthelp": { 244 | "hashes": [ 245 | "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", 246 | "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" 247 | ], 248 | "version": "==1.0.3" 249 | }, 250 | "sphinxcontrib-serializinghtml": { 251 | "hashes": [ 252 | "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", 253 | "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" 254 | ], 255 | "version": "==1.1.4" 256 | }, 257 | "urllib3": { 258 | "hashes": [ 259 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 260 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 261 | ], 262 | "version": "==1.25.9" 263 | }, 264 | "wcwidth": { 265 | "hashes": [ 266 | "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", 267 | "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" 268 | ], 269 | "version": "==0.1.9" 270 | }, 271 | "zipp": { 272 | "hashes": [ 273 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 274 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 275 | ], 276 | "version": "==3.1.0" 277 | } 278 | }, 279 | "develop": { 280 | "attrs": { 281 | "hashes": [ 282 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 283 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 284 | ], 285 | "version": "==19.3.0" 286 | }, 287 | "bumpversion": { 288 | "hashes": [ 289 | "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", 290 | "sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57" 291 | ], 292 | "index": "pypi", 293 | "version": "==0.5.3" 294 | }, 295 | "importlib-metadata": { 296 | "hashes": [ 297 | "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", 298 | "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" 299 | ], 300 | "markers": "python_version < '3.8'", 301 | "version": "==1.6.0" 302 | }, 303 | "more-itertools": { 304 | "hashes": [ 305 | "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", 306 | "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" 307 | ], 308 | "version": "==8.2.0" 309 | }, 310 | "packaging": { 311 | "hashes": [ 312 | "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", 313 | "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" 314 | ], 315 | "version": "==20.3" 316 | }, 317 | "pluggy": { 318 | "hashes": [ 319 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 320 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 321 | ], 322 | "version": "==0.13.1" 323 | }, 324 | "py": { 325 | "hashes": [ 326 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 327 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 328 | ], 329 | "version": "==1.8.1" 330 | }, 331 | "pyparsing": { 332 | "hashes": [ 333 | "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", 334 | "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" 335 | ], 336 | "version": "==3.0.0a1" 337 | }, 338 | "pytest": { 339 | "hashes": [ 340 | "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", 341 | "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" 342 | ], 343 | "index": "pypi", 344 | "version": "==5.4.1" 345 | }, 346 | "six": { 347 | "hashes": [ 348 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 349 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 350 | ], 351 | "version": "==1.14.0" 352 | }, 353 | "wcwidth": { 354 | "hashes": [ 355 | "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", 356 | "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" 357 | ], 358 | "version": "==0.1.9" 359 | }, 360 | "zipp": { 361 | "hashes": [ 362 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 363 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 364 | ], 365 | "version": "==3.1.0" 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /constant_sorrow/__about__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | __all__ = [ 4 | "__title__", "__summary__", "__version__", "__author__", "__email__", "__license__", "__copyright__", "__url__" 5 | ] 6 | 7 | __title__ = "constant_sorrow" 8 | 9 | __url__ = "https://github.com/nucypher/constantSorrow" 10 | 11 | __summary__ = "I am the man of constant sorrow; I've seen special values as ints, all my days." 12 | 13 | __version__ = "v0.1.0-alpha.9" 14 | 15 | __author__ = "NuCypher" 16 | 17 | __license__ = "Apache Software License v2.0" 18 | 19 | __email__ = "dev@nucypher.com" 20 | 21 | __copyright__ = 'Copyright (C) 2018 NuCypher' 22 | -------------------------------------------------------------------------------- /constant_sorrow/__init__.py: -------------------------------------------------------------------------------- 1 | from constant_sorrow.__about__ import __author__, __summary__, __title__, __version__ 2 | __all__ = ["__title__", "__summary__", "__version__", "__author__", ] 3 | 4 | 5 | from bytestring_splitter import BytestringSplitter 6 | _digest_length = 8 7 | 8 | default_constant_splitter = key_splitter = BytestringSplitter((bytes, _digest_length)) 9 | 10 | 11 | def constant_or_bytes(possible_constant): 12 | from .constants import _Constant 13 | from .constants import _constants_registry_by_hash 14 | """ 15 | Utility function for getting a constant (that has already been registered) from a serialized constant (ie, bytes of its hash) 16 | """ 17 | if _Constant in possible_constant.__class__.__bases__: 18 | result = possible_constant 19 | else: 20 | bytes_of_possible_constant = bytes(possible_constant) 21 | try: 22 | constant = _constants_registry_by_hash[bytes_of_possible_constant] 23 | result = constant 24 | except KeyError: 25 | result = bytes_of_possible_constant 26 | return result -------------------------------------------------------------------------------- /constant_sorrow/constants.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import sys 3 | from copy import deepcopy 4 | from types import ModuleType 5 | 6 | from . import _digest_length 7 | 8 | 9 | def hash_and_truncate(constant): 10 | return hashlib.sha512(constant._Constant__name.encode()).digest()[:_digest_length] 11 | 12 | 13 | class _Constant: 14 | __repr_content = None 15 | __bool_repr = None 16 | __uses_default_repr = True 17 | __has_been_stringified = False 18 | 19 | __doc__ = "Maybe your friends think this is just an instance; an object you'll never see any more." 20 | 21 | class OldKentucky(RuntimeError): 22 | pass 23 | 24 | def __init__(self, name): 25 | if not name.isupper(): 26 | raise ValueError( 27 | "Use ALL_CAPS names for constants. See https://www.python.org/dev/peps/pep-0008/#constants.") 28 | self.__name = name 29 | 30 | def __setattr__(self, key, value): 31 | if key in ("_Constant__repr_content", "_Constant__bool_repr", "_Constant__name", "_Constant__uses_default_repr", 32 | "_Constant__has_been_stringified"): 33 | super().__setattr__(key, value) 34 | else: 35 | raise TypeError("Don't try to set values on a constant. I mean, what's the point?") 36 | 37 | def __getattr__(self, item): 38 | try: 39 | return getattr(self.__repr_content, item) 40 | except AttributeError: 41 | raise AttributeError("Without a representation, you can't use {}.".format(item)) 42 | 43 | def __bytes__(self): 44 | if type(self.__repr_content) == str: 45 | return self._cast_repr(bytes, encoding="utf-8") 46 | else: 47 | return self._cast_repr(bytes) 48 | 49 | def __int__(self): 50 | return self._cast_repr(int) 51 | 52 | def __str__(self): 53 | # Unless there's an explicit repr, we want the str value to be the name. 54 | if type(self.__repr_content) is None or self.__uses_default_repr: 55 | self.__has_been_stringified = True 56 | return self.__name 57 | if type(self.__repr_content) == bytes: 58 | return self._cast_repr(str, encoding="utf-8") 59 | else: 60 | return self._cast_repr(str) 61 | 62 | def __bool__(self): 63 | if self.__bool_repr is None: 64 | if self.__repr_content is None: 65 | raise TypeError("The constant {} does not have a boolean representation.".format(self.__class__.__name__)) 66 | else: 67 | return bool(self.__repr_content) 68 | else: 69 | return self.__bool_repr 70 | 71 | def __repr__(self): 72 | if self.__repr_content is not None: 73 | return "{} ({})".format(self.__name, self.__repr_content) 74 | else: 75 | return self.__name 76 | 77 | def __add__(self, other): 78 | return self._cast_to_other_object_type_or_bytes(other) + other 79 | 80 | def __radd__(self, other): 81 | return other + self._cast_to_other_object_type_or_bytes(other) 82 | 83 | def __sub__(self, other): 84 | return self._cast_to_other_object_type_or_bytes(other) - other 85 | 86 | def __rsub__(self, other): 87 | return other - self._cast_to_other_object_type_or_bytes(other) 88 | 89 | def __mul__(self, other): 90 | return self._cast_to_other_object_type_or_bytes(other) * other 91 | 92 | def __rmul__(self, other): 93 | return other * self._cast_to_other_object_type_or_bytes(other) 94 | 95 | def __truediv__(self, other): 96 | return self._cast_to_other_object_type_or_bytes(other) / other 97 | 98 | def __rtruediv__(self, other): 99 | return other / self._cast_to_other_object_type_or_bytes(other) 100 | 101 | def __floordiv__(self, other): 102 | return self._cast_to_other_object_type_or_bytes(other) // other 103 | 104 | def __rfloordiv__(self, other): 105 | return other // self._cast_to_other_object_type_or_bytes(other) 106 | 107 | def __gt__(self, other): 108 | return self._cast_to_other_object_type_or_bytes(other) > other 109 | 110 | def __ge__(self, other): 111 | return self._cast_to_other_object_type_or_bytes(other) >= other 112 | 113 | def __lt__(self, other): 114 | return self._cast_to_other_object_type_or_bytes(other) < other 115 | 116 | def __le__(self, other): 117 | return self._cast_to_other_object_type_or_bytes(other) <= other 118 | 119 | def __eq__(self, other): 120 | try: 121 | for_comparison_sake = self._cast_to_other_object_type_or_bytes(other) 122 | except ValueError: # Can't cast to the other type, so obviously this isn't equal. 123 | return False 124 | return for_comparison_sake == other 125 | 126 | def __hash__(self): 127 | return hash(self._Constant__name) 128 | 129 | def __call__(self, representation): 130 | representation_will_change = self.__repr_content is not None and self.__repr_content is not representation 131 | if representation_will_change: 132 | message = "Can't set representation to a different value once set - it was " \ 133 | "already set to {} when you tried to set it to {}" 134 | raise ValueError(message.format(self.__repr_content, representation)) 135 | 136 | if self.__has_been_stringified: 137 | if not self.__name == str(representation): 138 | message = "This Constant has already been represented as the string {} and can't be changed to be represented by {}" 139 | raise ValueError(message.format(self.__name, str(representation))) 140 | 141 | elif self.__repr_content is representation: 142 | return self 143 | else: 144 | self.__uses_default_repr = False 145 | self.__repr_content = deepcopy(representation) 146 | 147 | return self 148 | 149 | def __index__(self): 150 | return int(self) 151 | 152 | def __len__(self): 153 | if self.__repr_content is not None: 154 | return len(self.__repr_content) 155 | else: 156 | return len(self.__name) 157 | 158 | def __iter__(self): 159 | for item in self.__repr_content: 160 | yield item 161 | 162 | @classmethod 163 | def set_constant_documentation(cls, doc): 164 | cls.__doc__ = doc 165 | 166 | @property 167 | def _sorrow_type(self): 168 | if self.__repr_content is None: 169 | raise self.OldKentucky 170 | repr_type = type(self.__repr_content) 171 | return repr_type 172 | 173 | def _cast_to_other_object_type_or_bytes(self, other): 174 | if type(other) in (bytes, int, str): 175 | # Cast to other object type if it's bytes, int, or str. 176 | caster = type(other) 177 | elif _Constant in other.__class__.__bases__: 178 | try: 179 | caster = other._sorrow_type 180 | except self.OldKentucky: 181 | caster = bytes 182 | else: 183 | caster = bytes 184 | return caster(self) 185 | 186 | def _cast_repr(self, caster, *args, **kwargs): 187 | """ 188 | Will cast this constant with the provided caster, passing args and kwargs. 189 | 190 | If there is no registered representation, will hash the name using sha512 and use the first 8 bytes 191 | of the digest. 192 | """ 193 | if self.__repr_content is None: 194 | self.__repr_content = hash_and_truncate(self) 195 | assert self.__uses_default_repr # Sanity check: we are indeed using the default repr here. If this has ever changed, something went wrong. 196 | 197 | return caster(self.__repr_content, *args, **kwargs) 198 | 199 | def bool_value(self, bool_value): 200 | if self.__repr_content is not None: 201 | if bool(self) is not bool(bool_value): 202 | raise ValueError("Based on the set representation, {} was previously {}; can't change to {}.".format( 203 | self.__name, 204 | bool(self), 205 | bool(bool_value))) 206 | 207 | if self.__bool_repr is not None: 208 | if bool(self) is not bool(bool_value): 209 | raise ValueError("The specified bool value for {} was previously {}; can't change to {}.".format( 210 | self.__name, 211 | bool(self), 212 | bool( 213 | bool_value))) 214 | 215 | self.__bool_repr = bool(bool_value) 216 | return self 217 | 218 | 219 | _constants_registry_by_name = {} 220 | _constants_registry_by_hash = {} 221 | 222 | 223 | class __ConstantFactory(ModuleType): 224 | 225 | def __getattr__(self, item): 226 | 227 | try: 228 | # External tools often look for dunders in modules; we'll raise a normal 229 | # AttributeError for those (other names that aren't all CAPS will end up with a ValueError). 230 | if (item.startswith("__") and item.endswith("__")): 231 | raise AttributeError 232 | constant = _constants_registry_by_name[item.upper()] 233 | except KeyError: 234 | 235 | _constant_class = type(item, (_Constant,), {}) # The actual class of the constant we'll return. 236 | constant = _constant_class(item) 237 | _constants_registry_by_name[item.upper()] = constant 238 | _constants_registry_by_hash[hash_and_truncate(constant)] = constant 239 | 240 | return constant 241 | 242 | sys.modules[__name__].__class__ = __ConstantFactory 243 | -------------------------------------------------------------------------------- /constant_sorrow/utilities.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nucypher/constantSorrow/2411a38204e2346e138c9cdc2e23af9ca8b5f7d0/constant_sorrow/utilities.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup 5 | from setuptools.command.install import install 6 | 7 | BASE_DIR = os.path.dirname(__file__) 8 | 9 | ABOUT = dict() 10 | with open(os.path.join(BASE_DIR, "constant_sorrow", "__about__.py")) as f: 11 | exec(f.read(), ABOUT) 12 | 13 | 14 | class VerifyVersionCommand(install): 15 | """Custom command to verify that the git tag matches our version""" 16 | description = 'verify that the git tag matches our version' 17 | 18 | def run(self): 19 | tag = os.getenv('CIRCLE_TAG') 20 | 21 | if tag != ABOUT['__version__']: 22 | info = "Git tag: {0} does not match the version of this app: {1}".format( 23 | tag, ABOUT['__version__'] 24 | ) 25 | sys.exit(info) 26 | 27 | 28 | INSTALL_REQUIRES = ['bytestring-splitter'] 29 | EXTRAS_REQUIRE = {'testing': ['pytest', 'bumpversion'], 30 | 'docs': ['sphinx', 'sphinx-autobuild']} 31 | 32 | setup(name=ABOUT['__title__'], 33 | url=ABOUT['__url__'], 34 | version=ABOUT['__version__'], 35 | author=ABOUT['__author__'], 36 | author_email=ABOUT['__email__'], 37 | description=ABOUT['__summary__'], 38 | extras_require=EXTRAS_REQUIRE, 39 | install_requires=INSTALL_REQUIRES, 40 | setup_requires=['pytest-runner'], # required for setup.py test 41 | packages=['constant_sorrow'], 42 | classifiers=[ 43 | "Development Status :: 2 - Pre-Alpha", 44 | "Natural Language :: English", 45 | "Programming Language :: Python :: Implementation", 46 | "Programming Language :: Python :: 3 :: Only", 47 | "Programming Language :: Python :: 3.5", 48 | "Programming Language :: Python :: 3.6", 49 | "Programming Language :: Python :: 3.7", 50 | "Topic :: Software Development :: Libraries :: Python Modules" 51 | ], 52 | python_requires='>=3', 53 | cmdclass={'verify': VerifyVersionCommand} 54 | ) 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nucypher/constantSorrow/2411a38204e2346e138c9cdc2e23af9ca8b5f7d0/tests/__init__.py -------------------------------------------------------------------------------- /tests/_just_import_for_testing.py: -------------------------------------------------------------------------------- 1 | from constant_sorrow import constants -------------------------------------------------------------------------------- /tests/docs_for_testing/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/docs_for_testing/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'constantSorrow tests' 21 | copyright = '2020, N/A' 22 | author = 'N/A' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | 'sphinx.ext.autodoc', 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 41 | 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = 'alabaster' 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /tests/docs_for_testing/index.rst: -------------------------------------------------------------------------------- 1 | Can we build it? 2 | ================ 3 | 4 | .. automodule:: tests._just_import_for_testing 5 | :members: -------------------------------------------------------------------------------- /tests/docs_for_testing/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/test_the_whole_story.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | from sphinx.cmd.make_mode import run_make_mode 4 | 5 | from constant_sorrow import constants, constant_or_bytes 6 | 7 | 8 | def test_establishing_a_constant(): 9 | # You get a constant by just picking an all-caps name and importing it. 10 | from constant_sorrow.constants import THIS_IS_A_VALID_CONSTANT 11 | 12 | # That name didn't exist before - you literally just make up 13 | # an all-caps name and import it. 14 | 15 | # But you can't make a constant in lower case. 16 | with pytest.raises(ValueError): 17 | constants.this_is_not_the_right_case_for_a_constant 18 | 19 | 20 | def test_different_constants_are_unequal(): 21 | # Constants with different names are not equal to each other by default. 22 | assert constants.ONE_THING != constants.ANOTHER_THING 23 | 24 | 25 | def test_same_constants_are_identical(): 26 | # However, constants with the same name are both equal and identical to one another. 27 | # They are literally the same object. 28 | from constant_sorrow.constants import SAME_THING 29 | assert constants.SAME_THING == SAME_THING 30 | assert constants.SAME_THING is SAME_THING 31 | 32 | 33 | def test_set_representation(): 34 | # Merely using a constant as a special value flag might not be enough. 35 | # You might want to represent it for the purposes of, for example, 36 | # sending it over the wire as bytes. 37 | from constant_sorrow.constants import AFRICAN_SWALLOW 38 | AFRICAN_SWALLOW(b"non-migratory") 39 | assert bytes(AFRICAN_SWALLOW) == b"non-migratory" 40 | 41 | 42 | def test_bytes_representation_default(): 43 | """By default, constants are represented as a SHA512 hash of their name, truncated to 8 bytes.""" 44 | another_constant = constants.ANOTHER_CONSTANT 45 | assert len(bytes(another_constant)) == 8 46 | assert bytes(another_constant) == constants.ANOTHER_CONSTANT 47 | 48 | 49 | def test_cast_representation(): 50 | constants.FOURTEEN(b"14") 51 | 52 | assert int(constants.FOURTEEN) == 14 53 | assert bytes(constants.FOURTEEN) == b"14" 54 | assert str(constants.FOURTEEN) == "14" 55 | 56 | 57 | def test_cant_change_representation(): 58 | constants.DINGOS("a certain dingo") 59 | 60 | # We can't change the value once it is set. 61 | with pytest.raises(ValueError): 62 | constants.DINGOS("something else") 63 | 64 | # However setting the same value again is permitted. 65 | constants.DINGOS("a certain dingo") 66 | 67 | assert bytes(constants.DINGOS) == b"a certain dingo" 68 | 69 | 70 | def test_bool_representation(): 71 | # Unlike representing as bytes, you can't automatically represent as bool. 72 | with pytest.raises(TypeError): 73 | bool(constants.NO_KNOWN_BOOL) 74 | 75 | # You can either set the representation... 76 | constants.WITH_BOOL_FROM_REPR("non-empty strings are True, obviously.") 77 | assert bool(constants.WITH_BOOL_FROM_REPR) is True 78 | 79 | # Or you can specifically set a bool representation. 80 | constants.WITH_SET_BOOL.bool_value(False) 81 | assert bool(constants.WITH_SET_BOOL) is False 82 | 83 | # A set boolean value will take precedence over the representation. 84 | # (because it's a Constant - it'd be bizarre behavior for the bool representation to change) 85 | constants.WITH_SET_BOOL("this string is non-empty, but this constant is still False.") 86 | assert bool(constants.WITH_SET_BOOL) is False 87 | 88 | # You can set it to the same value again... 89 | constants.WITH_BOOL.bool_value(False) 90 | 91 | # But you can't change the bool value once set. 92 | with pytest.raises(ValueError): 93 | constants.WITH_BOOL.bool_value(True) 94 | 95 | # Also, we can't take the first constant above and set its bool value to False. 96 | # It was True before, because prior to it having a bool_value, it was represented 97 | # by a non-empty string. So now, we can't change it to False. 98 | # After all - we want it to act like a constant. 99 | with pytest.raises(ValueError): 100 | constants.WITH_BOOL_FROM_REPR.bool_value(False) 101 | 102 | # We can, however, set it to True, because its current value is True. 103 | constants.WITH_BOOL_FROM_REPR.bool_value(True) 104 | assert bool(constants.WITH_BOOL_FROM_REPR) is True 105 | 106 | 107 | def test_str_representation(): 108 | # By default, Constants cast to str as their name: 109 | str(constants.BECAUSE_IT_IS_MY_NAME) == "BECAUSE_IT_IS_MY_NAME" 110 | 111 | # If a Constant is ever cast this way, its representation can't be changed to 112 | # an object whose string representation is different. 113 | # For example, this object that casts to the same string: 114 | 115 | class BecauseICannotHaveAnotherInMyLife: 116 | def __str__(self): 117 | return "BECAUSE_IT_IS_MY_NAME" 118 | 119 | constants.BECAUSE_IT_IS_MY_NAME(BecauseICannotHaveAnotherInMyLife()) 120 | 121 | # ...but this one will not cast away its good name, sir: 122 | class AbigailWilliams: 123 | def __str__(self): 124 | return "Mylène Demongeot" 125 | 126 | str(constants.WINONA_RYDER) 127 | 128 | with pytest.raises(ValueError): 129 | constants.WINONA_RYDER(AbigailWilliams()) 130 | 131 | # Note: it's possible to use an object which initially has a str repr that matches, but later changes. 132 | # This is almost certainly a bad idea, but if you're going to go that far, we trust you have a reason. 133 | class BecauseILieAndSignMyselfToLies: 134 | name = "JOHN PROCTOR" 135 | 136 | def __str__(self): 137 | return self.name 138 | 139 | constants.JOHN_PROCTOR(BecauseILieAndSignMyselfToLies()) 140 | BecauseILieAndSignMyselfToLies.name = "Daniel Plainview" 141 | assert str(constants.JOHN_PROCTOR) == "Daniel Plainview" 142 | # ...but again, why? This seems like a bad idea, but it's also not the place 143 | # of the Constant class to police the internal behavior of your classes. 144 | 145 | 146 | def test_cant_set_attrs(): 147 | # You can't set an attr on a constant. 148 | with pytest.raises(TypeError): 149 | constants.FISH_SLAPPING_DANCE.whatever = 4 150 | 151 | 152 | def test_repr_as_str(): 153 | # Constants are repr'd by their name; simple. 154 | from constant_sorrow.constants import HOLY_HAND_GRENADE 155 | assert repr(HOLY_HAND_GRENADE) == "HOLY_HAND_GRENADE" 156 | # If they have a representation set, that is added. 157 | HOLY_HAND_GRENADE("One, Two, Five!") 158 | assert repr(HOLY_HAND_GRENADE) == "HOLY_HAND_GRENADE (One, Two, Five!)" 159 | 160 | 161 | def test_arithmetic(): 162 | # Constants are summed by their representation. 163 | constants.FORTY_TWO(42) 164 | assert 80 + constants.FORTY_TWO == 80 + 42 165 | 166 | # Adding strings to a constant with a string representation yields a string. 167 | from constant_sorrow.constants import ROBERT 168 | ROBERT("Bob") 169 | assert ROBERT + " up and down in the water" == "Bob up and down in the water" 170 | 171 | # But adding bytes causes a cast to bytes. 172 | assert b"Sideshow " + ROBERT == b'Sideshow Bob' 173 | 174 | # Same with int. 175 | constants.THIRTY_SEVEN(b"37") 176 | assert constants.THIRTY_SEVEN + 10 == 47 177 | 178 | # sub, mul, and truediv work also. 179 | assert constants.THIRTY_SEVEN - 30 == 7 180 | assert 137 - constants.THIRTY_SEVEN == 100 181 | 182 | assert constants.THIRTY_SEVEN * 2 == 74 183 | assert 3 * constants.THIRTY_SEVEN == 111 184 | 185 | assert constants.THIRTY_SEVEN / 4 == 9.25 186 | assert 111 / constants.THIRTY_SEVEN == 3.0 187 | 188 | 189 | def test_constants_can_be_used_as_ints(): 190 | # A constant which is represented as an int can be used in most places 191 | # that an int can be used, even without casting first. 192 | constants.THREE(3) 193 | assert "humbug"[:constants.THREE] == "hum" 194 | 195 | 196 | def test_constant_length(): 197 | # By default, the length of a constant is the length of its name... 198 | assert len(constants.PEAR) == 4 199 | 200 | # ...but once a representation is set, then it will be that length. 201 | constants.PEAR("fruit") 202 | assert len(constants.PEAR) == 5 203 | 204 | 205 | def test_iterable_constant(): 206 | # Setup a new iterable constant of string literals 207 | names = ('Ron', 'Bob', 'Jerry', 'Phil', 'Keith', 'Donna') 208 | constants.MEMBERS(names) 209 | 210 | # The constant can be cast to a list... 211 | assert list(constants.MEMBERS) == list(names) 212 | 213 | # python 3.6 test 214 | # pigpen, *the_rest = list(constants.MEMBERS) 215 | # assert pigpen == 'Ron' 216 | # assert len(the_rest) == 5 217 | 218 | # ...unpacked into a function. 219 | def take_input_and_do_nothing_with_it(a, b, c, d, e, f): return True 220 | 221 | assert take_input_and_do_nothing_with_it(*constants.MEMBERS) 222 | 223 | # Hold a collection of constants... 224 | _weather = (constants.THUNDER('⛈'), 225 | constants.LIGHTNING('⚡'), 226 | constants.WIND('💨'), 227 | constants.RAIN('💧')) 228 | 229 | constants.WEATHER(_weather) 230 | 231 | def operate_on_input(a, b, c, d): 232 | assert a + b + c + d == '⛈⚡💨💧' 233 | return True 234 | 235 | # ...unpack and operate on the elements directly... 236 | assert operate_on_input(*constants.WEATHER) 237 | 238 | # ...or iterate over the constant 239 | assert ''.join(str(item) for item in constants.WEATHER) == '⛈⚡💨💧' 240 | 241 | 242 | def test_use_methods_on_representation(): 243 | # By default, accessing strange attributes with raise AttributeError. 244 | from constant_sorrow.constants import THE_OLD_MAN_THE_BOAT 245 | with pytest.raises(AttributeError): 246 | THE_OLD_MAN_THE_BOAT.upper() 247 | 248 | # If a representation is set, attributes other than ("__repr_content", "__bool_repr", "__name") pass through. 249 | THE_OLD_MAN_THE_BOAT("when Fred eats food gets thrown.") 250 | assert THE_OLD_MAN_THE_BOAT.upper() == 'WHEN FRED EATS FOOD GETS THROWN.' 251 | 252 | 253 | def test_useful_name_in_exception_output(): 254 | """ 255 | Constants use a metaclass which uses the Constant name as the string representation of the class (not only the instance). 256 | This is useful for meaningful error messages. 257 | """ 258 | try: 259 | constants.NORTHERN_RAILROAD[:7] 260 | except TypeError as e: 261 | assert e.args[0] == "'NORTHERN_RAILROAD' object is not subscriptable" 262 | else: 263 | pytest.fail("Expected this constant not to be subscriptable by default.") 264 | 265 | 266 | def test_setting_documentation(): 267 | from constant_sorrow.constants import NORTHERN_RAILROAD 268 | assert not NORTHERN_RAILROAD.__doc__ 269 | 270 | new_doc = "Maybe I'll die upon this train." 271 | NORTHERN_RAILROAD.set_constant_documentation(new_doc) 272 | assert NORTHERN_RAILROAD.__doc__ == new_doc 273 | 274 | from constant_sorrow.constants import MY_OWN_TRUE_LOVER 275 | NORTHERN_RAILROAD.set_constant_documentation("I never expect to see you again.") 276 | 277 | assert NORTHERN_RAILROAD.__doc__ != MY_OWN_TRUE_LOVER.__doc__ 278 | 279 | 280 | def test_constant_can_be_used_as_hashable_object(): 281 | from constant_sorrow.constants import DAPPER_DAN 282 | pomades = {DAPPER_DAN, "fop"} 283 | 284 | assert DAPPER_DAN in pomades 285 | dapper_dan_bytes = bytes(DAPPER_DAN) 286 | 287 | IN_A_COUPLE_WEEKS = constant_or_bytes(dapper_dan_bytes) 288 | assert DAPPER_DAN is IN_A_COUPLE_WEEKS 289 | 290 | assert DAPPER_DAN in pomades 291 | assert IN_A_COUPLE_WEEKS in pomades 292 | 293 | 294 | def test_sphinx_can_build_docs_with_constants(): 295 | # We've observed that sphinx has a problem building when trying to important constant_sorrow. 296 | docs_dir = os.path.join(os.path.dirname(__file__), 'docs_for_testing') 297 | exit_code = run_make_mode(args=["html", docs_dir, docs_dir]) 298 | assert exit_code is not 2 299 | --------------------------------------------------------------------------------