From 39d3a894411e3069cdb14354509153028a48e6c5 Mon Sep 17 00:00:00 2001
From: Vendula Poncova <vponcova@redhat.com>
Date: Wed, 2 Sep 2020 13:40:36 +0200
Subject: [PATCH] Propagate a lazy proxy of the storage model

The storage model for the partitioning modules should be always created lazily.
When we reset the partitioning, the new model shouldn't be created until we try
to work with it. Then we create a new copy of the storage model, hide disks that
are not selected and possibly initialize empty disks.

There is a problem with modules that need to be able to work with the same copy
of the storage model as the partitioning modules. At this moment, it is only the
device tree module. The suggested solution is to propagate a lazy proxy of the
storage model. It will not trigger the creation of the copy until we try to
access the attributes of the storage model.

Basically, the device tree module always gets the storage model on demand from
the storage property of the partitioning module.

Related: rhbz#1868577
---
 pyanaconda/core/util.py                       | 37 +++++++
 .../modules/storage/partitioning/base.py      | 28 ++++--
 .../module_part_interactive_test.py           | 18 ++++
 tests/nosetests/pyanaconda_tests/util_test.py | 97 ++++++++++++++++++-
 4 files changed, 170 insertions(+), 10 deletions(-)

diff --git a/pyanaconda/core/util.py b/pyanaconda/core/util.py
index 4615f9fd8..60b6ff310 100644
--- a/pyanaconda/core/util.py
+++ b/pyanaconda/core/util.py
@@ -1431,3 +1431,40 @@ def is_smt_enabled():
     except (IOError, ValueError):
         log.warning("Failed to detect SMT.")
         return False
+
+
+class LazyObject(object):
+    """The lazy object."""
+
+    def __init__(self, getter):
+        """Create a proxy of an object.
+
+        The object might not exist until we call the given
+        function. The function is called only when we try
+        to access the attributes of the object.
+
+        The returned object is not cached in this class.
+        We call the function every time.
+
+        :param getter: a function that returns the object
+        """
+        self._getter = getter
+
+    @property
+    def _object(self):
+        return self._getter()
+
+    def __eq__(self, other):
+        return self._object == other
+
+    def __hash__(self):
+        return self._object.__hash__()
+
+    def __getattr__(self, name):
+        return getattr(self._object, name)
+
+    def __setattr__(self, name, value):
+        if name in ("_getter", ):
+            return super().__setattr__(name, value)
+
+        return setattr(self._object, name, value)
diff --git a/pyanaconda/modules/storage/partitioning/base.py b/pyanaconda/modules/storage/partitioning/base.py
index 989fa0a7b..c8b4b95ac 100644
--- a/pyanaconda/modules/storage/partitioning/base.py
+++ b/pyanaconda/modules/storage/partitioning/base.py
@@ -17,12 +17,13 @@
 # License and may only be used or replicated with the express permission of
 # Red Hat, Inc.
 #
-from abc import abstractmethod, abstractproperty
+from abc import abstractmethod
 
 from blivet.devices import PartitionDevice, TmpFSDevice, LVMLogicalVolumeDevice, \
     LVMVolumeGroupDevice, MDRaidArrayDevice, BTRFSDevice
 
 from dasbus.server.publishable import Publishable
+from pyanaconda.core.util import LazyObject
 from pyanaconda.modules.common.base.base import KickstartBaseModule
 from pyanaconda.modules.common.errors.storage import UnavailableStorageError
 from pyanaconda.anaconda_loggers import get_module_logger
@@ -45,7 +46,8 @@ class PartitioningModule(KickstartBaseModule, Publishable):
         self._selected_disks = []
         self._device_tree_module = None
 
-    @abstractproperty
+    @property
+    @abstractmethod
     def partitioning_method(self):
         """Type of the partitioning method."""
         return None
@@ -67,8 +69,22 @@ class PartitioningModule(KickstartBaseModule, Publishable):
 
         return self._storage_playground
 
+    @property
+    def lazy_storage(self):
+        """The lazy storage model.
+
+        Provides a lazy access to the storage model. This property will not
+        trigger a creation of the storage playground. The playground will be
+        created on the first access of the storage attributes.
+        """
+        return LazyObject(lambda: self.storage)
+
     def _create_storage_playground(self):
         """Prepare the current storage model for partitioning."""
+        log.debug(
+            "Creating a new storage playground for %s with "
+            "selected disks %s.", self, self._selected_disks
+        )
         storage = self._current_storage.copy()
         storage.select_disks(self._selected_disks)
         return storage
@@ -77,16 +93,10 @@ class PartitioningModule(KickstartBaseModule, Publishable):
         """Update the current storage."""
         self._current_storage = storage
 
-        if self._device_tree_module:
-            self._device_tree_module.on_storage_changed(self.storage)
-
     def on_partitioning_reset(self):
         """Drop the storage playground."""
         self._storage_playground = None
 
-        if self._device_tree_module:
-            self._device_tree_module.on_storage_changed(self.storage)
-
     def on_selected_disks_changed(self, selection):
         """Keep the current disk selection."""
         self._selected_disks = selection
@@ -100,7 +110,7 @@ class PartitioningModule(KickstartBaseModule, Publishable):
 
         if not module:
             module = self._create_device_tree()
-            module.on_storage_changed(self.storage)
+            module.on_storage_changed(self.lazy_storage)
             self._device_tree_module = module
 
         return module
