[pitivi] check: rewrite for saner readability and maintainability.



commit 82d4156d166758ca06dbd85ef7dfd5d1eb8b77cf
Author: Mathieu Duponchelle <mduponchelle1 gmail com>
Date:   Sat Feb 8 01:45:39 2014 +0100

    check: rewrite for saner readability and maintainability.

 bin/pitivi.in                   |   22 +--
 pitivi/check.py                 |  364 ++++++++++++++++++---------------------
 pitivi/clipproperties.py        |   27 ++--
 pitivi/dialogs/depsmanager.py   |    4 +-
 pitivi/dialogs/startupwizard.py |    4 +-
 tests/test_utils.py             |   45 +++++
 6 files changed, 238 insertions(+), 228 deletions(-)
---
diff --git a/bin/pitivi.in b/bin/pitivi.in
index b14f4c7..3e879f2 100644
--- a/bin/pitivi.in
+++ b/bin/pitivi.in
@@ -106,23 +106,11 @@ def _initialize_modules():
         print "Failed to initialize modules: ", e
 
 
-def _check_dependencies():
-    from pitivi.check import check_hard_dependencies, check_soft_dependencies
-    missing_hard_deps = check_hard_dependencies()
-    # Yes, check soft deps even if hard ones are OK. We'll need them at runtime:
-    missing_soft_deps = check_soft_dependencies()
-
-    if missing_hard_deps:
-        print "\nERROR - The following hard dependencies are unmet:"
-        print "=================================================="
-        for dep in missing_hard_deps:
-            print "-", dep + ":", missing_hard_deps[dep]
-        if missing_soft_deps:
-            print "\nAdditionally, the following soft dependencies are missing..."
-            for dep in missing_soft_deps:
-                print "-", dep + ":", missing_soft_deps[dep]
-        sys.exit(2)
+def _check_requirements():
+    from pitivi.check import check_requirements
 
+    if not check_requirements():
+        sys.exit(2)
 
 def _run_pitivi():
     import pitivi.application as ptv
@@ -145,7 +133,7 @@ if __name__ == "__main__":
         # the classes in application.py will not even have the opportunity to run.
         # We do these checks on every startup (even outside the dev environment, for
         # soft deps); doing imports and gst registry checks has near-zero cost.
-        _check_dependencies()
+        _check_requirements()
         _run_pitivi()
     except KeyboardInterrupt:
         print "\tPitivi stopped by user with KeyboardInterrupt!"
diff --git a/pitivi/check.py b/pitivi/check.py
index 6012221..689f5bd 100644
--- a/pitivi/check.py
+++ b/pitivi/check.py
@@ -3,8 +3,7 @@
 #
 #       pitivi/check.py
 #
-# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
-# Copyright (c) 2012, Jean-François Fortin Tam <nekohayo gmail com>
+# Copyright (c) 2014, Mathieu Duponchelle <mduponchelle1 gmail com>
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -36,223 +35,202 @@ import sys
 
 from gettext import gettext as _
 
-
-# This list is meant to be a complete list for packagers.
-# Unless otherwise noted, modules are accessed through gobject introspection
-HARD_DEPS = {
-    "cairo": "1.10.0",  # using static python bindings
-    "Clutter": "1.12.0",
-    "ClutterGst": "2.0.0",
-    "GES": "1.0.0.0",  # packagers: in reality 1.1.90, but that GES version erronously reports itself as 
1.0.0.0
-    "Gio": None,
-    "gnonlin": "1.1.90",
-    "Gst": "1.2.0",
-    "Gtk": "3.8.0",
-    "numpy": None,  # using static python bindings
-
-    # The following are not checked, but needed for the rest to work:
-    "gobject-introspection": "1.34.0",
-    "gst-python": "1.1.90",
-    "pygobject": "3.4.0",
-}
-
-# For the list of soft dependencies, see the "check_soft_dependencies" method,
-# near the end of this file.
-# (library_name, why_we_need_it) tuples:
 missing_soft_deps = {}
 
 
