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