https://github.com/python/cpython/commit/8fd287b18f20f0a310203f574adec196530627c7
commit: 8fd287b18f20f0a310203f574adec196530627c7
branch: main
author: Zackery Spytz <zsp...@gmail.com>
committer: encukou <encu...@gmail.com>
date: 2024-01-16T08:51:46+01:00
summary:

gh-78502: Add a trackfd parameter to mmap.mmap() (GH-25425)

If *trackfd* is False, the file descriptor specified by *fileno*
will not be duplicated.

Co-authored-by: Erlend E. Aasland <erl...@python.org>
Co-authored-by: Petr Viktorin <encu...@gmail.com>
Co-authored-by: Serhiy Storchaka <storch...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2021-04-15-10-41-51.bpo-34321.36m6_l.rst
M Doc/library/mmap.rst
M Doc/whatsnew/3.13.rst
M Lib/test/test_mmap.py
M Modules/mmapmodule.c

diff --git a/Doc/library/mmap.rst b/Doc/library/mmap.rst
index ef6631ddcc68c8..758721433f77de 100644
--- a/Doc/library/mmap.rst
+++ b/Doc/library/mmap.rst
@@ -48,7 +48,7 @@ update the underlying file.
 
 To map anonymous memory, -1 should be passed as the fileno along with the 
length.
 
-.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset])
+.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT, offset=0)
 
    **(Windows version)** Maps *length* bytes from the file specified by the
    file handle *fileno*, and creates a mmap object.  If *length* is larger
@@ -71,7 +71,8 @@ To map anonymous memory, -1 should be passed as the fileno 
along with the length
 
    .. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap
 
-.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, 
access=ACCESS_DEFAULT[, offset])
+.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, \
+                access=ACCESS_DEFAULT, offset=0, *, trackfd=True)
    :noindex:
 
    **(Unix version)** Maps *length* bytes from the file specified by the file
@@ -102,10 +103,20 @@ To map anonymous memory, -1 should be passed as the 
fileno along with the length
    defaults to 0. *offset* must be a multiple of :const:`ALLOCATIONGRANULARITY`
    which is equal to :const:`PAGESIZE` on Unix systems.
 
+   If *trackfd* is ``False``, the file descriptor specified by *fileno* will
+   not be duplicated, and the resulting :class:`!mmap` object will not
+   be associated with the map's underlying file.
+   This means that the :meth:`~mmap.mmap.size` and :meth:`~mmap.mmap.resize`
+   methods will fail.
+   This mode is useful to limit the number of open file descriptors.
+
    To ensure validity of the created memory mapping the file specified
    by the descriptor *fileno* is internally automatically synchronized
    with the physical backing store on macOS.
 
+   .. versionchanged:: 3.13
+      The *trackfd* parameter was added.
+
    This example shows a simple way of using :class:`~mmap.mmap`::
 
       import mmap
@@ -254,9 +265,12 @@ To map anonymous memory, -1 should be passed as the fileno 
along with the length
 
    .. method:: resize(newsize)
 
-      Resizes the map and the underlying file, if any. If the mmap was created
-      with :const:`ACCESS_READ` or :const:`ACCESS_COPY`, resizing the map will
-      raise a :exc:`TypeError` exception.
+      Resizes the map and the underlying file, if any.
+
+      Resizing a map created with *access* of :const:`ACCESS_READ` or
+      :const:`ACCESS_COPY`, will raise a :exc:`TypeError` exception.
+      Resizing a map created with with *trackfd* set to ``False``,
+      will raise a :exc:`ValueError` exception.
 
       **On Windows**: Resizing the map will raise an :exc:`OSError` if there 
