Repository: tox-dev/pipdeptree (python)

Issue:
erroneous warnings with development versions of packages installed
### What pipdeptree version are you using?

2.23.0 or 2.23.1.dev6+g02f4f26

### Are you running pipdeptree in a virtual environment?

Yes

### Describe the problem

Development versions are not being correctly parsed.  In a (probably ill advised) venv where most things are installed from the development branches I am seeing many warnings like:

```
@ pipdeptree --reverse -p six
Warning!!! Possibly conflicting dependencies found:
* aiohttp==4.0.0a2.dev0
 - frozenlist [required: >=1.1.1, installed: 1.4.2.dev0]
 - multidict [required: >=4.5,<7.0, installed: 6.0.6.dev0]
 - yarl [required: >=1.0,<2.0, installed: 1.9.5.dev0]
* aiohttp-jinja2==1.6
 - aiohttp [required: >=3.9.0, installed: 4.0.0a2.dev0]
* aiosignal==1.3.1
 - frozenlist [required: >=1.1.0, installed: 1.4.2.dev0]
* area-detector-handlers==0.0.10.post2.dev0+gf58ca85
 - pandas [required: Any, installed: 3.0.0.dev0+1025.gb4d40a2bf6]
* argon2-cffi-bindings==21.2.0
 - cffi [required: >=1.0.1, installed: 1.17.0.dev0]
* arrow==1.3.0
 - python-dateutil [required: >=2.7.0, installed: 2.9.0.post1.dev1+g0353b78]
* awkward==2.6.6
 - numpy [required: >=1.18.0, installed: 2.1.0.dev0]
* awkward-cpp==35
 - numpy [required: >=1.18.0, installed: 2.1.0.dev0]
* blosc2==3.0.0b1
 - numpy [required: >=1.23.0, installed: 2.1.0.dev0]
 - msgpack [required: Any, installed: 1.1.0rc1]
 - numexpr [required: Any, installed: 2.10.2.dev0]
[...it goes on for a couple of screens...]
```


Pull request description:
Allow pre-releases when detecting version conflicts
Resolves #393.

This patch also removes unnecessary magic mock values from test_package.py.

Patch:
diff --git a/src/pipdeptree/_models/package.py b/src/pipdeptree/_models/package.py
index af98d60..9d40a44 100644
--- a/src/pipdeptree/_models/package.py
+++ b/src/pipdeptree/_models/package.py
@@ -232,7 +232,7 @@ class ReqPackage(Package):
         if self.is_missing:
             return True
 
-        return self.installed_version not in self._obj.specifier
+        return not self._obj.specifier.contains(self.installed_version, prereleases=True)
 
     @property
     def is_missing(self) -> bool:


Test patch:
diff --git a/tests/_models/test_package.py b/tests/_models/test_package.py
index 6a62d4c..6ad7721 100644
--- a/tests/_models/test_package.py
+++ b/tests/_models/test_package.py
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, Mock
 
 import pytest
+from packaging.specifiers import SpecifierSet
 
 from pipdeptree._models import DistPackage, ReqPackage
 from pipdeptree._models.package import Package
@@ -169,7 +170,7 @@ def test_dist_package_key_pep503_normalized() -> None:
 
 
 def test_req_package_key_pep503_normalized() -> None:
-    bar_req = MagicMock(version="4.1.0", specifier=[">=4.0"])
+    bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar.bar-bar-bar"
     rp = ReqPackage(bar_req)
     assert rp.key == "bar-bar-bar-bar"
@@ -177,7 +178,7 @@ def test_req_package_key_pep503_normalized() -> None:
 
 def test_req_package_render_as_root() -> None:
     bar = Mock(metadata={"Name": "bar"}, version="4.1.0")
-    bar_req = MagicMock(version="4.1.0", specifier=[">=4.0"])
+    bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
     is_frozen = False
@@ -189,7 +190,7 @@ def test_req_package_render_as_root_with_frozen() -> None:
     bar = Mock(metadata={"Name": "bar"}, version="4.1.0")
     bar.read_text = Mock(return_value=json_text)
     d = DistPackage(bar)
-    bar_req = MagicMock(version="4.1.0", specifier=[">=4.0"])
+    bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=d)
     is_frozen = True
@@ -199,16 +200,26 @@ def test_req_package_render_as_root_with_frozen() -> None:
 
 def test_req_package_render_as_branch() -> None:
     bar = Mock(metadata={"Name": "bar"}, version="4.1.0")
-    bar_req = MagicMock(version="4.1.0", specifier=[">=4.0"])
+    bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
     is_frozen = False
     assert rp.render_as_branch(frozen=is_frozen) == "bar [required: >=4.0, installed: 4.1.0]"
 
 
+def test_req_package_is_conflicting_handle_dev_versions() -> None:
+    # ensure that we can handle development versions when detecting conflicts
+    # see https://github.com/tox-dev/pipdeptree/issues/393
+    bar = Mock(metadata={"Name": "bar"}, version="1.2.3.dev0")
+    bar_req = MagicMock(specifier=SpecifierSet(">1.2.0"))
+    bar_req.name = "bar"
+    rp = ReqPackage(bar_req, dist=bar)
+    assert not rp.is_conflicting()
+
+
 def test_req_package_as_dict() -> None:
     bar = Mock(metadata={"Name": "bar"}, version="4.1.0")
-    bar_req = MagicMock(version="4.1.0", specifier=[">=4.0"])
+    bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
     result = rp.as_dict()
@@ -218,7 +229,7 @@ def test_req_package_as_dict() -> None:
 
 def test_req_package_as_dict_with_no_version_spec() -> None:
     bar = Mock(key="bar", version="4.1.0")
-    bar_req = MagicMock(version="4.1.0", specifier=[])
+    bar_req = MagicMock(specifier=[])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
     result = rp.as_dict()