New Upstream Release - python-pipdeptree
Ready changes
Summary
Merged new upstream version: 2.9.0 (was: 2.2.0).
Diff
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..44f0ccc
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+tidelift: "pypi/pipdeptree"
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000..79d091a
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 2.3.1 + | :white_check_mark: |
+| < 2.3.1 | :x: |
+
+## Reporting a Vulnerability
+
+To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift
+will coordinate the fix and disclosure.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..1230149
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 5476070..dd1da3b 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -1,54 +1,102 @@
name: check
on:
push:
+ tags-ignore: ["**"]
pull_request:
schedule:
- - cron: "0 8 * * *"
+ - cron: "0 8 * * *"
+
+concurrency:
+ group: check-${{ github.ref }}
+ cancel-in-progress: true
jobs:
test:
- name: Test on ${{ matrix.py }} under ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
+ name: test ${{ matrix.py }}
+ runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
- os:
- - Ubuntu-latest
py:
- - 3.9
- - 3.8
- - 3.7
- - 3.6
- - 3.5
- - 2.7
- - pypy3
- - pypy2
+ - "3.12.0-beta.1"
+ - "3.11"
+ - "3.10"
+ - "3.9"
+ - "3.8"
+ - "3.7"
+ steps:
+ - name: Install OS dependencies
+ run: |
+ for i in 1 2 3; do
+ echo "try $i" && sudo apt-get update -y && sudo apt-get install graphviz -y && exit 0;
+ done
+ exit 1
+ shell: bash
+ - name: setup python for tox
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: install tox
+ run: python -m pip install tox
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: setup python for test ${{ matrix.py }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.py }}
+ - name: Pick environment to run
+ run: |
+ import codecs; import os; import sys
+ env = "TOXENV=py3{}\n".format(sys.version_info[1])
+ print("Picked:\n{}for{}".format(env, sys.version))
+ with codecs.open(os.environ["GITHUB_ENV"], "a", "utf-8") as file_handler:
+ file_handler.write(env)
+ shell: python
+ - name: setup test suite
+ run: tox -vv --notest
+ - name: run test suite
+ run: tox --skip-pkg-install
+
+ check:
+ name: tox env ${{ matrix.tox_env }}
+ runs-on: ubuntu-22.04
+ strategy:
+ fail-fast: false
+ matrix:
+ tox_env:
+ - dev
+ - readme
+ steps:
+ - uses: actions/checkout@v3
+ - name: setup Python 3.11
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: install tox
+ run: python -m pip install tox
+ - name: run check for ${{ matrix.tox_env }}
+ run: python -m tox -e ${{ matrix.tox_env }}
+ env:
+ UPGRADE_ADVISORY: "yes"
+
+ publish:
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+ needs: [check, test]
+ runs-on: ubuntu-22.04
steps:
- - name: Setup graphviz
- uses: ts-graphviz/setup-graphviz@v1
- - name: Setup python for tox
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
- - name: Install tox
- run: python -m pip install tox
- - name: Setup python for test ${{ matrix.py }}
- uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.py }}
- - uses: actions/checkout@v2
- - name: Pick tox environment to run
- run: |
- import os
- import platform
- import sys
- major, minor, impl = sys.version_info[0], sys.version_info[1], platform.python_implementation()
- toxenv = ("py" if impl == "CPython" else "pypy") + ("{}{}".format(major, minor) if impl == "CPython" else ("3" if major == 3 else ""))
- env_file = open(os.environ['GITHUB_ENV'], "a")
- env_file.write("TOXENV={}\n".format(toxenv))
- env_file.close()
- shell: python
- - name: Setup test suite
- run: tox -vv --notest
- - name: Run test suite
- run: tox --skip-pkg-install
+ - name: setup python to build package
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: install build
+ run: python -m pip install build
+ - uses: actions/checkout@v3
+ - name: build package
+ run: python -m build --sdist --wheel . -o dist
+ - name: publish to PyPI
+ uses: pypa/gh-action-pypi-publish@v1.8.6
+ with:
+ skip_existing: true
+ user: __token__
+ password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..a2458d2
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,27 @@
+name: Release to PyPI
+on:
+ push:
+ tags: ["*"]
+
+jobs:
+ release:
+ runs-on: ubuntu-22.04
+ environment:
+ name: release
+ url: https://pypi.org/p/pipdeptree
+ permissions:
+ id-token: write
+ steps:
+ - name: Setup python to build package
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: Install build
+ run: python -m pip install build
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Build package
+ run: pyproject-build -s -w . -o dist
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@v1.8.6
diff --git a/.gitignore b/.gitignore
index 9bf295c..50c46c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,6 @@
*.pyc
-__pycache__
-*~
-*.egg-info/
-build/
+*.egg-info
dist/
.tox/
+/src/pipdeptree/version.py
tests/virtualenvs/equimapper/
-.coverage
-coverage.xml
-htmlcov/
-.cache/
-.pytest_cache/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..3a065dd
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,60 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: check-ast
+ - id: check-builtin-literals
+ - id: check-docstring-first
+ - id: check-merge-conflict
+ - id: check-yaml
+ - id: check-toml
+ - id: debug-statements
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.3.1
+ hooks:
+ - id: pyupgrade
+ args: ["--py37-plus"]
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+ - repo: https://github.com/psf/black
+ rev: 23.3.0
+ hooks:
+ - id: black
+ args: [--safe]
+ - repo: https://github.com/asottile/blacken-docs
+ rev: 1.13.0
+ hooks:
+ - id: blacken-docs
+ additional_dependencies: [black==23.3]
+ - repo: https://github.com/tox-dev/tox-ini-fmt
+ rev: "1.3.0"
+ hooks:
+ - id: tox-ini-fmt
+ args: ["-p", "fix"]
+ - repo: https://github.com/tox-dev/pyproject-fmt
+ rev: "0.9.2"
+ hooks:
+ - id: pyproject-fmt
+ - repo: https://github.com/PyCQA/flake8
+ rev: 6.0.0
+ hooks:
+ - id: flake8
+ additional_dependencies:
+ - flake8-bugbear==23.3.23
+ - flake8-comprehensions==3.12
+ - flake8-pytest-style==1.7.2
+ - flake8-spellcheck==0.28
+ - flake8-unused-arguments==0.0.13
+ - flake8-noqa==1.3.1
+ - pep8-naming==0.13.3
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: "v2.7.1"
+ hooks:
+ - id: prettier
+ additional_dependencies:
+ - "prettier@2.7.1"
+ - "@prettier/plugin-xml@2.2"
diff --git a/.prettierrc.toml b/.prettierrc.toml
new file mode 100644
index 0000000..ba170fe
--- /dev/null
+++ b/.prettierrc.toml
@@ -0,0 +1,2 @@
+printWidth = 120
+proseWrap = "always"
diff --git a/CHANGES.md b/CHANGES.md
index fa30b1e..1871745 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,244 +1,215 @@
-Changelog
-=========
+# Changelog
-2.2.0
------
+## 2.6.0
-* Fix pipdeptree to work with pip version 21.3. The _internal pip api
- that was being used earlier is now replaced with new functions. (PR
- #154)
+- Handle mermaid output for a reversed tree
-2.1.0
------
+## 2.5.2
-* JSON output is sorted alphabetically to make it deterministic
+- Fix Mermaid not working with reserved keyword package names.
-* Fix --freeze option due to breaking changes in pip's internal api in
- version > 21.1.1
+## 2.5.1
-* Include license file in dist package
+- Fix Mermaid flag.
-2.0.0
------
+## 2.5.0
-* Support for running in the context of a virtualenv (without
- installing pipdeptree inside the virtualenv)
+- Implements Mermaid output.
-* Avoid crash when rendering cyclic dependencies
+## 2.4.0
-* Fix graphviz (dot file) output
+- Make the output of the dot format deterministic and stable.
-* Handle a (rare) case while guessing version of a package
+## 2.3.3
-* Migrate from travisCI to Github workflows
+- Update README for tested Python versions.
-* Improve integration tests
+## 2.3.2
-2.0.0b1 (beta version)
-----------------------
+- Generalize license.
-* In this first beta release targeting `2.0.0`, the underlying code is
- heavily refactored to make different CLI options work well with each
- other. This was a serious limitation in older version `<=1.0.0`
- which made it difficult to extend the tool.
+## 2.3.1
- For more information about the plans for 2.0.0 release, please check
- `docs/v2beta-opts.org` file.
+- Use `importlib.metadata` to guess version of package before fallback to `pkg.__version__`.
- * The `--reverse`, `--packages` and `--exclude` flags now work
- with `--json-tree` and `--graph-output`
- * Dropped support for python `3.3` and added support for python
- `3.7` and `3.8`
+## 2.3.0
-* Another problem with older version was that tests setup was
- convoluted and involved loading packages pickled from one env into
- the current env (in which tests are run). Moreover there was no
- separation between unit tests and integration tests (flaky)
+- Move to a package layout
+- Add support for invocation via `-m`
+- Support Python 3.11
+- Code now formatted via isort/black and linted via flake8
+- Move readme and changelog to markdown
+- Now packaged via hatchling instead of setuptools
- * Tests have been separated into 2 suites (1) unit tests that
- totally rely on mock objects and run on every commit (
- travis.ci) and (2) end-to-end tests that need to be run
- manually.
- * The test setup for end-to-end tests has been greatly simplified
- although the "flakyness"" still remains because these tests are
- run against unpinned versions of `pip`. However this is by
- design because we want to know when `pipdeptree` fails with a
- new version of `pip`.
+## 2.2.1
-* Move continuous integration from Travis to Github Actions.
+- Fix `--user-only` and `--freeze` flags which were broken after the last release.
+- Fix for compatibility with new version of `graphviz` (\>= 0.18.1).
-1.0.0
------
+## 2.2.0
-* Use `pkg_resources` vendored with `pip`.
+- Fix pipdeptree to work with pip version 21.3. The \_internal pip api that was being used earlier is now replaced with
+ new functions. (PR #154)
-* Besides this, there's no other change in this major version release.
+## 2.1.0
+- JSON output is sorted alphabetically to make it deterministic
+- Fix \--freeze option due to breaking changes in pip\'s internal api in version \> 21.1.1
+- Include license file in dist package
-0.13.2
-------
+## 2.0.0
-* Fixed call to `FrozenRequirement.to_dist` to handle changes to the
- internal api of pip version 19.0. The api change is because
- dependency links support has been removed in pip 19.0
+- Support for running in the context of a virtualenv (without installing pipdeptree inside the virtualenv)
+- Avoid crash when rendering cyclic dependencies
+- Fix graphviz (dot file) output
+- Handle a (rare) case while guessing version of a package
+- Migrate from travisCI to Github workflows
+- Improve integration tests
- See more:
-
- - https://github.com/pypa/pip/pull/6060
- - https://github.com/pypa/pip/pull/5881/commits/46ffb13f13f69c509fd253329da49889008f8e23
-
-0.13.1
-------
-
-* Fixed import after changes in pip._internal introduced in pip
- version 18.1
+## 2.0.0b1 (beta version)
-0.13.0
-------
+- In this first beta release targeting [2.0.0]{.title-ref}, the underlying code is heavily refactored to make different
+ CLI options work well with each other. This was a serious limitation in older version [\<=1.0.0]{.title-ref} which
+ made it difficult to extend the tool.
-* Added `--exclude` option to exclude packages specified as CSV
+ For more information about the plans for 2.0.0 release, please check [docs/v2beta-opts.org]{.title-ref} file.
-* In case of multiple version specs eg. <x,>=y, fix the order to
- ensure consistent output. The sorting is naive - puts the '>' prior
- to '<', and '!'.
+ > - The [\--reverse]{.title-ref}, [\--packages]{.title-ref} and [\--exclude]{.title-ref} flags now work with
+ > [\--json-tree]{.title-ref} and [\--graph-output]{.title-ref}
+ > - Dropped support for python [3.3]{.title-ref} and added support for python [3.7]{.title-ref} and [3.8]{.title-ref}
-* [Developer affecting] Updated dependencies in test environments, thereby
- fixing the long standing issue of inconsistent test behaviour.
+- Another problem with older version was that tests setup was convoluted and involved loading packages pickled from one
+ env into the current env (in which tests are run). Moreover there was no separation between unit tests and integration
+ tests (flaky)
+ > - Tests have been separated into 2 suites (1) unit tests that totally rely on mock objects and run on every commit (
+ > travis.ci) and (2) end-to-end tests that need to be run manually.
+ > - The test setup for end-to-end tests has been greatly simplified although the \"flakyness\"\" still remains because
+ > these tests are run against unpinned versions of [pip]{.title-ref}. However this is by design because we want to
+ > know when [pipdeptree]{.title-ref} fails with a new version of [pip]{.title-ref}.
-0.12.1
-------
+- Move continuous integration from Travis to Github Actions.
-* Fix import of 'FrozenRequirement' for pip 10.0.0
+## 1.0.0
+- Use [pkg_resources]{.title-ref} vendored with [pip]{.title-ref}.
+- Besides this, there\'s no other change in this major version release.
-0.12.0
-------
+## 0.13.2
-* Changes to make pipdeptree work with pip 10.0.0. This change is
- backward compatible.
+- Fixed call to [FrozenRequirement.to_dist]{.title-ref} to handle changes to the internal api of pip version 19.0. The
+ api change is because dependency links support has been removed in pip 19.0
-0.11.0
-------
-
-* Added support for nested json output (`--json-tree` flag). Behaviour
- of `--json` stays the same.
-
-* Test environments have been updated to fix the builds.
+ See more:
-0.10.1
-------
+ - <https://github.com/pypa/pip/pull/6060>
+ - <https://github.com/pypa/pip/pull/5881/commits/46ffb13f13f69c509fd253329da49889008f8e23>
-* Fixed change of behaviour due to support for ``--json`` and
- ``--packages`` together. PR #65 was reverted for this.
+## 0.13.1
-0.10.0
-------
+- Fixed import after changes in pip.\_internal introduced in pip version 18.1
-* Dropped support for Python 2.6.
+## 0.13.0
-* ``--json`` and ``--packages`` options can now be used together.
+- Added [\--exclude]{.title-ref} option to exclude packages specified as CSV
+- In case of multiple version specs eg. \<x,\>=y, fix the order to ensure consistent output. The sorting is naive - puts
+ the \'\>\' prior to \'\<\', and \'!\'.
+- \[Developer affecting\] Updated dependencies in test environments, thereby fixing the long standing issue of
+ inconsistent test behaviour.
-* Fixed binary graphviz output on Python 3
+## 0.12.1
+- Fix import of \'FrozenRequirement\' for pip 10.0.0
-0.9.0
------
+## 0.12.0
-* Support for visualizing dependency tree of packages using Graphviz
- in various formats.
+- Changes to make pipdeptree work with pip 10.0.0. This change is backward compatible.
-* Support to consider only packages installed in the user directory.
+## 0.11.0
-* Fix the output to use a better term, "Any" instead of "None" if a
- dependency doesn't need to be of a specific version.
+- Added support for nested json output ([\--json-tree]{.title-ref} flag). Behaviour of [\--json]{.title-ref} stays the
+ same.
+- Test environments have been updated to fix the builds.
-* CLI option to print version.
+## 0.10.1
+- Fixed change of behaviour due to support for `--json` and `--packages` together. PR #65 was reverted for this.
-0.8.0
------
+## 0.10.0
-* Use pip's list of excluded default packages. This means that the
- ``pipdeptree`` package itself is no longer excluded and will appear
- in the output tree.
+- Dropped support for Python 2.6.
+- `--json` and `--packages` options can now be used together.
+- Fixed binary graphviz output on Python 3
-* Fix the bug that caused a package to appear in conflicting deps
- although it's installed version could be guessed.
+## 0.9.0
+- Support for visualizing dependency tree of packages using Graphviz in various formats.
+- Support to consider only packages installed in the user directory.
+- Fix the output to use a better term, \"Any\" instead of \"None\" if a dependency doesn\'t need to be of a specific
+ version.
+- CLI option to print version.
-0.7.0
------
+## 0.8.0
-* Fix for a bug in reverse mode.
-* Alphabetical sorting of packages in the output.
-* Fallback to guess installed version of packages "skipped" by pip.
+- Use pip\'s list of excluded default packages. This means that the `pipdeptree` package itself is no longer excluded
+ and will appear in the output tree.
+- Fix the bug that caused a package to appear in conflicting deps although it\'s installed version could be guessed.
-0.6.0
------
+## 0.7.0
-* Better checking for possibly "confusing" dependencies, hence the
- word "confusing" in the warning message is now replaced with
- "coflicting" [PR#37]
-* Fix a bug when rendering dependencies of packages [PR#38]
-* The ``--nowarn`` flag is now replaced with ``--warn`` with
- 'silence', 'suppress' and 'fail' as possible values, thus giving
- more control over what should happen when there are warnings. The
- default behaviour (ie. when the flag is not specified) remains the
- same. [PR#39]
-* Fixes for Python 3.5 support [PR#40]
+- Fix for a bug in reverse mode.
+- Alphabetical sorting of packages in the output.
+- Fallback to guess installed version of packages \"skipped\" by pip.
-0.5.0
------
+## 0.6.0
-* Add `--reverse` flag to show the dependency tree upside down.
-* Add `--packages` flag to show only select packages in output.
-* Add `--json` flag to output dependency tree as json that may be used
- by external tools.
+- Better checking for possibly \"confusing\" dependencies, hence the word \"confusing\" in the warning message is now
+ replaced with \"coflicting\" \[PR#37\]
+- Fix a bug when rendering dependencies of packages \[PR#38\]
+- The `--nowarn` flag is now replaced with `--warn` with \'silence\', \'suppress\' and \'fail\' as possible values, thus
+ giving more control over what should happen when there are warnings. The default behaviour (ie. when the flag is not
+ specified) remains the same. \[PR#39\]
+- Fixes for Python 3.5 support \[PR#40\]
+## 0.5.0
-0.4.3
------
+- Add [\--reverse]{.title-ref} flag to show the dependency tree upside down.
+- Add [\--packages]{.title-ref} flag to show only select packages in output.
+- Add [\--json]{.title-ref} flag to output dependency tree as json that may be used by external tools.
-* Add python support classifiers to setup.py
-* Include license and changelog in distribution tar ball
-* Removed bullets from output of pipdeptree if the `freeze` (-f) flag
- is set.
-* Changes related to test setup and travis-ci integration.
+## 0.4.3
+- Add python support classifiers to setup.py
+- Include license and changelog in distribution tar ball
+- Removed bullets from output of pipdeptree if the [freeze]{.title-ref} (-f) flag is set.
+- Changes related to test setup and travis-ci integration.
-0.4.2
------
+## 0.4.2
-* Fix Python 3.x incompatibility (`next()` instead of `.next()`)
-* Suppress error if a dep is in skipped packages
+- Fix Python 3.x incompatibility ([next()]{.title-ref} instead of [.next()]{.title-ref})
+- Suppress error if a dep is in skipped packages
-0.4.1
------
+## 0.4.1
-* Fix: Show warning about cyclic deps only if found
+- Fix: Show warning about cyclic deps only if found
-0.4
----
+## 0.4
-* Python 2.6 compatibility
-* Fix infinite recursion in case of cyclic dependencies
-* Show warnings about cyclic dependencies
-* Travis integration and other improvements
+- Python 2.6 compatibility
+- Fix infinite recursion in case of cyclic dependencies
+- Show warnings about cyclic dependencies
+- Travis integration and other improvements
-0.3
----
+## 0.3
-* Add `--freeze` flag
-* Warn about possible confusing dependencies
-* Some minor help text and README fixes
+- Add [\--freeze]{.title-ref} flag
+- Warn about possible confusing dependencies
+- Some minor help text and README fixes
-0.2
----
+## 0.2
-* Minor fixes
+- Minor fixes
-0.1
----
+## 0.1
First version
diff --git a/LICENSE b/LICENSE
index 852f337..d36eb27 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015 Vineet Naik (naikvin@gmail.com)
+Copyright (c) The pipdeptree developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 26b0dd0..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1 +0,0 @@
-include LICENSE CHANGES.md
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 2de3f5f..0000000
--- a/Makefile
+++ /dev/null
@@ -1,38 +0,0 @@
-.PHONY: clean test-env test test-cov test-tox-all test-e2e
-
-TOX_ENV ?= py36
-
-E2E_PYTHON_EXE ?= python3.6
-
-clean:
- find . -name '*.pyc' -exec rm -f {} +
- find . -name '*.pyo' -exec rm -f {} +
- find . -name '*~' -exec rm -f {} +
- find . -name '__pycache__' -exec rmdir {} +
-
-test-env:
- pip install -r dev-requirements.txt
-
-test:
- tox -e $(TOX_ENV)
-
-test-cov:
- tox -e $(TOX_ENV) -- -x -vv --cov=pipdeptree --cov-report=xml --cov-report=html --cov-report=term-missing
-
-# Requires all the versions of python executables to be present (See
-# tox.ini for list of python versions)
-test-tox-all:
- tox
-
-clean-e2e:
- rm -rf tests/profiles/*/.env_$(E2E_PYTHON_EXE)*
-
-test-e2e: clean-e2e
- cd tests && ./e2e-tests webapp
- cd tests && ./e2e-tests conflicting
- cd tests && ./e2e-tests cyclic
-
-test-e2e-quick:
- cd tests && ./e2e-tests webapp
- cd tests && ./e2e-tests conflicting
- cd tests && ./e2e-tests cyclic
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9d1b3a5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,302 @@
+# pipdeptree
+
+[![check](https://github.com/tox-dev/pipdeptree/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/pipdeptree/actions/workflows/check.yml)
+[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/tox-dev/pipdeptree/main.svg)](https://results.pre-commit.ci/latest/github/tox-dev/pipdeptree/main)
+
+`pipdeptree` is a command line utility for displaying the installed python packages in form of a dependency tree. It
+works for packages installed globally on a machine as well as in a virtualenv. Since `pip freeze` shows all dependencies
+as a flat list, finding out which are the top level packages and which packages do they depend on requires some effort.
+It\'s also tedious to resolve conflicting dependencies that could have been installed because older version of `pip`
+didn\'t have true dependency resolution[^1]. `pipdeptree` can help here by identifying conflicting dependencies
+installed in the environment.
+
+To some extent, `pipdeptree` is inspired by the `lein deps :tree` command of [Leiningen](http://leiningen.org/).
+
+## Installation
+
+```bash
+pip install pipdeptree
+```
+
+pipdeptree has been tested with Python versions `3.7`, `3.8`, `3.9` and `3.10`.
+
+## Running in virtualenvs
+
+`New in ver. 2.0.0`
+
+If you want to run pipdeptree in the context of a particular virtualenv, you can specify the `--python` option. Note
+that this capability has been recently added in version `2.0.0`.
+
+Alternatively, you may also install pipdeptree inside the virtualenv and then run it from there.
+
+## Usage and examples
+
+To give you a brief idea, here is the output of `pipdeptree` compared with `pip freeze`:
+
+```bash
+$ pip freeze
+Flask==0.10.1
+itsdangerous==0.24
+Jinja2==2.11.2
+-e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
+MarkupSafe==0.22
+pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
+Werkzeug==0.11.2
+```
+
+And now see what `pipdeptree` outputs,
+
+```bash
+$ pipdeptree
+Warning!!! Possibly conflicting dependencies found:
+* Jinja2==2.11.2
+ - MarkupSafe [required: >=0.23, installed: 0.22]
+------------------------------------------------------------------------
+Flask==0.10.1
+ - itsdangerous [required: >=0.21, installed: 0.24]
+ - Jinja2 [required: >=2.4, installed: 2.11.2]
+ - MarkupSafe [required: >=0.23, installed: 0.22]
+ - Werkzeug [required: >=0.7, installed: 0.11.2]
+Lookupy==0.1
+pipdeptree==2.0.0b1
+ - pip [required: >=6.0.0, installed: 20.1.1]
+setuptools==47.1.1
+wheel==0.34.2
+```
+
+## Is it possible to find out why a particular package is installed?
+
+`New in ver. 0.5.0`
+
+Yes, there\'s a `--reverse` (or simply `-r`) flag for this. To find out which packages depend on a particular
+package(s), it can be combined with `--packages` option as follows:
+
+```bash
+$ pipdeptree --reverse --packages itsdangerous,MarkupSafe
+Warning!!! Possibly conflicting dependencies found:
+* Jinja2==2.11.2
+ - MarkupSafe [required: >=0.23, installed: 0.22]
+------------------------------------------------------------------------
+itsdangerous==0.24
+ - Flask==0.10.1 [requires: itsdangerous>=0.21]
+MarkupSafe==0.22
+ - Jinja2==2.11.2 [requires: MarkupSafe>=0.23]
+ - Flask==0.10.1 [requires: Jinja2>=2.4]
+```
+
+## What\'s with the warning about conflicting dependencies?
+
+As seen in the above output, `pipdeptree` by default warns about possible conflicting dependencies. Any package that\'s
+specified as a dependency of multiple packages with different versions is considered as a conflicting dependency.
+Conflicting dependencies are possible if older version of pip\<=20.2
+([without the new resolver](https://github.com/pypa/pip/issues/988)[^2]) was ever used to install dependencies at some
+point. The warning is printed to stderr instead of stdout and it can be completely silenced by specifying the
+`-w silence` or `--warn silence` option. On the other hand, it can be made mode strict with `--warn fail`, in which case
+the command will not only print the warnings to stderr but also exit with a non-zero status code. This is useful if you
+want to fit this tool into your CI pipeline.
+
+**Note**: The `--warn` option is added in version `0.6.0`. If you are using an older version, use `--nowarn` flag to
+silence the warnings.
+
+## Warnings about circular dependencies
+
+In case any of the packages have circular dependencies (eg. package A depends on package B and package B depends on
+package A), then `pipdeptree` will print warnings about that as well.
+
+```bash
+$ pipdeptree --exclude pip,pipdeptree,setuptools,wheel
+Warning!!! Cyclic dependencies found:
+- CircularDependencyA => CircularDependencyB => CircularDependencyA
+- CircularDependencyB => CircularDependencyA => CircularDependencyB
+------------------------------------------------------------------------
+wsgiref==0.1.2
+argparse==1.2.1
+```
+
+Similar to the warnings about conflicting dependencies, these too are printed to stderr and can be controlled using the
+`--warn` option.
+
+In the above example, you can also see `--exclude` option which is the opposite of `--packages` ie. these packages will
+be excluded from the output.
+
+## Using pipdeptree to write requirements.txt file
+
+If you wish to track only top level packages in your `requirements.txt` file, it\'s possible by grep-ing[^3]. only the
+top-level lines from the output,
+
+```bash
+$ pipdeptree --warn silence | grep -E '^\w+'
+Flask==0.10.1
+gnureadline==8.0.0
+Lookupy==0.1
+pipdeptree==2.0.0b1
+setuptools==47.1.1
+wheel==0.34.2
+```
+
+There is a problem here though - The output doesn\'t mention anything about `Lookupy` being installed as an _editable_
+package (refer to the output of `pip freeze` above) and information about its source is lost. To fix this, `pipdeptree`
+must be run with a `-f` or `--freeze` flag.
+
+```bash
+$ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+'
+Flask==0.10.1
+gnureadline==8.0.0
+-e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
+pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
+setuptools==47.1.1
+wheel==0.34.2
+
+$ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+' > requirements.txt
+```
+
+The freeze flag will not prefix child dependencies with hyphens, so you could dump the entire output of `pipdeptree -f`
+to the requirements.txt file thus making it human-friendly (due to indentations) as well as pip-friendly.
+
+```bash
+$ pipdeptree -f | tee locked-requirements.txt
+Flask==0.10.1
+ itsdangerous==0.24
+ Jinja2==2.11.2
+ MarkupSafe==0.23
+ Werkzeug==0.11.2
+gnureadline==8.0.0
+-e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
+pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
+ pip==20.1.1
+setuptools==47.1.1
+wheel==0.34.2
+```
+
+On confirming that there are no conflicting dependencies, you can even treat this as a \"lock file\" where all packages,
+including the transient dependencies will be pinned to their currently installed versions. Note that the
+`locked-requirements.txt` file could end up with duplicate entries. Although `pip install` wouldn\'t complain about
+that, you can avoid duplicate lines (at the cost of losing indentation) as follows,
+
+```bash
+$ pipdeptree -f | sed 's/ //g' | sort -u > locked-requirements.txt
+```
+
+## Using pipdeptree with external tools
+
+`New in ver. 0.5.0`
+
+It\'s also possible to have `pipdeptree` output json representation of the dependency tree so that it may be used as
+input to other external tools.
+
+```bash
+$ pipdeptree --json
+```
+
+Note that `--json` will output a flat list of all packages with their immediate dependencies. This is not very useful in
+itself. To obtain nested json, use `--json-tree`
+
+`New in ver. 0.11.0`
+
+```bash
+$ pipdeptree --json-tree
+```
+
+## Visualizing the dependency graph
+
+![image](https://raw.githubusercontent.com/tox-dev/pipdeptree/main/docs/twine-pdt.png)
+
+The dependency graph can also be visualized using [GraphViz](http://www.graphviz.org/):
+
+```bash
+$ pipdeptree --graph-output dot > dependencies.dot
+$ pipdeptree --graph-output pdf > dependencies.pdf
+$ pipdeptree --graph-output png > dependencies.png
+$ pipdeptree --graph-output svg > dependencies.svg
+```
+
+Note that `graphviz` is an optional dependency ie. required only if you want to use `--graph-output`. If the version of
+`graphviz` installed in the env is older than 0.18.1, then a warning will be displayed about upgrading `graphviz`.
+Support for older versions of graphviz will be dropped soon.
+
+Since version `2.0.0b1`, `--package` and `--reverse` flags are supported for all output formats ie. text, json,
+json-tree and graph.
+
+In earlier versions, `--json`, `--json-tree` and `--graph-output` options override `--package` and `--reverse`.
+
+## Usage
+
+```bash
+usage: pipdeptree.py [-h] [-v] [-f] [--python PYTHON] [-a] [-l] [-u]
+ [-w [{silence,suppress,fail}]] [-r] [-p PACKAGES]
+ [-e PACKAGES] [-j] [--json-tree]
+ [--graph-output OUTPUT_FORMAT]
+
+Dependency tree of the installed python packages
+
+optional arguments:
+ -h, --help show this help message and exit
+ -v, --version show program's version number and exit
+ -f, --freeze Print names so as to write freeze files
+ --python PYTHON Python to use to look for packages in it (default:
+ where installed)
+ -a, --all list all deps at top level
+ -l, --local-only If in a virtualenv that has global access do not show
+ globally installed packages
+ -u, --user-only Only show installations in the user site dir
+ -w [{silence,suppress,fail}], --warn [{silence,suppress,fail}]
+ Warning control. "suppress" will show warnings but
+ return 0 whether or not they are present. "silence"
+ will not show warnings at all and always return 0.
+ "fail" will show warnings and return 1 if any are
+ present. The default is "suppress".
+ -r, --reverse Shows the dependency tree in the reverse fashion ie.
+ the sub-dependencies are listed with the list of
+ packages that need them under them.
+ -p PACKAGES, --packages PACKAGES
+ Comma separated list of select packages to show in the
+ output. Wildcards are supported, like 'somepackage.*'.
+ If set, --all will be ignored.
+ -e PACKAGES, --exclude PACKAGES
+ Comma separated list of select packages to exclude
+ from the output. Wildcards are supported, like
+ 'somepackage.*'. If set, --all will be ignored.
+ -j, --json Display dependency tree as json. This will yield "raw"
+ output that may be used by external tools. This option
+ overrides all other options.
+ --json-tree Display dependency tree as json which is nested the
+ same way as the plain text output printed by default.
+ This option overrides all other options (except
+ --json).
+ --graph-output OUTPUT_FORMAT
+ Print a dependency graph in the specified output
+ format. Available are all formats supported by
+ GraphViz, e.g.: dot, jpeg, pdf, png, svg
+```
+
+## Known issues
+
+1. `pipdeptree` relies on the internal API of `pip`. I fully understand that it\'s a bad idea but it mostly works! On
+ rare occasions, it breaks when a new version of `pip` is out with backward incompatible changes in internal API. So
+ beware if you are using this tool in environments in which `pip` version is unpinned, specially automation or CD/CI
+ pipelines.
+
+## Limitations & Alternatives
+
+`pipdeptree` merely looks at the installed packages in the current environment using pip, constructs the tree, then
+outputs it in the specified format. If you want to generate the dependency tree without installing the packages, then
+you need a dependency resolver. You might want to check alternatives such as
+[pipgrip](https://github.com/ddelange/pipgrip) or [poetry](https://github.com/python-poetry/poetry).
+
+## License
+
+MIT (See [LICENSE](./LICENSE))
+
+## Footnotes
+
+[^1]:
+ pip version 20.3 has been released in Nov 2020 with the dependency resolver
+ \<<https://blog.python.org/2020/11/pip-20-3-release-new-resolver.html>\>\_
+
+[^2]:
+ pip version 20.3 has been released in Nov 2020 with the dependency resolver
+ \<<https://blog.python.org/2020/11/pip-20-3-release-new-resolver.html>\>\_
+
+[^3]:
+ If you are on windows (powershell) you can run `pipdeptree --warn silence | Select-String -Pattern '^\w+'` instead
+ of grep
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 8ad04c2..0000000
--- a/README.rst
+++ /dev/null
@@ -1,439 +0,0 @@
-pipdeptree
-==========
-
-.. image:: https://github.com/naiquevin/pipdeptree/workflows/check/badge.svg
- :target: https://github.com/naiquevin/pipdeptree/actions
-
-
-``pipdeptree`` is a command line utility for displaying the installed
-python packages in form of a dependency tree. It works for packages
-installed globally on a machine as well as in a virtualenv. Since
-``pip freeze`` shows all dependencies as a flat list, finding out
-which are the top level packages and which packages do they depend on
-requires some effort. It's also tedious to resolve conflicting
-dependencies that could have been installed because older version of
-``pip`` didn't have true dependency resolution [1]_. ``pipdeptree``
-can help here by identifying conflicting dependencies installed in the
-environment.
-
-To some extent, ``pipdeptree`` is inspired by the ``lein deps :tree``
-command of `Leiningen <http://leiningen.org/>`_.
-
-
-Installation
-------------
-
-.. code-block:: bash
-
- $ pip install pipdeptree
-
-pipdeptree has been tested with Python versions ``2.7``, ``3.5``,
-``3.6``, ``3.7``, ``3.8``, ``3.9`` as well as ``pypy2`` and ``pypy3``.
-
-Python ``2.6`` is way past it's end of life but if you ever find
-yourself stuck on a legacy environment, version ``0.9.0`` *might*
-work.
-
-
-Running in virtualenvs
-----------------------
-
-`New in ver. 2.0.0`
-
-If you want to run pipdeptree in the context of a particular
-virtualenv, you can specify the ``--python`` option. Note that this
-capability has been recently added in version ``2.0.0``.
-
-Alternately, you may also install pipdeptree inside the virtualenv and
-then run it from there.
-
-
-Usage and examples
-------------------
-
-To give you a brief idea, here is the output of ``pipdeptree``
-compared with ``pip freeze``:
-
-.. code-block:: bash
-
- $ pip freeze
- Flask==0.10.1
- itsdangerous==0.24
- Jinja2==2.11.2
- -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
- MarkupSafe==0.22
- pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
- Werkzeug==0.11.2
-
-And now see what ``pipdeptree`` outputs,
-
-.. code-block:: bash
-
- $ pipdeptree
- Warning!!! Possibly conflicting dependencies found:
- * Jinja2==2.11.2
- - MarkupSafe [required: >=0.23, installed: 0.22]
- ------------------------------------------------------------------------
- Flask==0.10.1
- - itsdangerous [required: >=0.21, installed: 0.24]
- - Jinja2 [required: >=2.4, installed: 2.11.2]
- - MarkupSafe [required: >=0.23, installed: 0.22]
- - Werkzeug [required: >=0.7, installed: 0.11.2]
- Lookupy==0.1
- pipdeptree==2.0.0b1
- - pip [required: >=6.0.0, installed: 20.1.1]
- setuptools==47.1.1
- wheel==0.34.2
-
-
-Is it possible to find out why a particular package is installed?
------------------------------------------------------------------
-
-`New in ver. 0.5.0`
-
-Yes, there's a ``--reverse`` (or simply ``-r``) flag for this. To find
-out which packages depend on a particular package(s), it can be
-combined with ``--packages`` option as follows:
-
-.. code-block:: bash
-
- $ pipdeptree --reverse --packages itsdangerous,MarkupSafe
- Warning!!! Possibly conflicting dependencies found:
- * Jinja2==2.11.2
- - MarkupSafe [required: >=0.23, installed: 0.22]
- ------------------------------------------------------------------------
- itsdangerous==0.24
- - Flask==0.10.1 [requires: itsdangerous>=0.21]
- MarkupSafe==0.22
- - Jinja2==2.11.2 [requires: MarkupSafe>=0.23]
- - Flask==0.10.1 [requires: Jinja2>=2.4]
-
-
-What's with the warning about conflicting dependencies?
--------------------------------------------------------
-
-As seen in the above output, ``pipdeptree`` by default warns about
-possible conflicting dependencies. Any package that's specified as a
-dependency of multiple packages with different versions is considered
-as a conflicting dependency. Conflicting dependencies are possible if
-older version of pip<=20.2 (`without the new resolver
-<https://github.com/pypa/pip/issues/988>`_ [1]_) was ever used to
-install dependencies at some point. The warning is printed to stderr
-instead of stdout and it can be completely silenced by specifying the
-``-w silence`` or ``--warn silence`` option. On the other hand, it can
-be made mode strict with ``--warn fail``, in which case the command
-will not only print the warnings to stderr but also exit with a
-non-zero status code. This is useful if you want to fit this tool into
-your CI pipeline.
-
-**Note**: The ``--warn`` option is added in version ``0.6.0``. If you
-are using an older version, use ``--nowarn`` flag to silence the
-warnings.
-
-
-Warnings about circular dependencies
-------------------------------------
-
-In case any of the packages have circular dependencies (eg. package A
-depends on package B and package B depends on package A), then
-``pipdeptree`` will print warnings about that as well.
-
-.. code-block:: bash
-
- $ pipdeptree --exclude pip,pipdeptree,setuptools,wheel
- Warning!!! Cyclic dependencies found:
- - CircularDependencyA => CircularDependencyB => CircularDependencyA
- - CircularDependencyB => CircularDependencyA => CircularDependencyB
- ------------------------------------------------------------------------
- wsgiref==0.1.2
- argparse==1.2.1
-
-Similar to the warnings about conflicting dependencies, these too are
-printed to stderr and can be controlled using the ``--warn`` option.
-
-In the above example, you can also see ``--exclude`` option which is
-the opposite of ``--packages`` ie. these packages will be excluded
-from the output.
-
-
-Using pipdeptree to write requirements.txt file
------------------------------------------------
-
-If you wish to track only top level packages in your
-``requirements.txt`` file, it's possible by grep-ing [2]_. only the
-top-level lines from the output,
-
-.. code-block:: bash
-
- $ pipdeptree --warn silence | grep -E '^\w+'
- Flask==0.10.1
- gnureadline==8.0.0
- Lookupy==0.1
- pipdeptree==2.0.0b1
- setuptools==47.1.1
- wheel==0.34.2
-
-There is a problem here though - The output doesn't mention anything
-about ``Lookupy`` being installed as an *editable* package (refer to
-the output of ``pip freeze`` above) and information about its source
-is lost. To fix this, ``pipdeptree`` must be run with a ``-f`` or
-``--freeze`` flag.
-
-.. code-block:: bash
-
- $ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+'
- Flask==0.10.1
- gnureadline==8.0.0
- -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
- pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
- setuptools==47.1.1
- wheel==0.34.2
-
- $ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+' > requirements.txt
-
-The freeze flag will not prefix child dependencies with hyphens, so
-you could dump the entire output of ``pipdeptree -f`` to the
-requirements.txt file thus making it human-friendly (due to
-indentations) as well as pip-friendly.
-
-.. code-block:: bash
-
- $ pipdeptree -f | tee locked-requirements.txt
- Flask==0.10.1
- itsdangerous==0.24
- Jinja2==2.11.2
- MarkupSafe==0.23
- Werkzeug==0.11.2
- gnureadline==8.0.0
- -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
- pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
- pip==20.1.1
- setuptools==47.1.1
- wheel==0.34.2
-
-On confirming that there are no conflicting dependencies, you can even
-treat this as a "lock file" where all packages, including the
-transient dependencies will be pinned to their currently installed
-versions. Note that the ``locked-requirements.txt`` file could end up
-with duplicate entries. Although ``pip install`` wouldn't complain
-about that, you can avoid duplicate lines (at the cost of losing
-indentation) as follows,
-
-.. code-block:: bash
-
- $ pipdeptree -f | sed 's/ //g' | sort -u > locked-requirements.txt
-
-
-Using pipdeptree with external tools
-------------------------------------
-
-`New in ver. 0.5.0`
-
-It's also possible to have ``pipdeptree`` output json representation
-of the dependency tree so that it may be used as input to other
-external tools.
-
-.. code-block:: bash
-
- $ pipdeptree --json
-
-Note that ``--json`` will output a flat list of all packages with
-their immediate dependencies. This is not very useful in itself. To
-obtain nested json, use ``--json-tree``
-
-`New in ver. 0.11.0`
-
-.. code-block:: bash
-
- $ pipdeptree --json-tree
-
-
-Visualizing the dependency graph
---------------------------------
-
-.. image:: https://raw.githubusercontent.com/naiquevin/pipdeptree/master/docs/twine-pdt.png
-
-The dependency graph can also be visualized using `GraphViz
-<http://www.graphviz.org/>`_:
-
-.. code-block:: bash
-
- $ pipdeptree --graph-output dot > dependencies.dot
- $ pipdeptree --graph-output pdf > dependencies.pdf
- $ pipdeptree --graph-output png > dependencies.png
- $ pipdeptree --graph-output svg > dependencies.svg
-
-Note that ``graphviz`` is an optional dependency ie. required only if
-you want to use ``--graph-output``.
-
-Since version ``2.0.0b1``, ``--package`` and ``--reverse`` flags are
-supported for all output formats ie. text, json, json-tree and graph.
-
-In earlier versions, ``--json``, ``--json-tree`` and
-``--graph-output`` options override ``--package`` and ``--reverse``.
-
-
-Usage
------
-
-.. code-block:: bash
-
- usage: pipdeptree.py [-h] [-v] [-f] [--python PYTHON] [-a] [-l] [-u]
- [-w [{silence,suppress,fail}]] [-r] [-p PACKAGES]
- [-e PACKAGES] [-j] [--json-tree]
- [--graph-output OUTPUT_FORMAT]
-
- Dependency tree of the installed python packages
-
- optional arguments:
- -h, --help show this help message and exit
- -v, --version show program's version number and exit
- -f, --freeze Print names so as to write freeze files
- --python PYTHON Python to use to look for packages in it (default:
- where installed)
- -a, --all list all deps at top level
- -l, --local-only If in a virtualenv that has global access do not show
- globally installed packages
- -u, --user-only Only show installations in the user site dir
- -w [{silence,suppress,fail}], --warn [{silence,suppress,fail}]
- Warning control. "suppress" will show warnings but
- return 0 whether or not they are present. "silence"
- will not show warnings at all and always return 0.
- "fail" will show warnings and return 1 if any are
- present. The default is "suppress".
- -r, --reverse Shows the dependency tree in the reverse fashion ie.
- the sub-dependencies are listed with the list of
- packages that need them under them.
- -p PACKAGES, --packages PACKAGES
- Comma separated list of select packages to show in the
- output. If set, --all will be ignored.
- -e PACKAGES, --exclude PACKAGES
- Comma separated list of select packages to exclude
- from the output. If set, --all will be ignored.
- -j, --json Display dependency tree as json. This will yield "raw"
- output that may be used by external tools. This option
- overrides all other options.
- --json-tree Display dependency tree as json which is nested the
- same way as the plain text output printed by default.
- This option overrides all other options (except
- --json).
- --graph-output OUTPUT_FORMAT
- Print a dependency graph in the specified output
- format. Available are all formats supported by
- GraphViz, e.g.: dot, jpeg, pdf, png, svg
-
-Known issues
-------------
-
-1. ``pipdeptree`` relies on the internal API of ``pip``. I fully
- understand that it's a bad idea but it mostly works! On rare
- occasions, it breaks when a new version of ``pip`` is out with
- backward incompatible changes in internal API. So beware if you are
- using this tool in environments in which ``pip`` version is
- unpinned, specially automation or CD/CI pipelines.
-
-
-Limitations & Alternatives
---------------------------
-
-``pipdeptree`` merely looks at the installed packages in the current
-environment using pip, constructs the tree, then outputs it in the
-specified format. If you want to generate the dependency tree without
-installing the packages, then you need a dependency resolver. You
-might want to check alternatives such as `pipgrip
-<https://github.com/ddelange/pipgrip>`_ or `poetry
-<https://github.com/python-poetry/poetry>`_.
-
-
-Runing Tests (for contributors)
--------------------------------
-
-There are 2 test suites in this repo:
-
-1. Unit tests that use mock objects. These are configured to run on
- every push to the repo and on every PR thanks to Github Actions.
-
-2. End-to-end tests that are run against actual packages installed in
- virtualenvs
-
-Unit tests can be run against all version of python using `tox
-<http://tox.readthedocs.org/en/latest/>`_ as follows:
-
-.. code-block:: bash
-
- $ make test-tox-all
-
-This assumes that you have python versions specified in the
-``tox.ini`` file.
-
-If you don't want to install all the versions of python but want to
-run tests quickly against ``Python3.6`` only:
-
-.. code-block:: bash
-
- $ make test
-
-Unit tests are written using ``pytest`` and you can also run the tests
-with code coverage as follows,
-
-.. code-block:: bash
-
- $ make test-cov
-
-On the other hand, end-to-end tests actually create virtualenvs,
-install packages and then run tests against them. These tests are more
-reliable in the sense that they also test ``pipdeptree`` with the
-latest version of ``pip`` and ``setuptools``.
-
-The downside is that when new versions of ``pip`` or ``setuptools``
-are released, these need to be updated. At present the process is
-manual but I have plans to setup nightly builds for these for faster
-feedback.
-
-The end-to-end tests can be run as follows,
-
-.. code-block:: bash
-
- $ make test-e2e # starts with a clean virtualenvs
-
- $ # or
-
- $ make test-e2e-quick # reuses existing virtualenvs
-
-By default the e2e tests uses python executable ``python3.6``. To use
-an alternate version set the environment var ``E2E_PYTHON_EXE``.
-
-.. code-block:: bash
-
- $ E2E_PYTHON_EXE=python2.7 make test-e2e
-
-
-Release checklist
------------------
-
-#. Make sure that tests pass on Github Actions.
-#. Create a commit with following changes and push it to github
-#. Update the `__version__` in the `pipdeptree.py` file.
-
- #. Add Changelog in `CHANGES.md` file.
- #. Also update `README.md` if required.
-#. Create an annotated tag on the above commit and push the tag to
- github
-#. Upload new version to PyPI.
-
-
-License
--------
-
-MIT (See `LICENSE <./LICENSE>`_)
-
-Footnotes
----------
-
-.. [1] pip version 20.3 has been released in Nov 2020 with the
- dependency resolver
- <https://blog.python.org/2020/11/pip-20-3-release-new-resolver.html>_
-
-.. [2] If you are on windows (powershell) you can run
- ``pipdeptree --warn silence | Select-String -Pattern '^\w+'``
- instead of grep
diff --git a/debian/changelog b/debian/changelog
index 0e86bb4..fd93da2 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,10 @@
-python-pipdeptree (2.2.0-4) UNRELEASED; urgency=medium
+python-pipdeptree (2.9.0-1) UNRELEASED; urgency=medium
* Set upstream metadata fields: Repository-Browse.
* Update standards version to 4.6.1, no changes needed.
+ * New upstream release.
- -- Debian Janitor <janitor@jelmer.uk> Fri, 14 Oct 2022 01:29:00 -0000
+ -- Debian Janitor <janitor@jelmer.uk> Tue, 13 Jun 2023 00:11:44 -0000
python-pipdeptree (2.2.0-3) unstable; urgency=medium
diff --git a/docs/example-requirements.txt b/docs/example-requirements.txt
deleted file mode 100644
index 7e15b1d..0000000
--- a/docs/example-requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-Flask==0.10.1
-itsdangerous==0.24
-Jinja2
-MarkupSafe==0.22
-Werkzeug==0.11.2
-argparse
--e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy-master
-gnureadline==8.0.0
diff --git a/pipdeptree.py b/pipdeptree.py
deleted file mode 100644
index 3bf2d7c..0000000
--- a/pipdeptree.py
+++ /dev/null
@@ -1,888 +0,0 @@
-from __future__ import print_function
-import os
-import inspect
-import sys
-import subprocess
-from itertools import chain
-from collections import defaultdict, deque
-import argparse
-import json
-from importlib import import_module
-import tempfile
-
-try:
- from collections import OrderedDict
-except ImportError:
- from ordereddict import OrderedDict
-
-try:
- from collections.abc import Mapping
-except ImportError:
- from collections import Mapping
-
-from pip._vendor import pkg_resources
-# inline:
-# from graphviz import backend, Digraph
-
-
-__version__ = '2.2.0'
-
-
-flatten = chain.from_iterable
-
-
-def sorted_tree(tree):
- """Sorts the dict representation of the tree
-
- The root packages as well as the intermediate packages are sorted
- in the alphabetical order of the package names.
-
- :param dict tree: the pkg dependency tree obtained by calling
- `construct_tree` function
- :returns: sorted tree
- :rtype: collections.OrderedDict
-
- """
- return OrderedDict([(k, sorted(v)) for k, v in sorted(tree.items())])
-
-
-def guess_version(pkg_key, default='?'):
- """Guess the version of a pkg when pip doesn't provide it
-
- :param str pkg_key: key of the package
- :param str default: default version to return if unable to find
- :returns: version
- :rtype: string
-
- """
- try:
- m = import_module(pkg_key)
- except ImportError:
- return default
- else:
- v = getattr(m, '__version__', default)
- if inspect.ismodule(v):
- return getattr(v, '__version__', default)
- else:
- return v
-
-
-def frozen_req_from_dist(dist):
- # The `pip._internal.metadata` modules were introduced in 21.1.1
- # and the `pip._internal.operations.freeze.FrozenRequirement`
- # class now expects dist to be a subclass of
- # `pip._internal.metadata.BaseDistribution`, however the
- # `pip._internal.utils.misc.get_installed_distributions` continues
- # to return objects of type
- # pip._vendor.pkg_resources.DistInfoDistribution.
- #
- # This is a hacky backward compatible (with older versions of pip)
- # fix.
- try:
- from pip._internal import metadata
- except ImportError:
- pass
- else:
- dist = metadata.pkg_resources.Distribution(dist)
-
- try:
- return FrozenRequirement.from_dist(dist)
- except TypeError:
- return FrozenRequirement.from_dist(dist, [])
-
-
-class Package(object):
- """Abstract class for wrappers around objects that pip returns.
-
- This class needs to be subclassed with implementations for
- `render_as_root` and `render_as_branch` methods.
-
- """
-
- def __init__(self, obj):
- self._obj = obj
- self.project_name = obj.project_name
- self.key = obj.key
-
- def render_as_root(self, frozen):
- return NotImplementedError
-
- def render_as_branch(self, frozen):
- return NotImplementedError
-
- def render(self, parent=None, frozen=False):
- if not parent:
- return self.render_as_root(frozen)
- else:
- return self.render_as_branch(frozen)
-
- @staticmethod
- def frozen_repr(obj):
- return str(obj.as_requirement())
-
- def __getattr__(self, key):
- return getattr(self._obj, key)
-
- def __repr__(self):
- return '<{0}("{1}")>'.format(self.__class__.__name__, self.key)
-
- def __lt__(self, rhs):
- return self.key < rhs.key
-
-
-class DistPackage(Package):
- """Wrapper class for pkg_resources.Distribution instances
-
- :param obj: pkg_resources.Distribution to wrap over
- :param req: optional ReqPackage object to associate this
- DistPackage with. This is useful for displaying the
- tree in reverse
- """
-
- def __init__(self, obj, req=None):
- super(DistPackage, self).__init__(obj)
- self.version_spec = None
- self.req = req
-
- def render_as_root(self, frozen):
- if not frozen:
- return '{0}=={1}'.format(self.project_name, self.version)
- else:
- return self.__class__.frozen_repr(self._obj)
-
- def render_as_branch(self, frozen):
- assert self.req is not None
- if not frozen:
- parent_ver_spec = self.req.version_spec
- parent_str = self.req.project_name
- if parent_ver_spec:
- parent_str += parent_ver_spec
- return (
- '{0}=={1} [requires: {2}]'
- ).format(self.project_name, self.version, parent_str)
- else:
- return self.render_as_root(frozen)
-
- def as_requirement(self):
- """Return a ReqPackage representation of this DistPackage"""
- return ReqPackage(self._obj.as_requirement(), dist=self)
-
- def as_parent_of(self, req):
- """Return a DistPackage instance associated to a requirement
-
- This association is necessary for reversing the PackageDAG.
-
- If `req` is None, and the `req` attribute of the current
- instance is also None, then the same instance will be
- returned.
-
- :param ReqPackage req: the requirement to associate with
- :returns: DistPackage instance
-
- """
- if req is None and self.req is None:
- return self
- return self.__class__(self._obj, req)
-
- def as_dict(self):
- return {'key': self.key,
- 'package_name': self.project_name,
- 'installed_version': self.version}
-
-
-class ReqPackage(Package):
- """Wrapper class for Requirements instance
-
- :param obj: The `Requirements` instance to wrap over
- :param dist: optional `pkg_resources.Distribution` instance for
- this requirement
- """
-
- UNKNOWN_VERSION = '?'
-
- def __init__(self, obj, dist=None):
- super(ReqPackage, self).__init__(obj)
- self.dist = dist
-
- @property
- def version_spec(self):
- specs = sorted(self._obj.specs, reverse=True) # `reverse` makes '>' prior to '<'
- return ','.join([''.join(sp) for sp in specs]) if specs else None
-
- @property
- def installed_version(self):
- if not self.dist:
- return guess_version(self.key, self.UNKNOWN_VERSION)
- return self.dist.version
-
- @property
- def is_missing(self):
- return self.installed_version == self.UNKNOWN_VERSION
-
- def is_conflicting(self):
- """If installed version conflicts with required version"""
- # unknown installed version is also considered conflicting
- if self.installed_version == self.UNKNOWN_VERSION:
- return True
- ver_spec = (self.version_spec if self.version_spec else '')
- req_version_str = '{0}{1}'.format(self.project_name, ver_spec)
- req_obj = pkg_resources.Requirement.parse(req_version_str)
- return self.installed_version not in req_obj
-
- def render_as_root(self, frozen):
- if not frozen:
- return '{0}=={1}'.format(self.project_name, self.installed_version)
- elif self.dist:
- return self.__class__.frozen_repr(self.dist._obj)
- else:
- return self.project_name
-
- def render_as_branch(self, frozen):
- if not frozen:
- req_ver = self.version_spec if self.version_spec else 'Any'
- return (
- '{0} [required: {1}, installed: {2}]'
- ).format(self.project_name, req_ver, self.installed_version)
- else:
- return self.render_as_root(frozen)
-
- def as_dict(self):
- return {'key': self.key,
- 'package_name': self.project_name,
- 'installed_version': self.installed_version,
- 'required_version': self.version_spec}
-
-
-class PackageDAG(Mapping):
- """Representation of Package dependencies as directed acyclic graph
- using a dict (Mapping) as the underlying datastructure.
-
- The nodes and their relationships (edges) are internally
- stored using a map as follows,
-
- {a: [b, c],
- b: [d],
- c: [d, e],
- d: [e],
- e: [],
- f: [b],
- g: [e, f]}
-
- Here, node `a` has 2 children nodes `b` and `c`. Consider edge
- direction from `a` -> `b` and `a` -> `c` respectively.
-
- A node is expected to be an instance of a subclass of
- `Package`. The keys are must be of class `DistPackage` and each
- item in values must be of class `ReqPackage`. (See also
- ReversedPackageDAG where the key and value types are
- interchanged).
-
- """
-
- @classmethod
- def from_pkgs(cls, pkgs):
- pkgs = [DistPackage(p) for p in pkgs]
- idx = {p.key: p for p in pkgs}
- m = {p: [ReqPackage(r, idx.get(r.key))
- for r in p.requires()]
- for p in pkgs}
- return cls(m)
-
- def __init__(self, m):
- """Initialize the PackageDAG object
-
- :param dict m: dict of node objects (refer class docstring)
- :returns: None
- :rtype: NoneType
-
- """
- self._obj = m
- self._index = {p.key: p for p in list(self._obj)}
-
- def get_node_as_parent(self, node_key):
- """Get the node from the keys of the dict representing the DAG.
-
- This method is useful if the dict representing the DAG
- contains different kind of objects in keys and values. Use
- this method to lookup a node obj as a parent (from the keys of
- the dict) given a node key.
-
- :param node_key: identifier corresponding to key attr of node obj
- :returns: node obj (as present in the keys of the dict)
- :rtype: Object
-
- """
- try:
- return self._index[node_key]
- except KeyError:
- return None
-
- def get_children(self, node_key):
- """Get child nodes for a node by it's key
-
- :param str node_key: key of the node to get children of
- :returns: list of child nodes
- :rtype: ReqPackage[]
-
- """
- node = self.get_node_as_parent(node_key)
- return self._obj[node] if node else []
-
- def filter(self, include, exclude):
- """Filters nodes in a graph by given parameters
-
- If a node is included, then all it's children are also
- included.
-
- :param set include: set of node keys to include (or None)
- :param set exclude: set of node keys to exclude (or None)
- :returns: filtered version of the graph
- :rtype: PackageDAG
-
- """
- # If neither of the filters are specified, short circuit
- if include is None and exclude is None:
- return self
-
- # Note: In following comparisons, we use lower cased values so
- # that user may specify `key` or `project_name`. As per the
- # documentation, `key` is simply
- # `project_name.lower()`. Refer:
- # https://setuptools.readthedocs.io/en/latest/pkg_resources.html#distribution-objects
- if include:
- include = set([s.lower() for s in include])
- if exclude:
- exclude = set([s.lower() for s in exclude])
- else:
- exclude = set([])
-
- # Check for mutual exclusion of show_only and exclude sets
- # after normalizing the values to lowercase
- if include and exclude:
- assert not (include & exclude)
-
- # Traverse the graph in a depth first manner and filter the
- # nodes according to `show_only` and `exclude` sets
- stack = deque()
- m = {}
- seen = set([])
- for node in self._obj.keys():
- if node.key in exclude:
- continue
- if include is None or node.key in include:
- stack.append(node)
- while True:
- if len(stack) > 0:
- n = stack.pop()
- cldn = [c for c in self._obj[n]
- if c.key not in exclude]
- m[n] = cldn
- seen.add(n.key)
- for c in cldn:
- if c.key not in seen:
- cld_node = self.get_node_as_parent(c.key)
- if cld_node:
- stack.append(cld_node)
- else:
- # It means there's no root node
- # corresponding to the child node
- # ie. a dependency is missing
- continue
- else:
- break
-
- return self.__class__(m)
-
- def reverse(self):
- """Reverse the DAG, or turn it upside-down
-
- In other words, the directions of edges of the nodes in the
- DAG will be reversed.
-
- Note that this function purely works on the nodes in the
- graph. This implies that to perform a combination of filtering
- and reversing, the order in which `filter` and `reverse`
- methods should be applied is important. For eg. if reverse is
- called on a filtered graph, then only the filtered nodes and
- it's children will be considered when reversing. On the other
- hand, if filter is called on reversed DAG, then the definition
- of "child" nodes is as per the reversed DAG.
-
- :returns: DAG in the reversed form
- :rtype: ReversedPackageDAG
-
- """
- m = defaultdict(list)
- child_keys = set(r.key for r in flatten(self._obj.values()))
- for k, vs in self._obj.items():
- for v in vs:
- # if v is already added to the dict, then ensure that
- # we are using the same object. This check is required
- # as we're using array mutation
- try:
- node = [p for p in m.keys() if p.key == v.key][0]
- except IndexError:
- node = v
- m[node].append(k.as_parent_of(v))
- if k.key not in child_keys:
- m[k.as_requirement()] = []
- return ReversedPackageDAG(dict(m))
-
- def sort(self):
- """Return sorted tree in which the underlying _obj dict is an
- OrderedDict, sorted alphabetically by the keys
-
- :returns: Instance of same class with OrderedDict
-
- """
- return self.__class__(sorted_tree(self._obj))
-
- # Methods required by the abstract base class Mapping
- def __getitem__(self, *args):
- return self._obj.get(*args)
-
- def __iter__(self):
- return self._obj.__iter__()
-
- def __len__(self):
- return len(self._obj)
-
-
-class ReversedPackageDAG(PackageDAG):
- """Representation of Package dependencies in the reverse
- order.
-
- Similar to it's super class `PackageDAG`, the underlying
- datastructure is a dict, but here the keys are expected to be of
- type `ReqPackage` and each item in the values of type
- `DistPackage`.
-
- Typically, this object will be obtained by calling
- `PackageDAG.reverse`.
-
- """
-
- def reverse(self):
- """Reverse the already reversed DAG to get the PackageDAG again
-
- :returns: reverse of the reversed DAG
- :rtype: PackageDAG
-
- """
- m = defaultdict(list)
- child_keys = set(r.key for r in flatten(self._obj.values()))
- for k, vs in self._obj.items():
- for v in vs:
- try:
- node = [p for p in m.keys() if p.key == v.key][0]
- except IndexError:
- node = v.as_parent_of(None)
- m[node].append(k)
- if k.key not in child_keys:
- m[k.dist] = []
- return PackageDAG(dict(m))
-
-
-def render_text(tree, list_all=True, frozen=False):
- """Print tree as text on console
-
- :param dict tree: the package tree
- :param bool list_all: whether to list all the pgks at the root
- level or only those that are the
- sub-dependencies
- :param bool frozen: whether or not show the names of the pkgs in
- the output that's favourable to pip --freeze
- :returns: None
-
- """
- tree = tree.sort()
- nodes = tree.keys()
- branch_keys = set(r.key for r in flatten(tree.values()))
- use_bullets = not frozen
-
- if not list_all:
- nodes = [p for p in nodes if p.key not in branch_keys]
-
- def aux(node, parent=None, indent=0, chain=None):
- chain = chain or []
- node_str = node.render(parent, frozen)
- if parent:
- prefix = ' '*indent + ('- ' if use_bullets else '')
- node_str = prefix + node_str
- result = [node_str]
- children = [aux(c, node, indent=indent+2,
- chain=chain+[c.project_name])
- for c in tree.get_children(node.key)
- if c.project_name not in chain]
- result += list(flatten(children))
- return result
-
- lines = flatten([aux(p) for p in nodes])
- print('\n'.join(lines))
-
-
-def render_json(tree, indent):
- """Converts the tree into a flat json representation.
-
- The json repr will be a list of hashes, each hash having 2 fields:
- - package
- - dependencies: list of dependencies
-
- :param dict tree: dependency tree
- :param int indent: no. of spaces to indent json
- :returns: json representation of the tree
- :rtype: str
-
- """
- tree = tree.sort()
- return json.dumps([{'package': k.as_dict(),
- 'dependencies': [v.as_dict() for v in vs]}
- for k, vs in tree.items()],
- indent=indent)
-
-
-def render_json_tree(tree, indent):
- """Converts the tree into a nested json representation.
-
- The json repr will be a list of hashes, each hash having the following fields:
- - package_name
- - key
- - required_version
- - installed_version
- - dependencies: list of dependencies
-
- :param dict tree: dependency tree
- :param int indent: no. of spaces to indent json
- :returns: json representation of the tree
- :rtype: str
-
- """
- tree = tree.sort()
- branch_keys = set(r.key for r in flatten(tree.values()))
- nodes = [p for p in tree.keys() if p.key not in branch_keys]
-
- def aux(node, parent=None, chain=None):
- if chain is None:
- chain = [node.project_name]
-
- d = node.as_dict()
- if parent:
- d['required_version'] = node.version_spec if node.version_spec else 'Any'
- else:
- d['required_version'] = d['installed_version']
-
- d['dependencies'] = [
- aux(c, parent=node, chain=chain+[c.project_name])
- for c in tree.get_children(node.key)
- if c.project_name not in chain
- ]
-
- return d
-
- return json.dumps([aux(p) for p in nodes], indent=indent)
-
-
-def dump_graphviz(tree, output_format='dot', is_reverse=False):
- """Output dependency graph as one of the supported GraphViz output formats.
-
- :param dict tree: dependency graph
- :param string output_format: output format
- :returns: representation of tree in the specified output format
- :rtype: str or binary representation depending on the output format
-
- """
- try:
- from graphviz import backend, Digraph
- except ImportError:
- print('graphviz is not available, but necessary for the output '
- 'option. Please install it.', file=sys.stderr)
- sys.exit(1)
-
- if output_format not in backend.FORMATS:
- print('{0} is not a supported output format.'.format(output_format),
- file=sys.stderr)
- print('Supported formats are: {0}'.format(
- ', '.join(sorted(backend.FORMATS))), file=sys.stderr)
- sys.exit(1)
-
- graph = Digraph(format=output_format)
-
- if not is_reverse:
- for pkg, deps in tree.items():
- pkg_label = '{0}\\n{1}'.format(pkg.project_name, pkg.version)
- graph.node(pkg.key, label=pkg_label)
- for dep in deps:
- edge_label = dep.version_spec or 'any'
- if dep.is_missing:
- dep_label = '{0}\\n(missing)'.format(dep.project_name)
- graph.node(dep.key, label=dep_label, style='dashed')
- graph.edge(pkg.key, dep.key, style='dashed')
- else:
- graph.edge(pkg.key, dep.key, label=edge_label)
- else:
- for dep, parents in tree.items():
- dep_label = '{0}\\n{1}'.format(dep.project_name,
- dep.installed_version)
- graph.node(dep.key, label=dep_label)
- for parent in parents:
- # req reference of the dep associated with this
- # particular parent package
- req_ref = parent.req
- edge_label = req_ref.version_spec or 'any'
- graph.edge(dep.key, parent.key, label=edge_label)
-
- # Allow output of dot format, even if GraphViz isn't installed.
- if output_format == 'dot':
- return graph.source
-
- # As it's unknown if the selected output format is binary or not, try to
- # decode it as UTF8 and only print it out in binary if that's not possible.
- try:
- return graph.pipe().decode('utf-8')
- except UnicodeDecodeError:
- return graph.pipe()
-
-
-def print_graphviz(dump_output):
- """Dump the data generated by GraphViz to stdout.
-
- :param dump_output: The output from dump_graphviz
- """
- if hasattr(dump_output, 'encode'):
- print(dump_output)
- else:
- with os.fdopen(sys.stdout.fileno(), 'wb') as bytestream:
- bytestream.write(dump_output)
-
-
-def conflicting_deps(tree):
- """Returns dependencies which are not present or conflict with the
- requirements of other packages.
-
- e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed
-
- :param tree: the requirements tree (dict)
- :returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage
- :rtype: dict
-
- """
- conflicting = defaultdict(list)
- for p, rs in tree.items():
- for req in rs:
- if req.is_conflicting():
- conflicting[p].append(req)
- return conflicting
-
-
-def render_conflicts_text(conflicts):
- if conflicts:
- print('Warning!!! Possibly conflicting dependencies found:',
- file=sys.stderr)
- # Enforce alphabetical order when listing conflicts
- pkgs = sorted(conflicts.keys())
- for p in pkgs:
- pkg = p.render_as_root(False)
- print('* {}'.format(pkg), file=sys.stderr)
- for req in conflicts[p]:
- req_str = req.render_as_branch(False)
- print(' - {}'.format(req_str), file=sys.stderr)
-
-
-def cyclic_deps(tree):
- """Return cyclic dependencies as list of tuples
-
- :param PackageDAG pkgs: package tree/dag
- :returns: list of tuples representing cyclic dependencies
- :rtype: list
-
- """
- index = {p.key: set([r.key for r in rs]) for p, rs in tree.items()}
- cyclic = []
- for p, rs in tree.items():
- for r in rs:
- if p.key in index.get(r.key, []):
- p_as_dep_of_r = [x for x
- in tree.get(tree.get_node_as_parent(r.key))
- if x.key == p.key][0]
- cyclic.append((p, r, p_as_dep_of_r))
- return cyclic
-
-
-def render_cycles_text(cycles):
- if cycles:
- print('Warning!! Cyclic dependencies found:', file=sys.stderr)
- # List in alphabetical order of the dependency that's cycling
- # (2nd item in the tuple)
- cycles = sorted(cycles, key=lambda xs: xs[1].key)
- for a, b, c in cycles:
- print('* {0} => {1} => {2}'.format(a.project_name,
- b.project_name,
- c.project_name),
- file=sys.stderr)
-
-
-def get_parser():
- parser = argparse.ArgumentParser(description=(
- 'Dependency tree of the installed python packages'
- ))
- parser.add_argument('-v', '--version', action='version',
- version='{0}'.format(__version__))
- parser.add_argument('-f', '--freeze', action='store_true',
- help='Print names so as to write freeze files')
- parser.add_argument('--python', default=sys.executable,
- help='Python to use to look for packages in it (default: where'
- ' installed)')
- parser.add_argument('-a', '--all', action='store_true',
- help='list all deps at top level')
- parser.add_argument('-l', '--local-only',
- action='store_true', help=(
- 'If in a virtualenv that has global access '
- 'do not show globally installed packages'
- ))
- parser.add_argument('-u', '--user-only', action='store_true',
- help=(
- 'Only show installations in the user site dir'
- ))
- parser.add_argument('-w', '--warn', action='store', dest='warn',
- nargs='?', default='suppress',
- choices=('silence', 'suppress', 'fail'),
- help=(
- 'Warning control. "suppress" will show warnings '
- 'but return 0 whether or not they are present. '
- '"silence" will not show warnings at all and '
- 'always return 0. "fail" will show warnings and '
- 'return 1 if any are present. The default is '
- '"suppress".'
- ))
- parser.add_argument('-r', '--reverse', action='store_true',
- default=False, help=(
- 'Shows the dependency tree in the reverse fashion '
- 'ie. the sub-dependencies are listed with the '
- 'list of packages that need them under them.'
- ))
- parser.add_argument('-p', '--packages',
- help=(
- 'Comma separated list of select packages to show '
- 'in the output. If set, --all will be ignored.'
- ))
- parser.add_argument('-e', '--exclude',
- help=(
- 'Comma separated list of select packages to exclude '
- 'from the output. If set, --all will be ignored.'
- ), metavar='PACKAGES')
- parser.add_argument('-j', '--json', action='store_true', default=False,
- help=(
- 'Display dependency tree as json. This will yield '
- '"raw" output that may be used by external tools. '
- 'This option overrides all other options.'
- ))
- parser.add_argument('--json-tree', action='store_true', default=False,
- help=(
- 'Display dependency tree as json which is nested '
- 'the same way as the plain text output printed by default. '
- 'This option overrides all other options (except --json).'
- ))
- parser.add_argument('--graph-output', dest='output_format',
- help=(
- 'Print a dependency graph in the specified output '
- 'format. Available are all formats supported by '
- 'GraphViz, e.g.: dot, jpeg, pdf, png, svg'
- ))
- return parser
-
-
-def _get_args():
- parser = get_parser()
- return parser.parse_args()
-
-
-def handle_non_host_target(args):
- of_python = os.path.abspath(args.python)
- # if target is not current python re-invoke it under the actual host
- if of_python != os.path.abspath(sys.executable):
- # there's no way to guarantee that graphviz is available, so refuse
- if args.output_format:
- print("graphviz functionality is not supported when querying"
- " non-host python", file=sys.stderr)
- raise SystemExit(1)
- argv = sys.argv[1:] # remove current python executable
- for py_at, value in enumerate(argv):
- if value == "--python":
- del argv[py_at]
- del argv[py_at]
- elif value.startswith("--python"):
- del argv[py_at]
- # feed the file as argument, instead of file
- # to avoid adding the file path to sys.path, that can affect result
- file_path = inspect.getsourcefile(sys.modules[__name__])
- with open(file_path, 'rt') as file_handler:
- content = file_handler.read()
- cmd = [of_python, "-c", content]
- cmd.extend(argv)
- # invoke from an empty folder to avoid cwd altering sys.path
- cwd = tempfile.mkdtemp()
- try:
- return subprocess.call(cmd, cwd=cwd)
- finally:
- os.removedirs(cwd)
- return None
-
-
-def main():
- args = _get_args()
- result = handle_non_host_target(args)
- if result is not None:
- return result
- pkgs = list(pkg_resources.working_set)
-
- tree = PackageDAG.from_pkgs(pkgs)
-
- is_text_output = not any([args.json, args.json_tree, args.output_format])
-
- return_code = 0
-
- # Before any reversing or filtering, show warnings to console
- # about possibly conflicting or cyclic deps if found and warnings
- # are enabled (ie. only if output is to be printed to console)
- if is_text_output and args.warn != 'silence':
- conflicts = conflicting_deps(tree)
- if conflicts:
- render_conflicts_text(conflicts)
- print('-'*72, file=sys.stderr)
-
- cycles = cyclic_deps(tree)
- if cycles:
- render_cycles_text(cycles)
- print('-'*72, file=sys.stderr)
-
- if args.warn == 'fail' and (conflicts or cycles):
- return_code = 1
-
- # Reverse the tree (if applicable) before filtering, thus ensuring
- # that the filter will be applied on ReverseTree
- if args.reverse:
- tree = tree.reverse()
-
- show_only = set(args.packages.split(',')) if args.packages else None
- exclude = set(args.exclude.split(',')) if args.exclude else None
-
- if show_only is not None or exclude is not None:
- tree = tree.filter(show_only, exclude)
-
- if args.json:
- print(render_json(tree, indent=4))
- elif args.json_tree:
- print(render_json_tree(tree, indent=4))
- elif args.output_format:
- output = dump_graphviz(tree,
- output_format=args.output_format,
- is_reverse=args.reverse)
- print_graphviz(output)
- else:
- render_text(tree, args.all, args.freeze)
-
- return return_code
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..eb1f414
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,73 @@
+[build-system]
+build-backend = "hatchling.build"
+requires = [
+ "hatch-vcs>=0.3",
+ "hatchling>=1.14",
+]
+
+[project]
+name = "pipdeptree"
+description = "Command line utility to show dependency tree of packages."
+readme = "README.md"
+keywords = [
+ "application",
+ "cache",
+ "directory",
+ "log",
+ "user",
+]
+license.file = "LICENSE"
+maintainers = [
+ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" },
+ { name = "Vineet Naik", email = "naikvin@gmail.com" },
+]
+requires-python = ">=3.7"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+]
+dynamic = [
+ "version",
+]
+optional-dependencies.graphviz = [
+ "graphviz>=0.20.1",
+]
+optional-dependencies.test = [
+ "covdefaults>=2.3",
+ "diff-cover>=7.5",
+ "pip>=23.1",
+ "pytest>=7.3.1",
+ "pytest-cov>=4",
+ "pytest-mock>=3.10",
+ "virtualenv<21,>=20.21",
+]
+urls.Changelog = "https://github.com/tox-dev/pipdeptree/blob/main/CHANGES.md"
+urls.Documentation = "https://github.com/tox-dev/pipdeptree/blob/main/README.md#pipdeptree"
+urls.Homepage = "https://github.com/tox-dev/pipdeptree"
+urls.Source = "https://github.com/tox-dev/pipdeptree"
+urls.Tracker = "https://github.com/tox-dev/pipdeptree/issues"
+scripts.pipdeptree = "pipdeptree:main"
+
+[tool.hatch]
+build.hooks.vcs.version-file = "src/pipdeptree/version.py"
+version.source = "vcs"
+
+[tool.black]
+line-length = 120
+
+[tool.isort]
+profile = "black"
+known_first_party = ["pipdeptree"]
+
+[tool.coverage]
+html.show_contexts = true
+html.skip_covered = false
+paths.source = ["src", ".tox/*/lib/python*/site-packages", "*/src"]
+run.parallel = true
+run.plugins = ["covdefaults"]
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index c76dee9..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,14 +0,0 @@
-[pytest]
-norecursedirs = build docs/_build *.egg .tox *.venv profiles
-addopts =
- # --verbose
- --tb=short
- # Turn on --capture to have brief, less noisy output
- # You will only see output if the test fails
- # Use --capture no if you want to see it all or have problems debugging
- # --capture=fd
- # --capture=no
- # show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed.
- -rfEsxX
- # --junitxml=junit.xml
- # --cov=pipdeptree --cov-report=xml --cov-report=html --cov-report=term-missing
diff --git a/setup.py b/setup.py
deleted file mode 100644
index b93b754..0000000
--- a/setup.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import re
-import ast
-
-from setuptools import setup
-
-
-_version_re = re.compile(r'__version__\s+=\s+(.*)')
-
-with open('pipdeptree.py', 'rb') as f:
- version = str(ast.literal_eval(_version_re.search(
- f.read().decode('utf-8')).group(1)))
-
-
-with open('./README.rst') as f:
- long_desc = f.read()
-
-
-install_requires = ["pip >= 6.0.0"]
-
-
-setup(
- name='pipdeptree',
- version=version,
- author='Vineet Naik',
- author_email='naikvin@gmail.com',
- url='https://github.com/naiquevin/pipdeptree',
- license='MIT License',
- license_file='LICENSE',
- description='Command line utility to show dependency tree of packages',
- long_description=long_desc,
- install_requires=install_requires,
- extras_require={'graphviz': ['graphviz']},
- python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*',
- py_modules=['pipdeptree'],
- entry_points={
- 'console_scripts': [
- 'pipdeptree = pipdeptree:main'
- ]
- },
- classifiers=[
- 'Environment :: Console',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9'
- ]
-)
diff --git a/src/pipdeptree/__init__.py b/src/pipdeptree/__init__.py
new file mode 100644
index 0000000..0e85016
--- /dev/null
+++ b/src/pipdeptree/__init__.py
@@ -0,0 +1,1092 @@
+import argparse
+import fnmatch
+import inspect
+import json
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+from collections import defaultdict, deque
+from collections.abc import Mapping
+from importlib import import_module
+from itertools import chain
+from textwrap import dedent
+
+from pip._vendor import pkg_resources
+
+from .version import version as __version__
+
+try:
+ from pip._internal.operations.freeze import FrozenRequirement
+except ImportError:
+ from pip import FrozenRequirement
+
+
+def sorted_tree(tree):
+ """
+ Sorts the dict representation of the tree. The root packages as well as the intermediate packages are sorted in the
+ alphabetical order of the package names.
+
+ :param dict tree: the pkg dependency tree obtained by calling `construct_tree` function
+ :returns: sorted tree
+ :rtype: dict
+ """
+ return {k: sorted(v) for k, v in sorted(tree.items())}
+
+
+def guess_version(pkg_key, default="?"):
+ """Guess the version of a pkg when pip doesn't provide it
+
+ :param str pkg_key: key of the package
+ :param str default: default version to return if unable to find
+ :returns: version
+ :rtype: string
+ """
+ try:
+ if sys.version_info >= (3, 8): # pragma: >=3.8 cover
+ import importlib.metadata as importlib_metadata
+ else: # pragma: <3.8 cover
+ import importlib_metadata
+ return importlib_metadata.version(pkg_key)
+ except ImportError:
+ pass
+ # Avoid AssertionError with setuptools, see https://github.com/tox-dev/pipdeptree/issues/162
+ if pkg_key in {"setuptools"}:
+ return default
+ try:
+ m = import_module(pkg_key)
+ except ImportError:
+ return default
+ else:
+ v = getattr(m, "__version__", default)
+ if inspect.ismodule(v):
+ return getattr(v, "__version__", default)
+ else:
+ return v
+
+
+def frozen_req_from_dist(dist):
+ # The `pip._internal.metadata` modules were introduced in 21.1.1
+ # and the `pip._internal.operations.freeze.FrozenRequirement`
+ # class now expects dist to be a subclass of
+ # `pip._internal.metadata.BaseDistribution`, however the
+ # `pip._internal.utils.misc.get_installed_distributions` continues
+ # to return objects of type
+ # pip._vendor.pkg_resources.DistInfoDistribution.
+ #
+ # This is a hacky backward compatible (with older versions of pip)
+ # fix.
+ try:
+ from pip._internal import metadata
+ except ImportError:
+ pass
+ else:
+ dist = metadata.pkg_resources.Distribution(dist)
+
+ try:
+ return FrozenRequirement.from_dist(dist)
+ except TypeError:
+ return FrozenRequirement.from_dist(dist, [])
+
+
+class Package:
+ """
+ Abstract class for wrappers around objects that pip returns. This class needs to be subclassed with implementations
+ for `render_as_root` and `render_as_branch` methods.
+ """
+
+ def __init__(self, obj):
+ self._obj = obj
+ self.project_name = obj.project_name
+ self.key = obj.key
+
+ def render_as_root(self, frozen): # noqa: U100
+ return NotImplementedError
+
+ def render_as_branch(self, frozen): # noqa: U100
+ return NotImplementedError
+
+ def render(self, parent=None, frozen=False):
+ if not parent:
+ return self.render_as_root(frozen)
+ else:
+ return self.render_as_branch(frozen)
+
+ @staticmethod
+ def frozen_repr(obj):
+ fr = frozen_req_from_dist(obj)
+ return str(fr).strip()
+
+ def __getattr__(self, key):
+ return getattr(self._obj, key)
+
+ def __repr__(self):
+ return f'<{self.__class__.__name__}("{self.key}")>'
+
+ def __lt__(self, rhs):
+ return self.key < rhs.key
+
+
+class DistPackage(Package):
+ """
+ Wrapper class for pkg_resources.Distribution instances
+
+ :param obj: pkg_resources.Distribution to wrap over
+ :param req: optional ReqPackage object to associate this DistPackage with. This is useful for displaying the tree
+ in reverse
+ """
+
+ def __init__(self, obj, req=None):
+ super().__init__(obj)
+ self.version_spec = None
+ self.req = req
+
+ def render_as_root(self, frozen):
+ if not frozen:
+ return f"{self.project_name}=={self.version}"
+ else:
+ return self.__class__.frozen_repr(self._obj)
+
+ def render_as_branch(self, frozen):
+ assert self.req is not None
+ if not frozen:
+ parent_ver_spec = self.req.version_spec
+ parent_str = self.req.project_name
+ if parent_ver_spec:
+ parent_str += parent_ver_spec
+ return f"{self.project_name}=={self.version} [requires: {parent_str}]"
+ else:
+ return self.render_as_root(frozen)
+
+ def as_requirement(self):
+ """Return a ReqPackage representation of this DistPackage"""
+ return ReqPackage(self._obj.as_requirement(), dist=self)
+
+ def as_parent_of(self, req):
+ """
+ Return a DistPackage instance associated to a requirement. This association is necessary for reversing the
+ PackageDAG.
+
+ If `req` is None, and the `req` attribute of the current instance is also None, then the same instance will be
+ returned.
+
+ :param ReqPackage req: the requirement to associate with
+ :returns: DistPackage instance
+ """
+ if req is None and self.req is None:
+ return self
+ return self.__class__(self._obj, req)
+
+ def as_dict(self):
+ return {"key": self.key, "package_name": self.project_name, "installed_version": self.version}
+
+
+class ReqPackage(Package):
+ """
+ Wrapper class for Requirements instance
+
+ :param obj: The `Requirements` instance to wrap over
+ :param dist: optional `pkg_resources.Distribution` instance for this requirement
+ """
+
+ UNKNOWN_VERSION = "?"
+
+ def __init__(self, obj, dist=None):
+ super().__init__(obj)
+ self.dist = dist
+
+ @property
+ def version_spec(self):
+ specs = sorted(self._obj.specs, reverse=True) # `reverse` makes '>' prior to '<'
+ return ",".join(["".join(sp) for sp in specs]) if specs else None
+
+ @property
+ def installed_version(self):
+ if not self.dist:
+ return guess_version(self.key, self.UNKNOWN_VERSION)
+ return self.dist.version
+
+ @property
+ def is_missing(self):
+ return self.installed_version == self.UNKNOWN_VERSION
+
+ def is_conflicting(self):
+ """If installed version conflicts with required version"""
+ # unknown installed version is also considered conflicting
+ if self.installed_version == self.UNKNOWN_VERSION:
+ return True
+ ver_spec = self.version_spec if self.version_spec else ""
+ req_version_str = f"{self.project_name}{ver_spec}"
+ req_obj = pkg_resources.Requirement.parse(req_version_str)
+ return self.installed_version not in req_obj
+
+ def render_as_root(self, frozen):
+ if not frozen:
+ return f"{self.project_name}=={self.installed_version}"
+ elif self.dist:
+ return self.__class__.frozen_repr(self.dist._obj)
+ else:
+ return self.project_name
+
+ def render_as_branch(self, frozen):
+ if not frozen:
+ req_ver = self.version_spec if self.version_spec else "Any"
+ return f"{self.project_name} [required: {req_ver}, installed: {self.installed_version}]"
+ else:
+ return self.render_as_root(frozen)
+
+ def as_dict(self):
+ return {
+ "key": self.key,
+ "package_name": self.project_name,
+ "installed_version": self.installed_version,
+ "required_version": self.version_spec,
+ }
+
+
+class PackageDAG(Mapping):
+ """
+ Representation of Package dependencies as directed acyclic graph using a dict (Mapping) as the underlying
+ datastructure.
+
+ The nodes and their relationships (edges) are internally stored using a map as follows,
+
+ {a: [b, c],
+ b: [d],
+ c: [d, e],
+ d: [e],
+ e: [],
+ f: [b],
+ g: [e, f]}
+
+ Here, node `a` has 2 children nodes `b` and `c`. Consider edge direction from `a` -> `b` and `a` -> `c`
+ respectively.
+
+ A node is expected to be an instance of a subclass of `Package`. The keys are must be of class `DistPackage` and
+ each item in values must be of class `ReqPackage`. (See also ReversedPackageDAG where the key and value types are
+ interchanged).
+ """
+
+ @classmethod
+ def from_pkgs(cls, pkgs):
+ pkgs = [DistPackage(p) for p in pkgs]
+ idx = {p.key: p for p in pkgs}
+ m = {p: [ReqPackage(r, idx.get(r.key)) for r in p.requires()] for p in pkgs}
+ return cls(m)
+
+ def __init__(self, m):
+ """Initialize the PackageDAG object
+
+ :param dict m: dict of node objects (refer class docstring)
+ :returns: None
+ :rtype: NoneType
+
+ """
+ self._obj = m
+ self._index = {p.key: p for p in list(self._obj)}
+
+ def get_node_as_parent(self, node_key):
+ """
+ Get the node from the keys of the dict representing the DAG.
+
+ This method is useful if the dict representing the DAG contains different kind of objects in keys and values.
+ Use this method to look up a node obj as a parent (from the keys of the dict) given a node key.
+
+ :param node_key: identifier corresponding to key attr of node obj
+ :returns: node obj (as present in the keys of the dict)
+ :rtype: Object
+ """
+ try:
+ return self._index[node_key]
+ except KeyError:
+ return None
+
+ def get_children(self, node_key):
+ """
+ Get child nodes for a node by its key
+
+ :param str node_key: key of the node to get children of
+ :returns: list of child nodes
+ :rtype: ReqPackage[]
+ """
+ node = self.get_node_as_parent(node_key)
+ return self._obj[node] if node else []
+
+ def filter(self, include, exclude):
+ """
+ Filters nodes in a graph by given parameters
+
+ If a node is included, then all it's children are also included.
+
+ :param set include: set of node keys to include (or None)
+ :param set exclude: set of node keys to exclude (or None)
+ :returns: filtered version of the graph
+ :rtype: PackageDAG
+ """
+ # If neither of the filters are specified, short circuit
+ if include is None and exclude is None:
+ return self
+
+ # Note: In following comparisons, we use lower cased values so
+ # that user may specify `key` or `project_name`. As per the
+ # documentation, `key` is simply
+ # `project_name.lower()`. Refer:
+ # https://setuptools.readthedocs.io/en/latest/pkg_resources.html#distribution-objects
+ if include:
+ include = {s.lower() for s in include}
+ if exclude:
+ exclude = {s.lower() for s in exclude}
+ else:
+ exclude = set()
+
+ # Check for mutual exclusion of show_only and exclude sets
+ # after normalizing the values to lowercase
+ if include and exclude:
+ assert not (include & exclude)
+
+ # Traverse the graph in a depth first manner and filter the
+ # nodes according to `show_only` and `exclude` sets
+ stack = deque()
+ m = {}
+ seen = set()
+ for node in self._obj.keys():
+ if any(fnmatch.fnmatch(node.key, e) for e in exclude):
+ continue
+ if include is None or any(fnmatch.fnmatch(node.key, i) for i in include):
+ stack.append(node)
+ while True:
+ if len(stack) > 0:
+ n = stack.pop()
+ cldn = [c for c in self._obj[n] if not any(fnmatch.fnmatch(c.key, e) for e in exclude)]
+ m[n] = cldn
+ seen.add(n.key)
+ for c in cldn:
+ if c.key not in seen:
+ cld_node = self.get_node_as_parent(c.key)
+ if cld_node:
+ stack.append(cld_node)
+ else:
+ # It means there's no root node corresponding to the child node i.e.
+ # a dependency is missing
+ continue
+ else:
+ break
+
+ return self.__class__(m)
+
+ def reverse(self):
+ """
+ Reverse the DAG, or turn it upside-down.
+
+ In other words, the directions of edges of the nodes in the DAG will be reversed.
+
+ Note that this function purely works on the nodes in the graph. This implies that to perform a combination of
+ filtering and reversing, the order in which `filter` and `reverse` methods should be applied is important. For
+ e.g., if reverse is called on a filtered graph, then only the filtered nodes and it's children will be
+ considered when reversing. On the other hand, if filter is called on reversed DAG, then the definition of
+ "child" nodes is as per the reversed DAG.
+
+ :returns: DAG in the reversed form
+ :rtype: ReversedPackageDAG
+ """
+ m = defaultdict(list)
+ child_keys = {r.key for r in chain.from_iterable(self._obj.values())}
+ for k, vs in self._obj.items():
+ for v in vs:
+ # if v is already added to the dict, then ensure that
+ # we are using the same object. This check is required
+ # as we're using array mutation
+ try:
+ node = [p for p in m.keys() if p.key == v.key][0]
+ except IndexError:
+ node = v
+ m[node].append(k.as_parent_of(v))
+ if k.key not in child_keys:
+ m[k.as_requirement()] = []
+ return ReversedPackageDAG(dict(m))
+
+ def sort(self):
+ """
+ Return sorted tree in which the underlying _obj dict is an dict, sorted alphabetically by the keys.
+
+ :returns: Instance of same class with dict
+ """
+ return self.__class__(sorted_tree(self._obj))
+
+ # Methods required by the abstract base class Mapping
+ def __getitem__(self, *args):
+ return self._obj.get(*args)
+
+ def __iter__(self):
+ return self._obj.__iter__()
+
+ def __len__(self):
+ return len(self._obj)
+
+
+class ReversedPackageDAG(PackageDAG):
+ """Representation of Package dependencies in the reverse order.
+
+ Similar to it's super class `PackageDAG`, the underlying datastructure is a dict, but here the keys are expected to
+ be of type `ReqPackage` and each item in the values of type `DistPackage`.
+
+ Typically, this object will be obtained by calling `PackageDAG.reverse`.
+ """
+
+ def reverse(self):
+ """
+ Reverse the already reversed DAG to get the PackageDAG again
+
+ :returns: reverse of the reversed DAG
+ :rtype: PackageDAG
+ """
+ m = defaultdict(list)
+ child_keys = {r.key for r in chain.from_iterable(self._obj.values())}
+ for k, vs in self._obj.items():
+ for v in vs:
+ try:
+ node = [p for p in m.keys() if p.key == v.key][0]
+ except IndexError:
+ node = v.as_parent_of(None)
+ m[node].append(k)
+ if k.key not in child_keys:
+ m[k.dist] = []
+ return PackageDAG(dict(m))
+
+
+def render_text(tree, max_depth, list_all=True, frozen=False):
+ """Print tree as text on console
+
+ :param dict tree: the package tree
+ :param bool list_all: whether to list all the pgks at the root level or only those that are the sub-dependencies
+ :param bool frozen: show the names of the pkgs in the output that's favourable to pip --freeze
+ :returns: None
+ """
+ tree = tree.sort()
+ nodes = tree.keys()
+ branch_keys = {r.key for r in chain.from_iterable(tree.values())}
+
+ if not list_all:
+ nodes = [p for p in nodes if p.key not in branch_keys]
+
+ if sys.stdout.encoding.lower() in ("utf-8", "utf-16", "utf-32"):
+ _render_text_with_unicode(tree, nodes, max_depth, frozen)
+ else:
+ _render_text_without_unicode(tree, nodes, max_depth, frozen)
+
+
+def _render_text_with_unicode(tree, nodes, max_depth, frozen):
+ use_bullets = not frozen
+
+ def aux(
+ node,
+ parent=None,
+ indent=0,
+ cur_chain=None,
+ prefix="",
+ depth=0,
+ has_grand_parent=False,
+ is_last_child=False,
+ parent_is_last_child=False,
+ ):
+ cur_chain = cur_chain or []
+ node_str = node.render(parent, frozen)
+ next_prefix = ""
+ next_indent = indent + 2
+
+ if parent:
+ bullet = "├── "
+ if is_last_child:
+ bullet = "└── "
+
+ line_char = "│"
+ if not use_bullets:
+ line_char = ""
+ # Add 2 spaces so direct dependencies to a project are indented
+ bullet = " "
+
+ if has_grand_parent:
+ next_indent -= 1
+ if parent_is_last_child:
+ offset = 0 if len(line_char) == 1 else 1
+ prefix += " " * (indent + 1 - offset - depth)
+ else:
+ prefix += line_char + " " * (indent - depth)
+ # Without this extra space, bullets will point to the space just before the project name
+ prefix += " " if use_bullets else ""
+ next_prefix = prefix
+ node_str = prefix + bullet + node_str
+ result = [node_str]
+
+ children = tree.get_children(node.key)
+ children_strings = [
+ aux(
+ c,
+ node,
+ indent=next_indent,
+ cur_chain=cur_chain + [c.project_name],
+ prefix=next_prefix,
+ depth=depth + 1,
+ has_grand_parent=parent is not None,
+ is_last_child=c is children[-1],
+ parent_is_last_child=is_last_child,
+ )
+ for c in children
+ if c.project_name not in cur_chain and depth + 1 <= max_depth
+ ]
+
+ result += list(chain.from_iterable(children_strings))
+ return result
+
+ lines = chain.from_iterable([aux(p) for p in nodes])
+ print("\n".join(lines))
+
+
+def _render_text_without_unicode(tree, nodes, max_depth, frozen):
+ use_bullets = not frozen
+
+ def aux(node, parent=None, indent=0, cur_chain=None, depth=0):
+ cur_chain = cur_chain or []
+ node_str = node.render(parent, frozen)
+ if parent:
+ prefix = " " * indent + ("- " if use_bullets else "")
+ node_str = prefix + node_str
+ result = [node_str]
+ children = [
+ aux(c, node, indent=indent + 2, cur_chain=cur_chain + [c.project_name], depth=depth + 1)
+ for c in tree.get_children(node.key)
+ if c.project_name not in cur_chain and depth + 1 <= max_depth
+ ]
+ result += list(chain.from_iterable(children))
+ return result
+
+ lines = chain.from_iterable([aux(p) for p in nodes])
+ print("\n".join(lines))
+
+
+def render_json(tree, indent):
+ """
+ Converts the tree into a flat json representation.
+
+ The json repr will be a list of hashes, each hash having 2 fields:
+ - package
+ - dependencies: list of dependencies
+
+ :param dict tree: dependency tree
+ :param int indent: no. of spaces to indent json
+ :returns: json representation of the tree
+ :rtype: str
+ """
+ tree = tree.sort()
+ return json.dumps(
+ [{"package": k.as_dict(), "dependencies": [v.as_dict() for v in vs]} for k, vs in tree.items()], indent=indent
+ )
+
+
+def render_json_tree(tree, indent):
+ """
+ Converts the tree into a nested json representation.
+
+ The json repr will be a list of hashes, each hash having the following fields:
+
+ - package_name
+ - key
+ - required_version
+ - installed_version
+ - dependencies: list of dependencies
+
+ :param dict tree: dependency tree
+ :param int indent: no. of spaces to indent json
+ :returns: json representation of the tree
+ :rtype: str
+ """
+ tree = tree.sort()
+ branch_keys = {r.key for r in chain.from_iterable(tree.values())}
+ nodes = [p for p in tree.keys() if p.key not in branch_keys]
+
+ def aux(node, parent=None, cur_chain=None):
+ if cur_chain is None:
+ cur_chain = [node.project_name]
+
+ d = node.as_dict()
+ if parent:
+ d["required_version"] = node.version_spec if node.version_spec else "Any"
+ else:
+ d["required_version"] = d["installed_version"]
+
+ d["dependencies"] = [
+ aux(c, parent=node, cur_chain=cur_chain + [c.project_name])
+ for c in tree.get_children(node.key)
+ if c.project_name not in cur_chain
+ ]
+
+ return d
+
+ return json.dumps([aux(p) for p in nodes], indent=indent)
+
+
+def render_mermaid(tree) -> str:
+ """Produce a Mermaid flowchart from the dependency graph.
+
+ :param dict tree: dependency graph
+ """
+ # List of reserved keywords in Mermaid that cannot be used as node names.
+ # See: https://github.com/mermaid-js/mermaid/issues/4182#issuecomment-1454787806
+ reserved_ids: set[str] = {
+ "C4Component",
+ "C4Container",
+ "C4Deployment",
+ "C4Dynamic",
+ "_blank",
+ "_parent",
+ "_self",
+ "_top",
+ "call",
+ "class",
+ "classDef",
+ "click",
+ "end",
+ "flowchart",
+ "flowchart-v2",
+ "graph",
+ "interpolate",
+ "linkStyle",
+ "style",
+ "subgraph",
+ }
+ node_ids_map: dict[str:str] = {}
+
+ def mermaid_id(key: str) -> str:
+ """Returns a valid Mermaid node ID from a string."""
+ # If we have already seen this key, return the canonical ID.
+ canonical_id = node_ids_map.get(key)
+ if canonical_id is not None:
+ return canonical_id
+ # If the key is not a reserved keyword, return it as is, and update the map.
+ if key not in reserved_ids:
+ node_ids_map[key] = key
+ return key
+ # If the key is a reserved keyword, append a number to it.
+ number = 0
+ while True:
+ new_id = f"{key}_{number}"
+ if new_id not in node_ids_map:
+ node_ids_map[key] = new_id
+ return new_id
+ number += 1
+
+ # Use a sets to avoid duplicate entries.
+ nodes: set[str] = set()
+ edges: set[str] = set()
+
+ if isinstance(tree, ReversedPackageDAG):
+ for package, reverse_dependencies in tree.items():
+ package_label = "\\n".join(
+ (package.project_name, "(missing)" if package.is_missing else package.installed_version)
+ )
+ package_key = mermaid_id(package.key)
+ nodes.add(f'{package_key}["{package_label}"]')
+ for reverse_dependency in reverse_dependencies:
+ edge_label = reverse_dependency.req.version_spec or "any"
+ reverse_dependency_key = mermaid_id(reverse_dependency.key)
+ edges.add(f'{package_key} -- "{edge_label}" --> {reverse_dependency_key}')
+ else:
+ for package, dependencies in tree.items():
+ package_label = "\\n".join((package.project_name, package.version))
+ package_key = mermaid_id(package.key)
+ nodes.add(f'{package_key}["{package_label}"]')
+ for dependency in dependencies:
+ edge_label = dependency.version_spec or "any"
+ dependency_key = mermaid_id(dependency.key)
+ if dependency.is_missing:
+ dependency_label = f"{dependency.project_name}\\n(missing)"
+ nodes.add(f'{dependency_key}["{dependency_label}"]:::missing')
+ edges.add(f"{package_key} -.-> {dependency_key}")
+ else:
+ edges.add(f'{package_key} -- "{edge_label}" --> {dependency_key}')
+
+ # Produce the Mermaid Markdown.
+ indent = " " * 4
+ output = dedent(
+ f"""\
+ flowchart TD
+ {indent}classDef missing stroke-dasharray: 5
+ """
+ )
+ # Sort the nodes and edges to make the output deterministic.
+ output += indent
+ output += f"\n{indent}".join(node for node in sorted(nodes))
+ output += "\n" + indent
+ output += f"\n{indent}".join(edge for edge in sorted(edges))
+ output += "\n"
+ return output
+
+
+def dump_graphviz(tree, output_format="dot", is_reverse=False):
+ """Output dependency graph as one of the supported GraphViz output formats.
+
+ :param dict tree: dependency graph
+ :param string output_format: output format
+ :param bool is_reverse: reverse or not
+ :returns: representation of tree in the specified output format
+ :rtype: str or binary representation depending on the output format
+
+ """
+ try:
+ from graphviz import Digraph
+ except ImportError:
+ print("graphviz is not available, but necessary for the output " "option. Please install it.", file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ from graphviz import parameters
+ except ImportError:
+ from graphviz import backend
+
+ valid_formats = backend.FORMATS
+ print(
+ "Deprecation warning! Please upgrade graphviz to version >=0.18.0 "
+ "Support for older versions will be removed in upcoming release",
+ file=sys.stderr,
+ )
+ else:
+ valid_formats = parameters.FORMATS
+
+ if output_format not in valid_formats:
+ print(f"{output_format} is not a supported output format.", file=sys.stderr)
+ print(f"Supported formats are: {', '.join(sorted(valid_formats))}", file=sys.stderr)
+ sys.exit(1)
+
+ graph = Digraph(format=output_format)
+
+ if not is_reverse:
+ for pkg, deps in tree.items():
+ pkg_label = f"{pkg.project_name}\\n{pkg.version}"
+ graph.node(pkg.key, label=pkg_label)
+ for dep in deps:
+ edge_label = dep.version_spec or "any"
+ if dep.is_missing:
+ dep_label = f"{dep.project_name}\\n(missing)"
+ graph.node(dep.key, label=dep_label, style="dashed")
+ graph.edge(pkg.key, dep.key, style="dashed")
+ else:
+ graph.edge(pkg.key, dep.key, label=edge_label)
+ else:
+ for dep, parents in tree.items():
+ dep_label = f"{dep.project_name}\\n{dep.installed_version}"
+ graph.node(dep.key, label=dep_label)
+ for parent in parents:
+ # req reference of the dep associated with this
+ # particular parent package
+ req_ref = parent.req
+ edge_label = req_ref.version_spec or "any"
+ graph.edge(dep.key, parent.key, label=edge_label)
+
+ # Allow output of dot format, even if GraphViz isn't installed.
+ if output_format == "dot":
+ # Emulates graphviz.dot.Dot.__iter__() to force the sorting of graph.body.
+ # Fixes https://github.com/tox-dev/pipdeptree/issues/188
+ # That way we can guarantee the output of the dot format is deterministic
+ # and stable.
+ return "".join([tuple(graph)[0]] + sorted(graph.body) + [graph._tail])
+
+ # As it's unknown if the selected output format is binary or not, try to
+ # decode it as UTF8 and only print it out in binary if that's not possible.
+ try:
+ return graph.pipe().decode("utf-8")
+ except UnicodeDecodeError:
+ return graph.pipe()
+
+
+def print_graphviz(dump_output):
+ """
+ Dump the data generated by GraphViz to stdout.
+
+ :param dump_output: The output from dump_graphviz
+ """
+ if hasattr(dump_output, "encode"):
+ print(dump_output)
+ else:
+ with os.fdopen(sys.stdout.fileno(), "wb") as bytestream:
+ bytestream.write(dump_output)
+
+
+def conflicting_deps(tree):
+ """
+ Returns dependencies which are not present or conflict with the requirements of other packages.
+
+ e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed
+
+ :param tree: the requirements tree (dict)
+ :returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage
+ :rtype: dict
+ """
+ conflicting = defaultdict(list)
+ for p, rs in tree.items():
+ for req in rs:
+ if req.is_conflicting():
+ conflicting[p].append(req)
+ return conflicting
+
+
+def render_conflicts_text(conflicts):
+ if conflicts:
+ print("Warning!!! Possibly conflicting dependencies found:", file=sys.stderr)
+ # Enforce alphabetical order when listing conflicts
+ pkgs = sorted(conflicts.keys())
+ for p in pkgs:
+ pkg = p.render_as_root(False)
+ print(f"* {pkg}", file=sys.stderr)
+ for req in conflicts[p]:
+ req_str = req.render_as_branch(False)
+ print(f" - {req_str}", file=sys.stderr)
+
+
+def cyclic_deps(tree):
+ """
+ Return cyclic dependencies as list of tuples
+
+ :param PackageDAG tree: package tree/dag
+ :returns: list of tuples representing cyclic dependencies
+ :rtype: list
+ """
+ index = {p.key: {r.key for r in rs} for p, rs in tree.items()}
+ cyclic = []
+ for p, rs in tree.items():
+ for r in rs:
+ if p.key in index.get(r.key, []):
+ p_as_dep_of_r = [x for x in tree.get(tree.get_node_as_parent(r.key)) if x.key == p.key][0]
+ cyclic.append((p, r, p_as_dep_of_r))
+ return cyclic
+
+
+def render_cycles_text(cycles):
+ if cycles:
+ print("Warning!! Cyclic dependencies found:", file=sys.stderr)
+ # List in alphabetical order of the dependency that's cycling
+ # (2nd item in the tuple)
+ cycles = sorted(cycles, key=lambda xs: xs[1].key)
+ for a, b, c in cycles:
+ print(f"* {a.project_name} => {b.project_name} => {c.project_name}", file=sys.stderr)
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(description="Dependency tree of the installed python packages")
+ parser.add_argument("-v", "--version", action="version", version=f"{__version__}")
+ parser.add_argument("-f", "--freeze", action="store_true", help="Print names so as to write freeze files")
+ parser.add_argument(
+ "--python",
+ default=sys.executable,
+ help="Python to use to look for packages in it (default: where" " installed)",
+ )
+ parser.add_argument("-a", "--all", action="store_true", help="list all deps at top level")
+ parser.add_argument(
+ "-l",
+ "--local-only",
+ action="store_true",
+ help="If in a virtualenv that has global access " "do not show globally installed packages",
+ )
+ parser.add_argument("-u", "--user-only", action="store_true", help="Only show installations in the user site dir")
+ parser.add_argument(
+ "-w",
+ "--warn",
+ action="store",
+ dest="warn",
+ nargs="?",
+ default="suppress",
+ choices=("silence", "suppress", "fail"),
+ help=(
+ 'Warning control. "suppress" will show warnings '
+ "but return 0 whether or not they are present. "
+ '"silence" will not show warnings at all and '
+ 'always return 0. "fail" will show warnings and '
+ "return 1 if any are present. The default is "
+ '"suppress".'
+ ),
+ )
+ parser.add_argument(
+ "-r",
+ "--reverse",
+ action="store_true",
+ default=False,
+ help=(
+ "Shows the dependency tree in the reverse fashion "
+ "ie. the sub-dependencies are listed with the "
+ "list of packages that need them under them."
+ ),
+ )
+ parser.add_argument(
+ "-p",
+ "--packages",
+ help=(
+ "Comma separated list of select packages to show in the output. "
+ "Wildcards are supported, like 'somepackage.*'. "
+ "If set, --all will be ignored."
+ ),
+ )
+ parser.add_argument(
+ "-e",
+ "--exclude",
+ help=(
+ "Comma separated list of select packages to exclude from the output. "
+ "Wildcards are supported, like 'somepackage.*'. "
+ "If set, --all will be ignored."
+ ),
+ metavar="PACKAGES",
+ )
+ parser.add_argument(
+ "-j",
+ "--json",
+ action="store_true",
+ default=False,
+ help=(
+ "Display dependency tree as json. This will yield "
+ '"raw" output that may be used by external tools. '
+ "This option overrides all other options."
+ ),
+ )
+ parser.add_argument(
+ "--json-tree",
+ action="store_true",
+ default=False,
+ help=(
+ "Display dependency tree as json which is nested "
+ "the same way as the plain text output printed by default. "
+ "This option overrides all other options (except --json)."
+ ),
+ )
+ parser.add_argument(
+ "--mermaid",
+ action="store_true",
+ default=False,
+ help=("Display dependency tree as a Mermaid graph. " "This option overrides all other options."),
+ )
+ parser.add_argument(
+ "--graph-output",
+ dest="output_format",
+ help=(
+ "Print a dependency graph in the specified output "
+ "format. Available are all formats supported by "
+ "GraphViz, e.g.: dot, jpeg, pdf, png, svg"
+ ),
+ )
+ parser.add_argument(
+ "-d",
+ "--depth",
+ type=lambda x: int(x) if x.isdigit() and (int(x) >= 0) else parser.error("Depth must be a number that is >= 0"),
+ default=float("inf"),
+ help=(
+ "Display dependency tree up to a depth >=0 using the default text display. All other display options"
+ " ignore this argument."
+ ),
+ )
+ return parser
+
+
+def _get_args():
+ parser = get_parser()
+ return parser.parse_args()
+
+
+def handle_non_host_target(args):
+ of_python = os.path.abspath(args.python)
+ # if target is not current python re-invoke it under the actual host
+ if of_python != os.path.abspath(sys.executable):
+ # there's no way to guarantee that graphviz is available, so refuse
+ if args.output_format:
+ print("graphviz functionality is not supported when querying" " non-host python", file=sys.stderr)
+ raise SystemExit(1)
+ argv = sys.argv[1:] # remove current python executable
+ for py_at, value in enumerate(argv):
+ if value == "--python":
+ del argv[py_at]
+ del argv[py_at]
+ elif value.startswith("--python"):
+ del argv[py_at]
+
+ main_file = inspect.getsourcefile(sys.modules[__name__])
+ with tempfile.TemporaryDirectory() as project:
+ dest = os.path.join(project, "pipdeptree")
+ shutil.copytree(os.path.dirname(main_file), dest)
+ # invoke from an empty folder to avoid cwd altering sys.path
+ env = os.environ.copy()
+ env["PYTHONPATH"] = project
+ cmd = [of_python, "-m", "pipdeptree"]
+ cmd.extend(argv)
+ return subprocess.call(cmd, cwd=project, env=env)
+ return None
+
+
+def get_installed_distributions(local_only=False, user_only=False):
+ try:
+ from pip._internal.metadata import pkg_resources
+ except ImportError:
+ # For backward compatibility with python ver. 2.7 and pip
+ # version 20.3.4 (the latest pip version that works with python
+ # version 2.7)
+ from pip._internal.utils import misc
+
+ return misc.get_installed_distributions(local_only=local_only, user_only=user_only)
+ else:
+ dists = pkg_resources.Environment.from_paths(None).iter_installed_distributions(
+ local_only=local_only, skip=(), user_only=user_only
+ )
+ return [d._dist for d in dists]
+
+
+def main():
+ args = _get_args()
+ result = handle_non_host_target(args)
+ if result is not None:
+ return result
+
+ pkgs = get_installed_distributions(local_only=args.local_only, user_only=args.user_only)
+
+ tree = PackageDAG.from_pkgs(pkgs)
+
+ is_text_output = not any([args.json, args.json_tree, args.output_format])
+
+ return_code = 0
+
+ # Before any reversing or filtering, show warnings to console
+ # about possibly conflicting or cyclic deps if found and warnings
+ # are enabled (i.e. only if output is to be printed to console)
+ if is_text_output and args.warn != "silence":
+ conflicts = conflicting_deps(tree)
+ if conflicts:
+ render_conflicts_text(conflicts)
+ print("-" * 72, file=sys.stderr)
+
+ cycles = cyclic_deps(tree)
+ if cycles:
+ render_cycles_text(cycles)
+ print("-" * 72, file=sys.stderr)
+
+ if args.warn == "fail" and (conflicts or cycles):
+ return_code = 1
+
+ # Reverse the tree (if applicable) before filtering, thus ensuring
+ # that the filter will be applied on ReverseTree
+ if args.reverse:
+ tree = tree.reverse()
+
+ show_only = set(args.packages.split(",")) if args.packages else None
+ exclude = set(args.exclude.split(",")) if args.exclude else None
+
+ if show_only is not None or exclude is not None:
+ tree = tree.filter(show_only, exclude)
+
+ if args.json:
+ print(render_json(tree, indent=4))
+ elif args.json_tree:
+ print(render_json_tree(tree, indent=4))
+ elif args.mermaid:
+ print(render_mermaid(tree))
+ elif args.output_format:
+ output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse)
+ print_graphviz(output)
+ else:
+ render_text(tree, args.depth, args.all, args.freeze)
+
+ return return_code
diff --git a/src/pipdeptree/__main__.py b/src/pipdeptree/__main__.py
new file mode 100644
index 0000000..85cca3c
--- /dev/null
+++ b/src/pipdeptree/__main__.py
@@ -0,0 +1,6 @@
+import sys
+
+from pipdeptree import main
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/e2e-tests b/tests/e2e-tests
deleted file mode 100755
index cbd569e..0000000
--- a/tests/e2e-tests
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-PROFILE=$1
-PYTHON_EXE=${PYTHON_EXE:-python3.6}
-PIP_VERSION=${PIP_VERSION:-latest}
-
-if [ "$PROFILE" == "conflicting" ]; then
- PIP_VERSION=20.2.3
-fi
-
-cd profiles/$PROFILE
-
-echo "Profile dir: $(pwd)"
-
-env_dir=".env_$(basename $PYTHON_EXE)_pip-${PIP_VERSION}"
-
-echo "Profile env: $env_dir"
-
-if [ ! -d $env_dir ]; then
- virtualenv -p $PYTHON_EXE $env_dir
-fi
-
-pip=$env_dir/bin/pip
-
-if [ "$PIP_VERSION" == "latest" ]; then
- $pip install -U pip
-else
- $pip install pip==$PIP_VERSION
-fi
-
-# Install requirements
-$pip install -r requirements.txt
-
-# Install pipdeptree
-$pip install -e ../../../
-
-pip_deptree=$env_dir/bin/pipdeptree
-
-export TEST_PROFILE_DIR="profiles/$PROFILE"
-export PIPDEPTREE_EXE=$TEST_PROFILE_DIR/$pip_deptree
-
-cd -
-
-pytest -v e2e_tests.py
-
-
diff --git a/tests/e2e_tests.py b/tests/e2e_tests.py
deleted file mode 100644
index e203f75..0000000
--- a/tests/e2e_tests.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import json
-import os
-import shlex
-import subprocess
-
-from jinja2 import Environment, BaseLoader
-import pytest
-
-
-## Uncomment following lines for running in shell
-# os.environ['TEST_PROFILE_DIR'] = 'profiles/webapp'
-# os.environ['PIPDEPTREE_EXE'] = 'profiles/webapp/.env_python3.6_pip-latest/bin/pipdeptree'
-
-
-test_profile_dir = os.environ['TEST_PROFILE_DIR']
-pipdeptree_path = os.environ['PIPDEPTREE_EXE']
-
-
-def load_test_spec():
- test_spec_path = os.path.join(test_profile_dir, 'test_spec.json')
- with open(test_spec_path) as f:
- return json.load(f)
-
-
-test_spec = load_test_spec()
-
-
-def final_command(s):
- tmpl = Environment(loader=BaseLoader).from_string(s)
- return tmpl.render(pipdeptree=pipdeptree_path)
-
-
-def _test_cmp_with_file_contents(spec):
- p = subprocess.Popen(shlex.split(spec['command']),
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- out, err = p.communicate()
-
- assert spec['expected_returncode'] == p.returncode
-
- if spec['expected_output_file'] is not None:
- exp_output_file = os.path.join(test_profile_dir,
- spec['expected_output_file'])
- with open(exp_output_file, 'rb') as f:
- expected_output = f.read()
- assert expected_output == out
- else:
- assert out == b''
-
- if spec['expected_err_file'] is not None:
- exp_err_file = os.path.join(test_profile_dir,
- spec['expected_err_file'])
- with open(exp_err_file, 'rb') as f:
- expected_err = f.read()
- assert expected_err == err
- else:
- assert err == b''
-
-
-@pytest.mark.parametrize('spec', test_spec)
-def test_all_tests_in_profile(spec):
- spec['command'] = final_command(spec['command'])
- if spec['method'] == 'cmp_with_file_contents':
- _test_cmp_with_file_contents(spec)
diff --git a/tests/guess_version_setuptools.py b/tests/guess_version_setuptools.py
new file mode 100644
index 0000000..f9426f7
--- /dev/null
+++ b/tests/guess_version_setuptools.py
@@ -0,0 +1,16 @@
+import sys
+
+import pipdeptree
+
+if sys.version_info >= (3, 8):
+ import importlib.metadata as importlib_metadata
+else:
+ import importlib_metadata
+
+
+def raise_import_error(name):
+ raise ImportError(name)
+
+
+importlib_metadata.version = raise_import_error
+print(pipdeptree.guess_version("setuptools"), end="")
diff --git a/tests/profiles/conflicting/.gitignore b/tests/profiles/conflicting/.gitignore
deleted file mode 100644
index e6905a2..0000000
--- a/tests/profiles/conflicting/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-.env*
\ No newline at end of file
diff --git a/tests/profiles/conflicting/default.err b/tests/profiles/conflicting/default.err
deleted file mode 100644
index afa9767..0000000
--- a/tests/profiles/conflicting/default.err
+++ /dev/null
@@ -1,4 +0,0 @@
-Warning!!! Possibly conflicting dependencies found:
-* Jinja2==3.0.2
- - MarkupSafe [required: >=2.0, installed: 0.22]
-------------------------------------------------------------------------
diff --git a/tests/profiles/conflicting/default.out b/tests/profiles/conflicting/default.out
deleted file mode 100644
index ddc7358..0000000
--- a/tests/profiles/conflicting/default.out
+++ /dev/null
@@ -1,10 +0,0 @@
-argparse==1.4.0
-Flask==0.10.1
- - itsdangerous [required: >=0.21, installed: 0.24]
- - Jinja2 [required: >=2.4, installed: 3.0.2]
- - MarkupSafe [required: >=2.0, installed: 0.22]
- - Werkzeug [required: >=0.7, installed: 0.11.2]
-pipdeptree==2.1.0
- - pip [required: >=6.0.0, installed: 20.2.3]
-setuptools==58.2.0
-wheel==0.37.0
diff --git a/tests/profiles/conflicting/requirements.txt b/tests/profiles/conflicting/requirements.txt
deleted file mode 100644
index bc5fabc..0000000
--- a/tests/profiles/conflicting/requirements.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-Flask==0.10.1
-itsdangerous==0.24
-Jinja2
-MarkupSafe==0.22
-Werkzeug==0.11.2
-argparse
diff --git a/tests/profiles/conflicting/reverse.out b/tests/profiles/conflicting/reverse.out
deleted file mode 100644
index 1c4728b..0000000
--- a/tests/profiles/conflicting/reverse.out
+++ /dev/null
@@ -1,12 +0,0 @@
-argparse==1.4.0
-itsdangerous==0.24
- - Flask==0.10.1 [requires: itsdangerous>=0.21]
-MarkupSafe==0.22
- - Jinja2==3.0.2 [requires: MarkupSafe>=2.0]
- - Flask==0.10.1 [requires: Jinja2>=2.4]
-pip==20.2.3
- - pipdeptree==2.1.0 [requires: pip>=6.0.0]
-setuptools==58.2.0
-Werkzeug==0.11.2
- - Flask==0.10.1 [requires: Werkzeug>=0.7]
-wheel==0.37.0
diff --git a/tests/profiles/conflicting/test_spec.json b/tests/profiles/conflicting/test_spec.json
deleted file mode 100644
index 40e4419..0000000
--- a/tests/profiles/conflicting/test_spec.json
+++ /dev/null
@@ -1,34 +0,0 @@
-[
- {
- "id": "default_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}}",
- "expected_output_file": "default.out",
- "expected_err_file": "default.err",
- "expected_returncode": 0
- },
- {
- "id": "reverse_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -r",
- "expected_output_file": "reverse.out",
- "expected_err_file": "default.err",
- "expected_returncode": 0
- },
- {
- "id": "warning_silenced",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -w silence",
- "expected_output_file": "default.out",
- "expected_err_file": null,
- "expected_returncode": 0
- },
- {
- "id": "fail_if_conflicting",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -w fail",
- "expected_output_file": "default.out",
- "expected_err_file": "default.err",
- "expected_returncode": 1
- }
-]
diff --git a/tests/profiles/cyclic/.gitignore b/tests/profiles/cyclic/.gitignore
deleted file mode 100644
index e6905a2..0000000
--- a/tests/profiles/cyclic/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-.env*
\ No newline at end of file
diff --git a/tests/profiles/cyclic/default.err b/tests/profiles/cyclic/default.err
deleted file mode 100644
index 857656a..0000000
--- a/tests/profiles/cyclic/default.err
+++ /dev/null
@@ -1,4 +0,0 @@
-Warning!! Cyclic dependencies found:
-* CircularDependencyB => CircularDependencyA => CircularDependencyB
-* CircularDependencyA => CircularDependencyB => CircularDependencyA
-------------------------------------------------------------------------
diff --git a/tests/profiles/cyclic/default.out b/tests/profiles/cyclic/default.out
deleted file mode 100644
index c5ca807..0000000
--- a/tests/profiles/cyclic/default.out
+++ /dev/null
@@ -1,4 +0,0 @@
-pipdeptree==2.1.0
- - pip [required: >=6.0.0, installed: 21.3]
-setuptools==58.2.0
-wheel==0.37.0
diff --git a/tests/profiles/cyclic/requirements.txt b/tests/profiles/cyclic/requirements.txt
deleted file mode 100644
index 64644a8..0000000
--- a/tests/profiles/cyclic/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-CircularDependencyA
-CircularDependencyB
diff --git a/tests/profiles/cyclic/test_spec.json b/tests/profiles/cyclic/test_spec.json
deleted file mode 100644
index 6171fe1..0000000
--- a/tests/profiles/cyclic/test_spec.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
- {
- "id": "default_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}}",
- "expected_output_file": "default.out",
- "expected_err_file": "default.err",
- "expected_returncode": 0
- },
- {
- "id": "warning_silenced",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -w silence",
- "expected_output_file": "default.out",
- "expected_err_file": null,
- "expected_returncode": 0
- },
- {
- "id": "fail_if_cyclic",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -w fail",
- "expected_output_file": "default.out",
- "expected_err_file": "default.err",
- "expected_returncode": 1
- }
-]
diff --git a/tests/profiles/webapp/.gitignore b/tests/profiles/webapp/.gitignore
deleted file mode 100644
index e6905a2..0000000
--- a/tests/profiles/webapp/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-.env*
\ No newline at end of file
diff --git a/tests/profiles/webapp/all_flag.out b/tests/profiles/webapp/all_flag.out
deleted file mode 100644
index 587eac6..0000000
--- a/tests/profiles/webapp/all_flag.out
+++ /dev/null
@@ -1,83 +0,0 @@
-appnope==0.1.2
-backcall==0.2.0
-click==8.0.3
- - importlib-metadata [required: Any, installed: 4.8.1]
- - typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- - zipp [required: >=0.5, installed: 3.6.0]
-dataclasses==0.8
-decorator==5.1.0
-Flask==1.1.2
- - click [required: >=5.1, installed: 8.0.3]
- - importlib-metadata [required: Any, installed: 4.8.1]
- - typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- - zipp [required: >=0.5, installed: 3.6.0]
- - itsdangerous [required: >=0.24, installed: 2.0.1]
- - Jinja2 [required: >=2.10.1, installed: 3.0.2]
- - MarkupSafe [required: >=2.0, installed: 2.0.1]
- - Werkzeug [required: >=0.15, installed: 2.0.2]
- - dataclasses [required: Any, installed: 0.8]
-Flask-Script==2.0.6
- - Flask [required: Any, installed: 1.1.2]
- - click [required: >=5.1, installed: 8.0.3]
- - importlib-metadata [required: Any, installed: 4.8.1]
- - typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- - zipp [required: >=0.5, installed: 3.6.0]
- - itsdangerous [required: >=0.24, installed: 2.0.1]
- - Jinja2 [required: >=2.10.1, installed: 3.0.2]
- - MarkupSafe [required: >=2.0, installed: 2.0.1]
- - Werkzeug [required: >=0.15, installed: 2.0.2]
- - dataclasses [required: Any, installed: 0.8]
-gnureadline==8.0.0
-importlib-metadata==4.8.1
- - typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- - zipp [required: >=0.5, installed: 3.6.0]
-ipython==7.13.0
- - appnope [required: Any, installed: 0.1.2]
- - backcall [required: Any, installed: 0.2.0]
- - decorator [required: Any, installed: 5.1.0]
- - jedi [required: >=0.10, installed: 0.18.0]
- - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
- - pexpect [required: Any, installed: 4.8.0]
- - ptyprocess [required: >=0.5, installed: 0.7.0]
- - pickleshare [required: Any, installed: 0.7.5]
- - prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20]
- - wcwidth [required: Any, installed: 0.2.5]
- - pygments [required: Any, installed: 2.10.0]
- - setuptools [required: >=18.5, installed: 58.2.0]
- - traitlets [required: >=4.2, installed: 4.3.3]
- - decorator [required: Any, installed: 5.1.0]
- - ipython-genutils [required: Any, installed: 0.2.0]
- - six [required: Any, installed: 1.16.0]
-ipython-genutils==0.2.0
-itsdangerous==2.0.1
-jedi==0.18.0
- - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
-Jinja2==3.0.2
- - MarkupSafe [required: >=2.0, installed: 2.0.1]
-MarkupSafe==2.0.1
-parso==0.8.2
-pexpect==4.8.0
- - ptyprocess [required: >=0.5, installed: 0.7.0]
-pickleshare==0.7.5
-pip==21.3
-pipdeptree==2.1.0
- - pip [required: >=6.0.0, installed: 21.3]
-prompt-toolkit==3.0.20
- - wcwidth [required: Any, installed: 0.2.5]
-ptyprocess==0.7.0
-Pygments==2.10.0
-pymongo==3.10.1
-redis==3.4.1
-setuptools==58.2.0
-six==1.16.0
-slugify==0.0.1
-traitlets==4.3.3
- - decorator [required: Any, installed: 5.1.0]
- - ipython-genutils [required: Any, installed: 0.2.0]
- - six [required: Any, installed: 1.16.0]
-typing-extensions==3.10.0.2
-wcwidth==0.2.5
-Werkzeug==2.0.2
- - dataclasses [required: Any, installed: 0.8]
-wheel==0.37.0
-zipp==3.6.0
diff --git a/tests/profiles/webapp/default.out b/tests/profiles/webapp/default.out
deleted file mode 100644
index f825f40..0000000
--- a/tests/profiles/webapp/default.out
+++ /dev/null
@@ -1,35 +0,0 @@
-Flask-Script==2.0.6
- - Flask [required: Any, installed: 1.1.2]
- - click [required: >=5.1, installed: 8.0.3]
- - importlib-metadata [required: Any, installed: 4.8.1]
- - typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- - zipp [required: >=0.5, installed: 3.6.0]
- - itsdangerous [required: >=0.24, installed: 2.0.1]
- - Jinja2 [required: >=2.10.1, installed: 3.0.2]
- - MarkupSafe [required: >=2.0, installed: 2.0.1]
- - Werkzeug [required: >=0.15, installed: 2.0.2]
- - dataclasses [required: Any, installed: 0.8]
-gnureadline==8.0.0
-ipython==7.13.0
- - appnope [required: Any, installed: 0.1.2]
- - backcall [required: Any, installed: 0.2.0]
- - decorator [required: Any, installed: 5.1.0]
- - jedi [required: >=0.10, installed: 0.18.0]
- - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
- - pexpect [required: Any, installed: 4.8.0]
- - ptyprocess [required: >=0.5, installed: 0.7.0]
- - pickleshare [required: Any, installed: 0.7.5]
- - prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20]
- - wcwidth [required: Any, installed: 0.2.5]
- - pygments [required: Any, installed: 2.10.0]
- - setuptools [required: >=18.5, installed: 58.2.0]
- - traitlets [required: >=4.2, installed: 4.3.3]
- - decorator [required: Any, installed: 5.1.0]
- - ipython-genutils [required: Any, installed: 0.2.0]
- - six [required: Any, installed: 1.16.0]
-pipdeptree==2.1.0
- - pip [required: >=6.0.0, installed: 21.3]
-pymongo==3.10.1
-redis==3.4.1
-slugify==0.0.1
-wheel==0.37.0
diff --git a/tests/profiles/webapp/packages_opt.out b/tests/profiles/webapp/packages_opt.out
deleted file mode 100644
index 21c1b6f..0000000
--- a/tests/profiles/webapp/packages_opt.out
+++ /dev/null
@@ -1,17 +0,0 @@
-ipython==7.13.0
- - appnope [required: Any, installed: 0.1.2]
- - backcall [required: Any, installed: 0.2.0]
- - decorator [required: Any, installed: 5.1.0]
- - jedi [required: >=0.10, installed: 0.18.0]
- - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
- - pexpect [required: Any, installed: 4.8.0]
- - ptyprocess [required: >=0.5, installed: 0.7.0]
- - pickleshare [required: Any, installed: 0.7.5]
- - prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20]
- - wcwidth [required: Any, installed: 0.2.5]
- - pygments [required: Any, installed: 2.10.0]
- - setuptools [required: >=18.5, installed: 58.2.0]
- - traitlets [required: >=4.2, installed: 4.3.3]
- - decorator [required: Any, installed: 5.1.0]
- - ipython-genutils [required: Any, installed: 0.2.0]
- - six [required: Any, installed: 1.16.0]
diff --git a/tests/profiles/webapp/packages_reverse.out b/tests/profiles/webapp/packages_reverse.out
deleted file mode 100644
index cab8e48..0000000
--- a/tests/profiles/webapp/packages_reverse.out
+++ /dev/null
@@ -1,7 +0,0 @@
-click==8.0.3
- - Flask==1.1.2 [requires: click>=5.1]
- - Flask-Script==2.0.6 [requires: Flask]
-decorator==5.1.0
- - ipython==7.13.0 [requires: decorator]
- - traitlets==4.3.3 [requires: decorator]
- - ipython==7.13.0 [requires: traitlets>=4.2]
diff --git a/tests/profiles/webapp/requirements.txt b/tests/profiles/webapp/requirements.txt
deleted file mode 100644
index 3a740b2..0000000
--- a/tests/profiles/webapp/requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-Flask==1.1.2
-Flask-Script==2.0.6
-gnureadline==8.0.0
-pymongo==3.10.1
-redis==3.4.1
-slugify==0.0.1
-ipython==7.13.0
diff --git a/tests/profiles/webapp/reverse.out b/tests/profiles/webapp/reverse.out
deleted file mode 100644
index 37dc43a..0000000
--- a/tests/profiles/webapp/reverse.out
+++ /dev/null
@@ -1,57 +0,0 @@
-appnope==0.1.2
- - ipython==7.13.0 [requires: appnope]
-backcall==0.2.0
- - ipython==7.13.0 [requires: backcall]
-dataclasses==0.8
- - Werkzeug==2.0.2 [requires: dataclasses]
- - Flask==1.1.2 [requires: Werkzeug>=0.15]
- - Flask-Script==2.0.6 [requires: Flask]
-decorator==5.1.0
- - ipython==7.13.0 [requires: decorator]
- - traitlets==4.3.3 [requires: decorator]
- - ipython==7.13.0 [requires: traitlets>=4.2]
-gnureadline==8.0.0
-ipython-genutils==0.2.0
- - traitlets==4.3.3 [requires: ipython-genutils]
- - ipython==7.13.0 [requires: traitlets>=4.2]
-itsdangerous==2.0.1
- - Flask==1.1.2 [requires: itsdangerous>=0.24]
- - Flask-Script==2.0.6 [requires: Flask]
-MarkupSafe==2.0.1
- - Jinja2==3.0.2 [requires: MarkupSafe>=2.0]
- - Flask==1.1.2 [requires: Jinja2>=2.10.1]
- - Flask-Script==2.0.6 [requires: Flask]
-parso==0.8.2
- - jedi==0.18.0 [requires: parso>=0.8.0,<0.9.0]
- - ipython==7.13.0 [requires: jedi>=0.10]
-pickleshare==0.7.5
- - ipython==7.13.0 [requires: pickleshare]
-pip==21.3
- - pipdeptree==2.1.0 [requires: pip>=6.0.0]
-ptyprocess==0.7.0
- - pexpect==4.8.0 [requires: ptyprocess>=0.5]
- - ipython==7.13.0 [requires: pexpect]
-pygments==2.10.0
- - ipython==7.13.0 [requires: pygments]
-pymongo==3.10.1
-redis==3.4.1
-setuptools==58.2.0
- - ipython==7.13.0 [requires: setuptools>=18.5]
-six==1.16.0
- - traitlets==4.3.3 [requires: six]
- - ipython==7.13.0 [requires: traitlets>=4.2]
-slugify==0.0.1
-typing-extensions==3.10.0.2
- - importlib-metadata==4.8.1 [requires: typing-extensions>=3.6.4]
- - click==8.0.3 [requires: importlib-metadata]
- - Flask==1.1.2 [requires: click>=5.1]
- - Flask-Script==2.0.6 [requires: Flask]
-wcwidth==0.2.5
- - prompt-toolkit==3.0.20 [requires: wcwidth]
- - ipython==7.13.0 [requires: prompt-toolkit>=2.0.0,<3.1.0,!=3.0.1,!=3.0.0]
-wheel==0.37.0
-zipp==3.6.0
- - importlib-metadata==4.8.1 [requires: zipp>=0.5]
- - click==8.0.3 [requires: importlib-metadata]
- - Flask==1.1.2 [requires: click>=5.1]
- - Flask-Script==2.0.6 [requires: Flask]
diff --git a/tests/profiles/webapp/test_spec.json b/tests/profiles/webapp/test_spec.json
deleted file mode 100644
index 1b86f75..0000000
--- a/tests/profiles/webapp/test_spec.json
+++ /dev/null
@@ -1,42 +0,0 @@
-[
- {
- "id": "default_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}}",
- "expected_output_file": "default.out",
- "expected_err_file": null,
- "expected_returncode": 0
- },
- {
- "id": "reverse_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -r",
- "expected_output_file": "reverse.out",
- "expected_err_file": null,
- "expected_returncode": 0
- },
- {
- "id": "--all_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} --all",
- "expected_output_file": "all_flag.out",
- "expected_err_file": null,
- "expected_returncode": 0
- },
- {
- "id": "--packages_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} --packages pexpect,ipython",
- "expected_output_file": "packages_opt.out",
- "expected_err_file": null,
- "expected_returncode": 0
- },
- {
- "id": "--packages--reverse_output",
- "method": "cmp_with_file_contents",
- "command": "{{pipdeptree}} -r -p decorator,click",
- "expected_output_file": "packages_reverse.out",
- "expected_err_file": null,
- "expected_returncode": 0
- }
-]
diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py
index af854ab..f50094d 100644
--- a/tests/test_pipdeptree.py
+++ b/tests/test_pipdeptree.py
@@ -1,36 +1,43 @@
-from contextlib import contextmanager
import platform
+import random
+import subprocess
import sys
+from contextlib import contextmanager
+from itertools import chain
+from pathlib import Path
from tempfile import NamedTemporaryFile
+from textwrap import dedent, indent
+from typing import Any
+
try:
from unittest import mock
except ImportError:
- import mock
+ from unittest import mock
import pytest
import virtualenv
import pipdeptree as p
-
# Tests for DAG classes
+
def mock_pkgs(simple_graph):
for node, children in simple_graph.items():
nk, nv = node
- p = mock.Mock(key=nk, project_name=nk, version=nv)
- as_req = mock.Mock(key=nk, project_name=nk, specs=[('==', nv)])
- p.as_requirement = mock.Mock(return_value=as_req)
+ m = mock.Mock(key=nk, project_name=nk, version=nv)
+ as_req = mock.Mock(key=nk, project_name=nk, specs=[("==", nv)])
+ m.as_requirement = mock.Mock(return_value=as_req)
reqs = []
for child in children:
ck, cv = child
r = mock.Mock(key=ck, project_name=ck, specs=cv)
reqs.append(r)
- p.requires = mock.Mock(return_value=reqs)
- yield p
+ m.requires = mock.Mock(return_value=reqs)
+ yield m
-def mock_PackageDAG(simple_graph):
+def mock_package_dag(simple_graph):
pkgs = list(mock_pkgs(simple_graph))
return p.PackageDAG.from_pkgs(pkgs)
@@ -44,91 +51,102 @@ def sort_map_values(m):
return {k: sorted(v) for k, v in m.items()}
-t = mock_PackageDAG({
- ('a', '3.4.0'): [('b', [('>=', '2.0.0')]),
- ('c', [('>=', '5.7.1')])],
- ('b', '2.3.1'): [('d', [('>=', '2.30'), ('<', '2.42')])],
- ('c', '5.10.0'): [('d', [('>=', '2.30')]),
- ('e', [('>=', '0.12.1')])],
- ('d', '2.35'): [('e', [('>=', '0.9.0')])],
- ('e', '0.12.1'): [],
- ('f', '3.1'): [('b', [('>=', '2.1.0')])],
- ('g', '6.8.3rc1'): [('e', [('>=', '0.9.0')]),
- ('f', [('>=', '3.0.0')])]
-})
+t = mock_package_dag(
+ {
+ ("a", "3.4.0"): [("b", [(">=", "2.0.0")]), ("c", [(">=", "5.7.1")])],
+ ("b", "2.3.1"): [("d", [(">=", "2.30"), ("<", "2.42")])],
+ ("c", "5.10.0"): [("d", [(">=", "2.30")]), ("e", [(">=", "0.12.1")])],
+ ("d", "2.35"): [("e", [(">=", "0.9.0")])],
+ ("e", "0.12.1"): [],
+ ("f", "3.1"): [("b", [(">=", "2.1.0")])],
+ ("g", "6.8.3rc1"): [("e", [(">=", "0.9.0")]), ("f", [(">=", "3.0.0")])],
+ }
+)
-def test_PackageDAG__get_node_as_parent():
- assert 'b' == t.get_node_as_parent('b').key
- assert 'c' == t.get_node_as_parent('c').key
+def test_package_dag_get_node_as_parent():
+ assert "b" == t.get_node_as_parent("b").key
+ assert "c" == t.get_node_as_parent("c").key
-def test_PackageDAG_filter():
+def test_package_dag_filter():
# When both show_only and exclude are not specified, same tree
# object is returned
assert t.filter(None, None) is t
# when show_only is specified
- g1 = dag_to_dict(t.filter(set(['a', 'd']), None))
- expected = {'a': ['b', 'c'],
- 'b': ['d'],
- 'c': ['d', 'e'],
- 'd': ['e'],
- 'e': []}
+ g1 = dag_to_dict(t.filter({"a", "d"}, None))
+ expected = {"a": ["b", "c"], "b": ["d"], "c": ["d", "e"], "d": ["e"], "e": []}
assert expected == g1
# when exclude is specified
- g2 = dag_to_dict(t.filter(None, ['d']))
- expected = {'a': ['b', 'c'],
- 'b': [],
- 'c': ['e'],
- 'e': [],
- 'f': ['b'],
- 'g': ['e', 'f']}
+ g2 = dag_to_dict(t.filter(None, ["d"]))
+ expected = {"a": ["b", "c"], "b": [], "c": ["e"], "e": [], "f": ["b"], "g": ["e", "f"]}
assert expected == g2
# when both show_only and exclude are specified
- g3 = dag_to_dict(t.filter(set(['a', 'g']), set(['d', 'e'])))
- expected = {'a': ['b', 'c'],
- 'b': [],
- 'c': [],
- 'f': ['b'],
- 'g': ['f']}
+ g3 = dag_to_dict(t.filter({"a", "g"}, {"d", "e"}))
+ expected = {"a": ["b", "c"], "b": [], "c": [], "f": ["b"], "g": ["f"]}
assert expected == g3
# when conflicting values in show_only and exclude, AssertionError
# is raised
with pytest.raises(AssertionError):
- dag_to_dict(t.filter(set(['d']), set(['D', 'e'])))
+ dag_to_dict(t.filter({"d"}, {"D", "e"}))
+
+
+@pytest.fixture(scope="session")
+def t_fnmatch() -> Any:
+ return mock_package_dag(
+ {
+ ("a.a", "1"): [("a.b", []), ("a.c", [])],
+ ("a.b", "1"): [("a.c", [])],
+ ("b.a", "1"): [("b.b", [])],
+ ("b.b", "1"): [("a.b", [])],
+ }
+ )
+
+
+def test_package_dag_filter_fnmatch_include_a(t_fnmatch: Any) -> None:
+ # test include for a.*in the result we got only a.* nodes
+ graph = dag_to_dict(t_fnmatch.filter({"a.*"}, None))
+ assert graph == {"a.a": ["a.b", "a.c"], "a.b": ["a.c"]}
+
+
+def test_package_dag_filter_fnmatch_include_b(t_fnmatch: Any) -> None:
+ # test include for b.*, which has a.b and a.c in tree, but not a.a
+ # in the result we got the b.* nodes plus the a.b node as child in the tree
+ graph = dag_to_dict(t_fnmatch.filter({"b.*"}, None))
+ assert graph == {"b.a": ["b.b"], "b.b": ["a.b"], "a.b": ["a.c"]}
+
+
+def test_package_dag_filter_fnmatch_exclude_c(t_fnmatch: Any) -> None:
+ # test exclude for b.* in the result we got only a.* nodes
+ graph = dag_to_dict(t_fnmatch.filter(None, {"b.*"}))
+ assert graph == {"a.a": ["a.b", "a.c"], "a.b": ["a.c"]}
-def test_PackageDAG_reverse():
+def test_package_dag_filter_fnmatch_exclude_a(t_fnmatch: Any) -> None:
+ # test exclude for a.* in the result we got only b.* nodes
+ graph = dag_to_dict(t_fnmatch.filter(None, {"a.*"}))
+ assert graph == {"b.a": ["b.b"], "b.b": []}
+
+
+def test_package_dag_reverse():
t1 = t.reverse()
- expected = {'a': [],
- 'b': ['a', 'f'],
- 'c': ['a'],
- 'd': ['b', 'c'],
- 'e': ['c', 'd', 'g'],
- 'f': ['g'],
- 'g': []}
+ expected = {"a": [], "b": ["a", "f"], "c": ["a"], "d": ["b", "c"], "e": ["c", "d", "g"], "f": ["g"], "g": []}
assert isinstance(t1, p.ReversedPackageDAG)
assert sort_map_values(expected) == sort_map_values(dag_to_dict(t1))
- assert all([isinstance(k, p.ReqPackage) for k in t1.keys()])
- assert all([isinstance(v, p.DistPackage) for v in p.flatten(t1.values())])
+ assert all(isinstance(k, p.ReqPackage) for k in t1.keys())
+ assert all(isinstance(v, p.DistPackage) for v in chain.from_iterable(t1.values()))
# testing reversal of ReversedPackageDAG instance
- expected = {'a': ['b', 'c'],
- 'b': ['d'],
- 'c': ['d', 'e'],
- 'd': ['e'],
- 'e': [],
- 'f': ['b'],
- 'g': ['e', 'f']}
+ expected = {"a": ["b", "c"], "b": ["d"], "c": ["d", "e"], "d": ["e"], "e": [], "f": ["b"], "g": ["e", "f"]}
t2 = t1.reverse()
assert isinstance(t2, p.PackageDAG)
assert sort_map_values(expected) == sort_map_values(dag_to_dict(t2))
- assert all([isinstance(k, p.DistPackage) for k in t2.keys()])
- assert all([isinstance(v, p.ReqPackage) for v in p.flatten(t2.values())])
+ assert all(isinstance(k, p.DistPackage) for k in t2.keys())
+ assert all(isinstance(v, p.ReqPackage) for v in chain.from_iterable(t2.values()))
# Tests for Package classes
@@ -136,36 +154,31 @@ def test_PackageDAG_reverse():
# Note: For all render methods, we are only testing for frozen=False
# as mocks with frozen=True are a lot more complicated
-def test_DistPackage__render_as_root():
- foo = mock.Mock(key='foo', project_name='foo', version='20.4.1')
+
+def test_dist_package_render_as_root():
+ foo = mock.Mock(key="foo", project_name="foo", version="20.4.1")
dp = p.DistPackage(foo)
is_frozen = False
- assert 'foo==20.4.1' == dp.render_as_root(is_frozen)
+ assert "foo==20.4.1" == dp.render_as_root(is_frozen)
-def test_DistPackage__render_as_branch():
- foo = mock.Mock(key='foo', project_name='foo', version='20.4.1')
- bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
- bar_req = mock.Mock(key='bar',
- project_name='bar',
- version='4.1.0',
- specs=[('>=', '4.0')])
+def test_dist_package_render_as_branch():
+ foo = mock.Mock(key="foo", project_name="foo", version="20.4.1")
+ bar = mock.Mock(key="bar", project_name="bar", version="4.1.0")
+ bar_req = mock.Mock(key="bar", project_name="bar", version="4.1.0", specs=[(">=", "4.0")])
rp = p.ReqPackage(bar_req, dist=bar)
dp = p.DistPackage(foo).as_parent_of(rp)
is_frozen = False
- assert 'foo==20.4.1 [requires: bar>=4.0]' == dp.render_as_branch(is_frozen)
+ assert "foo==20.4.1 [requires: bar>=4.0]" == dp.render_as_branch(is_frozen)
-def test_DistPackage__as_parent_of():
- foo = mock.Mock(key='foo', project_name='foo', version='20.4.1')
+def test_dist_package_as_parent_of():
+ foo = mock.Mock(key="foo", project_name="foo", version="20.4.1")
dp = p.DistPackage(foo)
assert dp.req is None
- bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
- bar_req = mock.Mock(key='bar',
- project_name='bar',
- version='4.1.0',
- specs=[('>=', '4.0')])
+ bar = mock.Mock(key="bar", project_name="bar", version="4.1.0")
+ bar_req = mock.Mock(key="bar", project_name="bar", version="4.1.0", specs=[(">=", "4.0")])
rp = p.ReqPackage(bar_req, dist=bar)
dp1 = dp.as_parent_of(rp)
assert dp1._obj == dp._obj
@@ -175,180 +188,524 @@ def test_DistPackage__as_parent_of():
assert dp2 is dp
-def test_DistPackage__as_dict():
- foo = mock.Mock(key='foo', project_name='foo', version='1.3.2b1')
+def test_dist_package_as_dict():
+ foo = mock.Mock(key="foo", project_name="foo", version="1.3.2b1")
dp = p.DistPackage(foo)
result = dp.as_dict()
- expected = {'key': 'foo',
- 'package_name': 'foo',
- 'installed_version': '1.3.2b1'}
+ expected = {"key": "foo", "package_name": "foo", "installed_version": "1.3.2b1"}
assert expected == result
-def test_ReqPackage__render_as_root():
- bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
- bar_req = mock.Mock(key='bar',
- project_name='bar',
- version='4.1.0',
- specs=[('>=', '4.0')])
+def test_req_package_render_as_root():
+ bar = mock.Mock(key="bar", project_name="bar", version="4.1.0")
+ bar_req = mock.Mock(key="bar", project_name="bar", version="4.1.0", specs=[(">=", "4.0")])
rp = p.ReqPackage(bar_req, dist=bar)
is_frozen = False
- assert 'bar==4.1.0' == rp.render_as_root(is_frozen)
+ assert "bar==4.1.0" == rp.render_as_root(is_frozen)
-def test_ReqPackage__render_as_branch():
- bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
- bar_req = mock.Mock(key='bar',
- project_name='bar',
- version='4.1.0',
- specs=[('>=', '4.0')])
+def test_req_package_render_as_branch():
+ bar = mock.Mock(key="bar", project_name="bar", version="4.1.0")
+ bar_req = mock.Mock(key="bar", project_name="bar", version="4.1.0", specs=[(">=", "4.0")])
rp = p.ReqPackage(bar_req, dist=bar)
is_frozen = False
- assert 'bar [required: >=4.0, installed: 4.1.0]' == rp.render_as_branch(is_frozen)
+ assert "bar [required: >=4.0, installed: 4.1.0]" == rp.render_as_branch(is_frozen)
-def test_ReqPackage__as_dict():
- bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
- bar_req = mock.Mock(key='bar',
- project_name='bar',
- version='4.1.0',
- specs=[('>=', '4.0')])
+def test_req_package_as_dict():
+ bar = mock.Mock(key="bar", project_name="bar", version="4.1.0")
+ bar_req = mock.Mock(key="bar", project_name="bar", version="4.1.0", specs=[(">=", "4.0")])
rp = p.ReqPackage(bar_req, dist=bar)
result = rp.as_dict()
- expected = {'key': 'bar',
- 'package_name': 'bar',
- 'installed_version': '4.1.0',
- 'required_version': '>=4.0'}
+ expected = {"key": "bar", "package_name": "bar", "installed_version": "4.1.0", "required_version": ">=4.0"}
assert expected == result
# Tests for render_text
-#
-# @NOTE: These tests use mocked tree and it's not easy to test for
-# frozen=True with mocks. Hence those tests are covered only in
-# end-to-end tests. Check the ./e2e-tests script.
+
+
+class MockStdout:
+ """
+ A wrapper to stdout that mocks the `encoding` attribute (to have `render_text()` render with unicode/non-unicode)
+ and `write()` (so that `print()` calls can write to stdout).
+ """
+
+ def __init__(self, encoding):
+ self.stdout = sys.stdout
+ self.encoding = encoding
+
+ def encoding(self):
+ return self.encoding
+
+ def write(self, text):
+ self.stdout.write(text)
+
@pytest.mark.parametrize(
- "list_all,reverse,expected_output",
+ ("list_all", "reverse", "unicode", "expected_output"),
[
(
True,
False,
+ True,
[
- 'a==3.4.0',
- ' - b [required: >=2.0.0, installed: 2.3.1]',
- ' - d [required: >=2.30,<2.42, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - c [required: >=5.7.1, installed: 5.10.0]',
- ' - d [required: >=2.30, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - e [required: >=0.12.1, installed: 0.12.1]',
- 'b==2.3.1',
- ' - d [required: >=2.30,<2.42, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- 'c==5.10.0',
- ' - d [required: >=2.30, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - e [required: >=0.12.1, installed: 0.12.1]',
- 'd==2.35',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- 'e==0.12.1',
- 'f==3.1',
- ' - b [required: >=2.1.0, installed: 2.3.1]',
- ' - d [required: >=2.30,<2.42, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- 'g==6.8.3rc1',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - f [required: >=3.0.0, installed: 3.1]',
- ' - b [required: >=2.1.0, installed: 2.3.1]',
- ' - d [required: >=2.30,<2.42, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]'
- ]
+ "a==3.4.0",
+ "├── b [required: >=2.0.0, installed: 2.3.1]",
+ "│ └── d [required: >=2.30,<2.42, installed: 2.35]",
+ "│ └── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── c [required: >=5.7.1, installed: 5.10.0]",
+ " ├── d [required: >=2.30, installed: 2.35]",
+ " │ └── e [required: >=0.9.0, installed: 0.12.1]",
+ " └── e [required: >=0.12.1, installed: 0.12.1]",
+ "b==2.3.1",
+ "└── d [required: >=2.30,<2.42, installed: 2.35]",
+ " └── e [required: >=0.9.0, installed: 0.12.1]",
+ "c==5.10.0",
+ "├── d [required: >=2.30, installed: 2.35]",
+ "│ └── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── e [required: >=0.12.1, installed: 0.12.1]",
+ "d==2.35",
+ "└── e [required: >=0.9.0, installed: 0.12.1]",
+ "e==0.12.1",
+ "f==3.1",
+ "└── b [required: >=2.1.0, installed: 2.3.1]",
+ " └── d [required: >=2.30,<2.42, installed: 2.35]",
+ " └── e [required: >=0.9.0, installed: 0.12.1]",
+ "g==6.8.3rc1",
+ "├── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── f [required: >=3.0.0, installed: 3.1]",
+ " └── b [required: >=2.1.0, installed: 2.3.1]",
+ " └── d [required: >=2.30,<2.42, installed: 2.35]",
+ " └── e [required: >=0.9.0, installed: 0.12.1]",
+ ],
),
(
+ True,
True,
True,
[
- 'a==3.4.0',
- 'b==2.3.1',
- ' - a==3.4.0 [requires: b>=2.0.0]',
- ' - f==3.1 [requires: b>=2.1.0]',
- ' - g==6.8.3rc1 [requires: f>=3.0.0]',
- 'c==5.10.0',
- ' - a==3.4.0 [requires: c>=5.7.1]',
- 'd==2.35',
- ' - b==2.3.1 [requires: d>=2.30,<2.42]',
- ' - a==3.4.0 [requires: b>=2.0.0]',
- ' - f==3.1 [requires: b>=2.1.0]',
- ' - g==6.8.3rc1 [requires: f>=3.0.0]',
- ' - c==5.10.0 [requires: d>=2.30]',
- ' - a==3.4.0 [requires: c>=5.7.1]',
- 'e==0.12.1',
- ' - c==5.10.0 [requires: e>=0.12.1]',
- ' - a==3.4.0 [requires: c>=5.7.1]',
- ' - d==2.35 [requires: e>=0.9.0]',
- ' - b==2.3.1 [requires: d>=2.30,<2.42]',
- ' - a==3.4.0 [requires: b>=2.0.0]',
- ' - f==3.1 [requires: b>=2.1.0]',
- ' - g==6.8.3rc1 [requires: f>=3.0.0]',
- ' - c==5.10.0 [requires: d>=2.30]',
- ' - a==3.4.0 [requires: c>=5.7.1]',
- ' - g==6.8.3rc1 [requires: e>=0.9.0]',
- 'f==3.1',
- ' - g==6.8.3rc1 [requires: f>=3.0.0]',
- 'g==6.8.3rc1'
- ]
+ "a==3.4.0",
+ "b==2.3.1",
+ "├── a==3.4.0 [requires: b>=2.0.0]",
+ "└── f==3.1 [requires: b>=2.1.0]",
+ " └── g==6.8.3rc1 [requires: f>=3.0.0]",
+ "c==5.10.0",
+ "└── a==3.4.0 [requires: c>=5.7.1]",
+ "d==2.35",
+ "├── b==2.3.1 [requires: d>=2.30,<2.42]",
+ "│ ├── a==3.4.0 [requires: b>=2.0.0]",
+ "│ └── f==3.1 [requires: b>=2.1.0]",
+ "│ └── g==6.8.3rc1 [requires: f>=3.0.0]",
+ "└── c==5.10.0 [requires: d>=2.30]",
+ " └── a==3.4.0 [requires: c>=5.7.1]",
+ "e==0.12.1",
+ "├── c==5.10.0 [requires: e>=0.12.1]",
+ "│ └── a==3.4.0 [requires: c>=5.7.1]",
+ "├── d==2.35 [requires: e>=0.9.0]",
+ "│ ├── b==2.3.1 [requires: d>=2.30,<2.42]",
+ "│ │ ├── a==3.4.0 [requires: b>=2.0.0]",
+ "│ │ └── f==3.1 [requires: b>=2.1.0]",
+ "│ │ └── g==6.8.3rc1 [requires: f>=3.0.0]",
+ "│ └── c==5.10.0 [requires: d>=2.30]",
+ "│ └── a==3.4.0 [requires: c>=5.7.1]",
+ "└── g==6.8.3rc1 [requires: e>=0.9.0]",
+ "f==3.1",
+ "└── g==6.8.3rc1 [requires: f>=3.0.0]",
+ "g==6.8.3rc1",
+ ],
),
(
False,
False,
+ True,
[
- 'a==3.4.0',
- ' - b [required: >=2.0.0, installed: 2.3.1]',
- ' - d [required: >=2.30,<2.42, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - c [required: >=5.7.1, installed: 5.10.0]',
- ' - d [required: >=2.30, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - e [required: >=0.12.1, installed: 0.12.1]',
- 'g==6.8.3rc1',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ' - f [required: >=3.0.0, installed: 3.1]',
- ' - b [required: >=2.1.0, installed: 2.3.1]',
- ' - d [required: >=2.30,<2.42, installed: 2.35]',
- ' - e [required: >=0.9.0, installed: 0.12.1]',
- ]
+ "a==3.4.0",
+ "├── b [required: >=2.0.0, installed: 2.3.1]",
+ "│ └── d [required: >=2.30,<2.42, installed: 2.35]",
+ "│ └── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── c [required: >=5.7.1, installed: 5.10.0]",
+ " ├── d [required: >=2.30, installed: 2.35]",
+ " │ └── e [required: >=0.9.0, installed: 0.12.1]",
+ " └── e [required: >=0.12.1, installed: 0.12.1]",
+ "g==6.8.3rc1",
+ "├── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── f [required: >=3.0.0, installed: 3.1]",
+ " └── b [required: >=2.1.0, installed: 2.3.1]",
+ " └── d [required: >=2.30,<2.42, installed: 2.35]",
+ " └── e [required: >=0.9.0, installed: 0.12.1]",
+ ],
),
(
False,
True,
+ True,
[
- 'e==0.12.1',
- ' - c==5.10.0 [requires: e>=0.12.1]',
- ' - a==3.4.0 [requires: c>=5.7.1]',
- ' - d==2.35 [requires: e>=0.9.0]',
- ' - b==2.3.1 [requires: d>=2.30,<2.42]',
- ' - a==3.4.0 [requires: b>=2.0.0]',
- ' - f==3.1 [requires: b>=2.1.0]',
- ' - g==6.8.3rc1 [requires: f>=3.0.0]',
- ' - c==5.10.0 [requires: d>=2.30]',
- ' - a==3.4.0 [requires: c>=5.7.1]',
- ' - g==6.8.3rc1 [requires: e>=0.9.0]',
- ]
- )
- ]
+ "e==0.12.1",
+ "├── c==5.10.0 [requires: e>=0.12.1]",
+ "│ └── a==3.4.0 [requires: c>=5.7.1]",
+ "├── d==2.35 [requires: e>=0.9.0]",
+ "│ ├── b==2.3.1 [requires: d>=2.30,<2.42]",
+ "│ │ ├── a==3.4.0 [requires: b>=2.0.0]",
+ "│ │ └── f==3.1 [requires: b>=2.1.0]",
+ "│ │ └── g==6.8.3rc1 [requires: f>=3.0.0]",
+ "│ └── c==5.10.0 [requires: d>=2.30]",
+ "│ └── a==3.4.0 [requires: c>=5.7.1]",
+ "└── g==6.8.3rc1 [requires: e>=0.9.0]",
+ ],
+ ),
+ (
+ True,
+ False,
+ False,
+ [
+ "a==3.4.0",
+ " - b [required: >=2.0.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - c [required: >=5.7.1, installed: 5.10.0]",
+ " - d [required: >=2.30, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - e [required: >=0.12.1, installed: 0.12.1]",
+ "b==2.3.1",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ "c==5.10.0",
+ " - d [required: >=2.30, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - e [required: >=0.12.1, installed: 0.12.1]",
+ "d==2.35",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ "e==0.12.1",
+ "f==3.1",
+ " - b [required: >=2.1.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ "g==6.8.3rc1",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - f [required: >=3.0.0, installed: 3.1]",
+ " - b [required: >=2.1.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ ],
+ ),
+ (
+ True,
+ True,
+ False,
+ [
+ "a==3.4.0",
+ "b==2.3.1",
+ " - a==3.4.0 [requires: b>=2.0.0]",
+ " - f==3.1 [requires: b>=2.1.0]",
+ " - g==6.8.3rc1 [requires: f>=3.0.0]",
+ "c==5.10.0",
+ " - a==3.4.0 [requires: c>=5.7.1]",
+ "d==2.35",
+ " - b==2.3.1 [requires: d>=2.30,<2.42]",
+ " - a==3.4.0 [requires: b>=2.0.0]",
+ " - f==3.1 [requires: b>=2.1.0]",
+ " - g==6.8.3rc1 [requires: f>=3.0.0]",
+ " - c==5.10.0 [requires: d>=2.30]",
+ " - a==3.4.0 [requires: c>=5.7.1]",
+ "e==0.12.1",
+ " - c==5.10.0 [requires: e>=0.12.1]",
+ " - a==3.4.0 [requires: c>=5.7.1]",
+ " - d==2.35 [requires: e>=0.9.0]",
+ " - b==2.3.1 [requires: d>=2.30,<2.42]",
+ " - a==3.4.0 [requires: b>=2.0.0]",
+ " - f==3.1 [requires: b>=2.1.0]",
+ " - g==6.8.3rc1 [requires: f>=3.0.0]",
+ " - c==5.10.0 [requires: d>=2.30]",
+ " - a==3.4.0 [requires: c>=5.7.1]",
+ " - g==6.8.3rc1 [requires: e>=0.9.0]",
+ "f==3.1",
+ " - g==6.8.3rc1 [requires: f>=3.0.0]",
+ "g==6.8.3rc1",
+ ],
+ ),
+ (
+ False,
+ False,
+ False,
+ [
+ "a==3.4.0",
+ " - b [required: >=2.0.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - c [required: >=5.7.1, installed: 5.10.0]",
+ " - d [required: >=2.30, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - e [required: >=0.12.1, installed: 0.12.1]",
+ "g==6.8.3rc1",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - f [required: >=3.0.0, installed: 3.1]",
+ " - b [required: >=2.1.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ ],
+ ),
+ (
+ False,
+ True,
+ False,
+ [
+ "e==0.12.1",
+ " - c==5.10.0 [requires: e>=0.12.1]",
+ " - a==3.4.0 [requires: c>=5.7.1]",
+ " - d==2.35 [requires: e>=0.9.0]",
+ " - b==2.3.1 [requires: d>=2.30,<2.42]",
+ " - a==3.4.0 [requires: b>=2.0.0]",
+ " - f==3.1 [requires: b>=2.1.0]",
+ " - g==6.8.3rc1 [requires: f>=3.0.0]",
+ " - c==5.10.0 [requires: d>=2.30]",
+ " - a==3.4.0 [requires: c>=5.7.1]",
+ " - g==6.8.3rc1 [requires: e>=0.9.0]",
+ ],
+ ),
+ ],
)
-def test_render_text(capsys, list_all, reverse, expected_output):
+def test_render_text(capsys, list_all, reverse, unicode, expected_output):
tree = t.reverse() if reverse else t
- p.render_text(tree, list_all=list_all, frozen=False)
- captured = capsys.readouterr()
- assert '\n'.join(expected_output).strip() == captured.out.strip()
+ encoding = "utf-8" if unicode else "ascii"
+ with mock.patch("sys.stdout", MockStdout(encoding)):
+ p.render_text(tree, float("inf"), list_all=list_all, frozen=False)
+ captured = capsys.readouterr()
+ assert "\n".join(expected_output).strip() == captured.out.strip()
+
+
+@pytest.mark.parametrize(
+ ("unicode", "level", "expected_output"),
+ [
+ (
+ True,
+ 0,
+ [
+ "a==3.4.0",
+ "b==2.3.1",
+ "c==5.10.0",
+ "d==2.35",
+ "e==0.12.1",
+ "f==3.1",
+ "g==6.8.3rc1",
+ ],
+ ),
+ (
+ False,
+ 0,
+ [
+ "a==3.4.0",
+ "b==2.3.1",
+ "c==5.10.0",
+ "d==2.35",
+ "e==0.12.1",
+ "f==3.1",
+ "g==6.8.3rc1",
+ ],
+ ),
+ (
+ True,
+ 2,
+ [
+ "a==3.4.0",
+ "├── b [required: >=2.0.0, installed: 2.3.1]",
+ "│ └── d [required: >=2.30,<2.42, installed: 2.35]",
+ "└── c [required: >=5.7.1, installed: 5.10.0]",
+ " ├── d [required: >=2.30, installed: 2.35]",
+ " └── e [required: >=0.12.1, installed: 0.12.1]",
+ "b==2.3.1",
+ "└── d [required: >=2.30,<2.42, installed: 2.35]",
+ " └── e [required: >=0.9.0, installed: 0.12.1]",
+ "c==5.10.0",
+ "├── d [required: >=2.30, installed: 2.35]",
+ "│ └── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── e [required: >=0.12.1, installed: 0.12.1]",
+ "d==2.35",
+ "└── e [required: >=0.9.0, installed: 0.12.1]",
+ "e==0.12.1",
+ "f==3.1",
+ "└── b [required: >=2.1.0, installed: 2.3.1]",
+ " └── d [required: >=2.30,<2.42, installed: 2.35]",
+ "g==6.8.3rc1",
+ "├── e [required: >=0.9.0, installed: 0.12.1]",
+ "└── f [required: >=3.0.0, installed: 3.1]",
+ " └── b [required: >=2.1.0, installed: 2.3.1]",
+ ],
+ ),
+ (
+ False,
+ 2,
+ [
+ "a==3.4.0",
+ " - b [required: >=2.0.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - c [required: >=5.7.1, installed: 5.10.0]",
+ " - d [required: >=2.30, installed: 2.35]",
+ " - e [required: >=0.12.1, installed: 0.12.1]",
+ "b==2.3.1",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ "c==5.10.0",
+ " - d [required: >=2.30, installed: 2.35]",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - e [required: >=0.12.1, installed: 0.12.1]",
+ "d==2.35",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ "e==0.12.1",
+ "f==3.1",
+ " - b [required: >=2.1.0, installed: 2.3.1]",
+ " - d [required: >=2.30,<2.42, installed: 2.35]",
+ "g==6.8.3rc1",
+ " - e [required: >=0.9.0, installed: 0.12.1]",
+ " - f [required: >=3.0.0, installed: 3.1]",
+ " - b [required: >=2.1.0, installed: 2.3.1]",
+ ],
+ ),
+ ],
+)
+def test_render_text_given_depth(capsys, unicode, level, expected_output):
+ encoding = "utf-8" if unicode else "ascii"
+ with mock.patch("sys.stdout", MockStdout(encoding)):
+ p.render_text(t, level)
+ captured = capsys.readouterr()
+ assert "\n".join(expected_output).strip() == captured.out.strip()
# Tests for graph outputs
+
+def randomized_dag_copy(t):
+ """Returns a copy of the package tree fixture with dependencies in randomized order."""
+ # Extract the dependency graph from the package tree and randomize it.
+ randomized_graph = {}
+ randomized_nodes = list(t._obj.keys())
+ random.shuffle(randomized_nodes)
+ for node in randomized_nodes:
+ edges = t._obj[node]
+ random.shuffle(edges)
+ randomized_graph[node] = edges
+ assert set(randomized_graph) == set(t._obj)
+
+ # Create a randomized package tree.
+ randomized_dag = p.PackageDAG(randomized_graph)
+ assert len(t) == len(randomized_dag)
+ return randomized_dag
+
+
+def test_render_mermaid():
+ """Check both the sorted and randomized package tree produces the same sorted Mermaid output.
+
+ Rendering a reverse dependency tree should produce the same set of nodes. Edges should have
+ the same version spec label, but be resorted after swapping node positions.
+
+ `See how this renders
+ <https://mermaid.ink/img/pako:eNp9kcluwjAURX_FeutgeUhCErWs-IN21boL4yFEzYAyqKWIf-9LCISyqFf2ufe-QT6BaayDDHzZfJm9bnvyulU1wWNK3XVb50lVdF1R56Tr2-bTrazu0NfqY0aii1O_K9BK1ZKGlCn4uNAd0h1SQSXlN2qQGqQR5ezObBHbizm6QYfQIWSUi7sSHrGf2i0sR5Yji2lCZWsWQZPViijYPAs69cPnhuwetIiux1qTZubpl5xkwZOgoZgNdl7karhON4nuQRzTf3N2yaW3geaYX2L8cdj8n9xNk3dLegigcm2lC4v_exqdCvq9q5yCDK_WeT2UvQJVn9Gqh755OdYGsr4dXADDwerebQudt7qCzOuyQ3rQ9VvTVFcTPiE7wTdkgkVUSiHjlLOERUkYB3AcMeXrhKUp53GYcJ6KcwA_UwVGo_MvVqym_A?type=png)](https://mermaid.live/edit#pako:eNp9kcluwjAURX_FeutgeUhCErWs-IN21boL4yFEzYAyqKWIf-9LCISyqFf2ufe-QT6BaayDDHzZfJm9bnvyulU1wWNK3XVb50lVdF1R56Tr2-bTrazu0NfqY0aii1O_K9BK1ZKGlCn4uNAd0h1SQSXlN2qQGqQR5ezObBHbizm6QYfQIWSUi7sSHrGf2i0sR5Yji2lCZWsWQZPViijYPAs69cPnhuwetIiux1qTZubpl5xkwZOgoZgNdl7karhON4nuQRzTf3N2yaW3geaYX2L8cdj8n9xNk3dLegigcm2lC4v_exqdCvq9q5yCDK_WeT2UvQJVn9Gqh755OdYGsr4dXADDwerebQudt7qCzOuyQ3rQ9VvTVFcTPiE7wTdkgkVUSiHjlLOERUkYB3AcMeXrhKUp53GYcJ6KcwA_UwVGo_MvVqym_A>`_.
+ """
+
+ nodes = dedent(
+ """\
+ flowchart TD
+ classDef missing stroke-dasharray: 5
+ a["a\\n3.4.0"]
+ b["b\\n2.3.1"]
+ c["c\\n5.10.0"]
+ d["d\\n2.35"]
+ e["e\\n0.12.1"]
+ f["f\\n3.1"]
+ g["g\\n6.8.3rc1"]
+ """
+ )
+ dependency_edges = indent(
+ dedent(
+ """\
+ a -- ">=2.0.0" --> b
+ a -- ">=5.7.1" --> c
+ b -- ">=2.30,<2.42" --> d
+ c -- ">=0.12.1" --> e
+ c -- ">=2.30" --> d
+ d -- ">=0.9.0" --> e
+ f -- ">=2.1.0" --> b
+ g -- ">=0.9.0" --> e
+ g -- ">=3.0.0" --> f
+ """
+ ),
+ " " * 4,
+ ).rstrip()
+ reverse_dependency_edges = indent(
+ dedent(
+ """\
+ b -- ">=2.0.0" --> a
+ b -- ">=2.1.0" --> f
+ c -- ">=5.7.1" --> a
+ d -- ">=2.30" --> c
+ d -- ">=2.30,<2.42" --> b
+ e -- ">=0.12.1" --> c
+ e -- ">=0.9.0" --> d
+ e -- ">=0.9.0" --> g
+ f -- ">=3.0.0" --> g
+ """
+ ),
+ " " * 4,
+ ).rstrip()
+
+ for package_tree in (t, randomized_dag_copy(t)):
+ output = p.render_mermaid(package_tree)
+ assert output.rstrip() == nodes + dependency_edges
+ reversed_output = p.render_mermaid(package_tree.reverse())
+ assert reversed_output.rstrip() == nodes + reverse_dependency_edges
+
+
+def test_mermaid_reserved_ids():
+ package_tree = mock_package_dag(
+ {
+ ("click", "3.4.0"): [("click-extra", [(">=", "2.0.0")])],
+ }
+ )
+ output = p.render_mermaid(package_tree)
+ assert output == dedent(
+ """\
+ flowchart TD
+ classDef missing stroke-dasharray: 5
+ click-extra["click-extra\\n(missing)"]:::missing
+ click_0["click\\n3.4.0"]
+ click_0 -.-> click-extra
+ """
+ )
+
+
+def test_render_dot(capsys):
+ # Check both the sorted and randomized package tree produces the same sorted
+ # graphviz output.
+ for package_tree in (t, randomized_dag_copy(t)):
+ output = p.dump_graphviz(package_tree, output_format="dot")
+ p.print_graphviz(output)
+ out, _ = capsys.readouterr()
+ assert out == dedent(
+ """\
+ digraph {
+ \ta -> b [label=">=2.0.0"]
+ \ta -> c [label=">=5.7.1"]
+ \ta [label="a\\n3.4.0"]
+ \tb -> d [label=">=2.30,<2.42"]
+ \tb [label="b\\n2.3.1"]
+ \tc -> d [label=">=2.30"]
+ \tc -> e [label=">=0.12.1"]
+ \tc [label="c\\n5.10.0"]
+ \td -> e [label=">=0.9.0"]
+ \td [label="d\\n2.35"]
+ \te [label="e\\n0.12.1"]
+ \tf -> b [label=">=2.1.0"]
+ \tf [label="f\\n3.1"]
+ \tg -> e [label=">=0.9.0"]
+ \tg -> f [label=">=3.0.0"]
+ \tg [label="g\\n6.8.3rc1"]
+ }
+
+ """
+ )
+
+
def test_render_pdf():
- output = p.dump_graphviz(t, output_format='pdf')
+ output = p.dump_graphviz(t, output_format="pdf")
@contextmanager
def redirect_stdout(new_target):
@@ -361,126 +718,110 @@ def test_render_pdf():
with NamedTemporaryFile(delete=True) as f:
with redirect_stdout(f):
p.print_graphviz(output)
- rf = open(f.name, 'rb')
- assert b'%PDF' == rf.read()[:4]
+ rf = open(f.name, "rb")
+ assert b"%PDF" == rf.read()[:4]
# @NOTE: rf is not closed to avoid "bad filedescriptor" error
def test_render_svg(capsys):
- output = p.dump_graphviz(t, output_format='svg')
+ output = p.dump_graphviz(t, output_format="svg")
p.print_graphviz(output)
out, _ = capsys.readouterr()
- assert out.startswith('<?xml')
- assert '<svg' in out
- assert out.strip().endswith('</svg>')
+ assert out.startswith("<?xml")
+ assert "<svg" in out
+ assert out.strip().endswith("</svg>")
# Test for conflicting deps
+
@pytest.mark.parametrize(
- "mpkgs,expected_keys,expected_output",
+ ("mpkgs", "expected_keys", "expected_output"),
[
(
- {
- ('a', '1.0.1'): [('b', [('>=', '2.3.0')])],
- ('b', '1.9.1'): []
- },
- {'a': ['b']},
+ {("a", "1.0.1"): [("b", [(">=", "2.3.0")])], ("b", "1.9.1"): []},
+ {"a": ["b"]},
[
- 'Warning!!! Possibly conflicting dependencies found:',
- '* a==1.0.1',
- ' - b [required: >=2.3.0, installed: 1.9.1]'
- ]
+ "Warning!!! Possibly conflicting dependencies found:",
+ "* a==1.0.1",
+ " - b [required: >=2.3.0, installed: 1.9.1]",
+ ],
),
(
- {
- ('a', '1.0.1'): [('c', [('>=', '9.4.1')])],
- ('b', '2.3.0'): [('c', [('>=', '7.0')])],
- ('c', '8.0.1'): []
- },
- {'a': ['c']},
+ {("a", "1.0.1"): [("c", [(">=", "9.4.1")])], ("b", "2.3.0"): [("c", [(">=", "7.0")])], ("c", "8.0.1"): []},
+ {"a": ["c"]},
[
- 'Warning!!! Possibly conflicting dependencies found:',
- '* a==1.0.1',
- ' - c [required: >=9.4.1, installed: 8.0.1]'
- ]
+ "Warning!!! Possibly conflicting dependencies found:",
+ "* a==1.0.1",
+ " - c [required: >=9.4.1, installed: 8.0.1]",
+ ],
),
(
- {
- ('a', '1.0.1'): [('c', [('>=', '9.4.1')])],
- ('b', '2.3.0'): [('c', [('>=', '9.4.0')])]
- },
- {'a': ['c'], 'b': ['c']},
+ {("a", "1.0.1"): [("c", [(">=", "9.4.1")])], ("b", "2.3.0"): [("c", [(">=", "9.4.0")])]},
+ {"a": ["c"], "b": ["c"]},
[
- 'Warning!!! Possibly conflicting dependencies found:',
- '* a==1.0.1',
- ' - c [required: >=9.4.1, installed: ?]',
- '* b==2.3.0',
- ' - c [required: >=9.4.0, installed: ?]'
- ]
+ "Warning!!! Possibly conflicting dependencies found:",
+ "* a==1.0.1",
+ " - c [required: >=9.4.1, installed: ?]",
+ "* b==2.3.0",
+ " - c [required: >=9.4.0, installed: ?]",
+ ],
),
(
- {
- ('a', '1.0.1'): [('c', [('>=', '9.4.1')])],
- ('b', '2.3.0'): [('c', [('>=', '7.0')])],
- ('c', '9.4.1'): []
- },
+ {("a", "1.0.1"): [("c", [(">=", "9.4.1")])], ("b", "2.3.0"): [("c", [(">=", "7.0")])], ("c", "9.4.1"): []},
{},
- []
- )
- ]
+ [],
+ ),
+ ],
)
def test_conflicting_deps(capsys, mpkgs, expected_keys, expected_output):
- tree = mock_PackageDAG(mpkgs)
+ tree = mock_package_dag(mpkgs)
result = p.conflicting_deps(tree)
- result_keys = {k.key: [v.key for v in vs]
- for k, vs in result.items()}
+ result_keys = {k.key: [v.key for v in vs] for k, vs in result.items()}
assert expected_keys == result_keys
p.render_conflicts_text(result)
captured = capsys.readouterr()
- assert '\n'.join(expected_output).strip() == captured.err.strip()
+ assert "\n".join(expected_output).strip() == captured.err.strip()
# Tests for cyclic deps
+
@pytest.mark.parametrize(
- "mpkgs,expected_keys,expected_output",
+ ("mpkgs", "expected_keys", "expected_output"),
[
(
{
- ('a', '1.0.1'): [('b', [('>=', '2.0.0')])],
- ('b', '2.3.0'): [('a', [('>=', '1.0.1')])],
- ('c', '4.5.0'): [('d', [('==', '2.0')])],
- ('d', '2.0'): []
+ ("a", "1.0.1"): [("b", [(">=", "2.0.0")])],
+ ("b", "2.3.0"): [("a", [(">=", "1.0.1")])],
+ ("c", "4.5.0"): [("d", [("==", "2.0")])],
+ ("d", "2.0"): [],
},
- [('a', 'b', 'a'), ('b', 'a', 'b')],
- [
- 'Warning!! Cyclic dependencies found:',
- '* b => a => b',
- '* a => b => a'
- ]
+ [("a", "b", "a"), ("b", "a", "b")],
+ ["Warning!! Cyclic dependencies found:", "* b => a => b", "* a => b => a"],
),
- ( # if a dependency isn't installed, cannot verify cycles
+ ( # if a dependency isn't installed, cannot verify cycles
{
- ('a', '1.0.1'): [('b', [('>=', '2.0.0')])],
+ ("a", "1.0.1"): [("b", [(">=", "2.0.0")])],
},
[],
- [] # no output expected
- )
- ]
+ [], # no output expected
+ ),
+ ],
)
def test_cyclic_deps(capsys, mpkgs, expected_keys, expected_output):
- tree = mock_PackageDAG(mpkgs)
+ tree = mock_package_dag(mpkgs)
result = p.cyclic_deps(tree)
result_keys = [(a.key, b.key, c.key) for (a, b, c) in result]
assert sorted(expected_keys) == sorted(result_keys)
p.render_cycles_text(result)
captured = capsys.readouterr()
- assert '\n'.join(expected_output).strip() == captured.err.strip()
+ assert "\n".join(expected_output).strip() == captured.err.strip()
# Tests for the argparse parser
+
def test_parser_default():
parser = p.get_parser()
args = parser.parse_args([])
@@ -490,45 +831,74 @@ def test_parser_default():
def test_parser_j():
parser = p.get_parser()
- args = parser.parse_args(['-j'])
+ args = parser.parse_args(["-j"])
assert args.json
assert args.output_format is None
def test_parser_json():
parser = p.get_parser()
- args = parser.parse_args(['--json'])
+ args = parser.parse_args(["--json"])
assert args.json
assert args.output_format is None
def test_parser_json_tree():
parser = p.get_parser()
- args = parser.parse_args(['--json-tree'])
+ args = parser.parse_args(["--json-tree"])
assert args.json_tree
assert not args.json
assert args.output_format is None
+def test_parser_mermaid():
+ parser = p.get_parser()
+ args = parser.parse_args(["--mermaid"])
+ assert args.mermaid
+ assert not args.json
+ assert args.output_format is None
+
+
def test_parser_pdf():
parser = p.get_parser()
- args = parser.parse_args(['--graph-output', 'pdf'])
- assert args.output_format == 'pdf'
+ args = parser.parse_args(["--graph-output", "pdf"])
+ assert args.output_format == "pdf"
assert not args.json
def test_parser_svg():
parser = p.get_parser()
- args = parser.parse_args(['--graph-output', 'svg'])
- assert args.output_format == 'svg'
+ args = parser.parse_args(["--graph-output", "svg"])
+ assert args.output_format == "svg"
assert not args.json
+@pytest.mark.parametrize(
+ ("should_be_error", "depth_arg", "expected_value"),
+ [
+ (True, ["-d", "-1"], None),
+ (True, ["--depth", "string"], None),
+ (False, ["-d", "0"], 0),
+ (False, ["--depth", "8"], 8),
+ (False, [], float("inf")),
+ ],
+)
+def test_parser_depth(should_be_error, depth_arg, expected_value):
+ parser = p.get_parser()
+
+ if should_be_error:
+ with pytest.raises(SystemExit):
+ parser.parse_args(depth_arg)
+ else:
+ args = parser.parse_args(depth_arg)
+ assert args.depth == expected_value
+
+
@pytest.mark.parametrize("args_joined", [True, False])
def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined):
result = virtualenv.cli_run([str(tmp_path), "--activators", ""])
cmd = [sys.executable]
- cmd += ["--python={}".format(result.creator.exe)] if args_joined else ["--python", str(result.creator.exe)]
+ cmd += [f"--python={result.creator.exe}"] if args_joined else ["--python", str(result.creator.exe)]
monkeypatch.setattr(sys, "argv", cmd)
p.main()
out, _ = capfd.readouterr()
@@ -540,6 +910,8 @@ def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined):
expected = {"cffi", "greenlet", "pip", "readline", "setuptools", "wheel"}
else:
raise ValueError(implementation)
+ if sys.version_info >= (3, 12):
+ expected -= {"setuptools", "wheel"}
assert found == expected, out
monkeypatch.setattr(sys, "argv", cmd + ["--graph-output", "something"])
@@ -551,4 +923,7 @@ def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined):
assert err == "graphviz functionality is not supported when querying" " non-host python\n"
-
+def test_guess_version_setuptools():
+ script = Path(__file__).parent / "guess_version_setuptools.py"
+ output = subprocess.check_output([sys.executable, script], text=True)
+ assert output == "?"
diff --git a/tox.ini b/tox.ini
index 62958cb..5b23320 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,26 +1,68 @@
-# http://tox.readthedocs.org/ - sets up and runs the test suite based on a declarative configuration
[tox]
-envlist =
- py39
- py38
- py37
- py36
- py35
- py34
- py27
- pypy3
- pypy2
+requires =
+ tox>=4.2
+env_list =
+ fix
+ py312
+ py311
+ py310
+ py39
+ py38
+ py37
+ readme
+skip_missing_interpreters = true
[testenv]
-description = run test suite under {basepython}
+description = run the unit tests with pytest under {basepython}
+package = wheel
+wheel_build_env = .pkg
+extras =
+ graphviz
+ test
+set_env =
+ COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
+ COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
+ _COVERAGE_SRC = {envsitepackagesdir}/sphinx_argparse_cli
commands =
- pytest {posargs:-vv}
+ python -m pytest {tty:--color=yes} {posargs: \
+ --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}pipdeptree \
+ --cov {toxinidir}{/}tests --cov-fail-under=75 \
+ --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \
+ --cov-report html:{envtmpdir}{/}htmlcov --cov-report xml:{toxworkdir}{/}coverage.{envname}.xml \
+ tests}
+ diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}{/}coverage.{envname}.xml
+
+[testenv:fix]
+description = format the code base to adhere to our styles, and complain about what we cannot do automatically
+skip_install = true
deps =
- graphviz
- pip>=8.0.2
- pytest
- pytest-cov
- virtualenv>=20,<21
- mock;python_version<"3"
-extras =
- graphviz
+ pre-commit>=3.2.2
+commands =
+ pre-commit run --all-files --show-diff-on-failure
+
+[testenv:readme]
+description = check that the long description is valid
+base_python = python3.10
+skip_install = true
+deps =
+ build[virtualenv]>=0.10
+ twine>=4.0.2
+commands =
+ python -m build --sdist --wheel -o {envtmpdir} .
+ twine check {envtmpdir}/*
+
+[testenv:dev]
+description = generate a DEV environment
+package = editable
+commands =
+ python -m pip list --format=columns
+ python -c 'import sys; print(sys.executable)'
+
+[flake8]
+max-complexity = 22
+max-line-length = 120
+noqa-require-code = true
+dictionaries = en_US,python,technical,django
+
+[pep8]
+max-line-length = 120
diff --git a/whitelist.txt b/whitelist.txt
new file mode 100644
index 0000000..2c3764f
--- /dev/null
+++ b/whitelist.txt
@@ -0,0 +1,36 @@
+2nd
+basedistribution
+bytestream
+capfd
+capsys
+cld
+cldn
+copytree
+deque
+distinfodistribution
+dists
+dp1
+dp2
+exe
+filedescriptor
+fileno
+frozenrequirement
+g1
+g2
+g3
+getitem
+getsourcefile
+graphviz
+hacky
+ismodule
+mpkgs
+nk
+pipdeptree
+pkgs
+readouterr
+reqs
+rhs
+svg
+t1
+t2
+virtualenv
More details
Historical runs
- failed: E: pybuild pybuild:128: cannot detect build system, please use --system option or set PYBUILD_SYSTEM env. variable
- failed: E: pybuild pybuild:127: cannot detect build system, please use --system option or set PYBUILD_SYSTEM env. variable
- failed: E: pybuild pybuild:128: cannot detect build system, please use --system option or set PYBUILD_SYSTEM env. variable
- failed: E: pybuild pybuild:127: cannot detect build system, please use --system option or set PYBUILD_SYSTEM env. variable
- build-failed-stage-build: E: pybuild pybuild:127: cannot detect build system, please use --system option or set PYBUILD_SYSTEM env. variable
- no-space-on-device: [Errno 28] No space left on device
- success: Merged new upstream version 2.2.1