[pitivi] A bit more code, still not 100% done with the design and docs.



commit a85aeb2503582a9ed2f476c98fa9b288c644e7a5
Author: Edward Hervey <bilboed bilboed com>
Date:   Tue Mar 17 22:52:13 2009 +0100

    A bit more code, still not 100% done with the design and docs.
---
 pitivi/formatters/base.py   |  242 ++++++++++++++++++++++++++++++++++++++++---
 pitivi/formatters/format.py |   35 ++++---
 pitivi/project.py           |  212 ++++++++++++++------------------------
 pitivi/sourcelist.py        |    5 +
 pitivi/utils.py             |   30 ++++++
 5 files changed, 359 insertions(+), 165 deletions(-)

diff --git a/pitivi/formatters/base.py b/pitivi/formatters/base.py
index a3c1992..3ae0fa0 100644
--- a/pitivi/formatters/base.py
+++ b/pitivi/formatters/base.py
@@ -24,68 +24,277 @@ Base Formatter classes
 """
 
 from pitivi.project import Project
+from pitivi.utils import uri_is_reachable, uri_is_valid
+from pitivi.signalinterface import Signallable
 
 class FormatterError(Exception):
     pass
 
+class FormatterURIError(Exception):
+    """An error occured with a URI"""
+
 class FormatterLoadError(FormatterError):
-    pass
+    """An error occured while loading the Project"""
+
+class FormatterParseError(FormatterLoadError):
+    """An error occured while parsing the project file"""
 
 class FormatterSaveError(FormatterError):
-    pass
+    """An error occured while saving the Project"""
+
+class FormatterOverwriteError(FormatterSaveError):
+    """A project can't be saved because it will be overwritten"""
 
 # FIXME : How do we handle interaction with the UI ??
 # Do we blindly load everything and let the UI figure out what's missing from
 # the loaded project ?
 
-class Formatter(object):
+class Formatter(object, Signallable):
     """
     Provides convenience methods for storing and loading
     Project files.
 
+    Signals:
+     - C{missing-uri} : A uri can't be found.
+
     @cvar description: Description of the format.
     @type description: C{str}
+    @cvar project: The project being loaded/saved
+    @type project: L{Project}
     """
 
+    __signals__ = {
+        "missing-uri" : ["uri"]
+        }
+
     description = "Description of the format"
 
+    def __init__(self):
+        # mapping of directory changes
+        # key : old path
+        # value : new path
+        self.directorymapping = {}
+
+        self.project = None
+
+    #{ Load/Save methods
+
     def loadProject(self, location):
         """
         Loads the project from the given location.
 
-        @type location: L{str}
-        @param location: The location of a file. Needs to be an absolute URI.
+        @postcondition: There is no guarantee that the returned project
+        is fully loaded. Callers should check
 
-        @rtype: C{Project}
-        @return: The C{Project}
+        @type location: C{URI}
+        @param location: The location of a file. Needs to be an absolute URI.
+        @rtype: L{Project}
+        @return: The L{Project}
         @raise FormatterLoadError: If the file couldn't be properly loaded.
         """
-        raise FormatterLoadError("No Loading feature")
+        # check if the location is
+        # .. a uri
+        # .. a valid uri
+        # .. a reachable valid uri
+        # FIXME : Allow subclasses to handle this for 'online' (non-file://) URI
+        if not uri_is_valid(location) or not uri_is_reachable(location):
+            raise FormatterURIError()
 
-    def saveProject(self, project, location):
+        # parse the format (subclasses)
+        # FIXME : maybe have a convenience method for opening a location
+        self.parse(location)
+
+        # create a NewProject
+        # FIXME : allow subclasses to create their own Project subclass
+        project = Project()
+
+        # ask for all sources being used
+        uris = []
+        factories = []
+        wtf = []
+        for x in self._getSources():
+            if isinstance(x, SourceFactory):
+                factories.append(x)
+            elif isinstance(x, str):
+                uris.append(x)
+            else:
+                raise FormatterLoadError("Got invalid sources !")
+
+        # from this point on we're safe !
+        self.project = project
+        project._formatter = self
+
+        # add all factories to the project sourcelist
+        for fact in factories:
+            project.sources.addFactory(fact)
+
+        # if all sources were discovered, or don't require discovering,
+        if uris == []:
+            # then
+            # .. Fill in the timeline
+            self._fillTimeline(self)
+            # .. make the project as loaded
+            self.project.loaded = True
+        else:
+            # else
+            # .. connect to the sourcelist 'ready' signal
+            self.project.sources.connect("ready", self._sourcesReadyCb)
+            # .. Add all uris to be discovered to the project sourcelist
+            self.project.loaded = False
+            self.project.sources.addUris(uris)
+
+        # finally return the project.
+        return self.project
+
+    def saveProject(self, project, location, overwrite=False):
         """
         Saves the given project to the given location.
 
-        @type project: C{Project}
+        @type project: L{Project}
         @param project: The Project to store.
-        @type location: L{str}
+        @type location: C{URI}
         @param location: The location where to store the project. Needs to be
         an absolute URI.
+        @param overwrite: Whether to overwrite existing location.
+        @type overwrite: C{bool}
+        @raise FormatterURIError: If the location isn't a valid C{URI}.
+        @raise FormatterOverwriteError: If the location already exists and overwrite is False.
         @raise FormatterSaveError: If the file couldn't be properly stored.
         """
