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] </path/to/project> }}}
This commit is contained in:
parent
b5d2e8e181
commit
dc8da8823e
27
bin/laydi
27
bin/laydi
@ -3,7 +3,7 @@
|
|||||||
from getopt import getopt
|
from getopt import getopt
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from laydi import laydi, projectview, workflow, main
|
from laydi import laydi, project, projectview, workflow, main
|
||||||
#import workflows
|
#import workflows
|
||||||
from laydi import cfgparse
|
from laydi import cfgparse
|
||||||
import optparse
|
import optparse
|
||||||
@ -73,11 +73,20 @@ def parse_options():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Generate configuration file ~/.laydi if it does not exist.')
|
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:
|
for cf in conf_files:
|
||||||
if os.path.isfile(cf):
|
if os.path.isfile(cf):
|
||||||
cp.add_file(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__':
|
if __name__ == '__main__':
|
||||||
import gtk
|
import gtk
|
||||||
@ -86,6 +95,8 @@ if __name__ == '__main__':
|
|||||||
gnome.program_init(PROGRAM_NAME, VERSION)
|
gnome.program_init(PROGRAM_NAME, VERSION)
|
||||||
|
|
||||||
options, params = parse_options()
|
options, params = parse_options()
|
||||||
|
|
||||||
|
## Workflow setup
|
||||||
main.options = options
|
main.options = options
|
||||||
|
|
||||||
for dir in main.options.workflowdir.split(';'):
|
for dir in main.options.workflowdir.split(';'):
|
||||||
@ -112,8 +123,18 @@ if __name__ == '__main__':
|
|||||||
main.set_options(options)
|
main.set_options(options)
|
||||||
app = laydi.LaydiApp()
|
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_application(app)
|
||||||
main.set_projectview(projectview.ProjectView())
|
main.set_projectview(projectview.ProjectView(prjroot))
|
||||||
|
|
||||||
app.set_projectview(main.projectview)
|
app.set_projectview(main.projectview)
|
||||||
app.show()
|
app.show()
|
||||||
|
405
laydi/project.py
405
laydi/project.py
@ -1,154 +1,305 @@
|
|||||||
import os
|
import os, os.path
|
||||||
import scipy
|
import sys
|
||||||
import gobject
|
import configobj
|
||||||
import gtk
|
import time
|
||||||
import laydi
|
|
||||||
import logger, dataset, plots, main
|
|
||||||
|
|
||||||
class Project:
|
import dataset
|
||||||
"""A Project contains datasets, selections etc.
|
|
||||||
The project, of which the application has only one at any given time,
|
NAME = "laydi-cmd"
|
||||||
is the container for all datasets, plots and selections in use. The data
|
VERSION = "0.1.0"
|
||||||
in the project is organized in a gtk.TreeStrore that is displayed in the
|
PROJECT_VERSION_STRING = "Laydi project version 1"
|
||||||
navigator.
|
|
||||||
|
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"):
|
rootdir = dirname
|
||||||
self.data_tree = gtk.TreeStore(str,
|
anndir = os.path.join(dirname, "annotations")
|
||||||
str,
|
seldir = os.path.join(dirname, "selections")
|
||||||
object,
|
datadir = os.path.join(dirname, "data")
|
||||||
str,
|
exportdir = os.path.join(dirname, "exports")
|
||||||
str,
|
version_file_path = os.path.join(dirname, "VERSION")
|
||||||
gobject.TYPE_OBJECT,
|
|
||||||
float)
|
|
||||||
|
|
||||||
|
os.makedirs(rootdir)
|
||||||
|
for d in [anndir, seldir, datadir, exportdir]:
|
||||||
|
os.mkdir(d)
|
||||||
|
|
||||||
|
fd = open(version_file_path, "w")
|
||||||
|
print >> fd, PROJECT_VERSION_STRING
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 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 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.
|
||||||
|
"""
|
||||||
|
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 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.name = name
|
||||||
self.dim_names = []
|
self.idset = set(ids)
|
||||||
self._selection_observers = []
|
self.idlist = list(ids)
|
||||||
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 = {}
|
|
||||||
|
|
||||||
def add_selection_observer(self, observer):
|
if len(self.idset) != len(self.idlist):
|
||||||
self._selection_observers.append(observer)
|
raise Exception("Duplicate identifiers are not allowed")
|
||||||
observer.selection_changed(None, self.get_selection())
|
|
||||||
|
|
||||||
def notify_selection_listeners(self, dim_name):
|
def __getitem__(self, element):
|
||||||
"""Notifies observers"""
|
return self.idlist[element]
|
||||||
for observer in self._selection_observers:
|
|
||||||
observer.selection_changed(dim_name, self.get_selection())
|
|
||||||
|
|
||||||
def add_dataset_observer(self, observer):
|
def __getslice__(self, start, end):
|
||||||
self._dataset_observers.append(observer)
|
return self.idlist[start:end]
|
||||||
observer.dataset_changed()
|
|
||||||
|
|
||||||
def notify_dataset_listeners(self):
|
def __contains__(self, element):
|
||||||
"""Notifies observers when new datasets are added"""
|
return self.idset.__contains__(element)
|
||||||
for observer in self._dataset_observers:
|
|
||||||
observer.dataset_changed()
|
|
||||||
|
|
||||||
def set_selection(self, dim_name, selection):
|
def __str__(self):
|
||||||
"""Sets a current selection and notify observers"""
|
return "%s: %s" % (self.name, str(self.idlist))
|
||||||
self.sel_obj[dim_name] = set(selection)
|
|
||||||
self.notify_selection_listeners(dim_name)
|
|
||||||
self._last_selection = selection
|
|
||||||
|
|
||||||
def get_selection(self):
|
def __len__(self):
|
||||||
"""Returns the current selection object"""
|
return len(self.idlist)
|
||||||
return self.sel_obj
|
|
||||||
|
|
||||||
def delete_data(self, it):
|
def __iter__(self):
|
||||||
"""Delete elements from the project."""
|
return iter(self.idlist)
|
||||||
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'):
|
def intersection(self, dim):
|
||||||
"""Adds a set of data and plots to the navigator.
|
return self.idset.intersection(dim.idset)
|
||||||
|
|
||||||
This method is usually called after a Function in a workflow
|
def as_tuple(self):
|
||||||
has finished and returns its output."""
|
return (self.name, self.idlist)
|
||||||
|
|
||||||
if len(parents) > 0:
|
|
||||||
parent_iter = self._dataset_iter_map[parents[0]]
|
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:
|
else:
|
||||||
parent_iter = None
|
self.file_created(fn)
|
||||||
|
for fn in self.files - newfiles:
|
||||||
# Add the function node to the tree
|
if os.path.isdir(os.path.join(self.path, fn)):
|
||||||
icon = laydi.icon_factory.get("folder_grey")
|
self.dir_deleted(fn)
|
||||||
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:
|
else:
|
||||||
icon = laydi.icon_factory.get("dataset")
|
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
|
||||||
|
|
||||||
self.add_dataset(d)
|
def file_created(self, fn):
|
||||||
self.data_tree_insert(it, d.get_name(), d, None, "black", icon)
|
print "file created: %s" % fn
|
||||||
|
pass
|
||||||
|
|
||||||
# Any kind of plot
|
def file_changed(self, fn):
|
||||||
elif isinstance(d, plots.Plot):
|
print "file changed: %s" % fn
|
||||||
icon = laydi.icon_factory.get("line_plot")
|
pass
|
||||||
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
|
def file_removed(self, fn):
|
||||||
elif isinstance(d, dataset.Selection):
|
print "file removed: %s" % fn
|
||||||
self.add_selection(d)
|
pass
|
||||||
|
|
||||||
def data_tree_insert(self, parent, text, data, bg, fg, icon, selected = 0):
|
def dir_created(self, fn):
|
||||||
"""Inserts data into the tree view.
|
print "directory created: %s" % fn
|
||||||
@param text: The title of the object.
|
pass
|
||||||
@param data: A dataset, plot or function object.
|
|
||||||
@param bg: Background color.
|
def dir_changed(self, fn):
|
||||||
@param fg: Foreground (font) color.
|
print "directory changed: %s" % fn
|
||||||
@param icon: Pixmap icon.
|
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.
|
||||||
"""
|
"""
|
||||||
tree = self.data_tree
|
filepath = os.path.join(self.path, fn)
|
||||||
it = tree.append(parent)
|
name, ext = os.path.splitext(fn)
|
||||||
tree[it] = [text, type(data), data, bg, fg, icon, selected]
|
if ext == ".ftsv":
|
||||||
self._dataset_iter_map[data] = it
|
ds = dataset.read_ftsv(filepath)
|
||||||
return it
|
self.datasets.append(ds)
|
||||||
|
self.dsfiles[fn] = ds
|
||||||
|
|
||||||
def add_dataset(self, dataset):
|
def file_changed(self, fn):
|
||||||
"""Appends a new Dataset to the project."""
|
"""Called from update() when files are changed.
|
||||||
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):
|
Delete old dataset and load the new one when dataset files
|
||||||
"""Adds a new selection to the project."""
|
have been changed.
|
||||||
self.selections.append(selection)
|
"""
|
||||||
self.notify_dataset_listeners()
|
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)
|
||||||
|
|
||||||
def object_at(self, path):
|
ds = dataset.read_ftsv(filepath)
|
||||||
"""Returns the object at a given path in the tree."""
|
self.datasets.append(ds)
|
||||||
it = self.get_iter(path)
|
|
||||||
obj = self[it][2]
|
|
||||||
if obj:
|
|
||||||
obj.show()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
#def set_current_data(self, obj):
|
self.dsfiles[fn] = ds
|
||||||
# self.current_data = obj
|
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()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user