-def at_least_version(version, required):
-    for i, item in enumerate(version):
-        if required[i] != item:
-            return item > required[i]
+class Dependency(object):
+    """
+    This abstract class represents a module or component requirement.
+    @param modulename: The string allowing for import or lookup of the component.
+    @param version_required_string: A string in the format X.Y.Z or None if no version
+      check is necessary.
+    @param additional_message: A string that will be displayed to the user to further
+      explain the purpose of the missing component.
+    """
+    def __init__(self, modulename, version_required_string, additional_message=None):
+        self.version_required_string = version_required_string
+        self.modulename = modulename
+        self.satisfied = False
+        self.version_installed = None
+        self.component = None
+        self.additional_message = additional_message
+
+    def check(self):
+        """
+        Sets the satisfied flag to True or False.
+        """
+        self.component = self._try_importing_component()
+
+        if not self.component:
+            self.satisfied = False
+        elif self.version_required_string is None:
+            self.satisfied = True
+        else:
+            formatted_version = self._format_version(self.component)
+            self.version_installed = _version_to_string(formatted_version)
+
+            if formatted_version >= _string_to_list(self.version_required_string):
+                self.satisfied = True
+
+    def _try_importing_component(self):
+        """
+        Subclasses must implement that method to return an object
+        on which version will be inspectable.
+        Return None on failure to import.
+        """
+        raise NotImplementedError
+
+    def _format_version(self, module):
+        """
+        Subclasses must return the version number split
+        in an iterable of ints.
+        For example "1.2.10" should return [1, 2, 10]
+        """
+        raise NotImplementedError
+
+    def __repr__(self):
+        if self.satisfied:
+            return ""
+
+        message = "- " + self.modulename + " "
+        if not self.component:
+            message += _("not found on the system")
+        else:
+            message += self.version_installed + _(" is installed but ") +\
+                self.version_required_string + _(" is required")
 
-    return True
+        if self.additional_message is not None:
+            message += "\n    -> " + self.additional_message
 
+        return message
 
-def _initiate_audiosinks(Gst):
-    # Yes, this can still fail, if PulseAudio is non-responsive for example.
-    sink = Gst.ElementFactory.make("autoaudiosink", None)
-    if not sink:
-        return False
-    return True
 
+class GIDependency(Dependency):
+    def _try_importing_component(self):
+        try:
+            __import__("gi.repository." + self.modulename)
+            module = sys.modules["gi.repository." + self.modulename]
+        except ImportError:
+            module = None
+        return module
 
-def _try_import_from_gi(modulename):
-    try:
-        __import__("gi.repository." + modulename)
-        return True
-    except ImportError:
-        return False
+    def _format_version(self, module):
+        pass
 
 
-def _try_import(modulename):
-    try:
-        __import__(modulename)
-        return True
-    except ImportError:
-        return False
+class ClassicDependency(Dependency):
+    def _try_importing_component(self):
+        try:
+            __import__(self.modulename)
+            module = sys.modules[self.modulename]
+        except ImportError:
+            module = None
+        return module
 
+    def _format_version(self, module):
+        pass
 
-def _version_to_string(version):
-    return ".".join([str(x) for x in version])
 
+class GstPluginDependency(Dependency):
+    """
+    Don't call check on its instances before actually checking
+    Gst is importable.
+    """
+    def _try_importing_component(self):
+        from gi.repository import Gst
+        Gst.init(None)
 
-def _string_to_list(version):
-    return [int(x) for x in version.split(".")]
+        registry = Gst.Registry.get()
+        plugin = registry.find_plugin(self.modulename)
+        return plugin
 
+    def _format_version(self, plugin):
+        return _string_to_list(plugin.get_version())
 
