Repository: rr-/screeninfo

Issue:
get_monitors returns empty on osx when Enumerator not explicitly provided
tl;dr:
```py
>>> from screeninfo import get_monitors, Enumerator
>>> get_monitors()
[]
>>> get_monitors(Enumerator.OSX)
[Monitor(x=0, y=0, width=1680, height=1050, width_mm=None, height_mm=None, name=None)]
```

On those lines:
https://github.com/rr-/screeninfo/blob/master/screeninfo/screeninfo.py#L29-L33
```py
    for enumerator in Enumerator:
        try:
            return _get_monitors(enumerator)
        except Exception:
            pass
```

Seems some of the previous enumerator is not raising, so it returns before trying OSX, one solution is to also check for empty return on that try/catch

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2dc8643
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# 0.8 (2021-12-04)
+
+- Started tracking changes
+- Switched to poetry
+- Added ignoring of enumerators that work, but return no valid monitors
+- Added `ScreenInfoError` to list of top-level symbols
diff --git a/pyproject.toml b/pyproject.toml
index e87056b..534ceab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "screeninfo"
-version = "0.7"
+version = "0.8"
 description = "Fetch location and size of physical screens."
 authors = ["Marcin Kurczewski <rr-@sakuya.pl>"]
 license = "MIT"
diff --git a/screeninfo/__init__.py b/screeninfo/__init__.py
index ad1968a..2163dd4 100644
--- a/screeninfo/__init__.py
+++ b/screeninfo/__init__.py
@@ -1,2 +1,9 @@
 from .common import Enumerator, Monitor
-from .screeninfo import get_monitors
+from .screeninfo import ScreenInfoError, get_monitors
+
+__all__ = [
+    "Enumerator",
+    "Monitor",
+    "ScreenInfoError",
+    "get_monitors",
+]
diff --git a/screeninfo/screeninfo.py b/screeninfo/screeninfo.py
index 01ec49a..e10bb39 100644
--- a/screeninfo/screeninfo.py
+++ b/screeninfo/screeninfo.py
@@ -13,23 +13,20 @@ ENUMERATOR_MAP = {
 }
 
 
-def _get_monitors(enumerator: Enumerator) -> T.List[Monitor]:
-    return list(ENUMERATOR_MAP[enumerator].enumerate_monitors())
-
-
 def get_monitors(
     name: T.Union[Enumerator, str, None] = None
 ) -> T.List[Monitor]:
     """Returns a list of :class:`Monitor` objects based on active monitors."""
-    enumerator = Enumerator(name) if name is not None else None
+    if name is not None:
+        return list(ENUMERATOR_MAP[Enumerator(name)].enumerate_monitors())
 
-    if enumerator is not None:
-        return _get_monitors(enumerator)
-
-    for enumerator in Enumerator:
+    for enumerator in ENUMERATOR_MAP.keys():
         try:
-            return _get_monitors(enumerator)
-        except Exception:
-            pass
+            monitors = get_monitors(enumerator)
+        except Exception as ex:
+            monitors = []
+
+        if monitors:
+            return monitors
 
     raise ScreenInfoError("No enumerators available")


Test patch:
diff --git a/screeninfo/test_screeninfo.py b/screeninfo/test_screeninfo.py
index a30dfb5..26040ef 100644
--- a/screeninfo/test_screeninfo.py
+++ b/screeninfo/test_screeninfo.py
@@ -1,8 +1,9 @@
 from contextlib import contextmanager
+from unittest import mock
 
 import pytest
 
-from screeninfo import get_monitors
+from screeninfo import Enumerator, Monitor, ScreenInfoError, get_monitors
 
 
 @contextmanager
@@ -13,11 +14,131 @@ def not_raises(exception):
         raise pytest.fail("DID RAISE {0}".format(exception))
 
 
