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

Full run details

Historical runs