are other
       maps against the same named file. Resizing an anonymous map (ie against 
the
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 05b9b87a63252f..e96fcf9f5b0385 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -254,6 +254,9 @@ mmap
   that can be used where it requires a file-like object with seekable and
   the :meth:`~mmap.mmap.seek` method return the new absolute position.
   (Contributed by Donghee Na and Sylvie Liberman in :gh:`111835`.)
+* :class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is ``False``,
+  the file descriptor specified by *fileno* will not be duplicated.
+  (Contributed by Zackery Spytz and Petr Viktorin in :gh:`78502`.)
 
 opcode
 ------
diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py
index f1e4b1efe2d0aa..b89621e08577be 100644
--- a/Lib/test/test_mmap.py
+++ b/Lib/test/test_mmap.py
@@ -4,6 +4,7 @@
 from test.support.import_helper import import_module
 from test.support.os_helper import TESTFN, unlink
 import unittest
+import errno
 import os
 import re
 import itertools
@@ -266,6 +267,62 @@ def test_access_parameter(self):
                     self.assertRaises(TypeError, m.write_byte, 0)
                     m.close()
 
+    @unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
+    def test_trackfd_parameter(self):
+        size = 64
+        with open(TESTFN, "wb") as f:
+            f.write(b"a"*size)
+        for close_original_fd in True, False:
+            with self.subTest(close_original_fd=close_original_fd):
+                with open(TESTFN, "r+b") as f:
+                    with mmap.mmap(f.fileno(), size, trackfd=False) as m:
+                        if close_original_fd:
+                            f.close()
+                        self.assertEqual(len(m), size)
+                        with self.assertRaises(OSError) as err_cm:
+                            m.size()
+                        self.assertEqual(err_cm.exception.errno, errno.EBADF)
+                        with self.assertRaises(ValueError):
+                            m.resize(size * 2)
+                        with self.assertRaises(ValueError):
+                            m.resize(size // 2)
+                        self.assertEqual(m.closed, False)
+
+                        # Smoke-test other API
+                        m.write_byte(ord('X'))
+                        m[2] = ord('Y')
+                        m.flush()
+                        with open(TESTFN, "rb") as f:
+                            self.assertEqual(f.read(4), b'XaYa')
+                        self.assertEqual(m.tell(), 1)
+                        m.seek(0)
+                        self.assertEqual(m.tell(), 0)
+                        self.assertEqual(m.read_byte(), ord('X'))
+
+                self.assertEqual(m.closed, True)
+                self.assertEqual(os.stat(TESTFN).st_size, size)
+
+    @unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
+    def test_trackfd_neg1(self):
+        size = 64
+        with mmap.mmap(-1, size, trackfd=False) as m:
+            with self.assertRaises(OSError):
+                m.size()
+            with self.assertRaises(ValueError):
+                m.resize(size // 2)
+            self.assertEqual(len(m), size)
+            m[0] = ord('a')
+            assert m[0] == ord('a')
+
+    @unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
+    def test_no_trackfd_parameter_on_windows(self):
+        # 'trackffd' is an invalid keyword argument for this function
+        size = 64
+        with self.assertRaises(TypeError):
+            mmap.mmap(-1, size, trackfd=True)
+        with self.assertRaises(TypeError):
+            mmap.mmap(-1, size, trackfd=False)
+
     def test_bad_file_desc(self):
         # Try opening a bad file descriptor...
         self.assertRaises(OSError, mmap.mmap, -2, 4096)
diff --git a/Misc/NEWS.d/next/Library/2021-04-15-10-41-51.bpo-34321.36m6_l.rst 
b/Misc/NEWS.d/next/Library/2021-04-15-10-41-51.bpo-34321.36m6_l.rst
new file mode 100644
index 00000000000000..85912942eedf5a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-04-15-10-41-51.bpo-34321.36m6_l.rst
@@ -0,0 +1,2 @@
+:class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is
+``False``, the file descriptor specified by *fileno* will not be duplicated.
diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c
index 48902fb3547c9a..0cce7c27f9b16a 100644
--- a/Modules/mmapmodule.c
+++ b/Modules/mmapmodule.c
@@ -117,6 +117,7 @@ typedef struct {
 
 #ifdef UNIX
     int fd;
+    _Bool trackfd;
 #endif
 
     PyObject *weakreflist;
@@ -393,6 +394,13 @@ is_resizeable(mmap_object *self)
             "mmap can't resize with extant buffers exported.");
         return 0;
     }
+#ifdef UNIX
+    if (!self->trackfd) {
+        PyErr_SetString(PyExc_ValueError,
+            "mmap can't resize with trackfd=False.");
+        return 0;
+    }
+#endif
     if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
         return 1;
     PyErr_Format(PyExc_TypeError,
@@ -1154,7 +1162,7 @@ is 0, the maximum length of the map is the current size 
of the file,\n\
 except that if the file is empty Windows raises an exception (you cannot\n\
 create an empty mapping on Windows).\n\
 \n\
-Unix: mmap(fileno, length[, flags[, prot[, access[, offset]]]])\n\
+Unix: mmap(fileno, length[, flags[, prot[, access[, offset[, trackfd]]]]])\n\
 \n\
 Maps length bytes from the file specified by the file descriptor fileno,\n\
 and returns a mmap object.  If length is 0, the maximum length of the map\n\
@@ -1221,15 +1229,17 @@ new_mmap_object(PyTypeObject *type, PyObject *args, 
PyObject *kwdict)
     off_t offset = 0;
     int fd, flags = MAP_SHARED, prot = PROT_WRITE | PROT_READ;
     int devzero = -1;
-    int access = (int)ACCESS_DEFAULT;
+    int access = (int)ACCESS_DEFAULT, trackfd = 1;
     static char *keywords[] = {"fileno", "length",
                                "flags", "prot",
-                               "access", "offset", NULL};
+                               "access", "offset", "trackfd", NULL};
 
-    if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|iii" _Py_PARSE_OFF_T, 
keywords,
+    if (!PyArg_ParseTupleAndKeywords(args, kwdict,
+                                     "in|iii" _Py_PARSE_OFF_T "$p", keywords,
                                      &fd, &map_size, &flags, &prot,
-                                     &access, &offset))
+                                     &access, &offset, &trackfd)) {
         return NULL;
+    }
     if (map_size < 0) {
         PyErr_SetString(PyExc_OverflowError,
                         "memory mapped length must be positive");
@@ -1325,6 +1335,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, 
PyObject *kwdict)
     m_obj->weakreflist = NULL;
     m_obj->exports = 0;
     m_obj->offset = offset;
+    m_obj->trackfd = trackfd;
     if (fd == -1) {
         m_obj->fd = -1;
         /* Assume the caller wants to map anonymous memory.
@@ -1350,13 +1361,16 @@ new_mmap_object(PyTypeObject *type, PyObject *args, 
PyObject *kwdict)
         }
 #endif
     }
-    else {
+    else if (trackfd) {
         m_obj->fd = _Py_dup(fd);
         if (m_obj->fd == -1) {
             Py_DECREF(m_obj);
             return NULL;
         }
     }
+    else {
+        m_obj->fd = -1;
+    }
 
     Py_BEGIN_ALLOW_THREADS
     m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to