-def test_get_monitors_does_not_raise():
-    with not_raises(Exception):
-        list(get_monitors())
+def test_get_monitors_without_enumerators():
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP", {}
+    ) as mock_get_monitors:
+        with pytest.raises(ScreenInfoError):
+            get_monitors()
 
 
-def test_get_monitors_has_at_least_one_monitor():
-    # GitHub actions have no physical monitors
-    assert len(list(get_monitors())) >= 0
+def test_get_monitors_handles_faulty_enumerator():
+    enumerator = mock.Mock(
+        enumerate_monitors=mock.Mock(side_effect=ImportError)
+    )
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {Enumerator.Windows: enumerator},
+    ):
+        with pytest.raises(ScreenInfoError):
+            get_monitors()
+        enumerator.enumerate_monitors.assert_called_once()
+
+
+def test_get_monitors_ignores_enumerator_that_produces_no_results():
+    enumerator = mock.Mock(enumerate_monitors=mock.Mock(return_value=[]))
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {Enumerator.Windows: enumerator},
+    ):
+        with pytest.raises(ScreenInfoError):
+            get_monitors()
+        enumerator.enumerate_monitors.assert_called_once()
+
+
+def test_get_monitors_uses_working_enumerator():
+    enumerator = mock.Mock(
+        enumerate_monitors=mock.Mock(
+            return_value=[Monitor(x=0, y=0, width=800, height=600)]
+        )
+    )
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {Enumerator.Windows: enumerator},
+    ) as mock_get_monitors:
+        monitors = get_monitors()
+        assert len(monitors) == 1
+        assert monitors[0].x == 0
+        assert monitors[0].y == 0
+        assert monitors[0].width == 800
+        assert monitors[0].height == 600
+        enumerator.enumerate_monitors.assert_called_once()
+
+
+def test_get_monitors_uses_first_working_enumerator():
+    enumerator1 = mock.Mock(
+        enumerate_monitors=mock.Mock(side_effect=ImportError)
+    )
+    enumerator2 = mock.Mock(
+        enumerate_monitors=mock.Mock(
+            return_value=[Monitor(x=0, y=0, width=800, height=600)]
+        )
+    )
+    enumerator3 = mock.Mock(
+        enumerate_monitors=mock.Mock(side_effect=ImportError)
+    )
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {
+            Enumerator.Windows: enumerator1,
+            Enumerator.Xinerama: enumerator2,
+            Enumerator.DRM: enumerator3,
+        },
+    ) as mock_get_monitors:
+        monitors = get_monitors()
+        assert len(monitors) == 1
+        assert monitors[0].x == 0
+        assert monitors[0].y == 0
+        assert monitors[0].width == 800
+        assert monitors[0].height == 600
+        enumerator1.enumerate_monitors.assert_called_once()
+        enumerator2.enumerate_monitors.assert_called_once()
+        enumerator3.enumerate_monitors.assert_not_called()
+
+
+@pytest.mark.parametrize("param", ["windows", Enumerator.Windows])
+def test_get_monitors_with_concrete_enumerator(param):
+    enumerator = mock.Mock(
+        enumerate_monitors=mock.Mock(
+            return_value=[Monitor(x=0, y=0, width=800, height=600)]
+        )
+    )
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {Enumerator.Windows: enumerator},
+    ) as mock_get_monitors:
+        monitors = get_monitors(param)
+        assert len(monitors) == 1
+        assert monitors[0].x == 0
+        assert monitors[0].y == 0
+        assert monitors[0].width == 800
+        assert monitors[0].height == 600
+        enumerator.enumerate_monitors.assert_called_once()
+
+
+def test_get_monitors_with_invalid_enumerator():
+    enumerator = mock.Mock(
+        enumerate_monitors=mock.Mock(
+            return_value=[Monitor(x=0, y=0, width=800, height=600)]
+        )
+    )
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {Enumerator.Windows: enumerator},
+    ) as mock_get_monitors:
+        with pytest.raises(ValueError):
+            monitors = get_monitors("Invalid")
+
+
+def test_get_monitors_with_unlisted_enumerator():
+    enumerator = mock.Mock(
+        enumerate_monitors=mock.Mock(
+            return_value=[Monitor(x=0, y=0, width=800, height=600)]
+        )
+    )
+    with mock.patch(
+        "screeninfo.screeninfo.ENUMERATOR_MAP",
+        {Enumerator.Windows: enumerator},
+    ) as mock_get_monitors:
+        with pytest.raises(KeyError):
+            monitors = get_monitors(Enumerator.OSX)