├── .gitignore ├── .gitlab-ci.yml ├── .pydocstyle.ini ├── .style.yapf ├── .travis.yml ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── mypy.ini ├── pylintrc ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py ├── src └── tox_poetry_dev_dependencies │ ├── __init__.py │ └── _hooks.py ├── test └── test_unit.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | # Python 5 | /*.egg 6 | /.eggs/ 7 | /.mypy_cache/ 8 | /.pytest_cache/ 9 | /.tox/ 10 | /.venv/ 11 | /build/ 12 | /dist/ 13 | *.dist-info/ 14 | *.egg-info/ 15 | __pycache__/ 16 | *.pyc 17 | 18 | 19 | # EOF 20 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | '.review': 5 | script: 6 | - 'export TOXENV="${CI_JOB_NAME##review}"' 7 | - 'python3 -m pip install tox' 8 | - 'python3 -m tox' 9 | - 'python3 -m tox -e package' 10 | 11 | 'review py35': 12 | extends: '.review' 13 | image: 'python:3.5' 14 | 15 | 'review py36': 16 | extends: '.review' 17 | image: 'python:3.6' 18 | 19 | 'review py37': 20 | extends: '.review' 21 | image: 'python:3.7' 22 | 23 | 'review py38': 24 | extends: '.review' 25 | image: 'python:3.8' 26 | 27 | 'review py39': 28 | extends: '.review' 29 | image: 'python:3.9' 30 | 31 | 'review py310': 32 | allow_failure: true 33 | extends: '.review' 34 | image: 'python:3.10-rc' 35 | 36 | 37 | ... # EOF 38 | -------------------------------------------------------------------------------- /.pydocstyle.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [pydocstyle] 5 | match = .*\.py 6 | 7 | 8 | # EOF 9 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [style] 5 | 6 | based_on_style = pep8 7 | blank_line_before_nested_class_or_def = true 8 | blank_line_before_module_docstring = true 9 | dedent_closing_brackets = true 10 | 11 | 12 | # EOF 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | dist: 'xenial' 5 | 6 | language: 'python' 7 | 8 | python: 9 | - '3.5' 10 | - '3.6' 11 | - '3.7' 12 | - '3.8' 13 | - '3.9' 14 | - '3.10-dev' 15 | 16 | jobs: 17 | allow_failures: 18 | - python: '3.10-dev' 19 | 20 | install: 21 | - 'python3 -m pip install tox tox-travis' 22 | 23 | script: 24 | - 'python3 -m tox' 25 | - 'python3 -m tox -e package' 26 | 27 | deploy: 28 | - provider: 'releases' 29 | api_key: '${GITHUB_TOKEN}' 30 | file_glob: true 31 | file: 'dist/*' 32 | skip_cleanup: true 33 | on: 34 | python: '3.8' 35 | tags: true 36 | - provider: 'pypi' 37 | distributions: 'sdist bdist_wheel' 38 | user: '${PYPI_USER}' 39 | password: '${PYPI_PASSWORD}' 40 | on: 41 | python: '3.8' 42 | tags: true 43 | 44 | sudo: false 45 | 46 | 47 | ... # EOF 48 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. 2 | 3 | 4 | .. Keep the current version number on line number 6 5 | 6 | 0.0.10.dev0 7 | =========== 8 | 9 | 10 | 0.0.9 11 | ===== 12 | 13 | *2020-11-18* 14 | 15 | * Fix issue occuring when there are no 'dev' dependencies in the 'lockfile' 16 | 17 | 18 | 0.0.8 19 | ===== 20 | 21 | *2020-11-16* 22 | 23 | * Fix some compatibility issues for Python 3.5 24 | 25 | 26 | 0.0.7 27 | ===== 28 | 29 | *2020-11-09* 30 | 31 | * Fix issue with unwarranted exceptions for unsupported locked dependencies. 32 | 33 | 34 | 0.0.6 35 | ===== 36 | 37 | *2020-11-08* 38 | 39 | * Add support for Python 3.9 40 | * Fix issue with type hints that would cause failures on Python interpreters where `from __future__ import annotations` is not available (Python < 3.7). 41 | 42 | 43 | 0.0.5 44 | ===== 45 | 46 | *2020-10-19* 47 | 48 | * Add support for URL dependencies in lockfile 49 | 50 | 51 | 0.0.4 52 | ===== 53 | 54 | *2020-10-13* 55 | 56 | * Rename setting ``add_poetry_dev_dependencies`` to ``poetry_add_dev_dependencies``. 57 | * Add the ``poetry_experimental_add_locked_dependencies`` setting to let Tox add Poetry's locked dependencies from Poetry's lockfile (experimental feature). 58 | * Remove the *PEP 396* ``__version__``. This also allows getting rid of the dependency on `importlib-metadata``. 59 | 60 | 61 | 0.0.3 62 | ===== 63 | 64 | *2020-10-06* 65 | 66 | * Add the ``poetry_experimental_no_virtual_env`` setting to allow skipping the creation of the virtual environment (experimental feature) 67 | 68 | 69 | 0.0.2 70 | ===== 71 | 72 | *2020-09-28* 73 | 74 | * Allow download from alternative repositories (without authentication) for pip via environment variables 75 | 76 | 77 | 0.0.1 78 | ===== 79 | 80 | *2020-09-17* 81 | 82 | * Fix a small issue that blocked the usage under Python 3.5 83 | * Make the dependencies mandatory 84 | 85 | 86 | 0.0.0 87 | ===== 88 | 89 | *2020-09-11* 90 | 91 | * Initial implementation 92 | 93 | 94 | .. EOF 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. 2 | 3 | 4 | Git commit messages 5 | =================== 6 | 7 | As this project is hosted on multiple platforms (GitHub and GitLab currently), every line of the git commit messages mentioning a magic number (such as issue or pull request number for example) should be prefixed with the name of the platform. For example a line in a commit message of the form: 8 | 9 | .. code:: 10 | 11 | Closes #XX 12 | 13 | 14 | should be instead written as: 15 | 16 | .. code:: 17 | 18 | GitHub: closes #XX 19 | GitLab: refs #ZZ 20 | GitLab: refs #YY 21 | SomeThing: whatever #XX 22 | 23 | 24 | Some tools (Gerrit for example) can be configured (with regexes for example) to correctly link to the items on the right platform. 25 | 26 | * https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#commentlink 27 | 28 | 29 | Hacking 30 | ======= 31 | 32 | This project makes extensive use of `tox`_, `pytest`_, and `GNU Make`_. 33 | 34 | 35 | Development environment 36 | ----------------------- 37 | 38 | Use following command to create a Python virtual environment with all necessary dependencies:: 39 | 40 | tox --recreate -e develop 41 | 42 | This creates a Python virtual environment in the ``.tox/develop`` directory. It can be activated with the following command:: 43 | 44 | . .tox/develop/bin/activate 45 | 46 | 47 | Run test suite 48 | -------------- 49 | 50 | In a Python virtual environment run the following command:: 51 | 52 | make review 53 | 54 | Outside of a Python virtual environment run the following command:: 55 | 56 | tox --recreate 57 | 58 | 59 | Build and package 60 | ----------------- 61 | 62 | In a Python virtual environment run the following command:: 63 | 64 | make package 65 | 66 | Outside of a Python virtual environment run the following command:: 67 | 68 | tox --recreate -e package 69 | 70 | 71 | .. Links 72 | 73 | .. _`GNU Make`: https://www.gnu.org/software/make/ 74 | .. _`pytest`: https://pytest.org/ 75 | .. _`tox`: https://tox.readthedocs.io/ 76 | 77 | 78 | .. EOF 79 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | include CHANGELOG.rst 5 | include LICENSE.txt 6 | 7 | 8 | # EOF 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | source_dir := ./src 5 | tests_dir := ./test 6 | 7 | 8 | .DEFAULT_GOAL := all 9 | 10 | 11 | .PHONY: all 12 | all: develop review package 13 | 14 | 15 | .PHONY: refresh 16 | refresh: clean all 17 | 18 | 19 | .PHONY: develop 20 | develop: 21 | python3 setup.py develop 22 | 23 | 24 | .PHONY: package 25 | package: sdist wheel 26 | 27 | 28 | .PHONY: sdist 29 | sdist: 30 | python3 setup.py sdist 31 | python3 -m twine check dist/*.tar.gz 32 | 33 | 34 | .PHONY: wheel 35 | wheel: 36 | python3 setup.py bdist_wheel 37 | python3 -m twine check dist/*.whl 38 | 39 | 40 | .PHONY: format 41 | format: 42 | python3 -m yapf --in-place --parallel --recursive setup.py src test 43 | 44 | 45 | .PHONY: check 46 | check: 47 | python3 setup.py check 48 | 49 | 50 | .PHONY: lint 51 | lint: 52 | python3 -m pytest --mypy --pycodestyle --pydocstyle --pylint \ 53 | -m 'mypy or pycodestyle or pydocstyle or pylint' 54 | 55 | 56 | .PHONY: mypy 57 | mypy: 58 | python3 -m pytest --mypy -m mypy 59 | 60 | 61 | .PHONY: pycodestyle 62 | pycodestyle: 63 | python3 -m pytest --pycodestyle -m pycodestyle 64 | 65 | 66 | .PHONY: pydocstyle 67 | pydocstyle: 68 | python3 -m pytest --pydocstyle -m pydocstyle 69 | 70 | 71 | .PHONY: pylint 72 | pylint: 73 | python3 -m pytest --pylint -m pylint 74 | 75 | 76 | .PHONY: yapf 77 | yapf: 78 | python3 -m pytest --yapf -m yapf 79 | 80 | 81 | .PHONY: test 82 | test: pytest 83 | 84 | 85 | .PHONY: pytest 86 | pytest: 87 | python3 -m pytest 88 | 89 | 90 | .PHONY: review 91 | review: check 92 | python3 -m pytest --mypy --pycodestyle --pydocstyle --pylint 93 | 94 | 95 | .PHONY: clean 96 | clean: 97 | $(RM) --recursive ./.eggs/ 98 | $(RM) --recursive ./.mypy_cache/ 99 | $(RM) --recursive ./.pytest_cache/ 100 | $(RM) --recursive ./build/ 101 | $(RM) --recursive ./dist/ 102 | $(RM) --recursive ./__pycache__/ 103 | find $(source_dir) -name '*.dist-info' -type d -exec $(RM) --recursive {} + 104 | find $(source_dir) -name '*.egg-info' -type d -exec $(RM) --recursive {} + 105 | find $(source_dir) -name '*.pyc' -type f -exec $(RM) {} + 106 | find $(tests_dir) -name '*.pyc' -type f -exec $(RM) {} + 107 | find $(source_dir) -name '__pycache__' -type d -exec $(RM) --recursive {} + 108 | find $(tests_dir) -name '__pycache__' -type d -exec $(RM) --recursive {} + 109 | 110 | 111 | # 112 | # Options 113 | # 114 | 115 | # Disable default rules and suffixes (improve speed and avoid unexpected behaviour) 116 | MAKEFLAGS := --no-builtin-rules --warn-undefined-variables 117 | .SUFFIXES: 118 | 119 | 120 | # EOF 121 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. 2 | 3 | 4 | .. contents:: 5 | :backlinks: none 6 | 7 | 8 | Introduction 9 | ============ 10 | 11 | Tox plugin to help working with Poetry-based projects. 12 | 13 | 14 | Repositories 15 | ------------ 16 | 17 | Distributions: 18 | 19 | * https://pypi.org/project/tox-poetry-dev-dependencies/ 20 | 21 | 22 | Source code: 23 | 24 | * https://github.com/sinoroc/tox-poetry-dev-dependencies 25 | * https://gitlab.com/sinoroc/tox-poetry-dev-dependencies 26 | 27 | 28 | Usage 29 | ===== 30 | 31 | By default the plugin does not do anything. Use one of the following settings to activate the corresponding features. 32 | 33 | 34 | ``poetry_experimental_add_locked_dependencies`` 35 | ----------------------------------------------- 36 | 37 | Set the ``testenv`` setting ``poetry_experimental_add_locked_dependencies`` to ``True`` to let Tox add Poetry's locked dependencies from the *lockfile* to the ``deps`` list in the test environment. 38 | 39 | .. code:: 40 | 41 | [testenv:example] 42 | # ... 43 | poetry_experimental_add_locked_dependencies = True 44 | 45 | 46 | ``poetry_add_dev_dependencies`` 47 | ------------------------------- 48 | 49 | Set the ``testenv`` setting ``poetry_add_dev_dependencies`` to ``True`` to let Tox add Poetry's development dependencies to the ``deps`` list in the test environment. 50 | 51 | .. code:: 52 | 53 | [testenv:example] 54 | # ... 55 | poetry_add_dev_dependencies = True 56 | 57 | 58 | Dependency settings combination 59 | ------------------------------- 60 | 61 | The settings ``poetry_experimental_add_locked_dependencies`` and ``poetry_add_dev_dependencies`` are independent and can be used in combination. The following table shows the expected result for each possible combination of these two settings. 62 | 63 | The *source file* column shows which file is used as source for the dependencies. The ``deps`` column shows an example of what dependencies are expected to be added to ``deps`` for that test environment. In that example ``Lib = '~1.0'`` is a mandatory dependency locking to ``Lib==1.2.3`` and ``Dev = '~3.0'`` is a development dependency locking to ``Dev==3.2.1``. 64 | 65 | .. |downwards-double-arrow| unicode:: 0x21d3 66 | 67 | .. list-table:: 68 | :header-rows: 2 69 | :stub-columns: 1 70 | 71 | * - ``*_locked_dependencies`` 72 | - ``False`` 73 | - ``True`` 74 | - ``False`` 75 | - ``True`` 76 | * - ``*_dev_dependencies`` 77 | - ``False`` 78 | - ``False`` 79 | - ``True`` 80 | - ``True`` 81 | * - |downwards-double-arrow| 82 | - 83 | - 84 | - 85 | - 86 | * - read from file 87 | - *none* 88 | - ``poetry.lock`` 89 | - ``pyproject.toml`` 90 | - ``poetry.lock`` 91 | * - |downwards-double-arrow| 92 | - 93 | - 94 | - 95 | - 96 | * - add to ``deps`` 97 | - *nothing* 98 | - ``Lib==1.2.3`` 99 | - ``Dev~=3.0`` 100 | - ``Lib==1.2.3``, ``Dev==3.2.1`` 101 | 102 | 103 | ``poetry_use_source_repos`` 104 | --------------------------- 105 | 106 | Set the ``testenv`` setting ``poetry_use_source_repos`` to ``pip_env_vars`` to let Tox set the ``PIP_EXTRA_URL`` and ``PIP_EXTRA_INDEX_URL`` environment variables accordingly. 107 | 108 | .. code:: 109 | 110 | [testenv:example] 111 | # ... 112 | poetry_use_source_repos = pip_env_vars 113 | 114 | 115 | This will read sections such as the following from the ``pyproject.toml`` file: 116 | 117 | .. code:: 118 | 119 | [[tool.poetry.source]] 120 | name = "project-alpha" 121 | url = "https://alpha.example/simple" 122 | secondary = true 123 | 124 | [[tool.poetry.source]] 125 | name = "project-bravo" 126 | url = "https://bravo.example/simple" 127 | 128 | [[tool.poetry.source]] 129 | name = "project-charlie" 130 | url = "https://charlie.example/simple" 131 | default = true 132 | 133 | 134 | and set the environment variables: 135 | 136 | .. code:: 137 | 138 | PIP_INDEX_URL=https://charlie.example/simple 139 | PIP_EXTRA_INDEX_URL=https://bravo.example/simple https://pypi.org/simple https://alpha.example/simple 140 | 141 | 142 | If there is at least one non ``secondary`` source repository defined, then pip's default index server (*PyPI* ``https://pypi.org/simple``) is placed in ``PIP_EXTRA_INDEX_URL`` right before any ``secondary`` respository. 143 | 144 | If pip's environment variables are already defined then they are not overwritten. For example in a command like the following, the plugin does not overwrite the environment variable. 145 | 146 | .. code:: 147 | 148 | PIP_INDEX_URL=https://delta.example/simple tox 149 | 150 | 151 | ``poetry_experimental_no_virtual_env`` 152 | -------------------------------------- 153 | 154 | *Experimental feature* 155 | 156 | Set the ``testenv`` setting ``poetry_experimental_no_virtual_env`` to ``True`` to skip the creation of a virtual environment for this test environment. 157 | 158 | .. code:: 159 | 160 | [testenv:real] 161 | deps = 162 | poetry_experimental_no_virtual_env = True 163 | skip_install = True 164 | 165 | 166 | This might be useful in cases where all the required dependencies and tools are already available, i.e. they are already installed in global or user *site packages* directory, or maybe they are already installed directly in the system (via ``apt``, ``yum``, ``pacman``, etc.). 167 | 168 | For such environments it might be best to skip the installation of the project (``skip_install``) as well as keeping the list of dependencies empty (``deps``). 169 | 170 | 171 | Appendix 172 | ======== 173 | 174 | Installation 175 | ------------ 176 | 177 | It is a plugin for Tox and it is available on PyPI, install it however best fits the workflow. A useful thing to know though, is that starting with Tox version *3.8* it is possible to enforce the installation (in an isolated environment) of the plugin directly from within the ``tox.ini`` file, thanks to the ``requires`` setting (Tox *3.2*) and the *auto-provisioning* feature (Tox *3.8*): 178 | 179 | .. code:: 180 | 181 | [tox] 182 | requires = 183 | tox-poetry-dev-dependencies 184 | 185 | 186 | * https://tox.readthedocs.io/en/latest/config.html#conf-requires 187 | * https://tox.readthedocs.io/en/latest/example/basic.html#tox-auto-provisioning 188 | 189 | 190 | Similar projects 191 | ---------------- 192 | 193 | * https://pypi.org/project/tox-poetry-installer/ 194 | * https://pypi.org/project/tox-poetry/ 195 | 196 | 197 | .. EOF 198 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [mypy] 5 | files = *.py, src/**/*.py, test/**/*.py 6 | show_error_codes = True 7 | strict = True 8 | warn_return_any = True 9 | warn_unreachable = True 10 | warn_unused_configs = True 11 | warn_unused_ignores = True 12 | 13 | [mypy-poetry.*] 14 | ignore_missing_imports = True 15 | 16 | [mypy-setuptools.*] 17 | ignore_missing_imports = True 18 | 19 | [mypy-tomlkit.*] 20 | ignore_missing_imports = True 21 | 22 | [mypy-tox.*] 23 | ignore_missing_imports = True 24 | 25 | 26 | # EOF 27 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [MASTER] 5 | 6 | load-plugins = 7 | pylint.extensions.bad_builtin, 8 | pylint.extensions.broad_try_clause, 9 | pylint.extensions.check_elif, 10 | pylint.extensions.comparetozero, 11 | pylint.extensions.docparams, 12 | pylint.extensions.docstyle, 13 | pylint.extensions.emptystring, 14 | pylint.extensions.mccabe, 15 | pylint.extensions.overlapping_exceptions, 16 | pylint.extensions.redefined_variable_type, 17 | 18 | 19 | [MESSAGES CONTROL] 20 | 21 | enable = 22 | useless-suppression, 23 | 24 | 25 | # EOF 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [build-system] 5 | build-backend = 'setuptools.build_meta' 6 | requires = [ 7 | 'setuptools >= 43.0.0', # https://github.com/pypa/setuptools/pull/1634 8 | ] 9 | 10 | 11 | # EOF 12 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [pytest] 5 | addopts = 6 | --disable-pytest-warnings 7 | --pylint-error-types='CEFIRW' 8 | --yapfdiff 9 | 10 | 11 | # EOF 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [check] 5 | metadata = 1 6 | strict = 1 7 | 8 | 9 | [metadata] 10 | author = sinoroc 11 | author_email = sinoroc.code+python@gmail.com 12 | classifiers = 13 | Development Status :: 2 - Pre-Alpha 14 | Framework :: tox 15 | Programming Language :: Python :: 3 :: Only 16 | Programming Language :: Python :: 3.5 17 | Programming Language :: Python :: 3.6 18 | Programming Language :: Python :: 3.7 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: 3.9 21 | Topic :: Software Development :: Testing 22 | Typing :: Typed 23 | description = Tox plugin to help working with Poetry-based projects 24 | license = Apache-2.0 25 | license_file = LICENSE.txt 26 | long_description = file: README.rst 27 | long_description_content_type = text/x-rst 28 | name = tox-poetry-dev-dependencies 29 | project_urls = 30 | GitHub = https://github.com/sinoroc/tox-poetry-dev-dependencies 31 | GitLab = https://gitlab.com/sinoroc/tox-poetry-dev-dependencies 32 | url = https://pypi.org/project/tox-poetry-dev-dependencies/ 33 | 34 | 35 | [options] 36 | install_requires = 37 | poetry-core ~= 1.0 38 | tomlkit 39 | tox 40 | package_dir = 41 | = src 42 | packages = find: 43 | python_requires = ~= 3.5 44 | 45 | 46 | [options.entry_points] 47 | tox = 48 | poetry_dev_dependencies = tox_poetry_dev_dependencies._hooks 49 | 50 | 51 | [options.extras_require] 52 | dev_package = 53 | twine 54 | wheel 55 | dev_test = 56 | mypy 57 | pycodestyle 58 | pydocstyle 59 | pylint 60 | pytest 61 | pytest-mypy 62 | pytest-pycodestyle 63 | pytest-pydocstyle 64 | pytest-pylint 65 | pytest-yapf3 66 | yapf 67 | 68 | 69 | [options.packages.find] 70 | where = src 71 | 72 | 73 | # EOF 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Setup.""" 4 | 5 | import pathlib 6 | 7 | import setuptools 8 | 9 | 10 | def _get_version() -> str: 11 | file_name = 'CHANGELOG.rst' 12 | line_number = 5 13 | here_path = pathlib.Path(__file__).resolve().parent 14 | with here_path.joinpath(file_name).open() as file_: 15 | changelog = file_.read() 16 | version = changelog.splitlines()[line_number] 17 | return version 18 | 19 | 20 | def _main() -> None: 21 | setuptools.setup( 22 | # see 'setup.cfg' 23 | version=_get_version(), 24 | ) 25 | 26 | 27 | if __name__ == '__main__': 28 | _main() 29 | 30 | # EOF 31 | -------------------------------------------------------------------------------- /src/tox_poetry_dev_dependencies/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Tox plugin to help working with Poetry-based projects.""" 4 | 5 | # EOF 6 | -------------------------------------------------------------------------------- /src/tox_poetry_dev_dependencies/_hooks.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Tox plugin hooks.""" 4 | 5 | import pathlib 6 | import typing 7 | 8 | import poetry.core.factory 9 | import tomlkit 10 | import tox 11 | 12 | IndexServersT = typing.Tuple[ 13 | tox.config.IndexServerConfig, # PIP_INDEX_URL 14 | typing.List[tox.config.IndexServerConfig], # PIP_EXTRA_INDEX_URL 15 | ] 16 | 17 | LockedDepT = typing.Tuple[tox.config.DepConfig, str] 18 | LockedDepsT = typing.Dict[str, typing.List[LockedDepT]] 19 | 20 | PIP_DEFAULT_INDEX_SERVER_URL = 'https://pypi.org/simple' 21 | PIP_DEFAULT_INDEX_SERVER_NAME = 'pypi' 22 | 23 | POETRY_LOCKFILE_FILE_NAME = 'poetry.lock' 24 | 25 | 26 | class _Exception(Exception): 27 | """Base exception.""" 28 | 29 | 30 | class NoPoetryFound(_Exception): 31 | """No poetry found.""" 32 | 33 | 34 | class NoPyprojectTomlFound(_Exception): 35 | """No 'pyproject.toml' file found.""" 36 | 37 | 38 | class CanNotHaveMultipleDefaultSourceRepositories(_Exception): 39 | """Can not have multiple 'default' source repositories.""" 40 | 41 | 42 | @tox.hookimpl # type: ignore[misc] 43 | def tox_addoption(parser: tox.config.Parser) -> None: 44 | """Set hook.""" 45 | parser.add_testenv_attribute( 46 | 'poetry_add_dev_dependencies', 47 | 'bool', 48 | "Add Poetry's 'dev-dependencies' to the test environment.", 49 | default=False, 50 | ) 51 | parser.add_testenv_attribute( 52 | 'poetry_use_source_repos', 53 | 'string', 54 | ( 55 | "Use Poetry's source repositories. Set 'pip_env_vars' to set as" 56 | " Pip environment variables ('PIP_INDEX_URL' and" 57 | " 'PIP_EXTRA_INDEX_URL')." 58 | ), 59 | ) 60 | parser.add_testenv_attribute( 61 | 'poetry_experimental_no_virtual_env', 62 | 'bool', 63 | "(EXPERIMENTAL) Do not create a virtual environment.", 64 | default=False, 65 | ) 66 | parser.add_testenv_attribute( 67 | 'poetry_experimental_add_locked_dependencies', 68 | 'bool', 69 | ( 70 | "(EXPERIMENTAL) Add Poetry's locked dependencies from the lockfile" 71 | " to 'deps' in the test environment." 72 | ), 73 | default=False, 74 | ) 75 | 76 | 77 | def _is_test_env(env_config: tox.config.TestenvConfig) -> bool: 78 | """Check if it is a test environment. 79 | 80 | Tox creates environments for provisioning (`.tox`) and for isolated build 81 | (`.packaging`) in addition to the usual test environments. And in hooks 82 | such as `tox_testenv_create` it is not clear if the environment is a test 83 | environment or one of those environments used for tox's own purposes. 84 | 85 | So we start by excluding the provisioning environment named after 86 | `provision_tox_env` and the build isolation environment named after 87 | `isolated_build_env`. Then we keep only the environments listed in 88 | `envlist`. 89 | """ 90 | # 91 | is_test_env = False 92 | # 93 | tox_config = env_config.config 94 | env_name = env_config.envname 95 | # 96 | known_private_env_names = [] 97 | # 98 | provision_tox_env = getattr(tox_config, 'provision_tox_env', None) 99 | if provision_tox_env: 100 | known_private_env_names.append(provision_tox_env) 101 | # 102 | isolated_build_env = getattr(tox_config, 'isolated_build_env', None) 103 | if isolated_build_env: 104 | known_private_env_names.append(isolated_build_env) 105 | # 106 | if env_name not in known_private_env_names: 107 | if env_name in tox_config.envlist: 108 | is_test_env = True 109 | # 110 | return is_test_env 111 | 112 | 113 | @tox.hookimpl # type: ignore[misc] 114 | def tox_configure(config: tox.config.Config) -> None: 115 | """Set hook.""" 116 | # 117 | project_dir_path = pathlib.Path(str(config.setupdir)) 118 | # 119 | try: 120 | poetry_ = _get_poetry(project_dir_path) 121 | except (NoPoetryFound, NoPyprojectTomlFound): 122 | pass 123 | else: 124 | # 125 | locked_deps = _get_locked_deps(project_dir_path) 126 | _add_locked_dependencies(config, locked_deps) 127 | # 128 | dev_deps = _get_dev_requirements(poetry_) 129 | _add_dev_dependencies(config, dev_deps) 130 | # 131 | index_servers = _get_index_servers(poetry_) 132 | _add_index_servers(config, index_servers) 133 | 134 | 135 | @tox.hookimpl # type: ignore[misc] 136 | def tox_testenv_create( 137 | venv: tox.venv.VirtualEnv, 138 | action: tox.action.Action, # pylint: disable=unused-argument 139 | ) -> typing.Any: 140 | """Set hook.""" 141 | # 142 | result = None 143 | # 144 | if _is_test_env(venv.envconfig): 145 | if venv.envconfig.poetry_experimental_no_virtual_env is True: 146 | # 147 | tox.venv.cleanup_for_venv(venv) 148 | # 149 | python_link_name = venv.envconfig.get_envpython() 150 | python_link_path = pathlib.Path(python_link_name) 151 | python_link_path.parent.mkdir(parents=True) 152 | python_link_target = ( 153 | tox.interpreters.tox_get_python_executable(venv.envconfig) 154 | ) 155 | pathlib.Path(python_link_name).symlink_to(python_link_target) 156 | # 157 | result = True # anything but None 158 | # 159 | return result 160 | 161 | 162 | def _add_dev_dependencies( 163 | tox_config: tox.config.Config, 164 | dev_dep_configs: typing.Iterable[tox.config.DepConfig], 165 | ) -> None: 166 | # 167 | for env_config in tox_config.envconfigs.values(): 168 | if env_config.poetry_experimental_add_locked_dependencies is not True: 169 | if _is_test_env(env_config): 170 | if env_config.poetry_add_dev_dependencies is True: 171 | for dep_config in dev_dep_configs: 172 | env_config.deps.append(dep_config) 173 | 174 | 175 | def _add_index_servers( 176 | tox_config: tox.config.Config, 177 | index_servers: IndexServersT, 178 | ) -> None: 179 | # 180 | for env_config in tox_config.envconfigs.values(): 181 | if env_config.poetry_use_source_repos == 'pip_env_vars': 182 | _add_index_servers_as_pip_env_vars(env_config, index_servers) 183 | 184 | 185 | def _add_index_servers_as_pip_env_vars( 186 | env_config: tox.config.TestenvConfig, 187 | index_servers: IndexServersT, 188 | ) -> None: 189 | # 190 | pip_index_server = index_servers[0] 191 | pip_extra_index_servers = index_servers[1] 192 | # 193 | env_vars = env_config.setenv 194 | # 195 | if env_vars.get('PIP_INDEX_URL') is None and pip_index_server: 196 | env_vars['PIP_INDEX_URL'] = pip_index_server.url 197 | # 198 | if env_vars.get('PIP_EXTRA_INDEX_URL') is None and pip_extra_index_servers: 199 | env_config.setenv['PIP_EXTRA_INDEX_URL'] = ' '.join( 200 | [index_server.url for index_server in pip_extra_index_servers], 201 | ) 202 | 203 | 204 | def _add_locked_dependencies( 205 | tox_config: tox.config.Config, 206 | locked_deps: LockedDepsT, 207 | ) -> None: 208 | # 209 | for env_config in tox_config.envconfigs.values(): 210 | if _is_test_env(env_config): 211 | if env_config.poetry_experimental_add_locked_dependencies is True: 212 | _add_locked_deps(env_config, locked_deps, 'main') 213 | if env_config.poetry_add_dev_dependencies is True: 214 | _add_locked_deps(env_config, locked_deps, 'dev') 215 | 216 | 217 | def _add_locked_deps( 218 | env_config: tox.config.TestenvConfig, 219 | locked_deps: LockedDepsT, 220 | category: str, 221 | ) -> None: 222 | # 223 | for dep_tuple in locked_deps.get(category, []): 224 | _add_locked_dep(env_config, dep_tuple) 225 | 226 | 227 | def _add_locked_dep( 228 | env_config: tox.config.TestenvConfig, 229 | dep_tuple: LockedDepT, 230 | ) -> None: 231 | # 232 | (tox_dep_config, poetry_dep) = dep_tuple 233 | if tox_dep_config: 234 | env_config.deps.append(tox_dep_config) 235 | else: 236 | error_msg = ( 237 | "The plugin 'tox-poetry-dev-dependencies' can not add the" 238 | " following locked dependency to the 'deps' configuration setting" 239 | " for the test environement '{}': {}" 240 | ).format(env_config.envname, poetry_dep) 241 | # 242 | tox.reporter.error(error_msg) 243 | 244 | 245 | def _get_poetry(project_root_path: pathlib.Path) -> poetry.core.poetry.Poetry: 246 | poetry_factory = poetry.core.factory.Factory() 247 | try: 248 | poetry_ = poetry_factory.create_poetry(str(project_root_path)) 249 | except RuntimeError as exc: 250 | raise NoPyprojectTomlFound from exc 251 | except poetry.core.pyproject.exceptions.PyProjectException as exc: 252 | raise NoPoetryFound from exc 253 | return poetry_ 254 | 255 | 256 | def _get_dev_requirements( 257 | poetry_: poetry.core.poetry.Poetry, 258 | ) -> typing.List[tox.config.DepConfig]: 259 | # 260 | requirements = [ 261 | tox.config.DepConfig(dependency.to_pep_508()) 262 | for dependency in poetry_.package.dev_requires 263 | ] 264 | return requirements 265 | 266 | 267 | def _get_index_servers( 268 | poetry_: poetry.core.poetry.Poetry, 269 | ) -> IndexServersT: 270 | # 271 | poetry_source_repos = poetry_.local_config.get('source', []) 272 | # 273 | poetry_default_server = None 274 | poetry_normal_servers = [] 275 | poetry_secondary_servers = [] 276 | # 277 | for poetry_source_repo in poetry_source_repos: 278 | server = tox.config.IndexServerConfig( 279 | poetry_source_repo['name'], 280 | poetry_source_repo['url'], 281 | ) 282 | # 283 | if poetry_source_repo.get('default', False) is True: 284 | if poetry_default_server is None: 285 | poetry_default_server = server 286 | else: 287 | raise CanNotHaveMultipleDefaultSourceRepositories 288 | # 289 | elif poetry_source_repo.get('secondary', False) is True: 290 | poetry_secondary_servers.append(server) 291 | else: 292 | poetry_normal_servers.append(server) 293 | # 294 | pip_index_server = None 295 | pip_extra_index_servers = [] 296 | # 297 | if poetry_default_server: 298 | pip_index_server = poetry_default_server 299 | elif poetry_normal_servers: 300 | pip_index_server = poetry_normal_servers.pop(0) 301 | # 302 | pip_extra_index_servers.extend(poetry_normal_servers) 303 | # 304 | if pip_index_server: 305 | # Pip's default index (PyPI) is not the default anymore, so it needs to 306 | # be inserted again right before the secondary indexes. 307 | pip_default_index_server = tox.config.IndexServerConfig( 308 | PIP_DEFAULT_INDEX_SERVER_NAME, 309 | PIP_DEFAULT_INDEX_SERVER_URL, 310 | ) 311 | pip_extra_index_servers.append(pip_default_index_server) 312 | # 313 | pip_extra_index_servers.extend(poetry_secondary_servers) 314 | # 315 | index_servers = ( 316 | pip_index_server, 317 | pip_extra_index_servers, 318 | ) 319 | # 320 | return index_servers 321 | 322 | 323 | def _get_locked_deps( 324 | project_root_path: pathlib.Path, 325 | ) -> LockedDepsT: 326 | # 327 | locked_deps = {} # type: LockedDepsT 328 | # 329 | lock_file_path = project_root_path.joinpath(POETRY_LOCKFILE_FILE_NAME) 330 | if lock_file_path.is_file(): 331 | # 332 | lock_str = lock_file_path.read_text() 333 | lock_document = tomlkit.parse(lock_str) 334 | # 335 | for dependency in lock_document['package']: 336 | # 337 | dep_config = None 338 | dep_pep_508 = None 339 | # 340 | dep_name = dependency['name'] 341 | dep_version = dependency['version'] 342 | # 343 | dep_source = dependency.get('source', None) 344 | if dep_source: 345 | if dep_source['type'] == 'url': 346 | dep_url = dep_source['url'] 347 | dep_pep_508 = '{} @ {}'.format(dep_name, dep_url) 348 | else: 349 | dep_pep_508 = '{}=={}'.format(dep_name, dep_version) 350 | # 351 | if dep_pep_508: 352 | dep_config = tox.config.DepConfig(dep_pep_508) 353 | # 354 | dep_tuple = (dep_config, dependency) 355 | # 356 | dep_category = dependency['category'] 357 | locked_deps.setdefault(dep_category, []).append(dep_tuple) 358 | # 359 | return locked_deps 360 | 361 | 362 | # EOF 363 | -------------------------------------------------------------------------------- /test/test_unit.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Unit tests.""" 4 | 5 | import types 6 | import unittest 7 | 8 | import tox_poetry_dev_dependencies 9 | 10 | 11 | class TestDummy(unittest.TestCase): 12 | """Dummy.""" 13 | 14 | def test_dummy(self) -> None: 15 | """Dummy test.""" 16 | self.assertIsInstance(tox_poetry_dev_dependencies, types.ModuleType) 17 | 18 | 19 | # EOF 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [tox] 5 | envlist = 6 | py35 7 | py36 8 | py37 9 | py38 10 | py39 11 | isolated_build = True 12 | 13 | 14 | [testenv] 15 | commands = 16 | # Run review under Python 3.8 as it also includes linting and type checking 17 | py38: make review 18 | !py38: make test 19 | extras = 20 | dev_test 21 | whitelist_externals = 22 | make 23 | 24 | 25 | [testenv:py310] 26 | description = Outcome is ignored since Python 3.10 is not released yet 27 | ignore_outcome = True 28 | 29 | 30 | [testenv:package] 31 | commands = 32 | make package 33 | extras = 34 | dev_package 35 | 36 | 37 | [testenv:develop] 38 | description = Use this environment for interactive development (activate) 39 | # 40 | commands = 41 | extras = 42 | dev_package 43 | dev_test 44 | usedevelop = True 45 | 46 | 47 | # EOF 48 | --------------------------------------------------------------------------------