Patches in this set configure anaconda to actually look for addon UI elements and use them properly.
Some of them also enhance the API for anaconda-firstboot interaction. One last piece is missing there, the actual code to save to disk what was configured during installation.
--- anaconda | 10 ++++++---- pyanaconda/__init__.py | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/anaconda b/anaconda index 17bf3af..29207e2 100755 --- a/anaconda +++ b/anaconda @@ -404,7 +404,7 @@ def startDebugger(signum, frame): import epdb epdb.serve(skip=1)
-def setupDisplay(anaconda, opts): +def setupDisplay(anaconda, opts, addon_paths=None): from pyanaconda.ui.tui.simpleline import App from pyanaconda.ui.tui.spokes.askvnc import AskVNCSpoke from pykickstart.constants import DISPLAY_MODE_TEXT @@ -581,7 +581,7 @@ def setupDisplay(anaconda, opts): doStartupX11Actions()
# with X running we can initialize the UI interface - anaconda.initInterface() + anaconda.initInterface(addon_paths)
anaconda.instClass.configure(anaconda)
@@ -807,6 +807,9 @@ if __name__ == "__main__":
os.system("udevadm control --env=ANACONDA=1")
+ # Collect all addon paths + addon_paths = collect_addon_paths(constants.ADDON_PATHS) + # If we were given a kickstart file on the command line, parse (but do not # execute) that now. Otherwise, load in defaults from kickstart files # shipped with the installation media. @@ -829,7 +832,6 @@ if __name__ == "__main__": break
if not ksdata: - addon_paths = collect_addon_paths(constants.ADDON_PATHS) ksdata = kickstart.AnacondaKSHandler(addon_paths["ks"])
if ksdata.rescue.rescue: @@ -922,7 +924,7 @@ if __name__ == "__main__": initThreading()
# now start the interface - setupDisplay(anaconda, opts) + setupDisplay(anaconda, opts, addon_paths)
# Set flag to prompt for missing ks data if anaconda.displayMode == 'c': diff --git a/pyanaconda/__init__.py b/pyanaconda/__init__.py index a442f24..6c9e4d7 100644 --- a/pyanaconda/__init__.py +++ b/pyanaconda/__init__.py @@ -205,7 +205,7 @@ class Anaconda(object): f.write("--- traceback: %s ---\n" % filename) f.write(dump_text + "\n")
- def initInterface(self): + def initInterface(self, addon_paths=None): if self._intf: raise RuntimeError, "Second attempt to initialize the InstallInterface"
@@ -220,6 +220,9 @@ class Anaconda(object): else: raise RuntimeError("Unsupported displayMode: %s" % self.displayMode)
+ if addon_paths: + self._intf.update_paths(addon_paths) + def writeXdriver(self, root = None): # this should go away at some point, but until it does, we # need to keep it around.
--- pyanaconda/ui/common.py | 71 ++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 33 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 9a2b304..69d0390 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -438,38 +438,43 @@ 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 + try: + for module_file in os.listdir(path): + if (not module_file.endswith(".py")) and \ + (not module_file.endswith(".so")): + continue
- try: - mod_name = module_file[:module_file.rindex(".")] - except ValueError: - mod_name = module_file + if module_file == "__init__.py": + continue
- mod_info = None - module = None - - try: - imp.acquire_lock() - mod_info = imp.find_module(mod_name, [path]) - 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 - finally: - 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): - retval.append(val) - - return retval + try: + mod_name = module_file[:module_file.rindex(".")] + except ValueError: + mod_name = module_file + + mod_info = None + module = None + + try: + imp.acquire_lock() + mod_info = imp.find_module(mod_name, [path]) + 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 + finally: + 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): + retval.append(val) + + return retval + + # when the directory "path" does not exist + except OSError: + return []
On Wed, 2012-12-19 at 16:09 +0100, Martin Sivak wrote:
pyanaconda/ui/common.py | 71 ++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 33 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 9a2b304..69d0390 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -438,38 +438,43 @@ 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
- try:
for module_file in os.listdir(path):
if (not module_file.endswith(".py")) and \
(not module_file.endswith(".so")):
continue
try:
mod_name = module_file[:module_file.rindex(".")]
except ValueError:
mod_name = module_file
if module_file == "__init__.py":
continue
mod_info = None
module = None
try:
imp.acquire_lock()
mod_info = imp.find_module(mod_name, [path])
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
finally:
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):
retval.append(val)
- return retval
try:
mod_name = module_file[:module_file.rindex(".")]
except ValueError:
mod_name = module_file
mod_info = None
module = None
try:
imp.acquire_lock()
mod_info = imp.find_module(mod_name, [path])
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
finally:
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):
retval.append(val)
return retval
- # when the directory "path" does not exist
- except OSError:
return []
Wouldn't it be better to have something like:
try: items = os.listdir(path) except OSError: return []
for module_file in items: ...
instead of the long nested try-except block?
--- pyanaconda/ui/gui/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 02ebb63..b3c74de 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -74,6 +74,8 @@ class GUIObject(common.UIObject): mainWidgetName = None uiFile = ""
+ screenshots_directory = "/tmp/anaconda-screenshots" + def __init__(self, data): """Create a new UIObject instance, including loading its uiFile and all UI-related objects. @@ -134,10 +136,11 @@ class GUIObject(common.UIObject): return
# Make sure the screenshot directory exists. - if not os.access("/tmp/anaconda-screenshots", os.W_OK): - os.mkdir("/tmp/anaconda-screenshots") + if not os.access(self.screenshots_directory, os.W_OK): + os.mkdir(self.screenshots_directory)
- fn = "/tmp/anaconda-screenshots/screenshot-%04d.png" % _screenshotIndex + fn = os.path.join(self.screenshots_directory, + "screenshot-%04d.png" % _screenshotIndex)
win = window.get_window() width = win.get_width()
Anaconda will look for glade files also in the same directory where the source file requesting it is located.
This patch also adds updates dictionaries to the list of paths we use to look for spokes, hubs and categories. --- pyanaconda/ui/gui/__init__.py | 20 +++++++++++++++----- pyanaconda/ui/tui/__init__.py | 8 ++++++-- 2 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index b3c74de..1ffc490 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -122,8 +122,13 @@ class GUIObject(common.UIObject):
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) + dirs = path.split(":") + + # append the directory where this UIObject is defined + dirs.append(os.path.dirname(inspect.getfile(self.__class__))) + + for d in dirs: + testPath = os.path.join(d, self.uiFile) if os.path.isfile(testPath) and os.access(testPath, os.R_OK): return testPath
@@ -238,13 +243,18 @@ class GraphicalUserInterface(UserInterface):
basemask = "pyanaconda.ui.gui" basepath = os.path.dirname(__file__) + updatepath = "/tmp/updates/pyanaconda/ui/gui" + paths = UserInterface.paths + { "categories": [(basemask + ".categories.%s", - os.path.join(basepath, "categories"))], + os.path.join(path, "categories")) + for path in (updatepath, basepath)], "spokes": [(basemask + ".spokes.%s", - os.path.join(basepath, "spokes"))], + os.path.join(path, "spokes")) + for path in (updatepath, basepath)], "hubs": [(basemask + ".hubs.%s", - os.path.join(basepath, "hubs"))] + os.path.join(path, "hubs")) + for path in (updatepath, basepath)] }
def _list_hubs(self): diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py index b715d0a..1bf71e7 100644 --- a/pyanaconda/ui/tui/__init__.py +++ b/pyanaconda/ui/tui/__init__.py @@ -131,11 +131,15 @@ class TextUserInterface(ui.UserInterface):
basemask = "pyanaconda.ui.tui" basepath = os.path.dirname(__file__) + updatepath = "/tmp/updates/pyanaconda/ui/tui" + paths = ui.UserInterface.paths + { "spokes": [(basemask + ".spokes.%s", - os.path.join(basepath, "spokes"))], + os.path.join(path, "spokes")) + for path in (updatepath, basepath)], "hubs": [(basemask + ".hubs.%s", - os.path.join(basepath, "hubs"))] + os.path.join(path, "hubs")) + for path in (updatepath, basepath)] }
def _list_hubs(self):
This patch adds two pieces to Spoke classes:
should_run(environment_id, data) This method should return True if the spoke is to be displayed in the environment (currently only "anaconda" and "firstboot" are supported)
@property configured() This property should return list of strings that will be stored to a config file, which will be passed to firstboot and GIE and will control whether spokes are to be displayed again or not. --- pyanaconda/ui/common.py | 56 ++++++++++++++++++++++++++++-------- pyanaconda/ui/gui/hubs/__init__.py | 4 +++ pyanaconda/ui/gui/spokes/password.py | 7 ++--- pyanaconda/ui/tui/hubs/__init__.py | 4 +++ 4 files changed, 54 insertions(+), 17 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 69d0390..f4e3d21 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -116,25 +116,37 @@ class UIObject(object): return self._data
class FirstbootSpokeMixIn(object): - """This MixIn class marks Spokes as usable for Firstboot.""" - + """This MixIn class marks Spokes as usable for Firstboot + and Anaconda. + """ @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 + def should_run(cls, environment, data): + """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. + + It might be called multiple times, with or without (None) + the data argument. + """ + return environment in ("anaconda", "firstboot") + +class FirstbootOnlySpokeMixIn(object): + """This MixIn class marks Spokes as usable for Firstboot.""" @classmethod - def firstboot(cls): + def should_run(cls, environment, data): """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 - + FirstbootHub and False if it should be skipped. + + It might be called multiple times, with or without (None) + the data argument. + """ + return environment in ("firstboot") + 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 @@ -197,6 +209,18 @@ class Spoke(UIObject): self.instclass = instclass self.applyOnSkip = False
+ @classmethod + def should_run(cls, environment, data): + """This method is responsible for beginning Spoke initialization. + + It should return True if the spoke is to be shown while in + <environment> and False if it should be skipped. + + It might be called multiple times, with or without (None) + the data argument. + """ + return environment in ("anaconda") + def apply(self): """Apply the selections made on this Spoke to the object's preset data object. This method must be provided by every subclass. @@ -204,6 +228,14 @@ class Spoke(UIObject): raise NotImplementedError
@property + def configured(self): + """This method returns a list of textual ids that should + be written into the after-install customization status + file for the firstboot and GIE to know that the spoke was + configured and what value groups were provided.""" + return ["%s.%s" % (self.__class__.__module__, self.__class__.__name__)] + + @property def completed(self): """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 diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index 3975b37..1f5141a 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -147,6 +147,10 @@ class Hub(GUIObject, common.Hub):
selectors = [] for spokeClass in sorted(cats_and_spokes[c], key=lambda s: s.title): + # Check if this spoke is to be shown in anaconda + if not spokeClass.should_run("anaconda", self.data): + continue + # 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/password.py b/pyanaconda/ui/gui/spokes/password.py index 1bd424c..44be1ca 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -31,12 +31,13 @@ import string
from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory +from pyanaconda.ui.common import FirstbootSpokeMixIn #from _isys import isCapsLockEnabled
__all__ = ["PasswordSpoke"]
-class PasswordSpoke(NormalSpoke): +class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke): builderObjects = ["passwordWindow"]
mainWidgetName = "passwordWindow" @@ -47,10 +48,6 @@ 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/hubs/__init__.py b/pyanaconda/ui/tui/hubs/__init__.py index d669bc7..7d16ff3 100644 --- a/pyanaconda/ui/tui/hubs/__init__.py +++ b/pyanaconda/ui/tui/hubs/__init__.py @@ -56,6 +56,10 @@ class TUIHub(TUIObject, common.Hub):
# sort them according to their priority for s in sorted(spokes, key = lambda s: s.priority): + # Check if this spoke is to be shown in anaconda + if not s.should_run("anaconda", self.data): + continue + spoke = s(app, data, storage, payload, instclass) spoke.initialize()
On Wed, 2012-12-19 at 16:09 +0100, Martin Sivak wrote:
This patch adds two pieces to Spoke classes:
should_run(environment_id, data) This method should return True if the spoke is to be displayed in the environment (currently only "anaconda" and "firstboot" are supported)
@property configured() This property should return list of strings that will be stored to a config file, which will be passed to firstboot and GIE and will control whether spokes are to be displayed again or not.
pyanaconda/ui/common.py | 56 ++++++++++++++++++++++++++++-------- pyanaconda/ui/gui/hubs/__init__.py | 4 +++ pyanaconda/ui/gui/spokes/password.py | 7 ++--- pyanaconda/ui/tui/hubs/__init__.py | 4 +++ 4 files changed, 54 insertions(+), 17 deletions(-)
diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 69d0390..f4e3d21 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -116,25 +116,37 @@ class UIObject(object): return self._data
class FirstbootSpokeMixIn(object):
- """This MixIn class marks Spokes as usable for Firstboot."""
- """This MixIn class marks Spokes as usable for Firstboot
and Anaconda.
- """ @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
def should_run(cls, environment, data):
"""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.
I believe this sentence doesn't match the return statement at the end of this method.
It might be called multiple times, with or without (None)
the data argument.
"""
return environment in ("anaconda", "firstboot")
+class FirstbootOnlySpokeMixIn(object):
- """This MixIn class marks Spokes as usable for Firstboot."""
What about adding "ONLY" to the docstring ("...as usable ONLY for Firstboot.")?
@classmethod
- def firstboot(cls):
def should_run(cls, environment, data): """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
FirstbootHub and False if it should be skipped.
It might be called multiple times, with or without (None)
the data argument.
"""
return environment in ("firstboot")
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 @@ -197,6 +209,18 @@ class Spoke(UIObject): self.instclass = instclass self.applyOnSkip = False
- @classmethod
- def should_run(cls, environment, data):
"""This method is responsible for beginning Spoke initialization.
It should return True if the spoke is to be shown while in
<environment> and False if it should be skipped.
It might be called multiple times, with or without (None)
the data argument.
"""
return environment in ("anaconda")
- def apply(self): """Apply the selections made on this Spoke to the object's preset data object. This method must be provided by every subclass.
@@ -204,6 +228,14 @@ class Spoke(UIObject): raise NotImplementedError
@property
- def configured(self):
"""This method returns a list of textual ids that should
be written into the after-install customization status
file for the firstboot and GIE to know that the spoke was
configured and what value groups were provided."""
return ["%s.%s" % (self.__class__.__module__, self.__class__.__name__)]
- @property def completed(self): """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
diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index 3975b37..1f5141a 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -147,6 +147,10 @@ class Hub(GUIObject, common.Hub):
selectors = [] for spokeClass in sorted(cats_and_spokes[c], key=lambda s: s.title):
# Check if this spoke is to be shown in anaconda
if not spokeClass.should_run("anaconda", self.data):
continue
# 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/password.py b/pyanaconda/ui/gui/spokes/password.py index 1bd424c..44be1ca 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -31,12 +31,13 @@ import string
from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory +from pyanaconda.ui.common import FirstbootSpokeMixIn #from _isys import isCapsLockEnabled
__all__ = ["PasswordSpoke"]
-class PasswordSpoke(NormalSpoke): +class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke): builderObjects = ["passwordWindow"]
mainWidgetName = "passwordWindow"
@@ -47,10 +48,6 @@ class PasswordSpoke(NormalSpoke): icon = "dialog-password-symbolic" title = N_("ROOT PASSWORD")
- @classmethod
- def firstboot(cls):
return True
Does this work? I mean is FirstbootSpokeMixIn's should_run called instead of the Spoke's one? What does determine this? The ordering of the superclasses? I believe it is worth a comment in the code.
On Wed, 2012-12-19 at 16:09 +0100, Martin Sivak wrote:
Patches in this set configure anaconda to actually look for addon UI elements and use them properly.
Some of them also enhance the API for anaconda-firstboot interaction. One last piece is missing there, the actual code to save to disk what was configured during installation.
Apart from the inline comments this looks good to me.
anaconda-patches@lists.fedorahosted.org