├── .github └── workflows │ └── main.yaml ├── .gitignore ├── .gitlab-ci.yml ├── .pydocstyle.ini ├── .style.yapf ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── mypy.ini ├── pylintrc ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py ├── src └── deptree │ ├── __init__.py │ ├── __main__.py │ ├── _i18n.py │ ├── _meta.py │ ├── _pkg_resources.py │ ├── cli.py │ └── py.typed ├── test └── test_unit.py └── tox.ini /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | name: 'main-workflow' 5 | run-name: "Main workflow" 6 | 7 | on: 8 | push: 9 | 10 | jobs: 11 | 12 | 'review-job': 13 | 14 | name: "Review for Python ${{ matrix.python-version }}" 15 | runs-on: 'ubuntu-22.04' 16 | 17 | strategy: 18 | fail-fast: true 19 | matrix: 20 | python-version: 21 | - '3.7' 22 | - '3.8' 23 | - '3.9' 24 | - '3.10' 25 | - '3.11' 26 | 27 | env: 28 | 'BASE_PYTHON': '3.7' 29 | 30 | permissions: 31 | contents: 'write' 32 | discussions: 'write' 33 | 34 | steps: 35 | 36 | - name: "Setup Python ${{ matrix.python-version }}" 37 | uses: 'actions/setup-python@v4' 38 | with: 39 | python-version: '${{ matrix.python-version }}' 40 | 41 | - name: "Install tox" 42 | run: 'python -m pip install tox' 43 | 44 | - name: "Checkout code" 45 | uses: 'actions/checkout@v3' 46 | 47 | - name: "Review code for Python ${{ matrix.python-version }}" 48 | run: 'tox run -e py${{ matrix.python-version }}' 49 | 50 | - name: "Build distribution packages with Python ${{ matrix.python-version }}" 51 | if: "matrix.python-version == env.BASE_PYTHON" 52 | run: 'tox run -e package' 53 | 54 | - name: "Create GitHub release" 55 | if: "matrix.python-version == env.BASE_PYTHON && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')" 56 | run: 'gh release create --discussion-category "Announcements" --generate-notes --verify-tag "${GITHUB_REF_NAME}" dist/*' 57 | env: 58 | GITHUB_TOKEN: '${{ github.TOKEN }}' 59 | 60 | - name: "Clean `dist` directory before uploading to PyPI" 61 | if: "matrix.python-version == env.BASE_PYTHON && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')" 62 | run: "find './dist/' -type f -not -name '*.tar.gz' -not -name '*.whl' -delete" 63 | 64 | - name: "Publish distribution packages on PyPI" 65 | if: "matrix.python-version == env.BASE_PYTHON && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')" 66 | uses: 'pypa/gh-action-pypi-publish@v1.6.4' 67 | with: 68 | password: '${{ secrets.PYPI_API_TOKEN }}' 69 | 70 | 71 | ... # EOF 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | # Python 5 | /*.egg 6 | /.eggs/ 7 | /.pytest_cache/ 8 | /.tox/ 9 | /build/ 10 | /dist/ 11 | *.dist-info/ 12 | *.egg-info/ 13 | __pycache__/ 14 | *.pyc 15 | 16 | 17 | # vi 18 | .*.swp 19 | 20 | 21 | # EOF 22 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | '.review': 5 | before_script: 6 | - 'python -m pip install tox' 7 | script: 8 | - 'export TOXENV="${CI_JOB_NAME##review}"' 9 | - 'python -m tox' 10 | - 'python -m tox -e package' 11 | 12 | 'review py3.7': 13 | extends: '.review' 14 | image: 'python:3.7' 15 | 16 | 'review py3.8': 17 | extends: '.review' 18 | image: 'python:3.8' 19 | 20 | 'review py3.9': 21 | extends: '.review' 22 | image: 'python:3.9' 23 | 24 | 'review py3.10': 25 | extends: '.review' 26 | image: 'python:3.10' 27 | 28 | 'review py3.11': 29 | extends: '.review' 30 | image: 'python:3.11' 31 | 32 | 33 | ... # EOF 34 | -------------------------------------------------------------------------------- /.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_class_docstring = false 8 | blank_line_before_nested_class_or_def = true 9 | blank_line_before_module_docstring = true 10 | dedent_closing_brackets = true 11 | 12 | 13 | # EOF 14 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. 2 | 3 | 4 | .. Keep the current version number on line number 5 5 | 0.1.0.dev0 6 | ========== 7 | 8 | * Added type hints 9 | * Python minimum version *3.7* required 10 | 11 | 12 | 0.0.12 13 | ====== 14 | 15 | 2023-03-22 16 | 17 | * No functional changes 18 | * Development tooling and workflow improvements 19 | 20 | 21 | 0.0.11 22 | ====== 23 | 24 | 2023-03-21 25 | 26 | * Supported Python versions *3.7* to *3.11* 27 | * No functional changes 28 | * Development tooling and workflow improvements 29 | 30 | 31 | 0.0.10 32 | ====== 33 | 34 | 2020-04-17 35 | 36 | * Improve detection of circular dependencies 37 | 38 | 39 | 0.0.9 40 | ===== 41 | 42 | 2020-04-15 43 | 44 | * Show all dependencies (or dependents) in the flat view (instead of just 1 45 | level deep) 46 | * Show summary in CLI's help output 47 | * Rewrite code 48 | 49 | 50 | 0.0.8 51 | ===== 52 | 53 | 2020-01-13 54 | 55 | * Add '--flat' display option 56 | 57 | 58 | 0.0.7 59 | ===== 60 | 61 | 2019-11-13 62 | 63 | * Fix issue with unknown project in reverse 64 | 65 | 66 | 0.0.6 67 | ===== 68 | 69 | 2019-11-12 70 | 71 | * Handle unknown extras 72 | * Add help messages for CLI arguments 73 | 74 | 75 | 0.0.5 76 | ===== 77 | 78 | 2019-11-08 79 | 80 | * Rewrite implementation 81 | 82 | 83 | 0.0.4 84 | ===== 85 | 86 | 2019-10-24 87 | 88 | * Sort output alphabetically 89 | 90 | 91 | 0.0.3 92 | ===== 93 | 94 | 2019-10-23 95 | 96 | * Add 'reverse' to show tree of dependent projects 97 | 98 | 99 | 0.0.2 100 | ===== 101 | 102 | 2019-10-20 103 | 104 | * Add possibility to specify list of projects on command line 105 | * Add handling of extras 106 | * Change output format 107 | 108 | 109 | 0.0.1 110 | ===== 111 | 112 | 2019-09-04 113 | 114 | 115 | 0.0.0 116 | ===== 117 | 118 | 2019-09-04 119 | 120 | 121 | .. EOF 122 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. 2 | 3 | 4 | Hacking 5 | ======= 6 | 7 | This project makes extensive use of `tox`_, `pytest`_, and `GNU Make`_. 8 | 9 | 10 | Development environment 11 | ----------------------- 12 | 13 | Use following command to create a Python virtual environment with all 14 | necessary dependencies:: 15 | 16 | tox --recreate -e develop 17 | 18 | This creates a Python virtual environment in the ``.tox/develop`` directory. It 19 | can be activated with the following command:: 20 | 21 | . .tox/develop/bin/activate 22 | 23 | 24 | Run test suite 25 | -------------- 26 | 27 | In a Python virtual environment run the following command:: 28 | 29 | make review 30 | 31 | Outside of a Python virtual environment run the following command:: 32 | 33 | tox --recreate 34 | 35 | 36 | Build and package 37 | ----------------- 38 | 39 | In a Python virtual environment run the following command:: 40 | 41 | make package 42 | 43 | Outside of a Python virtual environment run the following command:: 44 | 45 | tox --recreate -e package 46 | 47 | 48 | Test circular dependencies 49 | -------------------------- 50 | 51 | These projects can be used to test for circular dependencies: 52 | 53 | * https://pypi.org/project/CircularDependencyA/ 54 | * https://pypi.org/project/CircularDependencyB/ 55 | * https://pypi.org/project/ineedyou/ 56 | * https://pypi.org/project/youneedme/ 57 | 58 | 59 | .. Links 60 | 61 | .. _`GNU Make`: https://www.gnu.org/software/make/ 62 | .. _`pytest`: https://pytest.org/ 63 | .. _`tox`: https://tox.readthedocs.io/ 64 | 65 | 66 | .. EOF 67 | -------------------------------------------------------------------------------- /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 | include src/deptree/py.typed 8 | 9 | 10 | # EOF 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | source_dir := ./src 5 | tests_dir := ./test 6 | 7 | 8 | .DEFAULT_GOAL := editable 9 | 10 | 11 | .PHONY: editable 12 | editable: 13 | python -m pip install --editable '.[dev-package,dev-test]' 14 | 15 | 16 | .PHONY: package 17 | package: sdist wheel zapp 18 | 19 | 20 | .PHONY: sdist 21 | sdist: 22 | python -m build --sdist 23 | python -m twine check dist/*.tar.gz 24 | 25 | 26 | .PHONY: wheel 27 | wheel: 28 | python -m build --wheel 29 | python -m twine check dist/*.whl 30 | 31 | 32 | .PHONY: zapp 33 | zapp: 34 | python setup.py bdist_zapp 35 | 36 | 37 | .PHONY: format 38 | format: 39 | python -m yapf --in-place --parallel --recursive setup.py $(source_dir) $(tests_dir) 40 | 41 | 42 | .PHONY: lint 43 | lint: 44 | python -m pytest --pycodestyle --pydocstyle --pylint --yapf -m 'pycodestyle or pydocstyle or pylint or yapf' 45 | 46 | 47 | .PHONY: mypy 48 | mypy: 49 | python -m pytest --mypy -m mypy 50 | 51 | 52 | .PHONY: pycodestyle 53 | pycodestyle: 54 | python -m pytest --pycodestyle -m pycodestyle 55 | 56 | 57 | .PHONY: pydocstyle 58 | pydocstyle: 59 | python -m pytest --pydocstyle -m pydocstyle 60 | 61 | 62 | .PHONY: pylint 63 | pylint: 64 | python -m pytest --pylint -m pylint 65 | 66 | 67 | .PHONY: yapf 68 | yapf: 69 | python -m pytest --yapf -m yapf 70 | 71 | 72 | .PHONY: test 73 | test: pytest 74 | 75 | 76 | .PHONY: pytest 77 | pytest: 78 | python -m pytest 79 | 80 | 81 | .PHONY: review 82 | review: 83 | python -m pytest --mypy --pycodestyle --pydocstyle --pylint --yapf 84 | 85 | 86 | .PHONY: clean 87 | clean: 88 | $(RM) --recursive ./.eggs/ 89 | $(RM) --recursive ./.pytest_cache/ 90 | $(RM) --recursive ./build/ 91 | $(RM) --recursive ./dist/ 92 | $(RM) --recursive ./.mypy_cache/ 93 | $(RM) --recursive ./__pycache__/ 94 | find $(source_dir) -name '*.dist-info' -type d -exec $(RM) --recursive {} + 95 | find $(source_dir) -name '*.egg-info' -type d -exec $(RM) --recursive {} + 96 | find $(source_dir) -name '*.pyc' -type f -exec $(RM) {} + 97 | find $(tests_dir) -name '*.pyc' -type f -exec $(RM) {} + 98 | find $(source_dir) -name '__pycache__' -type d -exec $(RM) --recursive {} + 99 | find $(tests_dir) -name '__pycache__' -type d -exec $(RM) --recursive {} + 100 | 101 | 102 | # 103 | # Options 104 | # 105 | 106 | # Disable default rules and suffixes (improve speed and avoid unexpected behaviour) 107 | MAKEFLAGS := --no-builtin-rules --warn-undefined-variables 108 | .SUFFIXES: 109 | 110 | 111 | # EOF 112 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. 2 | 3 | 4 | Introduction 5 | ============ 6 | 7 | Display installed Python projects as a tree of dependencies. 8 | 9 | 10 | Features 11 | -------- 12 | 13 | * Output compatible with ``requirements.txt`` 14 | 15 | * Show dependencies or dependents 16 | 17 | * Detect circular dependencies 18 | 19 | * Detect missing dependencies 20 | 21 | 22 | Repositories 23 | ------------ 24 | 25 | Distributions: 26 | 27 | * https://pypi.org/project/deptree/ 28 | 29 | 30 | Source code: 31 | 32 | * https://gitlab.com/sinoroc/deptree 33 | * https://github.com/sinoroc/deptree 34 | 35 | 36 | Usage 37 | ===== 38 | 39 | .. code:: 40 | 41 | $ deptree --help 42 | usage: deptree [-h] [--version] [-r] [-f] [project [project ...]] 43 | 44 | Display installed Python projects as a tree of dependencies 45 | 46 | positional arguments: 47 | project name of project whose dependencies (or dependents) to show 48 | 49 | optional arguments: 50 | -h, --help show this help message and exit 51 | --version show program's version number and exit 52 | -r, --reverse show dependent projects instead of dependencies 53 | -f, --flat show flat list instead of tree 54 | 55 | 56 | Examples 57 | -------- 58 | 59 | .. code:: 60 | 61 | $ deptree cryptography 62 | cryptography==2.9 # cryptography 63 | cffi==1.14.0 # cffi!=1.11.3,>=1.8 64 | pycparser==2.20 # pycparser 65 | six==1.14.0 # six>=1.4.1 66 | 67 | 68 | .. code:: 69 | 70 | $ deptree --reverse cryptography 71 | cryptography==2.9 # - 72 | SecretStorage==3.1.2 # cryptography 73 | keyring==21.2.0 # SecretStorage>=3; sys_platform == "linux" 74 | twine==3.1.1 # keyring>=15.1 75 | 76 | 77 | .. code:: 78 | 79 | $ deptree --flat cryptography 80 | cffi==1.14.0 81 | # pycparser 82 | 83 | cryptography==2.9 84 | # six>=1.4.1 85 | # cffi!=1.11.3,>=1.8 86 | 87 | pycparser==2.20 88 | 89 | six==1.14.0 90 | 91 | 92 | .. code:: 93 | 94 | $ deptree --flat --reverse cryptography 95 | # SecretStorage: cryptography 96 | cryptography==2.9 97 | 98 | # twine: keyring>=15.1 99 | keyring==21.2.0 100 | 101 | # keyring: SecretStorage>=3; sys_platform == "linux" 102 | SecretStorage==3.1.2 103 | 104 | twine==3.1.1 105 | 106 | 107 | .. code:: 108 | 109 | $ deptree CircularDependencyA 110 | CircularDependencyA==0.0.0 # CircularDependencyA 111 | CircularDependencyB==0.0.0 # CircularDependencyB 112 | CircularDependencyA # !!! CIRCULAR CircularDependencyA 113 | 114 | 115 | Installation 116 | ------------ 117 | 118 | For better comfort, use as a single-file isolated *zipapp*: 119 | 120 | * https://www.python.org/dev/peps/pep-0441/ 121 | * https://docs.python.org/3/library/zipapp.html 122 | 123 | 124 | For example: 125 | 126 | .. code:: 127 | 128 | $ python -m pip install --target ./deptree/ deptree 129 | $ python -m zipapp --python '/usr/bin/env python' --main 'deptree.cli:main' ./deptree/ 130 | $ mv ./deptree.pyz ~/.local/bin/deptree 131 | 132 | 133 | Or use `zapp`_, or `toolmaker`_. 134 | 135 | This way the tool can be used in virtual environments without installing it in 136 | the virtual environments. The tool can then see the projects installed in the 137 | virtual environment but without seeing itself. 138 | 139 | 140 | Details 141 | ======= 142 | 143 | Similar projects 144 | ---------------- 145 | 146 | * `johnnydep`_ 147 | * `pipdeptree`_ 148 | * `pipgrip`_ 149 | 150 | 151 | .. Links 152 | 153 | .. _`johnnydep`: https://pypi.org/project/johnnydep/ 154 | .. _`pipdeptree`: https://pypi.org/project/pipdeptree/ 155 | .. _`pipgrip`: https://pypi.org/project/pipgrip/ 156 | .. _`toolmaker`: https://pypi.org/project/toolmaker/ 157 | .. _`zapp`: https://pypi.org/project/zapp/ 158 | 159 | 160 | .. EOF 161 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [mypy] 5 | check_untyped_defs = True 6 | disallow_any_decorated = True 7 | disallow_any_explicit = True 8 | disallow_any_expr = True 9 | disallow_any_generics = True 10 | disallow_any_unimported = True 11 | disallow_incomplete_defs = True 12 | disallow_subclassing_any = True 13 | disallow_untyped_calls = True 14 | disallow_untyped_decorators = True 15 | disallow_untyped_defs = True 16 | files = *.py, src/**/*.py, test/**/*.py 17 | no_implicit_optional = True 18 | show_error_codes = True 19 | strict = True 20 | warn_no_return = True 21 | warn_redundant_casts = True 22 | warn_return_any = True 23 | warn_unreachable = True 24 | warn_unused_configs = True 25 | warn_unused_ignores = True 26 | 27 | 28 | # EOF 29 | -------------------------------------------------------------------------------- /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.code_style, 11 | pylint.extensions.comparetozero, 12 | pylint.extensions.confusing_elif, 13 | pylint.extensions.consider_refactoring_into_while_condition, 14 | pylint.extensions.consider_ternary_expression, 15 | pylint.extensions.dict_init_mutate, 16 | pylint.extensions.docparams, 17 | pylint.extensions.docstyle, 18 | pylint.extensions.dunder, 19 | # pylint.extensions.empty_comment, 20 | pylint.extensions.emptystring, 21 | pylint.extensions.eq_without_hash, 22 | pylint.extensions.for_any_all, 23 | pylint.extensions.magic_value, 24 | pylint.extensions.mccabe, 25 | pylint.extensions.no_self_use, 26 | pylint.extensions.overlapping_exceptions, 27 | pylint.extensions.private_import, 28 | pylint.extensions.redefined_variable_type, 29 | pylint.extensions.redefined_loop_name, 30 | pylint.extensions.set_membership, 31 | pylint.extensions.typing, 32 | pylint.extensions.while_used, 33 | 34 | 35 | [MESSAGES CONTROL] 36 | 37 | enable = 38 | use-symbolic-message-instead, 39 | useless-suppression, 40 | 41 | 42 | # EOF 43 | -------------------------------------------------------------------------------- /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 | [project] 12 | name = 'deptree' 13 | # 14 | authors = [ 15 | { name = 'sinoroc', email = 'sinoroc.code+python@gmail.com' }, 16 | ] 17 | description = 'Display installed Python projects as a tree of dependencies' 18 | license.text = 'Apache-2.0' 19 | readme = 'README.rst' 20 | urls.GitLab = 'https://gitlab.com/sinoroc/deptree' 21 | urls.GitHub = 'https://github.com/sinoroc/deptree' 22 | urls.PyPI = 'https://pypi.org/project/deptree/' 23 | # 24 | requires-python = '>=3.7' # for `dataclasses` 25 | # 26 | dependencies = [ 27 | 'importlib-metadata', 28 | 'setuptools', # for `pkg_resources` 29 | ] 30 | optional-dependencies.dev-package = [ 31 | 'build', 32 | 'twine', 33 | 'zapp', 34 | ] 35 | optional-dependencies.dev-test = [ 36 | 'pytest', 37 | 'pytest-mypy', 38 | 'pytest-pycodestyle', 39 | 'pytest-pydocstyle', 40 | 'pytest-pylint', 41 | 'pytest-yapf3', 42 | 'types-setuptools', 43 | 'yapf', 44 | ] 45 | # 46 | scripts.deptree = 'deptree.cli:main' 47 | # 48 | dynamic = [ 49 | 'version', 50 | ] 51 | 52 | 53 | # EOF 54 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [pytest] 5 | addopts = 6 | --disable-pytest-warnings 7 | --pylint-error-types='CEFIRW' 8 | --strict-config 9 | --strict-markers 10 | --yapfdiff 11 | 12 | 13 | # EOF 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [bdist_zapp] 5 | entry_point = deptree.cli:main 6 | 7 | 8 | [metadata] 9 | license_files = 10 | LICENSE.txt 11 | # `name` is necessary for `bdist_zapp` 12 | name = deptree 13 | url = https://pypi.org/project/deptree/ 14 | 15 | 16 | [options] 17 | include_package_data = True 18 | package_dir = 19 | = src 20 | packages = find: 21 | 22 | 23 | [options.packages.find] 24 | where = src 25 | 26 | 27 | # EOF 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Setup.""" 4 | 5 | import os 6 | 7 | import setuptools 8 | 9 | 10 | def _setup() -> None: 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | with open(os.path.join(here, 'CHANGELOG.rst'), encoding='utf_8') as file_: 13 | changelog = file_.read() 14 | 15 | version = changelog.splitlines()[4] 16 | 17 | setuptools.setup( 18 | # see 'setup.cfg' 19 | version=version, 20 | ) 21 | 22 | 23 | if __name__ == '__main__': 24 | _setup() 25 | 26 | # EOF 27 | -------------------------------------------------------------------------------- /src/deptree/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """deptree app.""" 4 | 5 | from . import _meta 6 | from . import cli 7 | 8 | # EOF 9 | -------------------------------------------------------------------------------- /src/deptree/__main__.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """deptree main entry point.""" 4 | 5 | import sys 6 | 7 | from . import cli 8 | 9 | if __name__ == '__main__': 10 | sys.exit(cli.main()) 11 | 12 | # EOF 13 | -------------------------------------------------------------------------------- /src/deptree/_i18n.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Internationalization.""" 4 | 5 | 6 | def _translate(message: str) -> str: 7 | return message 8 | 9 | 10 | _ = _translate 11 | 12 | # EOF 13 | -------------------------------------------------------------------------------- /src/deptree/_meta.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Meta information.""" 4 | 5 | import importlib_metadata 6 | 7 | PROJECT_NAME = 'deptree' 8 | 9 | 10 | def _get_metadata() -> importlib_metadata.PackageMetadata: 11 | return importlib_metadata.metadata(PROJECT_NAME) 12 | 13 | 14 | def get_summary() -> str: 15 | """Get project's summary.""" 16 | metadata = _get_metadata() 17 | return metadata['Summary'] 18 | 19 | 20 | def get_version() -> str: 21 | """Get project's version string.""" 22 | metadata = _get_metadata() 23 | return metadata['Version'] 24 | 25 | 26 | # EOF 27 | -------------------------------------------------------------------------------- /src/deptree/_pkg_resources.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Implementation based on ``pkg_resources`` from ``setuptools``.""" 4 | 5 | from __future__ import annotations 6 | 7 | import copy 8 | import dataclasses 9 | import enum 10 | import typing 11 | 12 | import pkg_resources 13 | 14 | if typing.TYPE_CHECKING: 15 | import collections.abc 16 | # 17 | Extra = typing.NewType('Extra', str) 18 | Extras = typing.Tuple[Extra, ...] 19 | ProjectKey = typing.NewType('ProjectKey', str) 20 | ProjectLabel = typing.NewType('ProjectLabel', str) 21 | ProjectVersion = typing.NewType('ProjectVersion', str) 22 | # 23 | Distributions = typing.NewType( 24 | 'Distributions', 25 | typing.Dict[ProjectKey, 'Distribution'], 26 | ) 27 | Requirements = typing.NewType( 28 | 'Requirements', 29 | typing.Dict[ProjectKey, 'Requirement'], 30 | ) 31 | Selection = typing.NewType( 32 | 'Selection', 33 | typing.Dict[ProjectKey, 'Requirement'], 34 | ) 35 | 36 | INDENTATION = 2 37 | 38 | 39 | class DeptreeException(Exception): 40 | """Base exception.""" 41 | 42 | 43 | class ImpossibleCase(DeptreeException): 44 | """Impossible case.""" 45 | 46 | 47 | class UnknownDistributionInChain(ImpossibleCase): 48 | """Distribution not found although it is in chain.""" 49 | 50 | 51 | class InvalidForwardRequirement(ImpossibleCase): 52 | """Invalid forward requirement.""" 53 | 54 | 55 | class InvalidReverseRequirement(ImpossibleCase): 56 | """Invalid reverse requirement.""" 57 | 58 | 59 | @dataclasses.dataclass 60 | class Requirement: 61 | """Dependency requirement.""" 62 | 63 | dependent_project_key: typing.Optional[ProjectKey] 64 | dependency_project_key: typing.Optional[ProjectKey] 65 | extras: Extras 66 | str_repr: str 67 | 68 | 69 | @dataclasses.dataclass 70 | class Distribution: 71 | """Distribution of a specific project for a specific version.""" 72 | 73 | conflicts: typing.List[ProjectKey] = ( 74 | dataclasses.field(default_factory=list) 75 | ) 76 | dependencies: Requirements = typing.cast( 77 | 'Requirements', 78 | dataclasses.field(default_factory=dict), 79 | ) 80 | dependents: typing.List[ProjectKey] = ( 81 | dataclasses.field(default_factory=list) 82 | ) 83 | found: bool = False 84 | project_name: typing.Optional[ProjectLabel] = None 85 | version: typing.Optional[ProjectVersion] = None 86 | 87 | 88 | def _display_conflict( 89 | distribution: Distribution, 90 | requirement: Requirement, 91 | depth: int = 0, 92 | ) -> None: 93 | # 94 | print( 95 | f"{' ' * INDENTATION * depth}" 96 | f"{distribution.project_name}=={distribution.version}" 97 | f" # !!! CONFLICT {requirement.str_repr}" 98 | ) 99 | 100 | 101 | def _display_circular( 102 | distribution: Distribution, 103 | requirement: Requirement, 104 | depth: int = 0, 105 | ) -> None: 106 | # 107 | print( 108 | f"{' ' * INDENTATION * depth}" 109 | f"{distribution.project_name}" 110 | f" # !!! CIRCULAR {requirement.str_repr}" 111 | ) 112 | 113 | 114 | def _display_flat(distribution: Distribution) -> None: 115 | # 116 | print(f"{distribution.project_name}=={distribution.version}") 117 | 118 | 119 | def _display_flat_dependency(requirement: Requirement) -> None: 120 | # 121 | print(f"# {requirement.str_repr}") 122 | 123 | 124 | def _display_flat_dependent( 125 | distribution: Distribution, 126 | requirement: Requirement, 127 | ) -> None: 128 | # 129 | print(f"# {distribution.project_name}: {requirement.str_repr}") 130 | 131 | 132 | def _display_good( 133 | distribution: Distribution, 134 | requirement: Requirement, 135 | depth: int = 0, 136 | ) -> None: 137 | # 138 | print( 139 | f"{' ' * INDENTATION * depth}" 140 | f"{distribution.project_name}=={distribution.version}" 141 | f" # {requirement.str_repr}" 142 | ) 143 | 144 | 145 | def _display_missing( 146 | project_key: ProjectKey, 147 | requirement: Requirement, 148 | depth: int = 0, 149 | ) -> None: 150 | # 151 | print( 152 | f"{' ' * INDENTATION * depth}" 153 | f"{project_key}" 154 | f" # !!! MISSING {requirement.str_repr}" 155 | ) 156 | 157 | 158 | def _display_unknown( 159 | project_key: ProjectKey, 160 | requirement: Requirement, 161 | depth: int = 0, 162 | ) -> None: 163 | # 164 | print( 165 | f"{' ' * INDENTATION * depth}" 166 | f"{project_key}" 167 | f" # !!! UNKNOWN {requirement.str_repr}" 168 | ) 169 | 170 | 171 | def _display_forward_tree( 172 | distributions: Distributions, 173 | requirement: Requirement, 174 | chain: typing.List[ProjectKey], 175 | ) -> None: 176 | # 177 | if requirement.dependency_project_key is None: 178 | raise InvalidForwardRequirement(requirement) 179 | # 180 | depth = len(chain) 181 | project_key = requirement.dependency_project_key 182 | distribution = distributions.get(project_key, None) 183 | if project_key in chain: 184 | if distribution is None: 185 | raise UnknownDistributionInChain(requirement, project_key) 186 | _display_circular(distribution, requirement, depth) 187 | elif not distribution: 188 | _display_unknown(project_key, requirement, depth) 189 | elif distribution.found is not True: 190 | _display_missing(project_key, requirement, depth) 191 | else: 192 | if distribution.conflicts: 193 | _display_conflict(distribution, requirement, depth) 194 | else: 195 | _display_good(distribution, requirement, depth) 196 | # 197 | for dependency in distribution.dependencies.values(): 198 | _display_forward_tree( 199 | distributions, 200 | dependency, 201 | chain + [project_key], 202 | ) 203 | 204 | 205 | def _display_reverse_tree( 206 | distributions: Distributions, 207 | requirement: Requirement, 208 | chain: typing.List[ProjectKey], 209 | ) -> None: 210 | # 211 | depth = len(chain) 212 | project_key = requirement.dependent_project_key 213 | if project_key is None: 214 | raise InvalidReverseRequirement(requirement) 215 | distribution = distributions.get(project_key, None) 216 | if project_key in chain: 217 | if distribution is None: 218 | raise UnknownDistributionInChain(requirement, project_key) 219 | _display_circular(distribution, requirement, depth) 220 | elif not distribution: 221 | _display_unknown(project_key, requirement, depth) 222 | else: 223 | if distribution.found is not True: 224 | _display_missing(project_key, requirement, depth) 225 | elif distribution.conflicts: 226 | _display_conflict(distribution, requirement, depth) 227 | else: 228 | _display_good(distribution, requirement, depth) 229 | # 230 | for dependent_key in distribution.dependents: 231 | dependent_distribution = distributions[dependent_key] 232 | dependencies = dependent_distribution.dependencies 233 | dependency_requirement = dependencies[project_key] 234 | _display_reverse_tree( 235 | distributions, 236 | dependency_requirement, 237 | chain + [project_key], 238 | ) 239 | 240 | 241 | def _display_forward_flat( 242 | distributions: Distributions, 243 | requirement: Requirement, 244 | ) -> None: 245 | # 246 | project_key = requirement.dependency_project_key 247 | if project_key is None: 248 | raise InvalidForwardRequirement(requirement) 249 | distribution = distributions.get(project_key, None) 250 | if not distribution: 251 | _display_unknown(project_key, requirement) 252 | else: 253 | if distribution.found is not True: 254 | _display_missing(project_key, requirement) 255 | elif distribution.conflicts: 256 | _display_conflict(distribution, requirement) 257 | else: 258 | _display_flat(distribution) 259 | # 260 | for dependency_requirement in distribution.dependencies.values(): 261 | _display_flat_dependency(dependency_requirement) 262 | # 263 | print("") 264 | 265 | 266 | def _display_reverse_flat( 267 | distributions: Distributions, 268 | requirement: Requirement, 269 | ) -> None: 270 | # 271 | project_key = requirement.dependent_project_key 272 | if project_key is None: 273 | raise InvalidReverseRequirement(requirement) 274 | distribution = distributions.get(project_key, None) 275 | if not distribution: 276 | _display_unknown(project_key, requirement) 277 | else: 278 | for dependent_key in distribution.dependents: 279 | dependent_distribution = distributions[dependent_key] 280 | dependencies = dependent_distribution.dependencies 281 | dependency_requirement = dependencies[project_key] 282 | _display_flat_dependent( 283 | distributions[dependent_key], 284 | dependency_requirement, 285 | ) 286 | # 287 | if distribution.found is not True: 288 | _display_missing(project_key, requirement) 289 | elif distribution.conflicts: 290 | _display_conflict(distribution, requirement) 291 | else: 292 | _display_flat(distribution) 293 | # 294 | print("") 295 | 296 | 297 | def _transform_requirement( 298 | dependent_project_key: typing.Optional[ProjectKey], 299 | requirement_: typing.Optional[pkg_resources.Requirement], 300 | ) -> Requirement: 301 | # 302 | requirement_key = ( 303 | typing.cast('ProjectKey', requirement_.key) if requirement_ else None 304 | ) 305 | extras = typing.cast('Extras', requirement_.extras) if requirement_ else () 306 | requirement = Requirement( 307 | dependent_project_key=dependent_project_key, 308 | dependency_project_key=requirement_key, 309 | extras=extras, 310 | str_repr=str(requirement_) if requirement_ else '-', 311 | ) 312 | return requirement 313 | 314 | 315 | def _make_requirement( 316 | project_key: ProjectKey, 317 | is_reverse: bool, 318 | ) -> Requirement: 319 | # 320 | requirement = None 321 | if is_reverse: 322 | requirement = _transform_requirement(project_key, None) 323 | else: 324 | requirement_ = pkg_resources.Requirement.parse(project_key) 325 | requirement = _transform_requirement(None, requirement_) 326 | return requirement 327 | 328 | 329 | def _select_flat_forward( 330 | distributions: Distributions, 331 | requirement: Requirement, 332 | selection: Selection, 333 | chain: typing.List[ProjectKey], 334 | ) -> None: 335 | # 336 | project_key = requirement.dependency_project_key 337 | if project_key is None: 338 | raise InvalidForwardRequirement(requirement) 339 | distribution = distributions.get(project_key, None) 340 | # 341 | if project_key not in selection: 342 | selection[project_key] = _make_requirement(project_key, False) 343 | # 344 | if project_key not in chain: 345 | if distribution: 346 | for dependency in distribution.dependencies.values(): 347 | _select_flat_forward( 348 | distributions, 349 | dependency, 350 | selection, 351 | chain + [project_key], 352 | ) 353 | 354 | 355 | def _select_flat_reverse( 356 | distributions: Distributions, 357 | requirement: Requirement, 358 | selection: Selection, 359 | chain: typing.List[ProjectKey], 360 | ) -> None: 361 | # 362 | project_key = requirement.dependent_project_key 363 | if project_key is None: 364 | raise InvalidReverseRequirement(requirement) 365 | distribution = distributions.get(project_key, None) 366 | # 367 | if project_key not in selection: 368 | selection[project_key] = _make_requirement(project_key, True) 369 | # 370 | if project_key not in chain: 371 | if distribution: 372 | for dependent_key in distribution.dependents: 373 | dependent_distribution = distributions[dependent_key] 374 | dependencies = dependent_distribution.dependencies 375 | dependency_requirement = dependencies[project_key] 376 | _select_flat_reverse( 377 | distributions, 378 | dependency_requirement, 379 | selection, 380 | chain + [project_key], 381 | ) 382 | 383 | 384 | def _select_flat( 385 | distributions: Distributions, 386 | is_reverse: bool, 387 | preselection: Selection, 388 | selection: Selection, 389 | ) -> None: 390 | # 391 | for project_key in preselection: 392 | requirement = preselection[project_key] 393 | if is_reverse: 394 | _select_flat_reverse( 395 | distributions, 396 | requirement, 397 | selection, 398 | [], 399 | ) 400 | else: 401 | _select_flat_forward( 402 | distributions, 403 | requirement, 404 | selection, 405 | [], 406 | ) 407 | 408 | 409 | def _visit_forward( 410 | distributions: Distributions, 411 | requirement: Requirement, 412 | visited: typing.List[ProjectKey], 413 | chain: typing.List[ProjectKey], 414 | ) -> None: 415 | # 416 | distribution_key = requirement.dependency_project_key 417 | if distribution_key is None: 418 | raise InvalidForwardRequirement(requirement) 419 | if distribution_key not in visited: 420 | visited.append(distribution_key) 421 | if distribution_key not in chain: 422 | distribution = distributions.get(distribution_key, None) 423 | if distribution: 424 | for dependency_key in distribution.dependencies: 425 | dependency = distribution.dependencies[dependency_key] 426 | _visit_forward( 427 | distributions, 428 | dependency, 429 | visited, 430 | chain + [distribution_key], 431 | ) 432 | 433 | 434 | def _visit_reverse( 435 | distributions: Distributions, 436 | requirement: Requirement, 437 | visited: typing.List[ProjectKey], 438 | chain: typing.List[ProjectKey], 439 | ) -> None: 440 | # 441 | distribution_key = requirement.dependent_project_key 442 | if distribution_key is None: 443 | raise InvalidReverseRequirement(requirement) 444 | if distribution_key not in visited: 445 | visited.append(distribution_key) 446 | if distribution_key not in chain: 447 | distribution = distributions.get(distribution_key, None) 448 | if distribution: 449 | for dependent_key in distribution.dependents: 450 | dependent_distribution = distributions[dependent_key] 451 | dependencies = dependent_distribution.dependencies 452 | dependency_requirement = dependencies[distribution_key] 453 | _visit_reverse( 454 | distributions, 455 | dependency_requirement, 456 | visited, 457 | chain + [distribution_key], 458 | ) 459 | 460 | 461 | def _find_orphan_cycles( 462 | distributions: Distributions, 463 | selection: Selection, 464 | is_reverse: bool, 465 | ) -> None: 466 | # 467 | visited: typing.List[ProjectKey] = [] 468 | for distribution_key in selection: 469 | if is_reverse: 470 | _visit_reverse( 471 | distributions, 472 | selection[distribution_key], 473 | visited, 474 | [], 475 | ) 476 | else: 477 | _visit_forward( 478 | distributions, 479 | selection[distribution_key], 480 | visited, 481 | [], 482 | ) 483 | # 484 | has_maybe_more_orphans = True 485 | max_detections = 99 486 | detections_counter = 0 487 | # pylint: disable-next=while-used 488 | while has_maybe_more_orphans and detections_counter < max_detections: 489 | detections_counter += 1 490 | has_maybe_more_orphans = False 491 | for distribution_key in distributions: 492 | if distribution_key not in visited: 493 | requirement = _make_requirement(distribution_key, is_reverse) 494 | selection[distribution_key] = requirement 495 | if is_reverse: 496 | _visit_reverse(distributions, requirement, visited, []) 497 | else: 498 | _visit_forward(distributions, requirement, visited, []) 499 | has_maybe_more_orphans = True 500 | break 501 | 502 | 503 | def _select_bottom( 504 | distributions: Distributions, 505 | selection: Selection, 506 | ) -> None: 507 | # 508 | for distribution_key in distributions: 509 | if distribution_key not in selection: 510 | distribution = distributions[distribution_key] 511 | if not distribution.dependencies: 512 | selection[distribution_key] = ( 513 | _make_requirement(distribution_key, True) 514 | ) 515 | _find_orphan_cycles(distributions, selection, True) 516 | 517 | 518 | def _select_top( 519 | distributions: Distributions, 520 | selection: Selection, 521 | ) -> None: 522 | # 523 | for distribution_key in distributions: 524 | if distribution_key not in selection: 525 | distribution = distributions[distribution_key] 526 | if not distribution.dependents: 527 | selection[distribution_key] = ( 528 | _make_requirement(distribution_key, False) 529 | ) 530 | _find_orphan_cycles(distributions, selection, False) 531 | 532 | 533 | class _SelectType(enum.Enum): 534 | ALL = enum.auto() 535 | BOTTOM = enum.auto() 536 | FLAT = enum.auto() 537 | USER = enum.auto() 538 | TOP = enum.auto() 539 | 540 | 541 | def _get_select_type( 542 | has_preselection: bool, 543 | is_flat: bool, 544 | is_reverse: bool, 545 | ) -> _SelectType: 546 | # 547 | selections = { 548 | (False, False, False): _SelectType.TOP, 549 | (False, False, True): _SelectType.BOTTOM, 550 | (False, True, False): _SelectType.ALL, 551 | (False, True, True): _SelectType.ALL, 552 | (True, False, False): _SelectType.USER, 553 | (True, False, True): _SelectType.USER, 554 | (True, True, False): _SelectType.FLAT, 555 | (True, True, True): _SelectType.FLAT, 556 | } 557 | select_type = selections[(has_preselection, is_flat, is_reverse)] 558 | return select_type 559 | 560 | 561 | def _discover_distributions( # pylint: disable=too-complex 562 | preselection: Selection, 563 | is_reverse: bool, 564 | is_flat: bool, 565 | ) -> typing.Tuple[Distributions, Selection]: 566 | # 567 | distributions = typing.cast('Distributions', {}) 568 | selection = copy.deepcopy(preselection) 569 | # 570 | select_type = _get_select_type(bool(preselection), is_flat, is_reverse) 571 | # 572 | for distribution_ in list(pkg_resources.working_set): 573 | project_key = typing.cast('ProjectKey', distribution_.key) 574 | distribution = distributions.setdefault( 575 | project_key, 576 | Distribution(), 577 | ) 578 | distribution.found = True 579 | distribution.project_name = ( 580 | typing.cast('ProjectLabel', distribution_.project_name) 581 | ) 582 | distribution.version = ( 583 | typing.cast('ProjectVersion', distribution_.version) 584 | ) 585 | # 586 | extras = ( 587 | preselection[project_key].extras 588 | if project_key in preselection else () 589 | ) 590 | # 591 | if select_type == _SelectType.ALL and project_key not in selection: 592 | selection[project_key] = _make_requirement(project_key, is_reverse) 593 | # 594 | for requirement_ in distribution_.requires(extras=extras): 595 | requirement = _transform_requirement(project_key, requirement_) 596 | dependency_key = requirement.dependency_project_key 597 | if dependency_key is None: 598 | raise InvalidForwardRequirement(requirement) 599 | dependency = distributions.setdefault( 600 | dependency_key, 601 | Distribution(), 602 | ) 603 | dependency.dependents.append(project_key) 604 | distribution.dependencies[dependency_key] = requirement 605 | # 606 | try: 607 | pkg_resources.get_distribution(requirement_) 608 | except pkg_resources.VersionConflict: 609 | dependency.conflicts.append(project_key) 610 | except pkg_resources.DistributionNotFound: 611 | pass 612 | # 613 | if ( # 614 | select_type == _SelectType.ALL 615 | and dependency_key not in selection 616 | ): 617 | selection[dependency_key] = _make_requirement( 618 | dependency_key, 619 | is_reverse, 620 | ) 621 | # 622 | if select_type == _SelectType.FLAT: 623 | _select_flat(distributions, is_reverse, preselection, selection) 624 | elif select_type == _SelectType.BOTTOM: 625 | _select_bottom(distributions, selection) 626 | elif select_type == _SelectType.TOP: 627 | _select_top(distributions, selection) 628 | # 629 | return (distributions, selection) 630 | 631 | 632 | def _make_preselection( 633 | user_selection: collections.abc.Iterable[str], 634 | is_reverse: bool, 635 | ) -> Selection: 636 | # 637 | preselection = typing.cast('Selection', {}) 638 | for item in user_selection: 639 | requirement = None 640 | requirement_ = pkg_resources.Requirement.parse(item) 641 | project_key = typing.cast('ProjectKey', requirement_.key) 642 | requirement = ( 643 | _transform_requirement(project_key, None) 644 | if is_reverse else _transform_requirement(None, requirement_) 645 | ) 646 | preselection[project_key] = requirement 647 | return preselection 648 | 649 | 650 | def main( 651 | user_selection: collections.abc.Iterable[str], 652 | is_reverse: bool, 653 | is_flat: bool, 654 | ) -> int: 655 | """CLI main function.""" 656 | # 657 | preselection = _make_preselection(user_selection, is_reverse) 658 | (distributions, selection) = _discover_distributions( 659 | preselection, 660 | is_reverse, 661 | is_flat, 662 | ) 663 | # 664 | for requirement_key in sorted(selection): 665 | requirement = selection[requirement_key] 666 | if is_flat: 667 | if is_reverse: 668 | _display_reverse_flat(distributions, requirement) 669 | else: 670 | _display_forward_flat(distributions, requirement) 671 | elif is_reverse: 672 | _display_reverse_tree(distributions, requirement, []) 673 | else: 674 | _display_forward_tree(distributions, requirement, []) 675 | return 0 676 | 677 | 678 | # EOF 679 | -------------------------------------------------------------------------------- /src/deptree/cli.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | """Command line interface.""" 4 | 5 | import argparse 6 | import typing 7 | 8 | from . import _i18n 9 | from . import _meta 10 | from . import _pkg_resources 11 | 12 | _ = _i18n._ 13 | 14 | 15 | def main() -> int: 16 | """CLI main function.""" 17 | args_parser = argparse.ArgumentParser( 18 | allow_abbrev=False, 19 | description=_meta.get_summary(), 20 | ) 21 | args_parser.add_argument( 22 | '--version', 23 | action='version', 24 | version=_meta.get_version(), 25 | ) 26 | args_parser.add_argument( 27 | '-r', 28 | '--reverse', 29 | action='store_true', 30 | help=_("show dependent projects instead of dependencies"), 31 | ) 32 | args_parser.add_argument( 33 | '-f', 34 | '--flat', 35 | action='store_true', 36 | help=_("show flat list instead of tree"), 37 | ) 38 | args_parser.add_argument( 39 | 'selected_projects', 40 | help=_("name of project whose dependencies (or dependents) to show"), 41 | metavar='project', 42 | nargs='*', 43 | ) 44 | args = args_parser.parse_args() 45 | # 46 | result = _pkg_resources.main( 47 | typing.cast(typing.List[str], args.selected_projects), 48 | typing.cast(bool, args.reverse), 49 | typing.cast(bool, args.flat), 50 | ) 51 | # 52 | return result 53 | 54 | 55 | # EOF 56 | -------------------------------------------------------------------------------- /src/deptree/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinoroc/deptree/654c855e185fd2bdc7feddda7860ab1d14d86576/src/deptree/py.typed -------------------------------------------------------------------------------- /test/test_unit.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | 3 | import unittest 4 | 5 | import deptree 6 | 7 | 8 | class TestSelectType(unittest.TestCase): 9 | """Selection type.""" 10 | 11 | def setUp(self) -> None: 12 | """Set up.""" 13 | self.get_select_type = ( 14 | # pylint: disable=protected-access 15 | deptree._pkg_resources._get_select_type 16 | ) 17 | self.select_type = ( 18 | # pylint: disable=protected-access 19 | deptree._pkg_resources._SelectType 20 | ) 21 | 22 | def test_select_type_all(self) -> None: 23 | """Selection type should be ``ALL``.""" 24 | self.assertEqual( 25 | self.get_select_type( 26 | has_preselection=False, 27 | is_flat=True, 28 | is_reverse=False, 29 | ), 30 | self.select_type.ALL, 31 | ) 32 | self.assertEqual( 33 | self.get_select_type( 34 | has_preselection=False, 35 | is_flat=True, 36 | is_reverse=True, 37 | ), 38 | self.select_type.ALL, 39 | ) 40 | 41 | def test_select_type_bottom(self) -> None: 42 | """Selection type should be ``BOTTOM``.""" 43 | self.assertEqual( 44 | self.get_select_type( 45 | has_preselection=False, 46 | is_flat=False, 47 | is_reverse=True, 48 | ), 49 | self.select_type.BOTTOM, 50 | ) 51 | 52 | def test_select_type_flatten(self) -> None: 53 | """Selection type should be ``FLAT``.""" 54 | self.assertEqual( 55 | self.get_select_type( 56 | has_preselection=True, 57 | is_flat=True, 58 | is_reverse=False, 59 | ), 60 | self.select_type.FLAT, 61 | ) 62 | self.assertEqual( 63 | self.get_select_type( 64 | has_preselection=True, 65 | is_flat=True, 66 | is_reverse=True, 67 | ), 68 | self.select_type.FLAT, 69 | ) 70 | 71 | def test_select_type_user(self) -> None: 72 | """Selection type should be ``USER``.""" 73 | self.assertEqual( 74 | self.get_select_type( 75 | has_preselection=True, 76 | is_flat=False, 77 | is_reverse=False, 78 | ), 79 | self.select_type.USER, 80 | ) 81 | self.assertEqual( 82 | self.get_select_type( 83 | has_preselection=True, 84 | is_flat=False, 85 | is_reverse=True, 86 | ), 87 | self.select_type.USER, 88 | ) 89 | 90 | def test_select_type_top(self) -> None: 91 | """Selection type should be ``TOP``.""" 92 | self.assertEqual( 93 | self.get_select_type( 94 | has_preselection=False, 95 | is_flat=False, 96 | is_reverse=False, 97 | ), 98 | self.select_type.TOP, 99 | ) 100 | 101 | 102 | # EOF 103 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | [tox] 5 | envlist = 6 | py3.7 7 | py3.8 8 | py3.9 9 | py3.10 10 | py3.11 11 | isolated_build = true 12 | min_version = 4.2.7 # https://github.com/tox-dev/tox/pull/2849 13 | skip_missing_interpreters = false 14 | 15 | 16 | [testenv] 17 | commands = 18 | py3.7: make review # More complete code review (including linting) 19 | !py3.7: make test 20 | extras = 21 | dev-test 22 | allowlist_externals = 23 | make 24 | 25 | 26 | [testenv:package] 27 | commands = 28 | make package 29 | extras = 30 | dev-package 31 | 32 | 33 | [testenv:develop] 34 | basepython = python3.7 35 | description = Activate this environment for interactive development 36 | # 37 | commands = 38 | extras = 39 | dev-package 40 | dev-test 41 | usedevelop = true 42 | 43 | 44 | # EOF 45 | --------------------------------------------------------------------------------