├── .circleci └── config.yml ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── docs └── index.md ├── mkdocs.yml ├── mktheapidocs ├── __init__.py ├── _version.py ├── mkapi.py └── plugin.py ├── setup.cfg ├── setup.py └── versioneer.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | defaults: 4 | - &base_docker 5 | - image: circleci/python:3.7 6 | - &run_always_org_context 7 | context: org-global 8 | filters: 9 | tags: 10 | only: /.*/ 11 | branches: 12 | ignore: gh-pages 13 | - &master_only_org_context 14 | context: org-global 15 | filters: 16 | branches: 17 | only: master 18 | - &tag_only_org_context 19 | context: org-global 20 | filters: 21 | branches: 22 | ignore: /.*/ 23 | tags: 24 | only: /.*/ 25 | - &tag_and_master_only_org_context 26 | context: org-global 27 | filters: 28 | branches: 29 | only: master 30 | tags: 31 | only: /.*/ 32 | 33 | jobs: 34 | lint_python: 35 | docker: *base_docker 36 | working_directory: /home/circleci/project 37 | steps: 38 | - checkout 39 | - run: 40 | name: Linting files with black 41 | command: | 42 | pip install black 43 | black --check . 44 | 45 | build_python_wheel: 46 | docker: *base_docker 47 | working_directory: /home/circleci/project/ 48 | steps: 49 | - checkout: 50 | path: /home/circleci/project/ 51 | - run: 52 | name: Building wheel 53 | command: python setup.py bdist_wheel 54 | - persist_to_workspace: 55 | root: /home/circleci/project 56 | paths: 57 | - dist 58 | - store_artifacts: 59 | path: /home/circleci/project/dist 60 | destination: wheel 61 | 62 | push_wheel: 63 | docker: *base_docker 64 | steps: 65 | - attach_workspace: 66 | at: /home/circleci/ 67 | - run: 68 | name: Upload Wheel 69 | command: | 70 | pip install twine 71 | twine upload /home/circleci/dist/* 72 | 73 | workflows: 74 | run_build_pipeline: 75 | jobs: 76 | - lint_python: 77 | <<: *run_always_org_context 78 | - build_python_wheel: 79 | <<: *run_always_org_context 80 | - push_wheel: 81 | requires: 82 | - lint_python 83 | - build_python_wheel 84 | <<: *tag_and_master_only_org_context 85 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | mktheapidocs/_version.py export-subst 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .idea 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to mktheapidocs will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## [Unreleased] 7 | ### Added 8 | 9 | 10 | ### Changed 11 | 12 | 13 | ### Fixed 14 | 15 | 16 | ### Removed 17 | 18 | ## [0.3.0] 19 | ### Fixed 20 | - Fixed compatibility with mkdocs 1.2 21 | 22 | ## [0.2.0] 23 | ### Fixed 24 | - Fixed compatibility with mkdocs 1.1 25 | 26 | ## [0.1.11] 27 | ### Fixed 28 | - Another case where return types have no name 29 | 30 | ## [0.1.10] 31 | ### Fixed 32 | - Return types are now shown for properties 33 | 34 | 35 | ## [0.1.9] 36 | ### Fixed 37 | - Return types are no longer mangled if the return value has no name 38 | 39 | ## [0.1.8] 40 | ### Fixed 41 | - Function signatures now work with newer black versions 42 | 43 | ## [0.1.7] 44 | ### Added 45 | - mkdocs plugin is now compatible with manually defined nav 46 | 47 | 48 | [Unreleased]: https://github.com/greenape/mktheapidocs/compare/0.3.0...master 49 | [0.3.0]: https://github.com/greenape/mktheapidocs/compare/0.2.0...0.3.0 50 | [0.2.0]: https://github.com/greenape/mktheapidocs/compare/0.1.11...0.2.0 51 | [0.1.11]: https://github.com/greenape/mktheapidocs/compare/0.1.10...0.1.11 52 | [0.1.10]: https://github.com/greenape/mktheapidocs/compare/0.1.8...0.1.10 53 | [0.1.9]: https://github.com/greenape/mktheapidocs/compare/0.1.8...0.1.9 54 | [0.1.8]: https://github.com/greenape/mktheapidocs/compare/0.1.7...0.1.8 55 | [0.1.7]: https://github.com/greenape/mktheapidocs/compare/0.1.6...0.1.7 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jonathan Gray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include mktheapidocs/_version.py 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | versioneer = "*" 8 | e1839a8 = {editable = true,path = "."} 9 | mkdocs = ">=1.2" 10 | pymdown-extensions = "*" 11 | mkdocs-material = "*" 12 | 13 | [dev-packages] 14 | twine = "*" 15 | 16 | [requires] 17 | python_version = "3.7" 18 | 19 | [pipenv] 20 | allow_prereleases = true 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "050574ed9e8fd00a63c01d94933605959310dc35d63f43a6f3145d55d203df1d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alabaster": { 20 | "hashes": [ 21 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 22 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 23 | ], 24 | "version": "==0.7.12" 25 | }, 26 | "babel": { 27 | "hashes": [ 28 | "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2", 29 | "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13" 30 | ], 31 | "markers": "python_version >= '3.6'", 32 | "version": "==2.10.1" 33 | }, 34 | "black": { 35 | "hashes": [ 36 | "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", 37 | "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", 38 | "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", 39 | "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", 40 | "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", 41 | "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", 42 | "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", 43 | "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", 44 | "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", 45 | "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", 46 | "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", 47 | "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", 48 | "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", 49 | "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", 50 | "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", 51 | "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", 52 | "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", 53 | "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", 54 | "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", 55 | "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", 56 | "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", 57 | "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", 58 | "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" 59 | ], 60 | "markers": "python_full_version >= '3.6.2'", 61 | "version": "==22.3.0" 62 | }, 63 | "certifi": { 64 | "hashes": [ 65 | "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", 66 | "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" 67 | ], 68 | "markers": "python_version >= '3.6'", 69 | "version": "==2022.5.18.1" 70 | }, 71 | "charset-normalizer": { 72 | "hashes": [ 73 | "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", 74 | "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" 75 | ], 76 | "markers": "python_version >= '3.5'", 77 | "version": "==2.0.12" 78 | }, 79 | "click": { 80 | "hashes": [ 81 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 82 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 83 | ], 84 | "markers": "python_version >= '3.7'", 85 | "version": "==8.1.3" 86 | }, 87 | "docutils": { 88 | "hashes": [ 89 | "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", 90 | "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" 91 | ], 92 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 93 | "version": "==0.18.1" 94 | }, 95 | "e1839a8": { 96 | "editable": true, 97 | "path": "." 98 | }, 99 | "ghp-import": { 100 | "hashes": [ 101 | "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", 102 | "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343" 103 | ], 104 | "version": "==2.1.0" 105 | }, 106 | "idna": { 107 | "hashes": [ 108 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 109 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 110 | ], 111 | "markers": "python_version >= '3.5'", 112 | "version": "==3.3" 113 | }, 114 | "imagesize": { 115 | "hashes": [ 116 | "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", 117 | "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" 118 | ], 119 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 120 | "version": "==1.3.0" 121 | }, 122 | "importlib-metadata": { 123 | "hashes": [ 124 | "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700", 125 | "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec" 126 | ], 127 | "markers": "python_version < '3.8'", 128 | "version": "==4.11.4" 129 | }, 130 | "jinja2": { 131 | "hashes": [ 132 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 133 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 134 | ], 135 | "markers": "python_version >= '3.7'", 136 | "version": "==3.1.2" 137 | }, 138 | "markdown": { 139 | "hashes": [ 140 | "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874", 141 | "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621" 142 | ], 143 | "markers": "python_version >= '3.6'", 144 | "version": "==3.3.7" 145 | }, 146 | "markupsafe": { 147 | "hashes": [ 148 | "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", 149 | "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", 150 | "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", 151 | "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", 152 | "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", 153 | "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", 154 | "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", 155 | "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", 156 | "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", 157 | "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", 158 | "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", 159 | "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", 160 | "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", 161 | "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", 162 | "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", 163 | "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", 164 | "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", 165 | "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", 166 | "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", 167 | "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", 168 | "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", 169 | "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", 170 | "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", 171 | "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", 172 | "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", 173 | "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", 174 | "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", 175 | "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", 176 | "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", 177 | "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", 178 | "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", 179 | "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", 180 | "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", 181 | "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", 182 | "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", 183 | "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", 184 | "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", 185 | "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", 186 | "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", 187 | "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" 188 | ], 189 | "markers": "python_version >= '3.7'", 190 | "version": "==2.1.1" 191 | }, 192 | "mergedeep": { 193 | "hashes": [ 194 | "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", 195 | "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307" 196 | ], 197 | "markers": "python_version >= '3.6'", 198 | "version": "==1.3.4" 199 | }, 200 | "mkdocs": { 201 | "hashes": [ 202 | "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde", 203 | "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea" 204 | ], 205 | "index": "pypi", 206 | "version": "==1.3.0" 207 | }, 208 | "mkdocs-material": { 209 | "hashes": [ 210 | "sha256:3dd30af894f6d5da3d8a2f8ffc04c90c4d0f1be013e654ec45f608373c131542", 211 | "sha256:4f9564af58f9c96f25c263cb705a40a82c833cb10c2626d6db6ddadedaa5b6c3" 212 | ], 213 | "index": "pypi", 214 | "version": "==8.3.3" 215 | }, 216 | "mkdocs-material-extensions": { 217 | "hashes": [ 218 | "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44", 219 | "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2" 220 | ], 221 | "markers": "python_version >= '3.6'", 222 | "version": "==1.0.3" 223 | }, 224 | "mktheapidocs": { 225 | "editable": true, 226 | "path": "." 227 | }, 228 | "mypy-extensions": { 229 | "hashes": [ 230 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 231 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 232 | ], 233 | "version": "==0.4.3" 234 | }, 235 | "numpydoc": { 236 | "hashes": [ 237 | "sha256:9494daf1c7612f59905fa09e65c9b8a90bbacb3804d91f7a94e778831e6fcfa5", 238 | "sha256:fd26258868ebcc75c816fe68e1d41e3b55bd410941acfb969dee3eef6e5cf260" 239 | ], 240 | "markers": "python_version >= '3.7'", 241 | "version": "==1.4.0" 242 | }, 243 | "packaging": { 244 | "hashes": [ 245 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 246 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 247 | ], 248 | "markers": "python_version >= '3.6'", 249 | "version": "==21.3" 250 | }, 251 | "pathspec": { 252 | "hashes": [ 253 | "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", 254 | "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" 255 | ], 256 | "version": "==0.9.0" 257 | }, 258 | "platformdirs": { 259 | "hashes": [ 260 | "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", 261 | "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" 262 | ], 263 | "markers": "python_version >= '3.7'", 264 | "version": "==2.5.2" 265 | }, 266 | "pygments": { 267 | "hashes": [ 268 | "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", 269 | "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" 270 | ], 271 | "markers": "python_version >= '3.6'", 272 | "version": "==2.12.0" 273 | }, 274 | "pymdown-extensions": { 275 | "hashes": [ 276 | "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0", 277 | "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4" 278 | ], 279 | "index": "pypi", 280 | "version": "==9.5" 281 | }, 282 | "pyparsing": { 283 | "hashes": [ 284 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 285 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 286 | ], 287 | "markers": "python_full_version >= '3.6.8'", 288 | "version": "==3.0.9" 289 | }, 290 | "python-dateutil": { 291 | "hashes": [ 292 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 293 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 294 | ], 295 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 296 | "version": "==2.8.2" 297 | }, 298 | "pytz": { 299 | "hashes": [ 300 | "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", 301 | "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" 302 | ], 303 | "version": "==2022.1" 304 | }, 305 | "pyyaml": { 306 | "hashes": [ 307 | "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", 308 | "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", 309 | "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", 310 | "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", 311 | "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", 312 | "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", 313 | "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", 314 | "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", 315 | "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", 316 | "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", 317 | "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", 318 | "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", 319 | "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", 320 | "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", 321 | "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", 322 | "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", 323 | "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", 324 | "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", 325 | "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", 326 | "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", 327 | "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", 328 | "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", 329 | "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", 330 | "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", 331 | "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", 332 | "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", 333 | "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", 334 | "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", 335 | "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", 336 | "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", 337 | "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", 338 | "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", 339 | "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" 340 | ], 341 | "markers": "python_version >= '3.6'", 342 | "version": "==6.0" 343 | }, 344 | "pyyaml-env-tag": { 345 | "hashes": [ 346 | "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", 347 | "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069" 348 | ], 349 | "markers": "python_version >= '3.6'", 350 | "version": "==0.1" 351 | }, 352 | "requests": { 353 | "hashes": [ 354 | "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", 355 | "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" 356 | ], 357 | "markers": "python_version >= '3.7' and python_version < '4'", 358 | "version": "==2.28.0" 359 | }, 360 | "six": { 361 | "hashes": [ 362 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 363 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 364 | ], 365 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 366 | "version": "==1.16.0" 367 | }, 368 | "snowballstemmer": { 369 | "hashes": [ 370 | "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", 371 | "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" 372 | ], 373 | "version": "==2.2.0" 374 | }, 375 | "sphinx": { 376 | "hashes": [ 377 | "sha256:36aa2a3c2f6d5230be94585bc5d74badd5f9ed8f3388b8eedc1726fe45b1ad30", 378 | "sha256:f4da1187785a5bc7312cc271b0e867a93946c319d106363e102936a3d9857306" 379 | ], 380 | "markers": "python_version >= '3.6'", 381 | "version": "==5.0.1" 382 | }, 383 | "sphinxcontrib-applehelp": { 384 | "hashes": [ 385 | "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", 386 | "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" 387 | ], 388 | "markers": "python_version >= '3.5'", 389 | "version": "==1.0.2" 390 | }, 391 | "sphinxcontrib-devhelp": { 392 | "hashes": [ 393 | "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", 394 | "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" 395 | ], 396 | "markers": "python_version >= '3.5'", 397 | "version": "==1.0.2" 398 | }, 399 | "sphinxcontrib-htmlhelp": { 400 | "hashes": [ 401 | "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", 402 | "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" 403 | ], 404 | "markers": "python_version >= '3.6'", 405 | "version": "==2.0.0" 406 | }, 407 | "sphinxcontrib-jsmath": { 408 | "hashes": [ 409 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 410 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 411 | ], 412 | "markers": "python_version >= '3.5'", 413 | "version": "==1.0.1" 414 | }, 415 | "sphinxcontrib-qthelp": { 416 | "hashes": [ 417 | "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", 418 | "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" 419 | ], 420 | "markers": "python_version >= '3.5'", 421 | "version": "==1.0.3" 422 | }, 423 | "sphinxcontrib-serializinghtml": { 424 | "hashes": [ 425 | "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", 426 | "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" 427 | ], 428 | "markers": "python_version >= '3.5'", 429 | "version": "==1.1.5" 430 | }, 431 | "tomli": { 432 | "hashes": [ 433 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 434 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 435 | ], 436 | "markers": "python_version < '3.11'", 437 | "version": "==2.0.1" 438 | }, 439 | "typed-ast": { 440 | "hashes": [ 441 | "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2", 442 | "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1", 443 | "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6", 444 | "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62", 445 | "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac", 446 | "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d", 447 | "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc", 448 | "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2", 449 | "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97", 450 | "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35", 451 | "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6", 452 | "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1", 453 | "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4", 454 | "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c", 455 | "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e", 456 | "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec", 457 | "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f", 458 | "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72", 459 | "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47", 460 | "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72", 461 | "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe", 462 | "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6", 463 | "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3", 464 | "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66" 465 | ], 466 | "markers": "python_version < '3.8' and implementation_name == 'cpython'", 467 | "version": "==1.5.4" 468 | }, 469 | "typing-extensions": { 470 | "hashes": [ 471 | "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", 472 | "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" 473 | ], 474 | "markers": "python_version < '3.10'", 475 | "version": "==4.2.0" 476 | }, 477 | "urllib3": { 478 | "hashes": [ 479 | "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", 480 | "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" 481 | ], 482 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 483 | "version": "==1.26.9" 484 | }, 485 | "versioneer": { 486 | "hashes": [ 487 | "sha256:9f0e9a2cb5ef521cbfd104d43a208dd9124dfb4accfa72d694e0d0430a0142bc", 488 | "sha256:dee243f8f084351a1d79eabefc2e55d58aa8a2af086a7e41602a6aafbd972a34" 489 | ], 490 | "index": "pypi", 491 | "version": "==0.22" 492 | }, 493 | "watchdog": { 494 | "hashes": [ 495 | "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412", 496 | "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654", 497 | "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306", 498 | "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33", 499 | "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd", 500 | "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7", 501 | "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892", 502 | "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609", 503 | "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6", 504 | "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1", 505 | "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591", 506 | "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d", 507 | "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d", 508 | "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c", 509 | "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3", 510 | "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39", 511 | "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213", 512 | "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330", 513 | "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428", 514 | "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1", 515 | "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846", 516 | "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153", 517 | "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3", 518 | "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9", 519 | "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658" 520 | ], 521 | "markers": "python_version >= '3.6'", 522 | "version": "==2.1.9" 523 | }, 524 | "zipp": { 525 | "hashes": [ 526 | "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", 527 | "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" 528 | ], 529 | "markers": "python_version >= '3.7'", 530 | "version": "==3.8.0" 531 | } 532 | }, 533 | "develop": { 534 | "bleach": { 535 | "hashes": [ 536 | "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1", 537 | "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565" 538 | ], 539 | "markers": "python_version >= '3.7'", 540 | "version": "==5.0.0" 541 | }, 542 | "certifi": { 543 | "hashes": [ 544 | "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", 545 | "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" 546 | ], 547 | "markers": "python_version >= '3.6'", 548 | "version": "==2022.5.18.1" 549 | }, 550 | "charset-normalizer": { 551 | "hashes": [ 552 | "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", 553 | "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" 554 | ], 555 | "markers": "python_version >= '3.5'", 556 | "version": "==2.0.12" 557 | }, 558 | "commonmark": { 559 | "hashes": [ 560 | "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", 561 | "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" 562 | ], 563 | "version": "==0.9.1" 564 | }, 565 | "docutils": { 566 | "hashes": [ 567 | "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", 568 | "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" 569 | ], 570 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 571 | "version": "==0.18.1" 572 | }, 573 | "idna": { 574 | "hashes": [ 575 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 576 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 577 | ], 578 | "markers": "python_version >= '3.5'", 579 | "version": "==3.3" 580 | }, 581 | "importlib-metadata": { 582 | "hashes": [ 583 | "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700", 584 | "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec" 585 | ], 586 | "markers": "python_version < '3.8'", 587 | "version": "==4.11.4" 588 | }, 589 | "keyring": { 590 | "hashes": [ 591 | "sha256:372ff2fc43ab779e3f87911c26e6c7acc8bb440cbd82683e383ca37594cb0617", 592 | "sha256:3ac00c26e4c93739e19103091a9986a9f79665a78cf15a4df1dba7ea9ac8da2f" 593 | ], 594 | "markers": "python_version >= '3.7'", 595 | "version": "==23.6.0" 596 | }, 597 | "pkginfo": { 598 | "hashes": [ 599 | "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594", 600 | "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c" 601 | ], 602 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 603 | "version": "==1.8.3" 604 | }, 605 | "pygments": { 606 | "hashes": [ 607 | "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", 608 | "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" 609 | ], 610 | "markers": "python_version >= '3.6'", 611 | "version": "==2.12.0" 612 | }, 613 | "readme-renderer": { 614 | "hashes": [ 615 | "sha256:73b84905d091c31f36e50b4ae05ae2acead661f6a09a9abb4df7d2ddcdb6a698", 616 | "sha256:a727999acfc222fc21d82a12ed48c957c4989785e5865807c65a487d21677497" 617 | ], 618 | "markers": "python_version >= '3.7'", 619 | "version": "==35.0" 620 | }, 621 | "requests": { 622 | "hashes": [ 623 | "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", 624 | "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" 625 | ], 626 | "markers": "python_version >= '3.7' and python_version < '4'", 627 | "version": "==2.28.0" 628 | }, 629 | "requests-toolbelt": { 630 | "hashes": [ 631 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 632 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 633 | ], 634 | "version": "==0.9.1" 635 | }, 636 | "rfc3986": { 637 | "hashes": [ 638 | "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", 639 | "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" 640 | ], 641 | "markers": "python_version >= '3.7'", 642 | "version": "==2.0.0" 643 | }, 644 | "rich": { 645 | "hashes": [ 646 | "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2", 647 | "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec" 648 | ], 649 | "markers": "python_version < '4' and python_full_version >= '3.6.3'", 650 | "version": "==12.4.4" 651 | }, 652 | "six": { 653 | "hashes": [ 654 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 655 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 656 | ], 657 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 658 | "version": "==1.16.0" 659 | }, 660 | "twine": { 661 | "hashes": [ 662 | "sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e", 663 | "sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0" 664 | ], 665 | "index": "pypi", 666 | "version": "==4.0.1" 667 | }, 668 | "typing-extensions": { 669 | "hashes": [ 670 | "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", 671 | "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" 672 | ], 673 | "markers": "python_version < '3.10'", 674 | "version": "==4.2.0" 675 | }, 676 | "urllib3": { 677 | "hashes": [ 678 | "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", 679 | "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" 680 | ], 681 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 682 | "version": "==1.26.9" 683 | }, 684 | "webencodings": { 685 | "hashes": [ 686 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 687 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 688 | ], 689 | "version": "==0.5.1" 690 | }, 691 | "zipp": { 692 | "hashes": [ 693 | "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", 694 | "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" 695 | ], 696 | "markers": "python_version >= '3.7'", 697 | "version": "==3.8.0" 698 | } 699 | } 700 | } 701 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mktheapidocs 2 | 3 | A plugin for [MkDocs](http://mkdocs.org) to generate API documentation from [numpydoc](http://numpydoc.readthedocs.org) style docstrings, and type annotations. 4 | 5 | ## Installation 6 | 7 | `pip install mktheapidocs[plugin]` 8 | 9 | ## Usage 10 | 11 | Add to the plugins section of your mkdocs.yml file, and list the modules you want to document. 12 | 13 | ```yaml 14 | plugins: 15 | - mktheapidocs: 16 | modules: 17 | : 18 | section: 19 | source_repo: 20 | hidden: ["submodules", "to", "omit"] 21 | ``` 22 | 23 | The plugin will find, and document all submodules, classes, attributes, functions etc. and, if you're using `mkdocs serve`, changes to the documentation will be reflected live. 24 | 25 | If you want to manually configure your nav, then you can specify where the api documentation section will be using an `api-docs-` placeholder. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: mktheapidocs 2 | repo_name: 'greenape/mktheapidocs' 3 | repo_url: https://github.com/greenape/mktheapidocs 4 | plugins: 5 | - mktheapidocs: 6 | modules: 7 | mktheapidocs: 8 | section: api 9 | source_repo: "https://github.com/greenape/mktheapidocs" 10 | 11 | theme: 12 | name: 'material' 13 | icon: 14 | repo: fontawesome/brands/github 15 | # Don't include MkDocs' JavaScript 16 | include_search_page: false 17 | search_index_only: true 18 | 19 | # Default values, taken from mkdocs_theme.yml 20 | language: en 21 | features: 22 | - navigation.tabs 23 | palette: 24 | primary: "green" 25 | accent: "green" 26 | font: 27 | text: Roboto 28 | code: Roboto Mono 29 | 30 | # Extensions 31 | markdown_extensions: 32 | - markdown.extensions.admonition 33 | - markdown.extensions.codehilite: 34 | guess_lang: false 35 | - markdown.extensions.def_list 36 | - markdown.extensions.footnotes 37 | - markdown.extensions.meta 38 | - markdown.extensions.toc: 39 | permalink: true 40 | - pymdownx.arithmatex 41 | - pymdownx.betterem: 42 | smart_enable: all 43 | - pymdownx.caret 44 | - pymdownx.critic 45 | - pymdownx.details 46 | - pymdownx.emoji: 47 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 48 | - pymdownx.inlinehilite 49 | - pymdownx.keys 50 | - pymdownx.magiclink 51 | - pymdownx.mark 52 | - pymdownx.smartsymbols 53 | - pymdownx.superfences 54 | - pymdownx.tasklist: 55 | custom_checkbox: true 56 | - pymdownx.tilde 57 | 58 | 59 | extra: 60 | social: 61 | - icon: fontawesome/brands/github 62 | link: 'https://github.com/greenape/mktheapidocs' -------------------------------------------------------------------------------- /mktheapidocs/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import get_versions 2 | 3 | __version__ = get_versions()["version"] 4 | del get_versions 5 | -------------------------------------------------------------------------------- /mktheapidocs/_version.py: -------------------------------------------------------------------------------- 1 | # This file helps to compute a version number in source trees obtained from 2 | # git-archive tarball (such as those provided by githubs download-from-tag 3 | # feature). Distribution tarballs (built by setup.py sdist) and build 4 | # directories (produced by setup.py build) will contain a much shorter file 5 | # that just contains the computed version number. 6 | 7 | # This file is released into the public domain. Generated by 8 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 9 | 10 | """Git implementation of _version.py.""" 11 | 12 | import errno 13 | import os 14 | import re 15 | import subprocess 16 | import sys 17 | 18 | 19 | def get_keywords(): 20 | """Get the keywords needed to look up the version information.""" 21 | # these strings will be replaced by git during git-archive. 22 | # setup.py/versioneer.py will grep for the variable names, so they must 23 | # each be defined on a line of their own. _version.py will just call 24 | # get_keywords(). 25 | git_refnames = " (HEAD -> master, tag: 0.3.1)" 26 | git_full = "4adec7c8651135a1635e19c2dbcafd5ea6cd341d" 27 | git_date = "2022-06-10 15:36:01 +0100" 28 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 29 | return keywords 30 | 31 | 32 | class VersioneerConfig: 33 | """Container for Versioneer configuration parameters.""" 34 | 35 | 36 | def get_config(): 37 | """Create, populate and return the VersioneerConfig() object.""" 38 | # these strings are filled in when 'setup.py versioneer' creates 39 | # _version.py 40 | cfg = VersioneerConfig() 41 | cfg.VCS = "git" 42 | cfg.style = "pep440" 43 | cfg.tag_prefix = "" 44 | cfg.parentdir_prefix = "" 45 | cfg.versionfile_source = "mktheapidocs/_version.py" 46 | cfg.verbose = False 47 | return cfg 48 | 49 | 50 | class NotThisMethod(Exception): 51 | """Exception raised if a method is not valid for the current scenario.""" 52 | 53 | 54 | LONG_VERSION_PY = {} 55 | HANDLERS = {} 56 | 57 | 58 | def register_vcs_handler(vcs, method): # decorator 59 | """Decorator to mark a method as the handler for a particular VCS.""" 60 | 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | 68 | return decorate 69 | 70 | 71 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen( 80 | [c] + args, 81 | cwd=cwd, 82 | env=env, 83 | stdout=subprocess.PIPE, 84 | stderr=(subprocess.PIPE if hide_stderr else None), 85 | ) 86 | break 87 | except EnvironmentError: 88 | e = sys.exc_info()[1] 89 | if e.errno == errno.ENOENT: 90 | continue 91 | if verbose: 92 | print("unable to run %s" % dispcmd) 93 | print(e) 94 | return None, None 95 | else: 96 | if verbose: 97 | print("unable to find command, tried %s" % (commands,)) 98 | return None, None 99 | stdout = p.communicate()[0].strip() 100 | if sys.version_info[0] >= 3: 101 | stdout = stdout.decode() 102 | if p.returncode != 0: 103 | if verbose: 104 | print("unable to run %s (error)" % dispcmd) 105 | print("stdout was %s" % stdout) 106 | return None, p.returncode 107 | return stdout, p.returncode 108 | 109 | 110 | def versions_from_parentdir(parentdir_prefix, root, verbose): 111 | """Try to determine the version from the parent directory name. 112 | 113 | Source tarballs conventionally unpack into a directory that includes both 114 | the project name and a version string. We will also support searching up 115 | two directory levels for an appropriately named parent directory 116 | """ 117 | rootdirs = [] 118 | 119 | for i in range(3): 120 | dirname = os.path.basename(root) 121 | if dirname.startswith(parentdir_prefix): 122 | return { 123 | "version": dirname[len(parentdir_prefix) :], 124 | "full-revisionid": None, 125 | "dirty": False, 126 | "error": None, 127 | "date": None, 128 | } 129 | else: 130 | rootdirs.append(root) 131 | root = os.path.dirname(root) # up a level 132 | 133 | if verbose: 134 | print( 135 | "Tried directories %s but none started with prefix %s" 136 | % (str(rootdirs), parentdir_prefix) 137 | ) 138 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 139 | 140 | 141 | @register_vcs_handler("git", "get_keywords") 142 | def git_get_keywords(versionfile_abs): 143 | """Extract version information from the given file.""" 144 | # the code embedded in _version.py can just fetch the value of these 145 | # keywords. When used from setup.py, we don't want to import _version.py, 146 | # so we do it with a regexp instead. This function is not used from 147 | # _version.py. 148 | keywords = {} 149 | try: 150 | f = open(versionfile_abs, "r") 151 | for line in f.readlines(): 152 | if line.strip().startswith("git_refnames ="): 153 | mo = re.search(r'=\s*"(.*)"', line) 154 | if mo: 155 | keywords["refnames"] = mo.group(1) 156 | if line.strip().startswith("git_full ="): 157 | mo = re.search(r'=\s*"(.*)"', line) 158 | if mo: 159 | keywords["full"] = mo.group(1) 160 | if line.strip().startswith("git_date ="): 161 | mo = re.search(r'=\s*"(.*)"', line) 162 | if mo: 163 | keywords["date"] = mo.group(1) 164 | f.close() 165 | except EnvironmentError: 166 | pass 167 | return keywords 168 | 169 | 170 | @register_vcs_handler("git", "keywords") 171 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 172 | """Get version information from git keywords.""" 173 | if not keywords: 174 | raise NotThisMethod("no keywords at all, weird") 175 | date = keywords.get("date") 176 | if date is not None: 177 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 178 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 179 | # -like" string, which we must then edit to make compliant), because 180 | # it's been around since git-1.5.3, and it's too difficult to 181 | # discover which version we're using, or to work around using an 182 | # older one. 183 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 184 | refnames = keywords["refnames"].strip() 185 | if refnames.startswith("$Format"): 186 | if verbose: 187 | print("keywords are unexpanded, not using") 188 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 189 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 190 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 191 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 192 | TAG = "tag: " 193 | tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) 194 | if not tags: 195 | # Either we're using git < 1.8.3, or there really are no tags. We use 196 | # a heuristic: assume all version tags have a digit. The old git %d 197 | # expansion behaves like git log --decorate=short and strips out the 198 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 199 | # between branches and tags. By ignoring refnames without digits, we 200 | # filter out many common branch names like "release" and 201 | # "stabilization", as well as "HEAD" and "master". 202 | tags = set([r for r in refs if re.search(r"\d", r)]) 203 | if verbose: 204 | print("discarding '%s', no digits" % ",".join(refs - tags)) 205 | if verbose: 206 | print("likely tags: %s" % ",".join(sorted(tags))) 207 | for ref in sorted(tags): 208 | # sorting will prefer e.g. "2.0" over "2.0rc1" 209 | if ref.startswith(tag_prefix): 210 | r = ref[len(tag_prefix) :] 211 | if verbose: 212 | print("picking %s" % r) 213 | return { 214 | "version": r, 215 | "full-revisionid": keywords["full"].strip(), 216 | "dirty": False, 217 | "error": None, 218 | "date": date, 219 | } 220 | # no suitable tags, so version is "0+unknown", but full hex is still there 221 | if verbose: 222 | print("no suitable tags, using unknown + full revision id") 223 | return { 224 | "version": "0+unknown", 225 | "full-revisionid": keywords["full"].strip(), 226 | "dirty": False, 227 | "error": "no suitable tags", 228 | "date": None, 229 | } 230 | 231 | 232 | @register_vcs_handler("git", "pieces_from_vcs") 233 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 234 | """Get version from 'git describe' in the root of the source tree. 235 | 236 | This only gets called if the git-archive 'subst' keywords were *not* 237 | expanded, and _version.py hasn't already been rewritten with a short 238 | version string, meaning we're inside a checked out source tree. 239 | """ 240 | GITS = ["git"] 241 | if sys.platform == "win32": 242 | GITS = ["git.cmd", "git.exe"] 243 | 244 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) 245 | if rc != 0: 246 | if verbose: 247 | print("Directory %s not under git control" % root) 248 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 249 | 250 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 251 | # if there isn't one, this yields HEX[-dirty] (no NUM) 252 | describe_out, rc = run_command( 253 | GITS, 254 | [ 255 | "describe", 256 | "--tags", 257 | "--dirty", 258 | "--always", 259 | "--long", 260 | "--match", 261 | "%s*" % tag_prefix, 262 | ], 263 | cwd=root, 264 | ) 265 | # --long was added in git-1.5.5 266 | if describe_out is None: 267 | raise NotThisMethod("'git describe' failed") 268 | describe_out = describe_out.strip() 269 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 270 | if full_out is None: 271 | raise NotThisMethod("'git rev-parse' failed") 272 | full_out = full_out.strip() 273 | 274 | pieces = {} 275 | pieces["long"] = full_out 276 | pieces["short"] = full_out[:7] # maybe improved later 277 | pieces["error"] = None 278 | 279 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 280 | # TAG might have hyphens. 281 | git_describe = describe_out 282 | 283 | # look for -dirty suffix 284 | dirty = git_describe.endswith("-dirty") 285 | pieces["dirty"] = dirty 286 | if dirty: 287 | git_describe = git_describe[: git_describe.rindex("-dirty")] 288 | 289 | # now we have TAG-NUM-gHEX or HEX 290 | 291 | if "-" in git_describe: 292 | # TAG-NUM-gHEX 293 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 294 | if not mo: 295 | # unparseable. Maybe git-describe is misbehaving? 296 | pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out 297 | return pieces 298 | 299 | # tag 300 | full_tag = mo.group(1) 301 | if not full_tag.startswith(tag_prefix): 302 | if verbose: 303 | fmt = "tag '%s' doesn't start with prefix '%s'" 304 | print(fmt % (full_tag, tag_prefix)) 305 | pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( 306 | full_tag, 307 | tag_prefix, 308 | ) 309 | return pieces 310 | pieces["closest-tag"] = full_tag[len(tag_prefix) :] 311 | 312 | # distance: number of commits since tag 313 | pieces["distance"] = int(mo.group(2)) 314 | 315 | # commit: short hex revision ID 316 | pieces["short"] = mo.group(3) 317 | 318 | else: 319 | # HEX: no tags 320 | pieces["closest-tag"] = None 321 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) 322 | pieces["distance"] = int(count_out) # total number of commits 323 | 324 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 325 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ 326 | 0 327 | ].strip() 328 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 329 | 330 | return pieces 331 | 332 | 333 | def plus_or_dot(pieces): 334 | """Return a + if we don't already have one, else return a .""" 335 | if "+" in pieces.get("closest-tag", ""): 336 | return "." 337 | return "+" 338 | 339 | 340 | def render_pep440(pieces): 341 | """Build up version string, with post-release "local version identifier". 342 | 343 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 344 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 345 | 346 | Exceptions: 347 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 348 | """ 349 | if pieces["closest-tag"]: 350 | rendered = pieces["closest-tag"] 351 | if pieces["distance"] or pieces["dirty"]: 352 | rendered += plus_or_dot(pieces) 353 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 354 | if pieces["dirty"]: 355 | rendered += ".dirty" 356 | else: 357 | # exception #1 358 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 359 | if pieces["dirty"]: 360 | rendered += ".dirty" 361 | return rendered 362 | 363 | 364 | def render_pep440_pre(pieces): 365 | """TAG[.post.devDISTANCE] -- No -dirty. 366 | 367 | Exceptions: 368 | 1: no tags. 0.post.devDISTANCE 369 | """ 370 | if pieces["closest-tag"]: 371 | rendered = pieces["closest-tag"] 372 | if pieces["distance"]: 373 | rendered += ".post.dev%d" % pieces["distance"] 374 | else: 375 | # exception #1 376 | rendered = "0.post.dev%d" % pieces["distance"] 377 | return rendered 378 | 379 | 380 | def render_pep440_post(pieces): 381 | """TAG[.postDISTANCE[.dev0]+gHEX] . 382 | 383 | The ".dev0" means dirty. Note that .dev0 sorts backwards 384 | (a dirty tree will appear "older" than the corresponding clean one), 385 | but you shouldn't be releasing software with -dirty anyways. 386 | 387 | Exceptions: 388 | 1: no tags. 0.postDISTANCE[.dev0] 389 | """ 390 | if pieces["closest-tag"]: 391 | rendered = pieces["closest-tag"] 392 | if pieces["distance"] or pieces["dirty"]: 393 | rendered += ".post%d" % pieces["distance"] 394 | if pieces["dirty"]: 395 | rendered += ".dev0" 396 | rendered += plus_or_dot(pieces) 397 | rendered += "g%s" % pieces["short"] 398 | else: 399 | # exception #1 400 | rendered = "0.post%d" % pieces["distance"] 401 | if pieces["dirty"]: 402 | rendered += ".dev0" 403 | rendered += "+g%s" % pieces["short"] 404 | return rendered 405 | 406 | 407 | def render_pep440_old(pieces): 408 | """TAG[.postDISTANCE[.dev0]] . 409 | 410 | The ".dev0" means dirty. 411 | 412 | Eexceptions: 413 | 1: no tags. 0.postDISTANCE[.dev0] 414 | """ 415 | if pieces["closest-tag"]: 416 | rendered = pieces["closest-tag"] 417 | if pieces["distance"] or pieces["dirty"]: 418 | rendered += ".post%d" % pieces["distance"] 419 | if pieces["dirty"]: 420 | rendered += ".dev0" 421 | else: 422 | # exception #1 423 | rendered = "0.post%d" % pieces["distance"] 424 | if pieces["dirty"]: 425 | rendered += ".dev0" 426 | return rendered 427 | 428 | 429 | def render_git_describe(pieces): 430 | """TAG[-DISTANCE-gHEX][-dirty]. 431 | 432 | Like 'git describe --tags --dirty --always'. 433 | 434 | Exceptions: 435 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 436 | """ 437 | if pieces["closest-tag"]: 438 | rendered = pieces["closest-tag"] 439 | if pieces["distance"]: 440 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 441 | else: 442 | # exception #1 443 | rendered = pieces["short"] 444 | if pieces["dirty"]: 445 | rendered += "-dirty" 446 | return rendered 447 | 448 | 449 | def render_git_describe_long(pieces): 450 | """TAG-DISTANCE-gHEX[-dirty]. 451 | 452 | Like 'git describe --tags --dirty --always -long'. 453 | The distance/hash is unconditional. 454 | 455 | Exceptions: 456 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 457 | """ 458 | if pieces["closest-tag"]: 459 | rendered = pieces["closest-tag"] 460 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 461 | else: 462 | # exception #1 463 | rendered = pieces["short"] 464 | if pieces["dirty"]: 465 | rendered += "-dirty" 466 | return rendered 467 | 468 | 469 | def render(pieces, style): 470 | """Render the given version pieces into the requested style.""" 471 | if pieces["error"]: 472 | return { 473 | "version": "unknown", 474 | "full-revisionid": pieces.get("long"), 475 | "dirty": None, 476 | "error": pieces["error"], 477 | "date": None, 478 | } 479 | 480 | if not style or style == "default": 481 | style = "pep440" # the default 482 | 483 | if style == "pep440": 484 | rendered = render_pep440(pieces) 485 | elif style == "pep440-pre": 486 | rendered = render_pep440_pre(pieces) 487 | elif style == "pep440-post": 488 | rendered = render_pep440_post(pieces) 489 | elif style == "pep440-old": 490 | rendered = render_pep440_old(pieces) 491 | elif style == "git-describe": 492 | rendered = render_git_describe(pieces) 493 | elif style == "git-describe-long": 494 | rendered = render_git_describe_long(pieces) 495 | else: 496 | raise ValueError("unknown style '%s'" % style) 497 | 498 | return { 499 | "version": rendered, 500 | "full-revisionid": pieces["long"], 501 | "dirty": pieces["dirty"], 502 | "error": None, 503 | "date": pieces.get("date"), 504 | } 505 | 506 | 507 | def get_versions(): 508 | """Get version information or return default if unable to do so.""" 509 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 510 | # __file__, we can work backwards from there to the root. Some 511 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 512 | # case we can only use expanded keywords. 513 | 514 | cfg = get_config() 515 | verbose = cfg.verbose 516 | 517 | try: 518 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) 519 | except NotThisMethod: 520 | pass 521 | 522 | try: 523 | root = os.path.realpath(__file__) 524 | # versionfile_source is the relative path from the top of the source 525 | # tree (where the .git directory might live) to this file. Invert 526 | # this to find the root from __file__. 527 | for i in cfg.versionfile_source.split("/"): 528 | root = os.path.dirname(root) 529 | except NameError: 530 | return { 531 | "version": "0+unknown", 532 | "full-revisionid": None, 533 | "dirty": None, 534 | "error": "unable to find root of source tree", 535 | "date": None, 536 | } 537 | 538 | try: 539 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 540 | return render(pieces, cfg.style) 541 | except NotThisMethod: 542 | pass 543 | 544 | try: 545 | if cfg.parentdir_prefix: 546 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 547 | except NotThisMethod: 548 | pass 549 | 550 | return { 551 | "version": "0+unknown", 552 | "full-revisionid": None, 553 | "dirty": None, 554 | "error": "unable to compute version", 555 | "date": None, 556 | } 557 | -------------------------------------------------------------------------------- /mktheapidocs/mkapi.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | import pathlib 4 | import importlib 5 | import black 6 | import re 7 | import click 8 | import enum 9 | from numpydoc.docscrape import NumpyDocString, FunctionDoc, ClassDoc 10 | from functools import cmp_to_key 11 | 12 | 13 | def get_line(thing): 14 | """ 15 | Get the line number for something. 16 | Parameters 17 | ---------- 18 | thing : function, class, module 19 | 20 | Returns 21 | ------- 22 | int 23 | Line number in the source file 24 | """ 25 | try: 26 | return inspect.getsourcelines(thing)[1] 27 | except TypeError: 28 | # Might be a property 29 | return inspect.getsourcelines(thing.fget)[1] 30 | except Exception as e: 31 | # print(thing) 32 | raise e 33 | 34 | 35 | def _sort_modules(mods): 36 | """Always sort `index` or `README` as first filename in list.""" 37 | 38 | def compare(x, y): 39 | x = x[1] 40 | y = y[1] 41 | if x == y: 42 | return 0 43 | if y.stem == "__init__.py": 44 | return 1 45 | if x.stem == "__init__.py" or x < y: 46 | return -1 47 | return 1 48 | 49 | return sorted(mods, key=cmp_to_key(compare)) 50 | 51 | 52 | def get_submodule_files(module, hide=["_version"]): 53 | modules = set() 54 | module_file = pathlib.Path(module.__file__).parent 55 | for root, dirs, files in os.walk(module_file): 56 | module_path = pathlib.Path(root).relative_to(module_file.parent) 57 | if not module_path.parts[-1].startswith("_"): 58 | try: 59 | for file in files: 60 | module_name = ( 61 | "" if "__init__.py" == file else inspect.getmodulename(file) 62 | ) 63 | if module_name is not None and module_name not in hide: 64 | submodule = importlib.import_module( 65 | ".".join((module_path / module_name).parts) 66 | ) 67 | modules.add((submodule, module_path / file)) 68 | except ModuleNotFoundError: 69 | print(f"Skipping {'.'.join(module_path.parts)} - not a module.") 70 | return _sort_modules(modules) 71 | 72 | 73 | def get_all_modules_from_files(module, hide=["__init__", "_version"]): 74 | modules = set() 75 | module_file = pathlib.Path(module.__file__).parent.parent 76 | dir_was = pathlib.Path().absolute() 77 | os.chdir(module_file) 78 | for root, dirs, files in os.walk(module.__name__): 79 | module_path = pathlib.Path(root) 80 | if not module_path.parts[-1].startswith("_"): 81 | try: 82 | module = importlib.import_module(".".join(module_path.parts)) 83 | if not module.__name__.startswith("_"): 84 | modules.add((module.__name__, module, False, module_path)) 85 | for file in files: 86 | module_name = inspect.getmodulename(file) 87 | if module_name is not None and module_name not in hide: 88 | submodule = importlib.import_module( 89 | ".".join( 90 | (module_path / inspect.getmodulename(file)).parts 91 | ) 92 | ) 93 | if not module.__name__.startswith( 94 | "_" 95 | ) and not submodule.__name__.startswith("_"): 96 | modules.add( 97 | ( 98 | submodule.__name__, 99 | submodule, 100 | True, 101 | module_path.absolute() / file, 102 | ) 103 | ) 104 | except ModuleNotFoundError: 105 | print(f"Skipping {'.'.join(module_path.parts)} - not a module.") 106 | os.chdir(dir_was) 107 | return modules 108 | 109 | 110 | def get_classes(module): 111 | return set( 112 | [ 113 | x 114 | for x in inspect.getmembers(module, inspect.isclass) 115 | if (not x[0].startswith("_")) 116 | and x[1].__module__ == module.__name__ 117 | and not type(x[1]) is enum.EnumMeta 118 | ] 119 | ) 120 | 121 | 122 | def get_enums(module): 123 | return set( 124 | [ 125 | x 126 | for x in inspect.getmembers(module, inspect.isclass) 127 | if (not x[0].startswith("_")) 128 | and x[1].__module__ == module.__name__ 129 | and type(x[1]) is enum.EnumMeta 130 | ] 131 | ) 132 | 133 | 134 | def get_funcs(module): 135 | return set( 136 | [ 137 | x 138 | for x in inspect.getmembers(module, inspect.isfunction) 139 | if (not x[0].startswith("_")) and x[1].__module__ == module.__name__ 140 | ] 141 | ) 142 | 143 | 144 | def get_available_funcs(module): 145 | shared_root = module.__name__.split(".")[0] 146 | return set( 147 | [ 148 | x 149 | for x in inspect.getmembers(module, inspect.isfunction) 150 | if (not x[0].startswith("_")) 151 | and x[1].__module__.split(".")[0] == shared_root 152 | ] 153 | ) 154 | 155 | 156 | def get_available_classes(module): 157 | shared_root = module.__name__.split(".")[0] 158 | return set( 159 | [ 160 | x 161 | for x in inspect.getmembers(module, inspect.isclass) 162 | if (not x[0].startswith("_")) 163 | and x[1].__module__.split(".")[0] == shared_root 164 | ] 165 | ) 166 | 167 | 168 | def deffed_here(thing, holder): 169 | return inspect.getfile(thing) == inspect.getfile(holder) 170 | 171 | 172 | def fix_footnotes(s): 173 | return re.subn("\[([0-9]+)\]_", r"[^\1]", s)[0] 174 | 175 | 176 | def mangle_types(types): 177 | default = re.findall("default .+", types) 178 | mangled = [] 179 | try: 180 | if len(default): 181 | default = re.sub("default (.+)", r"default ``\1``", default[0]) 182 | mangled.append(default) 183 | types = re.sub("default .+", "", types) 184 | curlied = re.findall("{.+}", types) 185 | no_curls = re.subn("{.+},?", "", types)[0] 186 | annotated = re.findall("[a-zA-Z]+\[.+\]", no_curls) 187 | no_curls = re.subn("[a-zA-Z]+\[.+\],?", "", no_curls)[0] 188 | ts = [t.strip() for t in no_curls.split(",")] 189 | ts = [t.split(" or ") for t in ts] 190 | ts = [item for sublist in ts for item in sublist if item != ""] 191 | types = ts + curlied + annotated 192 | for ix, typ in enumerate(types): 193 | ts = [f"``{t}``" for t in typ.split(" of ")] 194 | mangled.append(" of ".join(ts)) 195 | except Exception as e: 196 | # print(e) 197 | # print(default) 198 | # print(types) 199 | raise e 200 | output = reversed(mangled) 201 | 202 | return ", ".join(output) 203 | 204 | 205 | def mangle_examples(examples): 206 | was_in_python = False 207 | in_python = False 208 | lines = [] 209 | for line in examples: 210 | if line.startswith(">>>"): 211 | in_python = True 212 | if line == "": 213 | in_python = False 214 | if not in_python and was_in_python: 215 | lines.append("\n```\n") 216 | elif not in_python: 217 | lines.append(f"{line} ") 218 | elif in_python and not was_in_python: 219 | lines.append("\n```python\n") 220 | lines.append(re.sub(">>> ", "", line) + "\n") 221 | else: 222 | lines.append(re.sub(">>> ", "", line) + "\n") 223 | was_in_python = in_python 224 | if was_in_python: 225 | lines.append("\n```") 226 | lines.append("\n\n") 227 | return lines 228 | 229 | 230 | def notes_section(doc): 231 | lines = [] 232 | if "Notes" in doc and len(doc["Notes"]) > 0: 233 | lines.append("!!! note\n") 234 | lines.append(f" {' '.join(doc['Notes'])}\n\n") 235 | return lines 236 | 237 | 238 | def warnings_section(doc): 239 | lines = [] 240 | if "Warnings" in doc and len(doc["Warnings"]) > 0: 241 | lines.append("!!! warning\n") 242 | lines.append(f" {' '.join(doc['Warnings'])}\n\n") 243 | return lines 244 | 245 | 246 | def refs_section(doc): 247 | """ 248 | Generate a References section. 249 | 250 | Parameters 251 | ---------- 252 | doc : dict 253 | Dictionary produced by numpydoc 254 | 255 | Returns 256 | ------- 257 | list of str 258 | Markdown for references section 259 | """ 260 | lines = [] 261 | if "References" in doc and len(doc["References"]) > 0: 262 | # print("Found refs") 263 | for ref in doc["References"]: 264 | # print(ref) 265 | ref_num = re.findall("\[([0-9]+)\]", ref)[0] 266 | # print(ref_num) 267 | ref_body = " ".join(ref.split(" ")[2:]) 268 | # print(f"[^{ref_num}] {ref_body}" + "\n") 269 | lines.append(f"[^{ref_num}]: {ref_body}" + "\n\n") 270 | # print(lines) 271 | return lines 272 | 273 | 274 | def examples_section(doc, header_level): 275 | """ 276 | Generate markdown for Examples section. 277 | 278 | Parameters 279 | ---------- 280 | doc : dict 281 | Dict from numpydoc 282 | header_level : int 283 | Number of `#`s to use for header 284 | 285 | Returns 286 | ------- 287 | list of str 288 | Markdown for examples section 289 | """ 290 | lines = [] 291 | if "Examples" in doc and len(doc["Examples"]) > 0: 292 | lines.append(f"{'#'*(header_level+1)} Examples \n") 293 | egs = "\n".join(doc["Examples"]) 294 | lines += mangle_examples(doc["Examples"]) 295 | return lines 296 | 297 | 298 | def returns_section(thing, doc, header_level): 299 | """ 300 | Generate markdown for Returns section. 301 | 302 | Parameters 303 | ---------- 304 | thing : function 305 | Function to produce returns for 306 | doc : dict 307 | Dict from numpydoc 308 | header_level : int 309 | Number of `#`s to use for header 310 | 311 | Returns 312 | ------- 313 | list of str 314 | Markdown for examples section 315 | """ 316 | lines = [] 317 | return_type = None 318 | # print(thing) 319 | # print(doc) 320 | try: 321 | return_type = thing.__annotations__["return"] 322 | except AttributeError: 323 | try: 324 | return_type = thing.fget.__annotations__["return"] 325 | except: 326 | pass 327 | except KeyError: 328 | pass 329 | if return_type is None: 330 | return_type = "" 331 | else: 332 | # print(f"{thing} has annotated return type {return_type}") 333 | try: 334 | return_type = ( 335 | f"{return_type.__name__}" 336 | if return_type.__module__ == "builtins" 337 | else f"{return_type.__module__}.{return_type.__name__}" 338 | ) 339 | except AttributeError: 340 | return_type = str(return_type) 341 | # print(return_type) 342 | 343 | try: 344 | if "Returns" in doc and len(doc["Returns"]) > 0 or return_type != "": 345 | # print(doc["Returns"]) 346 | lines.append(f"{'#'*(header_level+1)} Returns\n") 347 | if return_type != "" and len(doc["Returns"]) == 1: 348 | name, typ, desc = doc["Returns"][0] 349 | if typ != "" and name != "": 350 | lines.append(f"- `{name}`: ``{return_type}``") 351 | else: 352 | lines.append(f"- ``{return_type}``") 353 | lines.append("\n\n") 354 | if desc != "": 355 | lines.append(f" {' '.join(desc)}\n\n") 356 | elif return_type != "": 357 | lines.append(f"- ``{return_type}``") 358 | lines.append("\n\n") 359 | else: 360 | for name, typ, desc in doc["Returns"]: 361 | if ":" in name: 362 | name, typ = name.split(":") 363 | 364 | if typ != "": 365 | if name != "": 366 | line = f"- `{name}`: {mangle_types(typ)}" 367 | else: 368 | line = f"- {mangle_types(typ)}" 369 | else: 370 | line = f"- {mangle_types(name)}" 371 | line += "\n\n" 372 | lines.append(line) 373 | lines.append(f" {' '.join(desc)}\n\n") 374 | except Exception as e: 375 | # print(e) 376 | # print(doc) 377 | pass 378 | return lines 379 | 380 | 381 | def summary(doc): 382 | """ 383 | Generate markdown for summary section. 384 | 385 | Parameters 386 | ---------- 387 | doc : dict 388 | Output from numpydoc 389 | 390 | Returns 391 | ------- 392 | list of str 393 | Markdown strings 394 | """ 395 | lines = [] 396 | if "Summary" in doc and len(doc["Summary"]) > 0: 397 | lines.append(fix_footnotes(" ".join(doc["Summary"]))) 398 | lines.append("\n") 399 | if "Extended Summary" in doc and len(doc["Extended Summary"]) > 0: 400 | lines.append(fix_footnotes(" ".join(doc["Extended Summary"]))) 401 | lines.append("\n") 402 | return lines 403 | 404 | 405 | def params_section(thing, doc, header_level): 406 | """ 407 | Generate markdown for Parameters section. 408 | 409 | Parameters 410 | ---------- 411 | thing : function 412 | Function to produce parameters from 413 | doc : dict 414 | Dict from numpydoc 415 | header_level : int 416 | Number of `#`s to use for header 417 | 418 | Returns 419 | ------- 420 | list of str 421 | Markdown for examples section 422 | """ 423 | lines = [] 424 | 425 | class_doc = doc["Parameters"] 426 | return type_list( 427 | inspect.signature(thing), 428 | class_doc, 429 | "#" * (header_level + 1) + " Parameters\n\n", 430 | ) 431 | 432 | 433 | def escape(string): 434 | """ 435 | Escape underscores in markdown. 436 | 437 | Parameters 438 | ---------- 439 | string : str 440 | String to escape 441 | 442 | Returns 443 | ------- 444 | str 445 | The string, with `_`s escaped with backslashes 446 | """ 447 | return string.replace("_", "\\_") 448 | 449 | 450 | def get_source_link(thing, source_location): 451 | """ 452 | Get a link to the line number a module/class/function is defined at. 453 | 454 | Parameters 455 | ---------- 456 | thing : function or class 457 | Thing to get the link for 458 | source_location : str 459 | GitHub url of the source code 460 | 461 | Returns 462 | ------- 463 | str 464 | String with link to the file & line number, or empty string if it 465 | couldn't be found 466 | """ 467 | try: 468 | lineno = get_line(thing) 469 | try: 470 | owner_module = inspect.getmodule(thing) 471 | assert owner_module is not None 472 | except (TypeError, AssertionError): 473 | owner_module = inspect.getmodule(thing.fget) 474 | 475 | thing_file = "/".join(owner_module.__name__.split(".")) 476 | if owner_module.__file__.endswith("__init__.py"): 477 | thing_file += "/__init__.py" 478 | else: 479 | thing_file += ".py" 480 | return ( 481 | f"Source: [{escape(thing_file)}]({source_location}/{thing_file}#L{lineno})" 482 | + "\n\n" 483 | ) 484 | except Exception as e: 485 | # print("Failed to find source file.") 486 | # print(e) 487 | # print(lineno) 488 | # print(thing) 489 | # print(owner_module) 490 | # print(thing_file) 491 | # print(source_location) 492 | pass 493 | return "" 494 | 495 | 496 | def get_signature(name, thing): 497 | """ 498 | Get the signature for a function or class, formatted nicely if possible. 499 | 500 | Parameters 501 | ---------- 502 | name : str 503 | Name of the thing, used as the first part of the signature 504 | thing : class or function 505 | Thing to get the signature of 506 | """ 507 | if inspect.ismodule(thing): 508 | return "" 509 | if isinstance(thing, property): 510 | func_sig = name 511 | else: 512 | try: 513 | sig = inspect.signature(thing) 514 | except TypeError: 515 | sig = inspect.signature(thing.fget) 516 | except ValueError: 517 | return "" 518 | func_sig = f"{name}{sig}" 519 | try: 520 | mode = black.FileMode(line_length=80) 521 | func_sig = black.format_str(func_sig, mode=mode).strip() 522 | except (ValueError, TypeError): 523 | pass 524 | return f"```python\n{func_sig}\n```\n" 525 | 526 | 527 | def _get_names(names, types): 528 | """ 529 | Get names, bearing in mind that there might be no name, 530 | no type, and that the `:` separator might be wrongly used. 531 | """ 532 | if types == "": 533 | try: 534 | names, types = names.split(":") 535 | except: 536 | pass 537 | return names.split(","), types 538 | 539 | 540 | def string_annotation(typ, default): 541 | """ 542 | Construct a string representation of a type annotation. 543 | 544 | Parameters 545 | ---------- 546 | typ : type 547 | Type to turn into a string 548 | default : any 549 | Default value (if any) of the type 550 | 551 | Returns 552 | ------- 553 | str 554 | String version of the type annotation 555 | """ 556 | try: 557 | type_string = ( 558 | f"`{typ.__name__}`" 559 | if typ.__module__ == "builtins" 560 | else f"`{typ.__module__}.{typ.__name__}`" 561 | ) 562 | except AttributeError: 563 | type_string = f"`{str(typ)}`" 564 | if default is None: 565 | type_string = f"{type_string}, default ``None``" 566 | elif default == inspect._empty: 567 | pass 568 | else: 569 | type_string = f"{type_string}, default ``{default}``" 570 | return type_string 571 | 572 | 573 | def type_list(signature, doc, header): 574 | """ 575 | Construct a list of types, preferring type annotations to 576 | docstrings if they are available. 577 | 578 | Parameters 579 | ---------- 580 | signature : Signature 581 | Signature of thing 582 | doc : list of tuple 583 | Numpydoc's type list section 584 | 585 | Returns 586 | ------- 587 | list of str 588 | Markdown formatted type list 589 | """ 590 | 591 | lines = [] 592 | docced = set() 593 | lines.append(header) 594 | try: 595 | for names, types, description in doc: 596 | names, types = _get_names(names, types) 597 | unannotated = [] 598 | for name in names: 599 | docced.add(name) 600 | try: 601 | typ = signature.parameters[name].annotation 602 | if typ == inspect._empty: 603 | raise AttributeError 604 | default = signature.parameters[name].default 605 | type_string = string_annotation(typ, default) 606 | lines.append(f"- `{name}`: {type_string}") 607 | lines.append("\n\n") 608 | except (AttributeError, KeyError): 609 | unannotated.append(name) # No annotation 610 | 611 | if len(unannotated) > 0: 612 | lines.append("- ") 613 | lines.append(", ".join(f"`{name}`" for name in unannotated)) 614 | if types != "" and len(unannotated) > 0: 615 | lines.append(f": {mangle_types(types)}") 616 | lines.append("\n\n") 617 | lines.append(f" {' '.join(description)}\n\n") 618 | for names, types, description in doc: 619 | names, types = _get_names(names, types) 620 | for name in names: 621 | if name not in docced: 622 | try: 623 | typ = signature.parameters[name].annotation 624 | default = signature.parameters[name].default 625 | type_string = string_annotation(typ, default) 626 | lines.append(f"- `{name}`: {type_string}") 627 | lines.append("\n\n") 628 | except (AttributeError, KeyError): 629 | lines.append(f"- `{name}`") 630 | lines.append("\n\n") 631 | except Exception as e: 632 | print(f"Couldn't get type list for {signature}: {e}") 633 | return lines if len(lines) > 1 else [] 634 | 635 | 636 | def _split_props(thing, doc): 637 | """ 638 | Separate properties from other kinds of member. 639 | """ 640 | props = inspect.getmembers(thing, lambda o: isinstance(o, property)) 641 | ps = [] 642 | docs = [ 643 | (*_get_names(names, types), names, types, desc) for names, types, desc in doc 644 | ] 645 | for prop_name, prop in props: 646 | in_doc = [d for d in enumerate(docs) if prop_name in d[0]] 647 | for d in in_doc: 648 | docs.remove(d) 649 | ps.append(prop_name) 650 | if len(docs) > 0: 651 | _, _, names, types, descs = zip(*docs) 652 | return ps, zip(names, types, descs) 653 | return ps, [] 654 | 655 | 656 | def attributes_section(thing, doc, header_level): 657 | """ 658 | Generate an attributes section for classes. 659 | 660 | Prefers type annotations, if they are present. 661 | 662 | Parameters 663 | ---------- 664 | thing : class 665 | Class to document 666 | doc : dict 667 | Numpydoc output 668 | header_level : int 669 | Number of `#`s to use for header 670 | 671 | Returns 672 | ------- 673 | list of str 674 | Markdown formatted attribute list 675 | """ 676 | # Get Attributes 677 | 678 | if not inspect.isclass(thing): 679 | return [] 680 | 681 | props, class_doc = _split_props(thing, doc["Attributes"]) 682 | tl = type_list(inspect.signature(thing), class_doc, "\n## Attributes\n\n") 683 | if len(tl) == 0 and len(props) > 0: 684 | tl.append("\n## Attributes\n\n") 685 | for prop in props: 686 | tl.append(f"- [`{prop}`](#{prop})\n\n") 687 | return tl 688 | 689 | 690 | def enum_doc(name, enum, header_level, source_location): 691 | """ 692 | Generate markdown for an enum 693 | 694 | Parameters 695 | ---------- 696 | name : str 697 | Name of the thing being documented 698 | enum : EnumMeta 699 | Enum to document 700 | header_level : int 701 | Heading level 702 | source_location : str 703 | URL of repo containing source code 704 | """ 705 | 706 | lines = [f"{'#'*header_level} Enum **{name}**\n\n"] 707 | lines.append(f"```python\n{name}\n```\n") 708 | lines.append(get_source_link(enum, source_location)) 709 | try: 710 | doc = NumpyDocString(inspect.getdoc(enum))._parsed_data 711 | lines += summary(doc) 712 | except: 713 | pass 714 | lines.append(f"{'#'*(header_level + 1)} Members\n\n") 715 | lines += [f"- `{str(v).split('.').pop()}`: `{v.value}` \n\n" for v in enum] 716 | return lines 717 | 718 | 719 | def to_doc(name, thing, header_level, source_location): 720 | """ 721 | Generate markdown for a class or function 722 | 723 | Parameters 724 | ---------- 725 | name : str 726 | Name of the thing being documented 727 | thing : class or function 728 | Class or function to document 729 | header_level : int 730 | Heading level 731 | source_location : str 732 | URL of repo containing source code 733 | """ 734 | 735 | if type(thing) is enum.EnumMeta: 736 | return enum_doc(name, thing, header_level, source_location) 737 | if inspect.isclass(thing): 738 | header = f"{'#'*header_level} Class **{name}**\n\n" 739 | else: 740 | header = f"{'#'*header_level} {name}\n\n" 741 | lines = [ 742 | header, 743 | get_signature(name, thing), 744 | get_source_link(thing, source_location), 745 | ] 746 | 747 | try: 748 | # print(f"{name}: {thing}") 749 | doc = NumpyDocString(inspect.getdoc(thing))._parsed_data 750 | lines += summary(doc) 751 | # print("Got summary") 752 | lines += attributes_section(thing, doc, header_level) 753 | # print("Got attribs") 754 | try: 755 | lines += params_section(thing, doc, header_level) 756 | except: 757 | pass # No params 758 | # print("Got params") 759 | lines += returns_section(thing, doc, header_level) 760 | # print("Got returns") 761 | lines += examples_section(doc, header_level) 762 | lines += notes_section(doc) 763 | lines += warnings_section(doc) 764 | lines += refs_section(doc) 765 | except Exception as e: 766 | # print(f"No docstring for {name}, src {source_location}: {e}") 767 | pass 768 | return lines 769 | 770 | 771 | def doc_module(module_name, module, output_dir, source_location, leaf): 772 | """ 773 | Document a module 774 | 775 | Parameters 776 | ---------- 777 | module_name : str 778 | module : module 779 | output_dir : str 780 | source_location : str 781 | leaf : bool 782 | """ 783 | path = pathlib.Path(output_dir).joinpath(*module.__name__.split(".")) 784 | available_classes = get_available_classes(module) 785 | deffed_classes = get_classes(module) 786 | deffed_funcs = get_funcs(module) 787 | deffed_enums = get_enums(module) 788 | alias_funcs = available_classes - deffed_classes 789 | if leaf: 790 | doc_path = path.with_suffix(".md") 791 | else: 792 | doc_path = path / "index.md" 793 | doc_path.parent.mkdir(parents=True, exist_ok=True) 794 | module_path = "/".join(module.__name__.split(".")) 795 | doc = [f"title: {module_name.split('.')[-1]}" + "\n"] 796 | module_doc = module.__doc__ 797 | 798 | # Module overview documentation 799 | if module_doc is not None: 800 | doc += to_doc(module.__name__, module, 1, source_location) 801 | else: 802 | doc.append(f"# {module.__name__}\n\n") 803 | doc.append("\n\n") 804 | for cls_name, cls in sorted(deffed_enums) + sorted(deffed_classes): 805 | doc += to_doc(cls_name, cls, 2, source_location) 806 | 807 | class_methods = [ 808 | x 809 | for x in inspect.getmembers(cls, inspect.isfunction) 810 | if (not x[0].startswith("_")) and deffed_here(x[1], cls) 811 | ] 812 | class_methods += inspect.getmembers(cls, lambda o: isinstance(o, property)) 813 | if len(class_methods) > 0: 814 | doc.append("## Methods \n\n") 815 | for method_name, method in class_methods: 816 | # print(method_name) 817 | doc += to_doc(method_name, method, 4, source_location) 818 | for fname, func in sorted(deffed_funcs): 819 | doc += to_doc(fname, func, 2, source_location) 820 | return doc_path.absolute(), "".join(doc) 821 | 822 | 823 | @click.command() 824 | @click.argument("module_name") 825 | @click.argument("output_dir") 826 | @click.argument("source-location") 827 | def cli(module_name, output_dir, source_location): 828 | make_api_doc(module_name, output_dir, source_location) 829 | 830 | 831 | def make_api_doc(module_name, output_dir, source_location): 832 | module = importlib.import_module(module_name) 833 | output_dir = pathlib.Path(output_dir).absolute() 834 | files = [] 835 | for module_name, module, leaf, file in get_all_modules_from_files(module): 836 | # print(module_name) 837 | def do_doc(): 838 | doc_path, doc = doc_module( 839 | module_name, module, output_dir, source_location, leaf 840 | ) 841 | with open(doc_path.absolute(), "w") as doc_file: 842 | doc_file.write(doc) 843 | 844 | do_doc() 845 | files.append((file, do_doc)) 846 | print(f"Built documentation for {file.absolute()}") 847 | return files 848 | 849 | 850 | if __name__ == "__main__": 851 | cli() 852 | -------------------------------------------------------------------------------- /mktheapidocs/plugin.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import importlib 3 | import mkdocs 4 | import os 5 | import pathlib 6 | 7 | from mkdocs.utils import nest_paths 8 | 9 | from .mkapi import get_submodule_files, doc_module 10 | 11 | 12 | class PyDocFile(mkdocs.structure.files.File): 13 | def __init__(self, path, src_dir, dest_dir, use_directory_urls, parent): 14 | self.parent = parent 15 | super().__init__(path, src_dir, dest_dir, use_directory_urls) 16 | self.abs_src_path = self.parent 17 | 18 | def is_documentation_page(self): 19 | return True 20 | 21 | def _get_stem(self): 22 | """Return the name of the file without it's extension.""" 23 | filename = os.path.basename(self.src_path) 24 | stem, ext = os.path.splitext(filename) 25 | return "index" if stem in ("index", "README", "__init__") else stem 26 | 27 | 28 | class Module(mkdocs.config.config_options.OptionallyRequired): 29 | """Validate modules specified are installed.""" 30 | 31 | def run_validation(self, value): 32 | try: 33 | for module, details in value.items(): 34 | importlib.import_module(module) 35 | if "section" not in details: 36 | raise mkdocs.config.config_options.ValidationError( 37 | f"Missing section for {module}" 38 | ) 39 | if "source_repo" not in details: 40 | raise mkdocs.config.config_options.ValidationError( 41 | f"Missing source_repo for {module}" 42 | ) 43 | except ModuleNotFoundError: 44 | raise mkdocs.config.config_options.ValidationError( 45 | f"{module} not found. Have you installed it?" 46 | ) 47 | except AttributeError: 48 | raise mkdocs.config.config_options.ValidationError( 49 | f"expected , but got {type(value)}" 50 | ) 51 | return value 52 | 53 | 54 | def find_section_anchor(nav, anchor): 55 | try: 56 | in_this_level = nav.index(anchor) 57 | return in_this_level, nav 58 | except ValueError: 59 | dicts = [l for l in nav if isinstance(l, dict)] 60 | for d in dicts: 61 | d_val = list(d.items())[0][1] 62 | try: 63 | in_this_level, nav = find_section_anchor(d_val, anchor) 64 | return in_this_level, nav 65 | except ValueError: 66 | pass 67 | raise ValueError 68 | 69 | 70 | class Plugin(mkdocs.plugins.BasePlugin): 71 | config_scheme = (("modules", Module(required=True)),) 72 | 73 | def on_config(self, config): 74 | # print(config) 75 | self.files = {} 76 | self.module_files = {} 77 | for module_name, details in self.config["modules"].items(): 78 | target = details["section"] 79 | self.module_files[target] = [] 80 | source_location = details["source_repo"] 81 | source_location = os.path.expandvars(source_location) 82 | module = importlib.import_module(module_name) 83 | importlib.reload(module) 84 | src_path = pathlib.Path(module.__file__).parent.parent.absolute() 85 | target_path = pathlib.Path(config["site_dir"]) 86 | for module, file in get_submodule_files(module, details.get("hidden", [])): 87 | importlib.reload(module) 88 | do_doc = functools.partial( 89 | doc_module, 90 | module.__name__, 91 | module, 92 | "", 93 | source_location, 94 | file.stem != "__init__.py", 95 | ) 96 | f = PyDocFile( 97 | target / file, 98 | src_path, 99 | target_path, 100 | True, 101 | pathlib.Path(module.__file__).absolute(), 102 | ) 103 | # print(f.__dict__) 104 | # print() 105 | self.files[f.url] = (f, do_doc) 106 | self.module_files[target].append(f) 107 | if config["nav"]: 108 | try: 109 | ix, nav = find_section_anchor(config["nav"], f"api-docs-{target}") 110 | nav[ix] = nest_paths(f.src_path for f in self.module_files[target])[ 111 | 0 112 | ] 113 | except ValueError: 114 | pass 115 | 116 | def on_files(self, files, **kwargs): 117 | for f, func in self.files.values(): 118 | files.append(f) 119 | return files 120 | 121 | def on_nav(self, nav, **kwargs): 122 | return nav 123 | 124 | def on_page_read_source(self, page, **kwargs): 125 | try: 126 | f, sf = self.files[page.url] 127 | # print(page.__dict__) 128 | # print() 129 | return sf()[1] 130 | except KeyError: 131 | return None 132 | 133 | # def on_pre_build(self, config): 134 | # root_path = pathlib.Path(config['docs_dir']) 135 | # self.files = list(chain(*[make_api_doc(module_name, root_path / target, source_location) for module_name, target, source_location in self.config['modules']])) 136 | 137 | def on_serve(self, server, config, builder, **kwargs): 138 | # print(server.__dict__) 139 | # print(config) 140 | for file, func in self.files.values(): 141 | server.watch(str(file.abs_src_path), None) 142 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | # See the docstring in versioneer.py for instructions. Note that you must 3 | # re-run 'versioneer.py setup' after changing this section, and commit the 4 | # resulting files. 5 | 6 | [versioneer] 7 | VCS = git 8 | style = pep440-pre 9 | versionfile_source = mktheapidocs/_version.py 10 | versionfile_build = mktheapidocs/_version.py 11 | tag_prefix = 12 | parentdir_prefix = 13 | 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Setup configuration for `mktheapidocs`. 5 | 6 | """ 7 | import versioneer 8 | 9 | try: 10 | from setuptools import setup, find_packages 11 | 12 | except ImportError: 13 | from distutils.core import setup 14 | 15 | __status__ = "Development" 16 | __author__ = "Jonathan Gray" 17 | __maintainer__ = "Jonathan Gray" 18 | __email__ = "jonathan.gray@nanosheep.net" 19 | __copyright__ = "Copyright 2018, Jonathan Gray" 20 | __license__ = """ 21 | MIT License 22 | 23 | Copyright (c) 2018 Jonathan Gray 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in all 33 | copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | SOFTWARE. 42 | """ 43 | 44 | 45 | setup( 46 | name="mktheapidocs", 47 | version=versioneer.get_version(), 48 | cmdclass=versioneer.get_cmdclass(), 49 | entry_points={ 50 | "console_scripts": ["mktheapidocs = mktheapidocs.mkapi:cli"], 51 | "mkdocs.plugins": ["mktheapidocs = mktheapidocs.plugin:Plugin"], 52 | }, 53 | description="Generate markdown API documentation from Numpydoc docstrings.", 54 | author=__author__, 55 | author_email=__email__, 56 | url="https://github.com/greenape/mktheapidocs", 57 | license=__license__, 58 | keywords="mkdocs documentation markdown", 59 | packages=["mktheapidocs"], 60 | include_package_data=True, 61 | install_requires=["numpydoc", "black", "click"], 62 | extras_require={"plugin": ["mkdocs >= 1.2"]}, 63 | platforms=["MacOS X", "Linux"], 64 | classifiers=[ 65 | "Development Status :: 3 - Alpha", 66 | "Intended Audience :: Developers", 67 | "License :: OSI Approved :: MIT License", 68 | "Programming Language :: Python :: 3.6", 69 | "Natural Language :: English", 70 | "Operating System :: MacOS :: MacOS X", 71 | "Operating System :: POSIX", 72 | "Operating System :: POSIX :: Linux", 73 | ], 74 | ) 75 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | # Version: 0.18 2 | 3 | """The Versioneer - like a rocketeer, but for versions. 4 | 5 | The Versioneer 6 | ============== 7 | 8 | * like a rocketeer, but for versions! 9 | * https://github.com/warner/python-versioneer 10 | * Brian Warner 11 | * License: Public Domain 12 | * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy 13 | * [![Latest Version] 14 | (https://pypip.in/version/versioneer/badge.svg?style=flat) 15 | ](https://pypi.python.org/pypi/versioneer/) 16 | * [![Build Status] 17 | (https://travis-ci.org/warner/python-versioneer.png?branch=master) 18 | ](https://travis-ci.org/warner/python-versioneer) 19 | 20 | This is a tool for managing a recorded version number in distutils-based 21 | python projects. The goal is to remove the tedious and error-prone "update 22 | the embedded version string" step from your release process. Making a new 23 | release should be as easy as recording a new tag in your version-control 24 | system, and maybe making new tarballs. 25 | 26 | 27 | ## Quick Install 28 | 29 | * `pip install versioneer` to somewhere to your $PATH 30 | * add a `[versioneer]` section to your setup.cfg (see below) 31 | * run `versioneer install` in your source tree, commit the results 32 | 33 | ## Version Identifiers 34 | 35 | Source trees come from a variety of places: 36 | 37 | * a version-control system checkout (mostly used by developers) 38 | * a nightly tarball, produced by build automation 39 | * a snapshot tarball, produced by a web-based VCS browser, like github's 40 | "tarball from tag" feature 41 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 42 | 43 | Within each source tree, the version identifier (either a string or a number, 44 | this tool is format-agnostic) can come from a variety of places: 45 | 46 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 47 | about recent "tags" and an absolute revision-id 48 | * the name of the directory into which the tarball was unpacked 49 | * an expanded VCS keyword ($Id$, etc) 50 | * a `_version.py` created by some earlier build step 51 | 52 | For released software, the version identifier is closely related to a VCS 53 | tag. Some projects use tag names that include more than just the version 54 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 55 | needs to strip the tag prefix to extract the version identifier. For 56 | unreleased software (between tags), the version identifier should provide 57 | enough information to help developers recreate the same tree, while also 58 | giving them an idea of roughly how old the tree is (after version 1.2, before 59 | version 1.3). Many VCS systems can report a description that captures this, 60 | for example `git describe --tags --dirty --always` reports things like 61 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 62 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 63 | uncommitted changes. 64 | 65 | The version identifier is used for multiple purposes: 66 | 67 | * to allow the module to self-identify its version: `myproject.__version__` 68 | * to choose a name and prefix for a 'setup.py sdist' tarball 69 | 70 | ## Theory of Operation 71 | 72 | Versioneer works by adding a special `_version.py` file into your source 73 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 74 | dynamically ask the VCS tool for version information at import time. 75 | 76 | `_version.py` also contains `$Revision$` markers, and the installation 77 | process marks `_version.py` to have this marker rewritten with a tag name 78 | during the `git archive` command. As a result, generated tarballs will 79 | contain enough information to get the proper version. 80 | 81 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 82 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 83 | that configures it. This overrides several distutils/setuptools commands to 84 | compute the version when invoked, and changes `setup.py build` and `setup.py 85 | sdist` to replace `_version.py` with a small static file that contains just 86 | the generated version data. 87 | 88 | ## Installation 89 | 90 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 91 | 92 | ## Version-String Flavors 93 | 94 | Code which uses Versioneer can learn about its version string at runtime by 95 | importing `_version` from your main `__init__.py` file and running the 96 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 97 | import the top-level `versioneer.py` and run `get_versions()`. 98 | 99 | Both functions return a dictionary with different flavors of version 100 | information: 101 | 102 | * `['version']`: A condensed version string, rendered using the selected 103 | style. This is the most commonly used value for the project's version 104 | string. The default "pep440" style yields strings like `0.11`, 105 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 106 | below for alternative styles. 107 | 108 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 109 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 110 | 111 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 112 | commit date in ISO 8601 format. This will be None if the date is not 113 | available. 114 | 115 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 116 | this is only accurate if run in a VCS checkout, otherwise it is likely to 117 | be False or None 118 | 119 | * `['error']`: if the version string could not be computed, this will be set 120 | to a string describing the problem, otherwise it will be None. It may be 121 | useful to throw an exception in setup.py if this is set, to avoid e.g. 122 | creating tarballs with a version string of "unknown". 123 | 124 | Some variants are more useful than others. Including `full-revisionid` in a 125 | bug report should allow developers to reconstruct the exact code being tested 126 | (or indicate the presence of local changes that should be shared with the 127 | developers). `version` is suitable for display in an "about" box or a CLI 128 | `--version` output: it can be easily compared against release notes and lists 129 | of bugs fixed in various releases. 130 | 131 | The installer adds the following text to your `__init__.py` to place a basic 132 | version in `YOURPROJECT.__version__`: 133 | 134 | from ._version import get_versions 135 | __version__ = get_versions()['version'] 136 | del get_versions 137 | 138 | ## Styles 139 | 140 | The setup.cfg `style=` configuration controls how the VCS information is 141 | rendered into a version string. 142 | 143 | The default style, "pep440", produces a PEP440-compliant string, equal to the 144 | un-prefixed tag name for actual releases, and containing an additional "local 145 | version" section with more detail for in-between builds. For Git, this is 146 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 147 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 148 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 149 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 150 | software (exactly equal to a known tag), the identifier will only contain the 151 | stripped tag, e.g. "0.11". 152 | 153 | Other styles are available. See [details.md](details.md) in the Versioneer 154 | source tree for descriptions. 155 | 156 | ## Debugging 157 | 158 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 159 | to return a version of "0+unknown". To investigate the problem, run `setup.py 160 | version`, which will run the version-lookup code in a verbose mode, and will 161 | display the full contents of `get_versions()` (including the `error` string, 162 | which may help identify what went wrong). 163 | 164 | ## Known Limitations 165 | 166 | Some situations are known to cause problems for Versioneer. This details the 167 | most significant ones. More can be found on Github 168 | [issues page](https://github.com/warner/python-versioneer/issues). 169 | 170 | ### Subprojects 171 | 172 | Versioneer has limited support for source trees in which `setup.py` is not in 173 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 174 | two common reasons why `setup.py` might not be in the root: 175 | 176 | * Source trees which contain multiple subprojects, such as 177 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 178 | "master" and "slave" subprojects, each with their own `setup.py`, 179 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 180 | distributions (and upload multiple independently-installable tarballs). 181 | * Source trees whose main purpose is to contain a C library, but which also 182 | provide bindings to Python (and perhaps other langauges) in subdirectories. 183 | 184 | Versioneer will look for `.git` in parent directories, and most operations 185 | should get the right version string. However `pip` and `setuptools` have bugs 186 | and implementation details which frequently cause `pip install .` from a 187 | subproject directory to fail to find a correct version string (so it usually 188 | defaults to `0+unknown`). 189 | 190 | `pip install --editable .` should work correctly. `setup.py install` might 191 | work too. 192 | 193 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 194 | some later version. 195 | 196 | [Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking 197 | this issue. The discussion in 198 | [PR #61](https://github.com/warner/python-versioneer/pull/61) describes the 199 | issue from the Versioneer side in more detail. 200 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 201 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 202 | pip to let Versioneer work correctly. 203 | 204 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 205 | `setup.cfg`, so subprojects were completely unsupported with those releases. 206 | 207 | ### Editable installs with setuptools <= 18.5 208 | 209 | `setup.py develop` and `pip install --editable .` allow you to install a 210 | project into a virtualenv once, then continue editing the source code (and 211 | test) without re-installing after every change. 212 | 213 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 214 | convenient way to specify executable scripts that should be installed along 215 | with the python package. 216 | 217 | These both work as expected when using modern setuptools. When using 218 | setuptools-18.5 or earlier, however, certain operations will cause 219 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 220 | script, which must be resolved by re-installing the package. This happens 221 | when the install happens with one version, then the egg_info data is 222 | regenerated while a different version is checked out. Many setup.py commands 223 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 224 | a different virtualenv), so this can be surprising. 225 | 226 | [Bug #83](https://github.com/warner/python-versioneer/issues/83) describes 227 | this one, but upgrading to a newer version of setuptools should probably 228 | resolve it. 229 | 230 | ### Unicode version strings 231 | 232 | While Versioneer works (and is continually tested) with both Python 2 and 233 | Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. 234 | Newer releases probably generate unicode version strings on py2. It's not 235 | clear that this is wrong, but it may be surprising for applications when then 236 | write these strings to a network connection or include them in bytes-oriented 237 | APIs like cryptographic checksums. 238 | 239 | [Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates 240 | this question. 241 | 242 | 243 | ## Updating Versioneer 244 | 245 | To upgrade your project to a new release of Versioneer, do the following: 246 | 247 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 248 | * edit `setup.cfg`, if necessary, to include any new configuration settings 249 | indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. 250 | * re-run `versioneer install` in your source tree, to replace 251 | `SRC/_version.py` 252 | * commit any changed files 253 | 254 | ## Future Directions 255 | 256 | This tool is designed to make it easily extended to other version-control 257 | systems: all VCS-specific components are in separate directories like 258 | src/git/ . The top-level `versioneer.py` script is assembled from these 259 | components by running make-versioneer.py . In the future, make-versioneer.py 260 | will take a VCS name as an argument, and will construct a version of 261 | `versioneer.py` that is specific to the given VCS. It might also take the 262 | configuration arguments that are currently provided manually during 263 | installation by editing setup.py . Alternatively, it might go the other 264 | direction and include code from all supported VCS systems, reducing the 265 | number of intermediate scripts. 266 | 267 | 268 | ## License 269 | 270 | To make Versioneer easier to embed, all its code is dedicated to the public 271 | domain. The `_version.py` that it creates is also in the public domain. 272 | Specifically, both are released under the Creative Commons "Public Domain 273 | Dedication" license (CC0-1.0), as described in 274 | https://creativecommons.org/publicdomain/zero/1.0/ . 275 | 276 | """ 277 | 278 | from __future__ import print_function 279 | 280 | try: 281 | import configparser 282 | except ImportError: 283 | import ConfigParser as configparser 284 | import errno 285 | import json 286 | import os 287 | import re 288 | import subprocess 289 | import sys 290 | 291 | 292 | class VersioneerConfig: 293 | """Container for Versioneer configuration parameters.""" 294 | 295 | 296 | def get_root(): 297 | """Get the project root directory. 298 | 299 | We require that all commands are run from the project root, i.e. the 300 | directory that contains setup.py, setup.cfg, and versioneer.py . 301 | """ 302 | root = os.path.realpath(os.path.abspath(os.getcwd())) 303 | setup_py = os.path.join(root, "setup.py") 304 | versioneer_py = os.path.join(root, "versioneer.py") 305 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 306 | # allow 'python path/to/setup.py COMMAND' 307 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 308 | setup_py = os.path.join(root, "setup.py") 309 | versioneer_py = os.path.join(root, "versioneer.py") 310 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 311 | err = ( 312 | "Versioneer was unable to run the project root directory. " 313 | "Versioneer requires setup.py to be executed from " 314 | "its immediate directory (like 'python setup.py COMMAND'), " 315 | "or in a way that lets it use sys.argv[0] to find the root " 316 | "(like 'python path/to/setup.py COMMAND')." 317 | ) 318 | raise VersioneerBadRootError(err) 319 | try: 320 | # Certain runtime workflows (setup.py install/develop in a setuptools 321 | # tree) execute all dependencies in a single python process, so 322 | # "versioneer" may be imported multiple times, and python's shared 323 | # module-import table will cache the first one. So we can't use 324 | # os.path.dirname(__file__), as that will find whichever 325 | # versioneer.py was first imported, even in later projects. 326 | me = os.path.realpath(os.path.abspath(__file__)) 327 | me_dir = os.path.normcase(os.path.splitext(me)[0]) 328 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 329 | if me_dir != vsr_dir: 330 | print( 331 | "Warning: build in %s is using versioneer.py from %s" 332 | % (os.path.dirname(me), versioneer_py) 333 | ) 334 | except NameError: 335 | pass 336 | return root 337 | 338 | 339 | def get_config_from_root(root): 340 | """Read the project setup.cfg file to determine Versioneer config.""" 341 | # This might raise EnvironmentError (if setup.cfg is missing), or 342 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 343 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 344 | # the top of versioneer.py for instructions on writing your setup.cfg . 345 | setup_cfg = os.path.join(root, "setup.cfg") 346 | parser = configparser.SafeConfigParser() 347 | with open(setup_cfg, "r") as f: 348 | parser.readfp(f) 349 | VCS = parser.get("versioneer", "VCS") # mandatory 350 | 351 | def get(parser, name): 352 | if parser.has_option("versioneer", name): 353 | return parser.get("versioneer", name) 354 | return None 355 | 356 | cfg = VersioneerConfig() 357 | cfg.VCS = VCS 358 | cfg.style = get(parser, "style") or "" 359 | cfg.versionfile_source = get(parser, "versionfile_source") 360 | cfg.versionfile_build = get(parser, "versionfile_build") 361 | cfg.tag_prefix = get(parser, "tag_prefix") 362 | if cfg.tag_prefix in ("''", '""'): 363 | cfg.tag_prefix = "" 364 | cfg.parentdir_prefix = get(parser, "parentdir_prefix") 365 | cfg.verbose = get(parser, "verbose") 366 | return cfg 367 | 368 | 369 | class NotThisMethod(Exception): 370 | """Exception raised if a method is not valid for the current scenario.""" 371 | 372 | 373 | # these dictionaries contain VCS-specific tools 374 | LONG_VERSION_PY = {} 375 | HANDLERS = {} 376 | 377 | 378 | def register_vcs_handler(vcs, method): # decorator 379 | """Decorator to mark a method as the handler for a particular VCS.""" 380 | 381 | def decorate(f): 382 | """Store f in HANDLERS[vcs][method].""" 383 | if vcs not in HANDLERS: 384 | HANDLERS[vcs] = {} 385 | HANDLERS[vcs][method] = f 386 | return f 387 | 388 | return decorate 389 | 390 | 391 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): 392 | """Call the given command(s).""" 393 | assert isinstance(commands, list) 394 | p = None 395 | for c in commands: 396 | try: 397 | dispcmd = str([c] + args) 398 | # remember shell=False, so use git.cmd on windows, not just git 399 | p = subprocess.Popen( 400 | [c] + args, 401 | cwd=cwd, 402 | env=env, 403 | stdout=subprocess.PIPE, 404 | stderr=(subprocess.PIPE if hide_stderr else None), 405 | ) 406 | break 407 | except EnvironmentError: 408 | e = sys.exc_info()[1] 409 | if e.errno == errno.ENOENT: 410 | continue 411 | if verbose: 412 | print("unable to run %s" % dispcmd) 413 | print(e) 414 | return None, None 415 | else: 416 | if verbose: 417 | print("unable to find command, tried %s" % (commands,)) 418 | return None, None 419 | stdout = p.communicate()[0].strip() 420 | if sys.version_info[0] >= 3: 421 | stdout = stdout.decode() 422 | if p.returncode != 0: 423 | if verbose: 424 | print("unable to run %s (error)" % dispcmd) 425 | print("stdout was %s" % stdout) 426 | return None, p.returncode 427 | return stdout, p.returncode 428 | 429 | 430 | LONG_VERSION_PY[ 431 | "git" 432 | ] = ''' 433 | # This file helps to compute a version number in source trees obtained from 434 | # git-archive tarball (such as those provided by githubs download-from-tag 435 | # feature). Distribution tarballs (built by setup.py sdist) and build 436 | # directories (produced by setup.py build) will contain a much shorter file 437 | # that just contains the computed version number. 438 | 439 | # This file is released into the public domain. Generated by 440 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 441 | 442 | """Git implementation of _version.py.""" 443 | 444 | import errno 445 | import os 446 | import re 447 | import subprocess 448 | import sys 449 | 450 | 451 | def get_keywords(): 452 | """Get the keywords needed to look up the version information.""" 453 | # these strings will be replaced by git during git-archive. 454 | # setup.py/versioneer.py will grep for the variable names, so they must 455 | # each be defined on a line of their own. _version.py will just call 456 | # get_keywords(). 457 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 458 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 459 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 460 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 461 | return keywords 462 | 463 | 464 | class VersioneerConfig: 465 | """Container for Versioneer configuration parameters.""" 466 | 467 | 468 | def get_config(): 469 | """Create, populate and return the VersioneerConfig() object.""" 470 | # these strings are filled in when 'setup.py versioneer' creates 471 | # _version.py 472 | cfg = VersioneerConfig() 473 | cfg.VCS = "git" 474 | cfg.style = "%(STYLE)s" 475 | cfg.tag_prefix = "%(TAG_PREFIX)s" 476 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 477 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 478 | cfg.verbose = False 479 | return cfg 480 | 481 | 482 | class NotThisMethod(Exception): 483 | """Exception raised if a method is not valid for the current scenario.""" 484 | 485 | 486 | LONG_VERSION_PY = {} 487 | HANDLERS = {} 488 | 489 | 490 | def register_vcs_handler(vcs, method): # decorator 491 | """Decorator to mark a method as the handler for a particular VCS.""" 492 | def decorate(f): 493 | """Store f in HANDLERS[vcs][method].""" 494 | if vcs not in HANDLERS: 495 | HANDLERS[vcs] = {} 496 | HANDLERS[vcs][method] = f 497 | return f 498 | return decorate 499 | 500 | 501 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 502 | env=None): 503 | """Call the given command(s).""" 504 | assert isinstance(commands, list) 505 | p = None 506 | for c in commands: 507 | try: 508 | dispcmd = str([c] + args) 509 | # remember shell=False, so use git.cmd on windows, not just git 510 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 511 | stdout=subprocess.PIPE, 512 | stderr=(subprocess.PIPE if hide_stderr 513 | else None)) 514 | break 515 | except EnvironmentError: 516 | e = sys.exc_info()[1] 517 | if e.errno == errno.ENOENT: 518 | continue 519 | if verbose: 520 | print("unable to run %%s" %% dispcmd) 521 | print(e) 522 | return None, None 523 | else: 524 | if verbose: 525 | print("unable to find command, tried %%s" %% (commands,)) 526 | return None, None 527 | stdout = p.communicate()[0].strip() 528 | if sys.version_info[0] >= 3: 529 | stdout = stdout.decode() 530 | if p.returncode != 0: 531 | if verbose: 532 | print("unable to run %%s (error)" %% dispcmd) 533 | print("stdout was %%s" %% stdout) 534 | return None, p.returncode 535 | return stdout, p.returncode 536 | 537 | 538 | def versions_from_parentdir(parentdir_prefix, root, verbose): 539 | """Try to determine the version from the parent directory name. 540 | 541 | Source tarballs conventionally unpack into a directory that includes both 542 | the project name and a version string. We will also support searching up 543 | two directory levels for an appropriately named parent directory 544 | """ 545 | rootdirs = [] 546 | 547 | for i in range(3): 548 | dirname = os.path.basename(root) 549 | if dirname.startswith(parentdir_prefix): 550 | return {"version": dirname[len(parentdir_prefix):], 551 | "full-revisionid": None, 552 | "dirty": False, "error": None, "date": None} 553 | else: 554 | rootdirs.append(root) 555 | root = os.path.dirname(root) # up a level 556 | 557 | if verbose: 558 | print("Tried directories %%s but none started with prefix %%s" %% 559 | (str(rootdirs), parentdir_prefix)) 560 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 561 | 562 | 563 | @register_vcs_handler("git", "get_keywords") 564 | def git_get_keywords(versionfile_abs): 565 | """Extract version information from the given file.""" 566 | # the code embedded in _version.py can just fetch the value of these 567 | # keywords. When used from setup.py, we don't want to import _version.py, 568 | # so we do it with a regexp instead. This function is not used from 569 | # _version.py. 570 | keywords = {} 571 | try: 572 | f = open(versionfile_abs, "r") 573 | for line in f.readlines(): 574 | if line.strip().startswith("git_refnames ="): 575 | mo = re.search(r'=\s*"(.*)"', line) 576 | if mo: 577 | keywords["refnames"] = mo.group(1) 578 | if line.strip().startswith("git_full ="): 579 | mo = re.search(r'=\s*"(.*)"', line) 580 | if mo: 581 | keywords["full"] = mo.group(1) 582 | if line.strip().startswith("git_date ="): 583 | mo = re.search(r'=\s*"(.*)"', line) 584 | if mo: 585 | keywords["date"] = mo.group(1) 586 | f.close() 587 | except EnvironmentError: 588 | pass 589 | return keywords 590 | 591 | 592 | @register_vcs_handler("git", "keywords") 593 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 594 | """Get version information from git keywords.""" 595 | if not keywords: 596 | raise NotThisMethod("no keywords at all, weird") 597 | date = keywords.get("date") 598 | if date is not None: 599 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 600 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 601 | # -like" string, which we must then edit to make compliant), because 602 | # it's been around since git-1.5.3, and it's too difficult to 603 | # discover which version we're using, or to work around using an 604 | # older one. 605 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 606 | refnames = keywords["refnames"].strip() 607 | if refnames.startswith("$Format"): 608 | if verbose: 609 | print("keywords are unexpanded, not using") 610 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 611 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 612 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 613 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 614 | TAG = "tag: " 615 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 616 | if not tags: 617 | # Either we're using git < 1.8.3, or there really are no tags. We use 618 | # a heuristic: assume all version tags have a digit. The old git %%d 619 | # expansion behaves like git log --decorate=short and strips out the 620 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 621 | # between branches and tags. By ignoring refnames without digits, we 622 | # filter out many common branch names like "release" and 623 | # "stabilization", as well as "HEAD" and "master". 624 | tags = set([r for r in refs if re.search(r'\d', r)]) 625 | if verbose: 626 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 627 | if verbose: 628 | print("likely tags: %%s" %% ",".join(sorted(tags))) 629 | for ref in sorted(tags): 630 | # sorting will prefer e.g. "2.0" over "2.0rc1" 631 | if ref.startswith(tag_prefix): 632 | r = ref[len(tag_prefix):] 633 | if verbose: 634 | print("picking %%s" %% r) 635 | return {"version": r, 636 | "full-revisionid": keywords["full"].strip(), 637 | "dirty": False, "error": None, 638 | "date": date} 639 | # no suitable tags, so version is "0+unknown", but full hex is still there 640 | if verbose: 641 | print("no suitable tags, using unknown + full revision id") 642 | return {"version": "0+unknown", 643 | "full-revisionid": keywords["full"].strip(), 644 | "dirty": False, "error": "no suitable tags", "date": None} 645 | 646 | 647 | @register_vcs_handler("git", "pieces_from_vcs") 648 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 649 | """Get version from 'git describe' in the root of the source tree. 650 | 651 | This only gets called if the git-archive 'subst' keywords were *not* 652 | expanded, and _version.py hasn't already been rewritten with a short 653 | version string, meaning we're inside a checked out source tree. 654 | """ 655 | GITS = ["git"] 656 | if sys.platform == "win32": 657 | GITS = ["git.cmd", "git.exe"] 658 | 659 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 660 | hide_stderr=True) 661 | if rc != 0: 662 | if verbose: 663 | print("Directory %%s not under git control" %% root) 664 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 665 | 666 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 667 | # if there isn't one, this yields HEX[-dirty] (no NUM) 668 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 669 | "--always", "--long", 670 | "--match", "%%s*" %% tag_prefix], 671 | cwd=root) 672 | # --long was added in git-1.5.5 673 | if describe_out is None: 674 | raise NotThisMethod("'git describe' failed") 675 | describe_out = describe_out.strip() 676 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 677 | if full_out is None: 678 | raise NotThisMethod("'git rev-parse' failed") 679 | full_out = full_out.strip() 680 | 681 | pieces = {} 682 | pieces["long"] = full_out 683 | pieces["short"] = full_out[:7] # maybe improved later 684 | pieces["error"] = None 685 | 686 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 687 | # TAG might have hyphens. 688 | git_describe = describe_out 689 | 690 | # look for -dirty suffix 691 | dirty = git_describe.endswith("-dirty") 692 | pieces["dirty"] = dirty 693 | if dirty: 694 | git_describe = git_describe[:git_describe.rindex("-dirty")] 695 | 696 | # now we have TAG-NUM-gHEX or HEX 697 | 698 | if "-" in git_describe: 699 | # TAG-NUM-gHEX 700 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 701 | if not mo: 702 | # unparseable. Maybe git-describe is misbehaving? 703 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 704 | %% describe_out) 705 | return pieces 706 | 707 | # tag 708 | full_tag = mo.group(1) 709 | if not full_tag.startswith(tag_prefix): 710 | if verbose: 711 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 712 | print(fmt %% (full_tag, tag_prefix)) 713 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 714 | %% (full_tag, tag_prefix)) 715 | return pieces 716 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 717 | 718 | # distance: number of commits since tag 719 | pieces["distance"] = int(mo.group(2)) 720 | 721 | # commit: short hex revision ID 722 | pieces["short"] = mo.group(3) 723 | 724 | else: 725 | # HEX: no tags 726 | pieces["closest-tag"] = None 727 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 728 | cwd=root) 729 | pieces["distance"] = int(count_out) # total number of commits 730 | 731 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 732 | date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], 733 | cwd=root)[0].strip() 734 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 735 | 736 | return pieces 737 | 738 | 739 | def plus_or_dot(pieces): 740 | """Return a + if we don't already have one, else return a .""" 741 | if "+" in pieces.get("closest-tag", ""): 742 | return "." 743 | return "+" 744 | 745 | 746 | def render_pep440(pieces): 747 | """Build up version string, with post-release "local version identifier". 748 | 749 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 750 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 751 | 752 | Exceptions: 753 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 754 | """ 755 | if pieces["closest-tag"]: 756 | rendered = pieces["closest-tag"] 757 | if pieces["distance"] or pieces["dirty"]: 758 | rendered += plus_or_dot(pieces) 759 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 760 | if pieces["dirty"]: 761 | rendered += ".dirty" 762 | else: 763 | # exception #1 764 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 765 | pieces["short"]) 766 | if pieces["dirty"]: 767 | rendered += ".dirty" 768 | return rendered 769 | 770 | 771 | def render_pep440_pre(pieces): 772 | """TAG[.post.devDISTANCE] -- No -dirty. 773 | 774 | Exceptions: 775 | 1: no tags. 0.post.devDISTANCE 776 | """ 777 | if pieces["closest-tag"]: 778 | rendered = pieces["closest-tag"] 779 | if pieces["distance"]: 780 | rendered += ".post.dev%%d" %% pieces["distance"] 781 | else: 782 | # exception #1 783 | rendered = "0.post.dev%%d" %% pieces["distance"] 784 | return rendered 785 | 786 | 787 | def render_pep440_post(pieces): 788 | """TAG[.postDISTANCE[.dev0]+gHEX] . 789 | 790 | The ".dev0" means dirty. Note that .dev0 sorts backwards 791 | (a dirty tree will appear "older" than the corresponding clean one), 792 | but you shouldn't be releasing software with -dirty anyways. 793 | 794 | Exceptions: 795 | 1: no tags. 0.postDISTANCE[.dev0] 796 | """ 797 | if pieces["closest-tag"]: 798 | rendered = pieces["closest-tag"] 799 | if pieces["distance"] or pieces["dirty"]: 800 | rendered += ".post%%d" %% pieces["distance"] 801 | if pieces["dirty"]: 802 | rendered += ".dev0" 803 | rendered += plus_or_dot(pieces) 804 | rendered += "g%%s" %% pieces["short"] 805 | else: 806 | # exception #1 807 | rendered = "0.post%%d" %% pieces["distance"] 808 | if pieces["dirty"]: 809 | rendered += ".dev0" 810 | rendered += "+g%%s" %% pieces["short"] 811 | return rendered 812 | 813 | 814 | def render_pep440_old(pieces): 815 | """TAG[.postDISTANCE[.dev0]] . 816 | 817 | The ".dev0" means dirty. 818 | 819 | Eexceptions: 820 | 1: no tags. 0.postDISTANCE[.dev0] 821 | """ 822 | if pieces["closest-tag"]: 823 | rendered = pieces["closest-tag"] 824 | if pieces["distance"] or pieces["dirty"]: 825 | rendered += ".post%%d" %% pieces["distance"] 826 | if pieces["dirty"]: 827 | rendered += ".dev0" 828 | else: 829 | # exception #1 830 | rendered = "0.post%%d" %% pieces["distance"] 831 | if pieces["dirty"]: 832 | rendered += ".dev0" 833 | return rendered 834 | 835 | 836 | def render_git_describe(pieces): 837 | """TAG[-DISTANCE-gHEX][-dirty]. 838 | 839 | Like 'git describe --tags --dirty --always'. 840 | 841 | Exceptions: 842 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 843 | """ 844 | if pieces["closest-tag"]: 845 | rendered = pieces["closest-tag"] 846 | if pieces["distance"]: 847 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 848 | else: 849 | # exception #1 850 | rendered = pieces["short"] 851 | if pieces["dirty"]: 852 | rendered += "-dirty" 853 | return rendered 854 | 855 | 856 | def render_git_describe_long(pieces): 857 | """TAG-DISTANCE-gHEX[-dirty]. 858 | 859 | Like 'git describe --tags --dirty --always -long'. 860 | The distance/hash is unconditional. 861 | 862 | Exceptions: 863 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 864 | """ 865 | if pieces["closest-tag"]: 866 | rendered = pieces["closest-tag"] 867 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 868 | else: 869 | # exception #1 870 | rendered = pieces["short"] 871 | if pieces["dirty"]: 872 | rendered += "-dirty" 873 | return rendered 874 | 875 | 876 | def render(pieces, style): 877 | """Render the given version pieces into the requested style.""" 878 | if pieces["error"]: 879 | return {"version": "unknown", 880 | "full-revisionid": pieces.get("long"), 881 | "dirty": None, 882 | "error": pieces["error"], 883 | "date": None} 884 | 885 | if not style or style == "default": 886 | style = "pep440" # the default 887 | 888 | if style == "pep440": 889 | rendered = render_pep440(pieces) 890 | elif style == "pep440-pre": 891 | rendered = render_pep440_pre(pieces) 892 | elif style == "pep440-post": 893 | rendered = render_pep440_post(pieces) 894 | elif style == "pep440-old": 895 | rendered = render_pep440_old(pieces) 896 | elif style == "git-describe": 897 | rendered = render_git_describe(pieces) 898 | elif style == "git-describe-long": 899 | rendered = render_git_describe_long(pieces) 900 | else: 901 | raise ValueError("unknown style '%%s'" %% style) 902 | 903 | return {"version": rendered, "full-revisionid": pieces["long"], 904 | "dirty": pieces["dirty"], "error": None, 905 | "date": pieces.get("date")} 906 | 907 | 908 | def get_versions(): 909 | """Get version information or return default if unable to do so.""" 910 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 911 | # __file__, we can work backwards from there to the root. Some 912 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 913 | # case we can only use expanded keywords. 914 | 915 | cfg = get_config() 916 | verbose = cfg.verbose 917 | 918 | try: 919 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 920 | verbose) 921 | except NotThisMethod: 922 | pass 923 | 924 | try: 925 | root = os.path.realpath(__file__) 926 | # versionfile_source is the relative path from the top of the source 927 | # tree (where the .git directory might live) to this file. Invert 928 | # this to find the root from __file__. 929 | for i in cfg.versionfile_source.split('/'): 930 | root = os.path.dirname(root) 931 | except NameError: 932 | return {"version": "0+unknown", "full-revisionid": None, 933 | "dirty": None, 934 | "error": "unable to find root of source tree", 935 | "date": None} 936 | 937 | try: 938 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 939 | return render(pieces, cfg.style) 940 | except NotThisMethod: 941 | pass 942 | 943 | try: 944 | if cfg.parentdir_prefix: 945 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 946 | except NotThisMethod: 947 | pass 948 | 949 | return {"version": "0+unknown", "full-revisionid": None, 950 | "dirty": None, 951 | "error": "unable to compute version", "date": None} 952 | ''' 953 | 954 | 955 | @register_vcs_handler("git", "get_keywords") 956 | def git_get_keywords(versionfile_abs): 957 | """Extract version information from the given file.""" 958 | # the code embedded in _version.py can just fetch the value of these 959 | # keywords. When used from setup.py, we don't want to import _version.py, 960 | # so we do it with a regexp instead. This function is not used from 961 | # _version.py. 962 | keywords = {} 963 | try: 964 | f = open(versionfile_abs, "r") 965 | for line in f.readlines(): 966 | if line.strip().startswith("git_refnames ="): 967 | mo = re.search(r'=\s*"(.*)"', line) 968 | if mo: 969 | keywords["refnames"] = mo.group(1) 970 | if line.strip().startswith("git_full ="): 971 | mo = re.search(r'=\s*"(.*)"', line) 972 | if mo: 973 | keywords["full"] = mo.group(1) 974 | if line.strip().startswith("git_date ="): 975 | mo = re.search(r'=\s*"(.*)"', line) 976 | if mo: 977 | keywords["date"] = mo.group(1) 978 | f.close() 979 | except EnvironmentError: 980 | pass 981 | return keywords 982 | 983 | 984 | @register_vcs_handler("git", "keywords") 985 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 986 | """Get version information from git keywords.""" 987 | if not keywords: 988 | raise NotThisMethod("no keywords at all, weird") 989 | date = keywords.get("date") 990 | if date is not None: 991 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 992 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 993 | # -like" string, which we must then edit to make compliant), because 994 | # it's been around since git-1.5.3, and it's too difficult to 995 | # discover which version we're using, or to work around using an 996 | # older one. 997 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 998 | refnames = keywords["refnames"].strip() 999 | if refnames.startswith("$Format"): 1000 | if verbose: 1001 | print("keywords are unexpanded, not using") 1002 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 1003 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 1004 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 1005 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 1006 | TAG = "tag: " 1007 | tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) 1008 | if not tags: 1009 | # Either we're using git < 1.8.3, or there really are no tags. We use 1010 | # a heuristic: assume all version tags have a digit. The old git %d 1011 | # expansion behaves like git log --decorate=short and strips out the 1012 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 1013 | # between branches and tags. By ignoring refnames without digits, we 1014 | # filter out many common branch names like "release" and 1015 | # "stabilization", as well as "HEAD" and "master". 1016 | tags = set([r for r in refs if re.search(r"\d", r)]) 1017 | if verbose: 1018 | print("discarding '%s', no digits" % ",".join(refs - tags)) 1019 | if verbose: 1020 | print("likely tags: %s" % ",".join(sorted(tags))) 1021 | for ref in sorted(tags): 1022 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1023 | if ref.startswith(tag_prefix): 1024 | r = ref[len(tag_prefix) :] 1025 | if verbose: 1026 | print("picking %s" % r) 1027 | return { 1028 | "version": r, 1029 | "full-revisionid": keywords["full"].strip(), 1030 | "dirty": False, 1031 | "error": None, 1032 | "date": date, 1033 | } 1034 | # no suitable tags, so version is "0+unknown", but full hex is still there 1035 | if verbose: 1036 | print("no suitable tags, using unknown + full revision id") 1037 | return { 1038 | "version": "0+unknown", 1039 | "full-revisionid": keywords["full"].strip(), 1040 | "dirty": False, 1041 | "error": "no suitable tags", 1042 | "date": None, 1043 | } 1044 | 1045 | 1046 | @register_vcs_handler("git", "pieces_from_vcs") 1047 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1048 | """Get version from 'git describe' in the root of the source tree. 1049 | 1050 | This only gets called if the git-archive 'subst' keywords were *not* 1051 | expanded, and _version.py hasn't already been rewritten with a short 1052 | version string, meaning we're inside a checked out source tree. 1053 | """ 1054 | GITS = ["git"] 1055 | if sys.platform == "win32": 1056 | GITS = ["git.cmd", "git.exe"] 1057 | 1058 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) 1059 | if rc != 0: 1060 | if verbose: 1061 | print("Directory %s not under git control" % root) 1062 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 1063 | 1064 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1065 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1066 | describe_out, rc = run_command( 1067 | GITS, 1068 | [ 1069 | "describe", 1070 | "--tags", 1071 | "--dirty", 1072 | "--always", 1073 | "--long", 1074 | "--match", 1075 | "%s*" % tag_prefix, 1076 | ], 1077 | cwd=root, 1078 | ) 1079 | # --long was added in git-1.5.5 1080 | if describe_out is None: 1081 | raise NotThisMethod("'git describe' failed") 1082 | describe_out = describe_out.strip() 1083 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 1084 | if full_out is None: 1085 | raise NotThisMethod("'git rev-parse' failed") 1086 | full_out = full_out.strip() 1087 | 1088 | pieces = {} 1089 | pieces["long"] = full_out 1090 | pieces["short"] = full_out[:7] # maybe improved later 1091 | pieces["error"] = None 1092 | 1093 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1094 | # TAG might have hyphens. 1095 | git_describe = describe_out 1096 | 1097 | # look for -dirty suffix 1098 | dirty = git_describe.endswith("-dirty") 1099 | pieces["dirty"] = dirty 1100 | if dirty: 1101 | git_describe = git_describe[: git_describe.rindex("-dirty")] 1102 | 1103 | # now we have TAG-NUM-gHEX or HEX 1104 | 1105 | if "-" in git_describe: 1106 | # TAG-NUM-gHEX 1107 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 1108 | if not mo: 1109 | # unparseable. Maybe git-describe is misbehaving? 1110 | pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out 1111 | return pieces 1112 | 1113 | # tag 1114 | full_tag = mo.group(1) 1115 | if not full_tag.startswith(tag_prefix): 1116 | if verbose: 1117 | fmt = "tag '%s' doesn't start with prefix '%s'" 1118 | print(fmt % (full_tag, tag_prefix)) 1119 | pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( 1120 | full_tag, 1121 | tag_prefix, 1122 | ) 1123 | return pieces 1124 | pieces["closest-tag"] = full_tag[len(tag_prefix) :] 1125 | 1126 | # distance: number of commits since tag 1127 | pieces["distance"] = int(mo.group(2)) 1128 | 1129 | # commit: short hex revision ID 1130 | pieces["short"] = mo.group(3) 1131 | 1132 | else: 1133 | # HEX: no tags 1134 | pieces["closest-tag"] = None 1135 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) 1136 | pieces["distance"] = int(count_out) # total number of commits 1137 | 1138 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 1139 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ 1140 | 0 1141 | ].strip() 1142 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1143 | 1144 | return pieces 1145 | 1146 | 1147 | def do_vcs_install(manifest_in, versionfile_source, ipy): 1148 | """Git-specific installation logic for Versioneer. 1149 | 1150 | For Git, this means creating/changing .gitattributes to mark _version.py 1151 | for export-subst keyword substitution. 1152 | """ 1153 | GITS = ["git"] 1154 | if sys.platform == "win32": 1155 | GITS = ["git.cmd", "git.exe"] 1156 | files = [manifest_in, versionfile_source] 1157 | if ipy: 1158 | files.append(ipy) 1159 | try: 1160 | me = __file__ 1161 | if me.endswith(".pyc") or me.endswith(".pyo"): 1162 | me = os.path.splitext(me)[0] + ".py" 1163 | versioneer_file = os.path.relpath(me) 1164 | except NameError: 1165 | versioneer_file = "versioneer.py" 1166 | files.append(versioneer_file) 1167 | present = False 1168 | try: 1169 | f = open(".gitattributes", "r") 1170 | for line in f.readlines(): 1171 | if line.strip().startswith(versionfile_source): 1172 | if "export-subst" in line.strip().split()[1:]: 1173 | present = True 1174 | f.close() 1175 | except EnvironmentError: 1176 | pass 1177 | if not present: 1178 | f = open(".gitattributes", "a+") 1179 | f.write("%s export-subst\n" % versionfile_source) 1180 | f.close() 1181 | files.append(".gitattributes") 1182 | run_command(GITS, ["add", "--"] + files) 1183 | 1184 | 1185 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1186 | """Try to determine the version from the parent directory name. 1187 | 1188 | Source tarballs conventionally unpack into a directory that includes both 1189 | the project name and a version string. We will also support searching up 1190 | two directory levels for an appropriately named parent directory 1191 | """ 1192 | rootdirs = [] 1193 | 1194 | for i in range(3): 1195 | dirname = os.path.basename(root) 1196 | if dirname.startswith(parentdir_prefix): 1197 | return { 1198 | "version": dirname[len(parentdir_prefix) :], 1199 | "full-revisionid": None, 1200 | "dirty": False, 1201 | "error": None, 1202 | "date": None, 1203 | } 1204 | else: 1205 | rootdirs.append(root) 1206 | root = os.path.dirname(root) # up a level 1207 | 1208 | if verbose: 1209 | print( 1210 | "Tried directories %s but none started with prefix %s" 1211 | % (str(rootdirs), parentdir_prefix) 1212 | ) 1213 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1214 | 1215 | 1216 | SHORT_VERSION_PY = """ 1217 | # This file was generated by 'versioneer.py' (0.18) from 1218 | # revision-control system data, or from the parent directory name of an 1219 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1220 | # of this file. 1221 | 1222 | import json 1223 | 1224 | version_json = ''' 1225 | %s 1226 | ''' # END VERSION_JSON 1227 | 1228 | 1229 | def get_versions(): 1230 | return json.loads(version_json) 1231 | """ 1232 | 1233 | 1234 | def versions_from_file(filename): 1235 | """Try to determine the version from _version.py if present.""" 1236 | try: 1237 | with open(filename) as f: 1238 | contents = f.read() 1239 | except EnvironmentError: 1240 | raise NotThisMethod("unable to read _version.py") 1241 | mo = re.search( 1242 | r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S 1243 | ) 1244 | if not mo: 1245 | mo = re.search( 1246 | r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S 1247 | ) 1248 | if not mo: 1249 | raise NotThisMethod("no version_json in _version.py") 1250 | return json.loads(mo.group(1)) 1251 | 1252 | 1253 | def write_to_version_file(filename, versions): 1254 | """Write the given version number to the given _version.py file.""" 1255 | os.unlink(filename) 1256 | contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) 1257 | with open(filename, "w") as f: 1258 | f.write(SHORT_VERSION_PY % contents) 1259 | 1260 | print("set %s to '%s'" % (filename, versions["version"])) 1261 | 1262 | 1263 | def plus_or_dot(pieces): 1264 | """Return a + if we don't already have one, else return a .""" 1265 | if "+" in pieces.get("closest-tag", ""): 1266 | return "." 1267 | return "+" 1268 | 1269 | 1270 | def render_pep440(pieces): 1271 | """Build up version string, with post-release "local version identifier". 1272 | 1273 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1274 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1275 | 1276 | Exceptions: 1277 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1278 | """ 1279 | if pieces["closest-tag"]: 1280 | rendered = pieces["closest-tag"] 1281 | if pieces["distance"] or pieces["dirty"]: 1282 | rendered += plus_or_dot(pieces) 1283 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1284 | if pieces["dirty"]: 1285 | rendered += ".dirty" 1286 | else: 1287 | # exception #1 1288 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 1289 | if pieces["dirty"]: 1290 | rendered += ".dirty" 1291 | return rendered 1292 | 1293 | 1294 | def render_pep440_pre(pieces): 1295 | """TAG[.post.devDISTANCE] -- No -dirty. 1296 | 1297 | Exceptions: 1298 | 1: no tags. 0.post.devDISTANCE 1299 | """ 1300 | if pieces["closest-tag"]: 1301 | rendered = pieces["closest-tag"] 1302 | if pieces["distance"]: 1303 | rendered += ".post.dev%d" % pieces["distance"] 1304 | else: 1305 | # exception #1 1306 | rendered = "0.post.dev%d" % pieces["distance"] 1307 | return rendered 1308 | 1309 | 1310 | def render_pep440_post(pieces): 1311 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1312 | 1313 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1314 | (a dirty tree will appear "older" than the corresponding clean one), 1315 | but you shouldn't be releasing software with -dirty anyways. 1316 | 1317 | Exceptions: 1318 | 1: no tags. 0.postDISTANCE[.dev0] 1319 | """ 1320 | if pieces["closest-tag"]: 1321 | rendered = pieces["closest-tag"] 1322 | if pieces["distance"] or pieces["dirty"]: 1323 | rendered += ".post%d" % pieces["distance"] 1324 | if pieces["dirty"]: 1325 | rendered += ".dev0" 1326 | rendered += plus_or_dot(pieces) 1327 | rendered += "g%s" % pieces["short"] 1328 | else: 1329 | # exception #1 1330 | rendered = "0.post%d" % pieces["distance"] 1331 | if pieces["dirty"]: 1332 | rendered += ".dev0" 1333 | rendered += "+g%s" % pieces["short"] 1334 | return rendered 1335 | 1336 | 1337 | def render_pep440_old(pieces): 1338 | """TAG[.postDISTANCE[.dev0]] . 1339 | 1340 | The ".dev0" means dirty. 1341 | 1342 | Eexceptions: 1343 | 1: no tags. 0.postDISTANCE[.dev0] 1344 | """ 1345 | if pieces["closest-tag"]: 1346 | rendered = pieces["closest-tag"] 1347 | if pieces["distance"] or pieces["dirty"]: 1348 | rendered += ".post%d" % pieces["distance"] 1349 | if pieces["dirty"]: 1350 | rendered += ".dev0" 1351 | else: 1352 | # exception #1 1353 | rendered = "0.post%d" % pieces["distance"] 1354 | if pieces["dirty"]: 1355 | rendered += ".dev0" 1356 | return rendered 1357 | 1358 | 1359 | def render_git_describe(pieces): 1360 | """TAG[-DISTANCE-gHEX][-dirty]. 1361 | 1362 | Like 'git describe --tags --dirty --always'. 1363 | 1364 | Exceptions: 1365 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1366 | """ 1367 | if pieces["closest-tag"]: 1368 | rendered = pieces["closest-tag"] 1369 | if pieces["distance"]: 1370 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1371 | else: 1372 | # exception #1 1373 | rendered = pieces["short"] 1374 | if pieces["dirty"]: 1375 | rendered += "-dirty" 1376 | return rendered 1377 | 1378 | 1379 | def render_git_describe_long(pieces): 1380 | """TAG-DISTANCE-gHEX[-dirty]. 1381 | 1382 | Like 'git describe --tags --dirty --always -long'. 1383 | The distance/hash is unconditional. 1384 | 1385 | Exceptions: 1386 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1387 | """ 1388 | if pieces["closest-tag"]: 1389 | rendered = pieces["closest-tag"] 1390 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1391 | else: 1392 | # exception #1 1393 | rendered = pieces["short"] 1394 | if pieces["dirty"]: 1395 | rendered += "-dirty" 1396 | return rendered 1397 | 1398 | 1399 | def render(pieces, style): 1400 | """Render the given version pieces into the requested style.""" 1401 | if pieces["error"]: 1402 | return { 1403 | "version": "unknown", 1404 | "full-revisionid": pieces.get("long"), 1405 | "dirty": None, 1406 | "error": pieces["error"], 1407 | "date": None, 1408 | } 1409 | 1410 | if not style or style == "default": 1411 | style = "pep440" # the default 1412 | 1413 | if style == "pep440": 1414 | rendered = render_pep440(pieces) 1415 | elif style == "pep440-pre": 1416 | rendered = render_pep440_pre(pieces) 1417 | elif style == "pep440-post": 1418 | rendered = render_pep440_post(pieces) 1419 | elif style == "pep440-old": 1420 | rendered = render_pep440_old(pieces) 1421 | elif style == "git-describe": 1422 | rendered = render_git_describe(pieces) 1423 | elif style == "git-describe-long": 1424 | rendered = render_git_describe_long(pieces) 1425 | else: 1426 | raise ValueError("unknown style '%s'" % style) 1427 | 1428 | return { 1429 | "version": rendered, 1430 | "full-revisionid": pieces["long"], 1431 | "dirty": pieces["dirty"], 1432 | "error": None, 1433 | "date": pieces.get("date"), 1434 | } 1435 | 1436 | 1437 | class VersioneerBadRootError(Exception): 1438 | """The project root directory is unknown or missing key files.""" 1439 | 1440 | 1441 | def get_versions(verbose=False): 1442 | """Get the project version from whatever source is available. 1443 | 1444 | Returns dict with two keys: 'version' and 'full'. 1445 | """ 1446 | if "versioneer" in sys.modules: 1447 | # see the discussion in cmdclass.py:get_cmdclass() 1448 | del sys.modules["versioneer"] 1449 | 1450 | root = get_root() 1451 | cfg = get_config_from_root(root) 1452 | 1453 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1454 | handlers = HANDLERS.get(cfg.VCS) 1455 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1456 | verbose = verbose or cfg.verbose 1457 | assert ( 1458 | cfg.versionfile_source is not None 1459 | ), "please set versioneer.versionfile_source" 1460 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1461 | 1462 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1463 | 1464 | # extract version from first of: _version.py, VCS command (e.g. 'git 1465 | # describe'), parentdir. This is meant to work for developers using a 1466 | # source checkout, for users of a tarball created by 'setup.py sdist', 1467 | # and for users of a tarball/zipball created by 'git archive' or github's 1468 | # download-from-tag feature or the equivalent in other VCSes. 1469 | 1470 | get_keywords_f = handlers.get("get_keywords") 1471 | from_keywords_f = handlers.get("keywords") 1472 | if get_keywords_f and from_keywords_f: 1473 | try: 1474 | keywords = get_keywords_f(versionfile_abs) 1475 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1476 | if verbose: 1477 | print("got version from expanded keyword %s" % ver) 1478 | return ver 1479 | except NotThisMethod: 1480 | pass 1481 | 1482 | try: 1483 | ver = versions_from_file(versionfile_abs) 1484 | if verbose: 1485 | print("got version from file %s %s" % (versionfile_abs, ver)) 1486 | return ver 1487 | except NotThisMethod: 1488 | pass 1489 | 1490 | from_vcs_f = handlers.get("pieces_from_vcs") 1491 | if from_vcs_f: 1492 | try: 1493 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1494 | ver = render(pieces, cfg.style) 1495 | if verbose: 1496 | print("got version from VCS %s" % ver) 1497 | return ver 1498 | except NotThisMethod: 1499 | pass 1500 | 1501 | try: 1502 | if cfg.parentdir_prefix: 1503 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1504 | if verbose: 1505 | print("got version from parentdir %s" % ver) 1506 | return ver 1507 | except NotThisMethod: 1508 | pass 1509 | 1510 | if verbose: 1511 | print("unable to compute version") 1512 | 1513 | return { 1514 | "version": "0+unknown", 1515 | "full-revisionid": None, 1516 | "dirty": None, 1517 | "error": "unable to compute version", 1518 | "date": None, 1519 | } 1520 | 1521 | 1522 | def get_version(): 1523 | """Get the short version string for this project.""" 1524 | return get_versions()["version"] 1525 | 1526 | 1527 | def get_cmdclass(): 1528 | """Get the custom setuptools/distutils subclasses used by Versioneer.""" 1529 | if "versioneer" in sys.modules: 1530 | del sys.modules["versioneer"] 1531 | # this fixes the "python setup.py develop" case (also 'install' and 1532 | # 'easy_install .'), in which subdependencies of the main project are 1533 | # built (using setup.py bdist_egg) in the same python process. Assume 1534 | # a main project A and a dependency B, which use different versions 1535 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1536 | # sys.modules by the time B's setup.py is executed, causing B to run 1537 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1538 | # sandbox that restores sys.modules to it's pre-build state, so the 1539 | # parent is protected against the child's "import versioneer". By 1540 | # removing ourselves from sys.modules here, before the child build 1541 | # happens, we protect the child from the parent's versioneer too. 1542 | # Also see https://github.com/warner/python-versioneer/issues/52 1543 | 1544 | cmds = {} 1545 | 1546 | # we add "version" to both distutils and setuptools 1547 | from distutils.core import Command 1548 | 1549 | class cmd_version(Command): 1550 | description = "report generated version string" 1551 | user_options = [] 1552 | boolean_options = [] 1553 | 1554 | def initialize_options(self): 1555 | pass 1556 | 1557 | def finalize_options(self): 1558 | pass 1559 | 1560 | def run(self): 1561 | vers = get_versions(verbose=True) 1562 | print("Version: %s" % vers["version"]) 1563 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1564 | print(" dirty: %s" % vers.get("dirty")) 1565 | print(" date: %s" % vers.get("date")) 1566 | if vers["error"]: 1567 | print(" error: %s" % vers["error"]) 1568 | 1569 | cmds["version"] = cmd_version 1570 | 1571 | # we override "build_py" in both distutils and setuptools 1572 | # 1573 | # most invocation pathways end up running build_py: 1574 | # distutils/build -> build_py 1575 | # distutils/install -> distutils/build ->.. 1576 | # setuptools/bdist_wheel -> distutils/install ->.. 1577 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1578 | # setuptools/install -> bdist_egg ->.. 1579 | # setuptools/develop -> ? 1580 | # pip install: 1581 | # copies source tree to a tempdir before running egg_info/etc 1582 | # if .git isn't copied too, 'git describe' will fail 1583 | # then does setup.py bdist_wheel, or sometimes setup.py install 1584 | # setup.py egg_info -> ? 1585 | 1586 | # we override different "build_py" commands for both environments 1587 | if "setuptools" in sys.modules: 1588 | from setuptools.command.build_py import build_py as _build_py 1589 | else: 1590 | from distutils.command.build_py import build_py as _build_py 1591 | 1592 | class cmd_build_py(_build_py): 1593 | def run(self): 1594 | root = get_root() 1595 | cfg = get_config_from_root(root) 1596 | versions = get_versions() 1597 | _build_py.run(self) 1598 | # now locate _version.py in the new build/ directory and replace 1599 | # it with an updated value 1600 | if cfg.versionfile_build: 1601 | target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) 1602 | print("UPDATING %s" % target_versionfile) 1603 | write_to_version_file(target_versionfile, versions) 1604 | 1605 | cmds["build_py"] = cmd_build_py 1606 | 1607 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1608 | from cx_Freeze.dist import build_exe as _build_exe 1609 | 1610 | # nczeczulin reports that py2exe won't like the pep440-style string 1611 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1612 | # setup(console=[{ 1613 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1614 | # "product_version": versioneer.get_version(), 1615 | # ... 1616 | 1617 | class cmd_build_exe(_build_exe): 1618 | def run(self): 1619 | root = get_root() 1620 | cfg = get_config_from_root(root) 1621 | versions = get_versions() 1622 | target_versionfile = cfg.versionfile_source 1623 | print("UPDATING %s" % target_versionfile) 1624 | write_to_version_file(target_versionfile, versions) 1625 | 1626 | _build_exe.run(self) 1627 | os.unlink(target_versionfile) 1628 | with open(cfg.versionfile_source, "w") as f: 1629 | LONG = LONG_VERSION_PY[cfg.VCS] 1630 | f.write( 1631 | LONG 1632 | % { 1633 | "DOLLAR": "$", 1634 | "STYLE": cfg.style, 1635 | "TAG_PREFIX": cfg.tag_prefix, 1636 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1637 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1638 | } 1639 | ) 1640 | 1641 | cmds["build_exe"] = cmd_build_exe 1642 | del cmds["build_py"] 1643 | 1644 | if "py2exe" in sys.modules: # py2exe enabled? 1645 | try: 1646 | from py2exe.distutils_buildexe import py2exe as _py2exe # py3 1647 | except ImportError: 1648 | from py2exe.build_exe import py2exe as _py2exe # py2 1649 | 1650 | class cmd_py2exe(_py2exe): 1651 | def run(self): 1652 | root = get_root() 1653 | cfg = get_config_from_root(root) 1654 | versions = get_versions() 1655 | target_versionfile = cfg.versionfile_source 1656 | print("UPDATING %s" % target_versionfile) 1657 | write_to_version_file(target_versionfile, versions) 1658 | 1659 | _py2exe.run(self) 1660 | os.unlink(target_versionfile) 1661 | with open(cfg.versionfile_source, "w") as f: 1662 | LONG = LONG_VERSION_PY[cfg.VCS] 1663 | f.write( 1664 | LONG 1665 | % { 1666 | "DOLLAR": "$", 1667 | "STYLE": cfg.style, 1668 | "TAG_PREFIX": cfg.tag_prefix, 1669 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1670 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1671 | } 1672 | ) 1673 | 1674 | cmds["py2exe"] = cmd_py2exe 1675 | 1676 | # we override different "sdist" commands for both environments 1677 | if "setuptools" in sys.modules: 1678 | from setuptools.command.sdist import sdist as _sdist 1679 | else: 1680 | from distutils.command.sdist import sdist as _sdist 1681 | 1682 | class cmd_sdist(_sdist): 1683 | def run(self): 1684 | versions = get_versions() 1685 | self._versioneer_generated_versions = versions 1686 | # unless we update this, the command will keep using the old 1687 | # version 1688 | self.distribution.metadata.version = versions["version"] 1689 | return _sdist.run(self) 1690 | 1691 | def make_release_tree(self, base_dir, files): 1692 | root = get_root() 1693 | cfg = get_config_from_root(root) 1694 | _sdist.make_release_tree(self, base_dir, files) 1695 | # now locate _version.py in the new base_dir directory 1696 | # (remembering that it may be a hardlink) and replace it with an 1697 | # updated value 1698 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 1699 | print("UPDATING %s" % target_versionfile) 1700 | write_to_version_file( 1701 | target_versionfile, self._versioneer_generated_versions 1702 | ) 1703 | 1704 | cmds["sdist"] = cmd_sdist 1705 | 1706 | return cmds 1707 | 1708 | 1709 | CONFIG_ERROR = """ 1710 | setup.cfg is missing the necessary Versioneer configuration. You need 1711 | a section like: 1712 | 1713 | [versioneer] 1714 | VCS = git 1715 | style = pep440 1716 | versionfile_source = src/myproject/_version.py 1717 | versionfile_build = myproject/_version.py 1718 | tag_prefix = 1719 | parentdir_prefix = myproject- 1720 | 1721 | You will also need to edit your setup.py to use the results: 1722 | 1723 | import versioneer 1724 | setup(version=versioneer.get_version(), 1725 | cmdclass=versioneer.get_cmdclass(), ...) 1726 | 1727 | Please read the docstring in ./versioneer.py for configuration instructions, 1728 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 1729 | """ 1730 | 1731 | SAMPLE_CONFIG = """ 1732 | # See the docstring in versioneer.py for instructions. Note that you must 1733 | # re-run 'versioneer.py setup' after changing this section, and commit the 1734 | # resulting files. 1735 | 1736 | [versioneer] 1737 | #VCS = git 1738 | #style = pep440 1739 | #versionfile_source = 1740 | #versionfile_build = 1741 | #tag_prefix = 1742 | #parentdir_prefix = 1743 | 1744 | """ 1745 | 1746 | INIT_PY_SNIPPET = """ 1747 | from ._version import get_versions 1748 | __version__ = get_versions()['version'] 1749 | del get_versions 1750 | """ 1751 | 1752 | 1753 | def do_setup(): 1754 | """Main VCS-independent setup function for installing Versioneer.""" 1755 | root = get_root() 1756 | try: 1757 | cfg = get_config_from_root(root) 1758 | except ( 1759 | EnvironmentError, 1760 | configparser.NoSectionError, 1761 | configparser.NoOptionError, 1762 | ) as e: 1763 | if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 1764 | print("Adding sample versioneer config to setup.cfg", file=sys.stderr) 1765 | with open(os.path.join(root, "setup.cfg"), "a") as f: 1766 | f.write(SAMPLE_CONFIG) 1767 | print(CONFIG_ERROR, file=sys.stderr) 1768 | return 1 1769 | 1770 | print(" creating %s" % cfg.versionfile_source) 1771 | with open(cfg.versionfile_source, "w") as f: 1772 | LONG = LONG_VERSION_PY[cfg.VCS] 1773 | f.write( 1774 | LONG 1775 | % { 1776 | "DOLLAR": "$", 1777 | "STYLE": cfg.style, 1778 | "TAG_PREFIX": cfg.tag_prefix, 1779 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1780 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1781 | } 1782 | ) 1783 | 1784 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") 1785 | if os.path.exists(ipy): 1786 | try: 1787 | with open(ipy, "r") as f: 1788 | old = f.read() 1789 | except EnvironmentError: 1790 | old = "" 1791 | if INIT_PY_SNIPPET not in old: 1792 | print(" appending to %s" % ipy) 1793 | with open(ipy, "a") as f: 1794 | f.write(INIT_PY_SNIPPET) 1795 | else: 1796 | print(" %s unmodified" % ipy) 1797 | else: 1798 | print(" %s doesn't exist, ok" % ipy) 1799 | ipy = None 1800 | 1801 | # Make sure both the top-level "versioneer.py" and versionfile_source 1802 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 1803 | # they'll be copied into source distributions. Pip won't be able to 1804 | # install the package without this. 1805 | manifest_in = os.path.join(root, "MANIFEST.in") 1806 | simple_includes = set() 1807 | try: 1808 | with open(manifest_in, "r") as f: 1809 | for line in f: 1810 | if line.startswith("include "): 1811 | for include in line.split()[1:]: 1812 | simple_includes.add(include) 1813 | except EnvironmentError: 1814 | pass 1815 | # That doesn't cover everything MANIFEST.in can do 1816 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 1817 | # it might give some false negatives. Appending redundant 'include' 1818 | # lines is safe, though. 1819 | if "versioneer.py" not in simple_includes: 1820 | print(" appending 'versioneer.py' to MANIFEST.in") 1821 | with open(manifest_in, "a") as f: 1822 | f.write("include versioneer.py\n") 1823 | else: 1824 | print(" 'versioneer.py' already in MANIFEST.in") 1825 | if cfg.versionfile_source not in simple_includes: 1826 | print( 1827 | " appending versionfile_source ('%s') to MANIFEST.in" 1828 | % cfg.versionfile_source 1829 | ) 1830 | with open(manifest_in, "a") as f: 1831 | f.write("include %s\n" % cfg.versionfile_source) 1832 | else: 1833 | print(" versionfile_source already in MANIFEST.in") 1834 | 1835 | # Make VCS-specific changes. For git, this means creating/changing 1836 | # .gitattributes to mark _version.py for export-subst keyword 1837 | # substitution. 1838 | do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 1839 | return 0 1840 | 1841 | 1842 | def scan_setup_py(): 1843 | """Validate the contents of setup.py against Versioneer's expectations.""" 1844 | found = set() 1845 | setters = False 1846 | errors = 0 1847 | with open("setup.py", "r") as f: 1848 | for line in f.readlines(): 1849 | if "import versioneer" in line: 1850 | found.add("import") 1851 | if "versioneer.get_cmdclass()" in line: 1852 | found.add("cmdclass") 1853 | if "versioneer.get_version()" in line: 1854 | found.add("get_version") 1855 | if "versioneer.VCS" in line: 1856 | setters = True 1857 | if "versioneer.versionfile_source" in line: 1858 | setters = True 1859 | if len(found) != 3: 1860 | print("") 1861 | print("Your setup.py appears to be missing some important items") 1862 | print("(but I might be wrong). Please make sure it has something") 1863 | print("roughly like the following:") 1864 | print("") 1865 | print(" import versioneer") 1866 | print(" setup( version=versioneer.get_version(),") 1867 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 1868 | print("") 1869 | errors += 1 1870 | if setters: 1871 | print("You should remove lines like 'versioneer.VCS = ' and") 1872 | print("'versioneer.versionfile_source = ' . This configuration") 1873 | print("now lives in setup.cfg, and should be removed from setup.py") 1874 | print("") 1875 | errors += 1 1876 | return errors 1877 | 1878 | 1879 | if __name__ == "__main__": 1880 | cmd = sys.argv[1] 1881 | if cmd == "setup": 1882 | errors = do_setup() 1883 | errors += scan_setup_py() 1884 | if errors: 1885 | sys.exit(1) 1886 | --------------------------------------------------------------------------------