Repository: tox-dev/pipdeptree (python)

Issue:
AssertionError with -u option
windows 10
Python 3.10.2
pip 21.3.1
pipdeptree 2.2.1

Executing `pipdeptree -u` gives the following:
```
C:\Programs\Python\lib\site-packages\_distutils_hack\__init__.py:17: UserWarning: Distutils was imported before Setuptools, but importing Setuptools also replaces the `distutils` module in `sys.modules`. This may lead to undesirable behaviors or errors. To avoid these issues, avoid using distutils directly, ensure that setuptools is installed in the traditional way (e.g. not an editable install), and/or make sure that setuptools is always imported before distutils.
  warnings.warn(
C:\Programs\Python\lib\site-packages\_distutils_hack\__init__.py:30: UserWarning: Setuptools is replacing distutils.

  warnings.warn("Setuptools is replacing distutils.")
Traceback (most recent call last):
  File "C:\Programs\Python\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Programs\Python\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Programs\Python\Scripts\pipdeptree.exe\__main__.py", line 7, in <module>
  File "C:\Programs\Python\lib\site-packages\pipdeptree.py", line 888, in main
    conflicts = conflicting_deps(tree)
  File "C:\Programs\Python\lib\site-packages\pipdeptree.py", line 689, in conflicting_deps
    if req.is_conflicting():
  File "C:\Programs\Python\lib\site-packages\pipdeptree.py", line 231, in is_conflicting
    if self.installed_version == self.UNKNOWN_VERSION:
  File "C:\Programs\Python\lib\site-packages\pipdeptree.py", line 221, in installed_version
    return guess_version(self.key, self.UNKNOWN_VERSION)
  File "C:\Programs\Python\lib\site-packages\pipdeptree.py", line 64, in guess_version
    m = import_module(pkg_key)
  File "C:\Programs\Python\lib\importlib\__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "C:\Programs\Python\lib\site-packages\setuptools\__init__.py", line 8, in <module>
    import _distutils_hack.override  # noqa: F401
  File "C:\Programs\Python\lib\site-packages\_distutils_hack\override.py", line 1, in <module>
    __import__('_distutils_hack').do_override()
  File "C:\Programs\Python\lib\site-packages\_distutils_hack\__init__.py", line 71, in do_override
    ensure_local_distutils()
  File "C:\Programs\Python\lib\site-packages\_distutils_hack\__init__.py", line 59, in ensure_local_distutils
    assert '_distutils' in core.__file__, core.__file__
AssertionError: C:\Programs\Python\lib\distutils\core.py
```
Thanks

Pull request description:
Use importlib.metadata for guess_version instead of importing the module
Fixes #162 

The current approach tries to import the package and look at `package.__version__`. This is not exactly reliable and also causes Assertion Error in the case where the packages is `setuptools` (because pipdeptree imports `pip` and `setuptools` can't be imported after `pip` because of weird hacks in setuptools). The better and simpler approach is to use `importlib.metadata.version`.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index cd945dd..23fd608 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 2.3.1
+
+- Use `importlib.metadata` to guess version of package before fallback to `pkg.__version__`.
+
 ## 2.3.0
 
 - Move to a package layout
diff --git a/pyproject.toml b/pyproject.toml
index d3862a2..fd5f2d6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,7 +23,7 @@ optional-dependencies.test = [
   "pytest>=7.1.3",
   "pytest-cov>=3",
   "pytest-mock>=3.8.2",
-  "virtualenv<21,>=20.16.4",
+  "virtualenv<21,>=20.16.5",
 ]
 optional-dependencies.graphviz = [
   "graphviz>=0.20.1",
diff --git a/src/pipdeptree/__init__.py b/src/pipdeptree/__init__.py
index 9040088..45d13b4 100644
--- a/src/pipdeptree/__init__.py
+++ b/src/pipdeptree/__init__.py
@@ -41,6 +41,17 @@ def guess_version(pkg_key, default="?"):
     :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:


Test patch:
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/test_pipdeptree.py b/tests/test_pipdeptree.py
index 6bde2fd..27c77dc 100644
--- a/tests/test_pipdeptree.py
+++ b/tests/test_pipdeptree.py
@@ -1,7 +1,9 @@
 import platform
+import subprocess
 import sys
 from contextlib import contextmanager
 from itertools import chain
+from pathlib import Path
 from tempfile import NamedTemporaryFile
 
 try:
@@ -488,3 +490,9 @@ def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined):
     assert context.value.code == 1
     assert not out
     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 == "?"