From dc8da8823e6b8dd421b66b4db9e8834792822381 Mon Sep 17 00:00:00 2001 From: einarr Date: Sun, 6 Mar 2011 23:08:09 +0000 Subject: [PATCH] Partial fix to #22. Laydi will require a project directory to run. If the project directory does not exist, i.e. if a new project should be created, the new -n switch is required. New syntax to run laydi is now {{{ laydi [options] }}} --- bin/laydi | 27 ++- laydi/project.py | 429 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 314 insertions(+), 142 deletions(-) diff --git a/bin/laydi b/bin/laydi index a5ae110..a659747 100755 --- a/bin/laydi +++ b/bin/laydi @@ -3,7 +3,7 @@ from getopt import getopt import os import sys -from laydi import laydi, projectview, workflow, main +from laydi import laydi, project, projectview, workflow, main #import workflows from laydi import cfgparse import optparse @@ -73,11 +73,20 @@ def parse_options(): action='store_true', help='Generate configuration file ~/.laydi if it does not exist.') + op.add_option('-n', '--new-project', + action='store_true', + help='Create new project directory.') + for cf in conf_files: if os.path.isfile(cf): cp.add_file(cf) - return cp.parse(op) + options, params = cp.parse(op) + if len(params) != 1: + print "error: project directory must be specified." + sys.exit(1) + + return options, params if __name__ == '__main__': import gtk @@ -86,6 +95,8 @@ if __name__ == '__main__': gnome.program_init(PROGRAM_NAME, VERSION) options, params = parse_options() + + ## Workflow setup main.options = options for dir in main.options.workflowdir.split(';'): @@ -112,8 +123,18 @@ if __name__ == '__main__': main.set_options(options) app = laydi.LaydiApp() + ## Project setup + prjroot = params[0] + if not project.is_project_directory(prjroot): + if options.new_project: + project.make_project_directory(prjroot) + else: + print "error: project directory not found: %s" % prjroot + print "notice: use the -n option to make a new project" + sys.exit(2) + main.set_application(app) - main.set_projectview(projectview.ProjectView()) + main.set_projectview(projectview.ProjectView(prjroot)) app.set_projectview(main.projectview) app.show() diff --git a/laydi/project.py b/laydi/project.py index e21a416..48be392 100644 --- a/laydi/project.py +++ b/laydi/project.py @@ -1,154 +1,305 @@ -import os -import scipy -import gobject -import gtk -import laydi -import logger, dataset, plots, main +import os, os.path +import sys +import configobj +import time -class Project: - """A Project contains datasets, selections etc. - The project, of which the application has only one at any given time, - is the container for all datasets, plots and selections in use. The data - in the project is organized in a gtk.TreeStrore that is displayed in the - navigator. +import dataset + +NAME = "laydi-cmd" +VERSION = "0.1.0" +PROJECT_VERSION_STRING = "Laydi project version 1" + +def is_project_directory(dirname): + """Verifies that a directory is a laydi project""" + + if not os.path.isdir(dirname): + return False + + ## Verify that the version is correct. + version_fn = os.path.join(dirname, "VERSION") + if not os.path.exists(version_fn): + return False + fd = open(version_fn) + line = fd.readline() + fd.close() + + if line.strip() != PROJECT_VERSION_STRING: + return False + + ## Require directories to be present. + if not os.path.isdir(os.path.join(dirname, "annotations")): + return False + if not os.path.isdir(os.path.join(dirname, "data")): + return False + if not os.path.isdir(os.path.join(dirname, "selections")): + return False + if not os.path.isdir(os.path.join(dirname, "exports")): + return False + + ## If no tests failed, return True + return True + + +def make_project_directory(dirname, force=False): + """Creates a project directory + + force: ignore that directory exists and proceed anyway. """ + if os.path.exists(dirname) and not force: + return False - def __init__(self, name="Testing"): - self.data_tree = gtk.TreeStore(str, - str, - object, - str, - str, - gobject.TYPE_OBJECT, - float) - - self.name = name - self.dim_names = [] - self._selection_observers = [] - self._dataset_observers = [] - self.current_data = [] - self.datasets = [] - self.sel_obj = dataset.Selection('Current Selection') - self.selections = [] - self._last_selection = None - self._dataset_iter_map = {} + rootdir = dirname + anndir = os.path.join(dirname, "annotations") + seldir = os.path.join(dirname, "selections") + datadir = os.path.join(dirname, "data") + exportdir = os.path.join(dirname, "exports") + version_file_path = os.path.join(dirname, "VERSION") - def add_selection_observer(self, observer): - self._selection_observers.append(observer) - observer.selection_changed(None, self.get_selection()) + os.makedirs(rootdir) + for d in [anndir, seldir, datadir, exportdir]: + os.mkdir(d) - def notify_selection_listeners(self, dim_name): - """Notifies observers""" - for observer in self._selection_observers: - observer.selection_changed(dim_name, self.get_selection()) + fd = open(version_file_path, "w") + print >> fd, PROJECT_VERSION_STRING + fd.close() - def add_dataset_observer(self, observer): - self._dataset_observers.append(observer) - observer.dataset_changed() - def notify_dataset_listeners(self): - """Notifies observers when new datasets are added""" - for observer in self._dataset_observers: - observer.dataset_changed() + +class Universe(object): + """A Universe is a collection of all existing identifiers in a set of datasets""" + + def __init__(self): + self.refcount = {} + + def register_dim(self, dim): + """Increase reference count for identifiers in Dimension object dim""" + d = self.refcount.get(dim.name, None) + if d == None: + d = {} + self.refcount[dim.name] = d + for i in dim: + d[i] = d.get(i, 0) + 1 - def set_selection(self, dim_name, selection): - """Sets a current selection and notify observers""" - self.sel_obj[dim_name] = set(selection) - self.notify_selection_listeners(dim_name) - self._last_selection = selection + def register_ds(self, ds): + """Increase reference count for identifiers in all Dimensions of dataset ds""" + for dim in ds.dims: + self.register_dim(dim) - def get_selection(self): - """Returns the current selection object""" - return self.sel_obj - - def delete_data(self, it): - """Delete elements from the project.""" - child = self.data_tree.iter_children(it) - while child != None: - c = self.data_tree.iter_next(child) - self.delete_data(child) - child = c - main.application.main_view.remove_view(self.data_tree.get(it, 2)[0]) - self.data_tree.remove(it) - - def add_data(self, parents, data, fun='Function'): - """Adds a set of data and plots to the navigator. - - This method is usually called after a Function in a workflow - has finished and returns its output.""" - - if len(parents) > 0: - parent_iter = self._dataset_iter_map[parents[0]] - else: - parent_iter = None - - # Add the function node to the tree - icon = laydi.icon_factory.get("folder_grey") - it = self.data_tree_insert(parent_iter, fun, None, None, "black", icon) - - # Add all returned datasets/plots/selections - for d in data: - # Any kind of dataset - if isinstance(d, dataset.Dataset): - if isinstance(d, dataset.GraphDataset): - icon = laydi.icon_factory.get("graph_dataset") - elif isinstance(d, dataset.CategoryDataset): - icon = laydi.icon_factory.get("category_dataset") - else: - icon = laydi.icon_factory.get("dataset") - - self.add_dataset(d) - self.data_tree_insert(it, d.get_name(), d, None, "black", icon) - - # Any kind of plot - elif isinstance(d, plots.Plot): - icon = laydi.icon_factory.get("line_plot") - self.data_tree_insert(it, d.get_title(), d, None, "black", icon) - d.set_selection_listener(self.set_selection) - self._selection_observers.append(d) - - # Selections are not added to the data tree - elif isinstance(d, dataset.Selection): - self.add_selection(d) - - def data_tree_insert(self, parent, text, data, bg, fg, icon, selected = 0): - """Inserts data into the tree view. - @param text: The title of the object. - @param data: A dataset, plot or function object. - @param bg: Background color. - @param fg: Foreground (font) color. - @param icon: Pixmap icon. + def unregister_dim(self, dim): + """Update reference count for identifiers in Dimension object dim + Update reference count for identifiers in Dimension object dim, and remove all + identifiers with a reference count of 0, as they do not (by definition) exist + any longer. """ - tree = self.data_tree - it = tree.append(parent) - tree[it] = [text, type(data), data, bg, fg, icon, selected] - self._dataset_iter_map[data] = it - return it + ids = self.refcount[dim.name] + for i in dim: + refcount = ids[i] + if refcount == 1: + ids.pop(i) + else: + ids[i] -= 1 + if len(ids) == 0: + self.refcount.pop(dim.name) - def add_dataset(self, dataset): - """Appends a new Dataset to the project.""" - logger.log('debug','Adding dataset: %s' %dataset.get_name()) - self.datasets.append(dataset) - for dim_name in dataset.get_all_dims(): - if dim_name not in self.dim_names: - self.dim_names.append(dim_name) - self.sel_obj[dim_name] = set() - self.notify_selection_listeners(dim_name) - self.notify_dataset_listeners() - def add_selection(self, selection): - """Adds a new selection to the project.""" - self.selections.append(selection) - self.notify_dataset_listeners() + def unregister_ds(self, ds): + """Update reference count for identifiers along Dimensions in Dataset ds. + Update reference count for identifiers along all Dimensions in + Dataset ds, and remove all identifiers with a reference count of 0, + as they do not (by definition) exist any longer. + """ + for dim in ds: + self.register_dim(dim) + + def register(self, obj): + if isinstance(obj, Dataset): + self.register_ds(obj) + else: + self.register_dim(obj) + + def unregister(self, obj): + if isinstance(obj, Dataset): + self.unregister_ds(obj) + else: + self.unregister_dim(obj) + + def __getent___(self, dimname): + return set(self.references[dimname].keys()) + + def __iter__(self): + return self.references.keys().__iter__() + + +class Dimension(object): + """A Dimension represents the set of identifiers an object has along an axis. + """ + def __init__(self, name, ids=[]): + self.name = name + self.idset = set(ids) + self.idlist = list(ids) + + if len(self.idset) != len(self.idlist): + raise Exception("Duplicate identifiers are not allowed") + + def __getitem__(self, element): + return self.idlist[element] + + def __getslice__(self, start, end): + return self.idlist[start:end] + + def __contains__(self, element): + return self.idset.__contains__(element) - def object_at(self, path): - """Returns the object at a given path in the tree.""" - it = self.get_iter(path) - obj = self[it][2] - if obj: - obj.show() - return obj + def __str__(self): + return "%s: %s" % (self.name, str(self.idlist)) - #def set_current_data(self, obj): - # self.current_data = obj + def __len__(self): + return len(self.idlist) + + def __iter__(self): + return iter(self.idlist) + + def intersection(self, dim): + return self.idset.intersection(dim.idset) + + def as_tuple(self): + return (self.name, self.idlist) + + +class Directory(object): + def __init__(self, path): + self.path = path + self.files = set() + self.timestamp = -1 + self.update() + + def update(self): + now = time.time() + newfiles = set(os.listdir(self.path)) + for fn in newfiles - self.files: + if os.path.isdir(os.path.join(self.path, fn)): + self.dir_created(fn) + else: + self.file_created(fn) + for fn in self.files - newfiles: + if os.path.isdir(os.path.join(self.path, fn)): + self.dir_deleted(fn) + else: + self.file_removed(fn) + for fn in self.files.intersection(newfiles): + filepath = os.path.join(self.path, fn) + if os.path.getctime(filepath) >= self.timestamp: + if os.path.isdir(filepath): + self.dir_changed(fn) + else: + self.file_changed(fn) + self.files = newfiles + self.timestamp = now + + def file_created(self, fn): + print "file created: %s" % fn + pass + + def file_changed(self, fn): + print "file changed: %s" % fn + pass + + def file_removed(self, fn): + print "file removed: %s" % fn + pass + + def dir_created(self, fn): + print "directory created: %s" % fn + pass + + def dir_changed(self, fn): + print "directory changed: %s" % fn + pass + + def dir_removed(self, fn): + print "directory removed: %s" % fn + pass + + +class DataDirectory(Directory): + def __init__(self, dirname, project): + self.project = project + self.datasets= [] + self.dsfiles = {} + Directory.__init__(self, dirname) + + def file_created(self, fn): + """Called from update() when new files are created. + + Load new datasets that have appeared since last update. + """ + filepath = os.path.join(self.path, fn) + name, ext = os.path.splitext(fn) + if ext == ".ftsv": + ds = dataset.read_ftsv(filepath) + self.datasets.append(ds) + self.dsfiles[fn] = ds + + def file_changed(self, fn): + """Called from update() when files are changed. + + Delete old dataset and load the new one when dataset files + have been changed. + """ + filepath = os.path.join(self.path, fn) + name, ext = os.path.splitext(fn) + print self.datasets + if ext == ".ftsv": + oldds = self.dsfiles[fn] + self.datasets.remove(oldds) + + ds = dataset.read_ftsv(filepath) + self.datasets.append(ds) + + self.dsfiles[fn] = ds + print self.datasets + + def file_removed(self, fn): + """Called from update() when a file is deleted + + Removes the associated dataset if a dataset file is removed. + """ + filepath = os.path.join(self.path, fn) + name, ext = os.path.splitext(fn) + if ext == ".ftsv": + ds = self.dsfiles[fn] + self.datasets.remove(ds) + self.dsfiles.pop(fn) + + def dir_created(self, fn): + """Called from update() when a subdirectory is created. + + Instantiate new handlers for the directory if possible. + """ + filepath = os.path.join(self.path, fn) + + +def SelectionDirectory(object): + def __init__(self, dirname): + pass + +class Project(object): + def __init__(self, dirname): + """Opens a project directory. The directory must exist and be a valid project.""" + + ## Set path names. + self.rootdir = dirname + self.anndir = os.path.join(dirname, "annotations") + self.seldir = os.path.join(dirname, "selections") + self.datadir = os.path.join(dirname, "data") + self.exportdir = os.path.join(dirname, "exports") + version_file_path = os.path.join(dirname, "VERSION") + + self.universe = Universe() + + self.data = DataDirectory(self.datadir, self) + + def update(self): + print "updating project" + self.data.update()