From 816110b0f8895ae816a26f4444a00a623d9b93f9 Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sat, 16 May 2026 17:52:52 +0200 Subject: [PATCH 1/8] [3.15] Add new modules --- tests/module.py | 12 ++++++++++++ vermin/rules.py | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/tests/module.py b/tests/module.py index 5210cee3..1d67413d 100644 --- a/tests/module.py +++ b/tests/module.py @@ -588,3 +588,15 @@ def test_test_support_import_helper(self): def test_test_support_warnings_helper(self): self.assertOnlyIn((3, 10), self.detect("import test.support.warnings_helper")) + + def test_math_integer(self): + self.assertOnlyIn((3, 15), self.detect("import math.integer")) + + def test_profiling(self): + self.assertOnlyIn((3, 15), self.detect("import profiling")) + + def test_profiling_sampling(self): + self.assertOnlyIn((3, 15), self.detect("import profiling.sampling")) + + def test_profiling_tracing(self): + self.assertOnlyIn((3, 15), self.detect("import profiling.tracing")) diff --git a/vermin/rules.py b/vermin/rules.py index 2988eb49..ba762ad7 100644 --- a/vermin/rules.py +++ b/vermin/rules.py @@ -124,6 +124,7 @@ def MOD_REQS(config): "logging.handlers": ((2, 3), (3, 0)), "lzma": (None, (3, 3)), "markupbase": ((2, 0), None), + "math.integer": (None, (3, 15)), "md5": ((2, 0), None), "modulefinder": ((2, 3), (3, 0)), "msilib": ((2, 5), (3, 0)), @@ -141,6 +142,9 @@ def MOD_REQS(config): "pkgutil": ((2, 3), (3, 0)), "platform": ((2, 3), (3, 0)), "popen2": ((2, 0), None), + "profiling": (None, (3, 15)), + "profiling.sampling": (None, (3, 15)), + "profiling.tracing": (None, (3, 15)), "pyclbr": ((2, 0), (3, 0)), "pydoc": ((2, 1), (3, 0)), "queue": (None, (3, 0)), From 17091bda4b4277f23fe842868ef7c9ea9cde4ed4 Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sat, 16 May 2026 18:47:25 +0200 Subject: [PATCH 2/8] [3.15] Add new classes, constants, decorators, functions --- tests/builtin_classes.py | 6 ++ tests/constants.py | 18 +++++ tests/decorators.py | 3 + tests/function.py | 170 +++++++++++++++++++++++++++++++++++++++ vermin/rules.py | 64 +++++++++++++++ 5 files changed, 261 insertions(+) diff --git a/tests/builtin_classes.py b/tests/builtin_classes.py index 5524ecc1..f49c56c0 100644 --- a/tests/builtin_classes.py +++ b/tests/builtin_classes.py @@ -33,3 +33,9 @@ def test_ExceptionGroup(self): def test_BaseExceptionGroup(self): self.assertOnlyIn((3, 11), self.detect("BaseExceptionGroup()")) + + def test_frozendict(self): + self.assertOnlyIn((3, 15), self.detect("frozendict()")) + + def test_sentinel(self): + self.assertOnlyIn((3, 15), self.detect("sentinel()")) diff --git a/tests/constants.py b/tests/constants.py index ea6c1ff0..473eb439 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -3950,3 +3950,21 @@ def test_kwargs_of_unittest_mock_Mock_call_args(self): def test_status_of_urllib_response_addinfourl(self): self.assertOnlyIn((3, 9), self.detect("from urllib.response.addinfourl import status")) + + def test_RLIMIT_NTHR_of_resource(self): + self.assertOnlyIn((3, 15), self.detect("from resource import RLIMIT_NTHR")) + + def test_RLIMIT_THREADS_of_resource(self): + self.assertOnlyIn((3, 15), self.detect("from resource import RLIMIT_THREADS")) + + def test_RLIMIT_UMTXP_of_resource(self): + self.assertOnlyIn((3, 15), self.detect("from resource import RLIMIT_UMTXP")) + + def test_RLIM_SAVED_CUR_of_resource(self): + self.assertOnlyIn((3, 15), self.detect("from resource import RLIM_SAVED_CUR")) + + def test_RLIM_SAVED_MAX_of_resource(self): + self.assertOnlyIn((3, 15), self.detect("from resource import RLIM_SAVED_MAX")) + + def test_HAS_PSK_TLS13_of_ssl(self): + self.assertOnlyIn((3, 15), self.detect("from ssl import HAS_PSK_TLS13")) diff --git a/tests/decorators.py b/tests/decorators.py index cdf9f736..ecbece9f 100644 --- a/tests/decorators.py +++ b/tests/decorators.py @@ -121,3 +121,6 @@ def foo(): pass""") def test_deprecated_of_warnings(self): self.assertOnlyIn((3, 13), self.detect("from warnings import deprecated")) + + def test_disjoint_base_of_typing(self): + self.assertOnlyIn((3, 15), self.detect("from typing import disjoint_base")) diff --git a/tests/function.py b/tests/function.py index ea5cca59..8d4cc414 100644 --- a/tests/function.py +++ b/tests/function.py @@ -4971,3 +4971,173 @@ def test___notes___from_BaseException(self): def test_add_note_from_BaseException(self): self.assertOnlyIn((3, 11), self.detect("BaseException.add_note()")) + + def test_take_bytes_of_bytearray(self): + self.assertOnlyIn((3, 15), self.detect("bytearray.take_bytes()")) + + def test_default_content_type_of_SimpleHTTPRequestHandler(self): + self.assertOnlyIn((3, 15), + self.detect( + "import http.server\n" + "http.server.SimpleHTTPRequestHandler.default_content_type")) + + def test_fmax_of_math(self): + self.assertOnlyIn((3, 15), self.detect("from math import fmax")) + + def test_fmin_of_math(self): + self.assertOnlyIn((3, 15), self.detect("from math import fmin")) + + def test_isnormal_of_math(self): + self.assertOnlyIn((3, 15), self.detect("from math import isnormal")) + + def test_issubnormal_of_math(self): + self.assertOnlyIn((3, 15), self.detect("from math import issubnormal")) + + def test_signbit_of_math(self): + self.assertOnlyIn((3, 15), self.detect("from math import signbit")) + + def test_set_name_of_mmap(self): + self.assertOnlyIn((3, 15), self.detect("import mmap\nmmap.mmap.set_name()")) + + def test_statx_of_os(self): + self.assertOnlyIn((3, 15), self.detect("from os import statx")) + + def test_prefixmatch_of_Pattern(self): + self.assertOnlyIn((3, 15), self.detect("import re\nre.Pattern.prefixmatch()")) + + def test_prefixmatch_of_re(self): + self.assertOnlyIn((3, 15), self.detect("from re import prefixmatch")) + + def test_reorganize_of_Shelf(self): + self.assertOnlyIn((3, 15), self.detect("import shelve\nshelve.Shelf.reorganize()")) + + def test_get_groups_of_SSLContext(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLContext.get_groups()")) + + def test_set_ciphersuites_of_SSLContext(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLContext.set_ciphersuites()")) + + def test_set_client_sigalgs_of_SSLContext(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLContext.set_client_sigalgs()")) + + def test_set_groups_of_SSLContext(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLContext.set_groups()")) + + def test_set_server_sigalgs_of_SSLContext(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLContext.set_server_sigalgs()")) + + def test_client_sigalg_of_SSLSocket(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLSocket.client_sigalg")) + + def test_group_of_SSLSocket(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLSocket.group")) + + def test_server_sigalg_of_SSLSocket(self): + self.assertOnlyIn((3, 15), self.detect("import ssl\nssl.SSLSocket.server_sigalg")) + + def test_get_sigalgs_of_ssl(self): + self.assertOnlyIn((3, 15), self.detect("from ssl import get_sigalgs")) + + def test_get_cells_of_Function(self): + self.assertOnlyIn((3, 15), self.detect("import symtable\nsymtable.Function.get_cells()")) + + def test_is_cell_of_Symbol(self): + self.assertOnlyIn((3, 15), self.detect("import symtable\nsymtable.Symbol.is_cell()")) + + def test_abi_info_of_sys(self): + self.assertOnlyIn((3, 15), self.detect("from sys import abi_info")) + + def test_get_lazy_imports_of_sys(self): + self.assertOnlyIn((3, 15), self.detect("from sys import get_lazy_imports")) + + def test_set_lazy_imports_of_sys(self): + self.assertOnlyIn((3, 15), self.detect("from sys import set_lazy_imports")) + + def test_set_lazy_imports_filter_of_sys(self): + self.assertOnlyIn((3, 15), self.detect("from sys import set_lazy_imports_filter")) + + def test_concurrent_tee_of_threading(self): + self.assertOnlyIn((3, 15), self.detect("from threading import concurrent_tee")) + + def test_serialize_iterator_of_threading(self): + self.assertOnlyIn((3, 15), self.detect("from threading import serialize_iterator")) + + def test_synchronized_iterator_of_threading(self): + self.assertOnlyIn((3, 15), self.detect("from threading import synchronized_iterator")) + + def test_detail_of_Event(self): + self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.Event.detail")) + + def test_user_data_of_Event(self): + self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.Event.user_data")) + + def test_search_all_of_Text(self): + self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.Text.search_all()")) + + def test_TypeForm_from_typing(self): + self.assertOnlyIn((3, 15), self.detect("from typing import TypeForm")) + + def test_FrameLocalsProxyType_from_types(self): + self.assertOnlyIn((3, 15), self.detect("from types import FrameLocalsProxyType")) + + def test_LazyImportType_from_types(self): + self.assertOnlyIn((3, 15), self.detect("from types import LazyImportType")) + + def test_MacOS_from_webbrowser(self): + self.assertOnlyIn((3, 15), self.detect("from webbrowser import MacOS")) + + def test_block_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import block")) + + def test_extended_pictographic_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import extended_pictographic")) + + def test_grapheme_cluster_break_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import grapheme_cluster_break")) + + def test_indic_conjunct_break_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import indic_conjunct_break")) + + def test_iter_graphemes_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import iter_graphemes")) + + def test_isxidcontinue_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import isxidcontinue")) + + def test_isxidstart_of_unicodedata(self): + self.assertOnlyIn((3, 15), self.detect("from unicodedata import isxidstart")) + + def test_getformat_of_Wave_read(self): + self.assertOnlyIn((3, 15), self.detect("import wave\nwave.Wave_read.getformat()")) + + def test_getformat_of_Wave_write(self): + self.assertOnlyIn((3, 15), self.detect("import wave\nwave.Wave_write.getformat()")) + + def test_setformat_of_Wave_write(self): + self.assertOnlyIn((3, 15), self.detect("import wave\nwave.Wave_write.setformat()")) + + def test_is_valid_name_of_xml(self): + self.assertOnlyIn((3, 15), self.detect("from xml import is_valid_name")) + + def test_is_valid_text_of_xml(self): + self.assertOnlyIn((3, 15), self.detect("from xml import is_valid_text")) + + def test_SetBillionLaughsAttackProtectionActivationThreshold(self): + self.assertOnlyIn((3, 15), + self.detect( + "import xml.parsers.expat\n" + "xml.parsers.expat.xmlparser." + "SetBillionLaughsAttackProtectionActivationThreshold")) + + def test_SetBillionLaughsAttackProtectionMaximumAmplification(self): + self.assertOnlyIn((3, 15), + self.detect( + "import xml.parsers.expat\n" + "xml.parsers.expat.xmlparser." + "SetBillionLaughsAttackProtectionMaximumAmplification")) + + def test_adler32_combine_of_zlib(self): + self.assertOnlyIn((3, 15), self.detect("from zlib import adler32_combine")) + + def test_crc32_combine_of_zlib(self): + self.assertOnlyIn((3, 15), self.detect("from zlib import crc32_combine")) diff --git a/vermin/rules.py b/vermin/rules.py index ba762ad7..dedf7971 100644 --- a/vermin/rules.py +++ b/vermin/rules.py @@ -223,6 +223,8 @@ def MOD_MEM_REQS(config): "set": ((2, 4), (3, 0)), "dict": ((2, 2), (3, 0)), "long": ((2, 0), None), + "frozendict": (None, (3, 15)), + "sentinel": (None, (3, 15)), # Classes "ConfigParser.ConfigParser": ((2, 3), None), @@ -441,6 +443,7 @@ def MOD_MEM_REQS(config): "typing.SupportsRound": (None, (3, 5)) if not bp("typing", config) else (None, (3, 2)), "typing.TypedDict": (None, (3, 8)) if not bp("typing", config) else ((2, 7), (3, 8)), "typing.TypeAliasType": (None, (3, 12)), + "typing.TypeForm": (None, (3, 15)), "typing.TypeIs": (None, (3, 13)), "unittest.IsolatedAsyncioTestCase": (None, (3, 8)), "unittest.TextTestResult": ((2, 7), (3, 2)), @@ -454,6 +457,7 @@ def MOD_MEM_REQS(config): "urllib2.HTTPCookieProcessor": ((2, 4), None), "urllib2.HTTPErrorProcessor": ((2, 4), None), "uuid.SafeUUID": (None, (3, 7)), + "webbrowser.MacOS": (None, (3, 15)), "warnings.catch_warnings": ((2, 6), (3, 0)), "weakref.WeakMethod": (None, (3, 4)), "weakref.WeakSet": ((2, 7), (3, 0)), @@ -611,6 +615,7 @@ def MOD_MEM_REQS(config): "bytearray.maketrans": (None, (3, 1)), "bytearray.removeprefix": (None, (3, 9)), "bytearray.removesuffix": (None, (3, 9)), + "bytearray.take_bytes": (None, (3, 15)), "bytes.hex": (None, (3, 5)), "bytes.isascii": (None, (3, 7)), "bytes.maketrans": (None, (3, 1)), @@ -1063,6 +1068,7 @@ def MOD_MEM_REQS(config): "http.server.BaseHTTPRequestHandler.flush_headers": (None, (3, 3)), "http.server.BaseHTTPRequestHandler.handle_expect_100": (None, (3, 2)), "http.server.BaseHTTPRequestHandler.send_response_only": (None, (3, 2)), + "http.server.SimpleHTTPRequestHandler.default_content_type": (None, (3, 15)), "httplib.HTTPConnection.set_tunnel": ((2, 7), None), "httplib.HTTPResponse.getheaders": ((2, 4), None), "imaplib.IMAP4.Idler.burst": (None, (3, 14)), @@ -1216,6 +1222,8 @@ def MOD_MEM_REQS(config): "math.expm1": ((2, 7), (3, 2)), "math.factorial": ((2, 6), (3, 0)), "math.fma": (None, (3, 13)), + "math.fmax": (None, (3, 15)), + "math.fmin": (None, (3, 15)), "math.fsum": ((2, 6), (3, 0)), "math.gamma": ((2, 7), (3, 2)), "math.gcd": (None, (3, 5)), @@ -1223,7 +1231,9 @@ def MOD_MEM_REQS(config): "math.isfinite": (None, (3, 2)), "math.isinf": ((2, 6), (3, 0)), "math.isnan": ((2, 6), (3, 0)), + "math.isnormal": (None, (3, 15)), "math.isqrt": (None, (3, 8)), + "math.issubnormal": (None, (3, 15)), "math.lcm": (None, (3, 9)), "math.lgamma": ((2, 7), (3, 2)), "math.log1p": ((2, 6), (3, 0)), @@ -1232,6 +1242,7 @@ def MOD_MEM_REQS(config): "math.perm": (None, (3, 8)), "math.prod": (None, (3, 8)), "math.remainder": (None, (3, 7)), + "math.signbit": (None, (3, 15)), "math.sumprod": (None, (3, 12)), "math.trunc": ((2, 6), (3, 0)), "math.ulp": (None, (3, 9)), @@ -1240,6 +1251,7 @@ def MOD_MEM_REQS(config): "mimetypes.guess_file_type": (None, (3, 13)), "mmap.mmap.madvise": (None, (3, 8)), "mmap.mmap.seekable": (None, (3, 13)), + "mmap.mmap.set_name": (None, (3, 15)), "msilib.Database.Close": (None, (3, 7)), "msvcrt.getwch": ((2, 6), (3, 0)), "msvcrt.getwche": ((2, 6), (3, 0)), @@ -1444,6 +1456,7 @@ def MOD_MEM_REQS(config): "os.setxattr": (None, (3, 3)), "os.splice": (None, (3, 10)), "os.startfile": ((2, 0), (3, 0)), + "os.statx": (None, (3, 15)), "os.sync": (None, (3, 3)), "os.timerfd_create": (None, (3, 13)), "os.timerfd_gettime": (None, (3, 13)), @@ -1519,8 +1532,10 @@ def MOD_MEM_REQS(config): "random.setstate": ((2, 1), (3, 0)), "random.triangular": ((2, 6), (3, 0)), "re.Pattern.fullmatch": (None, (3, 4)), + "re.Pattern.prefixmatch": (None, (3, 15)), "re.finditer": ((2, 2), (3, 0)), "re.fullmatch": (None, (3, 4)), + "re.prefixmatch": (None, (3, 15)), "readline.append_history_file": (None, (3, 5)), "readline.clear_history": ((2, 4), (3, 0)), "readline.get_completer": ((2, 3), (3, 0)), @@ -1545,6 +1560,7 @@ def MOD_MEM_REQS(config): "sgmllib.SGMLParser.convert_charref": ((2, 5), None), "sgmllib.SGMLParser.convert_codepoint": ((2, 5), None), "sgmllib.SGMLParser.convert_entityref": ((2, 5), None), + "shelve.Shelf.reorganize": (None, (3, 15)), "shlex.join": (None, (3, 8)), "shlex.quote": (None, (3, 3)), "shlex.shlex.pop_source": ((2, 1), (3, 0)), @@ -1646,20 +1662,28 @@ def MOD_MEM_REQS(config): "ssl.SSLContext.cert_store_stats": ((2, 7), (3, 4)), "ssl.SSLContext.get_ca_certs": ((2, 7), (3, 4)), "ssl.SSLContext.get_ciphers": (None, (3, 6)), + "ssl.SSLContext.get_groups": (None, (3, 15)), "ssl.SSLContext.load_default_certs": ((2, 7), (3, 4)), "ssl.SSLContext.load_dh_params": ((2, 7), (3, 3)), "ssl.SSLContext.set_alpn_protocols": ((2, 7), (3, 5)), + "ssl.SSLContext.set_ciphersuites": (None, (3, 15)), + "ssl.SSLContext.set_client_sigalgs": (None, (3, 15)), "ssl.SSLContext.set_ecdh_curve": ((2, 7), (3, 3)), + "ssl.SSLContext.set_groups": (None, (3, 15)), "ssl.SSLContext.set_npn_protocols": ((2, 7), (3, 3)), "ssl.SSLContext.set_psk_client_callback": (None, (3, 13)), "ssl.SSLContext.set_psk_server_callback": (None, (3, 13)), + "ssl.SSLContext.set_server_sigalgs": (None, (3, 15)), "ssl.SSLContext.set_servername_callback": ((2, 7), (3, 4)), + "ssl.SSLSocket.client_sigalg": (None, (3, 15)), "ssl.SSLSocket.compression": ((2, 7), (3, 3)), "ssl.SSLSocket.get_channel_binding": ((2, 7), (3, 3)), "ssl.SSLSocket.get_unverified_chain": (None, (3, 13)), "ssl.SSLSocket.get_verified_chain": (None, (3, 13)), + "ssl.SSLSocket.group": (None, (3, 15)), "ssl.SSLSocket.selected_alpn_protocol": ((2, 7), (3, 5)), "ssl.SSLSocket.selected_npn_protocol": ((2, 7), (3, 3)), + "ssl.SSLSocket.server_sigalg": (None, (3, 15)), "ssl.SSLSocket.sendfile": (None, (3, 5)), "ssl.SSLSocket.shared_ciphers": (None, (3, 5)), "ssl.SSLSocket.verify_client_post_handshake": (None, (3, 8)), @@ -1669,6 +1693,7 @@ def MOD_MEM_REQS(config): "ssl.enum_certificates": ((2, 7), (3, 4)), "ssl.enum_crls": ((2, 7), (3, 4)), "ssl.get_default_verify_paths": ((2, 7), (3, 4)), + "ssl.get_sigalgs": (None, (3, 15)), "ssl.match_hostname": ((2, 7), (3, 2)), "stat.S_ISDOOR": (None, (3, 4)), "stat.S_ISPORT": (None, (3, 4)), @@ -1698,7 +1723,9 @@ def MOD_MEM_REQS(config): "subprocess.check_call": ((2, 5), (3, 0)), "subprocess.check_output": ((2, 7), (3, 1)), "subprocess.run": (None, (3, 5)), + "symtable.Function.get_cells": (None, (3, 15)), "symtable.Symbol.is_annotated": (None, (3, 6)), + "symtable.Symbol.is_cell": (None, (3, 15)), "symtable.Symbol.is_comp_cell": (None, (3, 14)), "symtable.Symbol.is_comp_iter": (None, (3, 14)), "symtable.Symbol.is_free_class": (None, (3, 14)), @@ -1711,6 +1738,7 @@ def MOD_MEM_REQS(config): "sys._is_gil_enabled": (None, (3, 13)), "sys._is_immortal": (None, (3, 14)), "sys._is_interned": (None, (3, 13)), + "sys.abi_info": (None, (3, 15)), "sys.addaudithook": (None, (3, 8)), "sys.audit": (None, (3, 8)), "sys.breakpointhook": (None, (3, 7)), @@ -1719,6 +1747,7 @@ def MOD_MEM_REQS(config): "sys.get_asyncgen_hooks": (None, (3, 6)), "sys.get_coroutine_origin_tracking_depth": (None, (3, 7)), "sys.get_int_max_str_digits": (None, (3, 11)), + "sys.get_lazy_imports": (None, (3, 15)), "sys.getallocatedblocks": (None, (3, 4)), "sys.getandroidapilevel": (None, (3, 7)), "sys.getcheckinterval": ((2, 3), (3, 0)), @@ -1735,6 +1764,8 @@ def MOD_MEM_REQS(config): "sys.set_asyncgen_hooks": (None, (3, 6)), "sys.set_coroutine_origin_tracking_depth": (None, (3, 7)), "sys.set_int_max_str_digits": (None, (3, 11)), + "sys.set_lazy_imports": (None, (3, 15)), + "sys.set_lazy_imports_filter": (None, (3, 15)), "sys.setdefaultencoding": ((2, 0), None), "sys.setdlopenflags": ((2, 2), (3, 0)), "sys.setswitchinterval": (None, (3, 2)), @@ -1781,6 +1812,7 @@ def MOD_MEM_REQS(config): "threading.RLock.locked": (None, (3, 14)), "threading.Thread.is_alive": ((2, 6), (3, 0)), "threading.active_count": ((2, 6), (3, 0)), + "threading.concurrent_tee": (None, (3, 15)), "threading.current_thread": ((2, 6), (3, 0)), "threading.excepthook": (None, (3, 8)), "threading.get_ident": (None, (3, 3)), @@ -1788,11 +1820,13 @@ def MOD_MEM_REQS(config): "threading.getprofile": (None, (3, 10)), "threading.gettrace": (None, (3, 10)), "threading.main_thread": (None, (3, 4)), + "threading.serialize_iterator": (None, (3, 15)), "threading.setprofile": ((2, 3), (3, 0)), "threading.setprofile_all_threads": (None, (3, 12)), "threading.settrace": ((2, 3), (3, 0)), "threading.settrace_all_threads": (None, (3, 12)), "threading.stack_size": ((2, 5), (3, 0)), + "threading.synchronized_iterator": (None, (3, 15)), "time.clock_getres": (None, (3, 3)), "time.clock_gettime": (None, (3, 3)), "time.clock_gettime_ns": (None, (3, 7)), @@ -1813,6 +1847,9 @@ def MOD_MEM_REQS(config): "timeit.Timer.autorange": (None, (3, 6)), "timeit.repeat": ((2, 6), (3, 0)), "timeit.timeit": ((2, 6), (3, 0)), + "tkinter.Event.detail": (None, (3, 15)), + "tkinter.Event.user_data": (None, (3, 15)), + "tkinter.Text.search_all": (None, (3, 15)), "tkinter.info_patchlevel": (None, (3, 11)), "tokenize.generate_tokens": ((2, 2), (3, 0)), "tokenize.open": (None, (3, 2)), @@ -1835,6 +1872,8 @@ def MOD_MEM_REQS(config): "types.get_original_bases": (None, (3, 12)), "types.new_class": (None, (3, 3)), "types.prepare_class": (None, (3, 3)), + "types.FrameLocalsProxyType": (None, (3, 15)), + "types.LazyImportType": (None, (3, 15)), "types.resolve_bases": (None, (3, 7)), "typing.ParamSpec.evaluate_default": (None, (3, 14)), "typing.ParamSpec.has_default": (None, (3, 13)), @@ -1856,8 +1895,15 @@ def MOD_MEM_REQS(config): "typing.is_protocol": (None, (3, 13)), "typing.is_typeddict": (None, (3, 10)), "typing.reveal_type": (None, (3, 11)), + "unicodedata.block": (None, (3, 15)), "unicodedata.east_asian_width": ((2, 4), (3, 0)), + "unicodedata.extended_pictographic": (None, (3, 15)), + "unicodedata.grapheme_cluster_break": (None, (3, 15)), + "unicodedata.indic_conjunct_break": (None, (3, 15)), + "unicodedata.iter_graphemes": (None, (3, 15)), "unicodedata.is_normalized": (None, (3, 8)), + "unicodedata.isxidcontinue": (None, (3, 15)), + "unicodedata.isxidstart": (None, (3, 15)), "unicodedata.normalize": ((2, 3), (3, 0)), "unittest.IsolatedAsyncioTestCase.enterAsyncContext": (None, (3, 11)), "unittest.TestCase.addClassCleanup": (None, (3, 8)), @@ -1949,6 +1995,9 @@ def MOD_MEM_REQS(config): "weakref.WeakKeyDictionary.keyrefs": ((2, 5), (3, 0)), "weakref.WeakValueDictionary.itervaluerefs": ((2, 5), None), "weakref.WeakValueDictionary.valuerefs": ((2, 5), (3, 0)), + "wave.Wave_read.getformat": (None, (3, 15)), + "wave.Wave_write.getformat": (None, (3, 15)), + "wave.Wave_write.setformat": (None, (3, 15)), "webbrowser.controller.open_new_tab": ((2, 5), (3, 0)), "webbrowser.open_new_tab": ((2, 5), (3, 0)), "winreg.CreateKeyEx": (None, (3, 2)), @@ -1978,6 +2027,10 @@ def MOD_MEM_REQS(config): "xml.parsers.expat.xmlparser.GetInputContext": ((2, 1), (3, 0)), "xml.parsers.expat.xmlparser.SetAllocTrackerActivationThreshold": (None, (3, 14)), "xml.parsers.expat.xmlparser.SetAllocTrackerMaximumAmplification": (None, (3, 14)), + "xml.parsers.expat.xmlparser.SetBillionLaughsAttackProtection" + "ActivationThreshold": (None, (3, 15)), + "xml.parsers.expat.xmlparser.SetBillionLaughsAttackProtection" + "MaximumAmplification": (None, (3, 15)), "xml.parsers.expat.xmlparser.UseForeignDTD": ((2, 3), (3, 0)), "xml.parsers.expat.xmlparser.XmlDeclHandler": ((2, 1), (3, 0)), "xml.parsers.expat.xmlparser.buffer_size": ((2, 3), (3, 0)), @@ -1987,6 +2040,8 @@ def MOD_MEM_REQS(config): "xml.parsers.expat.xmlparser.specified_attributes": ((2, 1), (3, 0)), "xml.sax.saxutils.quoteattr": ((2, 2), (3, 0)), "xml.sax.saxutils.unescape": ((2, 3), (3, 0)), + "xml.is_valid_name": (None, (3, 15)), + "xml.is_valid_text": (None, (3, 15)), "xmlrpc.server.SimpleXMLRPCServer.register_introspection_functions": (None, (3, 0)), "zipfile.ZipFile.extract": ((2, 6), (3, 0)), "zipfile.ZipFile.extractall": ((2, 6), (3, 0)), @@ -2001,7 +2056,9 @@ def MOD_MEM_REQS(config): "zipimport.zipimporter.find_spec": (None, (3, 10)), "zipimport.zipimporter.get_filename": ((2, 7), (3, 1)), "zipimport.zipimporter.invalidate_caches": (None, (3, 10)), + "zlib.adler32_combine": (None, (3, 15)), "zlib.Compress.copy": ((2, 5), (3, 0)), + "zlib.crc32_combine": (None, (3, 15)), "zlib.Decompress.copy": ((2, 5), (3, 0)), # Variables and Constants @@ -2483,6 +2540,11 @@ def MOD_MEM_REQS(config): "resource.RLIMIT_SBSIZE": (None, (3, 4)), "resource.RLIMIT_SIGPENDING": (None, (3, 4)), "resource.RLIMIT_SWAP": (None, (3, 4)), + "resource.RLIMIT_NTHR": (None, (3, 15)), + "resource.RLIMIT_THREADS": (None, (3, 15)), + "resource.RLIMIT_UMTXP": (None, (3, 15)), + "resource.RLIM_SAVED_CUR": (None, (3, 15)), + "resource.RLIM_SAVED_MAX": (None, (3, 15)), "resource.RUSAGE_THREAD": (None, (3, 2)), "resource.getrusage.ru_idrss": ((2, 3), (3, 0)), "resource.getrusage.ru_inblock": ((2, 3), (3, 0)), @@ -2787,6 +2849,7 @@ def MOD_MEM_REQS(config): "ssl.HAS_NPN": ((2, 7), (3, 3)), "ssl.HAS_PHA": (None, (3, 14)), "ssl.HAS_PSK": (None, (3, 13)), + "ssl.HAS_PSK_TLS13": (None, (3, 15)), "ssl.HAS_SNI": ((2, 7), (3, 2)), "ssl.HAS_SSLv2": (None, (3, 7)), "ssl.HAS_SSLv3": (None, (3, 7)), @@ -3133,6 +3196,7 @@ def MOD_MEM_REQS(config): "functools.total_ordering": ((2, 7), (3, 2)), "reprlib.recursive_repr": (None, (3, 2)), "typing.dataclass_transform": (None, (3, 11)), + "typing.disjoint_base": (None, (3, 15)), "typing.final": (None, (3, 8)) if not bp("typing", config) else ((2, 7), (3, 8)), "typing.override": (None, (3, 12)), "typing.runtime_checkable": (None, (3, 8)) if not bp("typing", config) else ((2, 7), (3, 8)), From 495ce2531081747be119e5fbf765636156de5eb0 Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sun, 17 May 2026 14:50:16 +0200 Subject: [PATCH 3/8] [3.15] Add new kwargs --- tests/kwargs.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++ vermin/rules.py | 20 ++++++++ 2 files changed, 144 insertions(+) diff --git a/tests/kwargs.py b/tests/kwargs.py index f52d401b..0b9b5808 100644 --- a/tests/kwargs.py +++ b/tests/kwargs.py @@ -4542,6 +4542,130 @@ def test_key_of_bisect_insort_right(self): self.assertOnlyIn((3, 10), self.detect(""" import bisect bisect.insort_right(key='x') +""")) + + def test_max_threads_of_dump_traceback_from_faulthandler(self): + self.assertOnlyIn((3, 15), self.detect(""" +from faulthandler import dump_traceback +dump_traceback(max_threads=None) +""")) + + def test_max_threads_of_dump_traceback_later_from_faulthandler(self): + self.assertOnlyIn((3, 15), self.detect(""" +from faulthandler import dump_traceback_later +dump_traceback_later(max_threads=None) +""")) + + def test_max_threads_of_enable_from_faulthandler(self): + self.assertOnlyIn((3, 15), self.detect(""" +from faulthandler import enable +enable(max_threads=None) +""")) + + def test_max_threads_of_register_from_faulthandler(self): + self.assertOnlyIn((3, 15), self.detect(""" +from faulthandler import register +register(max_threads=None) +""")) + + def test_max_response_headers_of_HTTPConnection_from_http_client(self): + self.assertOnlyIn((3, 15), self.detect(""" +from http.client import HTTPConnection +HTTPConnection(max_response_headers=None) +""")) + + def test_max_response_headers_of_HTTPSConnection_from_http_client(self): + self.assertOnlyIn((3, 15), self.detect(""" +from http.client import HTTPSConnection +HTTPSConnection(max_response_headers=None) +""")) + + def test_extra_response_headers_of_SimpleHTTPRequestHandler_from_http_server(self): + self.assertOnlyIn((3, 15), self.detect(""" +from http.server import SimpleHTTPRequestHandler +SimpleHTTPRequestHandler(extra_response_headers=None) +""")) + + def test_fallback_to_class_doc_of_getdoc_from_inspect(self): + self.assertOnlyIn((3, 15), self.detect(""" +from inspect import getdoc +getdoc(fallback_to_class_doc=None) +""")) + + def test_inherit_class_doc_of_getdoc_from_inspect(self): + self.assertOnlyIn((3, 15), self.detect(""" +from inspect import getdoc +getdoc(inherit_class_doc=None) +""")) + + def test_array_hook_of_load_from_json(self): + self.assertOnlyIn((3, 15), self.detect(""" +from json import load +load(array_hook=None) +""")) + + def test_array_hook_of_loads_from_json(self): + self.assertOnlyIn((3, 15), self.detect(""" +from json import loads +loads(array_hook=None) +""")) + + def test_target_time_of_autorange_from_timeit_Timer(self): + self.assertOnlyIn((3, 15), self.detect(""" +from timeit import Timer +x = Timer() +x.autorange(target_time=None) +""")) + + def test_nolinestop_of_search_from_tkinter_Text(self): + self.assertOnlyIn((3, 15), self.detect(""" +from tkinter import Text +x = Text() +x.search(nolinestop=None) +""")) + + def test_strictlimits_of_search_from_tkinter_Text(self): + self.assertOnlyIn((3, 15), self.detect(""" +from tkinter import Text +x = Text() +x.search(strictlimits=None) +""")) + + def test_formatter_of_assertLogs_from_unittest_TestCase(self): + self.assertOnlyIn((3, 15), self.detect(""" +from unittest import TestCase +x = TestCase() +x.assertLogs(formatter=None) +""")) + + def test_missing_as_none_of_urldefrag_from_urllib_parse(self): + self.assertOnlyIn((3, 15), self.detect(""" +from urllib.parse import urldefrag +urldefrag(missing_as_none=None) +""")) + + def test_missing_as_none_of_urlparse_from_urllib_parse(self): + self.assertOnlyIn((3, 15), self.detect(""" +from urllib.parse import urlparse +urlparse(missing_as_none=None) +""")) + + def test_missing_as_none_of_urlsplit_from_urllib_parse(self): + self.assertOnlyIn((3, 15), self.detect(""" +from urllib.parse import urlsplit +urlsplit(missing_as_none=None) +""")) + + def test_keep_empty_of_urlunparse_from_urllib_parse(self): + self.assertOnlyIn((3, 15), self.detect(""" +from urllib.parse import urlunparse +urlunparse(keep_empty=None) +""")) + + def test_keep_empty_of_urlunsplit_from_urllib_parse(self): + self.assertOnlyIn((3, 15), self.detect(""" +from urllib.parse import urlunsplit +urlunsplit(keep_empty=None) """)) def test_kw_only_of_dataclasses_dataclass(self): diff --git a/vermin/rules.py b/vermin/rules.py index dedf7971..ecc6da7b 100644 --- a/vermin/rules.py +++ b/vermin/rules.py @@ -3511,6 +3511,10 @@ def KWARGS_REQS(config): ("exec", "locals"): (None, (3, 13)), ("eval", "globals"): (None, (3, 13)), ("eval", "locals"): (None, (3, 13)), + ("faulthandler.dump_traceback", "max_threads"): (None, (3, 15)), + ("faulthandler.dump_traceback_later", "max_threads"): (None, (3, 15)), + ("faulthandler.enable", "max_threads"): (None, (3, 15)), + ("faulthandler.register", "max_threads"): (None, (3, 15)), ("fileinput.FileInput", "encoding"): (None, (3, 10)), ("fileinput.FileInput", "errors"): (None, (3, 10)), ("fileinput.FileInput", "mode"): ((2, 5), (3, 0)), @@ -3578,14 +3582,17 @@ def KWARGS_REQS(config): ("heapq.nsmallest", "key"): ((2, 5), (3, 0)), ("html.parser.HTMLParser", "convert_charrefs"): (None, (3, 4)), ("http.client.HTTPConnection", "blocksize"): (None, (3, 7)), + ("http.client.HTTPConnection", "max_response_headers"): (None, (3, 15)), ("http.client.HTTPConnection", "source_address"): (None, (3, 2)), ("http.client.HTTPConnection.endheaders", "encode_chunked"): (None, (3, 6)), ("http.client.HTTPConnection.request", "encode_chunked"): (None, (3, 6)), ("http.client.HTTPSConnection", "check_hostname"): (None, (3, 2)), ("http.client.HTTPSConnection", "context"): (None, (3, 2)), + ("http.client.HTTPSConnection", "max_response_headers"): (None, (3, 15)), ("http.client.HTTPSConnection", "source_address"): (None, (3, 2)), ("http.server.BaseHTTPRequestHandler.send_error", "explain"): (None, (3, 4)), ("http.server.SimpleHTTPRequestHandler", "directory"): (None, (3, 7)), + ("http.server.SimpleHTTPRequestHandler", "extra_response_headers"): (None, (3, 15)), ("httplib.HTTPConnection", "source_address"): ((2, 7), None), ("httplib.HTTPConnection", "timeout"): ((2, 6), None), ("httplib.HTTPConnection.endheaders", "message_body"): ((2, 7), None), @@ -3601,6 +3608,8 @@ def KWARGS_REQS(config): ("inspect.Signature.format", "unquote_annotations"): (None, (3, 14)), ("inspect.Signature.from_callable", "globals"): (None, (3, 10)), ("inspect.Signature.from_callable", "locals"): (None, (3, 10)), + ("inspect.getdoc", "fallback_to_class_doc"): (None, (3, 15)), + ("inspect.getdoc", "inherit_class_doc"): (None, (3, 15)), ("inspect.signature", "annotation_format"): (None, (3, 14)), ("inspect.signature", "eval_str"): (None, (3, 10)), ("inspect.signature", "follow_wrapped"): (None, (3, 5)), @@ -3615,7 +3624,9 @@ def KWARGS_REQS(config): ("itertools.batched", "strict"): (None, (3, 13)), ("itertools.count", "step"): ((2, 7), (3, 1)), ("json.JSONDecoder", "object_pairs_hook"): ((2, 7), (3, 1)), + ("json.load", "array_hook"): (None, (3, 15)), ("json.load", "object_pairs_hook"): ((2, 7), (3, 1)), + ("json.loads", "array_hook"): (None, (3, 15)), ("linecache.getline", "module_globals"): ((2, 5), (3, 0)), ("locale.format", "monetary"): ((2, 5), (3, 0)), ("locale.format_string", "monetary"): (None, (3, 7)), @@ -4028,7 +4039,10 @@ def KWARGS_REQS(config): ("threading.Semaphore.release", "n"): (None, (3, 9)), ("threading.Thread", "context"): (None, (3, 14)), ("threading.Thread", "daemon"): (None, (3, 3)), + ("tkinter.Text.search", "nolinestop"): (None, (3, 15)), + ("tkinter.Text.search", "strictlimits"): (None, (3, 15)), ("timeit.Timer", "globals"): (None, (3, 5)), + ("timeit.Timer.autorange", "target_time"): (None, (3, 15)), ("timeit.repeat", "globals"): (None, (3, 5)), ("timeit.timeit", "globals"): (None, (3, 5)), ("tkinter.font.nametofont", "root"): (None, (3, 10)), @@ -4037,6 +4051,7 @@ def KWARGS_REQS(config): ("tracemalloc.Filter", "domain"): (None, (3, 6)), ("typing.get_type_hints", "include_extras"): (None, (3, 9)), ("unittest.TestCase.assertAlmostEqual", "delta"): ((2, 7), (3, 2)), + ("unittest.TestCase.assertLogs", "formatter"): (None, (3, 15)), ("unittest.TestCase.assertNotAlmostEqual", "delta"): ((2, 7), (3, 2)), ("unittest.TestCase.assertRaises", "msg"): (None, (3, 3)), ("unittest.TestCase.assertRaisesRegex", "msg"): (None, (3, 3)), @@ -4067,7 +4082,12 @@ def KWARGS_REQS(config): ("urllib.parse.parse_qsl", "max_num_fields"): (None, (3, 6)), # Security fix backported to 3.6.13+, 3.7.10+, 3.8.8+, 3.9.2+, 3.10+ ("urllib.parse.parse_qsl", "separator"): (None, (3, 6)), + ("urllib.parse.urldefrag", "missing_as_none"): (None, (3, 15)), ("urllib.parse.urlencode", "quote_via"): (None, (3, 5)), + ("urllib.parse.urlparse", "missing_as_none"): (None, (3, 15)), + ("urllib.parse.urlsplit", "missing_as_none"): (None, (3, 15)), + ("urllib.parse.urlunparse", "keep_empty"): (None, (3, 15)), + ("urllib.parse.urlunsplit", "keep_empty"): (None, (3, 15)), ("urllib.request.HTTPSHandler", "check_hostname"): (None, (3, 2)), ("urllib.request.HTTPSHandler", "context"): (None, (3, 2)), ("urllib.request.Request", "method"): (None, (3, 3)), From 64177cdee4892e2eae921a1cc75d47ee1f10ccca Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sun, 17 May 2026 15:24:03 +0200 Subject: [PATCH 4/8] [3.15] Detect PEP 798 unpacking in comprehension values --- README.rst | 31 ++++++++++++++++--------------- tests/lang.py | 13 +++++++++++++ vermin/source_state.py | 6 ++++++ vermin/source_visitor.py | 18 ++++++++++++++++++ 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index f7591428..ca1b9ffb 100644 --- a/README.rst +++ b/README.rst @@ -141,21 +141,22 @@ Features detected include v2/v3 ``print expr`` and ``print(expr)``, ``long``, f- asynchronous comprehensions, ``await`` in comprehensions, asynchronous ``for``-loops, boolean constants, named expressions, keyword-only parameters, positional-only parameters, ``nonlocal``, ``yield from``, exception context cause (``raise .. from ..``), ``except*``, ``set`` literals, -``set`` comprehensions, ``dict`` comprehensions, infix matrix multiplication, ``"..".format(..)``, -imports (``import X``, ``from X import Y``, ``from X import *``), function calls wrt. name and -kwargs, ``strftime`` + ``strptime`` directives used, function and variable annotations (also -``Final`` and ``Literal``), ``continue`` in ``finally`` block, modular inverse ``pow()``, array -typecodes, codecs error handler names, encodings, ``%`` formatting and directives for bytes and -bytearray, ``with`` statement, asynchronous ``with`` statement, multiple context expressions in a -``with`` statement, multiple context expressions in a ``with`` statement grouped with parenthesis, -unpacking assignment, generalized unpacking, ellipsis literal (``...``) out of slices, dictionary -union (``{..} | {..}``), dictionary union merge (``a = {..}; a |= {..}``), builtin generic type -annotations (``list[str]``), function decorators, class decorators, relaxed decorators, -``metaclass`` class keyword, pattern matching with ``match``, union types written as ``X | Y``, type -alias statements (``type X = SomeType``), type alias statements with lambdas/comprehensions in class -scopes, generic classes (``class C[T]: ...``), and template string literals (``t'{var}'``). It tries -to detect and ignore user-defined functions, classes, arguments, and variables with names that clash -with library-defined symbols. +``set`` comprehensions, ``dict`` comprehensions, unpacking in comprehension value expressions (``[*x +for x in range(10)]``), infix matrix multiplication, ``"..".format(..)``, imports (``import X``, +``from X import Y``, ``from X import *``), function calls wrt. name and kwargs, ``strftime`` + +``strptime`` directives used, function and variable annotations (also ``Final`` and ``Literal``), +``continue`` in ``finally`` block, modular inverse ``pow()``, array typecodes, codecs error handler +names, encodings, ``%`` formatting and directives for bytes and bytearray, ``with`` statement, +asynchronous ``with`` statement, multiple context expressions in a ``with`` statement, multiple +context expressions in a ``with`` statement grouped with parenthesis, unpacking assignment, +generalized unpacking, ellipsis literal (``...``) out of slices, dictionary union (``{..} | +{..}``), dictionary union merge (``a = {..}; a |= {..}``), builtin generic type annotations +(``list[str]``), function decorators, class decorators, relaxed decorators, ``metaclass`` class +keyword, pattern matching with ``match``, union types written as ``X | Y``, type alias statements +(``type X = SomeType``), type alias statements with lambdas/comprehensions in class scopes, generic +classes (``class C[T]: ...``), and template string literals (``t'{var}'``). It tries to detect and +ignore user-defined functions, classes, arguments, and variables with names that clash with +library-defined symbols. Caveats ======= diff --git a/tests/lang.py b/tests/lang.py index 079c1515..22df3670 100644 --- a/tests/lang.py +++ b/tests/lang.py @@ -1173,6 +1173,19 @@ def lower_upper(template): visitor = self.visit("f'hello'") self.assertFalse(visitor.template_string_literal()) + @VerminTest.skipUnlessVersion(3, 15) + def test_unpacking_in_comprehension(self): + visitor = self.visit("[*x for x in range(10)]") + self.assertTrue(visitor.unpacking_in_comprehension()) + self.assertOnlyIn((3, 15), visitor.minimum_versions()) + + visitor = self.visit("{*x for x in range(10)}") + self.assertTrue(visitor.unpacking_in_comprehension()) + self.assertOnlyIn((3, 15), visitor.minimum_versions()) + + visitor = self.visit("[x for x in range(10)]") + self.assertFalse(visitor.unpacking_in_comprehension()) + @VerminTest.skipUnlessVersion(3, 5) def test_bytes_format(self): visitor = self.visit("b'%x' % 10") diff --git a/vermin/source_state.py b/vermin/source_state.py index 4a848971..fe3fe3e5 100644 --- a/vermin/source_state.py +++ b/vermin/source_state.py @@ -127,6 +127,12 @@ def __init__(self, config, path=None, source=None): # `t'text {var}'` self.template_string_literal = False + # Track depth of nested comprehension value expressions (PEP 798). + self.comprehension_depth = 0 + + # `*x` unpacking in comprehension value expressions (PEP 798). + self.unpacking_in_comprehension = False + # Imported members of modules, like "exc_clear" of "sys". self.import_mem_mod = {} diff --git a/vermin/source_visitor.py b/vermin/source_visitor.py index 09594b0c..e9502d92 100644 --- a/vermin/source_visitor.py +++ b/vermin/source_visitor.py @@ -276,6 +276,9 @@ def metaclass_class_keyword(self): def generic_class(self): return self.__s.generic_class + def unpacking_in_comprehension(self): + return self.__s.unpacking_in_comprehension + def __get_source_line(self, line, col=0): if self.__s.source is None: return None # pragma: no cover @@ -529,6 +532,9 @@ def minimum_versions(self): if self.generic_class(): mins = self.__add_versions_entity(mins, (None, (3, 12)), "generic class `class C[T]: ...`") + if self.unpacking_in_comprehension(): + mins = self.__add_versions_entity(mins, (None, (3, 15)), + "unpacking in comprehension (PEP 798)") for directive in self.strftime_directives(): if directive in STRFTIME_REQS: vers = STRFTIME_REQS[directive] @@ -1080,6 +1086,10 @@ def visit_Starred(self, node): if isinstance(node.ctx, ast.Store): self.__s.unpacking_assignment = True self.__vvprint("unpacking assignment", versions=[None, (3, 0)]) + elif self.__s.comprehension_depth > 0 and isinstance(node.ctx, ast.Load): + # PEP 798: starred expression in comprehension value expression (3.15+). + self.__s.unpacking_in_comprehension = True + self.__vvprint("unpacking in comprehension (PEP 798)", versions=[None, (3, 15)]) self.generic_visit(node) def __check_generalized_unpacking(self, node): @@ -2062,30 +2072,38 @@ def __handle_comprehensions(self, comps): return user_defs_copy def visit_ListComp(self, node): + self.__s.comprehension_depth += 1 user_defs_copy = self.__handle_comprehensions(node.generators) self.generic_visit(node) self.__s.user_defs = user_defs_copy + self.__s.comprehension_depth -= 1 def visit_SetComp(self, node): + self.__s.comprehension_depth += 1 self.__s.set_comp = True self.__vvprint("set comprehensions", versions=[(2, 7), (3, 0)]) user_defs_copy = self.__handle_comprehensions(node.generators) self.generic_visit(node) self.__s.user_defs = user_defs_copy + self.__s.comprehension_depth -= 1 def visit_GeneratorExp(self, node): + self.__s.comprehension_depth += 1 user_defs_copy = self.__handle_comprehensions(node.generators) self.generic_visit(node) self.__s.user_defs = user_defs_copy + self.__s.comprehension_depth -= 1 def visit_DictComp(self, node): + self.__s.comprehension_depth += 1 self.__s.dict_comp = True self.__vvprint("dict comprehensions", versions=[(2, 7), (3, 0)]) user_defs_copy = self.__handle_comprehensions(node.generators) self.generic_visit(node) self.__s.user_defs = user_defs_copy + self.__s.comprehension_depth -= 1 def visit_comprehension(self, node): if hasattr(node, "is_async") and node.is_async == 1: From 14aa5572686a9cd8b057496aaec03c319bf06bdb Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sun, 17 May 2026 15:33:44 +0200 Subject: [PATCH 5/8] [3.15] Detect PEP 810 lazy imports --- README.rst | 7 ++++--- tests/lang.py | 20 ++++++++++++++++++++ vermin/source_state.py | 5 +++++ vermin/source_visitor.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index ca1b9ffb..d02ae325 100644 --- a/README.rst +++ b/README.rst @@ -154,9 +154,10 @@ generalized unpacking, ellipsis literal (``...``) out of slices, dictionary unio (``list[str]``), function decorators, class decorators, relaxed decorators, ``metaclass`` class keyword, pattern matching with ``match``, union types written as ``X | Y``, type alias statements (``type X = SomeType``), type alias statements with lambdas/comprehensions in class scopes, generic -classes (``class C[T]: ...``), and template string literals (``t'{var}'``). It tries to detect and -ignore user-defined functions, classes, arguments, and variables with names that clash with -library-defined symbols. +classes (``class C[T]: ...``), template string literals (``t'{var}'``), lazy imports (``lazy +import``, ``lazy from ... import``), and special module attributes like ``__lazy_modules__``. It +tries to detect and ignore user-defined functions, classes, arguments, and variables with names that +clash with library-defined symbols. Caveats ======= diff --git a/tests/lang.py b/tests/lang.py index 22df3670..0df3d6b9 100644 --- a/tests/lang.py +++ b/tests/lang.py @@ -1186,6 +1186,26 @@ def test_unpacking_in_comprehension(self): visitor = self.visit("[x for x in range(10)]") self.assertFalse(visitor.unpacking_in_comprehension()) + @VerminTest.skipUnlessVersion(3, 15) + def test_lazy_imports(self): + visitor = self.visit("lazy import foo") + self.assertTrue(visitor.lazy_imports()) + self.assertOnlyIn((3, 15), visitor.minimum_versions()) + + visitor = self.visit("lazy from bar import foo") + self.assertTrue(visitor.lazy_imports()) + self.assertOnlyIn((3, 15), visitor.minimum_versions()) + + visitor = self.visit("import foo") + self.assertFalse(visitor.lazy_imports()) + + def test_lazy_modules(self): + visitor = self.visit("__lazy_modules__ = {}") + self.assertTrue(visitor.lazy_modules()) + self.assertOnlyIn((3, 15), visitor.minimum_versions()) + + visitor = self.visit("__all__ = []") + self.assertFalse(visitor.lazy_modules()) @VerminTest.skipUnlessVersion(3, 5) def test_bytes_format(self): visitor = self.visit("b'%x' % 10") diff --git a/vermin/source_state.py b/vermin/source_state.py index fe3fe3e5..1c196130 100644 --- a/vermin/source_state.py +++ b/vermin/source_state.py @@ -133,6 +133,11 @@ def __init__(self, config, path=None, source=None): # `*x` unpacking in comprehension value expressions (PEP 798). self.unpacking_in_comprehension = False + # `lazy import` statement (PEP 810). + self.lazy_imports = False + + # `__lazy_modules__` module attribute. + self.lazy_modules = False # Imported members of modules, like "exc_clear" of "sys". self.import_mem_mod = {} diff --git a/vermin/source_visitor.py b/vermin/source_visitor.py index e9502d92..f697c111 100644 --- a/vermin/source_visitor.py +++ b/vermin/source_visitor.py @@ -279,6 +279,12 @@ def generic_class(self): def unpacking_in_comprehension(self): return self.__s.unpacking_in_comprehension + def lazy_imports(self): + return self.__s.lazy_imports + + def lazy_modules(self): + return self.__s.lazy_modules + def __get_source_line(self, line, col=0): if self.__s.source is None: return None # pragma: no cover @@ -535,6 +541,15 @@ def minimum_versions(self): if self.unpacking_in_comprehension(): mins = self.__add_versions_entity(mins, (None, (3, 15)), "unpacking in comprehension (PEP 798)") + + if self.lazy_imports(): + mins = self.__add_versions_entity(mins, (None, (3, 15)), + "lazy imports (PEP 810)") + + if self.lazy_modules(): + mins = self.__add_versions_entity(mins, (None, (3, 15)), + "`__lazy_modules__`") + for directive in self.strftime_directives(): if directive in STRFTIME_REQS: vers = STRFTIME_REQS[directive] @@ -1021,6 +1036,12 @@ def visit_Import(self, node): if self.__is_no_line(node.lineno): return + # PEP 810: `lazy import` (3.15+). + if hasattr(node, "is_lazy") and node.is_lazy: + self.__s.lazy_imports = True + self.__vvprint("lazy imports (PEP 810)", line=node.lineno, + versions=[None, (3, 15)]) + for name in node.names: line = node.lineno col = node.col_offset + 7 # "import" = 6 + 1 @@ -1042,6 +1063,12 @@ def visit_ImportFrom(self, node): if self.__is_no_line(node.lineno): return + # PEP 810: `lazy from ... import` (3.15+). + if hasattr(node, "is_lazy") and node.is_lazy: + self.__s.lazy_imports = True + self.__vvprint("lazy imports (PEP 810)", line=node.lineno, + versions=[None, (3, 15)]) + from_col = 5 # "from" = 4 + 1 self.__add_module(node.module, node.lineno, node.col_offset + from_col) @@ -1075,6 +1102,10 @@ def visit_Name(self, node): elif node.id in ("True", "False"): self.__s.bool_const = True self.__vvvprint("True/False constant", versions=[(2, 3), (3, 0)]) + elif node.id == "__lazy_modules__": + self.__s.lazy_modules = True + self.__vvprint("`__lazy_modules__`", line=node.lineno, + versions=[None, (3, 15)]) def visit_Print(self, node): # pragma: no cover self.__s.printv2 = True From d43f8657143bf94ba03aae4007242e184941a26f Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sun, 17 May 2026 15:37:52 +0200 Subject: [PATCH 6/8] [3.15] Detect slice type subscription support --- README.rst | 6 +++--- tests/lang.py | 9 +++++++++ vermin/source_state.py | 3 +++ vermin/source_visitor.py | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d02ae325..f370b1cc 100644 --- a/README.rst +++ b/README.rst @@ -155,9 +155,9 @@ generalized unpacking, ellipsis literal (``...``) out of slices, dictionary unio keyword, pattern matching with ``match``, union types written as ``X | Y``, type alias statements (``type X = SomeType``), type alias statements with lambdas/comprehensions in class scopes, generic classes (``class C[T]: ...``), template string literals (``t'{var}'``), lazy imports (``lazy -import``, ``lazy from ... import``), and special module attributes like ``__lazy_modules__``. It -tries to detect and ignore user-defined functions, classes, arguments, and variables with names that -clash with library-defined symbols. +import``, ``lazy from ... import``), special module attributes like ``__lazy_modules__``, and slice +type subscription (``slice[1:2:3]``). It tries to detect and ignore user-defined functions, classes, +arguments, and variables with names that clash with library-defined symbols. Caveats ======= diff --git a/tests/lang.py b/tests/lang.py index 0df3d6b9..8d574ef4 100644 --- a/tests/lang.py +++ b/tests/lang.py @@ -1206,6 +1206,15 @@ def test_lazy_modules(self): visitor = self.visit("__all__ = []") self.assertFalse(visitor.lazy_modules()) + + def test_slice_subscription(self): + visitor = self.visit("x = slice[1:2:3]") + self.assertTrue(visitor.slice_subscription()) + self.assertOnlyIn((3, 15), visitor.minimum_versions()) + + visitor = self.visit("x = slice(1, 2, 3)") + self.assertFalse(visitor.slice_subscription()) + @VerminTest.skipUnlessVersion(3, 5) def test_bytes_format(self): visitor = self.visit("b'%x' % 10") diff --git a/vermin/source_state.py b/vermin/source_state.py index 1c196130..9bce4a5a 100644 --- a/vermin/source_state.py +++ b/vermin/source_state.py @@ -138,6 +138,9 @@ def __init__(self, config, path=None, source=None): # `__lazy_modules__` module attribute. self.lazy_modules = False + + # `slice` type subscript support, e.g. `slice[1:2:3]`. + self.slice_subscription = False # Imported members of modules, like "exc_clear" of "sys". self.import_mem_mod = {} diff --git a/vermin/source_visitor.py b/vermin/source_visitor.py index f697c111..99cb4adf 100644 --- a/vermin/source_visitor.py +++ b/vermin/source_visitor.py @@ -285,6 +285,9 @@ def lazy_imports(self): def lazy_modules(self): return self.__s.lazy_modules + def slice_subscription(self): + return self.__s.slice_subscription + def __get_source_line(self, line, col=0): if self.__s.source is None: return None # pragma: no cover @@ -550,6 +553,10 @@ def minimum_versions(self): mins = self.__add_versions_entity(mins, (None, (3, 15)), "`__lazy_modules__`") + if self.slice_subscription(): + mins = self.__add_versions_entity(mins, (None, (3, 15)), + "slice subscription") + for directive in self.strftime_directives(): if directive in STRFTIME_REQS: vers = STRFTIME_REQS[directive] @@ -2291,6 +2298,13 @@ def match(name): if is_ellipsis_node(n): self.__s.ellipsis_nodes_in_slices.add(n) + # `slice[...]` subscript support (3.15+). + if isinstance(node.value, ast.Name) and node.value.id == "slice" \ + and "slice" not in self.__s.user_defs: + self.__s.slice_subscription = True + self.__vvprint("slice subscription", line=node.lineno, + versions=[None, (3, 15)]) + self.generic_visit(node) def visit_Match(self, node): From 0a2e516824c1aa974aebadfac799b2eaaccc7797 Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sun, 17 May 2026 18:27:56 +0200 Subject: [PATCH 7/8] Add missing rules --- tests/class.py | 24 ++++++++++++++++++++++++ tests/constants.py | 35 +++++++++++++++++++++++++++++++++++ tests/exception.py | 3 +++ tests/function.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ vermin/rules.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+) diff --git a/tests/class.py b/tests/class.py index c2efb9fc..d7f5282c 100644 --- a/tests/class.py +++ b/tests/class.py @@ -10,6 +10,9 @@ def test_RawConfigParser_of_ConfigParser(self): def test_SafeConfigParser_of_ConfigParser(self): self.assertOnlyIn((2, 3), self.detect("from ConfigParser import SafeConfigParser")) + def test_ExtendedInterpolation_of_configparser(self): + self.assertOnlyIn((3, 2), self.detect("from configparser import ExtendedInterpolation")) + def test_LifoQueue_of_Queue(self): self.assertOnlyIn((2, 6), self.detect("from Queue import LifoQueue")) @@ -19,9 +22,15 @@ def test_PriorityQueue_of_Queue(self): def test_ABC_of_abc(self): self.assertOnlyIn((3, 4), self.detect("from abc import ABC")) + def test_Format_of_annotationlib(self): + self.assertOnlyIn((3, 14), self.detect("from annotationlib import Format")) + def test_BooleanOptionalAction_of_argparse(self): self.assertOnlyIn((3, 9), self.detect("from argparse import BooleanOptionalAction")) + def test_Constant_of_ast(self): + self.assertOnlyIn((3, 6), self.detect("from ast import Constant")) + def test_Barrier_of_asyncio(self): self.assertOnlyIn((3, 11), self.detect("from asyncio import Barrier")) @@ -428,6 +437,12 @@ def test_Formatter_of_string(self): def test_Template_of_string(self): self.assertOnlyIn(((2, 4), (3, 0)), self.detect("from string import Template")) + def test_Interpolation_of_string_templatelib(self): + self.assertOnlyIn((3, 14), self.detect("from string.templatelib import Interpolation")) + + def test_Template_of_string_templatelib(self): + self.assertOnlyIn((3, 14), self.detect("from string.templatelib import Template")) + def test_Struct_of_struct(self): self.assertOnlyIn(((2, 5), (3, 0)), self.detect("from struct import Struct")) @@ -461,6 +476,9 @@ def test_Barrier_of_threading(self): def test_local_of_threading(self): self.assertOnlyIn(((2, 4), (3, 0)), self.detect("from threading import local")) + def test_ttk_of_tkinter(self): + self.assertOnlyIn((3, 1), self.detect("from tkinter import ttk")) + def test_struct_time_of_time(self): self.assertOnlyIn(((2, 2), (3, 0)), self.detect("from time import struct_time")) @@ -539,6 +557,9 @@ def test_EnumCheck_of_enum(self): def test_EnumDict_of_enum(self): self.assertOnlyIn((3, 13), self.detect("from enum import EnumDict")) + def test_EnumType_of_enum(self): + self.assertOnlyIn((3, 11), self.detect("from enum import EnumType")) + def test_Flag_of_enum(self): self.assertOnlyIn((3, 6), self.detect("from enum import Flag")) @@ -548,6 +569,9 @@ def test_FlagBoundary_of_enum(self): def test_IntFlag_of_enum(self): self.assertOnlyIn((3, 6), self.detect("from enum import IntFlag")) + def test_ReprEnum_of_enum(self): + self.assertOnlyIn((3, 11), self.detect("from enum import ReprEnum")) + def test_StrEnum_of_enum(self): self.assertOnlyIn((3, 11), self.detect("from enum import StrEnum")) diff --git a/tests/constants.py b/tests/constants.py index 473eb439..9da1a7bf 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -88,6 +88,12 @@ def test_line_number_of_dis_Instruction(self): def test_cache_info_of_dis_Instruction(self): self.assertOnlyIn((3, 13), self.detect("from dis import Instruction\nInstruction.cache_info")) + def test_hasarg_of_dis(self): + self.assertOnlyIn((3, 12), self.detect("from dis import hasarg")) + + def test_hasexc_of_dis(self): + self.assertOnlyIn((3, 12), self.detect("from dis import hasexc")) + def test_skips_of_doctest_DocTestRunner(self): self.assertOnlyIn((3, 13), self.detect("from doctest import DocTestRunner\nDocTestRunner.skips")) @@ -100,6 +106,24 @@ def test_verify_generated_headers_of_email_policy_Policy(self): self.assertOnlyIn((3, 13), self.detect(""" from email.policy import Policy Policy.verify_generated_headers +""")) + + def test_FORWARDREF_of_annotationlib_Format(self): + self.assertOnlyIn((3, 14), + self.detect("from annotationlib import Format\nFormat.FORWARDREF")) + + def test_STRING_of_annotationlib_Format(self): + self.assertOnlyIn((3, 14), + self.detect("from annotationlib import Format\nFormat.STRING")) + + def test_VALUE_of_annotationlib_Format(self): + self.assertOnlyIn((3, 14), + self.detect("from annotationlib import Format\nFormat.VALUE")) + + def test_VALUE_WITH_FAKE_GLOBALS_of_annotationlib_Format(self): + self.assertOnlyIn((3, 14), self.detect(""" +from annotationlib import Format +Format.VALUE_WITH_FAKE_GLOBALS """)) def test_mode_of_lzma_LZMAFile(self): @@ -1384,6 +1408,11 @@ def test_message_factory_from_email_policy_Policy(self): self.detect("from email.policy import Policy\n" "Policy().message_factory")) + def test_utf8_of_email_policy_EmailPolicy(self): + self.assertOnlyIn((3, 5), + self.detect("from email.policy import EmailPolicy\n" + "EmailPolicy.utf8")) + def test_EHWPOISON_of_errno(self): self.assertOnlyIn((3, 14), self.detect("from errno import EHWPOISON")) @@ -3387,6 +3416,9 @@ def test_minor_from_sys_version_info(self): self.detect("from sys import version_info\n" "version_info().minor")) + def test_monitoring_of_sys(self): + self.assertOnlyIn((3, 12), self.detect("from sys import monitoring")) + def test_releaselevel_from_sys_version_info(self): self.assertOnlyIn(((2, 7), (3, 0)), self.detect("from sys import version_info\n" @@ -3520,6 +3552,9 @@ def test___spec___from_types_ModuleType(self): self.detect("from types import ModuleType\n" "ModuleType().__spec__")) + def test___name___of_property(self): + self.assertOnlyIn((3, 13), self.detect("property.__name__")) + def test_NoneType_of_types(self): self.assertOnlyIn(((2, 0), (3, 10)), self.detect("from types import NoneType")) diff --git a/tests/exception.py b/tests/exception.py index d0050d28..94f6194d 100644 --- a/tests/exception.py +++ b/tests/exception.py @@ -1,6 +1,9 @@ from .testutils import VerminTest class VerminExceptionMemberTests(VerminTest): + def test_BrokenBarrierError_of_asyncio(self): + self.assertOnlyIn((3, 11), self.detect("from asyncio import BrokenBarrierError")) + def test_QueueShutDown_of_asyncio(self): self.assertOnlyIn((3, 13), self.detect("from asyncio import QueueShutDown")) diff --git a/tests/function.py b/tests/function.py index 8d4cc414..cd033992 100644 --- a/tests/function.py +++ b/tests/function.py @@ -446,6 +446,18 @@ def test_total_of_collections_Counter(self): c.total() """)) + def test___ixor___of_collections_Counter(self): + self.assertOnlyIn((3, 15), + self.detect("from collections import Counter\n" + "c = Counter()\n" + "c.__ixor__()")) + + def test___xor___of_collections_Counter(self): + self.assertOnlyIn((3, 15), + self.detect("from collections import Counter\n" + "c = Counter()\n" + "c.__xor__()")) + def test_suppress_of_contextlib(self): self.assertOnlyIn((3, 4), self.detect("import contextlib\ncontextlib.suppress()")) @@ -1687,6 +1699,11 @@ def test_time_ns_of_time(self): def test__check_future_of_asyncio_Task(self): self.assertOnlyIn((3, 11), self.detect("from asyncio import Task\nTask()._check_future()")) + def test_cancel_of_asyncio_TaskGroup(self): + self.assertOnlyIn((3, 15), + self.detect("from asyncio import TaskGroup\n" + "TaskGroup().cancel()")) + def test_run_of_asyncio(self): self.assertOnlyIn((3, 7), self.detect("import asyncio\nasyncio.run()")) @@ -2673,6 +2690,16 @@ def test_maketrans_from_collections_UserString(self): self.detect("from collections import UserString\n" "UserString().maketrans()")) + def test_removeprefix_from_collections_UserString(self): + self.assertOnlyIn((3, 9), + self.detect("from collections import UserString\n" + "UserString().removeprefix()")) + + def test_removesuffix_from_collections_UserString(self): + self.assertOnlyIn((3, 9), + self.detect("from collections import UserString\n" + "UserString().removesuffix()")) + def test_clear_from_collections_abc_MutableSequence(self): self.assertOnlyIn((3, 3), self.detect("from collections.abc import MutableSequence\n" @@ -2756,6 +2783,12 @@ def test_clear_from_dbm_gdbm(self): def test_clear_from_dbm_ndbm(self): self.assertOnlyIn((3, 13), self.detect("from dbm import ndbm\nndbm.clear()")) + def test_reorganize_of_dbm_dumb(self): + self.assertOnlyIn((3, 15), self.detect("from dbm import dumb\ndumb.reorganize()")) + + def test_reorganize_of_dbm_sqlite3(self): + self.assertOnlyIn((3, 15), self.detect("from dbm import sqlite3\nsqlite3.reorganize()")) + def test_DocFileSuite_from_doctest(self): self.assertOnlyIn(((2, 4), (3, 0)), self.detect("import doctest\ndoctest.DocFileSuite()")) @@ -5074,6 +5107,15 @@ def test_user_data_of_Event(self): def test_search_all_of_Text(self): self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.Text.search_all()")) + def test_grid_content_of_tkinter(self): + self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.grid_content()")) + + def test_pack_content_of_tkinter(self): + self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.pack_content()")) + + def test_place_content_of_tkinter(self): + self.assertOnlyIn((3, 15), self.detect("import tkinter\ntkinter.place_content()")) + def test_TypeForm_from_typing(self): self.assertOnlyIn((3, 15), self.detect("from typing import TypeForm")) @@ -5116,6 +5158,9 @@ def test_getformat_of_Wave_write(self): def test_setformat_of_Wave_write(self): self.assertOnlyIn((3, 15), self.detect("import wave\nwave.Wave_write.setformat()")) + def test_setparams_of_Wave_write(self): + self.assertOnlyIn((3, 15), self.detect("import wave\nwave.Wave_write.setparams()")) + def test_is_valid_name_of_xml(self): self.assertOnlyIn((3, 15), self.detect("from xml import is_valid_name")) diff --git a/vermin/rules.py b/vermin/rules.py index ecc6da7b..cbc55042 100644 --- a/vermin/rules.py +++ b/vermin/rules.py @@ -234,7 +234,9 @@ def MOD_MEM_REQS(config): "Queue.PriorityQueue": ((2, 6), None), "SimpleXMLRPCServer.CGIXMLRPCRequestHandler": ((2, 3), None), "abc.ABC": (None, (3, 4)), + "annotationlib.Format": (None, (3, 14)), "argparse.BooleanOptionalAction": (None, (3, 9)), + "ast.Constant": (None, (3, 6)), "asyncio.Barrier": (None, (3, 11)), "asyncio.BufferedProtocol": (None, (3, 7)), "asyncio.MultiLoopChildWatcher": (None, (3, 8)), @@ -307,9 +309,11 @@ def MOD_MEM_REQS(config): "email.policy.EmailPolicy": (None, (3, 3)), "enum.EnumCheck": (None, (3, 11)), "enum.EnumDict": (None, (3, 13)), + "enum.EnumType": (None, (3, 11)), "enum.Flag": (None, (3, 6)), "enum.FlagBoundary": (None, (3, 11)), "enum.IntFlag": (None, (3, 6)), + "enum.ReprEnum": (None, (3, 11)), "enum.StrEnum": (None, (3, 11)), "enum.auto": (None, (3, 6)), "ftplib.FTP_TLS": ((2, 7), (3, 2)), @@ -389,6 +393,8 @@ def MOD_MEM_REQS(config): "statistics.NormalDist": (None, (3, 8)), "string.Formatter": ((2, 6), (3, 0)), "string.Template": ((2, 4), (3, 0)), + "string.templatelib.Interpolation": (None, (3, 14)), + "string.templatelib.Template": (None, (3, 14)), "struct.Struct": ((2, 5), (3, 0)), "subprocess.CompletedProcess": (None, (3, 5)), "test.support.EnvironmentVarGuard": ((2, 6), None), @@ -524,6 +530,7 @@ def MOD_MEM_REQS(config): # Exceptions "ConfigParser.InterpolationMissingOptionError": ((2, 3), None), "ConfigParser.InterpolationSyntaxError": ((2, 3), None), + "asyncio.BrokenBarrierError": (None, (3, 11)), "asyncio.QueueShutDown": (None, (3, 13)), "concurrent.futures.BrokenExecutor": (None, (3, 7)), "concurrent.futures.InvalidStateError": (None, (3, 8)), @@ -659,6 +666,7 @@ def MOD_MEM_REQS(config): "memoryview.toreadonly": (None, (3, 8)), "next": ((2, 6), (3, 0)), "property": ((2, 2), (3, 0)), + "property.__name__": (None, (3, 13)), "property.deleter": ((2, 6), (3, 0)), "property.getter": ((2, 6), (3, 0)), "property.setter": ((2, 6), (3, 0)), @@ -753,6 +761,7 @@ def MOD_MEM_REQS(config): "asyncio.Task.get_name": (None, (3, 8)), "asyncio.Task.set_name": (None, (3, 8)), "asyncio.Task.uncancel": (None, (3, 11)), + "asyncio.TaskGroup.cancel": (None, (3, 15)), "asyncio.TimerHandle.when": (None, (3, 7)), "asyncio.WriteTransport.get_write_buffer_limits": (None, (3, 4)), "asyncio.all_tasks": (None, (3, 7)), @@ -825,6 +834,8 @@ def MOD_MEM_REQS(config): "codecs.iterencode": ((2, 5), (3, 0)), "codecs.namereplace_errors": (None, (3, 5)), "codecs.unregister": (None, (3, 10)), + "collections.Counter.__ixor__": (None, (3, 15)), + "collections.Counter.__xor__": (None, (3, 15)), "collections.Counter.subtract": (None, (3, 2)), "collections.Counter.total": (None, (3, 10)), "collections.OrderedDict.move_to_end": (None, (3, 2)), @@ -834,6 +845,8 @@ def MOD_MEM_REQS(config): "collections.UserString.format_map": (None, (3, 5)), "collections.UserString.isprintable": (None, (3, 5)), "collections.UserString.maketrans": (None, (3, 5)), + "collections.UserString.removeprefix": (None, (3, 9)), + "collections.UserString.removesuffix": (None, (3, 9)), "collections.abc.MutableSequence.clear": (None, (3, 3)), "collections.abc.MutableSequence.copy": (None, (3, 3)), "collections.deque.copy": (None, (3, 5)), @@ -848,6 +861,7 @@ def MOD_MEM_REQS(config): "configparser.ConfigParser.read_dict": bpv("configparser", (None, (3, 2)), config), "configparser.ConfigParser.read_file": bpv("configparser", (None, (3, 2)), config), "configparser.ConfigParser.read_string": bpv("configparser", (None, (3, 2)), config), + "configparser.ExtendedInterpolation": bpv("configparser", (None, (3, 2)), config), "configparser.RawConfigParser.read_dict": bpv("configparser", (None, (3, 2)), config), "configparser.RawConfigParser.read_file": bpv("configparser", (None, (3, 2)), config), "configparser.RawConfigParser.read_string": bpv("configparser", (None, (3, 2)), config), @@ -880,8 +894,10 @@ def MOD_MEM_REQS(config): "curses.unget_wch": (None, (3, 3)), "curses.update_lines_cols": (None, (3, 5)), "curses.window.get_wch": (None, (3, 3)), + "dbm.dumb.reorganize": (None, (3, 15)), "dbm.gdbm.clear": (None, (3, 13)), "dbm.ndbm.clear": (None, (3, 13)), + "dbm.sqlite3.reorganize": (None, (3, 15)), "datetime.date.fromisocalendar": (None, (3, 8)), "datetime.date.fromisoformat": (None, (3, 7)), "datetime.date.strptime": (None, (3, 14)), @@ -944,6 +960,8 @@ def MOD_MEM_REQS(config): "difflib.unified_diff": ((2, 3), (3, 0)), "dis.code_info": (None, (3, 2)), "dis.get_instructions": (None, (3, 4)), + "dis.hasarg": (None, (3, 12)), + "dis.hasexc": (None, (3, 12)), "dis.show_code": (None, (3, 2)), "dis.stack_effect": (None, (3, 4)), "doctest.DocFileSuite": ((2, 4), (3, 0)), @@ -1522,6 +1540,7 @@ def MOD_MEM_REQS(config): "pprint.pp": (None, (3, 8)), "pstats.Stats.get_stats_profile": (None, (3, 9)), "queue.Queue.shutdown": (None, (3, 13)), + "random.Random.randbytes": (None, (3, 9)), "random.binomialvariate": (None, (3, 12)), "random.choices": (None, (3, 6)), "random.getrandbits": ((2, 4), (3, 0)), @@ -1850,7 +1869,11 @@ def MOD_MEM_REQS(config): "tkinter.Event.detail": (None, (3, 15)), "tkinter.Event.user_data": (None, (3, 15)), "tkinter.Text.search_all": (None, (3, 15)), + "tkinter.grid_content": (None, (3, 15)), "tkinter.info_patchlevel": (None, (3, 11)), + "tkinter.pack_content": (None, (3, 15)), + "tkinter.place_content": (None, (3, 15)), + "tkinter.ttk": (None, (3, 1)), "tokenize.generate_tokens": ((2, 2), (3, 0)), "tokenize.open": (None, (3, 2)), "tokenize.untokenize": ((2, 5), (3, 0)), @@ -1998,6 +2021,7 @@ def MOD_MEM_REQS(config): "wave.Wave_read.getformat": (None, (3, 15)), "wave.Wave_write.getformat": (None, (3, 15)), "wave.Wave_write.setformat": (None, (3, 15)), + "wave.Wave_write.setparams": (None, (3, 15)), "webbrowser.controller.open_new_tab": ((2, 5), (3, 0)), "webbrowser.open_new_tab": ((2, 5), (3, 0)), "winreg.CreateKeyEx": (None, (3, 2)), @@ -2086,6 +2110,10 @@ def MOD_MEM_REQS(config): "__future__.unicode_literals": ((2, 6), (3, 0)), "__future__.with_statement": ((2, 5), (3, 0)), "_thread.TIMEOUT_MAX": (None, (3, 2)), + "annotationlib.Format.FORWARDREF": (None, (3, 14)), + "annotationlib.Format.STRING": (None, (3, 14)), + "annotationlib.Format.VALUE": (None, (3, 14)), + "annotationlib.Format.VALUE_WITH_FAKE_GLOBALS": (None, (3, 14)), "ast.AST._field_types": (None, (3, 13)), "ast.PyCF_ALLOW_TOP_LEVEL_AWAIT": (None, (3, 8)), "ast.PyCF_OPTIMIZED_AST": (None, (3, 13)), @@ -2198,6 +2226,7 @@ def MOD_MEM_REQS(config): "doctest.DocTestRunner.skipped": (None, (3, 13)), "email.message.Message.defects": ((2, 4), (3, 0)), "email.policy.EmailPolicy.content_manager": (None, (3, 4)), + "email.policy.EmailPolicy.utf8": (None, (3, 5)), "email.policy.Policy.message_factory": (None, (3, 6)), "email.policy.Policy.verify_generated_headers": (None, (3, 13)), "errno.EHWPOISON": (None, (3, 14)), @@ -3002,6 +3031,7 @@ def MOD_MEM_REQS(config): "sys.is_stack_trampoline_active": (None, (3, 12)), "sys.last_exc": (None, (3, 12)), "sys.long_info": ((2, 7), None), + "sys.monitoring": (None, (3, 12)), "sys.orig_argv": (None, (3, 10)), "sys.platlibdir": (None, (3, 9)), "sys.py3kwarning": ((2, 6), None), @@ -3157,6 +3187,8 @@ def MOD_MEM_REQS(config): "xml.parsers.expat.ExpatError.code": ((2, 1), (3, 0)), "xml.parsers.expat.ExpatError.lineno": ((2, 1), (3, 0)), "xml.parsers.expat.ExpatError.offset": ((2, 1), (3, 0)), + "xml.parsers.expat.SetAllocTrackerActivationThreshold": (None, (3, 15)), + "xml.parsers.expat.SetAllocTrackerMaximumAmplification": (None, (3, 15)), "xml.parsers.expat.XMLParserType.CurrentByteIndex": ((2, 4), (3, 0)), "xml.parsers.expat.XMLParserType.CurrentColumnNumber": ((2, 4), (3, 0)), "xml.parsers.expat.XMLParserType.CurrentLineNumber": ((2, 4), (3, 0)), From d08ec6bbc848075ba8af6c0270dfd2df0717540c Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Sun, 17 May 2026 19:36:50 +0200 Subject: [PATCH 8/8] Replace ast.Constant with getattr to avoid self-test violation --- vermin/source_visitor.py | 50 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/vermin/source_visitor.py b/vermin/source_visitor.py index 99cb4adf..c2142375 100644 --- a/vermin/source_visitor.py +++ b/vermin/source_visitor.py @@ -18,8 +18,8 @@ def is_int_node(node): if sys.version_info >= (3, 8): - return (isinstance(node, ast.Constant) and isinstance(node.value, int)) or \ - (isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant) and + return (isinstance(node, getattr(ast, "Constant")) and isinstance(node.value, int)) or \ + (isinstance(node, ast.UnaryOp) and isinstance(node.operand, getattr(ast, "Constant")) and isinstance(node.operand.value, int)) return (isinstance(node, ast.Num) and isinstance(node.n, int)) or \ (isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Num) and @@ -27,9 +27,10 @@ def is_int_node(node): def is_neg_int_node(node): if sys.version_info >= (3, 8): - return (isinstance(node, ast.Constant) and isinstance(node.value, int) and node.value < 0) or \ + return (isinstance(node, getattr(ast, "Constant")) and + isinstance(node.value, int) and node.value < 0) or \ (isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and - isinstance(node.operand, ast.Constant) and isinstance(node.operand.value, int)) + isinstance(node.operand, getattr(ast, "Constant")) and isinstance(node.operand.value, int)) return (isinstance(node, ast.Num) and isinstance(node.n, int) and node.n < 0) or \ (isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Num) and isinstance(node.operand.n, int)) @@ -38,7 +39,7 @@ def is_none_node(node): # pragma: no cover if isinstance(node, ast.Name) and node.id == 'None': return True if sys.version_info >= (3, 8): - if isinstance(node, ast.Constant) and node.value is None: + if isinstance(node, getattr(ast, "Constant")) and node.value is None: return True elif sys.version_info >= (3, 4): if isinstance(node, ast.NameConstant) and node.value is None: @@ -47,7 +48,7 @@ def is_none_node(node): # pragma: no cover def is_ellipsis_node(node): # pragma: no cover if sys.version_info >= (3, 8): - return isinstance(node, ast.Constant) and isinstance(node.value, type(Ellipsis)) + return isinstance(node, getattr(ast, "Constant")) and isinstance(node.value, type(Ellipsis)) return hasattr(ast, 'Ellipsis') and isinstance(node, ast.Ellipsis) # Generalized unpacking, or starred expressions, are allowed when used with assignment targets prior @@ -765,7 +766,7 @@ def __add_codecs_error_handler(self, func, node): if 0 <= idx < len(node.args): arg = node.args[idx] name = None - if sys.version_info >= (3, 8) and isinstance(arg, ast.Constant): + if sys.version_info >= (3, 8) and isinstance(arg, getattr(ast, "Constant")): name = arg.value elif hasattr(arg, "s"): name = arg.s @@ -779,7 +780,7 @@ def __add_codecs_error_handler(self, func, node): # Check for "errors" keyword arguments. for kw in node.keywords: if kw.arg == "errors": - if sys.version_info >= (3, 8) and isinstance(kw.value, ast.Constant): + if sys.version_info >= (3, 8) and isinstance(kw.value, getattr(ast, "Constant")): name = kw.value.value elif hasattr(kw.value, "s"): name = kw.value.s @@ -797,7 +798,7 @@ def __add_codecs_encoding(self, func, node): # Check indexed arguments. if 0 <= idx < len(node.args): arg = node.args[idx] - if sys.version_info >= (3, 8) and isinstance(arg, ast.Constant): + if sys.version_info >= (3, 8) and isinstance(arg, getattr(ast, "Constant")): name = arg.value elif hasattr(arg, "s"): name = arg.s @@ -812,7 +813,7 @@ def __add_codecs_encoding(self, func, node): # Check for "encoding", "data_encoding", "file_encoding" keyword arguments. for kw in node.keywords: if kw.arg in self.__s.codecs_encodings_kwargs: - if sys.version_info >= (3, 8) and isinstance(kw.value, ast.Constant): + if sys.version_info >= (3, 8) and isinstance(kw.value, getattr(ast, "Constant")): name = kw.value.value elif hasattr(kw.value, "s"): name = kw.value.s @@ -886,7 +887,7 @@ def __get_attribute_name(self, node): if len(full_name) == 0 or (full_name[0] != "list" and full_name[-1] != "list"): full_name.append("list") elif sys.version_info >= (3, 8): - if isinstance(attr, ast.Constant): + if isinstance(attr, getattr(ast, "Constant")): if isinstance(attr.value, str): name = "str" if len(full_name) == 0 or (full_name[0] != name and len(full_name) == 1): @@ -947,7 +948,7 @@ def __add_name_res_assign_node(self, node): # If rvalue is None elif sys.version_info >= (3, 8) and \ - isinstance(node.value, ast.Constant) and node.value.value is None: + isinstance(node.value, getattr(ast, "Constant")) and node.value.value is None: type_name = "None" elif ((3, 8) > sys.version_info >= (3, 4)) and \ isinstance(node.value, ast.NameConstant) and node.value.value is None: @@ -961,7 +962,7 @@ def __add_name_res_assign_node(self, node): type_name = node.value.id elif sys.version_info >= (3, 8): - if isinstance(node.value, ast.Constant): + if isinstance(node.value, getattr(ast, "Constant")): v = node.value.value if isinstance(v, str): value_name = "str" @@ -1209,7 +1210,7 @@ def visit_Call(self, node): self.__s.module_as_name[func.id] == "array.array"): for arg in node.args: if sys.version_info >= (3, 8): - if isinstance(arg, ast.Constant) and isinstance(arg.value, str): + if isinstance(arg, getattr(ast, "Constant")) and isinstance(arg.value, str): # "array" = 5 + 1 = 6 self.__add_array_typecode(arg.value, node.lineno, node.col_offset + 6) else: @@ -1228,7 +1229,8 @@ def visit_Call(self, node): elif hasattr(func, "attr"): attr = func.attr if (sys.version_info >= (3, 8) and attr == "format" and hasattr(func, "value") and - isinstance(func.value, ast.Constant) and isinstance(func.value.value, str) and + isinstance(func.value, getattr(ast, "Constant")) and + isinstance(func.value.value, str) and "{}" in func.value.value) \ or \ (sys.version_info < (3, 8) and attr == "format" and hasattr(func, "value") and @@ -1245,7 +1247,7 @@ def visit_Call(self, node): elif attr in ("strftime", "strptime") and hasattr(node, "args"): for arg in node.args: look_for = None - if sys.version_info >= (3, 8) and isinstance(arg, ast.Constant): + if sys.version_info >= (3, 8) and isinstance(arg, getattr(ast, "Constant")): look_for = arg.value elif hasattr(arg, "s"): look_for = arg.s @@ -1259,7 +1261,7 @@ def visit_Call(self, node): if self.__s.function_name == "array.array": for arg in node.args: if sys.version_info >= (3, 8): - if isinstance(arg, ast.Constant) and isinstance(arg.value, str): + if isinstance(arg, getattr(ast, "Constant")) and isinstance(arg.value, str): # "array.array" = 5 + 1 + 5 + 1 = 12 self.__add_array_typecode(arg.value, node.lineno, node.col_offset + 12) else: @@ -1368,10 +1370,11 @@ def __is_dict(self, node): elif isinstance(node, ast.Subscript): n = None if sys.version_info >= (3, 9): - if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, int): + if isinstance(node.slice, getattr(ast, "Constant")) and isinstance(node.slice.value, int): n = node.slice.value elif sys.version_info >= (3, 8): - if isinstance(node.slice, ast.Index) and isinstance(node.slice.value, ast.Constant) and \ + if isinstance(node.slice, ast.Index) and \ + isinstance(node.slice.value, getattr(ast, "Constant")) and \ isinstance(node.slice.value.value, int): n = node.slice.value.value else: @@ -1405,7 +1408,7 @@ def visit_BinOp(self, node): # BinOp(left=Bytes(s=b'%4x'), op=Mod(), right=Num(n=10)) # BinOp(left=Call(func=Name(id='bytearray', ctx=Load()), args=[Bytes(s=b'%x')], keywords=[]), # op=Mod(), right=Num(n=10)) - if (sys.version_info >= (3, 8) and isinstance(node.left, ast.Constant) and + if (sys.version_info >= (3, 8) and isinstance(node.left, getattr(ast, "Constant")) and isinstance(node.left.value, bytes) and isinstance(node.op, ast.Mod)) or \ (sys.version_info < (3, 8) and @@ -1479,7 +1482,7 @@ def __extract_fstring_value(self, node): # pragma: no cover if isinstance(n, ast.Name): value.append(n.id) - elif hasattr(ast, "Constant") and isinstance(n, ast.Constant): + elif hasattr(ast, "Constant") and isinstance(n, getattr(ast, "Constant")): v = str(n.value) if isinstance(n.value, str): v = "\"{}\"".format(v) @@ -1642,7 +1645,7 @@ def __extract_fstring_value(self, node): # pragma: no cover break elif sys.version_info >= (3, 9) \ - and isinstance(n, (ast.Constant, ast.Name, ast.Slice, ast.Tuple)): + and isinstance(n, (ast.Name, ast.Slice, ast.Tuple)): # Check on slice indices like a[0] or a[i] or a[0:1] or a[(0,1)] val = self.__extract_fstring_value(n) value.append(val) @@ -1704,7 +1707,8 @@ def visit_JoinedStr(self, node): # A self-referencing f-string will be at the end of the Constant, like "..stuff..expr=", and # the next value will be a FormattedValue(value=..) with Names or nested Calls with Names # inside, for instance. - if isinstance(val, ast.Constant) and hasattr(val, "value") and \ + const_type = getattr(ast, "Constant", None) + if const_type is not None and isinstance(val, const_type) and hasattr(val, "value") and \ isinstance(val.value, str) and val.value.strip().endswith("=") and i + 1 < total: next_val = node.values[i + 1] if isinstance(next_val, ast.FormattedValue):