-def _check_dependency(modulename, from_gobject_introspection):
-    """
-    Checks if the given module can be imported and is recent enough.
 
-    "modulename" is case-sensitive
-    "from_gobject_introspection" is a mandatory boolean variable.
+class GstDependency(GIDependency):
+    def _format_version(self, module):
+        return list(module.version())
 
-    Returns: [satisfied, version_required, version_installed]
-    """
-    VERSION_REQ = HARD_DEPS[modulename]
-    # What happens here is that we try to import the module. If it works,
-    # assign it to a "module" variable and check the version reqs with it.
-    module = None
-    if from_gobject_introspection is True:
-        if _try_import_from_gi(modulename):
-            module = sys.modules["gi.repository." + modulename]
-    else:
-        if _try_import(modulename):
-            module = sys.modules[modulename]
-
-    if module is None:
-        # Import failed, the dependency can't be satisfied, don't check versions
-        return [False, VERSION_REQ, None]
-    elif not VERSION_REQ:
-        # Import succeeded but there is no requirement, skip further checks
-        return [True, None, False]
-
-    # The import succeeded and there is a version requirement, so check it out:
-    if modulename in ("Gst", "GES"):
-        if list(module.version()) < _string_to_list(VERSION_REQ):
-            return [False, VERSION_REQ, _version_to_string(module.version())]
-        else:
-            return [True, None, _version_to_string(module.version())]
-    if modulename in ("Gtk", "Clutter", "ClutterGst"):
-        gtk_version_tuple = (module.MAJOR_VERSION, module.MINOR_VERSION, module.MICRO_VERSION)
-        if list(gtk_version_tuple) < _string_to_list(VERSION_REQ):
-            return [False, VERSION_REQ, _version_to_string(gtk_version_tuple)]
-        else:
-            return [True, None, _version_to_string(gtk_version_tuple)]
-    if modulename == "cairo":
-        if _string_to_list(module.cairo_version_string()) < _string_to_list(VERSION_REQ):
-            return [False, VERSION_REQ, module.cairo_version_string()]
-        else:
-            return [True, None, module.cairo_version_string()]
-
-    oops = 'Module "%s" is installed, but version checking is not defined in check_dependency' % modulename
-    raise NotImplementedError(oops)
-
-
-def check_hard_dependencies():
-    missing_hard_deps = {}
-
-    satisfied, req, inst = _check_dependency("Gst", True)
-    if not satisfied:
-        missing_hard_deps["GStreamer"] = (req, inst)
-    satisfied, req, inst = _check_dependency("Clutter", True)
-    if not satisfied:
-        missing_hard_deps["Clutter"] = (req, inst)
-    satisfied, req, inst = _check_dependency("ClutterGst", True)
-    if not satisfied:
-        missing_hard_deps["ClutterGst"] = (req, inst)
-    satisfied, req, inst = _check_dependency("GES", True)
-    if not satisfied:
-        missing_hard_deps["GES"] = (req, inst)
-    satisfied, req, inst = _check_dependency("cairo", False)
-    if not satisfied:
-        missing_hard_deps["Cairo"] = (req, inst)
-    satisfied, req, inst = _check_dependency("Gtk", True)
-    if not satisfied:
-        missing_hard_deps["GTK+"] = (req, inst)
-    satisfied, req, inst = _check_dependency("numpy", False)
-    if not satisfied:
-        missing_hard_deps["NumPy"] = (req, inst)
-
-    # Since we had to check Gst beforehand, we only do the import now:
-    from gi.repository import Gst
 
