Hi,
this patchset does quite a lot of generalization changes, which are needed for my new firstboot code. I cleaned it quite a bit, but some patches document the way I was thinking about the problem so I kept them for documentation purposes.
I successfully installed smoke4 with this code applied.
Summary of the changes:
- support for loading the UI glade files also from the directory the source file is specified - support for loading Spokes and Categories from multiple directories - better loading of python "extensions", no __init__.py necessary in their directories - new feature for kickstart - %addon section - path system that stores all the paths needed - a bit more general arguments for some classes for better reusability of our code - much better hack to load AnacondaWidgets, that also loads GI stuff along with the .so library - spokes have separate mandatory and completed methods (not used atm, but I will need it to save the list of configured spokes for Firstboot and GIE)
If you want to see this as a GIT repo instead, go to http://fedorapeople.org/cgit/msivak/public_git/anaconda.git/log/?h=firstboot... or do `git remote add msivak git://fedorapeople.org/home/fedora/msivak/public_git/anaconda.git`
I am using this external repo because it allows me to do rebases. Fedora Hosted has (or at least used to have) them disabled for security reasons.
Martin
--- pyanaconda/ui/common.py | 18 ++++++++++++++---- pyanaconda/ui/gui/hubs/__init__.py | 4 ++-- pyanaconda/ui/gui/spokes/__init__.py | 2 +- pyanaconda/ui/gui/spokes/datetime_spoke.py | 4 ++++ pyanaconda/ui/gui/spokes/network.py | 7 +++++-- pyanaconda/ui/gui/spokes/password.py | 4 ++++ pyanaconda/ui/gui/spokes/software.py | 4 ++++ pyanaconda/ui/gui/spokes/source.py | 4 ++++ pyanaconda/ui/tui/hubs/__init__.py | 2 +- pyanaconda/ui/tui/spokes/password.py | 4 ++++ pyanaconda/ui/tui/spokes/storage.py | 4 ++++ pyanaconda/ui/tui/spokes/time.py | 4 ++++ 12 files changed, 51 insertions(+), 10 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index bcac8b4..3452a04 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -171,13 +171,23 @@ class Spoke(UIObject):
@property def completed(self): - """Has this spoke been visited and completed? If not, a special warning - icon will be shown on the Hub beside the spoke, and a highlighted - message will be shown at the bottom of the Hub. Installation will not - be allowed to proceed until all spokes are complete. + """Has this spoke been visited and completed? If not and the spoke is + mandatory, a special warning icon will be shown on the Hub beside the + spoke, and a highlighted message will be shown at the bottom of the + Hub. Installation will not be allowed to proceed until all mandatory + spokes are complete. """ return False
+ @property + def mandatory(self): + """Mark this spoke as mandatory. Installation will not be allowed + to proceed until all mandatory spokes are complete. + + Spokes are mandatory unless marked as not being so. + """ + return True + def execute(self): """Cause the data object to take effect on the target system. This will usually be as simple as calling one or more of the execute methods on diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index c0df863..642ebb3 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -200,14 +200,14 @@ class Hub(GUIObject, common.Hub): spoke.selector.set_sensitive(spoke.ready) spoke.selector.set_property("status", spoke.status) spoke.selector.set_tooltip_markup(spoke.status) - spoke.selector.set_incomplete(not spoke.completed) + spoke.selector.set_incomplete(not spoke.completed and spoke.mandatory) self._handleCompleteness(spoke)
def _handleCompleteness(self, spoke): # Add the spoke to the incomplete list if it's now incomplete, and make # sure it's not on the list if it's now complete. Then show the box if # it's needed and hide it if it's not. - if spoke.completed: + if not spoke.mandatory or spoke.completed: if spoke in self._incompleteSpokes: self._incompleteSpokes.remove(spoke) else: diff --git a/pyanaconda/ui/gui/spokes/__init__.py b/pyanaconda/ui/gui/spokes/__init__.py index 9633eb9..f25d30c 100644 --- a/pyanaconda/ui/gui/spokes/__init__.py +++ b/pyanaconda/ui/gui/spokes/__init__.py @@ -17,7 +17,7 @@ # Red Hat, Inc. # # Red Hat Author(s): Chris Lumens clumens@redhat.com -# +# Martin Sivak msivak@redhat.com
from pyanaconda.ui import common from pyanaconda.ui.common import collect diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.py b/pyanaconda/ui/gui/spokes/datetime_spoke.py index 5bc07d0..91af50f 100644 --- a/pyanaconda/ui/gui/spokes/datetime_spoke.py +++ b/pyanaconda/ui/gui/spokes/datetime_spoke.py @@ -426,6 +426,10 @@ class DatetimeSpoke(NormalSpoke): def completed(self): return timezone.is_valid_timezone(self.data.timezone.timezone)
+ @property + def mandatory(self): + return True + def refresh(self): #update the displayed time self._update_datetime_timer_id = GLib.timeout_add_seconds(1, diff --git a/pyanaconda/ui/gui/spokes/network.py b/pyanaconda/ui/gui/spokes/network.py index e5b91d5..b953f61 100644 --- a/pyanaconda/ui/gui/spokes/network.py +++ b/pyanaconda/ui/gui/spokes/network.py @@ -980,10 +980,13 @@ class NetworkSpoke(NormalSpoke): @property def completed(self): # TODO: check also if source requires updates when implemented - return (self.data.method.method not in ("url", "nfs") or - len(self.network_control_box.activated_connections()) > 0) + return len(self.network_control_box.activated_connections()) > 0
@property + def mandatory(self): + return self.data.method.method in ("url", "nfs") + + @property def status(self): """ A short string describing which devices are connected. """ msg = _("Unknown") diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py index ac113af..c273846 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -82,6 +82,10 @@ class PasswordSpoke(NormalSpoke): else: return _("Root password is not set")
+ @property + def mandatory(self): + return False + def apply(self): self.data.rootpw.password = cryptPassword(self._password) self.data.rootpw.isCrypted = True diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py index 8fd4c59..5ad9028 100644 --- a/pyanaconda/ui/gui/spokes/software.py +++ b/pyanaconda/ui/gui/spokes/software.py @@ -131,6 +131,10 @@ class SoftwareSelectionSpoke(NormalSpoke): return self._get_selected_environment() is not None and processingDone
@property + def mandatory(self): + return True + + @property def ready(self): # By default, the software selection spoke is not ready. We have to # wait until the installation source spoke is completed. This could be diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py index fc1aacc..ac39ce6 100644 --- a/pyanaconda/ui/gui/spokes/source.py +++ b/pyanaconda/ui/gui/spokes/source.py @@ -581,6 +581,10 @@ class SourceSpoke(NormalSpoke): return not self._error and self.status and self.status != _("Nothing selected")
@property + def mandatory(self): + return True + + @property def ready(self): from pyanaconda.threads import threadMgr # By default, the source spoke is not ready. We have to wait until diff --git a/pyanaconda/ui/tui/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py index ae5b0b8..b7dade1 100644 --- a/pyanaconda/ui/tui/hubs/__init__.py +++ b/pyanaconda/ui/tui/hubs/__init__.py @@ -103,7 +103,7 @@ class TUIHub(TUIObject, common.Hub): # don't continue if key == _('c'): for spoke in self._spokes.values(): - if not spoke.completed: + if not spoke.completed and spoke.mandatory: print(_("Please complete all spokes before continuing")) return False return key diff --git a/pyanaconda/ui/tui/spokes/password.py b/pyanaconda/ui/tui/spokes/password.py index 8942e17..f812678 100644 --- a/pyanaconda/ui/tui/spokes/password.py +++ b/pyanaconda/ui/tui/spokes/password.py @@ -44,6 +44,10 @@ class PasswordSpoke(NormalTUISpoke): return bool(self.data.rootpw.password or self.data.rootpw.lock)
@property + def mandatory(self): + return True + + @property def status(self): if self.data.rootpw.password: return _("Password is set.") diff --git a/pyanaconda/ui/tui/spokes/storage.py b/pyanaconda/ui/tui/spokes/storage.py index 0032f2a..b5d483f 100644 --- a/pyanaconda/ui/tui/spokes/storage.py +++ b/pyanaconda/ui/tui/spokes/storage.py @@ -115,6 +115,10 @@ class StorageSpoke(NormalTUISpoke): return self._ready and not threadMgr.get("AnaStorageWatcher")
@property + def mandatory(self): + return True + + @property def status(self): """ A short string describing the current status of storage setup. """ msg = _("No disks selected") diff --git a/pyanaconda/ui/tui/spokes/time.py b/pyanaconda/ui/tui/spokes/time.py index cc26ec3..6a47401 100644 --- a/pyanaconda/ui/tui/spokes/time.py +++ b/pyanaconda/ui/tui/spokes/time.py @@ -48,6 +48,10 @@ class TimeZoneSpoke(NormalTUISpoke): return bool(self.data.timezone.timezone or self._selection)
@property + def mandatory(self): + return True + + @property def status(self): if self.data.timezone.timezone: return _("%s timezone") % self.data.timezone.timezone
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index bcac8b4..3452a04 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -171,13 +171,23 @@ class Spoke(UIObject):
@property def completed(self):
"""Has this spoke been visited and completed? If not, a special warningicon will be shown on the Hub beside the spoke, and a highlightedmessage will be shown at the bottom of the Hub. Installation will notbe allowed to proceed until all spokes are complete.
"""Has this spoke been visited and completed? If not and the spoke ismandatory, a special warning icon will be shown on the Hub beside thespoke, and a highlighted message will be shown at the bottom of theHub. Installation will not be allowed to proceed until all mandatoryspokes are complete. """ return False@property
def mandatory(self):
"""Mark this spoke as mandatory. Installation will not be allowedto proceed until all mandatory spokes are complete.Spokes are mandatory unless marked as not being so."""return True
Currently, we kind of fake having a mandatory setting by marking a spoke as incomplete if whatever ksdata it needs is not filled in, then attempting to fill in everything when the spoke is created. Could you give me an example of what new thing requires a new setting?'
Also, since you set mandatory = True in the base class here, you should be able to get rid of the property from most of the other spoke classes changed in this patch.
- Chris
There is no way to decide if it was filled or uses default arguments. And firstboot needs to know that to decide if it should show the spoke again or not.
----- Original Message -----
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index bcac8b4..3452a04 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -171,13 +171,23 @@ class Spoke(UIObject):
@property def completed(self):
"""Has this spoke been visited and completed? If not, aspecial warning
icon will be shown on the Hub beside the spoke, and ahighlighted
message will be shown at the bottom of the Hub.Installation will not
be allowed to proceed until all spokes are complete.
"""Has this spoke been visited and completed? If not andthe spoke is
mandatory, a special warning icon will be shown on theHub beside the
spoke, and a highlighted message will be shown at thebottom of the
Hub. Installation will not be allowed to proceed untilall mandatory
spokes are complete. """ return False@property
def mandatory(self):
"""Mark this spoke as mandatory. Installation will not beallowed
to proceed until all mandatory spokes are complete.Spokes are mandatory unless marked as not being so."""return TrueCurrently, we kind of fake having a mandatory setting by marking a spoke as incomplete if whatever ksdata it needs is not filled in, then attempting to fill in everything when the spoke is created. Could you give me an example of what new thing requires a new setting?'
Also, since you set mandatory = True in the base class here, you should be able to get rid of the property from most of the other spoke classes changed in this patch.
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
--- pyanaconda/ui/common.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 3452a04..d5ff8af 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -21,7 +21,7 @@ #
import os -import importlib +import imp import inspect
class UIObject(object): @@ -380,19 +380,19 @@ class Hub(UIObject):
def collect(module_pattern, path, pred): """Traverse the directory (given by path), import all files as a module - module_pattern % filename and find all classes withing that match + module_pattern % filename and find all classes within that match the given predicate. This is then returned as a list of classes.
It is suggested you use collect_categories or collect_spokes instead of this lower-level method.
:param module_pattern: the full name pattern (pyanaconda.ui.gui.spokes.%s) - of modules we about to import from path + we want to assign to imported modules :type module_pattern: string
:param path: the directory we are picking up modules from :type path: string - +
:param pred: function which marks classes as good to import :type pred: function with one argument returning True or False @@ -400,12 +400,25 @@ def collect(module_pattern, path, pred):
retval = [] for module_file in os.listdir(path): - if not module_file.endswith(".py") or module_file == "__init__.py": + if module_file == "__init__.py": continue
- mod_name = module_file[:-3] - module = importlib.import_module(module_pattern % mod_name) - + try: + mod_name = module_file[:module_file.rindex(".")] + except ValueError: + mod_name = module_file + + try: + imp.acquire_lock() + mod_info = imp.find_module(mod_name, path) + module = imp.load_module(module_pattern % mod_name, *mod_info) + imp.release_lock() + except ImportError: + continue + finally: + if mod_info[0]: + mod_info[0].close() + p = lambda obj: inspect.isclass(obj) and pred(obj)
for (name, val) in inspect.getmembers(module, p):
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/common.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 3452a04..d5ff8af 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -21,7 +21,7 @@ #
import os -import importlib +import imp import inspect
class UIObject(object): @@ -380,19 +380,19 @@ class Hub(UIObject):
def collect(module_pattern, path, pred): """Traverse the directory (given by path), import all files as a module
module_pattern % filename and find all classes withing that match
module_pattern % filename and find all classes within that match the given predicate. This is then returned as a list of classes. It is suggested you use collect_categories or collect_spokes instead of this lower-level method. :param module_pattern: the full name pattern (pyanaconda.ui.gui.spokes.%s)
of modules we about to import from path
we want to assign to imported modules :type module_pattern: string :param path: the directory we are picking up modules from :type path: string
Only added whitespace here ^^^^.
@@ -400,12 +400,25 @@ def collect(module_pattern, path, pred):
retval = [] for module_file in os.listdir(path):
if not module_file.endswith(".py") or module_file == "__init__.py":
if module_file == "__init__.py": continue
mod_name = module_file[:-3]module = importlib.import_module(module_pattern % mod_name)
try:mod_name = module_file[:module_file.rindex(".")]except ValueError:mod_name = module_filetry:imp.acquire_lock()mod_info = imp.find_module(mod_name, path)module = imp.load_module(module_pattern % mod_name, *mod_info)imp.release_lock()except ImportError:continuefinally:if mod_info[0]:mod_info[0].close()
I wish the imp module locking thing worked more like other lock-related code so you could do:
with imp.lock do: ...
but that's hardly a knock on your patch.
- Chris
--- pyanaconda/ui/__init__.py | 41 +++++++++++++++++++++++--------- pyanaconda/ui/gui/__init__.py | 39 +++++++++++++++++++++++------- pyanaconda/ui/gui/categories/__init__.py | 13 +++++++--- pyanaconda/ui/gui/hubs/__init__.py | 32 +++++++++++++++++++++---- pyanaconda/ui/gui/spokes/__init__.py | 17 ++++++++++--- 5 files changed, 111 insertions(+), 31 deletions(-)
diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py index 80d8e6f..aabc719 100644 --- a/pyanaconda/ui/__init__.py +++ b/pyanaconda/ui/__init__.py @@ -98,13 +98,36 @@ class UserInterface(object): """ raise NotImplementedError
- def getActionClasses(self, module_pattern, path, hubs, standalone_class): + def _collectActionClasses(self, module_pattern_w_path, standalone_class): """Collect all the Hub and Spoke classes which should be enqueued for - processing and order them according to their pre/post dependencies. + processing.
- :param module_pattern: the full name pattern (pyanaconda.ui.gui.spokes.%s) - of modules we about to import from path - :type module_pattern: string + :param module_pattern_w_path: the full name patterns (pyanaconda.ui.gui.spokes.%s) + and directory paths to modules we are about to import + :type module_pattern_w_path: list of (string, string) + + :param standalone_class: the parent type of Spokes we want to pick up + :type standalone_class: common.StandaloneSpoke based types + + :return: list of Spoke classes with standalone_class as a parent + :rtype: list of Spoke classes + + """ + standalones = [] + + for module_pattern, path in module_pattern_w_path: + standalones.extend(collect(module_pattern, path, lambda obj: issubclass(obj, standalone_class) and \ + getattr(obj, "preForHub", False) or getattr(obj, "postForHub", False))) + + return standalones + + def _orderActionClasses(self, spokes, hubs, standalone_class): + """Order all the Hub and Spoke classes which should be enqueued for + processing according to their pre/post dependencies. + + :param spokes: the classes we are to about order according + to the hub dependencies + :type spokes: list of Spoke instances
:param path: the directory we are picking up modules from :type path: string @@ -117,16 +140,12 @@ class UserInterface(object): :type standalone_class: common.StandaloneSpoke based types """
- - standalones = collect(module_pattern, path, lambda obj: issubclass(obj, standalone_class) and \ - getattr(obj, "preForHub", False) or getattr(obj, "postForHub", False)) - actionClasses = [] for hub in hubs: - actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "preForHub", None) == hub, standalones), + actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "preForHub", None) == hub, spokes), key=lambda obj: obj.priority)) actionClasses.append(hub) - actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "postForHub", None) == hub, standalones), + actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "postForHub", None) == hub, spokes), key=lambda obj: obj.priority))
return actionClasses diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 6bba736..f9b51e2 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -27,6 +27,7 @@ from pyanaconda.product import distributionText, isFinal
from pyanaconda.ui import UserInterface, common from pyanaconda.ui.gui.utils import enlightbox, gtk_thread_wait +from pyanaconda.product import isFinal, productName, productVersion
import gettext _ = lambda x: gettext.ldgettext("anaconda", x) @@ -42,7 +43,9 @@ class GraphicalUserInterface(UserInterface): """This is the standard GTK+ interface we try to steer everything to using. It is suitable for use both directly and via VNC. """ - def __init__(self, storage, payload, instclass): + def __init__(self, storage, payload, instclass, + distributionText = distributionText, isFinal = isFinal): + UserInterface.__init__(self, storage, payload, instclass)
self._actions = [] @@ -51,23 +54,41 @@ class GraphicalUserInterface(UserInterface):
self.data = None
+ self._distributionText = distributionText + self._isFinal = isFinal + # This is a hack to make sure the AnacondaWidgets library gets loaded # before the introspection stuff. import ctypes ctypes.CDLL("libAnacondaWidgets.so.0", ctypes.RTLD_GLOBAL)
- def setup(self, data): + def _list_hubs(self): from hubs.summary import SummaryHub from hubs.progress import ProgressHub + return [SummaryHub, ProgressHub] + + def _list_standalone_paths(self): + path = os.path.join(os.path.dirname(__file__), "spokes") + return [("pyanaconda.ui.gui.spokes.%s", path)] + + def _is_standalone(self, obj): from spokes import StandaloneSpoke + return isinstance(obj, StandaloneSpoke)
+ def setup(self, data): busyCursor() + + self._actions = self.getActionClasses(self._list_hubs()) + self.data = data
- hubs = [SummaryHub, ProgressHub] - path = os.path.join(os.path.dirname(__file__), "spokes") + def getActionClasses(self, hubs): + from spokes import StandaloneSpoke
- self._actions = self.getActionClasses("pyanaconda.ui.gui.spokes.%s", path, hubs, StandaloneSpoke) - self.data = data + # First, grab a list of all the standalone spokes. + standalones = self._collectActionClasses(self._list_standalone_paths(), StandaloneSpoke) + + # Second, order them according to their relationship + return self._orderActionClasses(standalones, hubs, StandaloneSpoke)
def _instantiateAction(self, actionClass): from spokes import StandaloneSpoke @@ -120,8 +141,8 @@ class GraphicalUserInterface(UserInterface):
self._currentAction.refresh()
- self._currentAction.window.set_beta(not isFinal) - self._currentAction.window.set_property("distribution", distributionText().upper()) + self._currentAction.window.set_beta(not self._isFinal()) + self._currentAction.window.set_property("distribution", self._distributionText().upper())
# Set fonts app-wide, where possible settings = Gtk.Settings.get_default() @@ -241,7 +262,7 @@ class GraphicalUserInterface(UserInterface):
nextAction.initialize() nextAction.window.set_beta(self._currentAction.window.get_beta()) - nextAction.window.set_property("distribution", distributionText().upper()) + nextAction.window.set_property("distribution", self._distributionText().upper())
if not nextAction.showable: self._currentAction.window.hide() diff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py index b71217f..8d96b33 100644 --- a/pyanaconda/ui/gui/categories/__init__.py +++ b/pyanaconda/ui/gui/categories/__init__.py @@ -71,6 +71,13 @@ class SpokeCategory(object):
return g
-def collect_categories(): - """Return a list of all category subclasses.""" - return collect("pyanaconda.ui.gui.categories.%s", os.path.dirname(__file__), lambda obj: getattr(obj, "displayOnHub", None) != None) +def collect_categories(mask_paths): + """Return a list of all category subclasses. Look for them in modules + imported as module_mask % basename(f) where f is name of all files in path. + """ + categories = [] + for mask, path in mask_paths: + spokes.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None) + + return categories + diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index 642ebb3..4447367 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -111,22 +111,44 @@ class Hub(GUIObject, common.Hub): action.apply() action.execute()
- def _createBox(self): - from gi.repository import Gtk, AnacondaWidgets - from pyanaconda.ui.gui.utils import setViewportBackground + def _list_spokes_mask_paths(self): + return [("pyanaconda.ui.gui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))] + + def _list_categories_mask_paths(self): + return [("pyanaconda.ui.gui.categories.%s", os.path.join(os.path.dirname(__file__), "categories"))] + + def _collectCategoriesAndSpokes(self): + """collects categories and spokes to be displayed on this Hub
+ :return: dictionary mapping category class to list of spoke classes + :rtype: dictionary[category class] -> [ list of spoke classes ] + """ + + ret = {} + # Collect all the categories this hub displays, then collect all the # spokes belonging to all those categories. - categories = sorted(filter(lambda c: c.displayOnHub == self.__class__, collect_categories()), + categories = sorted(filter(lambda c: c.displayOnHub == self.__class__, collect_categories(self._list_categories_mask_paths())), key=lambda c: c.title) + for c in categories: + ret[c] = collect_spokes(self._list_spokes_mask_paths(), c.__name__) + + return ret + + def _createBox(self): + from gi.repository import Gtk, AnacondaWidgets + from pyanaconda.ui.gui.utils import setViewportBackground
+ cats_and_spokes = self._collectCategoriesAndSpokes() + categories = cats_and_spokes.keys() + box = Gtk.VBox(False, 6)
for c in categories: obj = c()
selectors = [] - for spokeClass in sorted(collect_spokes(obj.__class__.__name__), key=lambda s: s.title): + for spokeClass in sorted(cats_and_spokes[c], key=lambda s: s.title): # Create the new spoke and populate its UI with whatever data. # From here on, this Spoke will always exist. spoke = spokeClass(self.data, self.storage, self.payload, self.instclass) diff --git a/pyanaconda/ui/gui/spokes/__init__.py b/pyanaconda/ui/gui/spokes/__init__.py index f25d30c..99039a6 100644 --- a/pyanaconda/ui/gui/spokes/__init__.py +++ b/pyanaconda/ui/gui/spokes/__init__.py @@ -94,8 +94,19 @@ class PersonalizationSpoke(Spoke, common.PersonalizationSpoke): Spoke.__init__(self, data) common.PersonalizationSpoke.__init__(self, data, storage, payload, instclass)
-def collect_spokes(category): +def collect_spokes(mask_paths, category): """Return a list of all spoke subclasses that should appear for a given - category. + category. Look for them in files imported as module_path % basename(f) + + :param mask_paths: list of mask, path tuples to search for classes + :type mask_paths: list of (mask, path) + + :return: list of Spoke classes belonging to category + :rtype: list of Spoke classes + """ - return collect("pyanaconda.ui.gui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category.__name__ == category) + spokes = [] + for mask, path in mask_paths: + spokes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category.__name__ == category)) + + return spokes
--- pyanaconda/ui/tui/hubs/__init__.py | 2 +- pyanaconda/ui/tui/spokes/__init__.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/pyanaconda/ui/tui/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py index b7dade1..0f505f7 100644 --- a/pyanaconda/ui/tui/hubs/__init__.py +++ b/pyanaconda/ui/tui/hubs/__init__.py @@ -52,7 +52,7 @@ class TUIHub(TUIObject, common.Hub):
# look for spokes having category present in self.categories for c in self.categories: - spokes = collect_spokes(c) + spokes = collect_spokes([("pyanaconda.ui.tui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))], c)
# sort them according to their priority for s in sorted(spokes, key = lambda s: s.priority): diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py index a51a19e..1abe008 100644 --- a/pyanaconda/ui/tui/spokes/__init__.py +++ b/pyanaconda/ui/tui/spokes/__init__.py @@ -81,13 +81,21 @@ class NormalTUISpoke(TUISpoke, NormalSpoke): class PersonalizationTUISpoke(TUISpoke, PersonalizationSpoke): pass
-def collect_spokes(category): +def collect_spokes(mask_paths, category): """Return a list of all spoke subclasses that should appear for a given category. """ - return collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category) - -def collect_categories(): - classes = collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "") + spokes = [] + for mask, path in mask_paths: + spokes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category)) + + return spokes + +def collect_categories(mask_paths): + classes = [] + for mask, path in mask_paths: + classes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")) + categories = set([c.category for c in classes]) return categories +
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/tui/hubs/__init__.py | 2 +- pyanaconda/ui/tui/spokes/__init__.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/pyanaconda/ui/tui/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py index b7dade1..0f505f7 100644 --- a/pyanaconda/ui/tui/hubs/__init__.py +++ b/pyanaconda/ui/tui/hubs/__init__.py @@ -52,7 +52,7 @@ class TUIHub(TUIObject, common.Hub):
# look for spokes having category present in self.categories for c in self.categories:
spokes = collect_spokes(c)
spokes = collect_spokes([("pyanaconda.ui.tui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))], c) # sort them according to their priority for s in sorted(spokes, key = lambda s: s.priority):diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py index a51a19e..1abe008 100644 --- a/pyanaconda/ui/tui/spokes/__init__.py +++ b/pyanaconda/ui/tui/spokes/__init__.py @@ -81,13 +81,21 @@ class NormalTUISpoke(TUISpoke, NormalSpoke): class PersonalizationTUISpoke(TUISpoke, PersonalizationSpoke): pass
-def collect_spokes(category): +def collect_spokes(mask_paths, category): """Return a list of all spoke subclasses that should appear for a given category. """
- return collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category)
-def collect_categories():
- classes = collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")
- spokes = []
- for mask, path in mask_paths:
spokes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category))- return spokes
+def collect_categories(mask_paths):
- classes = []
- for mask, path in mask_paths:
classes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")) categories = set([c.category for c in classes]) return categories
No new line needed.
diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py index a51a19e..1abe008 100644 --- a/pyanaconda/ui/tui/spokes/__init__.py +++ b/pyanaconda/ui/tui/spokes/__init__.py @@ -81,13 +81,21 @@ class NormalTUISpoke(TUISpoke, NormalSpoke): class PersonalizationTUISpoke(TUISpoke, PersonalizationSpoke): pass
-def collect_spokes(category): +def collect_spokes(mask_paths, category): """Return a list of all spoke subclasses that should appear for a given category. """
- return collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category)
-def collect_categories():
- classes = collect("pyanaconda.ui.tui.spokes.%s", os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")
- spokes = []
- for mask, path in mask_paths:
spokes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category))- return spokes
+def collect_categories(mask_paths):
- classes = []
- for mask, path in mask_paths:
classes.extend(collect(mask, path, lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")) categories = set([c.category for c in classes]) return categories
Couldn't this be combined with similar code we've got for the GUI?
- Chris
Nope, categories in GUI are GtkWidgets. TUI uses class attribute (string) so the logic is a bit different. And I only updated this to support multiple paths, the code is already in anaconda.
----- Original Message -----
diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py index a51a19e..1abe008 100644 --- a/pyanaconda/ui/tui/spokes/__init__.py +++ b/pyanaconda/ui/tui/spokes/__init__.py @@ -81,13 +81,21 @@ class NormalTUISpoke(TUISpoke, NormalSpoke): class PersonalizationTUISpoke(TUISpoke, PersonalizationSpoke): pass
-def collect_spokes(category): +def collect_spokes(mask_paths, category): """Return a list of all spoke subclasses that should appear for a given category. """
- return collect("pyanaconda.ui.tui.spokes.%s",
os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category == category)
-def collect_categories():
- classes = collect("pyanaconda.ui.tui.spokes.%s",
os.path.dirname(__file__), lambda obj: hasattr(obj, "category") and obj.category != None and obj.category != "")
- spokes = []
- for mask, path in mask_paths:
spokes.extend(collect(mask, path, lambda obj: hasattr(obj,"category") and obj.category != None and obj.category == category))
- return spokes
+def collect_categories(mask_paths):
- classes = []
- for mask, path in mask_paths:
classes.extend(collect(mask, path, lambda obj:hasattr(obj, "category") and obj.category != None and obj.category != ""))
- categories = set([c.category for c in classes]) return categories
Couldn't this be combined with similar code we've got for the GUI?
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
--- pyanaconda/ui/gui/categories/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py index 8d96b33..c318901 100644 --- a/pyanaconda/ui/gui/categories/__init__.py +++ b/pyanaconda/ui/gui/categories/__init__.py @@ -77,7 +77,7 @@ def collect_categories(mask_paths): """ categories = [] for mask, path in mask_paths: - spokes.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None) + categories.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None)
return categories
diff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py index 8d96b33..c318901 100644 --- a/pyanaconda/ui/gui/categories/__init__.py +++ b/pyanaconda/ui/gui/categories/__init__.py @@ -77,7 +77,7 @@ def collect_categories(mask_paths): """ categories = [] for mask, path in mask_paths:
spokes.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None)
categories.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None)return categories
Could you squash this with uh... I believe patch #3 is where this was changed?
- Chris
I already did.. I just forgot to do it before I sent this.
----- Original Message -----
diff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py index 8d96b33..c318901 100644 --- a/pyanaconda/ui/gui/categories/__init__.py +++ b/pyanaconda/ui/gui/categories/__init__.py @@ -77,7 +77,7 @@ def collect_categories(mask_paths): """ categories = [] for mask, path in mask_paths:
spokes.extend(collect(mask, path, lambda obj: getattr(obj,"displayOnHub", None) != None)
categories.extend(collect(mask, path, lambda obj:getattr(obj, "displayOnHub", None) != None)
return categoriesCould you squash this with uh... I believe patch #3 is where this was changed?
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
--- pyanaconda/ui/__init__.py | 30 +++++++++++++++++----- pyanaconda/ui/common.py | 14 ++++++++++ pyanaconda/ui/gui/__init__.py | 44 +++++++++++++++++++++++--------- pyanaconda/ui/gui/categories/__init__.py | 2 +- pyanaconda/ui/gui/hubs/__init__.py | 14 ++++------ pyanaconda/ui/tui/__init__.py | 24 ++++++++++++++--- pyanaconda/ui/tui/hubs/__init__.py | 2 +- 7 files changed, 96 insertions(+), 34 deletions(-)
diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py index aabc719..26e540a 100644 --- a/pyanaconda/ui/__init__.py +++ b/pyanaconda/ui/__init__.py @@ -22,6 +22,7 @@ __all__ = ["UserInterface"]
import os +import inspect from common import collect
class UserInterface(object): @@ -59,6 +60,27 @@ class UserInterface(object): from pyanaconda.errors import errorHandler errorHandler.ui = self
+ @property + def basepath(self): + """returns the directory name for UI subtree""" + return os.path.dirname(inspect.getfile(self.__class__)) + + @property + def basemask(self): + """returns the python module name for basepath directory""" + return "pyanaconda.ui" + + @property + def paths(self): + """return dictionary mapping plugin elements (spokes, hubs, categories) + to a list of tuples (module mask, search path)""" + return { + "spokes": [(self.basemask + ".spokes.%s", + os.path.join(self.basepath, "spokes"))], + "hubs": [(self.basemask + ".hubs.%s", + os.path.join(self.basepath, "hubs"))] + } + def setup(self, data): """Construct all the objects required to implement this interface. This method must be provided by all subclasses. @@ -121,7 +143,7 @@ class UserInterface(object):
return standalones
- def _orderActionClasses(self, spokes, hubs, standalone_class): + def _orderActionClasses(self, spokes, hubs): """Order all the Hub and Spoke classes which should be enqueued for processing according to their pre/post dependencies.
@@ -129,15 +151,9 @@ class UserInterface(object): to the hub dependencies :type spokes: list of Spoke instances
- :param path: the directory we are picking up modules from - :type path: string - :param hubs: the list of Hub classes we check to be in pre/postForHub attribute of Spokes to pick up :type hubs: common.Hub based types - - :param standalone_class: the parent type of Spokes we want to pick up - :type standalone_class: common.StandaloneSpoke based types """
actionClasses = [] diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index d5ff8af..11f5e0b 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -23,6 +23,14 @@ import os import imp import inspect +import copy + +class PathDict(dict): + """Dictionary class supporting + operator""" + def __add__(self, ext): + new_dict = copy.copy(self) + new_dict.update(ext) + return new_dict
class UIObject(object): """This is the base class from which all other UI classes are derived. It @@ -377,7 +385,13 @@ class Hub(UIObject): self.storage = storage self.payload = payload self.instclass = instclass + self.paths = {}
+ def set_path(self, path_id, paths): + """Update the paths attribute with list of tuples in the form (module + name format string, directory name)""" + self.paths[path_id] = paths + def collect(module_pattern, path, pred): """Traverse the directory (given by path), import all files as a module module_pattern % filename and find all classes within that match diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index f9b51e2..a09ce20 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -28,6 +28,7 @@ from pyanaconda.product import distributionText, isFinal from pyanaconda.ui import UserInterface, common from pyanaconda.ui.gui.utils import enlightbox, gtk_thread_wait from pyanaconda.product import isFinal, productName, productVersion +import os.path
import gettext _ = lambda x: gettext.ldgettext("anaconda", x) @@ -62,17 +63,19 @@ class GraphicalUserInterface(UserInterface): import ctypes ctypes.CDLL("libAnacondaWidgets.so.0", ctypes.RTLD_GLOBAL)
+ @property + def basemask(self): + return "pyanaconda.ui.gui" + def _list_hubs(self): - from hubs.summary import SummaryHub - from hubs.progress import ProgressHub + """Return a list of Hub classes to be imported to this interface""" + from .hubs.summary import SummaryHub + from .hubs.progress import ProgressHub return [SummaryHub, ProgressHub]
- def _list_standalone_paths(self): - path = os.path.join(os.path.dirname(__file__), "spokes") - return [("pyanaconda.ui.gui.spokes.%s", path)] - def _is_standalone(self, obj): - from spokes import StandaloneSpoke + """Is the spoke passes as obj standalone?""" + from .spokes import StandaloneSpoke return isinstance(obj, StandaloneSpoke)
def setup(self, data): @@ -82,13 +85,25 @@ class GraphicalUserInterface(UserInterface): self.data = data
def getActionClasses(self, hubs): - from spokes import StandaloneSpoke + """Grab all relevant standalone spokes, add them to the passed + list of hubs and order the list according to the + relationships between hubs and standalones.""" + from .spokes import StandaloneSpoke
# First, grab a list of all the standalone spokes. - standalones = self._collectActionClasses(self._list_standalone_paths(), StandaloneSpoke) + standalones = self._collectActionClasses(self.paths["spokes"], StandaloneSpoke)
# Second, order them according to their relationship - return self._orderActionClasses(standalones, hubs, StandaloneSpoke) + return self._orderActionClasses(standalones, hubs) + + @property + def paths(self): + _paths = UserInterface.paths.fget(self) + _paths.update({"categories": [(self.basemask + ".categories.%s", + os.path.join(self.basepath, "categories"))] + }) + return _paths +
def _instantiateAction(self, actionClass): from spokes import StandaloneSpoke @@ -97,10 +112,15 @@ class GraphicalUserInterface(UserInterface): # spoke API and setting up continue/quit signal handlers. obj = actionClass(self.data, self.storage, self.payload, self.instclass)
+ # set spoke search paths in Hubs + if hasattr(obj, "set_path"): + obj.set_path("spokes", self.paths["spokes"]) + obj.set_path("categories", self.paths["categories"]) + # If we are doing a kickstart install, some standalone spokes - # could already be filled out. In that case, we do not want + # could already be filled out. In taht case, we do not want # to display them. - if isinstance(obj, StandaloneSpoke) and obj.completed: + if self._is_standalone(obj) and obj.completed: del(obj) return None
diff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py index c318901..14dc2a0 100644 --- a/pyanaconda/ui/gui/categories/__init__.py +++ b/pyanaconda/ui/gui/categories/__init__.py @@ -77,7 +77,7 @@ def collect_categories(mask_paths): """ categories = [] for mask, path in mask_paths: - categories.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None) + categories.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None))
return categories
diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index 4447367..f9cc933 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -22,7 +22,9 @@ import gettext _ = lambda x: gettext.ldgettext("anaconda", x)
+ # pylint: disable-msg=E0611 +import os from gi.repository import GLib
from pyanaconda.flags import flags @@ -111,12 +113,6 @@ class Hub(GUIObject, common.Hub): action.apply() action.execute()
- def _list_spokes_mask_paths(self): - return [("pyanaconda.ui.gui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))] - - def _list_categories_mask_paths(self): - return [("pyanaconda.ui.gui.categories.%s", os.path.join(os.path.dirname(__file__), "categories"))] - def _collectCategoriesAndSpokes(self): """collects categories and spokes to be displayed on this Hub
@@ -128,10 +124,10 @@ class Hub(GUIObject, common.Hub):
# Collect all the categories this hub displays, then collect all the # spokes belonging to all those categories. - categories = sorted(filter(lambda c: c.displayOnHub == self.__class__, collect_categories(self._list_categories_mask_paths())), + categories = sorted(filter(lambda c: c.displayOnHub == self.__class__, collect_categories(self.paths["categories"])), key=lambda c: c.title) for c in categories: - ret[c] = collect_spokes(self._list_spokes_mask_paths(), c.__name__) + ret[c] = collect_spokes(self.paths["spokes"], c.__name__)
return ret
@@ -250,7 +246,7 @@ class Hub(GUIObject, common.Hub):
@property def continuePossible(self): - return len(self._incompleteSpokes) == 0 and len(self._notReadySpokes) == 0 + return len(self._incompleteSpokes)==0 and len(self._notReadySpokes) == 0
def _updateContinueButton(self): self.continueButton.set_sensitive(self.continuePossible) diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py index c4a15a0..cccbf70 100644 --- a/pyanaconda/ui/tui/__init__.py +++ b/pyanaconda/ui/tui/__init__.py @@ -129,27 +129,43 @@ class TextUserInterface(ui.UserInterface): ui.UserInterface.__init__(self, storage, payload, instclass) self._app = None
+ @property + def basemask(self): + return "pyanaconda.ui.tui" + + def _list_hubs(self): + """returns the list of hubs to use""" + return [SummaryHub, ProgressHub] + + def _is_standalone(self, spoke): + """checks if the passed spoke is standalone""" + return isinstance(spoke, StandaloneSpoke) + def setup(self, data): """Construct all the objects required to implement this interface. This method must be provided by all subclasses. """ self._app = tui.App(u"Anaconda", yes_or_no_question = YesNoDialog) - self._hubs = [SummaryHub, ProgressHub] + _hubs = self._list_hubs()
# First, grab a list of all the standalone spokes. path = os.path.join(os.path.dirname(__file__), "spokes") - actionClasses = self.getActionClasses("pyanaconda.ui.tui.spokes.%s", path, self._hubs, StandaloneSpoke) - + spokes = self._collectActionClasses(self.paths["spokes"], StandaloneSpoke) + actionClasses = self._orderActionClasses(spokes, _hubs) + for klass in actionClasses: obj = klass(self._app, data, self.storage, self.payload, self.instclass)
# If we are doing a kickstart install, some standalone spokes # could already be filled out. In taht case, we do not want # to display them. - if isinstance(obj, StandaloneSpoke) and obj.completed: + if self._is_standalone(obj) and obj.completed: del(obj) continue
+ if hasattr(obj, "set_path"): + obj.set_path("spokes", self.paths["spokes"]) + self._app.schedule_screen(obj)
def run(self): diff --git a/pyanaconda/ui/tui/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py index 0f505f7..d669bc7 100644 --- a/pyanaconda/ui/tui/hubs/__init__.py +++ b/pyanaconda/ui/tui/hubs/__init__.py @@ -52,7 +52,7 @@ class TUIHub(TUIObject, common.Hub):
# look for spokes having category present in self.categories for c in self.categories: - spokes = collect_spokes([("pyanaconda.ui.tui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))], c) + spokes = collect_spokes(self.paths["spokes"], c)
# sort them according to their priority for s in sorted(spokes, key = lambda s: s.priority):
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/__init__.py | 30 +++++++++++++++++----- pyanaconda/ui/common.py | 14 ++++++++++ pyanaconda/ui/gui/__init__.py | 44 +++++++++++++++++++++++--------- pyanaconda/ui/gui/categories/__init__.py | 2 +- pyanaconda/ui/gui/hubs/__init__.py | 14 ++++------ pyanaconda/ui/tui/__init__.py | 24 ++++++++++++++--- pyanaconda/ui/tui/hubs/__init__.py | 2 +- 7 files changed, 96 insertions(+), 34 deletions(-)
diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py index aabc719..26e540a 100644 --- a/pyanaconda/ui/__init__.py +++ b/pyanaconda/ui/__init__.py @@ -22,6 +22,7 @@ __all__ = ["UserInterface"]
import os +import inspect from common import collect
class UserInterface(object): @@ -59,6 +60,27 @@ class UserInterface(object): from pyanaconda.errors import errorHandler errorHandler.ui = self
- @property
- def basepath(self):
"""returns the directory name for UI subtree"""return os.path.dirname(inspect.getfile(self.__class__))- @property
- def basemask(self):
"""returns the python module name for basepath directory"""return "pyanaconda.ui"- @property
- def paths(self):
"""return dictionary mapping plugin elements (spokes, hubs, categories)to a list of tuples (module mask, search path)"""return {"spokes": [(self.basemask + ".spokes.%s",os.path.join(self.basepath, "spokes"))],"hubs": [(self.basemask + ".hubs.%s",os.path.join(self.basepath, "hubs"))]} def setup(self, data): """Construct all the objects required to implement this interface. This method must be provided by all subclasses.@@ -121,7 +143,7 @@ class UserInterface(object):
return standalones
- def _orderActionClasses(self, spokes, hubs, standalone_class):
- def _orderActionClasses(self, spokes, hubs): """Order all the Hub and Spoke classes which should be enqueued for processing according to their pre/post dependencies.
@@ -129,15 +151,9 @@ class UserInterface(object): to the hub dependencies :type spokes: list of Spoke instances
:param path: the directory we are picking up modules from:type path: string:param hubs: the list of Hub classes we check to be in pre/postForHub attribute of Spokes to pick up :type hubs: common.Hub based types:param standalone_class: the parent type of Spokes we want to pick up:type standalone_class: common.StandaloneSpoke based types """ actionClasses = []diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index d5ff8af..11f5e0b 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -23,6 +23,14 @@ import os import imp import inspect +import copy
+class PathDict(dict):
- """Dictionary class supporting + operator"""
- def __add__(self, ext):
new_dict = copy.copy(self)new_dict.update(ext)return new_dictclass UIObject(object): """This is the base class from which all other UI classes are derived. It @@ -377,7 +385,13 @@ class Hub(UIObject): self.storage = storage self.payload = payload self.instclass = instclass
self.paths = {}def set_path(self, path_id, paths):
"""Update the paths attribute with list of tuples in the form (modulename format string, directory name)"""self.paths[path_id] = pathsdef collect(module_pattern, path, pred): """Traverse the directory (given by path), import all files as a module module_pattern % filename and find all classes within that match diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index f9b51e2..a09ce20 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -28,6 +28,7 @@ from pyanaconda.product import distributionText, isFinal from pyanaconda.ui import UserInterface, common from pyanaconda.ui.gui.utils import enlightbox, gtk_thread_wait from pyanaconda.product import isFinal, productName, productVersion +import os.path
import gettext _ = lambda x: gettext.ldgettext("anaconda", x) @@ -62,17 +63,19 @@ class GraphicalUserInterface(UserInterface): import ctypes ctypes.CDLL("libAnacondaWidgets.so.0", ctypes.RTLD_GLOBAL)
- @property
- def basemask(self):
return "pyanaconda.ui.gui" def _list_hubs(self):
from hubs.summary import SummaryHubfrom hubs.progress import ProgressHub
"""Return a list of Hub classes to be imported to this interface"""from .hubs.summary import SummaryHubfrom .hubs.progress import ProgressHub return [SummaryHub, ProgressHub]
- def _list_standalone_paths(self):
path = os.path.join(os.path.dirname(__file__), "spokes")return [("pyanaconda.ui.gui.spokes.%s", path)]- def _is_standalone(self, obj):
from spokes import StandaloneSpoke
"""Is the spoke passes as obj standalone?"""
^^ typo?
from .spokes import StandaloneSpoke return isinstance(obj, StandaloneSpoke)def setup(self, data):
@@ -82,13 +85,25 @@ class GraphicalUserInterface(UserInterface): self.data = data
def getActionClasses(self, hubs):
from spokes import StandaloneSpoke
"""Grab all relevant standalone spokes, add them to the passedlist of hubs and order the list according to therelationships between hubs and standalones."""from .spokes import StandaloneSpoke # First, grab a list of all the standalone spokes.
standalones = self._collectActionClasses(self._list_standalone_paths(), StandaloneSpoke)
standalones = self._collectActionClasses(self.paths["spokes"], StandaloneSpoke) # Second, order them according to their relationship
return self._orderActionClasses(standalones, hubs, StandaloneSpoke)
return self._orderActionClasses(standalones, hubs)@property
def paths(self):
_paths = UserInterface.paths.fget(self)_paths.update({"categories": [(self.basemask + ".categories.%s",os.path.join(self.basepath, "categories"))]})return _pathsdef _instantiateAction(self, actionClass): from spokes import StandaloneSpoke
@@ -97,10 +112,15 @@ class GraphicalUserInterface(UserInterface): # spoke API and setting up continue/quit signal handlers. obj = actionClass(self.data, self.storage, self.payload, self.instclass)
# set spoke search paths in Hubsif hasattr(obj, "set_path"):obj.set_path("spokes", self.paths["spokes"])obj.set_path("categories", self.paths["categories"])# If we are doing a kickstart install, some standalone spokes
# could already be filled out. In that case, we do not want
# could already be filled out. In taht case, we do not want
^^^^ typo
# to display them.
if isinstance(obj, StandaloneSpoke) and obj.completed:
if self._is_standalone(obj) and obj.completed: del(obj) return Nonediff --git a/pyanaconda/ui/gui/categories/__init__.py b/pyanaconda/ui/gui/categories/__init__.py index c318901..14dc2a0 100644 --- a/pyanaconda/ui/gui/categories/__init__.py +++ b/pyanaconda/ui/gui/categories/__init__.py @@ -77,7 +77,7 @@ def collect_categories(mask_paths): """ categories = [] for mask, path in mask_paths:
categories.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None)
categories.extend(collect(mask, path, lambda obj: getattr(obj, "displayOnHub", None) != None))return categories
diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index 4447367..f9cc933 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -22,7 +22,9 @@ import gettext _ = lambda x: gettext.ldgettext("anaconda", x)
# pylint: disable-msg=E0611 +import os from gi.repository import GLib
No newline needed and # pylint:... should stay next to the GLib import.
from pyanaconda.flags import flags @@ -111,12 +113,6 @@ class Hub(GUIObject, common.Hub): action.apply() action.execute()
- def _list_spokes_mask_paths(self):
return [("pyanaconda.ui.gui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))]- def _list_categories_mask_paths(self):
return [("pyanaconda.ui.gui.categories.%s", os.path.join(os.path.dirname(__file__), "categories"))] def _collectCategoriesAndSpokes(self): """collects categories and spokes to be displayed on this Hub@@ -128,10 +124,10 @@ class Hub(GUIObject, common.Hub):
# Collect all the categories this hub displays, then collect all the # spokes belonging to all those categories.
categories = sorted(filter(lambda c: c.displayOnHub == self.__class__, collect_categories(self._list_categories_mask_paths())),
categories = sorted(filter(lambda c: c.displayOnHub == self.__class__, collect_categories(self.paths["categories"])), key=lambda c: c.title) for c in categories:
ret[c] = collect_spokes(self._list_spokes_mask_paths(), c.__name__)
ret[c] = collect_spokes(self.paths["spokes"], c.__name__) return ret@@ -250,7 +246,7 @@ class Hub(GUIObject, common.Hub):
@property def continuePossible(self):
return len(self._incompleteSpokes) == 0 and len(self._notReadySpokes) == 0
return len(self._incompleteSpokes)==0 and len(self._notReadySpokes) == 0
^^ missing whitespace
def _updateContinueButton(self): self.continueButton.set_sensitive(self.continuePossible)diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py index c4a15a0..cccbf70 100644 --- a/pyanaconda/ui/tui/__init__.py +++ b/pyanaconda/ui/tui/__init__.py @@ -129,27 +129,43 @@ class TextUserInterface(ui.UserInterface): ui.UserInterface.__init__(self, storage, payload, instclass) self._app = None
- @property
- def basemask(self):
return "pyanaconda.ui.tui"- def _list_hubs(self):
"""returns the list of hubs to use"""return [SummaryHub, ProgressHub]- def _is_standalone(self, spoke):
"""checks if the passed spoke is standalone"""return isinstance(spoke, StandaloneSpoke)- def setup(self, data): """Construct all the objects required to implement this interface. This method must be provided by all subclasses. """ self._app = tui.App(u"Anaconda", yes_or_no_question = YesNoDialog)
self._hubs = [SummaryHub, ProgressHub]
_hubs = self._list_hubs() # First, grab a list of all the standalone spokes. path = os.path.join(os.path.dirname(__file__), "spokes")
actionClasses = self.getActionClasses("pyanaconda.ui.tui.spokes.%s", path, self._hubs, StandaloneSpoke)
spokes = self._collectActionClasses(self.paths["spokes"], StandaloneSpoke)actionClasses = self._orderActionClasses(spokes, _hubs)for klass in actionClasses: obj = klass(self._app, data, self.storage, self.payload, self.instclass) # If we are doing a kickstart install, some standalone spokes # could already be filled out. In taht case, we do not want # to display them.
if isinstance(obj, StandaloneSpoke) and obj.completed:
if self._is_standalone(obj) and obj.completed: del(obj) continueif hasattr(obj, "set_path"):obj.set_path("spokes", self.paths["spokes"])self._app.schedule_screen(obj)def run(self):
diff --git a/pyanaconda/ui/tui/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py index 0f505f7..d669bc7 100644 --- a/pyanaconda/ui/tui/hubs/__init__.py +++ b/pyanaconda/ui/tui/hubs/__init__.py @@ -52,7 +52,7 @@ class TUIHub(TUIObject, common.Hub):
# look for spokes having category present in self.categories for c in self.categories:
spokes = collect_spokes([("pyanaconda.ui.tui.spokes.%s", os.path.join(os.path.dirname(__file__), "spokes"))], c)
spokes = collect_spokes(self.paths["spokes"], c) # sort them according to their priority for s in sorted(spokes, key = lambda s: s.priority):
--- pyanaconda/ui/common.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 11f5e0b..29373fa 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -109,6 +109,26 @@ class UIObject(object): def data(self): return self._data
+class FirstbootSpokeMixIn(object): + """This MixIn class marks Spokes as usable for Firstboot.""" + + @classmethod + def configure_tag(cls): + """This method defines textual id (or list of those) that will + be written into the after-install customization status + file for the firstboot and GIE to know that the spoke was + configured in anaconda.""" + return None + + @classmethod + def firstboot(cls): + """This method is responsible for beginning Spoke initialization + in the firstboot environment (even before __init__). + + It should return True if the spoke is to be shown on the + FirstbootHub and False if it should be skipped.""" + return True + class Spoke(UIObject): """A Spoke is a single configuration screen. There are several different places where a Spoke can be displayed, each of which will have its own
--- pyanaconda/ui/gui/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index a09ce20..748a614 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -59,9 +59,14 @@ class GraphicalUserInterface(UserInterface): self._isFinal = isFinal
# This is a hack to make sure the AnacondaWidgets library gets loaded - # before the introspection stuff. - import ctypes - ctypes.CDLL("libAnacondaWidgets.so.0", ctypes.RTLD_GLOBAL) + # before glade tries to use Anaconda types + # glade file should contain the following line to make this seamless + # + # <requires lib="AnacondaWidgets" version="1.0"/> + # + # but the current (3.4) version of GtkBuilder does not support + # requires elementrs with 3rd party libraries + from gi.repository import AnacondaWidgets
@property def basemask(self):
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/gui/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index a09ce20..748a614 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -59,9 +59,14 @@ class GraphicalUserInterface(UserInterface): self._isFinal = isFinal
# This is a hack to make sure the AnacondaWidgets library gets loaded
# before the introspection stuff.import ctypesctypes.CDLL("libAnacondaWidgets.so.0", ctypes.RTLD_GLOBAL)
# before glade tries to use Anaconda types# glade file should contain the following line to make this seamless## <requires lib="AnacondaWidgets" version="1.0"/>## but the current (3.4) version of GtkBuilder does not support# requires elementrs with 3rd party libraries
^^^ typo
from gi.repository import AnacondaWidgets@property def basemask(self):
--- pyanaconda/ui/gui/__init__.py | 3 +++ 1 file changed, 3 insertions(+)
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 748a614..15df8db 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -44,6 +44,9 @@ class GraphicalUserInterface(UserInterface): """This is the standard GTK+ interface we try to steer everything to using. It is suitable for use both directly and via VNC. """ + + TITLE = "%(productName)s %(productVersion)s INSTALLATION" + def __init__(self, storage, payload, instclass, distributionText = distributionText, isFinal = isFinal):
pyanaconda/ui/gui/__init__.py | 3 +++ 1 file changed, 3 insertions(+)
In the commit message, "it's" should be "its".
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 748a614..15df8db 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -44,6 +44,9 @@ class GraphicalUserInterface(UserInterface): """This is the standard GTK+ interface we try to steer everything to using. It is suitable for use both directly and via VNC. """
- TITLE = "%(productName)s %(productVersion)s INSTALLATION"
- def __init__(self, storage, payload, instclass, distributionText = distributionText, isFinal = isFinal):
Why the all-caps "TITLE"? Also, you'll need to mark this string for translation, and make sure pyanaconda/ui/gui/__init__.py is included in po/POTFILES.in.
Also, curious. This is the same as product.distributionText. Can you give me an example of why this is needed?
- Chris
It was removed in later patch in favour of distributionText (during the merge, because the code was older than distributionText)
As I said in the cover letter.. I kept some patches to document the development ideas and progress.
----- Original Message -----
pyanaconda/ui/gui/__init__.py | 3 +++ 1 file changed, 3 insertions(+)
In the commit message, "it's" should be "its".
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 748a614..15df8db 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -44,6 +44,9 @@ class GraphicalUserInterface(UserInterface): """This is the standard GTK+ interface we try to steer everything to using. It is suitable for use both directly and via VNC. """
- TITLE = "%(productName)s %(productVersion)s INSTALLATION"
- def __init__(self, storage, payload, instclass, distributionText = distributionText, isFinal = isFinal):
Why the all-caps "TITLE"? Also, you'll need to mark this string for translation, and make sure pyanaconda/ui/gui/__init__.py is included in po/POTFILES.in.
Also, curious. This is the same as product.distributionText. Can you give me an example of why this is needed?
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
--- pyanaconda/ui/gui/__init__.py | 350 ++++++++++++++++++++++-------------------- pyanaconda/ui/gui/main.glade | 6 +- 2 files changed, 181 insertions(+), 175 deletions(-)
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 15df8db..0af036d 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -36,10 +36,184 @@ _ = lambda x: gettext.ldgettext("anaconda", x) import logging log = logging.getLogger("anaconda")
-__all__ = ["GraphicalUserInterface", "UIObject", "busyCursor", "unbusyCursor"] +__all__ = ["GraphicalUserInterface", "UIObject", "busyCursor", "unbusyCursor", "QuitDialog"]
_screenshotIndex = 0
+class GUIObject(common.UIObject): + """This is the base class from which all other GUI classes are derived. It + thus contains only attributes and methods that are common to everything + else. It should not be directly instantiated. + + Class attributes: + + builderObjects -- A list of UI object names that should be extracted from + uiFile and exposed for this class to use. If this list + is empty, all objects will be exposed. + + Only the following kinds of objects need to be exported: + + (1) Top-level objects (like GtkDialogs) that are directly + used in Python. + + (2) Top-level objects that are not directly used in + Python, but are used by another object somewhere down + in the hierarchy. This includes things like a custom + GtkImage used by a button that is part of an exported + dialog, and a GtkListStore that is the model of a + Gtk*View that is part of an exported object. + mainWidgetName -- The name of the top-level widget this object + object implements. This will be the widget searched + for in uiFile by the window property. + uiFile -- The location of an XML file that describes the layout + of widgets shown by this object. UI files are + searched for relative to the same directory as this + object's module. + """ + builderObjects = [] + mainWidgetName = None + uiFile = "" + + def __init__(self, data): + """Create a new UIObject instance, including loading its uiFile and + all UI-related objects. + + Instance attributes: + + data -- An instance of a pykickstart Handler object. The Hub + never directly uses this instance. Instead, it passes + it down into Spokes when they are created and applied. + The Hub simply stores this instance so it doesn't need + to be passed by the user. + skipTo -- If this attribute is set to something other than None, + it must be the name of a class (as a string). Then, + the interface will skip to the first instance of that + class in the action list instead of going on to + whatever the next action is normally. + + Note that actions may only skip ahead, never backwards. + Also, standalone spokes may not skip to an individual + spoke off a hub. They can only skip to the hub + itself. + """ + common.UIObject.__init__(self, data) + + if self.__class__ is GUIObject: + raise TypeError("GUIObject is an abstract class") + + self.skipTo = None + self.applyOnSkip = False + + from gi.repository import Gtk + + self.builder = Gtk.Builder() + self.builder.set_translation_domain("anaconda") + self._window = None + + if self.builderObjects: + self.builder.add_objects_from_file(self._findUIFile(), self.builderObjects) + else: + self.builder.add_from_file(self._findUIFile()) + + self.builder.connect_signals(self) + self.window.connect("key-release-event", self._handlePrntScreen) + + def _findUIFile(self): + path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/") + for d in path.split(":"): + testPath = os.path.normpath(d + self.uiFile) + if os.path.isfile(testPath) and os.access(testPath, os.R_OK): + return testPath + + raise IOError("Could not load UI file '%s' for object '%s'" % (self.uiFile, self)) + + def _handlePrntScreen(self, window, event): + global _screenshotIndex + + if event.keyval != Gdk.KEY_Print: + return + + # Make sure the screenshot directory exists. + if not os.access("/tmp/anaconda-screenshots", os.W_OK): + os.mkdir("/tmp/anaconda-screenshots") + + fn = "/tmp/anaconda-screenshots/screenshot-%04d.png" % _screenshotIndex + + win = window.get_window() + width = win.get_width() + height = win.get_height() + + pixbuf = Gdk.pixbuf_get_from_window(win, 0, 0, width, height) + pixbuf.savev(fn, "png", [], []) + + _screenshotIndex += 1 + + @property + def window(self): + """Return the top-level object out of the GtkBuilder representation + previously loaded by the load method. + """ + + # This will raise an AttributeError if the subclass failed to set a + # mainWidgetName attribute, which is exactly what I want. + if not self._window: + self._window = self.builder.get_object(self.mainWidgetName) + + return self._window + + def clear_info(self): + """Clear any info bar from the bottom of the screen.""" + self.window.clear_info() + + def set_error(self, msg): + """Display an info bar along the bottom of the screen with the provided + message. This method is used to display critical errors anaconda + may not be able to do anything about, but that the user may. A + suitable background color and icon will be displayed. + """ + self.window.set_error(msg) + + def set_info(self, msg): + """Display an info bar along the bottom of the screen with the provided + message. This method is used to display informational text - + non-critical warnings during partitioning, for instance. The user + should investigate these messages but doesn't have to. A suitable + background color and icon will be displayed. + """ + self.window.set_info(msg) + + def set_warning(self, msg): + """Display an info bar along the bottom of the screen with the provided + message. This method is used to display errors the user needs to + attend to in order to continue installation. This is the bulk of + messages. A suitable background color and icon will be displayed. + """ + self.window.set_warning(msg) + + +class QuitDialog(GUIObject): + builderObjects = ["quitDialog"] + mainWidgetName = "quitDialog" + uiFile = "main.glade" + + MESSAGE = "" + + def run(self): + if self.MESSAGE: + self.builder.get_object("quit_message").set_label(_(self.MESSAGE)) + rc = self.window.run() + return rc + + +def busyCursor(): + window = Gdk.get_default_root_window() + window.set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH)) + +def unbusyCursor(): + window = Gdk.get_default_root_window() + window.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) + + class GraphicalUserInterface(UserInterface): """This is the standard GTK+ interface we try to steer everything to using. It is suitable for use both directly and via VNC. @@ -48,7 +222,8 @@ class GraphicalUserInterface(UserInterface): TITLE = "%(productName)s %(productVersion)s INSTALLATION"
def __init__(self, storage, payload, instclass, - distributionText = distributionText, isFinal = isFinal): + distributionText = distributionText, isFinal = isFinal, + quitDialog = QuitDialog):
UserInterface.__init__(self, storage, payload, instclass)
@@ -60,7 +235,8 @@ class GraphicalUserInterface(UserInterface):
self._distributionText = distributionText self._isFinal = isFinal - + self._quitDialog = quitDialog + # This is a hack to make sure the AnacondaWidgets library gets loaded # before glade tries to use Anaconda types # glade file should contain the following line to make this seamless @@ -308,7 +484,7 @@ class GraphicalUserInterface(UserInterface): self._actions.pop(0)
def _on_quit_clicked(self): - dialog = QuitDialog(None) + dialog = self._quitDialog(None) with enlightbox(self._currentAction.window, dialog.window): rc = dialog.run() dialog.window.destroy() @@ -316,169 +492,3 @@ class GraphicalUserInterface(UserInterface): if rc == 1: sys.exit(0)
-class GUIObject(common.UIObject): - """This is the base class from which all other GUI classes are derived. It - thus contains only attributes and methods that are common to everything - else. It should not be directly instantiated. - - Class attributes: - - builderObjects -- A list of UI object names that should be extracted from - uiFile and exposed for this class to use. If this list - is empty, all objects will be exposed. - - Only the following kinds of objects need to be exported: - - (1) Top-level objects (like GtkDialogs) that are directly - used in Python. - - (2) Top-level objects that are not directly used in - Python, but are used by another object somewhere down - in the hierarchy. This includes things like a custom - GtkImage used by a button that is part of an exported - dialog, and a GtkListStore that is the model of a - Gtk*View that is part of an exported object. - mainWidgetName -- The name of the top-level widget this object - object implements. This will be the widget searched - for in uiFile by the window property. - uiFile -- The location of an XML file that describes the layout - of widgets shown by this object. UI files are - searched for relative to the same directory as this - object's module. - """ - builderObjects = [] - mainWidgetName = None - uiFile = "" - - def __init__(self, data): - """Create a new UIObject instance, including loading its uiFile and - all UI-related objects. - - Instance attributes: - - data -- An instance of a pykickstart Handler object. The Hub - never directly uses this instance. Instead, it passes - it down into Spokes when they are created and applied. - The Hub simply stores this instance so it doesn't need - to be passed by the user. - skipTo -- If this attribute is set to something other than None, - it must be the name of a class (as a string). Then, - the interface will skip to the first instance of that - class in the action list instead of going on to - whatever the next action is normally. - - Note that actions may only skip ahead, never backwards. - Also, standalone spokes may not skip to an individual - spoke off a hub. They can only skip to the hub - itself. - """ - common.UIObject.__init__(self, data) - - if self.__class__ is GUIObject: - raise TypeError("GUIObject is an abstract class") - - self.skipTo = None - self.applyOnSkip = False - - from gi.repository import Gtk - - self.builder = Gtk.Builder() - self.builder.set_translation_domain("anaconda") - self._window = None - - if self.builderObjects: - self.builder.add_objects_from_file(self._findUIFile(), self.builderObjects) - else: - self.builder.add_from_file(self._findUIFile()) - - self.builder.connect_signals(self) - self.window.connect("key-release-event", self._handlePrntScreen) - - def _findUIFile(self): - path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/") - for d in path.split(":"): - testPath = os.path.normpath(d + self.uiFile) - if os.path.isfile(testPath) and os.access(testPath, os.R_OK): - return testPath - - raise IOError("Could not load UI file '%s' for object '%s'" % (self.uiFile, self)) - - def _handlePrntScreen(self, window, event): - global _screenshotIndex - - if event.keyval != Gdk.KEY_Print: - return - - # Make sure the screenshot directory exists. - if not os.access("/tmp/anaconda-screenshots", os.W_OK): - os.mkdir("/tmp/anaconda-screenshots") - - fn = "/tmp/anaconda-screenshots/screenshot-%04d.png" % _screenshotIndex - - win = window.get_window() - width = win.get_width() - height = win.get_height() - - pixbuf = Gdk.pixbuf_get_from_window(win, 0, 0, width, height) - pixbuf.savev(fn, "png", [], []) - - _screenshotIndex += 1 - - @property - def window(self): - """Return the top-level object out of the GtkBuilder representation - previously loaded by the load method. - """ - - # This will raise an AttributeError if the subclass failed to set a - # mainWidgetName attribute, which is exactly what I want. - if not self._window: - self._window = self.builder.get_object(self.mainWidgetName) - - return self._window - - def clear_info(self): - """Clear any info bar from the bottom of the screen.""" - self.window.clear_info() - - def set_error(self, msg): - """Display an info bar along the bottom of the screen with the provided - message. This method is used to display critical errors anaconda - may not be able to do anything about, but that the user may. A - suitable background color and icon will be displayed. - """ - self.window.set_error(msg) - - def set_info(self, msg): - """Display an info bar along the bottom of the screen with the provided - message. This method is used to display informational text - - non-critical warnings during partitioning, for instance. The user - should investigate these messages but doesn't have to. A suitable - background color and icon will be displayed. - """ - self.window.set_info(msg) - - def set_warning(self, msg): - """Display an info bar along the bottom of the screen with the provided - message. This method is used to display errors the user needs to - attend to in order to continue installation. This is the bulk of - messages. A suitable background color and icon will be displayed. - """ - self.window.set_warning(msg) - -class QuitDialog(GUIObject): - builderObjects = ["quitDialog"] - mainWidgetName = "quitDialog" - uiFile = "main.glade" - - def run(self): - rc = self.window.run() - return rc - -def busyCursor(): - window = Gdk.get_default_root_window() - window.set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH)) - -def unbusyCursor(): - window = Gdk.get_default_root_window() - window.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) diff --git a/pyanaconda/ui/gui/main.glade b/pyanaconda/ui/gui/main.glade index 2f773a4..b2bad86 100644 --- a/pyanaconda/ui/gui/main.glade +++ b/pyanaconda/ui/gui/main.glade @@ -18,11 +18,9 @@ <child> <object class="GtkButton" id="cancelButton"> <property name="label" translatable="yes">_Cancel</property> - <property name="use_action_appearance">False</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> - <property name="use_action_appearance">False</property> <property name="use_underline">True</property> </object> <packing> @@ -34,11 +32,9 @@ <child> <object class="GtkButton" id="quitButton"> <property name="label" translatable="yes">_Quit</property> - <property name="use_action_appearance">False</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> - <property name="use_action_appearance">False</property> <property name="use_underline">True</property> </object> <packing> @@ -74,7 +70,7 @@ </packing> </child> <child> - <object class="GtkLabel" id="label1"> + <object class="GtkLabel" id="quit_message"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes">Are you sure you wish to quit the
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/gui/__init__.py | 350 ++++++++++++++++++++++--------------------
Why did you move the GUIObject class within that file?
I was probably trying so solve the big ugly rebase conflict there..
----- Original Message -----
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/gui/__init__.py | 350 ++++++++++++++++++++++--------------------
Why did you move the GUIObject class within that file?
-- Vratislav Podzimek
Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
On Thu, 2012-12-13 at 11:22 -0500, Martin Sivak wrote:
I was probably trying so solve the big ugly rebase conflict there..
Well, it kind of "modifies git history for nothing". Do you think you could fix it?
--- pyanaconda/ui/common.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 29373fa..af1e5f9 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -434,6 +434,10 @@ def collect(module_pattern, path, pred):
retval = [] for module_file in os.listdir(path): + if (not module_file.endswith(".py")) and \ + (not module_file.endswith(".so")): + continue + if module_file == "__init__.py": continue
@@ -442,17 +446,19 @@ def collect(module_pattern, path, pred): except ValueError: mod_name = module_file
+ mod_info = None + module = None try: imp.acquire_lock() - mod_info = imp.find_module(mod_name, path) + mod_info = imp.find_module(mod_name, [path]) module = imp.load_module(module_pattern % mod_name, *mod_info) imp.release_lock() except ImportError: continue finally: - if mod_info[0]: + if mod_info and mod_info[0]: mod_info[0].close() - + p = lambda obj: inspect.isclass(obj) and pred(obj)
for (name, val) in inspect.getmembers(module, p):
--- pyanaconda/ui/__init__.py | 29 ++++++----------------------- pyanaconda/ui/common.py | 7 ++++++- pyanaconda/ui/gui/__init__.py | 22 ++++++++++------------ pyanaconda/ui/gui/spokes/password.py | 4 ++++ pyanaconda/ui/tui/__init__.py | 11 ++++++++--- 5 files changed, 34 insertions(+), 39 deletions(-)
diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py index 26e540a..3f39a7a 100644 --- a/pyanaconda/ui/__init__.py +++ b/pyanaconda/ui/__init__.py @@ -22,8 +22,7 @@ __all__ = ["UserInterface"]
import os -import inspect -from common import collect +from .common import collect, PathDict
class UserInterface(object): """This is the base class for all kinds of install UIs. It primarily @@ -60,27 +59,11 @@ class UserInterface(object): from pyanaconda.errors import errorHandler errorHandler.ui = self
- @property - def basepath(self): - """returns the directory name for UI subtree""" - return os.path.dirname(inspect.getfile(self.__class__)) - - @property - def basemask(self): - """returns the python module name for basepath directory""" - return "pyanaconda.ui" - - @property - def paths(self): - """return dictionary mapping plugin elements (spokes, hubs, categories) - to a list of tuples (module mask, search path)""" - return { - "spokes": [(self.basemask + ".spokes.%s", - os.path.join(self.basepath, "spokes"))], - "hubs": [(self.basemask + ".hubs.%s", - os.path.join(self.basepath, "hubs"))] - } - + + basepath = os.path.dirname(__file__) + basemask = "pyanaconda.ui" + paths = PathDict({}) + def setup(self, data): """Construct all the objects required to implement this interface. This method must be provided by all subclasses. diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index af1e5f9..b1a8dd3 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -29,7 +29,12 @@ class PathDict(dict): """Dictionary class supporting + operator""" def __add__(self, ext): new_dict = copy.copy(self) - new_dict.update(ext) + for key, value in ext.iteritems(): + try: + new_dict[key].extend(value) + except KeyError: + new_dict[key] = value[:] + return new_dict
class UIObject(object): diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 0af036d..2056307 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -247,9 +247,16 @@ class GraphicalUserInterface(UserInterface): # requires elementrs with 3rd party libraries from gi.repository import AnacondaWidgets
- @property - def basemask(self): - return "pyanaconda.ui.gui" + basemask = "pyanaconda.ui.gui" + basepath = os.path.dirname(__file__) + paths = UserInterface.paths + { + "categories": [(basemask + ".categories.%s", + os.path.join(basepath, "categories"))], + "spokes": [(basemask + ".spokes.%s", + os.path.join(basepath, "spokes"))], + "hubs": [(basemask + ".hubs.%s", + os.path.join(basepath, "hubs"))] + }
def _list_hubs(self): """Return a list of Hub classes to be imported to this interface""" @@ -280,15 +287,6 @@ class GraphicalUserInterface(UserInterface): # Second, order them according to their relationship return self._orderActionClasses(standalones, hubs)
- @property - def paths(self): - _paths = UserInterface.paths.fget(self) - _paths.update({"categories": [(self.basemask + ".categories.%s", - os.path.join(self.basepath, "categories"))] - }) - return _paths - - def _instantiateAction(self, actionClass): from spokes import StandaloneSpoke
diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py index c273846..1bd424c 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -47,6 +47,10 @@ class PasswordSpoke(NormalSpoke): icon = "dialog-password-symbolic" title = N_("ROOT PASSWORD")
+ @classmethod + def firstboot(cls): + return True + def __init__(self, *args): NormalSpoke.__init__(self, *args) self._password = None diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py index cccbf70..b715d0a 100644 --- a/pyanaconda/ui/tui/__init__.py +++ b/pyanaconda/ui/tui/__init__.py @@ -129,9 +129,14 @@ class TextUserInterface(ui.UserInterface): ui.UserInterface.__init__(self, storage, payload, instclass) self._app = None
- @property - def basemask(self): - return "pyanaconda.ui.tui" + basemask = "pyanaconda.ui.tui" + basepath = os.path.dirname(__file__) + paths = ui.UserInterface.paths + { + "spokes": [(basemask + ".spokes.%s", + os.path.join(basepath, "spokes"))], + "hubs": [(basemask + ".hubs.%s", + os.path.join(basepath, "hubs"))] + }
def _list_hubs(self): """returns the list of hubs to use"""
pyanaconda/ui/__init__.py | 29 ++++++----------------------- pyanaconda/ui/common.py | 7 ++++++- pyanaconda/ui/gui/__init__.py | 22 ++++++++++------------ pyanaconda/ui/gui/spokes/password.py | 4 ++++ pyanaconda/ui/tui/__init__.py | 11 ++++++++--- 5 files changed, 34 insertions(+), 39 deletions(-)
Can you merge this with #6?'
- Chris
--- pyanaconda/ui/gui/tools/run-spoke.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/pyanaconda/ui/gui/tools/run-spoke.py b/pyanaconda/ui/gui/tools/run-spoke.py index afddbd1..77ea37d 100755 --- a/pyanaconda/ui/gui/tools/run-spoke.py +++ b/pyanaconda/ui/gui/tools/run-spoke.py @@ -102,8 +102,13 @@ spoke = spokeClass(ksdata, storage, payload, instclass) if hasattr(spoke, "register_event_cb"): spoke.register_event_cb("continue", lambda: Gtk.main_quit()) spoke.register_event_cb("quit", lambda: Gtk.main_quit()) -spoke.initialize()
+if hasattr(spoke, "set_path"): + spoke.set_path("categories", [("pyanaconda.ui.gui.categories.%s", os.path.join(os.path.dirname(__file__), "..", "categories"))]) + spoke.set_path("spokes", [("pyanaconda.ui.gui.spokes.%s", os.path.join(os.path.dirname(__file__), "..", "spokes"))]) + +spoke.initialize() + if not spoke.showable: print "This %s is not showable, but I'll continue anyway." % spokeText
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/gui/tools/run-spoke.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/pyanaconda/ui/gui/tools/run-spoke.py b/pyanaconda/ui/gui/tools/run-spoke.py index afddbd1..77ea37d 100755 --- a/pyanaconda/ui/gui/tools/run-spoke.py +++ b/pyanaconda/ui/gui/tools/run-spoke.py @@ -102,8 +102,13 @@ spoke = spokeClass(ksdata, storage, payload, instclass) if hasattr(spoke, "register_event_cb"): spoke.register_event_cb("continue", lambda: Gtk.main_quit()) spoke.register_event_cb("quit", lambda: Gtk.main_quit()) -spoke.initialize()
+if hasattr(spoke, "set_path"):
- spoke.set_path("categories", [("pyanaconda.ui.gui.categories.%s", os.path.join(os.path.dirname(__file__), "..", "categories"))])
- spoke.set_path("spokes", [("pyanaconda.ui.gui.spokes.%s", os.path.join(os.path.dirname(__file__), "..", "spokes"))])
Please split these two lines in four shorter.
--- pyanaconda/ui/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index b1a8dd3..9c3ef8d 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -24,6 +24,7 @@ import os import imp import inspect import copy +import sys
class PathDict(dict): """Dictionary class supporting + operator""" @@ -453,10 +454,13 @@ def collect(module_pattern, path, pred):
mod_info = None module = None + try: imp.acquire_lock() mod_info = imp.find_module(mod_name, [path]) - module = imp.load_module(module_pattern % mod_name, *mod_info) + module = sys.modules.get(module_pattern % mod_name) + if not module: + module = imp.load_module(module_pattern % mod_name, *mod_info) imp.release_lock() except ImportError: continue
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/ui/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index b1a8dd3..9c3ef8d 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -24,6 +24,7 @@ import os import imp import inspect import copy +import sys
class PathDict(dict): """Dictionary class supporting + operator""" @@ -453,10 +454,13 @@ def collect(module_pattern, path, pred):
mod_info = None module = None
Trailing whitespace ^^^^
try: imp.acquire_lock() mod_info = imp.find_module(mod_name, [path])
module = imp.load_module(module_pattern % mod_name, *mod_info)
module = sys.modules.get(module_pattern % mod_name)if not module:module = imp.load_module(module_pattern % mod_name, *mod_info) imp.release_lock() except ImportError: continue
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index b1a8dd3..9c3ef8d 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -24,6 +24,7 @@ import os import imp import inspect import copy +import sys
class PathDict(dict): """Dictionary class supporting + operator""" @@ -453,10 +454,13 @@ def collect(module_pattern, path, pred):
mod_info = None module = None
try: imp.acquire_lock() mod_info = imp.find_module(mod_name, [path])
module = imp.load_module(module_pattern % mod_name, *mod_info)
module = sys.modules.get(module_pattern % mod_name)if not module:module = imp.load_module(module_pattern % mod_name, *mod_info) imp.release_lock() except ImportError: continue
ACK, but I'm curious under what circumstances you are seeing this, and if that's bad/dumb behavior that should also be changed.
- Chris
Well when one spoke file imports another while we are doing the directory traversal we could try loading the module again directly without this code (and it happened to me in the new firstboot code as I import quite lot of stuff from anaconda).
----- Original Message -----
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index b1a8dd3..9c3ef8d 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -24,6 +24,7 @@ import os import imp import inspect import copy +import sys
class PathDict(dict): """Dictionary class supporting + operator""" @@ -453,10 +454,13 @@ def collect(module_pattern, path, pred):
mod_info = None module = None
try: imp.acquire_lock() mod_info = imp.find_module(mod_name, [path])
module = imp.load_module(module_pattern % mod_name,*mod_info)
module = sys.modules.get(module_pattern % mod_name)if not module:module = imp.load_module(module_pattern %mod_name, *mod_info) imp.release_lock() except ImportError: continue
ACK, but I'm curious under what circumstances you are seeing this, and if that's bad/dumb behavior that should also be changed.
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
--- anaconda | 3 +- pyanaconda/addons.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++ pyanaconda/constants.py | 5 +- pyanaconda/install.py | 5 +- pyanaconda/kickstart.py | 41 +++++++++++-- pyanaconda/ui/__init__.py | 8 +++ 6 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 pyanaconda/addons.py
diff --git a/anaconda b/anaconda index 0789094..01de6f8 100755 --- a/anaconda +++ b/anaconda @@ -32,6 +32,7 @@
import atexit, sys, os, re, time, subprocess from tempfile import mkstemp +from pyanaconda import constants
# keep up with process ID of the window manager if we start it wm_pid = None @@ -828,7 +829,7 @@ if __name__ == "__main__": break
if not ksdata: - ksdata = kickstart.AnacondaKSHandler() + ksdata = kickstart.AnacondaKSHandler(constants.addon_paths)
if ksdata.rescue.rescue: anaconda.rescue = True diff --git a/pyanaconda/addons.py b/pyanaconda/addons.py new file mode 100644 index 0000000..0220107 --- /dev/null +++ b/pyanaconda/addons.py @@ -0,0 +1,142 @@ +# Methods and API for anaconda/firstboot 3rd party addons +# +# Copyright (C) 2012 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Martin Sivak msivak@redhat.com +# + +__all__ = ["AddonSection", "AddonRegistry", "AddonData", "collect_addon_paths"] + +import os +from pykickstart.sections import Section + +def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"): + """This method looks into the directories present + in toplevel_addon_paths and registers each subdirectory + as a new addonidentified by that subdirectory name. + + It then registers spokes, categories and data (ks) + paths for the application to use. By default is looks + for spokes and categories in <addon>/gui/ subdirectory + but that can be changed using the ui_subdir argument.""" + + module_paths = { + "spokes": [], + "ks": [], + "categories": [] + } + + for path in toplevel_addon_paths: + try: + files = os.listdir(path) + except OSError: + files = [] + + for addon_id in files: + addon_ks_path = os.path.join(path, addon_id, "ks") + if os.path.isdir(addon_ks_path): + module_paths["ks"].append(("anaconda.addon.%s.ks.%%s" % addon_id, addon_ks_path)) + + addon_spoke_path = os.path.join(path, addon_id, ui_subdir, "spokes") + if os.path.isdir(addon_spoke_path): + module_paths["spokes"].append(("anaconda.addon.%s.spokes.%%s" % addon_id, addon_spoke_path)) + + addon_category_path = os.path.join(path, addon_id, ui_subdir, "categories") + if os.path.isdir(addon_spoke_path): + module_paths["categories"].append(("anaconda.addon.%s.categories.%%s" % addon_id, addon_category_path)) + + return module_paths + +class AddonRegistry(object): + """This class represents the ksdata.addons object and + maintains the ids and data structures for loaded + addons. + + It acts as a proxy during kickstart save. + """ + + def __init__(self, dictionary): + self.__dict__ = dictionary + + def __str__(self): + return reduce(lambda acc,(id, addon): acc + "%%addon %s\n%s%%end\n" % (id, str(addon)), + self.__dict__.iteritems(), "") + + def execute(self, storage, ksdata, instClass): + """This method calls execute on all the registered addons.""" + for k, v in self.__dict__.iteritems(): + v.execute(storage, ksdata, instClass) + +class AddonData(object): + """This is a common parent class for loading and storing + 3rd party data to kickstart. It is instantiated by + kickstart parser and stored as ksdata.addons.<name> + to be used in the user interfaces. + + The mandatory method handle_line receives all lines + from the corresponding addon section in kickstart and + the mandatory __str__ implementation is responsible for + returning the proper kickstart text (to be placed into + the %addon section) back. + + There is also a mandatory method execute, which should + make all the described changes to the installed system. + """ + + def __init__(self, name): + self.name = name + self.content = "" + + def __str__(self): + return self.content + + def execute(self, storage, ksdata, instClass): + """Make the changes to the underlying system.""" + pass + + def handle_line(self, line): + """Process one kickstart line.""" + self.content += line + +class AddonSection(Section): + sectionOpen = "%addon" + + def __init__(self, *args, **kwargs): + Section.__init__(self, *args, **kwargs) + self.addon_id = None + + def handleLine(self, line): + if not self.handler: + return + + if not self.addon_id: + return + + addon = getattr(self.handler.addon, self.addon_id) + addon.handle_line(line) + + def handleHeader(self, lineno, args): + """Process the arguments to the %addon header.""" + Section.handleHeader(self, lineno, args) + op = KSOptionParser(version=self.version) + (opts, extra) = op.parse_args(args=args[1:], lineno=lineno) + self.addon_id = extra[0] + + # if the addon is not registered, create dummy placeholder for it + if self.addon_id and not hasattr(self.handler.addon, self.addon_id): + setattr(self.handler.addon, self.addon_id, AnacondaKSAddon(self.addon_id)) + diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py index 7ff5020..5cc066c 100644 --- a/pyanaconda/constants.py +++ b/pyanaconda/constants.py @@ -38,6 +38,9 @@ MAX_PART_SIZE = 1024*1024*1024 # install key related constants SKIP_KEY = -50
+# where to look for 3rd party addons +addon_paths = ["/usr/share/anaconda/addons"] + # pull in kickstart constants as well from pykickstart.constants import *
@@ -96,4 +99,4 @@ USEVNC = _("Start VNC") USETEXT = _("Use text mode")
# Runlevel files -RUNLEVELS = {3: 'multi-user.target', 5: 'graphical.target'} \ No newline at end of file +RUNLEVELS = {3: 'multi-user.target', 5: 'graphical.target'} diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 6754a0a..4e552ea 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -52,7 +52,7 @@ def doConfiguration(storage, payload, ksdata, instClass): from pyanaconda import progress from pyanaconda.kickstart import runPostScripts
- progress.send_init(4) + progress.send_init(5)
# Now run the execute methods of ksdata that require an installed system # to be present first. @@ -79,6 +79,9 @@ def doConfiguration(storage, payload, ksdata, instClass): ksdata.group.execute(storage, ksdata, instClass, u) ksdata.user.execute(storage, ksdata, instClass, u)
+ with progress_report(_("Configuring addons")): + ksdata.addon.execute(storage, ksdata, instClass, u) + with progress_report(_("Running post install scripts")): runPostScripts(ksdata.scripts)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 1ec294a..693ecd4 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -52,6 +52,8 @@ from pyanaconda import network from pyanaconda.simpleconfig import SimpleConfigFile from pyanaconda.users import getPassAlgo from pyanaconda.desktop import Desktop +from .ui.common import collect +from .addons import AddonSection, AddonData, AddonRegistry
from pykickstart.base import KickstartCommand from pykickstart.constants import * @@ -78,6 +80,7 @@ packagesSeen = False # so it needs to know about them in some additional way: have the topology ready. topology = None
+ class AnacondaKSScript(KSScript): def run(self, chroot): if self.inChroot: @@ -1342,11 +1345,38 @@ dataMap = {
superclass = returnClassForVersion()
+ class AnacondaKSHandler(superclass): - def __init__ (self): + AddonClassType = AddonData + + def __init__ (self, addon_paths = []): superclass.__init__(self, commandUpdates=commandMap, dataUpdates=dataMap) self.onPart = {}
+ # collect all kickstart addons for anaconda to addons dictionary + # which maps addon_id to it's own data structure based on BaseData + # with execute method + addons = {} + + # collect all AddonData subclasses from + # for p in addon_paths: <p>/<plugin id>/ks/*.(py|so) + # and register them under <plugin id> name + for module_name, path in addon_paths: + addon_id = os.path.basename(os.path.dirname(os.path.abspath(path))) + if not os.path.isdir(path): + continue + + classes = collect(module_name, path, lambda cls: issubclass(cls, self.AddonClassType)) + print classes + if classes: + addons[addon_id] = classes[0](name = addon_id) + + # Prepare the final structures for 3rd party addons + self.addon = AddonRegistry(addons) + + def __str__(self): + return superclass.__str__(self) + "\n" + str(self.addon) + class AnacondaPreParser(KickstartParser): # A subclass of KickstartParser that only looks for %pre scripts and # sets them up to be run. All other scripts and commands are ignored. @@ -1362,7 +1392,9 @@ class AnacondaPreParser(KickstartParser): self.registerSection(NullSection(self.handler, sectionOpen="%post")) self.registerSection(NullSection(self.handler, sectionOpen="%traceback")) self.registerSection(NullSection(self.handler, sectionOpen="%packages")) - + self.registerSection(NullSection(self.handler, sectionOpen="%addon")) + + class AnacondaKSParser(KickstartParser): def __init__ (self, handler, followIncludes=True, errorsAreFatal=True, missingIncludeIsFatal=True, scriptClass=AnacondaKSScript): @@ -1380,7 +1412,8 @@ class AnacondaKSParser(KickstartParser): self.registerSection(PostScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(PackageSection(self.handler)) - + self.registerSection(AddonSection(self.handler)) + def preScriptPass(f): # The first pass through kickstart file processing - look for %pre scripts # and run them. This must come in a separate pass in case a script @@ -1399,7 +1432,7 @@ def preScriptPass(f): def parseKickstart(f): # preprocessing the kickstart file has already been handled in initramfs.
- handler = AnacondaKSHandler() + handler = AnacondaKSHandler(constants.addon_paths) ksparser = AnacondaKSParser(handler)
# We need this so all the /dev/disk/* stuff is set up before parsing. diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py index 3f39a7a..9337a2a 100644 --- a/pyanaconda/ui/__init__.py +++ b/pyanaconda/ui/__init__.py @@ -63,6 +63,14 @@ class UserInterface(object): basepath = os.path.dirname(__file__) basemask = "pyanaconda.ui" paths = PathDict({}) + + @classmethod + def update_paths(cls, pathdict): + """Receives pathdict and appends it's contents to the current + class defined search path dictionary.""" + for k,v in pathdict.iteritems(): + cls.paths.setdefault(k, []) + cls.paths[k].extend(v)
def setup(self, data): """Construct all the objects required to implement this interface.
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
anaconda | 3 +- pyanaconda/addons.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++ pyanaconda/constants.py | 5 +- pyanaconda/install.py | 5 +- pyanaconda/kickstart.py | 41 +++++++++++-- pyanaconda/ui/__init__.py | 8 +++ 6 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 pyanaconda/addons.py
diff --git a/anaconda b/anaconda index 0789094..01de6f8 100755 --- a/anaconda +++ b/anaconda @@ -32,6 +32,7 @@
import atexit, sys, os, re, time, subprocess from tempfile import mkstemp +from pyanaconda import constants
# keep up with process ID of the window manager if we start it wm_pid = None @@ -828,7 +829,7 @@ if __name__ == "__main__": break
if not ksdata:
ksdata = kickstart.AnacondaKSHandler()
ksdata = kickstart.AnacondaKSHandler(constants.addon_paths)if ksdata.rescue.rescue: anaconda.rescue = True
diff --git a/pyanaconda/addons.py b/pyanaconda/addons.py new file mode 100644 index 0000000..0220107 --- /dev/null +++ b/pyanaconda/addons.py @@ -0,0 +1,142 @@ +# Methods and API for anaconda/firstboot 3rd party addons +# +# Copyright (C) 2012 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Martin Sivak msivak@redhat.com +#
+__all__ = ["AddonSection", "AddonRegistry", "AddonData", "collect_addon_paths"]
+import os +from pykickstart.sections import Section
+def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"):
no spaces needed^^^^^^^
- """This method looks into the directories present
in toplevel_addon_paths and registers each subdirectoryas a new addonidentified by that subdirectory name.
^^^ space missing
It then registers spokes, categories and data (ks)paths for the application to use. By default is looksfor spokes and categories in <addon>/gui/ subdirectorybut that can be changed using the ui_subdir argument."""- module_paths = {
"spokes": [],"ks": [],"categories": []}- for path in toplevel_addon_paths:
try:files = os.listdir(path)
Maybe 'directories' instead of 'files' would be better here? We expect directories there, right?
except OSError:files = []for addon_id in files:addon_ks_path = os.path.join(path, addon_id, "ks")if os.path.isdir(addon_ks_path):module_paths["ks"].append(("anaconda.addon.%s.ks.%%s" % addon_id, addon_ks_path))addon_spoke_path = os.path.join(path, addon_id, ui_subdir, "spokes")if os.path.isdir(addon_spoke_path):module_paths["spokes"].append(("anaconda.addon.%s.spokes.%%s" % addon_id, addon_spoke_path))addon_category_path = os.path.join(path, addon_id, ui_subdir, "categories")if os.path.isdir(addon_spoke_path):module_paths["categories"].append(("anaconda.addon.%s.categories.%%s" % addon_id, addon_category_path))- return module_paths
+class AddonRegistry(object):
- """This class represents the ksdata.addons object and
maintains the ids and data structures for loadedaddons.It acts as a proxy during kickstart save.- """
- def __init__(self, dictionary):
self.__dict__ = dictionary- def __str__(self):
return reduce(lambda acc,(id, addon): acc + "%%addon %s\n%s%%end\n" % (id, str(addon)),self.__dict__.iteritems(), "")- def execute(self, storage, ksdata, instClass):
"""This method calls execute on all the registered addons."""for k, v in self.__dict__.iteritems():v.execute(storage, ksdata, instClass)+class AddonData(object):
- """This is a common parent class for loading and storing
3rd party data to kickstart. It is instantiated bykickstart parser and stored as ksdata.addons.<name>to be used in the user interfaces.The mandatory method handle_line receives all linesfrom the corresponding addon section in kickstart andthe mandatory __str__ implementation is responsible forreturning the proper kickstart text (to be placed intothe %addon section) back.There is also a mandatory method execute, which shouldmake all the described changes to the installed system.- """
- def __init__(self, name):
self.name = nameself.content = ""- def __str__(self):
return self.content- def execute(self, storage, ksdata, instClass):
"""Make the changes to the underlying system."""pass- def handle_line(self, line):
"""Process one kickstart line."""self.content += line
Does line end with the newline character?
+class AddonSection(Section):
- sectionOpen = "%addon"
- def __init__(self, *args, **kwargs):
Section.__init__(self, *args, **kwargs)self.addon_id = None- def handleLine(self, line):
if not self.handler:returnif not self.addon_id:returnaddon = getattr(self.handler.addon, self.addon_id)addon.handle_line(line)- def handleHeader(self, lineno, args):
"""Process the arguments to the %addon header."""Section.handleHeader(self, lineno, args)op = KSOptionParser(version=self.version)(opts, extra) = op.parse_args(args=args[1:], lineno=lineno)self.addon_id = extra[0]# if the addon is not registered, create dummy placeholder for itif self.addon_id and not hasattr(self.handler.addon, self.addon_id):setattr(self.handler.addon, self.addon_id, AnacondaKSAddon(self.addon_id))
Why are you using setattr here?
diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py index 7ff5020..5cc066c 100644 --- a/pyanaconda/constants.py +++ b/pyanaconda/constants.py @@ -38,6 +38,9 @@ MAX_PART_SIZE = 1024*1024*1024 # install key related constants SKIP_KEY = -50
+# where to look for 3rd party addons +addon_paths = ["/usr/share/anaconda/addons"]
Could you change this to ADDON_PATHS?
# pull in kickstart constants as well from pykickstart.constants import *
@@ -96,4 +99,4 @@ USEVNC = _("Start VNC") USETEXT = _("Use text mode")
# Runlevel files -RUNLEVELS = {3: 'multi-user.target', 5: 'graphical.target'} \ No newline at end of file +RUNLEVELS = {3: 'multi-user.target', 5: 'graphical.target'} diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 6754a0a..4e552ea 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -52,7 +52,7 @@ def doConfiguration(storage, payload, ksdata, instClass): from pyanaconda import progress from pyanaconda.kickstart import runPostScripts
- progress.send_init(4)
progress.send_init(5)
# Now run the execute methods of ksdata that require an installed system # to be present first.
@@ -79,6 +79,9 @@ def doConfiguration(storage, payload, ksdata, instClass): ksdata.group.execute(storage, ksdata, instClass, u) ksdata.user.execute(storage, ksdata, instClass, u)
- with progress_report(_("Configuring addons")):
^^^^ I guess we need a better word here.
ksdata.addon.execute(storage, ksdata, instClass, u)- with progress_report(_("Running post install scripts")): runPostScripts(ksdata.scripts)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 1ec294a..693ecd4 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -52,6 +52,8 @@ from pyanaconda import network from pyanaconda.simpleconfig import SimpleConfigFile from pyanaconda.users import getPassAlgo from pyanaconda.desktop import Desktop +from .ui.common import collect +from .addons import AddonSection, AddonData, AddonRegistry
from pykickstart.base import KickstartCommand from pykickstart.constants import * @@ -78,6 +80,7 @@ packagesSeen = False # so it needs to know about them in some additional way: have the topology ready. topology = None
No newline needed here and the same goes for the AnacondaKSHandler class.
class AnacondaKSScript(KSScript): def run(self, chroot): if self.inChroot: @@ -1342,11 +1345,38 @@ dataMap = {
superclass = returnClassForVersion()
class AnacondaKSHandler(superclass):
- def __init__ (self):
- AddonClassType = AddonData
^^^ why CamelCase here?
def __init__ (self, addon_paths = []): superclass.__init__(self, commandUpdates=commandMap, dataUpdates=dataMap) self.onPart = {}
# collect all kickstart addons for anaconda to addons dictionary# which maps addon_id to it's own data structure based on BaseData# with execute methodaddons = {}# collect all AddonData subclasses from# for p in addon_paths: <p>/<plugin id>/ks/*.(py|so)# and register them under <plugin id> namefor module_name, path in addon_paths:addon_id = os.path.basename(os.path.dirname(os.path.abspath(path)))
Are all os.path.* calls needed here? Why to get abspath when you use dirname and basename afterwards?
if not os.path.isdir(path):continueclasses = collect(module_name, path, lambda cls: issubclass(cls, self.AddonClassType))print classes
I guess we don't need this print.
if classes:addons[addon_id] = classes[0](name = addon_id)
Could there be multiple classes? And if yes, how it is ensured that the right one is the first in the list?
# Prepare the final structures for 3rd party addonsself.addon = AddonRegistry(addons)- def __str__(self):
return superclass.__str__(self) + "\n" + str(self.addon)class AnacondaPreParser(KickstartParser): # A subclass of KickstartParser that only looks for %pre scripts and # sets them up to be run. All other scripts and commands are ignored. @@ -1362,7 +1392,9 @@ class AnacondaPreParser(KickstartParser): self.registerSection(NullSection(self.handler, sectionOpen="%post")) self.registerSection(NullSection(self.handler, sectionOpen="%traceback")) self.registerSection(NullSection(self.handler, sectionOpen="%packages"))
self.registerSection(NullSection(self.handler, sectionOpen="%addon"))class AnacondaKSParser(KickstartParser): def __init__ (self, handler, followIncludes=True, errorsAreFatal=True, missingIncludeIsFatal=True, scriptClass=AnacondaKSScript): @@ -1380,7 +1412,8 @@ class AnacondaKSParser(KickstartParser): self.registerSection(PostScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(PackageSection(self.handler))
self.registerSection(AddonSection(self.handler))def preScriptPass(f): # The first pass through kickstart file processing - look for %pre scripts # and run them. This must come in a separate pass in case a script @@ -1399,7 +1432,7 @@ def preScriptPass(f): def parseKickstart(f): # preprocessing the kickstart file has already been handled in initramfs.
- handler = AnacondaKSHandler()
handler = AnacondaKSHandler(constants.addon_paths) ksparser = AnacondaKSParser(handler)
# We need this so all the /dev/disk/* stuff is set up before parsing.
diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py index 3f39a7a..9337a2a 100644 --- a/pyanaconda/ui/__init__.py +++ b/pyanaconda/ui/__init__.py @@ -63,6 +63,14 @@ class UserInterface(object): basepath = os.path.dirname(__file__) basemask = "pyanaconda.ui" paths = PathDict({})
@classmethod
def update_paths(cls, pathdict):
"""Receives pathdict and appends it's contents to the currentclass defined search path dictionary."""for k,v in pathdict.iteritems():cls.paths.setdefault(k, [])cls.paths[k].extend(v)def setup(self, data): """Construct all the objects required to implement this interface.
- def handle_line(self, line):
"""Process one kickstart line."""self.content += lineDoes line end with the newline character?
Hmm.. good question, I have no idea :)
if self.addon_id and not hasattr(self.handler.addon,self.addon_id):
setattr(self.handler.addon, self.addon_id,AnacondaKSAddon(self.addon_id))
Why are you using setattr here?
Because self.addon_id (the id of the addon) is a string we got from the filesystem and I need to set an attribute with the same name?
- with progress_report(_("Configuring addons")):
^^^^ I guess we need a better word here.
Probably, but we will have real 3rd party addons too here, not just externally maintained components
topology = None
class AnacondaKSScript(KSScript):
No newline needed here and the same goes for the AnacondaKSHandler class.
That is actually PEP8 style.. two lines between classes. But I can remove it of course..
os.path.basename(os.path.dirname(os.path.abspath(path)))
Are all os.path.* calls needed here? Why to get abspath when you use dirname and basename afterwards?
Because if any path contains "something/../spokes" you will get "something/.." out of dirname and ".." out of basename.
if not os.path.isdir(path):continueclasses = collect(module_name, path, lambda cls:issubclass(cls, self.AddonClassType))
print classesI guess we don't need this print.
if classes:addons[addon_id] = classes[0](name = addon_id)Could there be multiple classes? And if yes, how it is ensured that the right one is the first in the list?
Only one matching class per plugin was the plan. But this is still open to changes. I need to see real use cases from other teams first (ID handling would have to change if we allow more classes).
On Thu, 2012-12-13 at 11:59 -0500, Martin Sivak wrote:
- def handle_line(self, line):
"""Process one kickstart line."""self.content += lineDoes line end with the newline character?
Hmm.. good question, I have no idea :)
if self.addon_id and not hasattr(self.handler.addon,self.addon_id):
setattr(self.handler.addon, self.addon_id,AnacondaKSAddon(self.addon_id))
Why are you using setattr here?
Because self.addon_id (the id of the addon) is a string we got from the filesystem and I need to set an attribute with the same name?
Ah, of course, I somehow got confused. :)
- with progress_report(_("Configuring addons")):
^^^^ I guess we need a better word here.Probably, but we will have real 3rd party addons too here, not just externally maintained components
I don't like 'Configuring', 'addon' is another story.
topology = None
class AnacondaKSScript(KSScript):
No newline needed here and the same goes for the AnacondaKSHandler class.
That is actually PEP8 style.. two lines between classes. But I can remove it of course..
os.path.basename(os.path.dirname(os.path.abspath(path)))
Are all os.path.* calls needed here? Why to get abspath when you use dirname and basename afterwards?
Because if any path contains "something/../spokes" you will get "something/.." out of dirname and ".." out of basename.
Ah, okay. Maybe there should be something like os.path.expandpath() for these cases.
if not os.path.isdir(path):continueclasses = collect(module_name, path, lambda cls:issubclass(cls, self.AddonClassType))
print classesI guess we don't need this print.
if classes:addons[addon_id] = classes[0](name = addon_id)Could there be multiple classes? And if yes, how it is ensured that the right one is the first in the list?
Only one matching class per plugin was the plan. But this is still open to changes. I need to see real use cases from other teams first (ID handling would have to change if we allow more classes).
Okay.
Something that just occurred to me when looking at this patch is that ksvalidator is going to fail on processing any kickstart file with a %addon section (or any other section defined outside pykickstart). Do we expect to see such kickstart files outside of anaconda? If so, this is going to need work.
- for path in toplevel_addon_paths:
try:files = os.listdir(path)except OSError:files = []for addon_id in files:addon_ks_path = os.path.join(path, addon_id, "ks")if os.path.isdir(addon_ks_path):module_paths["ks"].append(("anaconda.addon.%s.ks.%%s" % addon_id, addon_ks_path))addon_spoke_path = os.path.join(path, addon_id, ui_subdir, "spokes")if os.path.isdir(addon_spoke_path):module_paths["spokes"].append(("anaconda.addon.%s.spokes.%%s" % addon_id, addon_spoke_path))addon_category_path = os.path.join(path, addon_id, ui_subdir, "categories")if os.path.isdir(addon_spoke_path):module_paths["categories"].append(("anaconda.addon.%s.categories.%%s" % addon_id, addon_category_path))
Do you mean pyanaconda.addon in all these paths?
diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 6754a0a..4e552ea 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -52,7 +52,7 @@ def doConfiguration(storage, payload, ksdata, instClass): from pyanaconda import progress from pyanaconda.kickstart import runPostScripts
- progress.send_init(4)
progress.send_init(5)
# Now run the execute methods of ksdata that require an installed system # to be present first.
@@ -79,6 +79,9 @@ def doConfiguration(storage, payload, ksdata, instClass): ksdata.group.execute(storage, ksdata, instClass, u) ksdata.user.execute(storage, ksdata, instClass, u)
- with progress_report(_("Configuring addons")):
ksdata.addon.execute(storage, ksdata, instClass, u)- with progress_report(_("Running post install scripts")): runPostScripts(ksdata.scripts)
You may want to conditionalize these two so that they only happen if there are addons to begin with.
@@ -78,6 +80,7 @@ packagesSeen = False # so it needs to know about them in some additional way: have the topology ready. topology = None
class AnacondaKSScript(KSScript): def run(self, chroot): if self.inChroot: @@ -1342,11 +1345,38 @@ dataMap = {
superclass = returnClassForVersion()
class AnacondaKSHandler(superclass):
- def __init__ (self):
AddonClassType = AddonData
def __init__ (self, addon_paths = []): superclass.__init__(self, commandUpdates=commandMap, dataUpdates=dataMap) self.onPart = {}
# collect all kickstart addons for anaconda to addons dictionary# which maps addon_id to it's own data structure based on BaseData# with execute methodaddons = {}# collect all AddonData subclasses from# for p in addon_paths: <p>/<plugin id>/ks/*.(py|so)# and register them under <plugin id> namefor module_name, path in addon_paths:addon_id = os.path.basename(os.path.dirname(os.path.abspath(path)))if not os.path.isdir(path):continueclasses = collect(module_name, path, lambda cls: issubclass(cls, self.AddonClassType))print classesif classes:addons[addon_id] = classes[0](name = addon_id)# Prepare the final structures for 3rd party addonsself.addon = AddonRegistry(addons)def __str__(self):
return superclass.__str__(self) + "\n" + str(self.addon)class AnacondaPreParser(KickstartParser): # A subclass of KickstartParser that only looks for %pre scripts and # sets them up to be run. All other scripts and commands are ignored.
This will all get executed twice, just so you know: Once for processing the kickstart file in the special %pre step, and once for the real pass. That may not matter. I don't know how much stuff is expected to happen in this code and how many addons we realistically expect. It might also be best in its own method.
- Chris
Something that just occurred to me when looking at this patch is that ksvalidator is going to fail on processing any kickstart file with a %addon section (or any other section defined outside pykickstart). Do we expect to see such kickstart files outside of anaconda? If so, this is going to need work.
You are right. My goal was to not touch pykickstart at all. How does the validator handle %pre and %post sections? Those are also defined in pyanaconda/kickstart.py
module_paths["spokes"].append(("anaconda.addon.%s.spokes.%%s"
% addon_id, addon_spoke_path))
addon_category_path = os.path.join(path, addon_id,ui_subdir, "categories")
if os.path.isdir(addon_spoke_path):module_paths["categories"].append(("anaconda.addon.%s.categories.%%s"% addon_id, addon_category_path))
Do you mean pyanaconda.addon in all these paths?
Yes.. but it should not matter as it is just a key in sys.modules, I will fix it to be consistent with the rest of course.
- with progress_report(_("Configuring addons")):
ksdata.addon.execute(storage, ksdata, instClass, u)You may want to conditionalize these two so that they only happen if there are addons to begin with.
Good idea.
class AnacondaKSHandler(superclass):
- def __init__ (self):
AddonClassType = AddonData
def __init__ (self, addon_paths = []): superclass.__init__(self, commandUpdates=commandMap, dataUpdates=dataMap) self.onPart = {}
# collect all kickstart addons for anaconda to addonsdictionary
# which maps addon_id to it's own data structure based onBaseData
# with execute methodaddons = {}# collect all AddonData subclasses from# for p in addon_paths: <p>/<plugin id>/ks/*.(py|so)# and register them under <plugin id> namefor module_name, path in addon_paths:addon_id =os.path.basename(os.path.dirname(os.path.abspath(path)))
if not os.path.isdir(path):continueclasses = collect(module_name, path, lambda cls:issubclass(cls, self.AddonClassType))
print classesif classes:addons[addon_id] = classes[0](name = addon_id)# Prepare the final structures for 3rd party addonsself.addon = AddonRegistry(addons)- def __str__(self):
return superclass.__str__(self) + "\n" + str(self.addon)This will all get executed twice, just so you know: Once for processing the kickstart file in the special %pre step, and once for the real pass. That may not matter. I don't know how much stuff is expected to happen in this code and how many addons we realistically expect. It might also be best in its own method.
Ah, yes you are right. But I am not passing the addon_paths in that step, so it will skip the loop.
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
--- pyanaconda/addons.py | 47 ++++++++++++++++++++++++++++++++----------- pyanaconda/install.py | 5 ++++- pyanaconda/kickstart.py | 2 +- pyanaconda/ui/gui/__init__.py | 10 ++++++--- 4 files changed, 47 insertions(+), 17 deletions(-)
diff --git a/pyanaconda/addons.py b/pyanaconda/addons.py index 0220107..c2510fa 100644 --- a/pyanaconda/addons.py +++ b/pyanaconda/addons.py @@ -23,7 +23,8 @@ __all__ = ["AddonSection", "AddonRegistry", "AddonData", "collect_addon_paths"]
import os from pykickstart.sections import Section - +from pykickstart.parser import KSOptionParser + def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"): """This method looks into the directories present in toplevel_addon_paths and registers each subdirectory @@ -39,13 +40,13 @@ def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"): "ks": [], "categories": [] } - + for path in toplevel_addon_paths: try: files = os.listdir(path) except OSError: files = [] - + for addon_id in files: addon_ks_path = os.path.join(path, addon_id, "ks") if os.path.isdir(addon_ks_path): @@ -68,7 +69,7 @@ class AddonRegistry(object):
It acts as a proxy during kickstart save. """ - + def __init__(self, dictionary): self.__dict__ = dictionary
@@ -76,11 +77,20 @@ class AddonRegistry(object): return reduce(lambda acc,(id, addon): acc + "%%addon %s\n%s%%end\n" % (id, str(addon)), self.__dict__.iteritems(), "")
- def execute(self, storage, ksdata, instClass): + # pylint: disable-msg=C0103 + def execute(self, storage, ksdata, instClass, users): """This method calls execute on all the registered addons.""" for k, v in self.__dict__.iteritems(): - v.execute(storage, ksdata, instClass) - + if hasattr(v, "execute"): + v.execute(storage, ksdata, instClass, users) + + def setup(self, storage, ksdata, instClass): + """This method calls setup on all the registered addons.""" + for k, v in self.__dict__.iteritems(): + if hasattr(v, "setup"): + v.setup(storage, ksdata, instClass) + + class AddonData(object): """This is a common parent class for loading and storing 3rd party data to kickstart. It is instantiated by @@ -96,7 +106,7 @@ class AddonData(object): There is also a mandatory method execute, which should make all the described changes to the installed system. """ - + def __init__(self, name): self.name = name self.content = "" @@ -104,8 +114,21 @@ class AddonData(object): def __str__(self): return self.content
- def execute(self, storage, ksdata, instClass): - """Make the changes to the underlying system.""" + # pylint: disable-msg=C0103 + def setup(self, storage, ksdata, instClass): + """Make the changes to the install system. + + This method is called before the installation + is started and directly from spokes. It must be possible + to call it multiple times without breaking the environment.""" + pass + + def execute(self, storage, ksdata, instClass, users): + """Make the changes to the underlying system. + + This method is called only once in the post-install + setup phase. + """ pass
def handle_line(self, line): @@ -133,10 +156,10 @@ class AddonSection(Section): """Process the arguments to the %addon header.""" Section.handleHeader(self, lineno, args) op = KSOptionParser(version=self.version) - (opts, extra) = op.parse_args(args=args[1:], lineno=lineno) + (_opts, extra) = op.parse_args(args=args[1:], lineno=lineno) self.addon_id = extra[0]
# if the addon is not registered, create dummy placeholder for it if self.addon_id and not hasattr(self.handler.addon, self.addon_id): - setattr(self.handler.addon, self.addon_id, AnacondaKSAddon(self.addon_id)) + setattr(self.handler.addon, self.addon_id, AddonData(self.addon_id))
diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 4e552ea..af214ba 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -109,9 +109,12 @@ def doInstall(storage, payload, ksdata, instClass): steps = len(storage.devicetree.findActions(type="create", object="format")) + \ len(storage.devicetree.findActions(type="resize", object="format")) + \ len(storage.devicetree.findActions(type="migrate", object="format")) - steps += 4 # packages setup, packages, bootloader, post install + steps += 5 # pre setup phase, packages setup, packages, bootloader, post install progress.send_init(steps)
+ with progress_report(_("Setting up the install environment")): + ksdata.addon.setup(storage, ksdata, instClass) + # Do partitioning. payload.preStorage() turnOnFilesystems(storage) diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 693ecd4..b242e92 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -1432,7 +1432,7 @@ def preScriptPass(f): def parseKickstart(f): # preprocessing the kickstart file has already been handled in initramfs.
- handler = AnacondaKSHandler(constants.addon_paths) + handler = AnacondaKSHandler(addon_paths) ksparser = AnacondaKSParser(handler)
# We need this so all the /dev/disk/* stuff is set up before parsing. diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 2056307..1ffb728 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -18,7 +18,7 @@ # # Red Hat Author(s): Chris Lumens clumens@redhat.com # -import importlib, inspect, os, sys, time +import inspect, os, sys, time import meh.ui.gui
from gi.repository import Gdk @@ -120,8 +120,12 @@ class GUIObject(common.UIObject):
def _findUIFile(self): path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/") + + # look for files in the same dir as where the class is defined + path += ":%s" % os.path.dirname(inspect.getfile(self.__class__)) + for d in path.split(":"): - testPath = os.path.normpath(d + self.uiFile) + testPath = os.path.normpath(os.path.join(d, self.uiFile)) if os.path.isfile(testPath) and os.access(testPath, os.R_OK): return testPath
@@ -265,7 +269,7 @@ class GraphicalUserInterface(UserInterface): return [SummaryHub, ProgressHub]
def _is_standalone(self, obj): - """Is the spoke passes as obj standalone?""" + """Is the spoke passed as obj standalone?""" from .spokes import StandaloneSpoke return isinstance(obj, StandaloneSpoke)
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
pyanaconda/addons.py | 47 ++++++++++++++++++++++++++++++++----------- pyanaconda/install.py | 5 ++++- pyanaconda/kickstart.py | 2 +- pyanaconda/ui/gui/__init__.py | 10 ++++++--- 4 files changed, 47 insertions(+), 17 deletions(-)
diff --git a/pyanaconda/addons.py b/pyanaconda/addons.py index 0220107..c2510fa 100644 --- a/pyanaconda/addons.py +++ b/pyanaconda/addons.py @@ -23,7 +23,8 @@ __all__ = ["AddonSection", "AddonRegistry", "AddonData", "collect_addon_paths"]
import os from pykickstart.sections import Section
+from pykickstart.parser import KSOptionParser
def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"): """This method looks into the directories present in toplevel_addon_paths and registers each subdirectory @@ -39,13 +40,13 @@ def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"): "ks": [], "categories": [] }
- for path in toplevel_addon_paths: try: files = os.listdir(path) except OSError: files = []
for addon_id in files: addon_ks_path = os.path.join(path, addon_id, "ks") if os.path.isdir(addon_ks_path):@@ -68,7 +69,7 @@ class AddonRegistry(object):
It acts as a proxy during kickstart save. """
- def __init__(self, dictionary): self.__dict__ = dictionary
@@ -76,11 +77,20 @@ class AddonRegistry(object): return reduce(lambda acc,(id, addon): acc + "%%addon %s\n%s%%end\n" % (id, str(addon)), self.__dict__.iteritems(), "")
- def execute(self, storage, ksdata, instClass):
- # pylint: disable-msg=C0103
- def execute(self, storage, ksdata, instClass, users): """This method calls execute on all the registered addons.""" for k, v in self.__dict__.iteritems():
v.execute(storage, ksdata, instClass)
if hasattr(v, "execute"):v.execute(storage, ksdata, instClass, users)
Isn't execute method mandatory? I guess you are trying to prevent tracebacks, but we should probably check that much sooner, or provide
def execute(...): pass
in the generic Addon class.
- def setup(self, storage, ksdata, instClass):
"""This method calls setup on all the registered addons."""for k, v in self.__dict__.iteritems():if hasattr(v, "setup"):v.setup(storage, ksdata, instClass)class AddonData(object): """This is a common parent class for loading and storing 3rd party data to kickstart. It is instantiated by @@ -96,7 +106,7 @@ class AddonData(object): There is also a mandatory method execute, which should make all the described changes to the installed system. """
- def __init__(self, name): self.name = name self.content = ""
@@ -104,8 +114,21 @@ class AddonData(object): def __str__(self): return self.content
- def execute(self, storage, ksdata, instClass):
"""Make the changes to the underlying system."""
# pylint: disable-msg=C0103
def setup(self, storage, ksdata, instClass):
"""Make the changes to the install system.This method is called before the installationis started and directly from spokes. It must be possibleto call it multiple times without breaking the environment."""passdef execute(self, storage, ksdata, instClass, users):
"""Make the changes to the underlying system.This method is called only once in the post-installsetup phase.""" passdef handle_line(self, line):
@@ -133,10 +156,10 @@ class AddonSection(Section): """Process the arguments to the %addon header.""" Section.handleHeader(self, lineno, args) op = KSOptionParser(version=self.version)
(opts, extra) = op.parse_args(args=args[1:], lineno=lineno)
(_opts, extra) = op.parse_args(args=args[1:], lineno=lineno)
Why this ^^^^^^ renaming?
self.addon_id = extra[0] # if the addon is not registered, create dummy placeholder for it if self.addon_id and not hasattr(self.handler.addon, self.addon_id):
setattr(self.handler.addon, self.addon_id, AnacondaKSAddon(self.addon_id))
setattr(self.handler.addon, self.addon_id, AddonData(self.addon_id))diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 4e552ea..af214ba 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -109,9 +109,12 @@ def doInstall(storage, payload, ksdata, instClass): steps = len(storage.devicetree.findActions(type="create", object="format")) + \ len(storage.devicetree.findActions(type="resize", object="format")) + \ len(storage.devicetree.findActions(type="migrate", object="format"))
- steps += 4 # packages setup, packages, bootloader, post install
steps += 5 # pre setup phase, packages setup, packages, bootloader, post install progress.send_init(steps)
with progress_report(_("Setting up the install environment")):
ksdata.addon.setup(storage, ksdata, instClass)# Do partitioning. payload.preStorage() turnOnFilesystems(storage)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 693ecd4..b242e92 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -1432,7 +1432,7 @@ def preScriptPass(f): def parseKickstart(f): # preprocessing the kickstart file has already been handled in initramfs.
- handler = AnacondaKSHandler(constants.addon_paths)
- handler = AnacondaKSHandler(addon_paths)
Will this work? Are you importing addon_paths from the constants module somewhere?
ksparser = AnacondaKSParser(handler) # We need this so all the /dev/disk/* stuff is set up before parsing.diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 2056307..1ffb728 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -18,7 +18,7 @@ # # Red Hat Author(s): Chris Lumens clumens@redhat.com # -import importlib, inspect, os, sys, time +import inspect, os, sys, time import meh.ui.gui
from gi.repository import Gdk @@ -120,8 +120,12 @@ class GUIObject(common.UIObject):
def _findUIFile(self): path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/")
# look for files in the same dir as where the class is definedpath += ":%s" % os.path.dirname(inspect.getfile(self.__class__))for d in path.split(":"):
testPath = os.path.normpath(d + self.uiFile)
testPath = os.path.normpath(os.path.join(d, self.uiFile)) if os.path.isfile(testPath) and os.access(testPath, os.R_OK): return testPath@@ -265,7 +269,7 @@ class GraphicalUserInterface(UserInterface): return [SummaryHub, ProgressHub]
def _is_standalone(self, obj):
"""Is the spoke passes as obj standalone?"""
"""Is the spoke passed as obj standalone?"""
Okay, there is a fix for one of my previous comments.
for k, v in self.__dict__.iteritems():
v.execute(storage, ksdata, instClass)
if hasattr(v, "execute"):v.execute(storage, ksdata, instClass, users)Isn't execute method mandatory? I guess you are trying to prevent tracebacks, but we should probably check that much sooner, or provide
def execute(...): pass
in the generic Addon class.
It is mandatory and I provide the empty method too, this is just defensive programming :)
- handler = AnacondaKSHandler(constants.addon_paths)
- handler = AnacondaKSHandler(addon_paths)
Will this work? Are you importing addon_paths from the constants module somewhere?
there is "from constants import *" in that file at or around line 41
diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 4e552ea..af214ba 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -109,9 +109,12 @@ def doInstall(storage, payload, ksdata, instClass): steps = len(storage.devicetree.findActions(type="create", object="format")) + \ len(storage.devicetree.findActions(type="resize", object="format")) + \ len(storage.devicetree.findActions(type="migrate", object="format"))
- steps += 4 # packages setup, packages, bootloader, post install
steps += 5 # pre setup phase, packages setup, packages, bootloader, post install progress.send_init(steps)
with progress_report(_("Setting up the install environment")):
ksdata.addon.setup(storage, ksdata, instClass)# Do partitioning. payload.preStorage() turnOnFilesystems(storage)
Do you expect this step to take a significant amount of time? If not, I'd prefer to just skip the progress reporting. If you do expect it to take enough time to report, I'd prefer the text:
"Setting up the installation environment"
The thing is, we have an awful lot of strings about setting up and preparing to do things. If we can skip adding more, I'm all for it. I'd prefer we just get to doing the installation than preparing.
- Chris
It could take some time.. NTP setup, IPA/AD/Samba domain registration, ... so yes, I would prefer keeping the message.
----- Original Message -----
diff --git a/pyanaconda/install.py b/pyanaconda/install.py index 4e552ea..af214ba 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -109,9 +109,12 @@ def doInstall(storage, payload, ksdata, instClass): steps = len(storage.devicetree.findActions(type="create", object="format")) + \ len(storage.devicetree.findActions(type="resize", object="format")) + \ len(storage.devicetree.findActions(type="migrate", object="format"))
- steps += 4 # packages setup, packages, bootloader, post
install
- steps += 5 # pre setup phase, packages setup, packages,
bootloader, post install progress.send_init(steps)
- with progress_report(_("Setting up the install environment")):
ksdata.addon.setup(storage, ksdata, instClass)- # Do partitioning. payload.preStorage() turnOnFilesystems(storage)
Do you expect this step to take a significant amount of time? If not, I'd prefer to just skip the progress reporting. If you do expect it to take enough time to report, I'd prefer the text:
"Setting up the installation environment"The thing is, we have an awful lot of strings about setting up and preparing to do things. If we can skip adding more, I'm all for it. I'd prefer we just get to doing the installation than preparing.
- Chris
anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
I've gone through the patches and apart from the several comments and questions they look good to me. We just need to push them and start the work on first addons to find out what doesn't work as expected and what else is needed.
this patchset does quite a lot of generalization changes, which are needed for my new firstboot code. I cleaned it quite a bit, but some patches document the way I was thinking about the problem so I kept them for documentation purposes.
Thanks for taking a look at this.
Do you have an example of an addon so I can get a feel for how they're put together?
- Chris
anaconda-patches@lists.fedorahosted.org