Code

Do not depend on a CPython implementation detail anymore to make Roundup
[roundup.git] / roundup / instance.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
19 """Top-level tracker interface.
21 Open a tracker with:
23     >>> from roundup import instance
24     >>> db = instance.open('path to tracker home')
26 The "db" handle you get back is the tracker's hyperdb which has the interface
27 described in `roundup.hyperdb.Database`.
28 """
29 __docformat__ = 'restructuredtext'
31 import os
32 import sys
33 import __builtin__
34 from roundup import configuration, mailgw
35 from roundup import hyperdb, backends, actions
36 from roundup.cgi import client, templating
37 from roundup.cgi import actions as cgi_actions
39 class Vars:
40     def __init__(self, vars):
41         self.__dict__.update(vars)
43 class Tracker:
44     def __init__(self, tracker_home, optimize=0):
45         """New-style tracker instance constructor
47         Parameters:
48             tracker_home:
49                 tracker home directory
50             optimize:
51                 if set, precompile html templates
53         """
54         self.tracker_home = tracker_home
55         self.optimize = optimize
56         # if set, call schema_hook after executing schema.py will get
57         # same variables (in particular db) as schema.py main purpose is
58         # for regression tests
59         self.schema_hook = None
60         self.config = configuration.CoreConfig(tracker_home)
61         self.actions = {}
62         self.cgi_actions = {}
63         self.templating_utils = {}
64         self.load_interfaces()
65         self.templates = templating.Templates(self.config["TEMPLATES"])
66         self.backend = backends.get_backend(self.get_backend_name())
67         if self.optimize:
68             libdir = os.path.join(self.tracker_home, 'lib')
69             if os.path.isdir(libdir):
70                 sys.path.insert(1, libdir)
71             self.templates.precompileTemplates()
72             # initialize tracker extensions
73             for extension in self.get_extensions('extensions'):
74                 extension(self)
75             # load database schema
76             schemafilename = os.path.join(self.tracker_home, 'schema.py')
77             # Note: can't use built-in open()
78             #   because of the global function with the same name
79             schemafile = file(schemafilename, 'rt')
80             self.schema = compile(schemafile.read(), schemafilename, 'exec')
81             schemafile.close()
82             # load database detectors
83             self.detectors = self.get_extensions('detectors')
84             # db_open is set to True after first open()
85             self.db_open = 0
86             if libdir in sys.path:
87                 sys.path.remove(libdir)
89     def get_backend_name(self):
90         o = __builtin__.open
91         f = o(os.path.join(self.config.DATABASE, 'backend_name'))
92         name = f.readline().strip()
93         f.close()
94         return name
96     def open(self, name=None):
97         # load the database schema
98         # we cannot skip this part even if self.optimize is set
99         # because the schema has security settings that must be
100         # applied to each database instance
101         backend = self.backend
102         vars = {
103             'Class': backend.Class,
104             'FileClass': backend.FileClass,
105             'IssueClass': backend.IssueClass,
106             'String': hyperdb.String,
107             'Password': hyperdb.Password,
108             'Date': hyperdb.Date,
109             'Link': hyperdb.Link,
110             'Multilink': hyperdb.Multilink,
111             'Interval': hyperdb.Interval,
112             'Boolean': hyperdb.Boolean,
113             'Number': hyperdb.Number,
114             'db': backend.Database(self.config, name)
115         }
117         libdir = os.path.join(self.tracker_home, 'lib')
118         if os.path.isdir(libdir):
119             sys.path.insert(1, libdir)
120         if self.optimize:
121             # execute preloaded schema object
122             exec(self.schema, vars)
123             if callable (self.schema_hook):
124                 self.schema_hook(**vars)
125             # use preloaded detectors
126             detectors = self.detectors
127         else:
128             # execute the schema file
129             self._load_python('schema.py', vars)
130             if callable (self.schema_hook):
131                 self.schema_hook(**vars)
132             # reload extensions and detectors
133             for extension in self.get_extensions('extensions'):
134                 extension(self)
135             detectors = self.get_extensions('detectors')
136         if libdir in sys.path:
137             sys.path.remove(libdir)
138         db = vars['db']
139         # apply the detectors
140         for detector in detectors:
141             detector(db)
142         # if we are running in debug mode
143         # or this is the first time the database is opened,
144         # do database upgrade checks
145         if not (self.optimize and self.db_open):
146             # As a consistency check, ensure that every link property is
147             # pointing at a defined class.  Otherwise, the schema is
148             # internally inconsistent.  This is an important safety
149             # measure as it protects against an accidental schema change
150             # dropping a table while there are still links to the table;
151             # once the table has been dropped, there is no way to get it
152             # back, so it is important to drop it only if we are as sure
153             # as possible that it is no longer needed.
154             classes = db.getclasses()
155             for classname in classes:
156                 cl = db.getclass(classname)
157                 for propname, prop in cl.getprops().iteritems():
158                     if not isinstance(prop, (hyperdb.Link,
159                                              hyperdb.Multilink)):
160                         continue
161                     linkto = prop.classname
162                     if linkto not in classes:
163                         raise ValueError, \
164                             ("property %s.%s links to non-existent class %s"
165                              % (classname, propname, linkto))
167             db.post_init()
168             self.db_open = 1
169         return db
171     def load_interfaces(self):
172         """load interfaces.py (if any), initialize Client and MailGW attrs"""
173         vars = {}
174         if os.path.isfile(os.path.join(self.tracker_home, 'interfaces.py')):
175             self._load_python('interfaces.py', vars)
176         self.Client = vars.get('Client', client.Client)
177         self.MailGW = vars.get('MailGW', mailgw.MailGW)
179     def get_extensions(self, dirname):
180         """Load python extensions
182         Parameters:
183             dirname:
184                 extension directory name relative to tracker home
186         Return value:
187             list of init() functions for each extension
189         """
190         extensions = []
191         dirpath = os.path.join(self.tracker_home, dirname)
192         if os.path.isdir(dirpath):
193             sys.path.insert(1, dirpath)
194             for name in os.listdir(dirpath):
195                 if not name.endswith('.py'):
196                     continue
197                 vars = {}
198                 self._load_python(os.path.join(dirname, name), vars)
199                 extensions.append(vars['init'])
200             sys.path.remove(dirpath)
201         return extensions
203     def init(self, adminpw):
204         db = self.open('admin')
205         self._load_python('initial_data.py', {'db': db, 'adminpw': adminpw,
206             'admin_email': self.config['ADMIN_EMAIL']})
207         db.commit()
208         db.close()
210     def exists(self):
211         return self.backend.db_exists(self.config)
213     def nuke(self):
214         self.backend.db_nuke(self.config)
216     def _load_python(self, file, vars):
217         file = os.path.join(self.tracker_home, file)
218         execfile(file, vars)
219         return vars
221     def registerAction(self, name, action):
223         # The logic here is this:
224         # * if `action` derives from actions.Action,
225         #   it is executable as a generic action.
226         # * if, moreover, it also derives from cgi.actions.Bridge,
227         #   it may in addition be called via CGI
228         # * in all other cases we register it as a CGI action, without
229         #   any check (for backward compatibility).
230         if issubclass(action, actions.Action):
231             self.actions[name] = action
232             if issubclass(action, cgi_actions.Bridge):
233                 self.cgi_actions[name] = action
234         else:
235             self.cgi_actions[name] = action
237     def registerUtil(self, name, function):
238         self.templating_utils[name] = function
240 class TrackerError(Exception):
241     pass
244 class OldStyleTrackers:
245     def __init__(self):
246         self.number = 0
247         self.trackers = {}
249     def open(self, tracker_home, optimize=0):
250         """Open the tracker.
252         Parameters:
253             tracker_home:
254                 tracker home directory
255             optimize:
256                 if set, precompile html templates
258         Raise ValueError if the tracker home doesn't exist.
260         """
261         import imp
262         # sanity check existence of tracker home
263         if not os.path.exists(tracker_home):
264             raise ValueError, 'no such directory: "%s"'%tracker_home
266         # sanity check tracker home contents
267         for reqd in 'config dbinit select_db interfaces'.split():
268             if not os.path.exists(os.path.join(tracker_home, '%s.py'%reqd)):
269                 raise TrackerError, 'File "%s.py" missing from tracker '\
270                     'home "%s"'%(reqd, tracker_home)
272         if self.trackers.has_key(tracker_home):
273             return imp.load_package(self.trackers[tracker_home],
274                 tracker_home)
275         # register all available backend modules
276         backends.list_backends()
277         self.number = self.number + 1
278         modname = '_roundup_tracker_%s'%self.number
279         self.trackers[tracker_home] = modname
281         # load the tracker
282         tracker = imp.load_package(modname, tracker_home)
284         # ensure the tracker has all the required bits
285         for required in 'open init Client MailGW'.split():
286             if not hasattr(tracker, required):
287                 raise TrackerError, \
288                     'Required tracker attribute "%s" missing'%required
290         # load and apply the config
291         tracker.config = configuration.CoreConfig(tracker_home)
292         tracker.dbinit.config = tracker.config
294         tracker.optimize = optimize
295         tracker.templates = templating.Templates(tracker.config["TEMPLATES"])
296         if optimize:
297             tracker.templates.precompileTemplates()
299         return tracker
301 OldStyleTrackers = OldStyleTrackers()
302 def open(tracker_home, optimize=0):
303     if os.path.exists(os.path.join(tracker_home, 'dbinit.py')):
304         # user should upgrade...
305         return OldStyleTrackers.open(tracker_home, optimize=optimize)
307     return Tracker(tracker_home, optimize=optimize)
309 # vim: set filetype=python sts=4 sw=4 et si :