summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Gildea <stepheng-bpo@gildea.com>2023-11-11 17:41:33 (GMT)
committerGitHub <noreply@github.com>2023-11-11 17:41:33 (GMT)
commit38035fed9ba543d587c1fbba5c463d34edf3aff9 (patch)
treecfb93161065f04daa9a7c77b41d510d096ab126f
parentfa84e5fe0a3bd8e77c33b20867d71ac6bee270f9 (diff)
downloadcpython-38035fed9ba543d587c1fbba5c463d34edf3aff9.zip
cpython-38035fed9ba543d587c1fbba5c463d34edf3aff9.tar.gz
cpython-38035fed9ba543d587c1fbba5c463d34edf3aff9.tar.bz2
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>
-rw-r--r--Doc/library/mailbox.rst104
-rw-r--r--Lib/mailbox.py50
-rw-r--r--Lib/test/test_mailbox.py86
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS.d/next/Library/2023-04-26-16-37-00.gh-issue-90890.fIag4w.rst7
5 files changed, 247 insertions, 1 deletions
diff --git a/Doc/library/mailbox.rst b/Doc/library/mailbox.rst
index b27deb2..05ffaf6 100644
--- a/Doc/library/mailbox.rst
+++ b/Doc/library/mailbox.rst
@@ -424,6 +424,108 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF.
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::
`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::
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
``True``.
diff --git a/Lib/mailbox.py b/Lib/mailbox.py
index 59834a2..36afade 100644
--- a/Lib/mailbox.py
+++ b/Lib/mailbox.py
@@ -395,6 +395,56 @@ class Maildir(Mailbox):
f = open(os.path.join(self._path, self._lookup(key)), 'rb')
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):
"""Return an iterator over keys."""
self._refresh()
diff --git a/Lib/test/test_mailbox.py b/Lib/test/test_mailbox.py
index 4977a93..23fcbfa 100644
--- a/Lib/test/test_mailbox.py
+++ b/Lib/test/test_mailbox.py
@@ -847,6 +847,92 @@ class TestMaildir(TestMailbox, unittest.TestCase):
self._box.lock()
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):
# Test for bug #1569790: verify that folders returned by .get_folder()
# use the same factory function.
diff --git a/Misc/ACKS b/Misc/ACKS
index 812aa1b..6d3a4e3 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -630,6 +630,7 @@ Dinu Gherman
Subhendu Ghosh
Jonathan Giddy
Johannes Gijsbers
+Stephen Gildea
Michael Gilfix
Julian Gindi
Yannick Gingras
diff --git a/Misc/NEWS.d/next/Library/2023-04-26-16-37-00.gh-issue-90890.fIag4w.rst b/Misc/NEWS.d/next/Library/2023-04-26-16-37-00.gh-issue-90890.fIag4w.rst
new file mode 100644
index 0000000..ee2e69e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-26-16-37-00.gh-issue-90890.fIag4w.rst
@@ -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.