-        raise FormatterSaveError("No Saving feature")
+        if not uri_is_valid(location):
+            raise FormatterURIError()
+        if overwrite == False and uri_is_reachable(location):
+            raise FormatterOverwriteError()
+        return self._saveProject(project, location)
 
-    def canHandle(self, location):
+    #}
+
+    @classmethod
+    def canHandle(cls, location):
         """
         Can this Formatter load the project at the given location.
 
-        @type location: L{str}
+        @type location: C{URI}
         @param location: The location. Needs to be an absolute C{URI}.
-        @rtype: L{bool}
-        @return: True if this Formatter can load the C{Project}.
+        @rtype: C{bool}
+        @return: True if this Formatter can load the L{Project}.
         """
         raise NotImplementedError
 
+    #{ Subclass methods
+
+    def _saveProject(self, project, location):
+        """
+        Save the given project to the given location.
+
+        Sub classes should implement this.
+
+        @precondition: The location is guaranteed to be writable.
+
+        @param project: the project to store.
+        @type project: L{Project}
+        @type location: C{URI}
+        @param location: The location where to store the project. Needs to be
+        an absolute URI.
+        """
+        raise NotImplementedError
+
+    def _getSources(self):
+        """
+        Return all the sources used in a project.
+
+        To be implemented by subclasses.
+
+        The returned sources can be either:
+         - C{URI}
+         - any L{SourceFactory} fully-discovered subclass.
+
+        The returned locations (C{URI}) must be valid uri. Subclasses can
+        call L{validateSourceURI} to make sure the C{URI} is valid.
+
+        @precondition: L{_parse} will be called before, so subclasses can
+        use any information they extracted during that call.
+        @returns: A list of sources used in the given project.
+        """
+        raise NotImplementedError
+
+    def _parse(self, location):
+        """
+        Open and parse the given location.
+
+        To be implemented by subclasses.
+
+        If any error occurs during this step, subclasses should raise the
+        FormatterParseError exception.
+        """
+        raise NotImplementedError
+
+    #{ Missing uri methods
+
+    def addMapping(self, oldpath, newpath):
+        """
+        Add a mapping for moved files.
+
+        This should be called in callbacks from 'missing-uri'.
+
+        @param oldpath: Old location (as provided by 'missing-uri').
+        @type oldpath: C{URI}
+        @param newpath: The new location corresponding to oldpath.
+        @type newpath: C{URI}
+        """
+        raise NotImplementedError
+
+    def validateSourceURI(self, uri):
+        """
+        Makes sure the given uri is accessible for reading.
+
+        Subclasses should call this method for any C{URI} they parse,
+        in order to make sure they have the valid C{URI} on this given
+        setup.
+
+        @returns: The valid 'uri'. It might be different from the
+        input. Sub-classes must use this for any URI they wish to
+        read from. If no valid 'uri' can be found, None will be
+        returned.
+        @rtype: C{URI} or C{None}
+        """
+        if not uri_is_valid(uri):
+            return None
+
+        # skip non local uri
+        if not uri.split('://', 1)[0] in ["file"]:
+            return uri
+
+        # first check the good old way
+        if not uri_is_valid(uri) or not uri_is_reachable(uri):
+            return None
+
+        localpath = uri.split('://', 1)[1]
+
+        # else let's figure out if we have a compatible mapping
+        for k, v in self.directorymapping.iteritems():
+            if localpath.startswith(k):
+                return localpath.replace(k, v, 1)
+
+        # else, let's fire the signal...
+        self.emit('missing-uri', uri)
+
+        # and check again
+        for k, v in self.directorymapping.iteritems():
+            if localpath.startswith(k):
+                return localpath.replace(k, v, 1)
+
+        # Houston, we have lost contact with mission://fail
+        return None
+
+    #}
+
+    def _sourcesReadyCb(self, sources):
+        self._fillTimeline(self)
+        self.project.loaded = True
+        Project.emit(self.project, 'loaded')
+
+
 class LoadOnlyFormatter(Formatter):
     def saveProject(self, project, location):
         raise FormatterSaveError("No Saving feature")
