diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 808187061733be..7a6474e88f4d5d 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -239,14 +239,24 @@ the :mod:`glob` module.) Accepts a :term:`path-like object`. -.. function:: getsize(path, /) +.. function:: getsize(path, /, apparent=True) Return the size, in bytes, of *path*. Raise :exc:`OSError` if the file does - not exist or is inaccessible. + not exist or is inaccessible. If *apparent* is ``True``, the apparent size + (number of bytes) of the file is returned. If ``False``, the actual size + (disk space occupied) is returned. The actual size reflects the block size, + meaning it will typically be larger than the apparent size. However, the + inverse may also be true due to holes in ("sparse") files, internal + fragmentation, indirect blocks, etc. Passing ``apparent=False`` is only + supported on platforms where :data:`os.DEV_BSIZE` is available; a + :exc:`NotImplementedError` is raised otherwise. .. versionchanged:: 3.6 Accepts a :term:`path-like object`. + .. versionchanged:: 3.15 + Add the optional *apparent* parameter. + .. function:: isabs(path, /) diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 7547967c6b32f0..16c04847696af2 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2663,6 +2663,16 @@ features: .. versionadded:: 3.15 +.. data:: DEV_BSIZE + + The size, in bytes, of a block as reported by the :attr:`~os.stat_result.st_blocks` + field of :class:`~os.stat_result`. This is typically 512 bytes. + + :ref:`Availability `: Unix. + + .. versionadded:: 3.15 + + .. function:: pathconf(path, name) Return system configuration information relevant to a named file. *name* diff --git a/Lib/genericpath.py b/Lib/genericpath.py index 71ae19190839ae..9b61212fab35a7 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -82,9 +82,27 @@ def isdevdrive(path): return False -def getsize(filename, /): - """Return the size of a file, reported by os.stat().""" - return os.stat(filename).st_size +def getsize(filename, /, apparent=True): + """Return the size of a file, reported by os.stat(). + + If 'apparent' is True (default), the apparent size (number of bytes) of the + file is returned. If False, the actual size (disk space occupied) is + returned. The actual size reflects the block size, meaning it will + typically be larger than the apparent size. However, the inverse may also + be true due to holes in ("sparse") files, internal fragmentation, indirect + blocks, etc. + + Not all platforms support apparent=False; a NotImplementedError is raised + on platforms where os.DEV_BSIZE is not available. + """ + if apparent: + return os.stat(filename).st_size + _dev_bsize = getattr(os, 'DEV_BSIZE', None) + if _dev_bsize is None: + raise NotImplementedError( + "os.path.getsize() with apparent=False is not supported on this platform" + ) + return os.stat(filename).st_blocks * _dev_bsize def getmtime(filename, /): diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index 10d3f409d883c5..7e0fae476336f0 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -114,6 +114,53 @@ def test_getsize(self): create_file(filename, b'Hello World!') self.assertEqual(self.pathmodule.getsize(filename), 12) + os.remove(filename) + + open(filename, 'xb', 0).close() + os.truncate(filename, 512) + self.assertEqual(self.pathmodule.getsize(filename), 512) + + @unittest.skipUnless(hasattr(os, 'DEV_BSIZE'), + "os.DEV_BSIZE not available on this platform") + def test_getsize_actual(self): + filename = os_helper.TESTFN + self.addCleanup(os_helper.unlink, filename) + + # DEV_BSIZE varies across platforms + if support.is_android: + expected = 8192 + elif support.is_wasi: + expected = 0 + else: + expected = 4096 + + create_file(filename, b'Hello') + self.assertEqual(self.pathmodule.getsize(filename, apparent=False), expected) + os.remove(filename) + + create_file(filename, b'Hello World!') + self.assertEqual(self.pathmodule.getsize(filename, apparent=False), expected) + os.remove(filename) + + # DEV_BSIZE varies across platforms + if support.is_android: + expected = 4096 + else: + expected = 0 + + open(filename, 'xb', 0).close() + os.truncate(filename, 512) + self.assertEqual(self.pathmodule.getsize(filename, apparent=False), expected) + + @unittest.skipIf(hasattr(os, 'DEV_BSIZE'), + "os.DEV_BSIZE is available on this platform") + def test_getsize_actual_not_supported(self): + filename = os_helper.TESTFN + self.addCleanup(os_helper.unlink, filename) + + create_file(filename, b'Hello') + with self.assertRaises(NotImplementedError): + self.pathmodule.getsize(filename, apparent=False) def test_filetime(self): filename = os_helper.TESTFN diff --git a/Misc/NEWS.d/next/Library/2026-03-27-13-26-47.gh-issue-85264.iA_8Al.rst b/Misc/NEWS.d/next/Library/2026-03-27-13-26-47.gh-issue-85264.iA_8Al.rst new file mode 100644 index 00000000000000..642d5ed07dd7ab --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-27-13-26-47.gh-issue-85264.iA_8Al.rst @@ -0,0 +1,2 @@ +Add support for an ``apparent`` option for ``os.get_size`` to allow +retrieving the actual on-disk size of sparse files. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 07c2b73575f14e..04c8d5001903a2 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -18393,6 +18393,17 @@ all_ins(PyObject *m) /* STATX_ATTR_* constants are in the stat module */ #endif /* HAVE_STATX */ + /* Block size for the st_blocks field of stat(2). + * st_blocks is in 512-byte units on most platforms. DEV_BSIZE (or BSIZE + * as a fallback) from sys/param.h gives the actual platform value. */ +#if defined(HAVE_STRUCT_STAT_ST_BLOCKS) +# if defined(DEV_BSIZE) + if (PyModule_AddIntConstant(m, "DEV_BSIZE", DEV_BSIZE)) return -1; +# elif defined(BSIZE) + if (PyModule_AddIntConstant(m, "DEV_BSIZE", BSIZE)) return -1; +# endif +#endif + #if defined(__APPLE__) if (PyModule_AddIntConstant(m, "_COPYFILE_DATA", COPYFILE_DATA)) return -1; if (PyModule_AddIntConstant(m, "_COPYFILE_STAT", COPYFILE_STAT)) return -1;