Repository: rm-hull/luma.core

Issue:
Selectively cleanup RPi.GPIO pins
It's possible to pass an array of pins to `GPIO.cleanup(active_pins)`, e.g. `active_pins = [4, 5, 6]`. We should make sure `GPIO.cleanup()` only ever cleans up the active pins used by a luma library, not disabling other GPIO pins in the process.

See https://github.com/rm-hull/luma.core/blob/2.1.0/luma/core/interface/serial.py#L253

Discussion:
matemaciek: This fixes https://github.com/rm-hull/luma.oled/issues/325. Tests fixes are coming (-:
rm-hull: This a great improvement! Thank you. There are some build failures though, eg https://github.com/rm-hull/luma.core/pull/237/checks?check_run_id=2051211855#step:7:441

I can have a look at fixing if you can’t fathom what the issue is.
rm-hull: Please also add yourself to the list of contributor in  CONTRIBUTING.rst
matemaciek: I see that `cleanup` accepts also the list of ports, I'll change this PR to use it, as it will make both code and tests simpler. I'll also try to fix it for parallel connection.

Patch:
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 5381a26..1339d20 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -35,3 +35,4 @@ Contributors
 * @jakiee3y
 * Matthew Lovell (@mattblovell)
 * Tiago F. Pimentel (@antropoiese)
+* Maciej Sokolowski (@matemaciek)
diff --git a/luma/core/interface/parallel.py b/luma/core/interface/parallel.py
index 1b8a99d..dcaab7d 100644
--- a/luma/core/interface/parallel.py
+++ b/luma/core/interface/parallel.py
@@ -113,4 +113,4 @@ class bitbang_6800(object):
         Clean up GPIO resources if managed.
         """
         if self._managed:
-            self._gpio.cleanup()
+            self._gpio.cleanup([self._RS, self._E, *self._PINS])
diff --git a/luma/core/interface/serial.py b/luma/core/interface/serial.py
index 09ce200..a4d1532 100644
--- a/luma/core/interface/serial.py
+++ b/luma/core/interface/serial.py
@@ -250,7 +250,7 @@ class bitbang(object):
         Clean up GPIO resources if managed.
         """
         if self._managed:
-            self._gpio.cleanup()
+            self._gpio.cleanup([pin for pin in [self._SCLK, self._SDA, self._CE, self._DC, self._RST] if pin is not None])
 
 
 @lib.spidev
@@ -359,6 +359,14 @@ class gpio_cs_spi(spi):
         if self._gpio_CS:
             self._gpio.output(self._gpio_CS, self._gpio.LOW if self._cs_high else self._gpio.HIGH)
 
