Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/Core/Project.py
    1| # $Id: Project.py,v 1.10 2002/11/19 03:44:08 chalky Exp $
    2| #
    3| # This file is a part of Synopsis.
    4| # Copyright (C) 2000, 2001 Stefan Seefeld
    5| # Copyright (C) 2000, 2001 Stephen Davies
    6| #
    7| # Synopsis is free software; you can redistribute it and/or modify it
    8| # under the terms of the GNU General Public License as published by
    9| # the Free Software Foundation; either version 2 of the License, or
   10| # (at your option) any later version.
   11| #
   12| # This program is distributed in the hope that it will be useful,
   13| # but WITHOUT ANY WARRANTY; without even the implied warranty of
   14| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   15| # General Public License for more details.
   16| #
   17| # You should have received a copy of the GNU General Public License
   18| # along with this program; if not, write to the Free Software
   19| # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
   20| # 02111-1307, USA.
   21| #
   22| # $Log: Project.py,v $
   23| # Revision 1.10  2002/11/19 03:44:08  chalky
   24| # Changed SourcePath to SourceRule, included an Exclude rule
   25| #
   26| # Revision 1.9  2002/10/27 12:08:25  chalky
   27| # Add a project verbose flag, used by the Executor
   28| #
   29| # Revision 1.8  2002/09/28 06:17:01  chalky
   30| # Added is_project
   31| #
   32| # Revision 1.7  2002/09/20 10:36:40  chalky
   33| # Write the comment to the top of the project file
   34| #
   35| # Revision 1.6  2002/06/22 07:03:27  chalky
   36| # Updates to GUI - better editing of Actions, and can now execute some.
   37| #
   38| # Revision 1.5  2002/06/12 12:58:09  chalky
   39| # Added remove channel facility.
   40| #
   41| # Revision 1.4  2002/04/26 01:21:13  chalky
   42| # Bugs and cleanups
   43| #
   44| # Revision 1.3  2001/11/07 05:58:21  chalky
   45| # Reorganised UI, opening a .syn file now builds a simple project to view it
   46| #
   47| # Revision 1.2  2001/11/06 08:47:11  chalky
   48| # Silly bug, arrows, channels are saved
   49| #
   50| # Revision 1.1  2001/11/05 06:52:11  chalky
   51| # Major backside ui changes
   52| #
   53| 
   54| import sys, types
   55| from Synopsis.Core import Util
   56| from Action import *
   57| 
   58| def is_project_file(filename):
   59|     f = open(filename, "rt")
   60|     is_project = (f.readline() == '"""Synopsis Project File v1\n')
   61|     f.close()
   62|     return is_project
   63| 
   64| class Project:
   65|     """Encapsulates a single project. A project is a set of actions connected
   66|     by channels - each action may have one or more inputs and outputs which
   67|     are other actions. A project also has project-wide configuration, such as
   68|     the data directory."""
   69|     def __init__(self):
   70|         self.__data_dir = './'
   71|         self.__filename = None
   72|         self.__actions = ProjectActions(self)
   73|         self.__name = 'New Project'
   74|         self.__default_formatter = None
   75|         self.__verbose = 0
   76|     def filename(self):
   77|         "Returns the filename of this project, or None if not set yet"
   78|         return self.__filename
   79|     def set_filename(self, filename):
   80|         "Sets the filename of this file"
   81|         self.__filename = filename
   82|     def data_dir(self): return self.__data_dir
   83|     def set_data_dir(self, dir): self.__data_dir = dir
   84| 
   85|     def name(self): return self.__name
   86|     def set_name(self, name): self.__name = name
   87| 
   88|     def verbose(self): return self.__verbose
   89|     def set_verbose(self, verbose): self.__verbose = verbose
   90| 
   91|     def default_formatter(self): return self.__default_formatter
   92|     def set_default_formatter(self, action):
   93|         if isinstance(action, FormatAction) or action is None:
   94|             self.__default_formatter = action
   95| 
   96|     def actions(self):
   97|         "Returns a ProjectActions object"
   98|         return self.__actions
   99| 
  100|     def save(self):
  101|         if self.__filename is None:
  102|             raise Exception, 'Project has no filename'
  103|         file = open(self.__filename, 'w')
  104|         writer = ProjectWriter(file)
  105|         
  106|         writer.write_top(
  107|             '"""Synopsis Project File v1\n'+
  108|             'This file is auto-generated. If you must make changes by hand, '+
  109|             'please make a backup just in case."""\n')
  110|         # write project stuff like name, base dir, etc here
  111|         writer.write_item(self)
  112| 
  113|         writer.flush()
  114|         file.close()
  115| 
  116|     def load(self, filename):
  117|         ProjectReader(self).read(filename)
  118| 
  119| class ProjectActions:
  120|     """Manages the actions in a project.
  121| 
  122|     Clients can register for events related to the actions. The events
  123|     supported by the listener interface are:
  124| 
  125|     def action_added(self, action):
  126|     def action_moved(self, action):
  127|     def action_removed(self, action):
  128|     def channel_added(self, source, dest):
  129|     def channel_removed(self, source, dest):
  130|     """
  131| 
  132|     def __init__(self, project):
  133|         """Constructor"""
  134|         self.project = project
  135|         self.__actions = []
  136|         self.__action_names = {}
  137|         self.__listeners = []
  138| 
  139|     def actions(self):
  140|         """Returns the list of actions in this project Actions. The list
  141|         returned should be considered read-only"""
  142|         return self.__actions
  143| 
  144|     def get_action(self, name):
  145|         """Returns the Action object by name. This method uses a dictionary
  146|         lookup so should be preferred to iteration. Returns None if the name
  147|         is not found."""
  148|         if not self.__action_names.has_key(name): return None
  149|         return self.__action_names[name]
  150| 
  151|     def add_listener(self, l):
  152|         """Adds a listener to this Projec Actions' events. The listener may
  153|         implement any of the supported methods and will receive those events.
  154|         @see ProjectActions for more info."""
  155|         self.__listeners.append(l)
  156| 
  157|     def __fire(self, signal, *args):
  158|         """Fires the given event to all listeners"""
  159|         for l in self.__listeners:
  160|             if hasattr(l, signal):
  161|                apply(getattr(l, signal), args)
  162| 
  163|     def add_action(self, action):
  164|         """Adds the given action to this project"""
  165|         self.check_name(action)
  166|         self.__actions.append(action)
  167|         self.__action_names[action.name()] = action
  168|         self.__fire('action_added', action)
  169| 
  170|     def move_action(self, action, x, y):
  171|         """Moves the given action to the given screen coordinates"""
  172|         action.move_to(x, y)
  173|         self.__fire('action_moved', action)
  174| 
  175|     def move_action_by(self, action, dx, dy):
  176|         """Moves the given action by the given screen delta-coordinates"""
  177|         action.move_by(dx, dy)
  178|         self.__fire('action_moved', action)
  179| 
  180|     def remove_action(self, action):
  181|         """Removes the given action, and destroys all channels to/from it"""
  182|         if action not in self.__actions: return
  183|         # Remove all channels
  184|         for src in tuple(action.inputs()):
  185|             self.remove_channel(src, action)
  186|         for dst in tuple(action.outputs()):
  187|             self.remove_channel(action, dst)
  188|         # Remove action
  189|         self.__actions.remove(action)
  190|         del self.__action_names[action.name()]
  191|         # Check default_formatter
  192|         if action is self.project.default_formatter():
  193|             self.project.set_default_formatter(None)
  194|         self.__fire('action_removed', action)
  195| 
  196|     def rename_action(self, action, newname):
  197|         """Renames the given action to the given name"""
  198|         del self.__action_names[action.name()]
  199|         action.set_name(newname)
  200|         self.__action_names[newname] = action
  201| 
  202|     def add_channel(self, source, dest):
  203|         """Adds a "channel" between two actions. Causes the output of the
  204|         first to be connected to the input of the second. The event
  205|         'channel_added' is fired."""
  206|         if dest in source.outputs() and source in dest.inputs(): return
  207|         source.outputs().append(dest)
  208|         dest.inputs().append(source)
  209|         self.__fire('channel_added', source, dest)
  210| 
  211|     def remove_channel(self, source, dest):
  212|         """Removes a "channel" between two actions. If the channel doesn't
  213|         exist, it is silently ignored. The event 'channel_removed' is
  214|         fired."""
  215|         if dest not in source.outputs() and source not in dest.inputs():
  216|           return
  217|         source.outputs().remove(dest)
  218|         dest.inputs().remove(source)
  219|         self.__fire('channel_removed', source, dest)
  220| 
  221|     def action_changed(self, action):
  222|         """Indicates that the given action has changed, so that listeners can
  223|         update themselves"""
  224|         self.__fire('action_changed', action)
  225| 
  226|     def check_name(self, action):
  227|         "Checks the name, and renames if necessary"
  228|         if not self.__action_names.has_key(action.name()): return
  229|         copy = 1
  230|         name = action.name()
  231|         newname = '%s (%d)'%(name, copy)
  232|         while self.__action_names.has_key(newname):
  233|             copy = copy + 1
  234|             newname = '%s (%d)'%(name, copy)
  235|         action.set_name(newname)
  236| 
  237|     def is_valid_channel(self, source, dest):
  238|         """Returns true if the given source-dest pair would form a valid
  239|         channel.
  240|         Invalid pairs (eg: formatter->formatter) return false.
  241|         Cyclic channels are also disallowed."""
  242|         valid_pairs = {
  243|             SourceAction : (ParserAction,),
  244|             ParserAction : (CacherAction,LinkerAction,FormatAction),
  245|             LinkerAction : (CacherAction,LinkerAction,FormatAction),
  246|             CacherAction : (CacherAction,LinkerAction,FormatAction),
  247|             FormatAction : ()
  248|         }
  249|         if not isinstance(source, Action) or not isinstance(dest, Action):
  250|             return None
  251|         # First, check valid pair
  252|         if not valid_pairs.has_key(source.__class__):
  253|             return None
  254|         if dest.__class__ not in valid_pairs[source.__class__]:
  255|             return None
  256|         # Second, check that dest isn't already output of source
  257|         if dest in source.outputs() or dest is source:
  258|             return None
  259|         # Third, check cycles: descend input tree checking for dest
  260|         check = list(source.inputs())
  261|         while len(check):
  262|             action = check.pop(0)
  263|             if action is dest:
  264|                return None
  265|             check.extend(action.inputs())
  266|         # If got this far, combination is valid!
  267|         return 1
  268| 
  269| 
  270| 
  271| class ProjectWriter (Util.PyWriter):
  272|     def write_Project(self, project):
  273|         self.write("class Project:\n")
  274|         self.indent()
  275|         # write project stuff like name, base dir, etc here
  276|         self.write_attr('name', project.name())
  277|         self.write_attr('data_dir', project.data_dir())
  278|         self.write_attr('verbose', project.verbose())
  279|         self.write_item(project.actions())
  280|         dfm = project.default_formatter()
  281|         if dfm: self.write_attr('default_formatter', dfm.name())
  282|         self.outdent()
  283| 
  284|     def write_ProjectActions(self, actions):
  285|         self.write_attr('actions', self.long(actions.actions()))
  286|         channels = []
  287|         for source in actions.actions():
  288|             for dest in source.outputs():
  289|                channels.append( (source.name(), dest.name()) )
  290|         self.write_attr('channels', self.long(channels))
  291| 
  292|     def write_SimpleSourceRule(self, rule):
  293|         self.write_list((rule.type, rule.files))
  294| 
  295|     def write_GlobSourceRule(self, rule):
  296|         self.write_list((rule.type, rule.dirs, rule.glob, rule.recursive))
  297| 
  298|     def write_ExcludeSourceRule(self, rule):
  299|         self.write_list((rule.type, rule.glob))
  300| 
  301|     def write_SourceAction(self, action):
  302|         self.write_list(('SourceAction', 
  303|             action.x(), action.y(), action.name(),
  304|             self.long(action.rules()) ))
  305|     
  306|     def write_ParserAction(self, action):
  307|         self.write_list(('ParserAction',
  308|             action.x(), action.y(), action.name(),
  309|             self.flatten_struct(action.config()) ))
  310|     
  311|     def write_LinkerAction(self, action):
  312|         self.write_list(('LinkerAction',
  313|             action.x(), action.y(), action.name(),
  314|             self.flatten_struct(action.config()) ))
  315|     
  316|     def write_FormatAction(self, action):
  317|         self.write_list(('FormatAction',
  318|             action.x(), action.y(), action.name(),
  319|             self.flatten_struct(action.config()) ))
  320|     
  321|     def write_CacherAction(self, action):
  322|         self.write_list(('CacherAction',
  323|             action.x(), action.y(), action.name(),
  324|             action.dir, action.file))
  325| 
  326| class ProjectReader:
  327|     """A class that reads projects"""
  328|     def __init__(self, project):
  329|         self.project = project
  330|     def read(self, filename):
  331|         # Execute the file
  332|         exec_dict = {}
  333|         execfile(filename, exec_dict)
  334|         # Get 'Project' attribute
  335|         if not exec_dict.has_key('Project'):
  336|             raise Exception, 'Project load failed: Project attribute not found'
  337|         project = exec_dict['Project']
  338|         # Read attributes
  339|         self.project.set_filename(filename)
  340|         self.read_Project(project)
  341|     def read_Project(self, proj_obj):
  342|         project = self.project
  343|         # Read project stuff like name
  344|         if hasattr(proj_obj, 'name'): project.set_name(proj_obj.name)
  345|         if hasattr(proj_obj, 'verbose'): project.set_verbose(proj_obj.verbose)
  346|         if hasattr(proj_obj, 'data_dir'): project.set_data_dir(proj_obj.data_dir)
  347|         self.read_ProjectActions(proj_obj)
  348|         if hasattr(proj_obj, 'default_formatter'):
  349|             action = project.actions().get_action(proj_obj.default_formatter)
  350|             if action: project.set_default_formatter(action)
  351|     def read_ProjectActions(self, project_obj):
  352|         for action in project_obj.actions:
  353|             t = action[0]
  354|             if t in ('SourceAction', 'CacherAction', 'FormatAction',
  355|                    'ParserAction', 'LinkerAction'):
  356|                getattr(self, 'read_'+t)(action)
  357|          else:
  358|                raise Exception, 'Unknown action type: %s'%action[0]
  359|         if hasattr(project_obj, 'channels'):
  360|             actions = self.project.actions()
  361|             for source, dest in project_obj.channels:
  362|                src_obj = actions.get_action(source)
  363|                dst_obj = actions.get_action(dest)
  364|                actions.add_channel(src_obj, dst_obj)
  365| 
  366|     def read_SourceAction(self, action):
  367|         type, x, y, name, rules = action
  368|         self.action = SourceAction(x, y, name) 
  369|         self.project.actions().add_action(self.action)
  370|         for rule in rules: self.read_SourceRule(rule)
  371| 
  372|     def read_ParserAction(self, action):
  373|         type, x, y, name, config = action
  374|         self.action = ParserAction(x, y, name) 
  375|         self.project.actions().add_action(self.action)
  376|         self.action.set_config(config)
  377| 
  378|     def read_LinkerAction(self, action):
  379|         type, x, y, name, config = action
  380|         self.action = LinkerAction(x, y, name) 
  381|         self.project.actions().add_action(self.action)
  382|         self.action.set_config(config)
  383| 
  384|     def read_FormatAction(self, action):
  385|         type, x, y, name, config = action
  386|         self.action = FormatAction(x, y, name) 
  387|         self.project.actions().add_action(self.action)
  388|         self.action.set_config(config)
  389| 
  390|     def read_SourceRule(self, rule):
  391|         rules = self.action.rules()
  392|         if rule[0] == 'Simple':
  393|             if type(rule[1]) == types.ListType:
  394|                rules.append(SimpleSourceRule(rule[1]))
  395|          else:
  396|                # Backwards compat.
  397|                rules.append(SimpleSourceRule([rule[1]]))
  398|         elif rule[0] == 'Glob':
  399|             rules.append(GlobSourceRule(rule[1], rule[2], rule[3]))
  400|         elif rule[0] == 'Exclude':
  401|             rules.append(ExcludeSourceRule(rule[1]))
  402|         elif rule[0] == 'Dir':
  403|             # Backwards compat.
  404|             rules.append(GlobSourceRule([rule[1]], rule[2], 0))
  405|         elif rule[0] == 'Base':
  406|             # Backwards compat.
  407|             rules.append(GlobSourceRule([rule[1]], rule[2], 1))
  408|         else:
  409|             print "Warning: unknown source rule:",repr(rule)
  410| 
  411|     def read_CacherAction(self, action):
  412|         type, x, y, name, dir, file = action
  413|         self.action = CacherAction(x, y, name)
  414|         self.project.actions().add_action(self.action)
  415|         self.action.dir = dir
  416|         self.action.file = file
  417|         
  418|