-    registry = Gst.Registry.get()
-    # Special case: gnonlin is a plugin, not a python module to be imported,
-    # we can't use check_dependency to determine the version:
-    inst = registry.find_plugin("gnonlin")
-    if not inst:
-        missing_hard_deps["GNonLin"] = (HARD_DEPS["gnonlin"], inst)
-    else:
-        inst = inst.get_version()
-        if _string_to_list(inst) < _string_to_list(HARD_DEPS["gnonlin"]):
-            missing_hard_deps["GNonLin"] = (HARD_DEPS["gnonlin"], inst)
-
-    # Prepare the list of hard deps errors to warn about later:
-    for dependency in missing_hard_deps:
-        req = missing_hard_deps[dependency][0]
-        inst = missing_hard_deps[dependency][1]
-        if req and not inst:
-            message = "%s or newer is required, but was not found on your system." % req
-        elif req and inst:
-            message = "%s or newer is required, but only version %s was found." % (req, inst)
-        else:
-            message = "not found on your system."
-        missing_hard_deps[dependency] = message
+class GtkOrClutterDependency(GIDependency):
+    def _format_version(self, module):
+        return [module.MAJOR_VERSION, module.MINOR_VERSION, module.MICRO_VERSION]
 
-    # And finally, do a few last checks for basic sanity.
-    # Yes, a broken/dead autoaudiosink is still possible in 2012 with PulseAudio
-    if not _initiate_audiosinks(Gst):
-        missing_hard_deps["autoaudiosink"] = \
-            "Could not initiate audio output sink. "\
-            "Make sure you have a valid one (pulsesink, alsasink or osssink)."
 
-    return missing_hard_deps
+class CairoDependency(ClassicDependency):
+    def __init__(self, version_required_string):
+        ClassicDependency.__init__(self, "cairo", version_required_string)
 
+    def _format_version(self, module):
+        return _string_to_list(module.cairo_version_string())
 
-def check_soft_dependencies():
-    """
-    Verify for the presence of optional modules that enhance the user experience
 
-    If those are missing from the system, the user will be notified of their
-    existence by the presence of a "Missing dependencies..." button at startup.
-    """
+HARD_DEPENDENCIES = (CairoDependency("1.10.0"),
+                     GtkOrClutterDependency("Clutter", "1.12.0"),
+                     GtkOrClutterDependency("ClutterGst", "2.0.0"),
+                     GstDependency("Gst", "1.2.0"),
+                     GstDependency("GES", "1.0.0.0"),
+                     GtkOrClutterDependency("Gtk", "3.8.0"),
+                     ClassicDependency("numpy", None),
+                     GIDependency("Gio", None),
+                     GstPluginDependency("gnonlin", "1.1.90"))
+
+SOFT_DEPENDENCIES = (ClassicDependency("pycanberra", None,
+                                       _("enables sound notifications when rendering is complete")),
+                     GIDependency("GnomeDesktop", None,
+                                  _("file thumbnails provided by GNOME's thumbnailers")),
+                     GIDependency("Notify", None,
+                                  _("enables visual notifications when rendering is complete")),
+                     GstPluginDependency("libav", None,
+                                         _("additional multimedia codecs through the Libav library")))
+
+
+def _check_audiosinks():
     from gi.repository import Gst
 
