mirror of https://github.com/python/cpython
gh-90890: New methods to access mailbox.Maildir message info and flags (#103905)
New methods to access mailbox.Maildir message info and flags: get_info, set_info, get_flags, set_flags, add_flag, remove_flag. These methods speed up accessing a message's info and/or flags and are useful when it is not necessary to access the message's contents, as when iterating over a Maildir to find messages with specific flags. --------- * Add more str type checking * modernize to f-strings instead of % Co-authored-by: Gregory P. Smith <greg@krypto.org>
This commit is contained in:
parent
fa84e5fe0a
commit
38035fed9b
|
@ -424,6 +424,108 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF.
|
||||||
remove the underlying message while the returned file remains open.
|
remove the underlying message while the returned file remains open.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: get_flags(key)
|
||||||
|
|
||||||
|
Return as a string the flags that are set on the message
|
||||||
|
corresponding to *key*.
|
||||||
|
This is the same as ``get_message(key).get_flags()`` but much
|
||||||
|
faster, because it does not open the message file.
|
||||||
|
Use this method when iterating over the keys to determine which
|
||||||
|
messages are interesting to get.
|
||||||
|
|
||||||
|
If you do have a :class:`MaildirMessage` object, use
|
||||||
|
its :meth:`~MaildirMessage.get_flags` method instead, because
|
||||||
|
changes made by the message's :meth:`~MaildirMessage.set_flags`,
|
||||||
|
:meth:`~MaildirMessage.add_flag` and :meth:`~MaildirMessage.remove_flag`
|
||||||
|
methods are not reflected here until the mailbox's
|
||||||
|
:meth:`__setitem__` method is called.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: set_flags(key, flags)
|
||||||
|
|
||||||
|
On the message corresponding to *key*, set the flags specified
|
||||||
|
by *flags* and unset all others.
|
||||||
|
Calling ``some_mailbox.set_flags(key, flags)`` is similar to ::
|
||||||
|
|
||||||
|
one_message = some_mailbox.get_message(key)
|
||||||
|
one_message.set_flags(flags)
|
||||||
|
some_mailbox[key] = one_message
|
||||||
|
|
||||||
|
but faster, because it does not open the message file.
|
||||||
|
|
||||||
|
If you do have a :class:`MaildirMessage` object, use
|
||||||
|
its :meth:`~MaildirMessage.set_flags` method instead, because
|
||||||
|
changes made with this mailbox method will not be visible to the
|
||||||
|
message object's method, :meth:`~MaildirMessage.get_flags`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: add_flag(key, flag)
|
||||||
|
|
||||||
|
On the message corresponding to *key*, set the flags specified
|
||||||
|
by *flag* without changing other flags. To add more than one
|
||||||
|
flag at a time, *flag* may be a string of more than one character.
|
||||||
|
|
||||||
|
Considerations for using this method versus the message object's
|
||||||
|
:meth:`~MaildirMessage.add_flag` method are similar to
|
||||||
|
those for :meth:`set_flags`; see the discussion there.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: remove_flag(key, flag)
|
||||||
|
|
||||||
|
On the message corresponding to *key*, unset the flags specified
|
||||||
|
by *flag* without changing other flags. To remove more than one
|
||||||
|
flag at a time, *flag* may be a string of more than one character.
|
||||||
|
|
||||||
|
Considerations for using this method versus the message object's
|
||||||
|
:meth:`~MaildirMessage.remove_flag` method are similar to
|
||||||
|
those for :meth:`set_flags`; see the discussion there.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: get_info(key)
|
||||||
|
|
||||||
|
Return a string containing the info for the message
|
||||||
|
corresponding to *key*.
|
||||||
|
This is the same as ``get_message(key).get_info()`` but much
|
||||||
|
faster, because it does not open the message file.
|
||||||
|
Use this method when iterating over the keys to determine which
|
||||||
|
messages are interesting to get.
|
||||||
|
|
||||||
|
If you do have a :class:`MaildirMessage` object, use
|
||||||
|
its :meth:`~MaildirMessage.get_info` method instead, because
|
||||||
|
changes made by the message's :meth:`~MaildirMessage.set_info` method
|
||||||
|
are not reflected here until the mailbox's :meth:`__setitem__` method
|
||||||
|
is called.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: set_info(key, info)
|
||||||
|
|
||||||
|
Set the info of the message corresponding to *key* to *info*.
|
||||||
|
Calling ``some_mailbox.set_info(key, flags)`` is similar to ::
|
||||||
|
|
||||||
|
one_message = some_mailbox.get_message(key)
|
||||||
|
one_message.set_info(info)
|
||||||
|
some_mailbox[key] = one_message
|
||||||
|
|
||||||
|
but faster, because it does not open the message file.
|
||||||
|
|
||||||
|
If you do have a :class:`MaildirMessage` object, use
|
||||||
|
its :meth:`~MaildirMessage.set_info` method instead, because
|
||||||
|
changes made with this mailbox method will not be visible to the
|
||||||
|
message object's method, :meth:`~MaildirMessage.get_info`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
`maildir man page from Courier <https://www.courier-mta.org/maildir.html>`_
|
`maildir man page from Courier <https://www.courier-mta.org/maildir.html>`_
|
||||||
|
@ -838,7 +940,7 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF.
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
A message is typically moved from :file:`new` to :file:`cur` after its
|
A message is typically moved from :file:`new` to :file:`cur` after its
|
||||||
mailbox has been accessed, whether or not the message is has been
|
mailbox has been accessed, whether or not the message has been
|
||||||
read. A message ``msg`` has been read if ``"S" in msg.get_flags()`` is
|
read. A message ``msg`` has been read if ``"S" in msg.get_flags()`` is
|
||||||
``True``.
|
``True``.
|
||||||
|
|
||||||
|
|
|
@ -395,6 +395,56 @@ class Maildir(Mailbox):
|
||||||
f = open(os.path.join(self._path, self._lookup(key)), 'rb')
|
f = open(os.path.join(self._path, self._lookup(key)), 'rb')
|
||||||
return _ProxyFile(f)
|
return _ProxyFile(f)
|
||||||
|
|
||||||
|
def get_info(self, key):
|
||||||
|
"""Get the keyed message's "info" as a string."""
|
||||||
|
subpath = self._lookup(key)
|
||||||
|
if self.colon in subpath:
|
||||||
|
return subpath.split(self.colon)[-1]
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def set_info(self, key, info: str):
|
||||||
|
"""Set the keyed message's "info" string."""
|
||||||
|
if not isinstance(info, str):
|
||||||
|
raise TypeError(f'info must be a string: {type(info)}')
|
||||||
|
old_subpath = self._lookup(key)
|
||||||
|
new_subpath = old_subpath.split(self.colon)[0]
|
||||||
|
if info:
|
||||||
|
new_subpath += self.colon + info
|
||||||
|
if new_subpath == old_subpath:
|
||||||
|
return
|
||||||
|
old_path = os.path.join(self._path, old_subpath)
|
||||||
|
new_path = os.path.join(self._path, new_subpath)
|
||||||
|
os.rename(old_path, new_path)
|
||||||
|
self._toc[key] = new_subpath
|
||||||
|
|
||||||
|
def get_flags(self, key):
|
||||||
|
"""Return as a string the standard flags that are set on the keyed message."""
|
||||||
|
info = self.get_info(key)
|
||||||
|
if info.startswith('2,'):
|
||||||
|
return info[2:]
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def set_flags(self, key, flags: str):
|
||||||
|
"""Set the given flags and unset all others on the keyed message."""
|
||||||
|
if not isinstance(flags, str):
|
||||||
|
raise TypeError(f'flags must be a string: {type(flags)}')
|
||||||
|
# TODO: check if flags are valid standard flag characters?
|
||||||
|
self.set_info(key, '2,' + ''.join(sorted(set(flags))))
|
||||||
|
|
||||||
|
def add_flag(self, key, flag: str):
|
||||||
|
"""Set the given flag(s) without changing others on the keyed message."""
|
||||||
|
if not isinstance(flag, str):
|
||||||
|
raise TypeError(f'flag must be a string: {type(flag)}')
|
||||||
|
# TODO: check that flag is a valid standard flag character?
|
||||||
|
self.set_flags(key, ''.join(set(self.get_flags(key)) | set(flag)))
|
||||||
|
|
||||||
|
def remove_flag(self, key, flag: str):
|
||||||
|
"""Unset the given string flag(s) without changing others on the keyed message."""
|
||||||
|
if not isinstance(flag, str):
|
||||||
|
raise TypeError(f'flag must be a string: {type(flag)}')
|
||||||
|
if self.get_flags(key):
|
||||||
|
self.set_flags(key, ''.join(set(self.get_flags(key)) - set(flag)))
|
||||||
|
|
||||||
def iterkeys(self):
|
def iterkeys(self):
|
||||||
"""Return an iterator over keys."""
|
"""Return an iterator over keys."""
|
||||||
self._refresh()
|
self._refresh()
|
||||||
|
|
|
@ -847,6 +847,92 @@ class TestMaildir(TestMailbox, unittest.TestCase):
|
||||||
self._box.lock()
|
self._box.lock()
|
||||||
self._box.unlock()
|
self._box.unlock()
|
||||||
|
|
||||||
|
def test_get_info(self):
|
||||||
|
# Test getting message info from Maildir, not the message.
|
||||||
|
msg = mailbox.MaildirMessage(self._template % 0)
|
||||||
|
key = self._box.add(msg)
|
||||||
|
self.assertEqual(self._box.get_info(key), '')
|
||||||
|
msg.set_info('OurTestInfo')
|
||||||
|
self._box[key] = msg
|
||||||
|
self.assertEqual(self._box.get_info(key), 'OurTestInfo')
|
||||||
|
|
||||||
|
def test_set_info(self):
|
||||||
|
# Test setting message info from Maildir, not the message.
|
||||||
|
# This should immediately rename the message file.
|
||||||
|
msg = mailbox.MaildirMessage(self._template % 0)
|
||||||
|
key = self._box.add(msg)
|
||||||
|
def check_info(oldinfo, newinfo):
|
||||||
|
oldfilename = os.path.join(self._box._path, self._box._lookup(key))
|
||||||
|
newsubpath = self._box._lookup(key).split(self._box.colon)[0]
|
||||||
|
if newinfo:
|
||||||
|
newsubpath += self._box.colon + newinfo
|
||||||
|
newfilename = os.path.join(self._box._path, newsubpath)
|
||||||
|
# assert initial conditions
|
||||||
|
self.assertEqual(self._box.get_info(key), oldinfo)
|
||||||
|
if not oldinfo:
|
||||||
|
self.assertNotIn(self._box._lookup(key), self._box.colon)
|
||||||
|
self.assertTrue(os.path.exists(oldfilename))
|
||||||
|
if oldinfo != newinfo:
|
||||||
|
self.assertFalse(os.path.exists(newfilename))
|
||||||
|
# do the rename
|
||||||
|
self._box.set_info(key, newinfo)
|
||||||
|
# assert post conditions
|
||||||
|
if not newinfo:
|
||||||
|
self.assertNotIn(self._box._lookup(key), self._box.colon)
|
||||||
|
if oldinfo != newinfo:
|
||||||
|
self.assertFalse(os.path.exists(oldfilename))
|
||||||
|
self.assertTrue(os.path.exists(newfilename))
|
||||||
|
self.assertEqual(self._box.get_info(key), newinfo)
|
||||||
|
# none -> has info
|
||||||
|
check_info('', 'info1')
|
||||||
|
# has info -> same info
|
||||||
|
check_info('info1', 'info1')
|
||||||
|
# has info -> different info
|
||||||
|
check_info('info1', 'info2')
|
||||||
|
# has info -> none
|
||||||
|
check_info('info2', '')
|
||||||
|
# none -> none
|
||||||
|
check_info('', '')
|
||||||
|
|
||||||
|
def test_get_flags(self):
|
||||||
|
# Test getting message flags from Maildir, not the message.
|
||||||
|
msg = mailbox.MaildirMessage(self._template % 0)
|
||||||
|
key = self._box.add(msg)
|
||||||
|
self.assertEqual(self._box.get_flags(key), '')
|
||||||
|
msg.set_flags('T')
|
||||||
|
self._box[key] = msg
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'T')
|
||||||
|
|
||||||
|
def test_set_flags(self):
|
||||||
|
msg = mailbox.MaildirMessage(self._template % 0)
|
||||||
|
key = self._box.add(msg)
|
||||||
|
self.assertEqual(self._box.get_flags(key), '')
|
||||||
|
self._box.set_flags(key, 'S')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'S')
|
||||||
|
|
||||||
|
def test_add_flag(self):
|
||||||
|
msg = mailbox.MaildirMessage(self._template % 0)
|
||||||
|
key = self._box.add(msg)
|
||||||
|
self.assertEqual(self._box.get_flags(key), '')
|
||||||
|
self._box.add_flag(key, 'B')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'B')
|
||||||
|
self._box.add_flag(key, 'B')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'B')
|
||||||
|
self._box.add_flag(key, 'AC')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'ABC')
|
||||||
|
|
||||||
|
def test_remove_flag(self):
|
||||||
|
msg = mailbox.MaildirMessage(self._template % 0)
|
||||||
|
key = self._box.add(msg)
|
||||||
|
self._box.set_flags(key, 'abc')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'abc')
|
||||||
|
self._box.remove_flag(key, 'b')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'ac')
|
||||||
|
self._box.remove_flag(key, 'b')
|
||||||
|
self.assertEqual(self._box.get_flags(key), 'ac')
|
||||||
|
self._box.remove_flag(key, 'ac')
|
||||||
|
self.assertEqual(self._box.get_flags(key), '')
|
||||||
|
|
||||||
def test_folder (self):
|
def test_folder (self):
|
||||||
# Test for bug #1569790: verify that folders returned by .get_folder()
|
# Test for bug #1569790: verify that folders returned by .get_folder()
|
||||||
# use the same factory function.
|
# use the same factory function.
|
||||||
|
|
|
@ -630,6 +630,7 @@ Dinu Gherman
|
||||||
Subhendu Ghosh
|
Subhendu Ghosh
|
||||||
Jonathan Giddy
|
Jonathan Giddy
|
||||||
Johannes Gijsbers
|
Johannes Gijsbers
|
||||||
|
Stephen Gildea
|
||||||
Michael Gilfix
|
Michael Gilfix
|
||||||
Julian Gindi
|
Julian Gindi
|
||||||
Yannick Gingras
|
Yannick Gingras
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
New methods :meth:`mailbox.Maildir.get_info`,
|
||||||
|
:meth:`mailbox.Maildir.set_info`, :meth:`mailbox.Maildir.get_flags`,
|
||||||
|
:meth:`mailbox.Maildir.set_flags`, :meth:`mailbox.Maildir.add_flag`,
|
||||||
|
:meth:`mailbox.Maildir.remove_flag`. These methods speed up accessing a
|
||||||
|
message's info and/or flags and are useful when it is not necessary to
|
||||||
|
access the message's contents, as when iterating over a Maildir to find
|
||||||
|
messages with specific flags.
|
Loading…
Reference in New Issue