@@ -101,3 +310,4 @@ class DefaultFormatter(Formatter):
     description = "PiTiVi default file format"
 
     pass
+
diff --git a/pitivi/formatters/format.py b/pitivi/formatters/format.py
index 1930182..53d9d7a 100644
--- a/pitivi/formatters/format.py
+++ b/pitivi/formatters/format.py
@@ -25,7 +25,7 @@ High-level tools for using Formatters
 
 # FIXME : We need a registry of all available formatters
 
-def load_project(uri, formatter=None):
+def load_project(uri, formatter=None, missinguricallback=None):
     """
     Load the project from the given location.
 
@@ -34,31 +34,40 @@ def load_project(uri, formatter=None):
     @type uri: L{str}
     @param uri: The location of the project. Needs to be an
     absolute URI.
-    @type formatter: C{Formatter}
+    @type formatter: L{Formatter}
     @param formatter: If specified, try loading the project with that
-    C{Formatter}. If not specified, will try all available C{Formatter}s.
+    L{Formatter}. If not specified, will try all available L{Formatter}s.
     @raise FormatterLoadError: If the location couldn't be properly loaded.
-    @return: The loaded C{Project}
+    @param missinguricallback: A callback that will be used if some
+    files to load can't be found anymore. The callback shall call the
+    formatter's addMapping() method with the moved location.
+    @type missinguricallback: C{callable}
+    @return: The project. The caller needs to ensure the loading is
+    finished before using it. See the 'loaded' property and signal of
+    L{Project}.
+    @rtype: L{Project}.
     """
     raise NotImplementedError
 
-def save_project(project, uri, formatter=None):
+def save_project(project, uri, formatter=None, overwrite=False):
     """
-    Save the C{Project} to the given location.
+    Save the L{Project} to the given location.
 
     If specified, use the given formatter.
 
-    @type project: C{Project}
-    @param project: The C{Project} to save.
+    @type project: L{Project}
+    @param project: The L{Project} to save.
     @type uri: L{str}
     @param uri: The location to store the project to. Needs to
     be an absolute URI.
-    @type formatter: C{Formatter}
-    @param formatter: The C{Formatter} to use to store the project if specified.
+    @type formatter: L{Formatter}
+    @param formatter: The L{Formatter} to use to store the project if specified.
     If it is not specified, then it will be saved at its original format.
+    @param overwrite: Whether to overwrite existing location.
+    @type overwrite: C{bool}
     @raise FormatterSaveError: If the file couldn't be properly stored.
-    @return: Whether the file was successfully stored
-    @rtype: L{bool}
+
+    @see: L{Formatter.saveProject}
     """
     raise NotImplementedError
 
@@ -69,7 +78,7 @@ def can_handle_location(uri):
     @type uri: L{str}
     @param uri: The location of the project. Needs to be an
     absolute URI.
-    @return: Whether the location contains a valid C{Project}.
+    @return: Whether the location contains a valid L{Project}.
     @rtype: L{bool}
     """
     raise NotImplementedError
diff --git a/pitivi/project.py b/pitivi/project.py
index 040aa7f..c55e4a8 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -40,41 +40,45 @@ from pitivi.configure import APPNAME
 from pitivi.signalinterface import Signallable
 from pitivi.action import ViewAction
 
+class ProjectError(Exception):
+    """Project error"""
+    pass
+
+class ProjectSaveLoadError(ProjectError):
+    """Error while loading/saving project"""
+    pass
+
 class Project(object, Signallable, Loggable):
-    """ The base class for PiTiVi projects
-    Signals
-
-       boolean save-uri-requested()
-            The the current project has been requested to save itself, but
-            needs a URI to which to save. Handlers should first call
-            setUri(), with the uri to save the file (optionally
-            specifying the file format) and return True, or simply
-            return False to cancel the file save operation.
-
-        boolean confirm-overwrite()
-            The project has been requested to save itself, but the file on
-            disk either already exists, or has been changed since the previous
-            load/ save operation. In this case, the project wants permition to
-            overwrite before continuing. handlers should return True if it is
-            ok to overwrit the file, or False otherwise. By default, this
-            signal handler assumes True.
-
-        void settings-changed()
-            The project settings have changed
-
-    @cvar timeline: The timeline
+    """The base class for PiTiVi projects
+
+    @ivar name: The name of the project
+    @type name: C{str}
+    @ivar description: A description of the project
+    @type description: C{str}
+    @ivar sources: The sources used by this project
+    @type sources: L{SourceList}
+    @ivar timeline: The timeline
     @type timeline: L{Timeline}
-    @cvar pipeline: The timeline's pipeline
+    @ivar pipeline: The timeline's pipeline
     @type pipeline: L{Pipeline}
-    @cvar factory: The timeline factory
+    @ivar factory: The timeline factory
     @type factory: L{TimelineSourceFactory}
+    @ivar format: The format under which the project is currently stored.
+    @type format: L{FormatterClass}
+    @ivar loaded: Whether the project is fully loaded or not.
+    @type loaded: C{bool}
+
+    Signals:
+     - C{missing-plugins} : A plugin is missing for the given uri
+     - C{loaded} : The project is now fully loaded.
     """
 
     __signals__ = {
         "save-uri-requested" : None,
         "confirm-overwrite" : ["location"],
         "settings-changed" : None,
-        "missing-plugins": ["uri", "detail", "description"]
+        "missing-plugins": ["uri", "detail", "description"],
+        "loaded" : None
         }
 
     def __init__(self, name="", uri=None, **kwargs):