-    # Description strings are translatable as they are shown in the Pitivi UI.
-    if not _try_import("pycanberra"):
-        missing_soft_deps["PyCanberra"] = \
-            _("enables sound notifications when rendering is complete")
-
-    if not _try_import_from_gi("GnomeDesktop"):
-        missing_soft_deps["libgnome-desktop"] = \
-            _("file thumbnails provided by GNOME's thumbnailers")
-    if not _try_import_from_gi("Notify"):
-        missing_soft_deps["libnotify"] = \
-            _("enables visual notifications when rendering is complete")
-
-    registry = Gst.Registry.get()
-    if not registry.find_plugin("libav"):
-        missing_soft_deps["GStreamer Libav plugin"] = \
-            _("additional multimedia codecs through the Libav library")
-
-    # Apparently, doing a registry.find_plugin("frei0r") is not enough.
-    # Sometimes it still returns something even when frei0r is uninstalled,
-    # and anyway we're looking specifically for the scale0tilt filter.
-    # Don't use Gst.ElementFactory.make for this check, it's very I/O intensive.
-    # Instead, ask the registry with .lookup_feature or .check_feature_version:
-    if not registry.lookup_feature("frei0r-filter-scale0tilt"):
-        missing_soft_deps["Frei0r"] = \
-            _("additional video effects, clip transformation feature")
-
-    # TODO: we're not actually checking for gst bad and ugly... by definition,
-    # gst bad is a set of plugins that can move to gst good or ugly, and anyway
-    # distro packagers may split/combine the gstreamer plugins into any way they
-    # see fit. We can do a registry.find_plugin for specific encoders, but we
-    # don't really have something generic to rely on; ideas/patches welcome.
-    return missing_soft_deps
+    # Yes, this can still fail, if PulseAudio is non-responsive for example.
+    sink = Gst.ElementFactory.make("autoaudiosink", None)
+    if not sink:
+        return False
+    return True
+
+
+def _version_to_string(version):
+    return ".".join([str(x) for x in version])
+
+
+def _string_to_list(version):
+    return [int(x) for x in version.split(".")]
+
+
+def check_requirements():
+    hard_dependencies_satisfied = True
+    for dependency in HARD_DEPENDENCIES:
+        dependency.check()
+        if not dependency.satisfied:
+            if hard_dependencies_satisfied:
+                print _("\nERROR - The following hard dependencies are unmet:")
+                print "=================================================="
+            print dependency
+            hard_dependencies_satisfied = False
+
+    for dependency in SOFT_DEPENDENCIES:
+        dependency.check()
+        if not dependency.satisfied:
+            missing_soft_deps[dependency.modulename] = dependency
+            print _("Missing soft dependency:")
+            print dependency
+
+    if not hard_dependencies_satisfied:
+        return False
+
+    if not _check_audiosinks():
+        print _("Could not create audio output sink. "
+                "Make sure you have a valid one (pulsesink, alsasink or osssink).")
+        return False
+
+    return True
 
 
 def initialize_modules():
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 6b21111..6a1bbab 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -31,7 +31,6 @@ from gi.repository import GES
 
 from gettext import gettext as _
 
-from pitivi.check import missing_soft_deps
 from pitivi.configure import get_ui_dir
 
 from pitivi.dialogs.depsmanager import DepsManager
@@ -488,14 +487,13 @@ class TransformationProperties(Gtk.Expander):
         self.default_values = {}
         self.set_label(_("Transformation"))
 
-        if not "Frei0r" in missing_soft_deps:
-            self.builder = Gtk.Builder()
-            self.builder.add_from_file(os.path.join(get_ui_dir(),
-                        "cliptransformation.ui"))
+        self.builder = Gtk.Builder()
+        self.builder.add_from_file(os.path.join(get_ui_dir(),
+                                                "cliptransformation.ui"))
 
-            self.add(self.builder.get_object("transform_box"))
-            self.show_all()
-            self._initButtons()
+        self.add(self.builder.get_object("transform_box"))
+        self.show_all()
+        self._initButtons()
         self.connect('notify::expanded', self._expandedCb)
         self.hide()
 
@@ -521,13 +519,12 @@ class TransformationProperties(Gtk.Expander):
         self.app.gui.viewer.setZoom(scale.get_value())
 
     def _expandedCb(self, expander, params):
-        if not "Frei0r" in missing_soft_deps:
-            if self._selected_clip:
-                self.effect = self._findOrCreateEffect("frei0r-filter-scale0tilt")
-                self._updateSpinButtons()
-                self.set_expanded(self.get_expanded())
-                self._updateBoxVisibility()
-                self.zoom_scale.set_value(1.0)
+        if self._selected_clip:
+            self.effect = self._findOrCreateEffect("frei0r-filter-scale0tilt")
+            self._updateSpinButtons()
+            self.set_expanded(self.get_expanded())
+            self._updateBoxVisibility()
+            self.zoom_scale.set_value(1.0)
         else:
             if self.get_expanded():
                 DepsManager(self.app)
