├── .flake8 ├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── single_source ├── __init__.py ├── errors.py ├── py.typed └── version.py └── tests ├── __init__.py ├── conftest.py ├── data ├── pyproject.toml └── setup.py ├── test_regex.py └── test_version.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = 4 | .mypy_cache, 5 | .hg, 6 | .git, 7 | .eggs, 8 | venv, 9 | .tox, 10 | _build, 11 | buck-out, 12 | build, 13 | dist 14 | 15 | ignore = W503, E203 16 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | env: 13 | POETRY_VIRTUALENVS_CREATE: false 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 23 | 24 | steps: 25 | - uses: actions/checkout@v4.1.6 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v5.1.0 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install poetry 33 | poetry install -v 34 | - name: Lint with flake8 35 | run: | 36 | # stop the build if there are Python syntax errors or undefined names 37 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 38 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 39 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 40 | - name: Formatting 41 | run: | 42 | black . --check 43 | isort . --check 44 | - name: Test with pytest 45 | run: | 46 | pytest 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm 2 | .idea 3 | 4 | # vscode 5 | .vscode/ 6 | 7 | # MacOS 8 | .DS_Store 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-docstring-first 8 | - id: check-toml 9 | - id: check-added-large-files 10 | - repo: https://github.com/myint/autoflake 11 | rev: v1.4 12 | hooks: 13 | - id: autoflake 14 | args: [ '--in-place', '--remove-all-unused-imports', '--remove-unused-variable' ] 15 | - repo: https://github.com/asottile/seed-isort-config 16 | rev: v2.2.0 17 | hooks: 18 | - id: seed-isort-config 19 | - repo: https://github.com/timothycrosley/isort 20 | rev: master 21 | hooks: 22 | - id: isort 23 | additional_dependencies: [pyproject] 24 | exclude: README.md 25 | - repo: https://github.com/psf/black 26 | rev: 20.8b1 27 | hooks: 28 | - id: black 29 | - repo: https://gitlab.com/pycqa/flake8 30 | rev: 3.8.3 31 | hooks: 32 | - id: flake8 33 | additional_dependencies: [flake8-bugbear] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniil Shadrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Single-source: There is only one truth 2 | > `single-source` helps to reduce the entropy in your Python project by keeping 3 | > single source of truth. 4 | 5 | The targets of this library are modern Python projects which want to have 6 | one source of truth for version, name and etc. 7 | 8 | At the moment, the library provides the single point for a package version. 9 | 10 | It supports Python 3.8+. 11 | 12 | ## Quick start 13 | 14 | ```python 15 | # root_package/__init__.py 16 | from pathlib import Path 17 | from single_source import get_version 18 | 19 | __version__ = get_version(__name__, Path(__file__).parent.parent) 20 | ``` 21 | 22 | ## Root of the problem 23 | 24 | You use modern `pyproject.toml` and want to keep the version of your package 25 | here: 26 | ```toml 27 | # pyproject.toml 28 | [tool.poetry] 29 | name = "modern-project" 30 | version = "0.1.0" 31 | ``` 32 | 33 | Let's imagine the version of your package is required in some place of the code. 34 | 35 | Since you need the version in your Python code, you may want to duplicate the version by putting it as a string variable to some python file: 36 | ```python 37 | # modern_project/__init__.py 38 | __version__ = "0.1.0" 39 | 40 | # modern_project/version.py 41 | version = "0.1.0" 42 | ``` 43 | 44 | Then you realize you don't want to have the version in a python file and in pyproject.toml at the same time. It's harder to keep them consistent and easier to forget to bump both versions before release. 45 | 46 | Also, you don't want to build the wheel by creating some script for auto incrementing the version in both places (and use it in your CI flow, for example). Instead you want use `poetry version` commands. 47 | 48 | ## Installation 49 | You can install `single-source` via [pip](https://pip.pypa.io/en/stable/) 50 | ```bash 51 | pip3 install single-source 52 | ``` 53 | 54 | or via [poetry](https://python-poetry.org/docs/#installation) 55 | ```bash 56 | poetry add single-source 57 | ``` 58 | 59 | The library also available as 60 | [a conda package](https://docs.conda.io/projects/conda/en/latest/) in 61 | [conda-forge](https://anaconda.org/conda-forge/repo) channel 62 | ```bash 63 | conda install single-source --channel conda-forge 64 | ``` 65 | 66 | ## Advanced usage 67 | ### Changing default value 68 | If it's not possible to get the version from package metadata or 69 | there is no pyproject.toml `get_version` returns `""` - empty string by default. 70 | You can change this value by providing a value as a `default_return` keyword argument. 71 | 72 | ```python 73 | from pathlib import Path 74 | from single_source import get_version 75 | 76 | path_to_pyproject_dir = Path(__file__).parent.parent 77 | __version__ = get_version(__name__, path_to_pyproject_dir, default_return=None) 78 | ``` 79 | 80 | ### Raising an exception 81 | You may want to raise an exception in case the version of the package 82 | has not been found. 83 | ```python 84 | from pathlib import Path 85 | from single_source import get_version, VersionNotFoundError 86 | 87 | path_to_pyproject_dir = Path(__file__).parent.parent 88 | try: 89 | __version__ = get_version(__name__, path_to_pyproject_dir, fail=True) 90 | except VersionNotFoundError: 91 | pass 92 | ``` 93 | 94 | 95 | ### Not only pyproject.toml 96 | You can use `single-source` even if you still store the version of your library 97 | in `setup.py` or in any other `utf-8` encoded text file. 98 | 99 | >First, try without custom `regex`, probably it can parse the version 100 | 101 | If the default internal `regex` does not find the version in your file, 102 | the only thing you need to provide is a custom `regex` to `get_version`: 103 | ```python 104 | from single_source import get_version 105 | 106 | custom_regex = r"\s*version\s*=\s*[\"']\s*([-.\w]{3,})\s*[\"']\s*" 107 | 108 | path_to_file = "~/my-project/some_file_with_version.txt" 109 | __version__ = get_version(__name__, path_to_file, version_regex=custom_regex) 110 | ``` 111 | Version must be in the first group `()` in the custom regex. 112 | 113 | ## Contributing 114 | Pull requests are welcome. For major changes, please open an issue first to 115 | discuss what you would like to change. 116 | 117 | Please make sure to update tests as appropriate. 118 | 119 | ## License 120 | [MIT](https://choosealicense.com/licenses/mit/) 121 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.8 3 | 4 | [mypy-tests.*] 5 | ignore_errors = True 6 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "attrs" 5 | version = "23.2.0" 6 | description = "Classes Without Boilerplate" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, 11 | {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, 12 | ] 13 | 14 | [package.extras] 15 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 16 | dev = ["attrs[tests]", "pre-commit"] 17 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 18 | tests = ["attrs[tests-no-zope]", "zope-interface"] 19 | tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] 20 | tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 21 | 22 | [[package]] 23 | name = "black" 24 | version = "24.4.2" 25 | description = "The uncompromising code formatter." 26 | optional = false 27 | python-versions = ">=3.8" 28 | files = [ 29 | {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, 30 | {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, 31 | {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, 32 | {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, 33 | {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, 34 | {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, 35 | {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, 36 | {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, 37 | {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, 38 | {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, 39 | {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, 40 | {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, 41 | {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, 42 | {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, 43 | {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, 44 | {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, 45 | {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, 46 | {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, 47 | {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, 48 | {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, 49 | {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, 50 | {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, 51 | ] 52 | 53 | [package.dependencies] 54 | click = ">=8.0.0" 55 | mypy-extensions = ">=0.4.3" 56 | packaging = ">=22.0" 57 | pathspec = ">=0.9.0" 58 | platformdirs = ">=2" 59 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 60 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 61 | 62 | [package.extras] 63 | colorama = ["colorama (>=0.4.3)"] 64 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 65 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 66 | uvloop = ["uvloop (>=0.15.2)"] 67 | 68 | [[package]] 69 | name = "cfgv" 70 | version = "3.4.0" 71 | description = "Validate configuration and produce human readable error messages." 72 | optional = false 73 | python-versions = ">=3.8" 74 | files = [ 75 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 76 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 77 | ] 78 | 79 | [[package]] 80 | name = "click" 81 | version = "8.1.7" 82 | description = "Composable command line interface toolkit" 83 | optional = false 84 | python-versions = ">=3.7" 85 | files = [ 86 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 87 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 88 | ] 89 | 90 | [package.dependencies] 91 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 92 | 93 | [[package]] 94 | name = "colorama" 95 | version = "0.4.6" 96 | description = "Cross-platform colored terminal text." 97 | optional = false 98 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 99 | files = [ 100 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 101 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 102 | ] 103 | 104 | [[package]] 105 | name = "distlib" 106 | version = "0.3.8" 107 | description = "Distribution utilities" 108 | optional = false 109 | python-versions = "*" 110 | files = [ 111 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 112 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 113 | ] 114 | 115 | [[package]] 116 | name = "exceptiongroup" 117 | version = "1.2.1" 118 | description = "Backport of PEP 654 (exception groups)" 119 | optional = false 120 | python-versions = ">=3.7" 121 | files = [ 122 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 123 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 124 | ] 125 | 126 | [package.extras] 127 | test = ["pytest (>=6)"] 128 | 129 | [[package]] 130 | name = "filelock" 131 | version = "3.14.0" 132 | description = "A platform independent file lock." 133 | optional = false 134 | python-versions = ">=3.8" 135 | files = [ 136 | {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, 137 | {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, 138 | ] 139 | 140 | [package.extras] 141 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 142 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 143 | typing = ["typing-extensions (>=4.8)"] 144 | 145 | [[package]] 146 | name = "flake8" 147 | version = "5.0.4" 148 | description = "the modular source code checker: pep8 pyflakes and co" 149 | optional = false 150 | python-versions = ">=3.6.1" 151 | files = [ 152 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 153 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 154 | ] 155 | 156 | [package.dependencies] 157 | mccabe = ">=0.7.0,<0.8.0" 158 | pycodestyle = ">=2.9.0,<2.10.0" 159 | pyflakes = ">=2.5.0,<2.6.0" 160 | 161 | [[package]] 162 | name = "flake8-bugbear" 163 | version = "23.3.12" 164 | description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." 165 | optional = false 166 | python-versions = ">=3.7" 167 | files = [ 168 | {file = "flake8-bugbear-23.3.12.tar.gz", hash = "sha256:e3e7f74c8a49ad3794a7183353026dabd68c74030d5f46571f84c1fb0eb79363"}, 169 | {file = "flake8_bugbear-23.3.12-py3-none-any.whl", hash = "sha256:beb5c7efcd7ccc2039ef66a77bb8db925e7be3531ff1cb4d0b7030d0e2113d72"}, 170 | ] 171 | 172 | [package.dependencies] 173 | attrs = ">=19.2.0" 174 | flake8 = ">=3.0.0" 175 | 176 | [package.extras] 177 | dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] 178 | 179 | [[package]] 180 | name = "identify" 181 | version = "2.5.36" 182 | description = "File identification library for Python" 183 | optional = false 184 | python-versions = ">=3.8" 185 | files = [ 186 | {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, 187 | {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, 188 | ] 189 | 190 | [package.extras] 191 | license = ["ukkonen"] 192 | 193 | [[package]] 194 | name = "iniconfig" 195 | version = "2.0.0" 196 | description = "brain-dead simple config-ini parsing" 197 | optional = false 198 | python-versions = ">=3.7" 199 | files = [ 200 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 201 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 202 | ] 203 | 204 | [[package]] 205 | name = "isort" 206 | version = "5.13.2" 207 | description = "A Python utility / library to sort Python imports." 208 | optional = false 209 | python-versions = ">=3.8.0" 210 | files = [ 211 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 212 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 213 | ] 214 | 215 | [package.extras] 216 | colors = ["colorama (>=0.4.6)"] 217 | 218 | [[package]] 219 | name = "mccabe" 220 | version = "0.7.0" 221 | description = "McCabe checker, plugin for flake8" 222 | optional = false 223 | python-versions = ">=3.6" 224 | files = [ 225 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 226 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 227 | ] 228 | 229 | [[package]] 230 | name = "mypy" 231 | version = "1.10.0" 232 | description = "Optional static typing for Python" 233 | optional = false 234 | python-versions = ">=3.8" 235 | files = [ 236 | {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, 237 | {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, 238 | {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, 239 | {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, 240 | {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, 241 | {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, 242 | {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, 243 | {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, 244 | {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, 245 | {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, 246 | {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, 247 | {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, 248 | {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, 249 | {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, 250 | {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, 251 | {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, 252 | {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, 253 | {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, 254 | {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, 255 | {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, 256 | {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, 257 | {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, 258 | {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, 259 | {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, 260 | {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, 261 | {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, 262 | {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, 263 | ] 264 | 265 | [package.dependencies] 266 | mypy-extensions = ">=1.0.0" 267 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 268 | typing-extensions = ">=4.1.0" 269 | 270 | [package.extras] 271 | dmypy = ["psutil (>=4.0)"] 272 | install-types = ["pip"] 273 | mypyc = ["setuptools (>=50)"] 274 | reports = ["lxml"] 275 | 276 | [[package]] 277 | name = "mypy-extensions" 278 | version = "1.0.0" 279 | description = "Type system extensions for programs checked with the mypy type checker." 280 | optional = false 281 | python-versions = ">=3.5" 282 | files = [ 283 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 284 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 285 | ] 286 | 287 | [[package]] 288 | name = "nodeenv" 289 | version = "1.9.1" 290 | description = "Node.js virtual environment builder" 291 | optional = false 292 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 293 | files = [ 294 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 295 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 296 | ] 297 | 298 | [[package]] 299 | name = "packaging" 300 | version = "24.1" 301 | description = "Core utilities for Python packages" 302 | optional = false 303 | python-versions = ">=3.8" 304 | files = [ 305 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 306 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 307 | ] 308 | 309 | [[package]] 310 | name = "pathspec" 311 | version = "0.12.1" 312 | description = "Utility library for gitignore style pattern matching of file paths." 313 | optional = false 314 | python-versions = ">=3.8" 315 | files = [ 316 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 317 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 318 | ] 319 | 320 | [[package]] 321 | name = "platformdirs" 322 | version = "4.2.2" 323 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 324 | optional = false 325 | python-versions = ">=3.8" 326 | files = [ 327 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 328 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 329 | ] 330 | 331 | [package.extras] 332 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 333 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 334 | type = ["mypy (>=1.8)"] 335 | 336 | [[package]] 337 | name = "pluggy" 338 | version = "1.5.0" 339 | description = "plugin and hook calling mechanisms for python" 340 | optional = false 341 | python-versions = ">=3.8" 342 | files = [ 343 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 344 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 345 | ] 346 | 347 | [package.extras] 348 | dev = ["pre-commit", "tox"] 349 | testing = ["pytest", "pytest-benchmark"] 350 | 351 | [[package]] 352 | name = "pre-commit" 353 | version = "3.5.0" 354 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 355 | optional = false 356 | python-versions = ">=3.8" 357 | files = [ 358 | {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, 359 | {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, 360 | ] 361 | 362 | [package.dependencies] 363 | cfgv = ">=2.0.0" 364 | identify = ">=1.0.0" 365 | nodeenv = ">=0.11.1" 366 | pyyaml = ">=5.1" 367 | virtualenv = ">=20.10.0" 368 | 369 | [[package]] 370 | name = "pycodestyle" 371 | version = "2.9.1" 372 | description = "Python style guide checker" 373 | optional = false 374 | python-versions = ">=3.6" 375 | files = [ 376 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 377 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 378 | ] 379 | 380 | [[package]] 381 | name = "pyflakes" 382 | version = "2.5.0" 383 | description = "passive checker of Python programs" 384 | optional = false 385 | python-versions = ">=3.6" 386 | files = [ 387 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 388 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 389 | ] 390 | 391 | [[package]] 392 | name = "pytest" 393 | version = "8.2.2" 394 | description = "pytest: simple powerful testing with Python" 395 | optional = false 396 | python-versions = ">=3.8" 397 | files = [ 398 | {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, 399 | {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, 400 | ] 401 | 402 | [package.dependencies] 403 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 404 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 405 | iniconfig = "*" 406 | packaging = "*" 407 | pluggy = ">=1.5,<2.0" 408 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 409 | 410 | [package.extras] 411 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 412 | 413 | [[package]] 414 | name = "pytest-mock" 415 | version = "3.14.0" 416 | description = "Thin-wrapper around the mock package for easier use with pytest" 417 | optional = false 418 | python-versions = ">=3.8" 419 | files = [ 420 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 421 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 422 | ] 423 | 424 | [package.dependencies] 425 | pytest = ">=6.2.5" 426 | 427 | [package.extras] 428 | dev = ["pre-commit", "pytest-asyncio", "tox"] 429 | 430 | [[package]] 431 | name = "pyyaml" 432 | version = "6.0.1" 433 | description = "YAML parser and emitter for Python" 434 | optional = false 435 | python-versions = ">=3.6" 436 | files = [ 437 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 438 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 439 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 440 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 441 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 442 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 443 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 444 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 445 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 446 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 447 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 448 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 449 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 450 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 451 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 452 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 453 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 454 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 455 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 456 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 457 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 458 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 459 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 460 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 461 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 462 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 463 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 464 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 465 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 466 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 467 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 468 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 469 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 470 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 471 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 472 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 473 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 474 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 475 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 476 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 477 | ] 478 | 479 | [[package]] 480 | name = "toml" 481 | version = "0.10.2" 482 | description = "Python Library for Tom's Obvious, Minimal Language" 483 | optional = false 484 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 485 | files = [ 486 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 487 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 488 | ] 489 | 490 | [[package]] 491 | name = "tomli" 492 | version = "2.0.1" 493 | description = "A lil' TOML parser" 494 | optional = false 495 | python-versions = ">=3.7" 496 | files = [ 497 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 498 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 499 | ] 500 | 501 | [[package]] 502 | name = "typing-extensions" 503 | version = "4.12.2" 504 | description = "Backported and Experimental Type Hints for Python 3.8+" 505 | optional = false 506 | python-versions = ">=3.8" 507 | files = [ 508 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 509 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 510 | ] 511 | 512 | [[package]] 513 | name = "virtualenv" 514 | version = "20.26.2" 515 | description = "Virtual Python Environment builder" 516 | optional = false 517 | python-versions = ">=3.7" 518 | files = [ 519 | {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, 520 | {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, 521 | ] 522 | 523 | [package.dependencies] 524 | distlib = ">=0.3.7,<1" 525 | filelock = ">=3.12.2,<4" 526 | platformdirs = ">=3.9.1,<5" 527 | 528 | [package.extras] 529 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 530 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 531 | 532 | [metadata] 533 | lock-version = "2.0" 534 | python-versions = "^3.8" 535 | content-hash = "1c972a49ee31b6facef06a1c9bc35d9ffee0c7f87b7dd4f3087e548567afb860" 536 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "single-source" 3 | version = "0.4.0" 4 | description = "Access to the project version in Python code for PEP 621-style projects" 5 | authors = ["Daniil Shadrin "] 6 | maintainers = ["Daniil Shadrin "] 7 | readme = "README.md" 8 | license = "MIT" 9 | homepage = "https://github.com/rabbit72/single-source" 10 | repository = "https://github.com/rabbit72/single-source.git" 11 | 12 | keywords = ["pyproject", "version", "__version__", "poetry", "single source"] 13 | classifiers = [ 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | ] 21 | packages = [{include = "single_source"}] 22 | 23 | [tool.poetry.urls] 24 | "Bug Tracker" = "https://github.com/rabbit72/single-source/issues" 25 | 26 | [tool.poetry.dev-dependencies] 27 | toml = "^0.10.2" 28 | pytest = "*" 29 | pytest-mock = "*" 30 | black = "^24.4.2" 31 | isort = {version = "*", extras = ["pyproject"]} 32 | flake8 = "*" 33 | flake8-bugbear = "*" 34 | pre-commit = "*" 35 | mypy = "^1.10.0" 36 | 37 | 38 | [tool.poetry.dependencies] 39 | python = "^3.8" 40 | 41 | 42 | [tool.black] 43 | line-length = 88 44 | target-version = ['py38'] 45 | 46 | # backward compatibility with black 47 | [tool.isort] 48 | multi_line_output = 3 49 | include_trailing_comma = true 50 | force_grid_wrap = 0 51 | use_parentheses = true 52 | line_length = 88 53 | known_third_party = ["pytest", "setuptools", "toml"] 54 | 55 | # backward compatibility with black 56 | [tool.pylint.messages_control] 57 | disable = "C0330, C0326" 58 | [tool.pylint.format] 59 | max-line-length = "88" 60 | 61 | [build-system] 62 | requires = ["poetry-core>=1.0.0"] 63 | build-backend = "poetry.core.masonry.api" 64 | -------------------------------------------------------------------------------- /single_source/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "__version__", 3 | "get_version", 4 | "SingleSourceError", 5 | "VersionNotFoundError", 6 | ) 7 | 8 | from pathlib import Path 9 | 10 | from .errors import SingleSourceError, VersionNotFoundError 11 | from .version import get_version 12 | 13 | __version__ = get_version(__name__, Path(__file__).parent.parent) 14 | -------------------------------------------------------------------------------- /single_source/errors.py: -------------------------------------------------------------------------------- 1 | class SingleSourceError(Exception): 2 | """Base exception for the library""" 3 | 4 | 5 | class VersionNotFoundError(SingleSourceError): 6 | """ 7 | Raise when the version of a package has not been found 8 | neither in package metadata nor in a file 9 | """ 10 | -------------------------------------------------------------------------------- /single_source/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabbit72/single-source/73ca29c6f5f97b23c406ea7943c9a84a68de1bcc/single_source/py.typed -------------------------------------------------------------------------------- /single_source/version.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata as importlib_metadata 2 | import re 3 | from pathlib import Path 4 | from typing import Optional, Union 5 | 6 | from .errors import VersionNotFoundError 7 | 8 | VERSION_REGEX = r"\s*version\s*=\s*[\"']\s*([-.\w]{3,})\s*[\"']\s*" 9 | 10 | 11 | def get_version( 12 | package_name: str, 13 | target_dir_or_file: Union[Path, str], 14 | *, 15 | fail: bool = False, 16 | default_return: Optional[str] = "", 17 | version_regex: str = VERSION_REGEX, 18 | ) -> Optional[str]: 19 | """ 20 | Retrieve your project version from package metadata or file with regex 21 | (by default from "pyproject.toml") 22 | 23 | :param package_name: The name of your package 24 | :type package_name: str 25 | :param target_dir_or_file: A path to a file or to a directory 26 | which contains pyproject.toml 27 | :type target_dir_or_file: Union[Path, str] 28 | :param fail: Raises VersionNotFoundError if True, by default False 29 | :type fail: bool 30 | :param default_return: Returns when the version hasn't been found, 31 | empty string by default, ignored when fail is True 32 | :type default_return: Optional[str] 33 | :param version_regex: Regular expression for parsing version from a file 34 | :type version_regex: str 35 | 36 | :raises: VersionNotFoundError 37 | :return: Version of the package, returns "default_return" value 38 | if version cannot be parsed 39 | :rtype: Optional[str] 40 | """ 41 | if isinstance(target_dir_or_file, (Path, str)): 42 | target_path = Path(target_dir_or_file) 43 | else: 44 | raise TypeError( 45 | f"'target_dir_or_file' argument can be 'str' or " 46 | f"'pathlib.Path' type, got {type(target_dir_or_file)}" 47 | ) 48 | 49 | pyproject_name = "pyproject.toml" 50 | if not target_path.is_file(): 51 | target_path /= pyproject_name 52 | 53 | version: Optional[str] = _get_version_from_path(target_path, version_regex) 54 | if version is None: 55 | version = _get_version_from_metadata(package_name) 56 | 57 | if not version and fail: 58 | raise VersionNotFoundError( 59 | f"You either have to install '{package_name}' package " 60 | f"or {target_path} must contain 'version' in [tool.poetry]" 61 | ) 62 | return version or default_return 63 | 64 | 65 | def _get_version_from_metadata(package_name: str) -> Optional[str]: 66 | """Implements a getting version flow for installed package""" 67 | try: 68 | version: str = importlib_metadata.version(package_name) # type: ignore 69 | except importlib_metadata.PackageNotFoundError: # type: ignore 70 | return None 71 | else: 72 | return version.strip() 73 | 74 | 75 | def _get_version_from_path(file_path: Path, version_regex: str) -> Optional[str]: 76 | """ 77 | Implements a getting version from file flow for developers, 78 | without installed package 79 | """ 80 | compiled_version_regex = re.compile(version_regex) 81 | try: 82 | with file_path.open(mode="r", encoding="utf-8") as file_with_version: 83 | for line in file_with_version: 84 | version: Optional[re.Match] = compiled_version_regex.search(line) # type: ignore # noqa: E501 85 | if version is not None: 86 | return version.group(1).strip() 87 | except (FileNotFoundError, UnicodeDecodeError): 88 | pass 89 | return None 90 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabbit72/single-source/73ca29c6f5f97b23c406ea7943c9a84a68de1bcc/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | import toml 5 | 6 | TEST_VERSION_STRINGS = [ 7 | ("version = '2.0.1-alpha.0' ", "2.0.1-alpha.0"), 8 | (" version= ' 20.0.0 '", "20.0.0"), 9 | ("version='5.8rc'", "5.8rc"), 10 | ("version='5.8RC0' ", "5.8RC0"), 11 | ('version = "2.0.0.dev0"', "2.0.0.dev0"), 12 | ('version = " 2.0.0-beta"', "2.0.0-beta"), 13 | ('version = "2.0.0b1"', "2.0.0b1"), 14 | ("version = '20.8b1'", "20.8b1"), 15 | ("version = '2018.08'", "2018.08"), 16 | ] 17 | 18 | 19 | @pytest.fixture 20 | def non_existing_package_name(): 21 | return "_random_package_" 22 | 23 | 24 | @pytest.fixture 25 | def bad_pyproject_path() -> Path: 26 | return Path("/some_dir/pyproject.toml") 27 | 28 | 29 | @pytest.fixture 30 | def correct_pyproject_path() -> Path: 31 | return Path(__file__).parent / "data" / "pyproject.toml" 32 | 33 | 34 | @pytest.fixture 35 | def version_from_pyproject(correct_pyproject_path: Path) -> str: 36 | pyproject = toml.load(str(correct_pyproject_path)) 37 | return pyproject["tool"]["poetry"]["version"].strip() 38 | 39 | 40 | @pytest.fixture 41 | def correct_setuppy_path() -> Path: 42 | return Path(__file__).parent / "data" / "setup.py" 43 | -------------------------------------------------------------------------------- /tests/data/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "modern-project" 3 | version = "0.1.0" 4 | -------------------------------------------------------------------------------- /tests/data/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup # type: ignore 2 | 3 | setup( 4 | name="HelloWorld", 5 | version="1.1.1.beta10", 6 | packages=find_packages(), 7 | scripts=["say_hello.py"], 8 | # Project uses reStructuredText, so ensure that the docutils get 9 | # installed or upgraded on the target machine 10 | install_requires=["docutils>=0.3"], 11 | package_data={ 12 | # If any package contains *.txt or *.rst files, include them: 13 | "": ["*.txt", "*.rst"], 14 | # And include any *.msg files found in the "hello" package, too: 15 | "hello": ["*.msg"], 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /tests/test_regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | from single_source.version import VERSION_REGEX 6 | 7 | from .conftest import TEST_VERSION_STRINGS 8 | 9 | 10 | @pytest.mark.parametrize("test_string,expected_version", TEST_VERSION_STRINGS) 11 | def test_default_version_regex(test_string, expected_version): 12 | version_regex = re.compile(VERSION_REGEX) 13 | version_from_string = version_regex.match(test_string).group(1) 14 | 15 | assert version_from_string == expected_version 16 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from single_source.version import ( 7 | VERSION_REGEX, 8 | VersionNotFoundError, 9 | _get_version_from_metadata, 10 | _get_version_from_path, 11 | get_version, 12 | ) 13 | 14 | 15 | def test_get_version_from_installed_package(mocker): 16 | expected_version = "2.0.1-alpha.0" 17 | import importlib.metadata 18 | 19 | mocker.patch("importlib.metadata.version") 20 | importlib.metadata.version.return_value = expected_version 21 | 22 | version_from_metadata = _get_version_from_metadata("does not matter") 23 | assert version_from_metadata == expected_version 24 | 25 | 26 | def test_get_version_from_pyproject( 27 | correct_pyproject_path: Path, version_from_pyproject: str 28 | ): 29 | version = _get_version_from_path(correct_pyproject_path, VERSION_REGEX) 30 | assert version == version_from_pyproject 31 | 32 | 33 | def test_get_version_raise_exception(non_existing_package_name, bad_pyproject_path): 34 | with pytest.raises(VersionNotFoundError): 35 | get_version(non_existing_package_name, bad_pyproject_path, fail=True) 36 | 37 | 38 | def test_get_version_default_return_works( 39 | non_existing_package_name, bad_pyproject_path 40 | ): 41 | custom_return_value = "my_version" 42 | raise_exception = False 43 | 44 | return_value = get_version( 45 | non_existing_package_name, 46 | bad_pyproject_path, 47 | fail=raise_exception, 48 | default_return=custom_return_value, 49 | ) 50 | 51 | assert return_value == custom_return_value 52 | 53 | 54 | def test_get_version_from_setuppy(correct_setuppy_path): 55 | expected_version = "1.1.1.beta10" 56 | 57 | version = _get_version_from_path(correct_setuppy_path, VERSION_REGEX) 58 | assert version == expected_version 59 | 60 | 61 | def test_get_version_pyproject_first_priority_than_from_installed_package( 62 | correct_pyproject_path: Path, version_from_pyproject: str 63 | ): 64 | package_name = "dummy_name" 65 | fail = True 66 | 67 | version = get_version(package_name, correct_pyproject_path.parent, fail=fail) 68 | assert version == version_from_pyproject 69 | --------------------------------------------------------------------------------