@@ -91,9 +95,13 @@ class Project(object, Signallable, Loggable):
         self.urichanged = False
         self.format = None
         self.sources = SourceList(self)
+
         self.settingssigid = 0
         self._dirty = False
 
+        # formatter instance used for loading project.
+        self._formatter = None
+
         self.sources.connect('missing-plugins', self._sourceListMissingPluginsCb)
 
         self.timeline = Timeline()
@@ -111,110 +119,15 @@ class Project(object, Signallable, Loggable):
         self.view_action = ViewAction()
         self.view_action.addProducers(self.factory)
 
-        # don't want to make calling load() necessary for blank projects
-        if self.uri == None:
-            self._loaded = True
-        else:
-            self._loaded = False
+        # the loading formatter will set this accordingly
+        self.loaded = True
 
     def release(self):
         self.pipeline.release()
         self.pipeline = None
 
-    def load(self):
-        """ call this to load a project from a file (once) """
-        if self._loaded:
-            # should this return false?
-            self.warning("Already loaded !!!")
-            return True
-        try:
-            res = self._load()
-        except:
-            self.error("An Exception was raised during loading !")
-            traceback.print_exc()
-            res = False
-        finally:
-            return res
-
-    def _load(self):
-        """
-        loads the project from a file
-        Private method, use load() instead
-        """
-        self.log("uri:%s", self.uri)
-        self.debug("Creating timeline")
-        # FIXME : This should be discovered !
-        saveformat = "pickle"
-        if self.uri and file_is_project(self.uri):
-            loader = ProjectSaver.newProjectSaver(saveformat)
-            path = gst.uri_get_location(self.uri)
-            fileobj = open(path, "r")
-            try:
-                tree = loader.openFromFile(fileobj)
-                self.fromDataFormat(tree)
-            except ProjectLoadError:
-                self.error("Error while loading the project !!!")
-                return False
-            finally:
-                fileobj.close()
-            self.format = saveformat
-            self.urichanged = False
-            self.debug("Done loading !")
-            return True
-        return False
-
-    def _save(self):
-        """ internal save function """
-        if uri_is_valid(self.uri):
-            path = gst.uri_get_location(self.uri)
-        else:
-            self.warning("uri '%s' is invalid, aborting save", self.uri)
-            return False
-
-        #TODO: a bit more sophisticated overwite detection
-        if os.path.exists(path) and self.urichanged:
-            overwriteres = self.emit("confirm-overwrite", self.uri)
-            if overwriteres == False:
-                self.log("aborting save because overwrite was denied")
-                return False
-
-        try:
-            fileobj = open(path, "w")
-            loader = ProjectSaver.newProjectSaver(self.format)
-            tree = self.toDataFormat()
-            loader.saveToFile(tree, fileobj)
-            self._dirty = False
-            self.urichanged = False
-            self.log("Project file saved successfully !")
-            return True
-        except IOError:
-            return False
-
-    def save(self):
-        """ Saves the project to the project's current file """
-        self.log("saving...")
-        if self.uri:
-            return self._save()
-
-        self.log("requesting for a uri to save to...")
-        saveres = self.emit("save-uri-requested")
-        if saveres == None or saveres == True:
-            self.log("'save-uri-requested' returned True, self.uri:%s", self.uri)
-            if self.uri:
-                return self._save()
-
-        self.log("'save-uri-requested' returned False or uri wasn't set, aborting save")
-        return False
-
-    def saveAs(self):
-        """ Saves the project to the given file name """
-        if not self.emit("save-uri-requested"):
-            return False
-        if not self.uri:
-            return False
-        return self._save()
-
-    # setting methods
+    #{ Settings methods
+
     def _settingsChangedCb(self, unused_settings):
         self.emit('settings-changed')
 