diff --git a/pitivi/dialogs/depsmanager.py b/pitivi/dialogs/depsmanager.py
index e4de733..4698209 100644
--- a/pitivi/dialogs/depsmanager.py
+++ b/pitivi/dialogs/depsmanager.py
@@ -91,8 +91,8 @@ class DepsManager(object):
         missing dependencies
         """
         label_contents = ""
-        for dep, description in missing_soft_deps.iteritems():
-            label_contents += "• %s (%s)\n" % (dep, description)
+        for depname, dep in missing_soft_deps.iteritems():
+            label_contents += "• %s (%s)\n" % (dep.modulename, dep.additional_message)
         self.builder.get_object("pkg_list").set_text(label_contents)
 
     def show(self):
diff --git a/pitivi/dialogs/startupwizard.py b/pitivi/dialogs/startupwizard.py
index 8662651..8f58c08 100644
--- a/pitivi/dialogs/startupwizard.py
+++ b/pitivi/dialogs/startupwizard.py
@@ -74,8 +74,10 @@ class StartUpWizard(object):
 
         self.recent_chooser.add_filter(_filter)
 
+        missing_button = self.builder.get_object("missing_deps_button")
+
         if not missing_soft_deps:
-            self.builder.get_object("missing_deps_button").hide()
+            missing_button.hide()
 
         self.app.projectManager.connect("new-project-failed", self._projectFailedCb)
         self.app.projectManager.connect("new-project-loaded", self._projectLoadedCb)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b997b12..e448972 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -4,6 +4,7 @@
 #       test_utils.py
 #
 # Copyright (c) 2009, Alessandro Decina <alessandro decina collabora co uk>
+# Copyright (c) 2014, Mathieu Duponchelle <mduponchelle1 gmail com>
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,6 +25,7 @@ from unittest import TestCase
 
 from gi.repository import Gst
 from pitivi.utils.ui import beautify_length
+from pitivi.check import *
 
 second = Gst.SECOND
 minute = second * 60
@@ -50,3 +52,46 @@ class TestBeautifyLength(TestCase):
     def testBeautifyHoursAndMinutes(self):
         self.failUnlessEqual(beautify_length(hour + minute + second),
                 "1 hour, 1 minute")
+
+
+class TestDependencyChecks(TestCase):
+    def testDependencies(self):
+        gi_dep = GstDependency("Gst", "1.0.0")
+        gi_dep.check()
+        self.failUnless(gi_dep.satisfied)
+
+        gi_dep = GstDependency("Gst", "9.9.9")
+        gi_dep.check()
+        self.failIf(gi_dep.satisfied)
+
+        gi_dep = GstDependency("ThisShouldNotExist", None)
+        gi_dep.check()
+        self.failIf(gi_dep.satisfied)
+
+        gi_dep = GtkOrClutterDependency("Gtk", "3.0.0")
+        gi_dep.check()
+        self.failUnless(gi_dep.satisfied)
+
+        gi_dep = GtkOrClutterDependency("Gtk", "9.9.9")
+        gi_dep.check()
+        self.failIf(gi_dep.satisfied)
+
+        cairo_dep = CairoDependency("1.0.0")
+        cairo_dep.check()
+        self.failUnless(cairo_dep.satisfied)
+
+        cairo_dep = CairoDependency("9.9.9")
+        cairo_dep.check()
+        self.failIf(cairo_dep.satisfied)
+
+        classic_dep = ClassicDependency("numpy", None)
+        classic_dep.check()
+        self.failUnless(classic_dep.satisfied)
+
+        gst_plugin_dep = GstPluginDependency("gnonlin", "1.1.90")
+        gst_plugin_dep.check()
+        self.failUnless(gst_plugin_dep.satisfied)
+
+        gst_plugin_dep = GstPluginDependency("gnonlin", "9.9.9")
+        gst_plugin_dep.check()
+        self.failIf(gst_plugin_dep.satisfied)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]