From fca5112f17f1fda9206e0d3b313c6f8b5a23ee9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=87=E5=BD=AC=E5=AE=87?= <3431359639@qq.com> Date: Mon, 24 Aug 2026 12:38:11 +0800 Subject: [PATCH 1/7] gh-126253: Ignore unknown Windows file attribute bits in scandir test --- Lib/test/test_os/test_os.py | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 81b3043eb7e75bc..36e476e3fc589cf 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -84,6 +84,17 @@ HAVE_WHEEL_GROUP = sys.platform.startswith('freebsd') and os.getgid() == 0 +def _get_file_attribute_mask(): + mask = 0 + for name in dir(stat): + if name.startswith("FILE_ATTRIBUTE_"): + mask |= getattr(stat, name) + return mask + + +_FILE_ATTRIBUTE_MASK = _get_file_attribute_mask() + + def requires_os_func(name): return unittest.skipUnless(hasattr(os, name), 'requires os.%s' % name) @@ -5004,8 +5015,15 @@ def assert_stat_equal(self, stat1, stat2, skip_fields): if attr in ("st_dev", "st_ino", "st_nlink", "st_ctime", "st_ctime_ns"): continue - self.assertEqual(getattr(stat1, attr), - getattr(stat2, attr), + value1 = getattr(stat1, attr) + value2 = getattr(stat2, attr) + if attr == "st_file_attributes": + # Filesystems may report attribute bits that aren't + # exposed by the stat module. + value1 &= _FILE_ATTRIBUTE_MASK + value2 &= _FILE_ATTRIBUTE_MASK + self.assertEqual(value1, + value2, (stat1, stat2, attr)) else: self.assertEqual(stat1, stat2) @@ -5093,6 +5111,27 @@ def test_attributes(self): entry = entries['symlink_file.txt'] self.check_entry(entry, 'symlink_file.txt', False, True, True) + @unittest.skipUnless(sys.platform == 'win32', 'Windows-specific test') + def test_attributes_unknown_bits(self): + base = os.stat(self.path) + file_attributes = base.st_file_attributes + unknown_attribute = 0x10000000 + self.assertFalse(unknown_attribute & _FILE_ATTRIBUTE_MASK) + + stat1 = os.stat_result( + tuple(base), {"st_file_attributes": file_attributes}) + stat2 = os.stat_result( + tuple(base), + {"st_file_attributes": file_attributes | unknown_attribute}) + self.assert_stat_equal(stat1, stat2, True) + + stat3 = os.stat_result( + tuple(base), + {"st_file_attributes": + file_attributes ^ stat.FILE_ATTRIBUTE_READONLY}) + with self.assertRaises(AssertionError): + self.assert_stat_equal(stat1, stat3, True) + @unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.") def test_attributes_junctions(self): dirname = os.path.join(self.path, "tgtdir") From 4fcb07ecc7a646831073e429c0291e6dbd4db022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=87=E5=BD=AC=E5=AE=87?= <3431359639@qq.com> Date: Wed, 16 Sep 2026 22:37:55 +0800 Subject: [PATCH 2/7] gh-126253: Add a Tests NEWS entry --- .../next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst diff --git a/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst b/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst new file mode 100644 index 000000000000000..91cbc74fc0db26e --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst @@ -0,0 +1,4 @@ +Fix ``test_os.test_attributes`` on Windows when NTFS reports +``st_file_attributes`` bits that Python's :mod:`stat` module does not +expose. The ``os.scandir()`` vs ``os.stat()`` comparison now ignores +unknown bits while still checking every public ``FILE_ATTRIBUTE_*`` bit. From f15f1d1cfeac3f47bc58359ba6a31655b92859a5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:04:36 +0200 Subject: [PATCH 3/7] Rename _FILE_ATTRIBUTE_MASK to FILE_ATTRIBUTE_MASK, add comment, trim NEWS Co-authored-by: Victor Stinner Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/test/test_os/test_os.py | 11 ++++++----- .../2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst | 5 +---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index eadb7598a7a4bd1..0af40520ce46fd2 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -92,7 +92,7 @@ def _get_file_attribute_mask(): return mask -_FILE_ATTRIBUTE_MASK = _get_file_attribute_mask() +FILE_ATTRIBUTE_MASK = _get_file_attribute_mask() def requires_os_func(name): @@ -5048,8 +5048,8 @@ def assert_stat_equal(self, stat1, stat2, skip_fields): if attr == "st_file_attributes": # Filesystems may report attribute bits that aren't # exposed by the stat module. - value1 &= _FILE_ATTRIBUTE_MASK - value2 &= _FILE_ATTRIBUTE_MASK + value1 &= FILE_ATTRIBUTE_MASK + value2 &= FILE_ATTRIBUTE_MASK self.assertEqual(value1, value2, (stat1, stat2, attr)) @@ -5143,8 +5143,9 @@ def test_attributes(self): def test_attributes_unknown_bits(self): base = os.stat(self.path) file_attributes = base.st_file_attributes - unknown_attribute = 0x10000000 - self.assertFalse(unknown_attribute & _FILE_ATTRIBUTE_MASK) + # Directory flag of DOS file attribute in NTFS + unknown_attribute = 0x1_000_0000 + self.assertFalse(unknown_attribute & FILE_ATTRIBUTE_MASK) stat1 = os.stat_result( tuple(base), {"st_file_attributes": file_attributes}) diff --git a/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst b/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst index 91cbc74fc0db26e..370d822f883aa9a 100644 --- a/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst +++ b/Misc/NEWS.d/next/Tests/2026-09-16-14-37-28.gh-issue-126253.9FOKPp.rst @@ -1,4 +1 @@ -Fix ``test_os.test_attributes`` on Windows when NTFS reports -``st_file_attributes`` bits that Python's :mod:`stat` module does not -expose. The ``os.scandir()`` vs ``os.stat()`` comparison now ignores -unknown bits while still checking every public ``FILE_ATTRIBUTE_*`` bit. +Fix ``test_os.test_attributes()`` on Windows NTFS: ignore unknown file attributes. From e29f4587568758ab63b82d770184730cf116107c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:05:12 +0200 Subject: [PATCH 4/7] Remove redundant assert_stat_equal --- Lib/test/test_os/test_os.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 0af40520ce46fd2..05e01bb7035b3fb 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -5035,27 +5035,6 @@ def get_entries(self, names): self.assertEqual(sorted(entries.keys()), names) return entries - def assert_stat_equal(self, stat1, stat2, skip_fields): - if skip_fields: - for attr in dir(stat1): - if not attr.startswith("st_"): - continue - if attr in ("st_dev", "st_ino", "st_nlink", "st_ctime", - "st_ctime_ns"): - continue - value1 = getattr(stat1, attr) - value2 = getattr(stat2, attr) - if attr == "st_file_attributes": - # Filesystems may report attribute bits that aren't - # exposed by the stat module. - value1 &= FILE_ATTRIBUTE_MASK - value2 &= FILE_ATTRIBUTE_MASK - self.assertEqual(value1, - value2, - (stat1, stat2, attr)) - else: - self.assertEqual(stat1, stat2) - def test_uninstantiable(self): scandir_iter = os.scandir(self.path) self.assertRaises(TypeError, type(scandir_iter)) From 946a09ca6abb68b625c2f552b8943118eceb18e7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:06:52 +0200 Subject: [PATCH 5/7] Revert "Remove redundant assert_stat_equal" This reverts commit e29f4587568758ab63b82d770184730cf116107c. --- Lib/test/test_os/test_os.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 05e01bb7035b3fb..0af40520ce46fd2 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -5035,6 +5035,27 @@ def get_entries(self, names): self.assertEqual(sorted(entries.keys()), names) return entries + def assert_stat_equal(self, stat1, stat2, skip_fields): + if skip_fields: + for attr in dir(stat1): + if not attr.startswith("st_"): + continue + if attr in ("st_dev", "st_ino", "st_nlink", "st_ctime", + "st_ctime_ns"): + continue + value1 = getattr(stat1, attr) + value2 = getattr(stat2, attr) + if attr == "st_file_attributes": + # Filesystems may report attribute bits that aren't + # exposed by the stat module. + value1 &= FILE_ATTRIBUTE_MASK + value2 &= FILE_ATTRIBUTE_MASK + self.assertEqual(value1, + value2, + (stat1, stat2, attr)) + else: + self.assertEqual(stat1, stat2) + def test_uninstantiable(self): scandir_iter = os.scandir(self.path) self.assertRaises(TypeError, type(scandir_iter)) From 6928e672b6832518573533c5d6d39fed5249df10 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:07:18 +0200 Subject: [PATCH 6/7] Remove redundant test_attributes_unknown_bits --- Lib/test/test_os/test_os.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 0af40520ce46fd2..ce98633bf7e9666 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -5139,28 +5139,6 @@ def test_attributes(self): entry = entries['symlink_file.txt'] self.check_entry(entry, 'symlink_file.txt', False, True, True) - @unittest.skipUnless(sys.platform == 'win32', 'Windows-specific test') - def test_attributes_unknown_bits(self): - base = os.stat(self.path) - file_attributes = base.st_file_attributes - # Directory flag of DOS file attribute in NTFS - unknown_attribute = 0x1_000_0000 - self.assertFalse(unknown_attribute & FILE_ATTRIBUTE_MASK) - - stat1 = os.stat_result( - tuple(base), {"st_file_attributes": file_attributes}) - stat2 = os.stat_result( - tuple(base), - {"st_file_attributes": file_attributes | unknown_attribute}) - self.assert_stat_equal(stat1, stat2, True) - - stat3 = os.stat_result( - tuple(base), - {"st_file_attributes": - file_attributes ^ stat.FILE_ATTRIBUTE_READONLY}) - with self.assertRaises(AssertionError): - self.assert_stat_equal(stat1, stat3, True) - @unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.") def test_attributes_junctions(self): dirname = os.path.join(self.path, "tgtdir") From 8cf64a2c5668e84f53d2f047d8472ee826352752 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Nov 2026 18:38:15 +0100 Subject: [PATCH 7/7] Apply suggestion from @vstinner --- Lib/test/test_os/test_os.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index ce98633bf7e9666..a67afa36237929f 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -92,6 +92,8 @@ def _get_file_attribute_mask(): return mask +# gh-126253: Ignore unknown attributes, such as 0x1_000_0000 +# (Directory flag of DOS file attribute in NTFS) FILE_ATTRIBUTE_MASK = _get_file_attribute_mask()