@@ -289,22 +202,49 @@ class Project(object, Signallable, Loggable):
 
         return settings
 
+    #}
+
+    def _sourceListMissingPluginsCb(self, source_list, uri, detail, description):
+        return self.emit('missing-plugins', uri, detail, description)
+
+    #{ Save and Load features
+
+    def save(self, location=None, overwrite=False):
+        """
+        Save the project to the given location.
+
+        @param location: The location to write to. If not specified, the
+        current project location will be used (if set).
+        @type location: C{URI}
+        @param overwrite: Whether to overwrite existing location.
+        @type overwrite: C{bool}
+
+        @raises ProjectSaveLoadError: If no uri was provided and none was set
+        previously.
+        """
+        self.log("saving...")
+        location = location or self.uri
+
+        if location == None:
+            raise ProjectSaveLoadError("Location unknown")
+
+        save_project(self, location or self.uri, self.format,
+                     overwrite)
+
+        self.uri = location
+
     def setModificationState(self, state):
         self._dirty = state
 
     def hasUnsavedModifications(self):
         return self._dirty
 
-    def _sourceListMissingPluginsCb(self, source_list, uri, detail, description):
-        return self.emit('missing-plugins', uri, detail, description)
-
-def uri_is_valid(uri):
-    """ Checks if the given uri is a valid uri (of type file://) """
-    return gst.uri_get_protocol(uri) == "file"
+    def markLoaded(self):
+        """
+        Mark the project as loaded.
 
-def file_is_project(uri):
-    """ returns True if the given uri is a PitiviProject file"""
-    if not uri_is_valid(uri):
-        raise NotImplementedError(
-            _("%s doesn't yet handle non local projects") % APPNAME)
-    return os.path.isfile(gst.uri_get_location(uri))
+        Will emit the 'loaded' signal. Only meant to be used by
+        L{Formatter}s.
+        """
+        self.loaded = True
+        self.emit('loaded')
diff --git a/pitivi/sourcelist.py b/pitivi/sourcelist.py
index 468f96a..f7cac59 100644
--- a/pitivi/sourcelist.py
+++ b/pitivi/sourcelist.py
@@ -31,6 +31,11 @@ class SourceList(object, Signallable, Loggable):
     """
     Contains the sources for a project, stored as FileSourceFactory
 
+    @ivar project: The owner project
+    @type project: L{Project}
+    @ivar discoverer: The discoverer used
+    @type discoverer: L{Discoverer}
+
     Signals:
      - C{file_added} : A file has been completely discovered and is valid.
      - C{file_removed} : A file was removed from the SourceList.
diff --git a/pitivi/utils.py b/pitivi/utils.py
index ba5dc75..e521b89 100644
--- a/pitivi/utils.py
+++ b/pitivi/utils.py
@@ -24,6 +24,7 @@
 
 import gobject
 import gst, bisect
+import os
 from pitivi.signalinterface import Signallable
 import pitivi.log.log as log
 
@@ -191,6 +192,35 @@ def filter_(caps):
     f.props.caps = gst.caps_from_string(caps)
     return f
 
+
+## URI functions
+
+
+def uri_is_valid(uri):
+    """Checks if the given uri is a valid uri (of type file://)
+
+    @param uri: The location to check
+    @type uri: C{URI}
+    """
+    res = gst.uri_is_valid(uri) and gst.uri_get_protocol(uri) == "file"
+    if res:
+        return len(os.path.basename(gst.uri_get_location(uri))) > 0
+    return res
+
+def uri_is_reachable(uri):
+    """ Check whether the given uri is reachable and we can read/write
+    to it.
+
+    @param uri: The location to check
+    @type uri: C{URI}
+    @return: C{True} if the uri is reachable.
+    @rtype: C{bool}
+    """
+    if not uri_is_valid(uri):
+        raise NotImplementedError(
+            _("%s doesn't yet handle non local projects") % APPNAME)
+    return os.path.isfile(gst.uri_get_location(uri))
+
 class PropertyChangeTracker(object, Signallable):
     def __init__(self, timeline_object):
         self.properties = {}



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