]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-90890: New methods to access mailbox.Maildir message info and flags (#103905)
authorStephen Gildea <stepheng-bpo@gildea.com>
Sat, 11 Nov 2023 17:41:33 +0000 (09:41 -0800)
committerGitHub <noreply@github.com>
Sat, 11 Nov 2023 17:41:33 +0000 (17:41 +0000)
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>
Doc/library/mailbox.rst
Lib/mailbox.py
Lib/test/test_mailbox.py
Misc/ACKS
Misc/NEWS.d/next/Library/2023-04-26-16-37-00.gh-issue-90890.fIag4w.rst [new file with mode: 0644]

index b27deb20f13236af8f3177ee43cd0dd143c4531e..05ffaf6c9b336e2eb291b3e5bbec9651e696e380 100644 (file)
@@ -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``.
 
index 59834a2b3b52434ccbc8bce7a2d35d8d74e72c04..36afaded705d0a7c1948ca61c4503ccab511c5c2 100644 (file)
@@ -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()
index 4977a9369ddf88cbbb8ddd90861fe3148b3c10f7..23fcbfac1f9c891575226a146528465fa45b012b 100644 (file)
@@ -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.
index 812aa1be6e796a071fe33d38c72eb9d3a0634333..6d3a4e3fdb8fe7215831902945450012bea27765 100644 (file)
--- 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 (file)
index 0000000..ee2e69e
--- /dev/null
@@ -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.