c88c21c549b969e542ff1d3582eef4bd166979b9
[notmuch] / cnotmuch / database.py
1 import os
2 from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
4 from cnotmuch.thread import Threads
5 from cnotmuch.message import Messages
6 from cnotmuch.tag import Tags
7
8 class Database(object):
9     """Represents a notmuch database (wraps notmuch_database_t)
10
11     .. note:: Do remember that as soon as we tear down this object,
12            all underlying derived objects such as queries, threads,
13            messages, tags etc will be freed by the underlying library
14            as well. Accessing these objects will lead to segfaults and
15            other unexpected behavior. See above for more details.
16     """
17     _std_db_path = None
18     """Class attribute to cache user's default database"""
19
20     MODE = Enum(['READ_ONLY','READ_WRITE'])
21     """Constants: Mode in which to open the database"""
22
23     """notmuch_database_get_directory"""
24     _get_directory = nmlib.notmuch_database_get_directory
25     _get_directory.restype = c_void_p
26
27     """notmuch_database_get_path (notmuch_database_t *database)"""
28     _get_path = nmlib.notmuch_database_get_path
29     _get_path.restype = c_char_p
30
31     """notmuch_database_get_version"""
32     _get_version = nmlib.notmuch_database_get_version
33     _get_version.restype = c_uint
34
35     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
36     _open = nmlib.notmuch_database_open 
37     _open.restype = c_void_p
38
39     """notmuch_database_upgrade"""
40     _upgrade = nmlib.notmuch_database_upgrade
41     _upgrade.argtypes = [c_void_p, c_void_p, c_void_p]
42
43     """ notmuch_database_find_message """
44     _find_message = nmlib.notmuch_database_find_message
45     _find_message.restype = c_void_p
46
47     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
48     _get_all_tags = nmlib.notmuch_database_get_all_tags
49     _get_all_tags.restype = c_void_p
50
51     """ notmuch_database_create(const char *path):"""
52     _create = nmlib.notmuch_database_create
53     _create.restype = c_void_p
54
55     def __init__(self, path=None, create=False, mode= 0):
56         """If *path* is *None*, we will try to read a users notmuch 
57         configuration and use his configured database. The location of the 
58         configuration file can be specified through the environment variable
59         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
60
61         If *create* is `True`, the database will always be created in
62         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
63
64         :param path:   Directory to open/create the database in (see
65                        above for behavior if `None`)
66         :type path:    `str` or `None`
67         :param create: Pass `False` to open an existing, `True` to create a new
68                        database.  
69         :type create:  bool
70         :param mode:   Mode to open a database in. Is always 
71                        :attr:`MODE`.READ_WRITE when creating a new one.
72         :type mode:    :attr:`MODE`
73         :returns:      Nothing
74         :exception:    :exc:`NotmuchError` in case of failure.
75         """
76         self._db = None
77         if path is None:
78             # no path specified. use a user's default database
79             if Database._std_db_path is None:
80                 #the following line throws a NotmuchError if it fails
81                 Database._std_db_path = self._get_user_default_db()
82             path = Database._std_db_path
83
84         if create == False:
85             self.open(path, mode)
86         else:
87             self.create(path)
88
89     def _verify_initialized_db(self):
90         """Raises a NotmuchError in case self._db is still None"""
91         if self._db is None:
92             raise NotmuchError(STATUS.NOT_INITIALIZED)            
93
94     def create(self, path):
95         """Creates a new notmuch database
96
97         This function is used by __init__() and usually does not need
98         to be called directly. It wraps the underlying
99         *notmuch_database_create* function and creates a new notmuch
100         database at *path*. It will always return a database in
101         :attr:`MODE`.READ_WRITE mode as creating an empty database for
102         reading only does not make a great deal of sense.
103
104         :param path: A directory in which we should create the database.
105         :type path: str
106         :returns: Nothing
107         :exception: :exc:`NotmuchError` in case of any failure
108                     (after printing an error message on stderr).
109         """
110         if self._db is not None:
111             raise NotmuchError(
112             message="Cannot create db, this Database() already has an open one.")
113
114         res = Database._create(path, Database.MODE.READ_WRITE)
115
116         if res is None:
117             raise NotmuchError(
118                 message="Could not create the specified database")
119         self._db = res
120
121     def open(self, path, mode= 0): 
122         """Opens an existing database
123
124         This function is used by __init__() and usually does not need
125         to be called directly. It wraps the underlying
126         *notmuch_database_open* function.
127
128         :param status: Open the database in read-only or read-write mode
129         :type status:  :attr:`MODE` 
130         :returns: Nothing
131         :exception: Raises :exc:`NotmuchError` in case
132                     of any failure (after printing an error message on stderr).
133         """
134
135         res = Database._open(path, mode)
136
137         if res is None:
138             raise NotmuchError(
139                 message="Could not open the specified database")
140         self._db = res
141
142     def get_path(self):
143         """Returns the file path of an open database
144
145         Wraps notmuch_database_get_path"""
146         # Raise a NotmuchError if not initialized
147         self._verify_initialized_db()
148
149         return Database._get_path(self._db)
150
151     def get_version(self):
152         """Returns the database format version
153
154         :returns: The database version as positive integer
155         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
156                     the database was not intitialized.
157         """
158         # Raise a NotmuchError if not initialized
159         self._verify_initialized_db()
160
161         return Database._get_version (self._db)
162
163     def needs_upgrade(self):
164         """Does this database need to be upgraded before writing to it?
165
166         If this function returns True then no functions that modify the
167         database (:meth:`add_message`, :meth:`add_tag`,
168         :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade` 
169         is called successfully first.
170
171         :returns: `True` or `False`
172         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
173                     the database was not intitialized.
174         """
175         # Raise a NotmuchError if not initialized
176         self._verify_initialized_db()
177
178         return notmuch_database_needs_upgrade(self._db) 
179
180     def upgrade(self):
181         """Upgrades the current database
182
183         After opening a database in read-write mode, the client should
184         check if an upgrade is needed (notmuch_database_needs_upgrade) and
185         if so, upgrade with this function before making any modifications.
186
187         NOT IMPLEMENTED: The optional progress_notify callback can be
188         used by the caller to provide progress indication to the
189         user. If non-NULL it will be called periodically with
190         'progress' as a floating-point value in the range of [0.0
191         .. 1.0] indicating the progress made so far in the upgrade
192         process.
193         """
194         # Raise a NotmuchError if not initialized
195         self._verify_initialized_db()
196
197         status = Database._upgrade (self._db, None, None)
198         #TODO: catch exceptions, document return values and etc
199         return status
200
201     def get_directory(self, path):
202         """Returns a :class:`Directory` of path, 
203         (creating it if it does not exist(?))
204
205         .. warning:: This call needs a writeable database in 
206            Database.MODE.READ_WRITE mode. The underlying library will exit the
207            program if this method is used on a read-only database!
208
209         :param path: A str containing the path relative to the path of database
210         (see :meth:`get_path`), or else should be an absolute path
211         with initial components that match the path of 'database'.
212
213         :returns: :class:`Directory` or raises an exception.
214         :exception: :exc:`NotmuchError`
215
216                   STATUS.NOT_INITIALIZED 
217                     If the database was not intitialized.
218
219                   STATUS.FILE_ERROR
220                     If path is not relative database or absolute with initial 
221                     components same as database.
222
223         """
224         # Raise a NotmuchError if not initialized
225         self._verify_initialized_db()
226
227         # sanity checking if path is valid, and make path absolute
228         if path[0] == os.sep:
229             # we got an absolute path
230             if not path.startswith(self.get_path()):
231                 # but its initial components are not equal to the db path
232                 raise NotmuchError(STATUS.FILE_ERROR, 
233                                    message="Database().get_directory() called with a wrong absolute path.")
234             abs_dirpath = path
235         else:
236             #we got a relative path, make it absolute
237             abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path))
238
239         dir_p = Database._get_directory(self._db, path);
240
241         # return the Directory, init it with the absolute path
242         return Directory(abs_dirpath, dir_p, self)
243
244     def add_message(self, filename):
245         """Adds a new message to the database
246
247         `filename` should be a path relative to the path of the open
248         database (see :meth:`get_path`), or else should be an absolute
249         filename with initial components that match the path of the
250         database.
251
252         The file should be a single mail message (not a multi-message mbox)
253         that is expected to remain at its current location, since the
254         notmuch database will reference the filename, and will not copy the
255         entire contents of the file.
256
257         :returns: On success, we return 
258
259            1) a :class:`Message` object that can be used for things
260               such as adding tags to the just-added message.
261            2) one of the following STATUS values:
262
263               STATUS.SUCCESS
264                   Message successfully added to database.
265               STATUS.DUPLICATE_MESSAGE_ID
266                   Message has the same message ID as another message already
267                   in the database. The new filename was successfully added
268                   to the message in the database.
269
270         :rtype:   2-tuple(:class:`Message`, STATUS)
271
272         :exception: Raises a :exc:`NotmuchError` with the following meaning.
273               If such an exception occurs, nothing was added to the database.
274
275               STATUS.FILE_ERROR
276                       An error occurred trying to open the file, (such as 
277                       permission denied, or file not found, etc.).
278               STATUS.FILE_NOT_EMAIL
279                       The contents of filename don't look like an email message.
280               STATUS.READ_ONLY_DATABASE
281                       Database was opened in read-only mode so no message can
282                       be added.
283               STATUS.NOT_INITIALIZED
284                       The database has not been initialized.
285         """
286         # Raise a NotmuchError if not initialized
287         self._verify_initialized_db()
288
289         msg_p = c_void_p()
290         status = nmlib.notmuch_database_add_message(self._db,
291                                                   filename,
292                                                   byref(msg_p))
293  
294         if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
295             raise NotmuchError(status)
296
297         #construct Message() and return
298         msg = Message(msg_p, self)
299         return (msg, status)
300
301     def remove_message(self, filename):
302         """Removes a message from the given notmuch database
303
304         Note that only this particular filename association is removed from
305         the database. If the same message (as determined by the message ID)
306         is still available via other filenames, then the message will
307         persist in the database for those filenames. When the last filename
308         is removed for a particular message, the database content for that
309         message will be entirely removed.
310
311         :returns: A STATUS.* value with the following meaning:
312
313              STATUS.SUCCESS
314                The last filename was removed and the message was removed 
315                from the database.
316              STATUS.DUPLICATE_MESSAGE_ID
317                This filename was removed but the message persists in the 
318                database with at least one other filename.
319
320         :exception: Raises a :exc:`NotmuchError` with the following meaning.
321              If such an exception occurs, nothing was removed from the database.
322
323              STATUS.READ_ONLY_DATABASE
324                Database was opened in read-only mode so no message can be 
325                removed.
326              STATUS.NOT_INITIALIZED
327                The database has not been initialized.
328         """
329         # Raise a NotmuchError if not initialized
330         self._verify_initialized_db()
331
332         status = nmlib.notmuch_database_remove_message(self._db,
333                                                        filename)
334
335     def find_message(self, msgid):
336         """Returns a :class:`Message` as identified by its message ID
337
338         Wraps the underlying *notmuch_database_find_message* function.
339
340         :param msgid: The message ID
341         :type msgid: string
342         :returns: :class:`Message` or `None` if no message is found or if an
343                   out-of-memory situation occurs.
344         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
345                   the database was not intitialized.
346         """
347         # Raise a NotmuchError if not initialized
348         self._verify_initialized_db()
349
350         msg_p = Database._find_message(self._db, msgid)
351         if msg_p is None:
352             return None
353         return Message(msg_p, self)
354
355     def get_all_tags(self):
356         """Returns :class:`Tags` with a list of all tags found in the database
357
358         :returns: :class:`Tags`
359         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
360         """
361         # Raise a NotmuchError if not initialized
362         self._verify_initialized_db()
363
364         tags_p = Database._get_all_tags (self._db)
365         if tags_p == None:
366             raise NotmuchError(STATUS.NULL_POINTER)
367         return Tags(tags_p, self)
368
369     def create_query(self, querystring):
370         """Returns a :class:`Query` derived from this database
371
372         This is a shorthand method for doing::
373
374           # short version
375           # Automatically frees the Database() when 'q' is deleted
376
377           q  = Database(dbpath).create_query('from:"Biene Maja"')
378
379           # long version, which is functionally equivalent but will keep the
380           # Database in the 'db' variable around after we delete 'q':
381
382           db = Database(dbpath)
383           q  = Query(db,'from:"Biene Maja"')
384
385         This function is a python extension and not in the underlying C API.
386         """
387         # Raise a NotmuchError if not initialized
388         self._verify_initialized_db()
389
390         return Query(self, querystring)
391
392     def __repr__(self):
393         return "'Notmuch DB " + self.get_path() + "'"
394
395     def __del__(self):
396         """Close and free the notmuch database if needed"""
397         if self._db is not None:
398             nmlib.notmuch_database_close(self._db)
399
400     def _get_user_default_db(self):
401         """ Reads a user's notmuch config and returns his db location
402
403         Throws a NotmuchError if it cannot find it"""
404         from ConfigParser import SafeConfigParser
405         config = SafeConfigParser()
406         conf_f = os.getenv('NOTMUCH_CONFIG',
407                            os.path.expanduser('~/.notmuch-config'))
408         config.read(conf_f)
409         if not config.has_option('database','path'):
410             raise NotmuchError(message=
411                                "No DB path specified and no user default found")
412         return config.get('database','path')
413
414     @property
415     def db_p(self):
416         """Property returning a pointer to `notmuch_database_t` or `None`
417
418         This should normally not be needed by a user (and is not yet
419         guaranteed to remain stable in future versions).
420         """
421         return self._db
422
423 #------------------------------------------------------------------------------
424 class Query(object):
425     """Represents a search query on an opened :class:`Database`.
426
427     A query selects and filters a subset of messages from the notmuch
428     database we derive from.
429
430     Query() provides an instance attribute :attr:`sort`, which
431     contains the sort order (if specified via :meth:`set_sort`) or
432     `None`.
433
434     Technically, it wraps the underlying *notmuch_query_t* struct.
435
436     .. note:: Do remember that as soon as we tear down this object,
437            all underlying derived objects such as threads,
438            messages, tags etc will be freed by the underlying library
439            as well. Accessing these objects will lead to segfaults and
440            other unexpected behavior. See above for more details.
441     """
442     # constants
443     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
444     """Constants: Sort order in which to return results"""
445
446     """notmuch_query_create"""
447     _create = nmlib.notmuch_query_create
448     _create.restype = c_void_p
449
450     """notmuch_query_search_threads"""
451     _search_threads = nmlib.notmuch_query_search_threads
452     _search_threads.restype = c_void_p
453
454     """notmuch_query_search_messages"""
455     _search_messages = nmlib.notmuch_query_search_messages
456     _search_messages.restype = c_void_p
457
458
459     """notmuch_query_count_messages"""
460     _count_messages = nmlib.notmuch_query_count_messages
461     _count_messages.restype = c_uint
462
463     def __init__(self, db, querystr):
464         """
465         :param db: An open database which we derive the Query from.
466         :type db: :class:`Database`
467         :param querystr: The query string for the message.
468         :type querystr: str
469         """
470         self._db = None
471         self._query = None
472         self.sort = None
473         self.create(db, querystr)
474
475     def create(self, db, querystr):
476         """Creates a new query derived from a Database
477
478         This function is utilized by __init__() and usually does not need to 
479         be called directly.
480
481         :param db: Database to create the query from.
482         :type db: :class:`Database`
483         :param querystr: The query string
484         :type querystr: str
485         :returns: Nothing
486         :exception: :exc:`NotmuchError`
487
488                       * STATUS.NOT_INITIALIZED if db is not inited
489                       * STATUS.NULL_POINTER if the query creation failed 
490                         (too little memory)
491         """
492         if db.db_p is None:
493             raise NotmuchError(STATUS.NOT_INITIALIZED)            
494         # create reference to parent db to keep it alive
495         self._db = db
496         
497         # create query, return None if too little mem available
498         query_p = Query._create(db.db_p, querystr)
499         if query_p is None:
500             NotmuchError(STATUS.NULL_POINTER)
501         self._query = query_p
502
503     def set_sort(self, sort):
504         """Set the sort order future results will be delivered in
505
506         Wraps the underlying *notmuch_query_set_sort* function.
507
508         :param sort: Sort order (see :attr:`Query.SORT`)
509         :returns: Nothing
510         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
511                     been initialized.
512         """
513         if self._query is None:
514             raise NotmuchError(STATUS.NOT_INITIALIZED)
515
516         self.sort = sort
517         nmlib.notmuch_query_set_sort(self._query, sort)
518
519     def search_threads(self):
520         """Execute a query for threads
521
522         Execute a query for threads, returning a :class:`Threads` iterator.
523         The returned threads are owned by the query and as such, will only be 
524         valid until the Query is deleted.
525
526         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
527         match the query. The method :meth:`Message.get_flag` allows us
528         to get the value of this flag.
529
530         Technically, it wraps the underlying
531         *notmuch_query_search_threads* function.
532
533         :returns: :class:`Threads`
534         :exception: :exc:`NotmuchError`
535
536                       * STATUS.NOT_INITIALIZED if query is not inited
537                       * STATUS.NULL_POINTER if search_threads failed 
538         """
539         if self._query is None:
540             raise NotmuchError(STATUS.NOT_INITIALIZED)            
541
542         threads_p = Query._search_threads(self._query)
543
544         if threads_p is None:
545             NotmuchError(STATUS.NULL_POINTER)
546
547         return Threads(threads_p,self)
548
549     def search_messages(self):
550         """Filter messages according to the query and return
551         :class:`Messages` in the defined sort order
552
553         Technically, it wraps the underlying
554         *notmuch_query_search_messages* function.
555
556         :returns: :class:`Messages`
557         :exception: :exc:`NotmuchError`
558
559                       * STATUS.NOT_INITIALIZED if query is not inited
560                       * STATUS.NULL_POINTER if search_messages failed 
561         """
562         if self._query is None:
563             raise NotmuchError(STATUS.NOT_INITIALIZED)            
564
565         msgs_p = Query._search_messages(self._query)
566
567         if msgs_p is None:
568             NotmuchError(STATUS.NULL_POINTER)
569
570         return Messages(msgs_p,self)
571
572     def count_messages(self):
573         """Estimate the number of messages matching the query
574
575         This function performs a search and returns Xapian's best
576         guess as to the number of matching messages. It is much faster
577         than performing :meth:`search_messages` and counting the
578         result with `len()` (although it always returned the same
579         result in my tests). Technically, it wraps the underlying
580         *notmuch_query_count_messages* function.
581
582         :returns: :class:`Messages`
583         :exception: :exc:`NotmuchError`
584
585                       * STATUS.NOT_INITIALIZED if query is not inited
586         """
587         if self._query is None:
588             raise NotmuchError(STATUS.NOT_INITIALIZED)            
589
590         return Query._count_messages(self._query)
591
592     def __del__(self):
593         """Close and free the Query"""
594         if self._query is not None:
595             nmlib.notmuch_query_destroy (self._query)
596
597
598 #------------------------------------------------------------------------------
599 class Directory(object):
600     """Represents a directory entry in the notmuch directory
601
602     Modifying attributes of this object will modify the
603     database, not the real directory attributes.
604
605     The Directory object is usually derived from another object
606     e.g. via :meth:`Database.get_directory`, and will automatically be
607     become invalid whenever that parent is deleted. You should
608     therefore initialized this object handing it a reference to the
609     parent, preventing the parent from automatically being garbage
610     collected.
611     """
612
613     """notmuch_directory_get_mtime"""
614     _get_mtime = nmlib.notmuch_directory_get_mtime
615     _get_mtime.restype = c_long
616
617     """notmuch_directory_set_mtime"""
618     _set_mtime = nmlib.notmuch_directory_set_mtime
619     _set_mtime.argtypes = [c_char_p, c_long]
620
621     """notmuch_directory_get_child_files"""
622     _get_child_files = nmlib.notmuch_directory_get_child_files
623     _get_child_files.restype = c_void_p
624
625     """notmuch_directory_get_child_directories"""
626     _get_child_directories = nmlib.notmuch_directory_get_child_directories
627     _get_child_directories.restype = c_void_p
628
629     def _verify_dir_initialized(self):
630         """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
631         if self._dir_p is None:
632             raise NotmuchError(STATUS.NOT_INITIALIZED)            
633
634     def __init__(self, path, dir_p, parent):
635         """
636         :param path:   The absolute path of the directory object.
637         :param dir_p:  The pointer to an internal notmuch_directory_t object.
638         :param parent: The object this Directory is derived from
639                        (usually a Database()). We do not directly use
640                        this, but store a reference to it as long as
641                        this Directory object lives. This keeps the
642                        parent object alive.
643         """
644         self._path = path
645         self._dir_p = dir_p
646         self._parent = parent
647
648
649     def set_mtime (self, mtime):
650         """Sets the mtime value of this directory in the database
651
652         The intention is for the caller to use the mtime to allow efficient
653         identification of new messages to be added to the database. The
654         recommended usage is as follows:
655
656         * Read the mtime of a directory from the filesystem
657  
658         * Call :meth:`Database.add_message` for all mail files in
659           the directory
660
661         * Call notmuch_directory_set_mtime with the mtime read from the 
662           filesystem.  Then, when wanting to check for updates to the
663           directory in the future, the client can call :meth:`get_mtime`
664           and know that it only needs to add files if the mtime of the 
665           directory and files are newer than the stored timestamp.
666
667           .. note:: :meth:`get_mtime` function does not allow the caller 
668                  to distinguish a timestamp of 0 from a non-existent
669                  timestamp. So don't store a timestamp of 0 unless you are
670                  comfortable with that.  
671
672           :param mtime: A (time_t) timestamp 
673           :returns: Nothing on success, raising an exception on failure.
674           :exception: :exc:`NotmuchError`:
675
676                         STATUS.XAPIAN_EXCEPTION
677                           A Xapian exception occurred, mtime not stored.
678                         STATUS.READ_ONLY_DATABASE
679                           Database was opened in read-only mode so directory 
680                           mtime cannot be modified.
681                         STATUS.NOT_INITIALIZED
682                           The directory has not been initialized
683         """
684         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
685         self._verify_dir_initialized()
686
687         #TODO: make sure, we convert the mtime parameter to a 'c_long'
688         status = Directory._set_mtime(self._dir_p, mtime)
689
690         #return on success
691         if status == STATUS.SUCCESS:
692             return
693         #fail with Exception otherwise
694         raise NotmuchError(status)
695
696     def get_mtime (self):
697         """Gets the mtime value of this directory in the database
698
699         Retrieves a previously stored mtime for this directory.
700
701         :param mtime: A (time_t) timestamp 
702         :returns: Nothing on success, raising an exception on failure.
703         :exception: :exc:`NotmuchError`:
704
705                         STATUS.NOT_INITIALIZED
706                           The directory has not been initialized
707         """
708         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
709         self._verify_dir_initialized()
710
711         return Directory._get_mtime (self._dir_p)
712
713
714     # Make mtime attribute a property of Directory()
715     mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
716                      and setting of the Directory *mtime*""")
717
718     def get_child_files(self):
719         """Gets a Filenames iterator listing all the filenames of
720         messages in the database within the given directory.
721  
722         The returned filenames will be the basename-entries only (not
723         complete paths.
724         """
725         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
726         self._verify_dir_initialized()
727
728         files_p = Directory._get_child_files(self._dir_p)
729         return Filenames(files_p, self)
730
731     def get_child_directories(self):
732         """Gets a Filenams iterator listing all the filenames of
733         sub-directories in the database within the given directory
734         
735         The returned filenames will be the basename-entries only (not
736         complete paths.
737         """
738         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
739         self._verify_dir_initialized()
740
741         files_p = Directory._get_child_directories(self._dir_p)
742         return Filenames(files_p, self)
743
744     @property
745     def path(self):
746         """Returns the absolute path of this Directory"""
747         return self._path
748
749     def __repr__(self):
750         """Object representation"""
751         return "<cnotmuch Directory object '%s'>" % self._path
752
753     def __del__(self):
754         """Close and free the Directory"""
755         if self._dir_p is not None:
756             nmlib.notmuch_directory_destroy(self._dir_p)
757
758 #------------------------------------------------------------------------------
759 class Filenames(object):
760     """An iterator over File- or Directory names that are stored in the notmuch database
761     """
762
763     #notmuch_filenames_get
764     _get = nmlib.notmuch_filenames_get
765     _get.restype = c_char_p
766
767     def __init__(self, files_p, parent):
768         """
769         :param files_p: The pointer to an internal notmuch_filenames_t object.
770         :param parent: The object this Directory is derived from
771                        (usually a Directory()). We do not directly use
772                        this, but store a reference to it as long as
773                        this Directory object lives. This keeps the
774                        parent object alive.
775         """
776         self._files_p = files_p
777         self._parent = parent
778
779     def __iter__(self):
780         """ Make Filenames an iterator """
781         return self
782
783     def next(self):
784         if self._files_p is None:
785             raise NotmuchError(STATUS.NOT_INITIALIZED)
786
787         if not nmlib.notmuch_filenames_valid(self._files_p):
788             self._files_p = None
789             raise StopIteration
790
791         file = Filenames._get (self._files_p)
792         nmlib.notmuch_filenames_move_to_next(self._files_p)
793         return file
794
795     def __len__(self):
796         """len(:class:`Filenames`) returns the number of contained files
797
798         .. note:: As this iterates over the files, we will not be able to 
799                iterate over them again! So this will fail::
800
801                  #THIS FAILS
802                  files = Database().get_directory('').get_child_files()
803                  if len(files) > 0:              #this 'exhausts' msgs
804                      # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
805                      for file in files: print file
806         """
807         if self._files_p is None:
808             raise NotmuchError(STATUS.NOT_INITIALIZED)
809
810         i=0
811         while nmlib.notmuch_filenames_valid(self._files_p):
812             nmlib.notmuch_filenames_move_to_next(self._files_p)
813             i += 1
814         self._files_p = None
815         return i
816
817     def __del__(self):
818         """Close and free Filenames"""
819         if self._files_p is not None:
820             nmlib.notmuch_filenames_destroy(self._files_p)