diff --git a/tests/nosetests/pyanaconda_tests/module_part_interactive_test.py b/tests/nosetests/pyanaconda_tests/module_part_interactive_test.py
index 13d33feab..32fe589b7 100644
--- a/tests/nosetests/pyanaconda_tests/module_part_interactive_test.py
+++ b/tests/nosetests/pyanaconda_tests/module_part_interactive_test.py
@@ -71,6 +71,24 @@ class InteractivePartitioningInterfaceTestCase(unittest.TestCase):
         """Test Method property."""
         self.assertEqual(self.interface.PartitioningMethod, PARTITIONING_METHOD_INTERACTIVE)
 
+    @patch_dbus_publish_object
+    def lazy_storage_test(self, publisher):
+        """Make sure that the storage playground is created lazily."""
+        self.module.on_storage_changed(create_storage())
+
+        device_tree_module = self.module.get_device_tree()
+        self.assertIsNone(self.module._storage_playground)
+
+        device_tree_module.get_disks()
+        self.assertIsNotNone(self.module._storage_playground)
+
+        self.module.on_partitioning_reset()
+        self.module.on_storage_changed(create_storage())
+        self.assertIsNone(self.module._storage_playground)
+
+        device_tree_module.get_actions()
+        self.assertIsNotNone(self.module._storage_playground)
+
     @patch_dbus_publish_object
     def get_device_tree_test(self, publisher):
         """Test GetDeviceTree."""
diff --git a/tests/nosetests/pyanaconda_tests/util_test.py b/tests/nosetests/pyanaconda_tests/util_test.py
index 1da8362dc..76f1c4465 100644
--- a/tests/nosetests/pyanaconda_tests/util_test.py
+++ b/tests/nosetests/pyanaconda_tests/util_test.py
@@ -29,7 +29,7 @@ from unittest.mock import Mock, patch
 from pyanaconda.errors import ExitError
 from pyanaconda.core.process_watchers import WatchProcesses
 from pyanaconda.core import util
-from pyanaconda.core.util import synchronized
+from pyanaconda.core.util import synchronized, LazyObject
 from pyanaconda.core.configuration.anaconda import conf
 
 from timer import timer
@@ -829,3 +829,98 @@ class MiscTests(unittest.TestCase):
         )
         self.assertEqual(get_anaconda_version_string(), "1.0")
         self.assertEqual(get_anaconda_version_string(build_time_version=True), "1.0-1")
+
+
+class LazyObjectTestCase(unittest.TestCase):
+
+    class Object(object):
+
+        def __init__(self):
+            self._x = 0
+
+        @property
+        def x(self):
+            return self._x
+
+        @x.setter
+        def x(self, value):
+            self._x = value
+
+        def f(self, value):
+            self._x += value
+
+    def setUp(self):
+        self._obj = None
+
+    @property
+    def obj(self):
+        if not self._obj:
+            self._obj = self.Object()
+
+        return self._obj
+
+    @property
+    def lazy_obj(self):
+        return LazyObject(lambda: self.obj)
+
+    def get_set_test(self):
+        self.assertIsNotNone(self.lazy_obj)
+        self.assertIsNone(self._obj)
+
+        self.assertEqual(self.lazy_obj.x, 0)
+        self.assertIsNotNone(self._obj)
+
+        self.obj.x = -10
+        self.assertEqual(self.obj.x, -10)
+        self.assertEqual(self.lazy_obj.x, -10)
+
+        self.lazy_obj.x = 10
+        self.assertEqual(self.obj.x, 10)
+        self.assertEqual(self.lazy_obj.x, 10)
+
+        self.lazy_obj.f(90)
+        self.assertEqual(self.obj.x, 100)
+        self.assertEqual(self.lazy_obj.x, 100)
+
+    def eq_test(self):
+        a = object()
+        lazy_a1 = LazyObject(lambda: a)
+        lazy_a2 = LazyObject(lambda: a)
+
+        self.assertEqual(a, lazy_a1)
+        self.assertEqual(lazy_a1, a)
+
+        self.assertEqual(a, lazy_a2)
+        self.assertEqual(lazy_a2, a)
+
+        self.assertEqual(lazy_a1, lazy_a2)
+        self.assertEqual(lazy_a2, lazy_a1)
+
+        self.assertEqual(lazy_a1, lazy_a1)
+        self.assertEqual(lazy_a2, lazy_a2)
+
+    def neq_test(self):
+        a = object()
+        lazy_a = LazyObject(lambda: a)
+
+        b = object()
+        lazy_b = LazyObject(lambda: b)
+
+        self.assertNotEqual(b, lazy_a)
+        self.assertNotEqual(lazy_a, b)
+
+        self.assertNotEqual(lazy_a, lazy_b)
+        self.assertNotEqual(lazy_b, lazy_a)
+
+    def hash_test(self):
+        a = object()
+        lazy_a1 = LazyObject(lambda: a)
+        lazy_a2 = LazyObject(lambda: a)
+
+        b = object()
+        lazy_b1 = LazyObject(lambda: b)
+        lazy_b2 = LazyObject(lambda: b)
+
+        self.assertEqual({a, lazy_a1, lazy_a2}, {a})
+        self.assertEqual({b, lazy_b1, lazy_b2}, {b})
+        self.assertEqual({lazy_a1, lazy_b2}, {a, b})