+    def cleanup(self):
+        """
+        Close pin if it was set up.
+        """
+        if self._gpio_CS is not None:
+            self._gpio.cleanup(self._gpio_CS)
+        super(gpio_cs_spi, self).cleanup()
+
 
 class noop(object):
     """
@@ -418,7 +426,7 @@ class __FTDI_WRAPPER_GPIO:
 
         self._gpio.write(self._data)
 
-    def cleanup(self):
+    def cleanup(self, pin):
         pass
 
 


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index 569ee69..c561fec 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -111,3 +111,17 @@ def multi_mock_open(*file_contents):
 
 def skip_unsupported_platform(err):
     pytest.skip(f'{type(err).__name__} ({str(err)})')
+
+
+def _positional_args_list(mock):
+    return [call[0] for call in mock.call_args_list]
+
+
+def assert_only_cleans_whats_setup(gpio):
+    setups = _positional_args_list(gpio.setup)
+    cleanups = _positional_args_list(gpio.cleanup)
+    for cleanup in cleanups:
+        assert len(cleanup) > 0, 'calling gpio.cleanup without specifying pins cleans all pins'
+    pins_set_up = {args[0] for args in setups}
+    pins_clean = {args[0] for args in setups}
+    assert pins_clean == pins_set_up, 'set pins {} but cleaned pins {}'.format(pins_set_up, pins_clean)
diff --git a/tests/test_bitbang.py b/tests/test_bitbang.py
index 04e473d..32f66ce 100644
--- a/tests/test_bitbang.py
+++ b/tests/test_bitbang.py
@@ -13,7 +13,7 @@ import luma.core.error
 
 import pytest
 
-from helpers import rpi_gpio_missing
+from helpers import rpi_gpio_missing, assert_only_cleans_whats_setup
 
 
 gpio = Mock(unsafe=True)
@@ -56,7 +56,14 @@ def test_cleanup():
     serial = bitbang(gpio=gpio)
     serial._managed = True
     serial.cleanup()
-    gpio.cleanup.assert_called_once_with()
+    assert_only_cleans_whats_setup(gpio)
+
+
+def test_cleanup_custom_pins():
+    serial = bitbang(gpio=gpio, SCLK=13, SDA=14, CE=15, DC=16, RST=17)
+    serial._managed = True
+    serial.cleanup()
+    assert_only_cleans_whats_setup(gpio)
 
 
 def test_unsupported_gpio_platform():
diff --git a/tests/test_ftdi_spi.py b/tests/test_ftdi_spi.py
index 9debffb..baa3c32 100644
--- a/tests/test_ftdi_spi.py
+++ b/tests/test_ftdi_spi.py
@@ -9,7 +9,7 @@ Tests for the :py:class:`luma.core.interface.serial.ftdi_spi` class.
 
 from unittest.mock import Mock, call, patch
 from luma.core.interface.serial import ftdi_spi
-from helpers import fib
+from helpers import fib, assert_only_cleans_whats_setup
 
 
 @patch('pyftdi.spi.SpiController')
@@ -72,3 +72,4 @@ def test_cleanup(mock_controller):
     serial = ftdi_spi(device='ftdi://dummy', bus_speed_hz=16000000, gpio_CS=3, gpio_DC=5, gpio_RST=6)
     serial.cleanup()
     instance.terminate.assert_called_once_with()
+    assert_only_cleans_whats_setup(gpio)
diff --git a/tests/test_gpio_cs_spi.py b/tests/test_gpio_cs_spi.py
index c1a9e23..5e01762 100644
--- a/tests/test_gpio_cs_spi.py
+++ b/tests/test_gpio_cs_spi.py
@@ -13,7 +13,7 @@ from unittest.mock import Mock, call
 from luma.core.interface.serial import gpio_cs_spi
 import luma.core.error
 
-from helpers import get_spidev, rpi_gpio_missing, fib
+from helpers import get_spidev, rpi_gpio_missing, fib, assert_only_cleans_whats_setup
 
 
 spidev = Mock(unsafe=True)
@@ -84,7 +84,7 @@ def test_cleanup():
     serial.cleanup()
     verify_gpio_cs_spi_init(9, 1)
     spidev.close.assert_called_once_with()
-    gpio.cleanup.assert_called_once_with()
+    assert_only_cleans_whats_setup(gpio)
 
 
 def test_init_device_not_found():
diff --git a/tests/test_parallel.py b/tests/test_parallel.py
index a83c77b..b7acff6 100644
--- a/tests/test_parallel.py
+++ b/tests/test_parallel.py
@@ -13,7 +13,7 @@ import luma.core.error
 
 import pytest
 
-from helpers import rpi_gpio_missing
+from helpers import rpi_gpio_missing, assert_only_cleans_whats_setup
 
 
 gpio = Mock(unsafe=True)
@@ -71,7 +71,7 @@ def test_cleanup():
     serial = bitbang_6800(gpio=gpio)
     serial._managed = True
     serial.cleanup()
-    gpio.cleanup.assert_called_once_with()
+    assert_only_cleans_whats_setup(gpio)
 
 
 def test_unsupported_gpio_platform():
diff --git a/tests/test_spi.py b/tests/test_spi.py
index 06ead81..4e91bb5 100644
--- a/tests/test_spi.py
+++ b/tests/test_spi.py
@@ -13,7 +13,7 @@ from unittest.mock import Mock, call
 from luma.core.interface.serial import spi
 import luma.core.error
 
-from helpers import get_spidev, rpi_gpio_missing, fib
+from helpers import get_spidev, rpi_gpio_missing, fib, assert_only_cleans_whats_setup
 
 
 spidev = Mock(unsafe=True)
@@ -83,7 +83,7 @@ def test_cleanup():
     serial.cleanup()
     verify_spi_init(9, 1)
     spidev.close.assert_called_once_with()
-    gpio.cleanup.assert_called_once_with()
+    assert_only_cleans_whats_setup(gpio)
 
 
 def test_init_device_not_found():