From 02d917d6d06b219efef25c8782edc5e79334e1ce Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 8 May 2026 14:34:11 +0100 Subject: [PATCH 001/127] [mypyc] Add librt.random module (#21433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stdlib `random` module is fairly often used in performance critical code, and it's not super efficient. Add `librt.random` with a subset of the stdlib module interface that is optimized for performance when compiled. Use ChaCha8 as the algorithm. Based on some research, this is a modern, high-quality PRNG algorithm. It's used by Go `math/rand/v2`, among others. This is a non-cryptographic PRNG, similar to the stdlib `random` module (but this uses a different algorithm). I used Claude Code and Codex to write all the code, but I iterated on it quite a lot and did a bunch of manual validation and code review. I also asked Codex to explicitly check that the ChaCha8 implementation is correct by comparing it to a reference implementation. Use thread-local RNG state for module-level functions to enable good scaling in free-threaded builds. There's some extra complexity from having to free the state at thread exit. I asked a LLM to generate and run a benchmark, and here are the results on 3.14: ``` │ Function │ vs stdlib(compiled) │ vs stdlib(interpreted) │ │ random() │ 3.2x │ 4.8x │ │ randint() │ 16.6x │ 18.0x │ │ randrange() │ 14.5x │ 16.2x │ │ choice() │ 12.9x │ 10.3x │ ``` `choice()` was replaced with `randrange` when using `librt`, since we don't provide it as part of this fairly minimal API. --- mypy/typeshed/stubs/librt/librt/random.pyi | 22 + mypyc/build.py | 4 + mypyc/codegen/emitmodule.py | 5 + mypyc/ir/deps.py | 1 + mypyc/ir/rtypes.py | 7 +- mypyc/lib-rt/random/librt_random.c | 762 +++++++++++++++++++++ mypyc/lib-rt/random/librt_random.h | 10 + mypyc/lib-rt/random/librt_random_api.c | 45 ++ mypyc/lib-rt/random/librt_random_api.h | 32 + mypyc/lib-rt/setup.py | 13 + mypyc/primitives/librt_random_ops.py | 104 +++ mypyc/primitives/registry.py | 1 + mypyc/test-data/irbuild-librt-random.test | 119 ++++ mypyc/test-data/run-librt-random.test | 344 ++++++++++ mypyc/test/test_irbuild.py | 1 + mypyc/test/test_run.py | 1 + 16 files changed, 1470 insertions(+), 1 deletion(-) create mode 100644 mypy/typeshed/stubs/librt/librt/random.pyi create mode 100644 mypyc/lib-rt/random/librt_random.c create mode 100644 mypyc/lib-rt/random/librt_random.h create mode 100644 mypyc/lib-rt/random/librt_random_api.c create mode 100644 mypyc/lib-rt/random/librt_random_api.h create mode 100644 mypyc/primitives/librt_random_ops.py create mode 100644 mypyc/test-data/irbuild-librt-random.test create mode 100644 mypyc/test-data/run-librt-random.test diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi new file mode 100644 index 0000000000000..d1330aa56faf1 --- /dev/null +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -0,0 +1,22 @@ +from typing import final, overload + +from mypy_extensions import i64 + +def random() -> float: ... +def randint(a: i64, b: i64) -> i64: ... +@overload +def randrange(stop: i64, /) -> i64: ... +@overload +def randrange(start: i64, stop: i64, /) -> i64: ... +def seed(n: i64, /) -> None: ... + +@final +class Random: + def __init__(self, seed: i64 | None = None) -> None: ... + def randint(self, a: i64, b: i64) -> i64: ... + @overload + def randrange(self, stop: i64, /) -> i64: ... + @overload + def randrange(self, start: i64, stop: i64, /) -> i64: ... + def random(self) -> float: ... + def seed(self, n: i64, /) -> None: ... diff --git a/mypyc/build.py b/mypyc/build.py index d55334d8ac800..08eeb13c91752 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -121,6 +121,7 @@ class ModDesc(NamedTuple): ["vecs"], ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), + ModDesc("librt.random", ["random/librt_random.c"], ["random/librt_random.h"], ["random"]), ] try: @@ -631,6 +632,9 @@ def get_cflags( # Disables C Preprocessor (cpp) warnings # See https://github.com/mypyc/mypyc/issues/956 "-Wno-cpp", + "-Wno-array-bounds", + "-Wno-stringop-overread", + "-Wno-stringop-overflow", ] if log_trace: cflags.append("-DMYPYC_LOG_TRACE") diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 2025426188412..3f10df7fa8c98 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -59,6 +59,7 @@ from mypyc.errors import Errors from mypyc.ir.deps import ( LIBRT_BASE64, + LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_TIME, LIBRT_VECS, @@ -1224,6 +1225,10 @@ def emit_module_exec_func( emitter.emit_line("if (import_librt_vecs() < 0) {") emitter.emit_line("return -1;") emitter.emit_line("}") + if LIBRT_RANDOM in module.dependencies: + emitter.emit_line("if (import_librt_random() < 0) {") + emitter.emit_line("return -1;") + emitter.emit_line("}") emitter.emit_line("PyObject* modname = NULL;") if self.multi_phase_init: emitter.emit_line(f"{module_static} = module;") diff --git a/mypyc/ir/deps.py b/mypyc/ir/deps.py index 20b1f102ee383..751845d3a324c 100644 --- a/mypyc/ir/deps.py +++ b/mypyc/ir/deps.py @@ -109,6 +109,7 @@ def get_header(self) -> str: LIBRT_BASE64: Final = Capsule("librt.base64") LIBRT_VECS: Final = Capsule("librt.vecs") LIBRT_TIME: Final = Capsule("librt.time") +LIBRT_RANDOM: Final = Capsule("librt.random") BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c") BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c") diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 60e9b49582bc3..db29f9e304d8d 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -41,7 +41,7 @@ class to enable the new behavior. In rare cases, adding a new from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeGuard, TypeVar, Union, final from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name -from mypyc.ir.deps import LIBRT_STRINGS, LIBRT_VECS, Dependency +from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_VECS, Dependency from mypyc.namegen import NameGenerator if TYPE_CHECKING: @@ -544,10 +544,15 @@ def __hash__(self) -> int: ("librt.strings.BytesWriter", (LIBRT_STRINGS,)), ("librt.strings.StringWriter", (LIBRT_STRINGS,)), ] +} | { + "librt.random.Random": RPrimitive( + "librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,) + ) } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] string_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.StringWriter"] +random_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.random.Random"] def is_native_rprimitive(rtype: RType) -> bool: diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c new file mode 100644 index 0000000000000..7dc590eaa5946 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random.c @@ -0,0 +1,762 @@ +#include "pythoncapi_compat.h" + +#define PY_SSIZE_T_CLEAN +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "mypyc_util.h" +#include "CPy.h" +#include "librt_random.h" + +// +// ChaCha8 PRNG with forward secrecy +// + +#define CHACHA8_RESEED_INTERVAL 16 + +typedef struct { + uint32_t seed[8]; // 256-bit key + uint32_t buf[16]; // output buffer: one ChaCha8 block + uint32_t counter; // block counter + uint8_t used; // index into buf + uint8_t n; // usable values in buf (8 or 16) + uint8_t blocks_left; // blocks until next reseed +} chacha8_rng; + +static inline uint32_t +rotl32(uint32_t x, int n) { + return (x << n) | (x >> (32 - n)); +} + +#define QUARTERROUND(a, b, c, d) \ + do { \ + a += b; d ^= a; d = rotl32(d, 16); \ + c += d; b ^= c; b = rotl32(b, 12); \ + a += b; d ^= a; d = rotl32(d, 8); \ + c += d; b ^= c; b = rotl32(b, 7); \ + } while (0) + +static void +chacha8_block(const uint32_t seed[8], uint32_t counter, uint32_t out[16]) +{ + // "expand 32-byte k" + uint32_t s[16] = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + seed[0], seed[1], seed[2], seed[3], + seed[4], seed[5], seed[6], seed[7], + counter, 0, 0, 0 // counter (low 32), counter (high 32), nonce + }; + + memcpy(out, s, sizeof(uint32_t) * 16); + + // 4 double-rounds = 8 rounds + for (int i = 0; i < 4; i++) { + // Column rounds + QUARTERROUND(out[0], out[4], out[ 8], out[12]); + QUARTERROUND(out[1], out[5], out[ 9], out[13]); + QUARTERROUND(out[2], out[6], out[10], out[14]); + QUARTERROUND(out[3], out[7], out[11], out[15]); + // Diagonal rounds + QUARTERROUND(out[0], out[5], out[10], out[15]); + QUARTERROUND(out[1], out[6], out[11], out[12]); + QUARTERROUND(out[2], out[7], out[ 8], out[13]); + QUARTERROUND(out[3], out[4], out[ 9], out[14]); + } + + // Add original state back (standard ChaCha finalization) + for (int i = 0; i < 16; i++) + out[i] += s[i]; +} + +// Fill entropy from OS via os.urandom(), which handles short reads, +// EINTR, and platform differences internally. +// Returns 0 on success, -1 on failure (with Python exception set). +static int +fill_os_entropy(void *buf, size_t len) +{ + PyObject *os_mod = PyImport_ImportModule("os"); + if (os_mod == NULL) + return -1; + PyObject *bytes = PyObject_CallMethod(os_mod, "urandom", "n", (Py_ssize_t)len); + Py_DECREF(os_mod); + if (bytes == NULL) + return -1; + memcpy(buf, PyBytes_AS_STRING(bytes), len); + Py_DECREF(bytes); + return 0; +} + +static void +chacha8_refill(chacha8_rng *rng) +{ + chacha8_block(rng->seed, rng->counter, rng->buf); + rng->counter++; + rng->used = 0; + rng->blocks_left--; + + if (unlikely(rng->blocks_left == 0)) { + // Forward secrecy reseed: steal last 8 words as new key + memcpy(rng->seed, rng->buf + 8, sizeof(uint32_t) * 8); + rng->n = 8; // only 8 words usable this block + rng->counter = 0; + rng->blocks_left = CHACHA8_RESEED_INTERVAL; + } else { + rng->n = 16; + } +} + +static inline uint32_t +chacha8_next(chacha8_rng *rng) +{ + if (unlikely(rng->used >= rng->n)) + chacha8_refill(rng); + return rng->buf[rng->used++]; +} + +// Return 64 bits of randomness (two consecutive 32-bit words, single bounds check). +static inline uint64_t +chacha8_next64(chacha8_rng *rng) +{ + // Need 2 words available; if fewer than 2, refill first. + if (unlikely(rng->used + 1 >= rng->n)) + // Use two separate calls to handle block boundary correctly. + return ((uint64_t)chacha8_next(rng) << 32) | chacha8_next(rng); + uint32_t hi = rng->buf[rng->used++]; + uint32_t lo = rng->buf[rng->used++]; + return ((uint64_t)hi << 32) | lo; +} + +// Return a uniformly distributed random value in [0, range). +// Use Lemire's nearly divisionless method for small ranges, and a portable +// rejection sampler for larger ranges to avoid non-standard 128-bit arithmetic. +static inline uint64_t +chacha8_next_ranged(chacha8_rng *rng, uint64_t range) +{ + assert(range != 0); + if (likely(range <= UINT32_MAX)) { + // 32-bit Lemire: multiply r * range to get 64-bit product, + // upper 32 bits are the result in [0, range). + uint64_t m = (uint64_t)chacha8_next(rng) * range; + uint32_t lo = (uint32_t)m; + if (unlikely(lo < range)) { + uint32_t thresh = (uint32_t)(-(uint32_t)range) % (uint32_t)range; + while (lo < thresh) { + m = (uint64_t)chacha8_next(rng) * range; + lo = (uint32_t)m; + } + } + return m >> 32; + } + // If range is a power of two, masking produces an unbiased result. + if ((range & (range - 1)) == 0) { + return chacha8_next64(rng) & (range - 1); + } + uint64_t r; + // In unsigned arithmetic, -range is 2**64 - range, so this computes + // 2**64 % range. Rejecting values below this threshold leaves exactly + // floor(2**64 / range) full buckets of size range, avoiding modulo bias. + uint64_t thresh = -range % range; + do { + r = chacha8_next64(rng); + } while (unlikely(r < thresh)); + return r % range; +} + +// Return a random i64 starting at 'start', with 'range' possible values. +// A zero range represents the full 2**64 i64 domain. +static inline int64_t +random_i64_from_range(chacha8_rng *rng, int64_t start, uint64_t range) +{ + uint64_t offset = range == 0 ? chacha8_next64(rng) : chacha8_next_ranged(rng, range); + return (int64_t)((uint64_t)start + offset); +} + +static void +chacha8_reset(chacha8_rng *rng) +{ + rng->counter = 0; + rng->used = 16; // force immediate refill on first call + rng->n = 16; + rng->blocks_left = CHACHA8_RESEED_INTERVAL; +} + +static int +chacha8_init(chacha8_rng *rng) +{ + if (fill_os_entropy(rng->seed, sizeof(rng->seed)) < 0) + return -1; + chacha8_reset(rng); + return 0; +} + +// Seed from an integer by hashing it through ChaCha8 to fill the 256-bit key. +static void +chacha8_seed_int(chacha8_rng *rng, int64_t seed_val) +{ + // Use the integer to construct a simple initial key, then run one + // ChaCha8 block to diffuse it across all 256 bits. + memset(rng->seed, 0, sizeof(rng->seed)); + rng->seed[0] = (uint32_t)(seed_val & 0xFFFFFFFF); + rng->seed[1] = (uint32_t)((uint64_t)seed_val >> 32); + + uint32_t out[16]; + chacha8_block(rng->seed, 0, out); + memcpy(rng->seed, out, sizeof(rng->seed)); + chacha8_reset(rng); +} + +// +// Thread-local global RNG for module-level random()/randint() +// +// thread_local pointer for fast access (direct %fs/%gs-relative load), +// platform TLS key with destructor for cleanup on thread exit. +// + +#ifdef _WIN32 +static __declspec(thread) chacha8_rng *tls_rng = NULL; +#else +static __thread chacha8_rng *tls_rng = NULL; +#endif + +#ifdef _WIN32 +static DWORD tls_key = FLS_OUT_OF_INDEXES; + +static void NTAPI +tls_rng_destructor(void *ptr) +{ + if (ptr != NULL) { + memset(ptr, 0, sizeof(chacha8_rng)); + PyMem_RawFree(ptr); + } +} +#else +static pthread_key_t tls_key; + +static void +tls_rng_destructor(void *ptr) +{ + if (ptr != NULL) { + memset(ptr, 0, sizeof(chacha8_rng)); + PyMem_RawFree(ptr); + } +} +#endif + +static int tls_key_created = 0; + +static int +ensure_tls_key(void) +{ + if (likely(tls_key_created)) + return 0; +#ifdef _WIN32 + tls_key = FlsAlloc(tls_rng_destructor); + if (tls_key == FLS_OUT_OF_INDEXES) { + PyErr_SetString(PyExc_OSError, "FlsAlloc failed"); + return -1; + } +#else + if (pthread_key_create(&tls_key, tls_rng_destructor) != 0) { + PyErr_SetString(PyExc_OSError, "pthread_key_create failed"); + return -1; + } +#endif + tls_key_created = 1; + return 0; +} + +// Get the thread-local RNG, initializing on first use. +// Returns NULL with Python exception set on failure. +static inline chacha8_rng * +get_thread_rng(void) +{ + chacha8_rng *rng = tls_rng; + if (likely(rng != NULL)) + return rng; + + // First use on this thread — allocate and seed + rng = PyMem_RawMalloc(sizeof(chacha8_rng)); + if (rng == NULL) { + PyErr_NoMemory(); + return NULL; + } + if (chacha8_init(rng) < 0) { + PyMem_RawFree(rng); + return NULL; + } + + // Register with platform TLS for destructor +#ifdef _WIN32 + FlsSetValue(tls_key, rng); +#else + pthread_setspecific(tls_key, rng); +#endif + + tls_rng = rng; + return rng; +} + +// Return a random double in [0.0, 1.0) with 53 bits of mantissa precision. +static inline double +random_double_impl(chacha8_rng *rng) +{ + uint64_t r = chacha8_next64(rng); + return (double)(r >> 11) * (1.0 / 9007199254740992.0); // 1/2^53 +} + +// +// Module-level random() and randint() +// + +static PyObject* +module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + return PyFloat_FromDouble(random_double_impl(rng)); +} + +// Generate random integer in [a, b] using the given RNG. +static inline PyObject* +randint_impl(chacha8_rng *rng, int64_t a, int64_t b) +{ + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return PyLong_FromLongLong(random_i64_from_range(rng, a, range)); +} + +static PyObject* +module_randint(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 2) { + PyErr_Format(PyExc_TypeError, + "randint() takes exactly 2 arguments (%zd given)", nargs); + return NULL; + } + + int64_t a = CPyLong_AsInt64(args[0]); + if (unlikely(a == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + int64_t b = CPyLong_AsInt64(args[1]); + if (unlikely(b == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + if (a > b) { + PyErr_SetString(PyExc_ValueError, + "empty range for randint()"); + return NULL; + } + + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + + return randint_impl(rng, a, b); +} + +// Parse 1 or 2 int args for randrange([start,] stop). +// Sets *a to start (default 0), *b to stop-1. +// Returns 0 on success, -1 on error (with exception set). +static int +parse_randrange_args(PyObject *const *args, Py_ssize_t nargs, + int64_t *a, int64_t *b) +{ + if (nargs == 1) { + *a = 0; + int64_t stop = CPyLong_AsInt64(args[0]); + if (unlikely(stop == CPY_LL_INT_ERROR && PyErr_Occurred())) + return -1; + if (stop <= 0) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return -1; + } + *b = stop - 1; + } else if (nargs == 2) { + *a = CPyLong_AsInt64(args[0]); + if (unlikely(*a == CPY_LL_INT_ERROR && PyErr_Occurred())) + return -1; + int64_t stop = CPyLong_AsInt64(args[1]); + if (unlikely(stop == CPY_LL_INT_ERROR && PyErr_Occurred())) + return -1; + if (*a >= stop) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return -1; + } + *b = stop - 1; + } else { + PyErr_Format(PyExc_TypeError, + "randrange() takes 1 or 2 arguments (%zd given)", nargs); + return -1; + } + return 0; +} + +static PyObject* +module_randrange(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + int64_t a, b; + if (parse_randrange_args(args, nargs, &a, &b) < 0) + return NULL; + + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + + return randint_impl(rng, a, b); +} + +static PyObject* +module_seed(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, + "seed() takes exactly 1 argument (%zd given)", nargs); + return NULL; + } + int64_t seed_val = CPyLong_AsInt64(args[0]); + if (unlikely(seed_val == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + + chacha8_seed_int(rng, seed_val); + Py_RETURN_NONE; +} + +// +// Random Python type +// + +typedef struct { + PyObject_HEAD + chacha8_rng rng; +} RandomObject; + +static PyTypeObject RandomType; + +static PyObject* +Random_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + if (type != &RandomType) { + PyErr_SetString(PyExc_TypeError, "Random cannot be subclassed"); + return NULL; + } + + RandomObject *self = (RandomObject *)type->tp_alloc(type, 0); + // Seeding is done in tp_init + return (PyObject *)self; +} + +static int +Random_init(RandomObject *self, PyObject *args, PyObject *kwds) +{ + PyObject *seed_obj = NULL; + + if (!PyArg_ParseTuple(args, "|O", &seed_obj)) { + return -1; + } + + if (kwds != NULL && PyDict_Size(kwds) > 0) { + PyErr_SetString(PyExc_TypeError, + "Random() takes no keyword arguments"); + return -1; + } + + if (seed_obj == NULL || seed_obj == Py_None) { + if (chacha8_init(&self->rng) < 0) + return -1; + } else { + int64_t seed_val = CPyLong_AsInt64(seed_obj); + if (unlikely(seed_val == CPY_LL_INT_ERROR && PyErr_Occurred())) + return -1; + chacha8_seed_int(&self->rng, seed_val); + } + + return 0; +} + +// Internal constructors for capsule API (bypass tp_new/tp_init) + +static PyObject * +Random_internal(void) { + RandomObject *self = (RandomObject *)RandomType.tp_alloc(&RandomType, 0); + if (self == NULL) + return NULL; + if (chacha8_init(&self->rng) < 0) { + Py_DECREF(self); + return NULL; + } + return (PyObject *)self; +} + +static PyObject * +Random_from_seed_internal(int64_t seed_val) { + RandomObject *self = (RandomObject *)RandomType.tp_alloc(&RandomType, 0); + if (self == NULL) + return NULL; + chacha8_seed_int(&self->rng, seed_val); + return (PyObject *)self; +} + +static PyTypeObject * +Random_type_internal(void) { + return &RandomType; +} + +static int64_t +Random_randrange1_internal(PyObject *self, int64_t stop) { + if (unlikely(stop <= 0)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + return (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, (uint64_t)stop); +} + +static int64_t +Random_randrange2_internal(PyObject *self, int64_t start, int64_t stop) { + if (unlikely(start >= stop)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + uint64_t range = (uint64_t)stop - (uint64_t)start; + return random_i64_from_range(&((RandomObject *)self)->rng, start, range); +} + +static int64_t +Random_randint_internal(PyObject *self, int64_t a, int64_t b) { + if (unlikely(a > b)) { + PyErr_SetString(PyExc_ValueError, "empty range for randint()"); + return CPY_LL_INT_ERROR; + } + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return random_i64_from_range(&((RandomObject *)self)->rng, a, range); +} + +static double +Random_random_internal(PyObject *self) { + return random_double_impl(&((RandomObject *)self)->rng); +} + +static PyObject* +Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + if (nargs != 2) { + PyErr_Format(PyExc_TypeError, + "randint() takes exactly 2 arguments (%zd given)", nargs); + return NULL; + } + + int64_t a = CPyLong_AsInt64(args[0]); + if (unlikely(a == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + int64_t b = CPyLong_AsInt64(args[1]); + if (unlikely(b == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + if (a > b) { + PyErr_SetString(PyExc_ValueError, + "empty range for randint()"); + return NULL; + } + + return randint_impl(&self->rng, a, b); +} + +static PyObject* +Random_randrange(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + int64_t a, b; + if (parse_randrange_args(args, nargs, &a, &b) < 0) + return NULL; + return randint_impl(&self->rng, a, b); +} + +static PyObject* +Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { + return PyFloat_FromDouble(random_double_impl(&self->rng)); +} + +static PyObject* +Random_seed(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, + "seed() takes exactly 1 argument (%zd given)", nargs); + return NULL; + } + int64_t seed_val = CPyLong_AsInt64(args[0]); + if (unlikely(seed_val == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + chacha8_seed_int(&self->rng, seed_val); + Py_RETURN_NONE; +} + +static PyMethodDef Random_methods[] = { + {"randint", (PyCFunction) Random_randint, METH_FASTCALL, + PyDoc_STR("Return random integer in range [a, b], including both end points.") + }, + {"randrange", (PyCFunction) Random_randrange, METH_FASTCALL, + PyDoc_STR("Return random integer in range [start, stop).") + }, + {"random", (PyCFunction) Random_random, METH_NOARGS, + PyDoc_STR("Return random float in [0.0, 1.0).") + }, + {"seed", (PyCFunction) Random_seed, METH_FASTCALL, + PyDoc_STR("Seed the random number generator with an integer.") + }, + {NULL} /* Sentinel */ +}; + +static PyTypeObject RandomType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "Random", + .tp_doc = PyDoc_STR("Fast random number generator using ChaCha8"), + .tp_basicsize = sizeof(RandomObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = Random_new, + .tp_init = (initproc) Random_init, + .tp_methods = Random_methods, +}; + +// Module definition + +static PyMethodDef librt_random_module_methods[] = { + {"random", (PyCFunction) module_random, METH_NOARGS, + PyDoc_STR("Return random float in [0.0, 1.0) using thread-local RNG.") + }, + {"randint", (PyCFunction) module_randint, METH_FASTCALL, + PyDoc_STR("Return random integer in range [a, b] using thread-local RNG.") + }, + {"randrange", (PyCFunction) module_randrange, METH_FASTCALL, + PyDoc_STR("Return random integer in range [start, stop) using thread-local RNG.") + }, + {"seed", (PyCFunction) module_seed, METH_FASTCALL, + PyDoc_STR("Seed the thread-local RNG with an integer.") + }, + {NULL, NULL, 0, NULL} +}; + +// Module-level internal functions for mypyc primitives (use thread-local RNG) + +static double +module_random_internal(void) { + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return CPY_FLOAT_ERROR; + return random_double_impl(rng); +} + +static int64_t +module_randint_internal(int64_t a, int64_t b) { + if (unlikely(a > b)) { + PyErr_SetString(PyExc_ValueError, "empty range for randint()"); + return CPY_LL_INT_ERROR; + } + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return CPY_LL_INT_ERROR; + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return random_i64_from_range(rng, a, range); +} + +static int64_t +module_randrange1_internal(int64_t stop) { + if (unlikely(stop <= 0)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return CPY_LL_INT_ERROR; + return (int64_t)chacha8_next_ranged(rng, (uint64_t)stop); +} + +static int64_t +module_randrange2_internal(int64_t start, int64_t stop) { + if (unlikely(start >= stop)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return CPY_LL_INT_ERROR; + uint64_t range = (uint64_t)stop - (uint64_t)start; + return random_i64_from_range(rng, start, range); +} + +static int +random_abi_version(void) { + return LIBRT_RANDOM_ABI_VERSION; +} + +static int +random_api_version(void) { + return LIBRT_RANDOM_API_VERSION; +} + +static int +librt_random_module_exec(PyObject *m) +{ + if (ensure_tls_key() < 0) { + return -1; + } + if (PyType_Ready(&RandomType) < 0) { + return -1; + } + if (PyModule_AddObjectRef(m, "Random", (PyObject *) &RandomType) < 0) { + return -1; + } + // Export mypyc internal C API via capsule + static void *librt_random_api[LIBRT_RANDOM_API_LEN] = { + (void *)random_abi_version, + (void *)random_api_version, + (void *)Random_internal, + (void *)Random_from_seed_internal, + (void *)Random_type_internal, + (void *)Random_random_internal, + (void *)Random_randint_internal, + (void *)Random_randrange1_internal, + (void *)Random_randrange2_internal, + (void *)module_random_internal, + (void *)module_randint_internal, + (void *)module_randrange1_internal, + (void *)module_randrange2_internal, + }; + PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); + if (PyModule_Add(m, "_C_API", c_api_object) < 0) { + return -1; + } + return 0; +} + +static PyModuleDef_Slot librt_random_module_slots[] = { + {Py_mod_exec, librt_random_module_exec}, +#ifdef Py_MOD_GIL_NOT_USED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + +static PyModuleDef librt_random_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "random", + .m_doc = "Fast random number generation using ChaCha8", + .m_size = 0, + .m_methods = librt_random_module_methods, + .m_slots = librt_random_module_slots, +}; + +PyMODINIT_FUNC +PyInit_random(void) +{ + return PyModuleDef_Init(&librt_random_module); +} diff --git a/mypyc/lib-rt/random/librt_random.h b/mypyc/lib-rt/random/librt_random.h new file mode 100644 index 0000000000000..2eabfbd021bc9 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random.h @@ -0,0 +1,10 @@ +#ifndef LIBRT_RANDOM_H +#define LIBRT_RANDOM_H + +#include + +#define LIBRT_RANDOM_ABI_VERSION 1 +#define LIBRT_RANDOM_API_VERSION 9 +#define LIBRT_RANDOM_API_LEN 13 + +#endif // LIBRT_RANDOM_H diff --git a/mypyc/lib-rt/random/librt_random_api.c b/mypyc/lib-rt/random/librt_random_api.c new file mode 100644 index 0000000000000..157fa82b82eb3 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random_api.c @@ -0,0 +1,45 @@ +#include + +#include "librt_random_api.h" + +void *LibRTRandom_API[LIBRT_RANDOM_API_LEN] = {0}; + +int +import_librt_random(void) +{ + PyObject *mod = PyImport_ImportModule("librt.random"); + if (mod == NULL) + return -1; + Py_DECREF(mod); // we import just for the side effect of making the below work. + void **capsule = (void **)PyCapsule_Import("librt.random._C_API", 0); + if (capsule == NULL) + return -1; + + // Only after version validation succeeds can we safely copy the full table. + int (*abi_version)(void) = (int (*)(void))capsule[0]; + int (*api_version)(void) = (int (*)(void))capsule[1]; + if (abi_version() != LIBRT_RANDOM_ABI_VERSION) { + char err[128]; + snprintf(err, sizeof(err), "ABI version conflict for librt.random, expected %d, found %d", + LIBRT_RANDOM_ABI_VERSION, + abi_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + if (api_version() < LIBRT_RANDOM_API_VERSION) { + char err[128]; + snprintf(err, sizeof(err), + "API version conflict for librt.random, expected %d or newer, found %d (hint: upgrade librt)", + LIBRT_RANDOM_API_VERSION, + api_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + // Provider API version is >= our expected version, which (by the API + // compatibility contract) means it has at least LIBRT_RANDOM_API_LEN + // entries, so this copy is safe. + memcpy(LibRTRandom_API, capsule, sizeof(LibRTRandom_API)); + return 0; +} diff --git a/mypyc/lib-rt/random/librt_random_api.h b/mypyc/lib-rt/random/librt_random_api.h new file mode 100644 index 0000000000000..2794de0dd7e58 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random_api.h @@ -0,0 +1,32 @@ +#ifndef LIBRT_RANDOM_API_H +#define LIBRT_RANDOM_API_H + +#include +#include +#include +#include "librt_random.h" + +int +import_librt_random(void); + +extern void *LibRTRandom_API[LIBRT_RANDOM_API_LEN]; + +#define LibRTRandom_ABIVersion (*(int (*)(void)) LibRTRandom_API[0]) +#define LibRTRandom_APIVersion (*(int (*)(void)) LibRTRandom_API[1]) +#define LibRTRandom_Random_internal (*(PyObject* (*)(void)) LibRTRandom_API[2]) +#define LibRTRandom_Random_from_seed_internal (*(PyObject* (*)(int64_t)) LibRTRandom_API[3]) +#define LibRTRandom_Random_type_internal (*(PyTypeObject* (*)(void)) LibRTRandom_API[4]) +#define LibRTRandom_Random_random_internal (*(double (*)(PyObject*)) LibRTRandom_API[5]) +#define LibRTRandom_Random_randint_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[6]) +#define LibRTRandom_Random_randrange1_internal (*(int64_t (*)(PyObject*, int64_t)) LibRTRandom_API[7]) +#define LibRTRandom_Random_randrange2_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[8]) +#define LibRTRandom_module_random_internal (*(double (*)(void)) LibRTRandom_API[9]) +#define LibRTRandom_module_randint_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[10]) +#define LibRTRandom_module_randrange1_internal (*(int64_t (*)(int64_t)) LibRTRandom_API[11]) +#define LibRTRandom_module_randrange2_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[12]) + +static inline bool CPyRandom_Check(PyObject *obj) { + return Py_TYPE(obj) == LibRTRandom_Random_type_internal(); +} + +#endif // LIBRT_RANDOM_API_H diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index 49b6c10201317..371b322ca18b2 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -151,5 +151,18 @@ def run(self) -> None: Extension( "librt.time", ["time/librt_time.c"], include_dirs=["."], extra_compile_args=cflags ), + Extension( + "librt.random", + [ + "random/librt_random.c", + "init.c", + "int_ops.c", + "exc_ops.c", + "pythonsupport.c", + "getargsfast.c", + ], + include_dirs=["."], + extra_compile_args=cflags, + ), ] ) diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py new file mode 100644 index 0000000000000..6aaee84ecd0d6 --- /dev/null +++ b/mypyc/primitives/librt_random_ops.py @@ -0,0 +1,104 @@ +from mypyc.ir.deps import LIBRT_RANDOM +from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER +from mypyc.ir.rtypes import float_rprimitive, int64_rprimitive, random_rprimitive +from mypyc.primitives.registry import function_op, method_op + +# Random() -- construct with OS entropy +function_op( + name="librt.random.Random", + arg_types=[], + return_type=random_rprimitive, + c_function_name="LibRTRandom_Random_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Random(seed) -- construct with integer seed +function_op( + name="librt.random.Random", + arg_types=[int64_rprimitive], + return_type=random_rprimitive, + c_function_name="LibRTRandom_Random_from_seed_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Random.randint(a, b) -- return random integer in [a, b] +method_op( + name="randint", + arg_types=[random_rprimitive, int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randint_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Random.randrange(stop) -- return random integer in [0, stop) +method_op( + name="randrange", + arg_types=[random_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randrange1_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Random.randrange(start, stop) -- return random integer in [start, stop) +method_op( + name="randrange", + arg_types=[random_rprimitive, int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randrange2_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Random.random() -- return random float in [0.0, 1.0) +method_op( + name="random", + arg_types=[random_rprimitive], + return_type=float_rprimitive, + c_function_name="LibRTRandom_Random_random_internal", + error_kind=ERR_NEVER, + dependencies=[LIBRT_RANDOM], +) + +# Module-level random() -- return random float using thread-local RNG +function_op( + name="librt.random.random", + arg_types=[], + return_type=float_rprimitive, + c_function_name="LibRTRandom_module_random_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randrange(stop) -- return random integer using thread-local RNG +function_op( + name="librt.random.randrange", + arg_types=[int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randrange1_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randrange(start, stop) -- return random integer using thread-local RNG +function_op( + name="librt.random.randrange", + arg_types=[int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randrange2_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randint(a, b) -- return random integer using thread-local RNG +function_op( + name="librt.random.randint", + arg_types=[int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randint_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_RANDOM], +) diff --git a/mypyc/primitives/registry.py b/mypyc/primitives/registry.py index c04b4ff65a757..e22a044d9bb27 100644 --- a/mypyc/primitives/registry.py +++ b/mypyc/primitives/registry.py @@ -403,6 +403,7 @@ def load_global_op(name: str, type: RType, src: str) -> LoadAddressDescription: import mypyc.primitives.dict_ops import mypyc.primitives.float_ops import mypyc.primitives.int_ops +import mypyc.primitives.librt_random_ops import mypyc.primitives.librt_strings_ops import mypyc.primitives.librt_time_ops import mypyc.primitives.librt_vecs_ops diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test new file mode 100644 index 0000000000000..9215c13c88d6e --- /dev/null +++ b/mypyc/test-data/irbuild-librt-random.test @@ -0,0 +1,119 @@ +[case testLibrtRandomConstructor_64bit] +from librt.random import Random + +def make_random() -> Random: + return Random() +[out] +def make_random(): + r0 :: librt.random.Random +L0: + r0 = LibRTRandom_Random_internal() + return r0 + +[case testLibrtRandomConstructorWithSeed_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def make_random_seeded(n: i64) -> Random: + return Random(n) +[out] +def make_random_seeded(n): + n :: i64 + r0 :: librt.random.Random +L0: + r0 = LibRTRandom_Random_from_seed_internal(n) + return r0 + +[case testLibrtRandomRandrange_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def randrange1(r: Random, stop: i64) -> i64: + return r.randrange(stop) +def randrange2(r: Random, start: i64, stop: i64) -> i64: + return r.randrange(start, stop) +[out] +def randrange1(r, stop): + r :: librt.random.Random + stop, r0 :: i64 +L0: + r0 = LibRTRandom_Random_randrange1_internal(r, stop) + return r0 +def randrange2(r, start, stop): + r :: librt.random.Random + start, stop, r0 :: i64 +L0: + r0 = LibRTRandom_Random_randrange2_internal(r, start, stop) + return r0 + +[case testLibrtRandomRandint_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def randint(r: Random, a: i64, b: i64) -> i64: + return r.randint(a, b) +[out] +def randint(r, a, b): + r :: librt.random.Random + a, b, r0 :: i64 +L0: + r0 = LibRTRandom_Random_randint_internal(r, a, b) + return r0 + +[case testLibrtRandomRandom_64bit] +from librt.random import Random + +def rand(r: Random) -> float: + return r.random() +[out] +def rand(r): + r :: librt.random.Random + r0 :: float +L0: + r0 = LibRTRandom_Random_random_internal(r) + return r0 + +[case testLibrtRandomModuleRandom_64bit] +from librt.random import random + +def module_random() -> float: + return random() +[out] +def module_random(): + r0 :: float +L0: + r0 = LibRTRandom_module_random_internal() + return r0 + +[case testLibrtRandomModuleRandint_64bit] +from librt.random import randint +from mypy_extensions import i64 + +def module_randint(a: i64, b: i64) -> i64: + return randint(a, b) +[out] +def module_randint(a, b): + a, b, r0 :: i64 +L0: + r0 = LibRTRandom_module_randint_internal(a, b) + return r0 + +[case testLibrtRandomModuleRandrange_64bit] +from librt.random import randrange +from mypy_extensions import i64 + +def module_randrange1(stop: i64) -> i64: + return randrange(stop) +def module_randrange2(start: i64, stop: i64) -> i64: + return randrange(start, stop) +[out] +def module_randrange1(stop): + stop, r0 :: i64 +L0: + r0 = LibRTRandom_module_randrange1_internal(stop) + return r0 +def module_randrange2(start, stop): + start, stop, r0 :: i64 +L0: + r0 = LibRTRandom_module_randrange2_internal(start, stop) + return r0 diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test new file mode 100644 index 0000000000000..0b34222678018 --- /dev/null +++ b/mypyc/test-data/run-librt-random.test @@ -0,0 +1,344 @@ +[case testRandom_librt] +from typing import Any + +from librt.random import Random, random, randint, randrange, seed +from mypy_extensions import i64 +from testutil import assertRaises + +# +# Random object basics +# + +def test_random_construct() -> None: + r = Random() + assert isinstance(r, Random) + +def test_randint_basic() -> None: + r = Random() + for i in range(100): + val = r.randint(0, 10) + assert 0 <= val <= 10 + +def test_randint_single_value() -> None: + r = Random() + for i in range(10): + assert r.randint(5, 5) == 5 + +def test_randint_negative_range() -> None: + r = Random() + for i in range(100): + val = r.randint(-10, -1) + assert -10 <= val <= -1 + +def test_randint_mixed_range() -> None: + r = Random() + for i in range(100): + val = r.randint(-5, 5) + assert -5 <= val <= 5 + +def test_randint_large_range() -> None: + r = Random() + for i in range(100): + val = r.randint(0, 1000000) + assert 0 <= val <= 1000000 + +def test_randint_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.randint(0, 1000000)) + # With range 0-1000000 and 100 samples, we should get at least 2 distinct values + assert len(values) > 1 + +def test_random_basic() -> None: + r = Random() + for i in range(100): + val = r.random() + assert 0.0 <= val < 1.0 + +def test_random_returns_float() -> None: + r = Random() + val = r.random() + assert isinstance(val, float) + +def test_random_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.random()) + assert len(values) > 1 + +def test_randrange_one_arg() -> None: + r = Random() + for i in range(100): + val = r.randrange(10) + assert 0 <= val < 10 + +def test_randrange_two_args() -> None: + r = Random() + for i in range(100): + val = r.randrange(5, 15) + assert 5 <= val < 15 + +def test_randrange_negative() -> None: + r = Random() + for i in range(100): + val = r.randrange(-10, 0) + assert -10 <= val < 0 + +def test_randrange_single_value() -> None: + r = Random() + for i in range(10): + assert r.randrange(7, 8) == 7 + +def test_randrange_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.randrange(1000000)) + assert len(values) > 1 + +def test_constructor_seed() -> None: + r1 = Random(42) + r2 = Random(42) + vals1 = [r1.randint(0, 1000000) for _ in range(20)] + vals2 = [r2.randint(0, 1000000) for _ in range(20)] + assert vals1 == vals2 + +def test_constructor_seed_different() -> None: + r1 = Random(42) + r2 = Random(43) + vals1 = [r1.randint(0, 1000000) for _ in range(20)] + vals2 = [r2.randint(0, 1000000) for _ in range(20)] + assert vals1 != vals2 + +def test_constructor_none_seed() -> None: + r = Random(None) + val = r.random() + assert 0.0 <= val < 1.0 + +def test_seed_method() -> None: + r = Random(0) + r.seed(42) + vals1 = [r.randint(0, 1000000) for _ in range(20)] + r.seed(42) + vals2 = [r.randint(0, 1000000) for _ in range(20)] + assert vals1 == vals2 + +def test_seed_method_resets_state() -> None: + r = Random(42) + expected = [r.randint(0, 1000000) for _ in range(20)] + # Consume some values, then reseed + r.seed(42) + actual = [r.randint(0, 1000000) for _ in range(20)] + assert expected == actual + +# +# Module-level functions +# + +def test_module_random_basic() -> None: + for i in range(100): + val = random() + assert 0.0 <= val < 1.0 + +def test_module_random_returns_float() -> None: + assert isinstance(random(), float) + +def test_module_random_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(random()) + assert len(values) > 1 + +def test_module_randint_basic() -> None: + for i in range(100): + val = randint(0, 10) + assert 0 <= val <= 10 + +def test_module_randint_single_value() -> None: + for i in range(10): + assert randint(5, 5) == 5 + +def test_module_randint_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(randint(0, 1000000)) + assert len(values) > 1 + +def test_module_randrange_one_arg() -> None: + for i in range(100): + val = randrange(10) + assert 0 <= val < 10 + +def test_module_randrange_two_args() -> None: + for i in range(100): + val = randrange(5, 15) + assert 5 <= val < 15 + +def test_module_randrange_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(randrange(1000000)) + assert len(values) > 1 + +def test_module_seed_reproducible() -> None: + seed(42) + vals1 = [randint(0, 1000000) for _ in range(20)] + seed(42) + vals2 = [randint(0, 1000000) for _ in range(20)] + assert vals1 == vals2 + +def test_module_seed_different() -> None: + seed(42) + vals1 = [randint(0, 1000000) for _ in range(20)] + seed(43) + vals2 = [randint(0, 1000000) for _ in range(20)] + assert vals1 != vals2 + +# +# Wrapper function calling convention (via Any) +# + +def test_method_random_via_wrapper() -> None: + r: Any = Random(42) + val = r.random() + assert isinstance(val, float) + assert 0.0 <= val < 1.0 + +def test_method_seed_via_wrapper() -> None: + r: Any = Random(0) + r.seed(42) + val = r.random() + assert 0.0 <= val < 1.0 + +def test_module_random_via_wrapper() -> None: + random_any: Any = random + val = random_any() + assert isinstance(val, float) + assert 0.0 <= val < 1.0 + +def test_module_randint_via_wrapper() -> None: + randint_any: Any = randint + val = randint_any(0, 10) + assert 0 <= val <= 10 + +def test_module_seed_via_wrapper() -> None: + seed_any: Any = seed + seed_any(42) + +# +# Wide i64 ranges +# + +def method_randint(r: Random, a: i64, b: i64) -> i64: + return r.randint(a, b) + +def method_randrange(r: Random, a: i64, b: i64) -> i64: + return r.randrange(a, b) + +def module_randint(a: i64, b: i64) -> i64: + return randint(a, b) + +def module_randrange(a: i64, b: i64) -> i64: + return randrange(a, b) + +def test_full_i64_randint_native() -> None: + lo: i64 = -9223372036854775808 + hi: i64 = 9223372036854775807 + r = Random(42) + saw_non_min = False + for i in range(20): + val = method_randint(r, lo, hi) + assert lo <= val <= hi + if val != lo: + saw_non_min = True + assert saw_non_min + +def test_full_i64_randint_module_native() -> None: + lo: i64 = -9223372036854775808 + hi: i64 = 9223372036854775807 + saw_non_min = False + for i in range(20): + val = module_randint(lo, hi) + assert lo <= val <= hi + if val != lo: + saw_non_min = True + assert saw_non_min + +def test_wide_i64_randrange_native() -> None: + lo: i64 = -9223372036854775808 + hi: i64 = 9223372036854775807 + r = Random(43) + for i in range(20): + val = method_randrange(r, lo, hi) + assert lo <= val < hi + val = module_randrange(lo, hi) + assert lo <= val < hi + +def test_full_i64_randint_python_api() -> None: + r: Any = Random(42) + lo = -9223372036854775808 + hi = 9223372036854775807 + saw_non_min = False + for i in range(20): + val = r.randint(lo, hi) + assert lo <= val <= hi + if val != lo: + saw_non_min = True + assert saw_non_min + +def test_wide_i64_randrange_python_api() -> None: + r: Any = Random(43) + randrange_any: Any = randrange + lo = -9223372036854775808 + hi = 9223372036854775807 + for i in range(20): + val = r.randrange(lo, hi) + assert lo <= val < hi + val = randrange_any(lo, hi) + assert lo <= val < hi + +# +# Error handling +# + +def test_randint_empty_range() -> None: + r = Random() + with assertRaises(ValueError, "empty range"): + r.randint(10, 5) + +def test_randint_wrong_arg_count() -> None: + r = Random() + with assertRaises(TypeError): + r.randint(1) # type: ignore[call-arg] + with assertRaises(TypeError): + r.randint(1, 2, 3) # type: ignore[call-arg] + +def test_module_randint_empty_range() -> None: + with assertRaises(ValueError, "empty range"): + randint(10, 5) + +def test_randrange_empty_range() -> None: + r = Random() + with assertRaises(ValueError, "empty range"): + r.randrange(0) + with assertRaises(ValueError, "empty range"): + r.randrange(-5) + with assertRaises(ValueError, "empty range"): + r.randrange(10, 10) + with assertRaises(ValueError, "empty range"): + r.randrange(10, 5) + +def test_randrange_wrong_arg_count() -> None: + r = Random() + with assertRaises(TypeError): + r.randrange() # type: ignore[call-overload] + with assertRaises(TypeError): + r.randrange(1, 2, 3) # type: ignore[call-overload] + +def test_module_randrange_empty_range() -> None: + with assertRaises(ValueError, "empty range"): + randrange(0) + with assertRaises(ValueError, "empty range"): + randrange(10, 5) diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index f1f0ec777c3da..7e3993e267e74 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -59,6 +59,7 @@ "irbuild-math.test", "irbuild-weakref.test", "irbuild-librt-strings.test", + "irbuild-librt-random.test", "irbuild-base64.test", "irbuild-time.test", "irbuild-match.test", diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index 8fb861f5c2aae..e7be5fcf8425a 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -81,6 +81,7 @@ "run-librt-strings.test", "run-base64.test", "run-librt-time.test", + "run-librt-random.test", "run-match.test", "run-vecs-i64-interp.test", "run-vecs-misc-interp.test", From db297c5f14415ffdb8352fb3aafd34cbb4254b19 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Fri, 8 May 2026 20:22:49 +0300 Subject: [PATCH 002/127] [mypyc] Enable incremental self-compilation (#21369) Six fixes on top of #21299, all required to self-compile mypy or to install a `separate=True` wheel via pip. - `mypyc/build.py`: pip invokes `setup.py` twice when building a wheel. On the second invocation mypy's incremental cache is fully warm, so we generate no new C source for any group; the resulting extensions ship without their entry points and import as stubs. - **Fix**: when a group emits no C source, reuse the .c file from the previous pass. - `mypyc/codegen/{emit,emitfunc}.py`: when code in one compiled group reads an attribute on an object whose class lives in another group, the generated cast depends on that other group's struct definitions. We weren't recording the dependency, so the C compiler couldn't see the layout and the build failed. - **Fix**: register the dependency at the cast site. - `mypyc/codegen/emitmodule.py`: when mypy compiles itself, a generated shim file can share a basename with a runtime C file. The C compiler resolves the runtime include relative to the shim's directory and picks up the shim instead. - **Fix**: search the include path explicitly so shims can't shadow runtime files. - `mypyc/lib-rt/misc_ops.c`: each compiled module gets its own shared library next to it in the package tree. The runtime was computing the module's file path as if a single shared library sat above the whole package, which doubled the package prefix and broke submodule lookups. - **Fix**: detect the per-module case and use only the module's leaf name. - `mypyc/irbuild/prepare.py`: traits and builtin-derived classes don't get a real C constructor emitted. A clean build sidesteps that, but a fully cached rebuild was taking the direct-call path and producing C that referenced a constructor that doesn't exist. - **Fix**: skip the registration the same way a clean build does. - `mypyc/build.py`: on every build_ext, setuptools rewrites every compiled .so in the source tree even when nothing changed. On macOS this invalidates the OS signature cache, so every import on the next run pays a re-verification cost. - **Fix**: skip the copy when source and destination already match, taking a 1-line edit rebuild from ~72s to ~6s. This is really a `setup` tools limitation though (relevant [mypy issue](https://github.com/mypyc/mypyc/issues/1068) ?) I also added a `MYPYC_SEPARATE` env knob so CI can exercise the codegen path against mypy itself. ## Benchmarks Mypy self-compile on macOS, `MYPYC_OPT_LEVEL=0`, `-j 11`. Three scenarios: | | monolithic | separate=True | |:---:|:---:|:---:| | Clean build | 180s | 108s | | No-op rebuild | 124s | 5s | | 1-line edit | 106s | 6s | --- mypyc/build.py | 87 +++++++++++++++++++++++++++++++++++-- mypyc/codegen/emit.py | 12 +++++ mypyc/codegen/emitfunc.py | 5 +++ mypyc/codegen/emitmodule.py | 44 ++++++++++++++++--- mypyc/irbuild/prepare.py | 7 ++- mypyc/lib-rt/misc_ops.c | 60 ++++++++++++++++++++----- setup.py | 2 + 7 files changed, 198 insertions(+), 19 deletions(-) diff --git a/mypyc/build.py b/mypyc/build.py index 08eeb13c91752..84633086d2724 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -450,6 +450,70 @@ def write_file(path: str, contents: str) -> None: os.utime(path, times=(new_mtime, new_mtime)) +_MYPYC_EXTENSION_MARKER = "_mypyc_skip_redundant_inplace_copy" +_setuptools_patch_applied = False + + +def _patch_setuptools_copy_extensions_to_source() -> None: + """Skip redundant `.so` copies for extensions we generated. + + setuptools' copy_extensions_to_source rewrites every `.so` in the + source tree on every build_ext, even when nothing changed. On macOS + this invalidates AMFI's signature cache (~100 ms re-verification per + `.so` on the next import), eating most of the separate=True + incremental speedup. + + The patch is global because copy_extensions_to_source runs during + setup()'s build_ext command, after mypycify() has already returned; + we can't scope a context manager around it. Instead the skip only + fires for extensions tagged by mypycify (via the marker attribute), + so other setuptools users in the same setup.py see the unmodified + upstream behavior, including stub writes. + """ + global _setuptools_patch_applied + if _setuptools_patch_applied: + return + _setuptools_patch_applied = True + + from setuptools.command.build_ext import build_ext as _build_ext + + original = _build_ext.copy_extensions_to_source + + def _files_match(a: str, b: str) -> bool: + try: + sa = os.stat(a) + sb = os.stat(b) + except OSError: + return False + # Compare size + whole-second mtime. distutils' copy_file + # propagates the source mtime, but macOS drops sub-second + # precision on write so the float values never match verbatim. + return sa.st_size == sb.st_size and int(sa.st_mtime) == int(sb.st_mtime) + + def patched(self: Any) -> None: + build_py = self.get_finalized_command("build_py") + + def is_redundant(ext: Any) -> bool: + if not getattr(ext, _MYPYC_EXTENSION_MARKER, False): + return False + inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext) + return _files_match(regular_file, inplace_file) + + # Hide our already-fresh extensions from setuptools' loop and + # let it handle whatever's left. Delegating instead of + # reimplementing the body means future setuptools changes carry + # over for free. self.extensions is restored before we return + # so anything that inspects it later sees the original list. + saved = self.extensions + self.extensions = [ext for ext in saved if not is_redundant(ext)] + try: + original(self) + finally: + self.extensions = saved + + _build_ext.copy_extensions_to_source = patched # type: ignore[method-assign] + + def construct_groups( sources: list[BuildSource], separate: bool | list[tuple[list[str], str | None]], @@ -513,7 +577,7 @@ def get_header_deps(cfiles: list[tuple[str, str]]) -> list[str]: """ headers: set[str] = set() for _, contents in cfiles: - headers.update(re.findall(r'#include "(.*)"', contents)) + headers.update(re.findall(r'#include [<"]([^>"]+)[>"]', contents)) return sorted(headers) @@ -573,12 +637,21 @@ def mypyc_build( cfilenames = [] for cfile, ctext in cfiles: cfile = os.path.join(compiler_options.target_dir, cfile) - if not options.mypyc_skip_c_generation: + # Empty contents marks a file the previous run already wrote + # (fully-cached group): skip the rewrite and just reuse it. + if ctext and not options.mypyc_skip_c_generation: write_file(cfile, ctext) if os.path.splitext(cfile)[1] == ".c": cfilenames.append(cfile) - deps = [os.path.join(compiler_options.target_dir, dep) for dep in get_header_deps(cfiles)] + # The header regex matches both quote styles, so the result can + # include system headers like `` that don't live under + # target_dir. Joining those produces non-existent paths which + # would force a full rebuild on every run via Extension.depends. + candidate_deps = ( + os.path.join(compiler_options.target_dir, dep) for dep in get_header_deps(cfiles) + ) + deps = [d for d in candidate_deps if os.path.exists(d)] group_cfilenames.append((cfilenames, deps)) return groups, group_cfilenames, source_deps @@ -755,6 +828,9 @@ def mypycify( have no backward compatibility guarantees! """ + # Skip redundant inplace .so copies on every build_ext invocation. + _patch_setuptools_copy_extensions_to_source() + # Figure out our configuration compiler_options = CompilerOptions( strip_asserts=strip_asserts, @@ -869,4 +945,9 @@ def mypycify( ) ) + # Tag every extension we own so the build_ext patch knows it's + # safe to skip the redundant inplace copy for these specifically. + for ext in extensions: + setattr(ext, _MYPYC_EXTENSION_MARKER, True) + return extensions diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 45ff34ab045d5..01cf3593a8d60 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -326,6 +326,18 @@ def get_group_prefix(self, obj: ClassIR | FuncDecl) -> str: # See docs above return self.get_module_group_prefix(obj.module_name) + def register_group_dep(self, cl: ClassIR) -> None: + """Record `cl`'s defining group as a cross-group dep, if any. + + Call this when emitting code that refers to `cl`'s struct + layout: the .c file consuming that layout needs the defining + group's `__native_*.h` included, and group_deps drives which + headers get pulled in. + """ + target_group = self.context.group_map.get(cl.module_name) + if target_group and target_group != self.context.group_name: + self.context.group_deps.add(target_group) + def static_name(self, id: str, module: str | None, prefix: str = STATIC_PREFIX) -> str: """Create name of a C static variable. diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index e4a8922a103d4..dcb606f6ab51b 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -360,6 +360,11 @@ def get_attr_expr(self, obj: str, op: GetAttr | SetAttr, decl_cl: ClassIR) -> st classes, and *(obj + attr_offset) for attributes defined by traits. We also insert all necessary C casts here. """ + # The struct cast below needs the defining group's __native.h + # included by the consuming .c file. Record both the receiver + # and declaring classes as cross-group deps. + self.emitter.register_group_dep(op.class_type.class_ir) + self.emitter.register_group_dep(decl_cl) cast = f"({op.class_type.struct_name(self.emitter.names)} *)" if decl_cl.is_trait and op.class_type.class_ir.is_trait: # For pure trait access find the offset first, offsets diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 3f10df7fa8c98..fa0a4385f4fb5 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -363,7 +363,12 @@ def compile_ir_to_c( if source.module in modules } if not group_modules: - ctext[group_name] = [] + # Fully-cached group (e.g. pip's second setup.py invoke for + # the wheel phase): no fresh IR was produced. Reuse the file + # list recorded in any module's IR cache so the linker still + # sees the previous run's outputs; empty content is a "do + # not rewrite" sentinel for mypyc_build. + ctext[group_name] = _load_cached_group_files(group_sources, result) continue generator = GroupGenerator( group_modules, source_paths, group_name, mapper.group_map, names, compiler_options @@ -373,6 +378,32 @@ def compile_ir_to_c( return ctext +def _load_cached_group_files( + group_sources: list[BuildSource], result: BuildResult +) -> list[tuple[str, str]]: + """Read the .c/.h paths recorded for this group on the previous run. + + All modules in a group share the same src_hashes map, so the first + readable IR cache is sufficient. Returns paths paired with empty + content so callers can distinguish "reuse on disk" from "newly + generated". + """ + for source in group_sources: + state = result.graph.get(source.module) + if state is None: + continue + try: + ir_json = result.manager.metastore.read(get_state_ir_cache_name(state)) + except (FileNotFoundError, OSError): + continue + try: + ir_data = json.loads(ir_json) + except json.JSONDecodeError: + continue + return [(path, "") for path in ir_data.get("src_hashes", {})] + return [] + + def get_ir_cache_name(id: str, path: str, options: Options) -> str: meta_path, _, _ = get_cache_names(id, path, options) # Mypyc uses JSON cache even with --fixed-format-cache (for now). @@ -615,16 +646,19 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]: base_emitter = Emitter(self.context) # Optionally just include the runtime library c files to - # reduce the number of compiler invocations needed + # reduce the number of compiler invocations needed. + # Use <> form (only -I paths) so a shim file with the same + # basename as a runtime file can't shadow it. Triggered by + # mypyc/lower/int_ops.py vs lib-rt/int_ops.c on mypy self-compile. if self.compiler_options.include_runtime_files: for name in RUNTIME_C_FILES: - base_emitter.emit_line(f'#include "{name}"') + base_emitter.emit_line(f"#include <{name}>") # Include conditional source files source_deps = collect_source_dependencies(self.modules) for source_dep in sorted(source_deps, key=lambda d: d.path): - base_emitter.emit_line(f'#include "{source_dep.path}"') + base_emitter.emit_line(f"#include <{source_dep.path}>") if self.compiler_options.depends_on_librt_internal: - base_emitter.emit_line('#include "internal/librt_internal_api.c"') + base_emitter.emit_line("#include ") base_emitter.emit_line(f'#include "__native{self.short_group_suffix}.h"') base_emitter.emit_line(f'#include "__native_internal{self.short_group_suffix}.h"') emitter = base_emitter diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 09bfc8339b404..f143ce1b44025 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -182,7 +182,12 @@ def load_type_map(mapper: Mapper, modules: list[MypyFile], deser_ctx: DeserMaps) continue mapper.type_to_ir[node.node] = ir mapper.symbol_fullnames.add(node.node.fullname) - mapper.func_to_decl[node.node] = ir.ctor + # Trait/builtin-base classes have an ir.ctor FuncDecl + # but no emitted CPyDef_, so a cross-group direct + # call would hit an undefined symbol. Mirror the skip + # in prepare_init_method. + if not ir.is_trait and not ir.builtin_base: + mapper.func_to_decl[node.node] = ir.ctor for module in modules: for func in get_module_func_defs(module): diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index 2aaadb2ac47d2..392dba0deca4c 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -1281,12 +1281,17 @@ static int CPyImport_SetModuleFile(PyObject *modobj, PyObject *module_name, Py_DECREF(file); return 0; } - // Derive __file__ from the shared library's __file__ (for its - // directory), the module name (dots -> path separators), and the - // extension suffix. E.g. for module "a.b.c", shared lib - // "/path/to/group__mypyc.cpython-312-x86_64-linux-gnu.so", - // suffix ".cpython-312-x86_64-linux-gnu.so": - // => "/path/to/a/b/c.cpython-312-x86_64-linux-gnu.so" + // Derive __file__ from the shared lib's directory, the module + // name, and the extension suffix. Two layouts: + // + // Monolithic: one shared lib above the package tree holds many + // modules, so append the full dotted module path. + // separate=True: each module has its own "__mypyc.so" + // next to the module, so dirname(shared_lib) is already inside + // the parent package. Append only the last segment. + // + // Detect the separate=True case by matching the shared lib's + // basename against "__mypyc". PyObject *derived_file = NULL; if (shared_lib_file != NULL && shared_lib_file != Py_None && PyUnicode_Check(shared_lib_file)) { @@ -1314,30 +1319,65 @@ static int CPyImport_SetModuleFile(PyObject *modobj, PyObject *module_name, if (module_path == NULL) { return -1; } + + // Compute the module's last dotted segment for the separate=True check. + Py_ssize_t name_len = PyUnicode_GetLength(module_name); + Py_ssize_t last_dot = PyUnicode_FindChar(module_name, '.', 0, name_len, -1); + PyObject *last_segment; + if (last_dot >= 0) { + last_segment = PyUnicode_Substring(module_name, last_dot + 1, name_len); + } else { + last_segment = module_name; + Py_INCREF(last_segment); + } + if (last_segment == NULL) { + Py_DECREF(module_path); + return -1; + } + // Compare shared_lib_file basename against "__mypyc". + PyObject *expected_basename = PyUnicode_FromFormat( + "%U__mypyc%U", last_segment, ext_suffix); + PyObject *actual_basename; + if (sep >= 0) { + actual_basename = PyUnicode_Substring(shared_lib_file, sep + 1, sf_len); + } else { + actual_basename = shared_lib_file; + Py_INCREF(actual_basename); + } + int is_per_module_lib = 0; + if (expected_basename != NULL && actual_basename != NULL) { + is_per_module_lib = + (PyUnicode_Compare(expected_basename, actual_basename) == 0); + } + Py_XDECREF(expected_basename); + Py_XDECREF(actual_basename); + // For packages, __file__ should point to __init__, // e.g. "a/b/__init__.cpython-312-x86_64-linux-gnu.so". + PyObject *file_path = is_per_module_lib ? last_segment : module_path; if (sep >= 0) { PyObject *dir = PyUnicode_Substring(shared_lib_file, 0, sep); if (dir != NULL) { if (is_package) { derived_file = PyUnicode_FromFormat( "%U%c%U%c__init__%U", dir, (int)sep_char, - module_path, (int)sep_char, ext_suffix); + file_path, (int)sep_char, ext_suffix); } else { derived_file = PyUnicode_FromFormat( "%U%c%U%U", dir, (int)sep_char, - module_path, ext_suffix); + file_path, ext_suffix); } Py_DECREF(dir); } } else { if (is_package) { derived_file = PyUnicode_FromFormat( - "%U%c__init__%U", module_path, (int)SEP[0], ext_suffix); + "%U%c__init__%U", file_path, (int)SEP[0], ext_suffix); } else { - derived_file = PyUnicode_FromFormat("%U%U", module_path, ext_suffix); + derived_file = PyUnicode_FromFormat("%U%U", file_path, ext_suffix); } } + Py_DECREF(last_segment); Py_DECREF(module_path); } if (derived_file == NULL && !PyErr_Occurred()) { diff --git a/setup.py b/setup.py index d36a6bfa2c2dc..1879f6892ba8f 100644 --- a/setup.py +++ b/setup.py @@ -153,6 +153,7 @@ def run(self) -> None: debug_level = os.getenv("MYPYC_DEBUG_LEVEL", "1") force_multifile = os.getenv("MYPYC_MULTI_FILE", "") == "1" log_trace = bool(int(os.getenv("MYPYC_LOG_TRACE", "0"))) + separate = os.getenv("MYPYC_SEPARATE", "") == "1" ext_modules = mypycify( mypyc_targets + ["--config-file=mypy_bootstrap.ini"], opt_level=opt_level, @@ -161,6 +162,7 @@ def run(self) -> None: # our Appveyor builds run out of memory sometimes. multi_file=sys.platform == "win32" or force_multifile, log_trace=log_trace, + separate=separate, # Mypy itself is allowed to use native_internal extension. depends_on_librt_internal=True, ) From 284a6cc9e83fdc021b065c333fe44563b2cc67bd Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 8 May 2026 20:36:19 +0100 Subject: [PATCH 003/127] Respect file config comments for stale modules (#21444) In mypy 1.19, re-running `mypy` with a saved cache and a file containing an inline configuration comment (e.g. `# mypy: disable-error-code="import-not-found"`) acted 'correctly', i.e. the same behaviour on the first run vs subsequent runs. In mypy 1.20 and newer, this is no longer the case. Running `mypy` with a pre-existing cache fails to respect inline configuration comments for stale modules, leading to confusing false-positive errors that can be hard to debug. This PR introduces a failing test as at current master, and then a fix for the issue. I believe this is the right fix, though happy to change as suggested by the maintainers, I'm not nearly as familiar with mypy internals. Notably also from L2376-80, it's by design that file-level comments aren't cached: ```python # Note that the options we store in the cache are the options as # specified by the command line/config file and *don't* reflect # updates made by inline config directives in the file. This is # important, or otherwise the options would never match when # verifying the cache. ``` I believe that this regression was introduced in #20773 A Co-authored-by: Adam Turner --- mypy/build.py | 6 +++--- test-data/unit/check-incremental.test | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 21a5559b329a3..8d5db0bab8dfa 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -4769,13 +4769,13 @@ def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None: t2 = time.time() stale = scc + # Parse before verify_dependencies so that inline config comments + # (e.g. "# mypy: disable-error-code") are applied to options. + manager.parse_all([graph[id] for id in stale], post_parse=False) for id in stale: # Re-generate import errors in case this module was loaded from the cache. if graph[id].meta: graph[id].verify_dependencies(suppressed_only=True) - # We may already have parsed the modules, or not. - # If the former, parse_file() is a no-op. - manager.parse_all([graph[id] for id in stale], post_parse=False) if "typing" in scc: # For historical reasons we need to manually add typing aliases # for built-in generic collections, see docstring of diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 6911a350376f3..db15b73419109 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -7995,3 +7995,19 @@ import mod [out2] main:2: error: Cannot find implementation or library stub for module named "mod" main:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports + +[case testIncrementalFileConfigCommentsStale] +-- When a dependency changes, the importing module becomes stale and is +-- reprocessed via process_stale_scc. As inline config comments are not cached +-- (by design), moving the order of processing the stale SCC can accidentally +-- break file config comments on subsequent runs. +# mypy: disable-error-code="import-not-found" +import nonexistent +import b +[file b.py] +x = 1 +[file b.py.2] +x = "hello" +[builtins fixtures/module.pyi] +[stale b] +[rechecked b] From 46acbe85c0e1703ebf2e6d4c699772edcdcf4652 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 9 May 2026 10:22:08 +0200 Subject: [PATCH 004/127] Start testing Python 3.15 (#21439) The first beta for Python 3.15 was released yesterday. Start running CI tests for it. --- .github/workflows/test.yml | 7 +++++++ mypyc/codegen/emit.py | 2 +- mypyc/lib-rt/byteswriter_extra_ops.h | 2 +- mypyc/lib-rt/function_wrapper.c | 2 +- mypyc/lib-rt/stringwriter_extra_ops.h | 2 +- mypyc/test-data/run-misc.test | 5 ++++- mypyc/test-data/run-python312.test | 3 ++- pyproject.toml | 1 + test-data/unit/cmdline.test | 4 ++-- test-requirements.txt | 4 ++-- tox.ini | 1 + 11 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5242739f8f846..27fa756b7f607 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,12 @@ jobs: toxenv: py tox_extra_args: "-n 4" test_mypyc: true + - name: Test suite with py315-ubuntu, mypyc-compiled + python: '3.15' + os: ubuntu-24.04-arm + toxenv: py + tox_extra_args: "-n 4" + test_mypyc: true - name: Test suite with py314t-ubuntu, mypyc-compiled python: '3.14t' os: ubuntu-24.04-arm @@ -196,6 +202,7 @@ jobs: if: ${{ !(matrix.debug_build || endsWith(matrix.python, '-dev')) }} with: python-version: ${{ matrix.python }} + allow-prereleases: true - name: Install tox run: | diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 01cf3593a8d60..b89c91343e66c 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -245,7 +245,7 @@ def object_annotation(self, obj: object, line: str) -> str: If it contains illegal characters, an empty string is returned.""" line_width = self._indent + len(line) - formatted = pprint.pformat(obj, compact=True, width=max(90 - line_width, 20)) + formatted = pprint.pformat(obj, compact=True, indent=1, width=max(90 - line_width, 20)) if any(x in formatted for x in ("/*", "*/", "\0")): return "" diff --git a/mypyc/lib-rt/byteswriter_extra_ops.h b/mypyc/lib-rt/byteswriter_extra_ops.h index dc715600653d5..4aec322a730cd 100644 --- a/mypyc/lib-rt/byteswriter_extra_ops.h +++ b/mypyc/lib-rt/byteswriter_extra_ops.h @@ -1,9 +1,9 @@ #ifndef BYTESWRITER_EXTRA_OPS_H #define BYTESWRITER_EXTRA_OPS_H +#include #include #include -#include #include "mypyc_util.h" #include "strings/librt_strings_api.h" diff --git a/mypyc/lib-rt/function_wrapper.c b/mypyc/lib-rt/function_wrapper.c index ccb1824d24b4a..348c3316cd258 100644 --- a/mypyc/lib-rt/function_wrapper.c +++ b/mypyc/lib-rt/function_wrapper.c @@ -1,6 +1,6 @@ #define PY_SSIZE_T_CLEAN -#include #include "CPy.h" +#include #define CPyFunction_weakreflist(f) (((PyCFunctionObject *)f)->m_weakreflist) #define CPyFunction_class(f) ((PyObject*) ((PyCMethodObject *) (f))->mm_class) diff --git a/mypyc/lib-rt/stringwriter_extra_ops.h b/mypyc/lib-rt/stringwriter_extra_ops.h index 0da9a4d9d7f71..bac6dd6b3e95c 100644 --- a/mypyc/lib-rt/stringwriter_extra_ops.h +++ b/mypyc/lib-rt/stringwriter_extra_ops.h @@ -1,9 +1,9 @@ #ifndef STRINGWRITER_EXTRA_OPS_H #define STRINGWRITER_EXTRA_OPS_H +#include #include #include -#include #include "mypyc_util.h" #include "strings/librt_strings_api.h" diff --git a/mypyc/test-data/run-misc.test b/mypyc/test-data/run-misc.test index eda44e16871f2..d48884ada853a 100644 --- a/mypyc/test-data/run-misc.test +++ b/mypyc/test-data/run-misc.test @@ -971,7 +971,10 @@ print(z) [case testCheckVersion] import sys -if sys.version_info[:2] == (3, 15): +if sys.version_info[:2] == (3, 16): + def version() -> int: + return 16 +elif sys.version_info[:2] == (3, 15): def version() -> int: return 15 elif sys.version_info[:2] == (3, 14): diff --git a/mypyc/test-data/run-python312.test b/mypyc/test-data/run-python312.test index 5ed6dca9ecb28..f3dc272fc9fa1 100644 --- a/mypyc/test-data/run-python312.test +++ b/mypyc/test-data/run-python312.test @@ -229,9 +229,10 @@ type C[*Ts] = tuple[*Ts] def test_type_var_tuple_type_alias() -> None: if sys.version_info >= (3, 15): # type: ignore[operator] assert str(C[int, str]) == "_frozen_importlib.C[int, str]" + assert str(getattr(C, "__value__")) == "tuple[typing.Unpack[~Ts]]" else: assert str(C[int, str]) == "C[int, str]" - assert str(getattr(C, "__value__")) == "tuple[typing.Unpack[Ts]]" + assert str(getattr(C, "__value__")) == "tuple[typing.Unpack[Ts]]" type D[**P] = Callable[P, int] diff --git a/pyproject.toml b/pyproject.toml index 9313335b0d969..050b7027719fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Software Development", "Typing :: Typed", ] diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index eb8f4931fa2fe..cfba7a81e9285 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -363,11 +363,11 @@ mypy: error: Mypy no longer supports checking Python 2 code. Consider pinning to python_version = 3.10 [out] -[case testPythonVersionAccepted314] +[case testPythonVersionAccepted315] # cmd: mypy -c pass [file mypy.ini] \[mypy] -python_version = 3.14 +python_version = 3.15 [out] [case testPythonVersionFallback] diff --git a/test-requirements.txt b/test-requirements.txt index 8ac31e0b34666..60e582bc12fe0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -46,7 +46,7 @@ pre-commit==4.3.0 # via -r test-requirements.in psutil==7.1.0 # via -r test-requirements.in -pygments==2.19.2 +pygments==2.20.0 # via pytest pytest==8.4.2 # via @@ -67,7 +67,7 @@ types-setuptools==80.9.0.20250822 # via -r build-requirements.txt typing-extensions==4.15.0 # via -r mypy-requirements.txt -virtualenv==20.34.0 +virtualenv==20.35.4 # via pre-commit # The following packages are considered to be unsafe in a requirements file: diff --git a/tox.ini b/tox.ini index 2126970afa991..ab81b00d121f5 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ envlist = py312, py313, py314, + py315, docs, lint, type, From b74aae46e8a2d2c552fb9b4b7901f7a3294def88 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 9 May 2026 16:24:34 +0100 Subject: [PATCH 005/127] Bump version to 2.2.0+dev (#21451) The release branch has been cut: https://github.com/python/mypy/tree/release-2.1 --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index 82a0d52db14f7..a33ca938708f4 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "2.1.0+dev" +__version__ = "2.2.0+dev" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From 971904db4737fdb2ce21e677c438d1702af5cc40 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 9 May 2026 20:59:01 +0200 Subject: [PATCH 006/127] Reorder librt Python.h includes (#21446) --- mypyc/lib-rt/CPy.h | 2 +- mypyc/lib-rt/pythonsupport.h | 2 +- mypyc/lib-rt/strings/librt_strings.h | 2 +- mypyc/lib-rt/strings/librt_strings_api.h | 2 +- mypyc/lib-rt/test_capi.cc | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 89ef4d0749a45..c22c4162669bd 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -3,8 +3,8 @@ #ifndef CPY_CPY_H #define CPY_CPY_H -#include #include +#include #include #include #include diff --git a/mypyc/lib-rt/pythonsupport.h b/mypyc/lib-rt/pythonsupport.h index 4c82ff6a3c037..1b0583543fe48 100644 --- a/mypyc/lib-rt/pythonsupport.h +++ b/mypyc/lib-rt/pythonsupport.h @@ -6,8 +6,8 @@ #ifndef CPY_PYTHONSUPPORT_H #define CPY_PYTHONSUPPORT_H -#include #include +#include #include "pythoncapi_compat.h" #include #include diff --git a/mypyc/lib-rt/strings/librt_strings.h b/mypyc/lib-rt/strings/librt_strings.h index 0bf1587e6f44f..e6236f7950929 100644 --- a/mypyc/lib-rt/strings/librt_strings.h +++ b/mypyc/lib-rt/strings/librt_strings.h @@ -1,8 +1,8 @@ #ifndef LIBRT_STRINGS_H #define LIBRT_STRINGS_H -#include #include +#include #include "librt_strings_common.h" // ABI version -- only an exact match is compatible. This will only be changed in diff --git a/mypyc/lib-rt/strings/librt_strings_api.h b/mypyc/lib-rt/strings/librt_strings_api.h index f0bb761bcaa2e..536b90ad7f21c 100644 --- a/mypyc/lib-rt/strings/librt_strings_api.h +++ b/mypyc/lib-rt/strings/librt_strings_api.h @@ -4,8 +4,8 @@ int import_librt_strings(void); -#include #include +#include #include "librt_strings.h" extern void *LibRTStrings_API[LIBRT_STRINGS_API_LEN]; diff --git a/mypyc/lib-rt/test_capi.cc b/mypyc/lib-rt/test_capi.cc index 4b183de5743e9..bf15a47e63dd1 100644 --- a/mypyc/lib-rt/test_capi.cc +++ b/mypyc/lib-rt/test_capi.cc @@ -1,8 +1,8 @@ // Test cases -#include #include #include "CPy.h" +#include static PyObject *moduleDict; From 2b5a84d55fee98e6b8736038d9491fa092b191ae Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 9 May 2026 21:00:38 +0200 Subject: [PATCH 007/127] Update test requirements (#21448) --- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 4 +-- test-requirements.txt | 51 +++++++++++++++++++++----------------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 66e7c997f4fad..b8303342a254c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -44,7 +44,7 @@ jobs: with: python-version: '3.12' - name: Install tox - run: pip install tox==4.26.0 + run: pip install tox==4.53.1 - name: Setup tox environment run: tox run -e ${{ env.TOXENV }} --notest - name: Test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27fa756b7f607..b06f181746c35 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -213,7 +213,7 @@ jobs: echo debug build; python -c 'import sysconfig; print(bool(sysconfig.get_config_var("Py_DEBUG")))' echo os.cpu_count; python -c 'import os; print(os.cpu_count())' echo os.sched_getaffinity; python -c 'import os; print(len(getattr(os, "sched_getaffinity", lambda *args: [])(0)))' - pip install tox==4.26.0 + pip install tox==4.53.1 - name: Compiled with mypyc if: ${{ matrix.test_mypyc }} @@ -278,7 +278,7 @@ jobs: default: 3.11.1 command: python -c "import platform; print(f'{platform.architecture()=} {platform.machine()=}');" - name: Install tox - run: pip install tox==4.26.0 + run: pip install tox==4.53.1 - name: Setup tox environment run: tox run -e py --notest - name: Test diff --git a/test-requirements.txt b/test-requirements.txt index 60e582bc12fe0..708f4514a492d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,71 +5,76 @@ # pip-compile --allow-unsafe --output-file=test-requirements.txt --strip-extras test-requirements.in # ast-serialize==0.3.0 + # via -r mypy-requirements.txt +attrs==26.1.0 # via -r test-requirements.in -attrs==25.4.0 - # via -r test-requirements.in -cfgv==3.4.0 +cfgv==3.5.0 # via pre-commit -coverage==7.10.7 +coverage==7.13.5 # via pytest-cov distlib==0.4.0 # via virtualenv -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.20.0 +filelock==3.29.0 # via # -r test-requirements.in + # python-discovery # virtualenv -identify==2.6.15 +identify==2.6.19 # via pre-commit -iniconfig==2.1.0 +iniconfig==2.3.0 # via pytest librt==0.10.0 ; platform_python_implementation != "PyPy" # via -r mypy-requirements.txt -lxml==6.0.2 ; python_version < "3.15" +lxml==6.1.0 ; python_version < "3.15" # via -r test-requirements.in mypy-extensions==1.1.0 # via -r mypy-requirements.txt -nodeenv==1.9.1 +nodeenv==1.10.0 # via pre-commit -packaging==25.0 +packaging==26.2 # via pytest -pathspec==1.0.0 +pathspec==1.1.1 # via -r mypy-requirements.txt -platformdirs==4.5.0 - # via virtualenv +platformdirs==4.9.6 + # via + # python-discovery + # virtualenv pluggy==1.6.0 # via # pytest # pytest-cov -pre-commit==4.3.0 +pre-commit==4.6.0 # via -r test-requirements.in -psutil==7.1.0 +psutil==7.2.2 # via -r test-requirements.in pygments==2.20.0 # via pytest -pytest==8.4.2 +pytest==9.0.3 # via # -r test-requirements.in # pytest-cov # pytest-xdist -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r test-requirements.in pytest-xdist==3.8.0 # via -r test-requirements.in +python-discovery==1.3.0 + # via virtualenv pyyaml==6.0.3 # via pre-commit -tomli==2.3.0 +tomli==2.4.1 # via -r test-requirements.in -types-psutil==7.0.0.20251001 +types-psutil==7.2.2.20260508 # via -r build-requirements.txt -types-setuptools==80.9.0.20250822 +types-setuptools==82.0.0.20260508 # via -r build-requirements.txt typing-extensions==4.15.0 # via -r mypy-requirements.txt -virtualenv==20.35.4 +virtualenv==21.3.1 # via pre-commit # The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 +setuptools==82.0.1 # via -r test-requirements.in From 38b1bdfd6cd0f9b89500eda40836b74826b7123e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 10 May 2026 09:50:01 +0300 Subject: [PATCH 008/127] Fix function call message change for small number of args (#21432) Refs https://github.com/python/mypy/issues/21427 I used `2` as a magic number, because in my opinion it is easier to tell the difference when all errors show up for the small number of args. --------- Co-authored-by: hauntsaninja --- mypy/checkexpr.py | 1 + test-data/unit/check-functions.test | 47 +++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 123c5f821ed29..71ffa7c4ff23b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1787,6 +1787,7 @@ def check_callable_call( might_have_shifted_args = ( not self.msg.prefer_simple_messages() + and len(args) >= 2 # see gh-21427 and all(k == ARG_POS for k in callee.arg_kinds) and all(k == ARG_POS for k in arg_kinds) and len(arg_kinds) == len(callee.arg_kinds) - 1 diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index bd2bf26613940..893eefb36f874 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3814,9 +3814,9 @@ f(1, b'x', 1) main:3: error: Missing positional argument "y" in call to "f" [case testMissingPositionalArgShiftDetectedFirst] -def f(x: int, y: str, z: bytes) -> None: ... +def f(x: int, y: str, z: bytes, last: float) -> None: ... -f("hello", b'x') +f("hello", b'x', 1.5) [builtins fixtures/primitives.pyi] [out] main:3: error: Missing positional argument "x" in call to "f" @@ -3891,3 +3891,46 @@ f("hello", b'x') main:3: error: Missing positional argument "z" in call to "f" main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str" + +[case testMissingPositionalArgNamesHigherN] +# See https://github.com/python/mypy/issues/21427 +def convert2(first: int, second: str) -> None: ... + +# Possibly omitted arg, but we still issue two errors because there is only one argument +convert2("hello") # E: Missing positional argument "second" in call to "convert2" \ + # E: Argument 1 to "convert2" has incompatible type "str"; expected "int" + +# Other cases +convert2() # E: Missing positional arguments "first", "second" in call to "convert2" + +convert2("hello", 1) # E: Argument 1 to "convert2" has incompatible type "str"; expected "int" \ + # E: Argument 2 to "convert2" has incompatible type "int"; expected "str" + +def convert3(first: int, second: str, third: float) -> None: ... + +# Possibly omitted arg, but we now only issue one error +convert3("hello", 3.15) # E: Missing positional argument "first" in call to "convert3" + +# Other cases +convert3("hello") # E: Missing positional arguments "second", "third" in call to "convert3" \ + # E: Argument 1 to "convert3" has incompatible type "str"; expected "int" + +convert3(3.15, "hello") # E: Missing positional argument "third" in call to "convert3" \ + # E: Argument 1 to "convert3" has incompatible type "float"; expected "int" + +def convert4(first: int, second: str, third: float, fourth: bytes) -> None: ... + +# Possibly omitted arg, but we now only issue one error +convert4("hello", 3.15, b'') # E: Missing positional argument "first" in call to "convert4" + +# Other cases +convert4("hello") # E: Missing positional arguments "second", "third", "fourth" in call to "convert4" \ + # E: Argument 1 to "convert4" has incompatible type "str"; expected "int" + +convert4("hello", 3.15) # E: Missing positional arguments "third", "fourth" in call to "convert4" \ + # E: Argument 1 to "convert4" has incompatible type "str"; expected "int" \ + # E: Argument 2 to "convert4" has incompatible type "float"; expected "str" + +convert4(b'', "hello", 3.15) # E: Missing positional argument "fourth" in call to "convert4" \ + # E: Argument 1 to "convert4" has incompatible type "bytes"; expected "int" +[builtins fixtures/primitives.pyi] From 010f0a2c4a5e4b7ac4e9fd1388ac072ffd2e8e5d Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 10 May 2026 00:08:18 -0700 Subject: [PATCH 009/127] Fix nondeterminism from nonassociativity of overload joins (#21455) Fixes #21445 See also my previous PRs #19147 and #19158 --- mypy/solve.py | 4 +++ test-data/unit/check-generics.test | 42 +++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/mypy/solve.py b/mypy/solve.py index e3709106996cd..4a5eec47ca60d 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -17,6 +17,7 @@ AnyType, Instance, NoneType, + Overloaded, ParamSpecType, ProperType, TupleType, @@ -253,6 +254,9 @@ def _join_sorted_key(t: Type) -> int: return -2 if isinstance(t, NoneType): return -1 + + if isinstance(t, Overloaded): + return 1 return 0 diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index a3a5b02d54f89..b6a97c70f4950 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -3542,7 +3542,7 @@ reveal_type(C.foo) # N: Revealed type is "def [T] (self: __main__.B[T`1]) -> T` reveal_type(C[int].foo) # N: Revealed type is "def (self: __main__.B[builtins.int]) -> builtins.int" reveal_type(D.foo) # N: Revealed type is "def (self: __main__.B[builtins.int]) -> builtins.int" -[case testDeterminismFromJoinOrderingInSolver] +[case testDeterminismFromJoinOrderingInSolver1] # Used to fail non-deterministically # https://github.com/python/mypy/issues/19121 from __future__ import annotations @@ -3595,6 +3595,46 @@ def draw_none( takes_int_str_none(c3) [builtins fixtures/tuple.pyi] +[case testDeterminismFromJoinOrderingInSolver2] +# Used to fail non-deterministically +# https://github.com/python/mypy/issues/21445 +from typing import Generic, Iterable, TypeVar, overload + +class A: ... + +@overload +def f0(a: A, b: object, /) -> object: ... +@overload +def f0(a: object, b: int, /) -> object: ... +def f0(a, b, /): ... + +@overload +def f1(a: int, b: object, /) -> object: ... +@overload +def f1(a: object, b: A, /) -> object: ... +def f1(a, b, /): ... + +def g(a, b, /): ... + +T = TypeVar("T") +K = TypeVar("K") +V = TypeVar("V") + +class ziplike(Generic[T]): + def __new__(cls, x: str, y: tuple[V, ...], /) -> ziplike[tuple[str, V]]: + raise + def __iter__(self) -> ziplike[T]: + return self + def __next__(self) -> T: + raise + +class dictlike(Generic[K, V]): + def __init__(self, arg: Iterable[tuple[K, V]]) -> None: pass + +x = dictlike(ziplike("012", (f0, f1, g))) +reveal_type(x) # N: Revealed type is "__main__.dictlike[builtins.str, Overload(def (Any, Any) -> Any, def (Any, Any) -> Any, def (Any, Any) -> Any, def (Any, Any) -> Any)]" +[builtins fixtures/dict.pyi] + [case testPropertyWithGenericSetter] from typing import TypeVar From 06e48a017efcb683b1deca3a40a9ccde78f5d4c4 Mon Sep 17 00:00:00 2001 From: Victor Letichevsky <101418385+victorletichevsky@users.noreply.github.com> Date: Sun, 10 May 2026 05:05:20 -0300 Subject: [PATCH 010/127] (minor) Small simplifications in combine_function_signatures (#19196) This pull request contributes to issue #5917, which encourages the decomposition and simplification of overly long methods to improve code clarity and maintainability. --- mypy/checkexpr.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 71ffa7c4ff23b..48ea7ab51f61b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3243,6 +3243,7 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call assert types, "Trying to merge no callables" if not all(isinstance(c, CallableType) for c in types): return AnyType(TypeOfAny.special_form) + callables = cast("list[CallableType]", types) if len(callables) == 1: return callables[0] @@ -3260,11 +3261,11 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call # confusing and ought to be re-written anyways.) callables, variables = merge_typevars_in_callables_by_name(callables) - new_args: list[list[Type]] = [[] for _ in range(len(callables[0].arg_types))] + new_args: list[list[Type]] = [[] for _ in callables[0].arg_types] new_kinds = list(callables[0].arg_kinds) new_returns: list[Type] = [] - too_complex = False + for target in callables: # We fall back to Callable[..., Union[]] if the functions do not have # the exact same signature. The only exception is if one arg is optional and @@ -3277,15 +3278,13 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call for i, (new_kind, target_kind) in enumerate(zip(new_kinds, target.arg_kinds)): if new_kind == target_kind: continue - elif new_kind.is_positional() and target_kind.is_positional(): + if new_kind.is_positional() and target_kind.is_positional(): new_kinds[i] = ARG_POS else: too_complex = True break - if too_complex: - break # outer loop - + break for i, arg in enumerate(target.arg_types): new_args[i].append(arg) new_returns.append(target.ret_type) @@ -3302,13 +3301,8 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call implicit=True, ) - final_args = [] - for args_list in new_args: - new_type = make_simplified_union(args_list) - final_args.append(new_type) - return callables[0].copy_modified( - arg_types=final_args, + arg_types=[make_simplified_union(args) for args in new_args], arg_kinds=new_kinds, ret_type=union_return, variables=variables, From b199f90057ed371b5aaea21d2158aa62272a2f6d Mon Sep 17 00:00:00 2001 From: Pranav Manglik Date: Sun, 10 May 2026 15:05:56 +0530 Subject: [PATCH 011/127] Analyse typeddict decorators (#21267) Previously, the semantic analyzer's special-case handling for TypedDict (analyze_typeddict_classdef) was returning early without visiting the decorators list in the ClassDef. This caused mypy to silently ignore invalid names, attributes, or subscripted expressions used as decorators on these classes. Fixes #21030 --------- Co-authored-by: hauntsaninja --- mypy/semanal.py | 25 +++++++++++++------------ test-data/unit/check-typeddict.test | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index a958043fa35c2..39230650e0544 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2110,18 +2110,20 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: and defn.info.typeddict_type and not has_placeholder(defn.info.typeddict_type) ): - # This is a valid TypedDict, and it is fully analyzed. - return True - is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn) + # Don't reprocess everything + is_typeddict = True + info = defn.info + else: + is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn) if is_typeddict: - for decorator in defn.decorators: - decorator.accept(self) - if info is not None: - self.analyze_class_decorator_common(defn, info, decorator) if info is None: self.mark_incomplete(defn.name, defn) else: self.prepare_class_def(defn, info, custom_names=True) + for decorator in defn.decorators: + decorator.accept(self) + if defn.info: + self.analyze_class_decorator_common(defn, decorator) return True return False @@ -2153,7 +2155,7 @@ def analyze_namedtuple_classdef( with self.scope.class_scope(defn.info): for deco in defn.decorators: deco.accept(self) - self.analyze_class_decorator_common(defn, defn.info, deco) + self.analyze_class_decorator_common(defn, deco) with self.named_tuple_analyzer.save_namedtuple_body(info): self.analyze_class_body_common(defn) return True @@ -2235,7 +2237,7 @@ def leave_class(self) -> None: def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None: decorator.accept(self) - self.analyze_class_decorator_common(defn, defn.info, decorator) + self.analyze_class_decorator_common(defn, decorator) if isinstance(decorator, RefExpr): if decorator.fullname in RUNTIME_PROTOCOL_DECOS: if defn.info.is_protocol: @@ -2247,13 +2249,12 @@ def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None ): defn.info.dataclass_transform_spec = self.parse_dataclass_transform_spec(decorator) - def analyze_class_decorator_common( - self, defn: ClassDef, info: TypeInfo, decorator: Expression - ) -> None: + def analyze_class_decorator_common(self, defn: ClassDef, decorator: Expression) -> None: """Common method for applying class decorators. Called on regular classes, typeddicts, and namedtuples. """ + info = defn.info if refers_to_fullname(decorator, FINAL_DECORATOR_NAMES): info.is_final = True elif refers_to_fullname(decorator, DISJOINT_BASE_DECORATOR_NAMES): diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 622004758364b..c74ae96d7763e 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -235,6 +235,20 @@ reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testTypedDictDecoratorUndefinedNames] +from typing import TypedDict + +@abc # E: Name "abc" is not defined +@efg.hij # E: Name "efg" is not defined +@klm[nop] # E: Name "klm" is not defined \ + # E: Name "nop" is not defined +@qrs.tuv[wxy] # E: Name "qrs" is not defined \ + # E: Name "wxy" is not defined +class A(TypedDict): + x: int +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testTypedDictWithClassmethodAlternativeConstructorDoesNotCrash] # https://github.com/python/mypy/issues/5653 from typing import TypedDict From 82fb613070f2b03e28df2191b5de88a58ad3aaaa Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 10 May 2026 13:30:29 +0300 Subject: [PATCH 012/127] Fix formatting in `error_code_list2.rst` (#21457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: Снимок экрана 2026-05-10 в 13 27 42 --- docs/source/error_code_list2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 36cf05d4f3f85..5dfe483d2cfe2 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -550,7 +550,7 @@ Example: Check that overrides of mutable attributes are safe [mutable-override] ---------------------------------------------------------------------- -`mutable-override` will enable the check for unsafe overrides of mutable attributes. +``mutable-override`` will enable the check for unsafe overrides of mutable attributes. For historical reasons, and because this is a relatively common pattern in Python, this check is not enabled by default. The example below is unsafe, and will be flagged when this error code is enabled: From 492c9aadb344c63afe33329a6cffbe2020c192a4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 May 2026 19:49:35 +0100 Subject: [PATCH 013/127] Bump librt version to 0.11.0 (#21458) --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index 27c76a0f3f6a8..0216f47852baa 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -5,5 +5,5 @@ typing_extensions>=4.14.0; python_version>='3.15' mypy_extensions>=1.0.0 pathspec>=1.0.0 tomli>=1.1.0; python_version<'3.11' -librt>=0.10.0; platform_python_implementation != 'PyPy' +librt>=0.11.0; platform_python_implementation != 'PyPy' ast-serialize>=0.3.0,<1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 050b7027719fc..e5dd37644e2d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires = [ "mypy_extensions>=1.0.0", "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.10.0; platform_python_implementation != 'PyPy'", + "librt>=0.11.0; platform_python_implementation != 'PyPy'", # the following is from build-requirements.txt "types-psutil", "types-setuptools", @@ -58,7 +58,7 @@ dependencies = [ "mypy_extensions>=1.0.0", "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.10.0; platform_python_implementation != 'PyPy'", + "librt>=0.11.0; platform_python_implementation != 'PyPy'", "ast-serialize>=0.3.0,<1.0.0", ] dynamic = ["version"] diff --git a/test-requirements.txt b/test-requirements.txt index 708f4514a492d..ec04e52e8a495 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -25,7 +25,7 @@ identify==2.6.19 # via pre-commit iniconfig==2.3.0 # via pytest -librt==0.10.0 ; platform_python_implementation != "PyPy" +librt==0.11.0 ; platform_python_implementation != "PyPy" # via -r mypy-requirements.txt lxml==6.1.0 ; python_version < "3.15" # via -r test-requirements.in From c0cced35c1451e04a3aa7d643605262987505863 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 May 2026 14:26:51 -0700 Subject: [PATCH 014/127] typeform: remove a regex call, rearrange checks (#21459) Related to https://github.com/python/mypy/pull/21262#issuecomment-4364872582 --- mypy/semanal.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 39230650e0544..fb6f299a37956 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -364,13 +364,6 @@ # string literal as a type expression. _MULTIPLE_WORDS_NONTYPE_RE = re.compile(r'\s*[^\s.\'"|\[]+\s+[^\s.\'"|\[]') -# Matches any valid Python identifier, including identifiers with Unicode characters. -# -# [^\d\W] = word character that is not a digit -# \w = word character -# \Z = match end of string; does not allow a trailing \n, unlike $ -_IDENTIFIER_RE = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) - class SemanticAnalyzer( NodeVisitor[None], SemanticAnalyzerInterface, SemanticAnalyzerPluginInterface, SplittingVisitor @@ -8043,16 +8036,9 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: return elif isinstance(maybe_type_expr, StrExpr): str_value = maybe_type_expr.value # cache - # Filter out string literals with common patterns that could not - # possibly be in a type expression - if _MULTIPLE_WORDS_NONTYPE_RE.match(str_value): - # A common pattern in string literals containing a sentence. - # But cannot be a type expression. - maybe_type_expr.as_type = None - return # Filter out string literals which look like an identifier but # cannot be a type expression, for a few common reasons - if _IDENTIFIER_RE.fullmatch(str_value): + if str_value.isidentifier(): sym = self.lookup(str_value, UnboundType(str_value), suppress_errors=True) if sym is None: # Does not refer to anything in the local symbol table @@ -8078,13 +8064,21 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None: return else: # does not look like an identifier if '"' in str_value or "'" in str_value: - # Only valid inside a Literal[...] type + # Only valid inside a Literal[...] or Annotated[..., ...] type if "[" not in str_value: - # Cannot be a Literal[...] type + # Cannot be a Literal[...] or Annotated[..., ...] type maybe_type_expr.as_type = None return - elif str_value == "": - # Empty string is not a valid type + elif len(str_value) < 2 or str_value.isspace(): + # Whitespace-only strings cannot be valid types. Very short strings can + # only be valid if they are identifiers, but we already checked for those. + maybe_type_expr.as_type = None + return + # Filter out string literals with common patterns that could not + # possibly be in a type expression + if _MULTIPLE_WORDS_NONTYPE_RE.match(str_value): + # A common pattern in string literals containing a sentence. + # But cannot be a type expression. maybe_type_expr.as_type = None return elif isinstance(maybe_type_expr, IndexExpr): From a9008315ef56aa3843b957d372989f455acdd7bb Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 10 May 2026 23:31:22 -0700 Subject: [PATCH 015/127] Improve negative narrowing for membership checks on tuples (#21456) Related to #21411 Fixes #16093 --- mypy/checker.py | 56 ++++++++++++++------- test-data/unit/check-isinstance.test | 12 ++--- test-data/unit/check-narrowing.test | 65 ++++++++++++++++++++++++- test-data/unit/check-typevar-tuple.test | 9 ++-- 4 files changed, 111 insertions(+), 31 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2d23f9c133164..e7c546628bc5e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6771,25 +6771,45 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa else_map = {} if left_index in narrowable_operand_index_to_hash: - collection_item_type = get_proper_type(builtin_item_type(iterable_type)) - if collection_item_type is not None: - if_map, else_map = self.narrow_type_by_identity_equality( - "==", - operands=[operands[left_index], operands[right_index]], - operand_types=[item_type, collection_item_type], - expr_indices=[0, 1], - narrowable_indices={0}, - ) - if else_map and not ( - isinstance(p_typ := get_proper_type(iterable_type), TupleType) - and all( - is_singleton_equality_type(get_proper_type(item)) - for item in p_typ.items + p_iterable_type = get_proper_type(iterable_type) + if ( + isinstance(p_iterable_type, TupleType) + and find_unpack_in_list(p_iterable_type.items) is None + ): + # For some tuples, we can do negative narrowing, e.g. `x not in (None,)` + all_if_maps = [] + all_else_maps = [] + for known_item in p_iterable_type.items: + # Match the should_coerce_literals logic from narrow_type_by_identity_equality + p_known_item = get_proper_type(known_item) + if is_literal_type_like(p_known_item) or ( + isinstance(p_known_item, Instance) and p_known_item.type.is_enum + ): + known_item = coerce_to_literal(known_item) + if_map, else_map = self.narrow_type_by_identity_equality( + "==", + operands=[operands[left_index], operands[right_index]], + operand_types=[item_type, known_item], + expr_indices=[0, 1], + narrowable_indices={0}, ) - ): - # In general, we can't do negative narrowing, since e.g. the container - # could just be empty. However, we can do negative narrowing for some - # tuples e.g. `x not in (None,)` + all_if_maps.append(if_map) + if is_singleton_equality_type(get_proper_type(known_item)): + all_else_maps.append(else_map) + if_map = reduce_or_conditional_type_maps(all_if_maps) + else_map = reduce_and_conditional_type_maps(all_else_maps, use_meet=True) + else: + collection_item_type = get_proper_type(builtin_item_type(iterable_type)) + if collection_item_type is not None: + if_map, else_map = self.narrow_type_by_identity_equality( + "==", + operands=[operands[left_index], operands[right_index]], + operand_types=[item_type, collection_item_type], + expr_indices=[0, 1], + narrowable_indices={0}, + ) + # We can't do negative narrowing, since e.g. the container could + # just be empty. else_map = {} if right_index in narrowable_operand_index_to_hash: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 029224e122168..fe093220aa9da 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2294,18 +2294,16 @@ def f(x: Optional[int], lst: Optional[List[int]], nested_any: List[List[Any]]) - [case testNarrowTypeAfterInTuple] # flags: --warn-unreachable -from typing import Optional class A: pass class B(A): pass class C(A): pass -y: Optional[B] -if y in (B(), C()): - reveal_type(y) # N: Revealed type is "__main__.B" -else: - reveal_type(y) # N: Revealed type is "__main__.B | None" +def f(y: B | None): + if y in (B(), C()): + reveal_type(y) # N: Revealed type is "__main__.B" + else: + reveal_type(y) # N: Revealed type is "__main__.B | None" [builtins fixtures/tuple.pyi] -[out] [case testNarrowTypeAfterInNamedTuple] # flags: --warn-unreachable diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 4f23d9147205e..944161810389b 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3201,7 +3201,7 @@ class X: [builtins fixtures/dict.pyi] -[case testTypeNarrowingStringInLiteralContainer] +[case testNarrowStringInLiteralContainer] # flags: --strict-equality --warn-unreachable from typing import Literal @@ -3235,6 +3235,69 @@ def narrow_set(x: str, t: set[Literal['a', 'b']]): reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/primitives.pyi] +[case testNarrowLiteralInLiteralContainer] +# flags: --strict-equality --warn-unreachable +from typing import Literal + +def narrow_tuple_exact(x: Literal['a', 'b', 'c'], t: tuple[Literal['a'], Literal['b']]): + if x in t: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['c']" + + if x not in t: + reveal_type(x) # N: Revealed type is "Literal['c']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + +def narrow_tuple_expression(x: Literal['a', 'b', 'c']): + # TODO: this should match narrow_tuple_exact + if x in ('a', 'b'): + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x not in ('a', 'b'): + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + +def narrow_tuple_union(x: Literal['a', 'b', 'c'], t: tuple[Literal['a', 'b']]): + if x in t: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x not in t: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + +def narrow_tuple_with_other_type(x: Literal['a', 'b', 'c'], t: tuple[Literal['a'], int]): + if x in t: + reveal_type(x) # N: Revealed type is "Literal['a']" + else: + reveal_type(x) # N: Revealed type is "Literal['b'] | Literal['c']" + +def narrow_homo_tuple(x: Literal['a', 'b', 'c'], t: tuple[Literal['a', 'b'], ...]): + if x in t: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + +def narrow_list(x: Literal['a', 'b', 'c'], t: list[Literal['a', 'b']]): + if x in t: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + +def narrow_set(x: Literal['a', 'b', 'c'], t: set[Literal['a', 'b']]): + if x in t: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" +[builtins fixtures/primitives.pyi] + [case testNarrowingLiteralInLiteralContainer] # flags: --strict-equality --warn-unreachable diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 703653227e200..7ca21b280aad0 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2145,14 +2145,13 @@ match(b) # E: Argument 1 to "match" has incompatible type "Bad"; expected "PC[U [builtins fixtures/tuple.pyi] [case testVariadicTupleCollectionCheck] -from typing import Tuple, Optional from typing_extensions import Unpack -allowed: Tuple[int, Unpack[Tuple[int, ...]]] +allowed: tuple[int, Unpack[tuple[int, ...]]] -x: Optional[int] -if x in allowed: - reveal_type(x) # N: Revealed type is "builtins.int" +def f(x: int | None): + if x in allowed: + reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] [case testJoinOfVariadicTupleCallablesNoCrash] From 67cdcc37cbb216b5c565f62c48fb6f3fc4a3a59a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 11 May 2026 14:18:59 +0100 Subject: [PATCH 016/127] [mypyc] Document librt.random (#21463) --- mypyc/doc/index.rst | 1 + mypyc/doc/librt.rst | 2 + mypyc/doc/librt_random.rst | 97 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 mypyc/doc/librt_random.rst diff --git a/mypyc/doc/index.rst b/mypyc/doc/index.rst index 8be1086bdd707..aacf275de9885 100644 --- a/mypyc/doc/index.rst +++ b/mypyc/doc/index.rst @@ -33,6 +33,7 @@ generate fast code. librt librt_base64 + librt_random librt_strings librt_time librt_vecs diff --git a/mypyc/doc/librt.rst b/mypyc/doc/librt.rst index a9492e1d61268..23206f8cfe806 100644 --- a/mypyc/doc/librt.rst +++ b/mypyc/doc/librt.rst @@ -26,6 +26,8 @@ Follow submodule links in the table to a detailed description of each submodule. - Description * - :doc:`librt.base64 ` - Fast Base64 encoding and decoding + * - :doc:`librt.random ` + - Pseudorandom number generation * - :doc:`librt.strings ` - String and bytes utilities * - :doc:`librt.time ` diff --git a/mypyc/doc/librt_random.rst b/mypyc/doc/librt_random.rst new file mode 100644 index 0000000000000..d5543661ce987 --- /dev/null +++ b/mypyc/doc/librt_random.rst @@ -0,0 +1,97 @@ +.. _librt-random: + +librt.random +============ + +The ``librt.random`` module is part of the ``librt`` package on PyPI, and it provides +pseudorandom number generation utilities. It can be used as a significantly faster +alternative to the stdlib :mod:`random` module in compiled code. It can also be faster +than stdlib ``random`` in interpreted code, depending on use case. + +The module uses the `ChaCha8 `__ algorithm with forward +secrecy. It is **not** suitable for cryptographic use, but it provides high-quality, +statistically uniform output. + +Functions +--------- + +The module provides module-level functions that use thread-local state, so they are +safe to call concurrently from multiple threads without external locking, and they +scale well even if used from multiple threads: + +.. function:: random() -> float + + Return a random floating-point number in the range [0.0, 1.0). + +.. function:: randint(a: i64, b: i64) -> i64 + + Return a random integer *n* such that *a* <= *n* <= *b*. + +.. function:: randrange(stop: i64, /) -> i64 + randrange(start: i64, stop: i64, /) -> i64 + + Return a random integer from the range. With one argument, the range is [0, *stop*). + With two arguments, the range is [*start*, *stop*). + +.. function:: seed(n: i64, /) -> None + + Seed the thread-local random number generator. This only affects module-level + functions called from the current thread. + +Random class +------------ + +.. class:: Random(seed: i64 | None = None) + + A pseudorandom number generator instance with its own independent state. Use this + when you need reproducible sequences or want to avoid interference with the + thread-local state used by the module-level functions. + + If *seed* is ``None``, the generator is seeded from OS entropy + (via :func:`os.urandom`). + + It's not safe to use the same ``Random`` instance concurrently from multiple + threads without synchronization on free-threaded Python builds. + + .. method:: random() -> float + + Return a random floating-point number in the range [0.0, 1.0). + + .. method:: randint(a: i64, b: i64) -> i64 + + Return a random integer *n* such that *a* <= *n* <= *b*. + + .. method:: randrange(stop: i64, /) -> i64 + randrange(start: i64, stop: i64, /) -> i64 + + Return a random integer from the range. With one argument, the range is [0, *stop*). + With two arguments, the range is [*start*, *stop*). + + .. method:: seed(n: i64, /) -> None + + Reseed the generator. + +Example +------- + +Using module-level functions:: + + from librt.random import randint, seed + + def roll_dice() -> i64: + return randint(1, 6) + +Using a ``Random`` instance for reproducible sequences:: + + from librt.random import Random + + def generate_data() -> list[i64]: + rng = Random(42) + return [rng.randint(0, 100) for _ in range(10)] + +Backward compatibility +---------------------- + +New versions of this module are not guaranteed to generate the same results when +using the same seed. A specific seed only produces predictable random numbers on a +specific version of ``librt``. In the future we might provide stronger guarantees. From e53693be05a200543a5b00edd2bb82bbf7c12329 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 11 May 2026 16:21:35 +0100 Subject: [PATCH 017/127] Add changelog for mypy 2.1 (#21464) The plan is to get this small feature release out today (Mon May 11). Related issue: #21450 --- CHANGELOG.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4f487a030a8..d01af76edf0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,87 @@ ## Next Release +## Mypy 2.1 + +We’ve just uploaded mypy 2.1.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). +Mypy is a static type checker for Python. This release includes new features, performance +improvements and bug fixes. You can install it as follows: + + python3 -m pip install -U mypy + +You can read the full documentation for this release on [Read the Docs](http://mypy.readthedocs.io). + +### librt.vecs: Fast Growable Array Type for Mypyc + +The new `librt.vecs` module provides an efficient growable array type `vec` that is +optimized for mypyc use. It provides fast, packed arrays with integer and floating point +value types, which can be **several times faster** than `list`, and tens of times faster +than `array.array` in code compiled using mypyc. It also supports nested `vec` objects and +non-value-type items, such as ``vec[vec[str]]``. + +Refer to the [documentation](https://mypyc.readthedocs.io/en/latest/librt_vecs.html) for +the details. + +Contributed by Jukka Lehtosalo. + +### librt.random: Fast Pseudo-Random Number Generation + +The new `librt.random` module provides fast pseudo-random number generation that is +optimized for code compiled using mypyc. It can be 3x to 10x faster than the stdlib +`random` module in compiled code. + +Refer to the [documentation](https://mypyc.readthedocs.io/en/latest/librt_random.html) for +the details. + +Contributed by Jukka Lehtosalo (PR [21433](https://github.com/python/mypy/pull/21433)). + +### Mypyc Improvements + +- Make compilation order with multiple files consistent (Piotr Sawicki, PR [21419](https://github.com/python/mypy/pull/21419)) +- Fix crash on accessing `StopAsyncIteration` (Piotr Sawicki, PR [21406](https://github.com/python/mypy/pull/21406)) +- Fix incremental compilation with `separate` flag (Vaggelis Danias, PR [21299](https://github.com/python/mypy/pull/21299)) + +### Fixes to Crashes + +- Fix crash on partial type with `--allow-redefinition` and `global` declaration (Jukka Lehtosalo, PR [21428](https://github.com/python/mypy/pull/21428)) +- Fix broken awaitable generator patching (Ivan Levkivskyi, PR [21435](https://github.com/python/mypy/pull/21435)) + +### Changes to Messages + +- Fix function call error message for small number of arguments (sobolevn, PR [21432](https://github.com/python/mypy/pull/21432)) + +### Other Notable Fixes and Improvements + +- Rely on typeshed stubs for `slice` typing (Ivan Levkivskyi, PR [21401](https://github.com/python/mypy/pull/21401)) +- Improve negative narrowing for membership checks on tuples (Shantanu, PR [21456](https://github.com/python/mypy/pull/21456)) +- Narrow match captures based on previous cases (Shantanu, PR [21405](https://github.com/python/mypy/pull/21405)) +- Fix nondeterminism in overload resolution (Shantanu, PR [21455](https://github.com/python/mypy/pull/21455)) +- Respect file config comments for stale modules (Adam Turner, PR [21444](https://github.com/python/mypy/pull/21444)) +- Fix JSON output mode for syntax errors in parallel mode (Adam Turner, PR [21434](https://github.com/python/mypy/pull/21434)) +- Fix type variable with values as a supertype (Ivan Levkivskyi, PR [21431](https://github.com/python/mypy/pull/21431)) +- Add support for configuring `--num-workers` with an environment variable (Kevin Kannammalil, PR [21407](https://github.com/python/mypy/pull/21407)) +- Respect JSON output mode for syntax errors (Adam Turner, PR [21386](https://github.com/python/mypy/pull/21386)) +- Analyze `TypedDict` decorators (Pranav Manglik, PR [21267](https://github.com/python/mypy/pull/21267)) + +### Typeshed Updates + +Please see [git log](https://github.com/python/typeshed/commits/main?after=e4d32e01bee44241a5e7c33298c261175b9f1bdb+0&branch=main&path=stdlib) for full list of standard library typeshed stub changes. + +### Acknowledgements + +Thanks to all mypy contributors who contributed to this release: + +- Adam Turner +- Ivan Levkivskyi +- Jukka Lehtosalo +- Kevin Kannammalil +- Piotr Sawicki +- Shantanu +- sobolevn +- Vaggelis Danias + +I’d also like to thank my employer, Dropbox, for supporting mypy development. + ## Mypy 2.0 We’ve just uploaded mypy 2.0.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). From d62a508bd9730ac12f6a2e4c2e2c6eb701eed22f Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 12 May 2026 14:19:24 -0700 Subject: [PATCH 018/127] Fix crash on Unpack used without arguments in class bases (#21470) Fixes #21467. `analyze_unbound_tvar` accessed `t.args[0]` unconditionally when a class base was `Unpack` with no arguments (e.g. `class C(Protocol[Unpack]): ...`), raising `IndexError: tuple index out of range` during semantic analysis. This adds a guard returning `None` in that case, so the existing "Free type variable expected in Protocol[...]" error is reported instead of crashing. Added a regression test in `check-typevar-tuple.test`. --- mypy/semanal.py | 3 +++ test-data/unit/check-typevar-tuple.test | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index fb6f299a37956..da58c95869667 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2403,6 +2403,9 @@ def analyze_unbound_tvar(self, t: Type) -> tuple[str, TypeVarLikeExpr] | None: if isinstance(t, UnboundType): sym = self.lookup_qualified(t.name, t) if sym and sym.fullname in UNPACK_TYPE_NAMES: + if not t.args: + # Unpack used without arguments, e.g. `Protocol[Unpack]` + return None inner_t = t.args[0] if isinstance(inner_t, UnboundType): return self.analyze_unbound_tvar_impl(inner_t, is_unpacked=True) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 7ca21b280aad0..ff5cdc8719bc6 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2765,3 +2765,11 @@ def func(d: Callable[[Unpack[Ts]], T]) -> T: ... y = func[1, int] # E: Type application is only supported for generic classes \ # E: Invalid type: try using Literal[1] instead? [builtins fixtures/tuple.pyi] + +[case testTypeVarTupleUnpackWithoutArgsInProtocol] +# https://github.com/python/mypy/issues/21467 +from typing import Protocol, Unpack + +class C(Protocol[Unpack]): # E: Free type variable expected in Protocol[...] + pass +[builtins fixtures/tuple.pyi] From db331b44ac2b4ef35b138cd5ecba7731c656ca4c Mon Sep 17 00:00:00 2001 From: Colinxu2020 <63941938+colinxu2020@users.noreply.github.com> Date: Wed, 13 May 2026 14:40:36 +0800 Subject: [PATCH 019/127] [mypyc] Fix reference leak in mypyc bytes concatenation (#21469) Fixes [Mypyc #1192](https://github.com/mypyc/mypyc/issues/1192) Fix a reference leak in mypyc-generated code for `bytes + bytes`. The `bytes + bytes` primitive for `CPyBytes_Concat` was marked as stealing the left operand with `steals=[True, False]`. However, `CPyBytes_Concat` does not steal or decref either argument; it allocates and returns a new `bytes` object. Because of this mismatch, the refcount pass skipped emitting a `dec_ref` for owned left operands. For code such as: ```python return (1).to_bytes(4, "big") + (2).to_bytes(4, "big") ```` the generated IR decrefed only the right operand after `CPyBytes_Concat`, leaving the owned left operand alive. Chained concatenations amplified the leak because intermediate concat results could also become leaked left operands. This PR removes the incorrect `steals=[True, False]` annotation from the `bytes + bytes` primitive, so both operands are treated as non-stolen and owned operands are decrefed normally after the call. The tests verify that the generated IR decrefs both owned operands after `CPyBytes_Concat`. --- mypyc/primitives/bytes_ops.py | 1 - mypyc/test-data/refcount.test | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/mypyc/primitives/bytes_ops.py b/mypyc/primitives/bytes_ops.py index 2db4879441025..96e8069f317ed 100644 --- a/mypyc/primitives/bytes_ops.py +++ b/mypyc/primitives/bytes_ops.py @@ -64,7 +64,6 @@ return_type=bytes_rprimitive, c_function_name="CPyBytes_Concat", error_kind=ERR_MAGIC, - steals=[True, False], ) # bytes * int diff --git a/mypyc/test-data/refcount.test b/mypyc/test-data/refcount.test index 2f03a159dfacb..918c84ee3b0ad 100644 --- a/mypyc/test-data/refcount.test +++ b/mypyc/test-data/refcount.test @@ -2125,3 +2125,31 @@ L3: r8 = box(None, 1) inc_ref r8 return r8 + +[case testBytesConcatRefcount] +def f(a: bytes, b: bytes) -> bytes: + return b"1" + a + b +[out] +def f(a, b): + a, b, r0, r1, r2 :: bytes +L0: + r0 = b'1' + r1 = CPyBytes_Concat(r0, a) + r2 = CPyBytes_Concat(r1, b) + dec_ref r1 + return r2 + +[case testChainedBytesConcatRefcount] +def f(a: bytes, b: bytes, c: bytes) -> bytes: + return b"1" + a + b + c +[out] +def f(a, b, c): + a, b, c, r0, r1, r2, r3 :: bytes +L0: + r0 = b'1' + r1 = CPyBytes_Concat(r0, a) + r2 = CPyBytes_Concat(r1, b) + dec_ref r1 + r3 = CPyBytes_Concat(r2, c) + dec_ref r2 + return r3 From d2b023417be30efb44c6cbe60530c956069ff70d Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 13 May 2026 01:10:29 -0700 Subject: [PATCH 020/127] Narrow membership in statically known containers (#21461) The negative narrowing here could be more aggressive, but we will need better literal handling to unblock it Builds on #21456 Fixes #13684 in combination with previous PRs. There is one remaining diagnostic, but that one is desirable --- mypy/checker.py | 27 ++++++++++++++++++--- test-data/unit/check-isinstance.test | 8 ------- test-data/unit/check-narrowing.test | 36 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e7c546628bc5e..80402e71dce69 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6772,14 +6772,34 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa if left_index in narrowable_operand_index_to_hash: p_iterable_type = get_proper_type(iterable_type) + container_item_types = None + + # Can we statically determine container contents? if ( isinstance(p_iterable_type, TupleType) and find_unpack_in_list(p_iterable_type.items) is None ): - # For some tuples, we can do negative narrowing, e.g. `x not in (None,)` + container_item_types = p_iterable_type.items + else: + container_expr = collapse_walrus(operands[right_index]) + if isinstance(container_expr, (ListExpr, SetExpr)): + if all(not isinstance(i, StarExpr) for i in container_expr.items): + container_item_types = [ + self.lookup_type(e) for e in container_expr.items + ] + elif isinstance(container_expr, DictExpr): + if all(k is not None for k, v in container_expr.items): + container_item_types = [ + self.lookup_type(cast(Expression, k)) + for k, v in container_expr.items + ] + + if container_item_types is not None: + # If we know the exact contents, we can potentially do negative narrowing, + # e.g. `x not in (None,)` all_if_maps = [] all_else_maps = [] - for known_item in p_iterable_type.items: + for known_item in container_item_types: # Match the should_coerce_literals logic from narrow_type_by_identity_equality p_known_item = get_proper_type(known_item) if is_literal_type_like(p_known_item) or ( @@ -6799,7 +6819,7 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa if_map = reduce_or_conditional_type_maps(all_if_maps) else_map = reduce_and_conditional_type_maps(all_else_maps, use_meet=True) else: - collection_item_type = get_proper_type(builtin_item_type(iterable_type)) + collection_item_type = get_proper_type(builtin_item_type(p_iterable_type)) if collection_item_type is not None: if_map, else_map = self.narrow_type_by_identity_equality( "==", @@ -6813,6 +6833,7 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa else_map = {} if right_index in narrowable_operand_index_to_hash: + # E.g. narrows the right operand in `if "key" in typed_dict` if_type, else_type = self.conditional_types_for_iterable( item_type, iterable_type ) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index fe093220aa9da..acd81839fcdc7 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2254,7 +2254,6 @@ if y not in x: else: reveal_type(y) # N: Revealed type is "builtins.int" [builtins fixtures/list.pyi] -[out] [case testNarrowTypeAfterInListOfOptional] # flags: --warn-unreachable @@ -2268,7 +2267,6 @@ if y not in x: else: reveal_type(y) # N: Revealed type is "builtins.int | None" [builtins fixtures/list.pyi] -[out] [case testNarrowTypeAfterInListNonOverlapping] # flags: --warn-unreachable @@ -2319,7 +2317,6 @@ if y not in nt: else: reveal_type(y) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[out] [case testNarrowTypeAfterInDict] # flags: --warn-unreachable @@ -2336,7 +2333,6 @@ if y not in x: else: reveal_type(y) # N: Revealed type is "builtins.str" [builtins fixtures/dict.pyi] -[out] [case testNarrowTypeAfterInNoAnyOrObject] # flags: --warn-unreachable @@ -2356,7 +2352,6 @@ else: reveal_type(y) # N: Revealed type is "builtins.int | None" [typing fixtures/typing-medium.pyi] [builtins fixtures/list.pyi] -[out] [case testNarrowTypeAfterInUserDefined] # flags: --warn-unreachable @@ -2378,7 +2373,6 @@ else: reveal_type(y) # N: Revealed type is "builtins.int | None" [typing fixtures/typing-full.pyi] [builtins fixtures/list.pyi] -[out] [case testNarrowTypeAfterInSet] # flags: --warn-unreachable @@ -2395,7 +2389,6 @@ if y not in s: else: reveal_type(y) # N: Revealed type is "builtins.str" [builtins fixtures/set.pyi] -[out] [case testNarrowTypeAfterInTypedDict] # flags: --warn-unreachable @@ -2412,7 +2405,6 @@ def f() -> None: reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[out] [case testIsinstanceWidensWithAnyArg] # flags: --warn-unreachable diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 944161810389b..7bab7baa6cebd 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3291,11 +3291,47 @@ def narrow_list(x: Literal['a', 'b', 'c'], t: list[Literal['a', 'b']]): else: reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + if x in ['a', 'b']: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x in ['a', 'b', *[]]: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + def narrow_set(x: Literal['a', 'b', 'c'], t: set[Literal['a', 'b']]): if x in t: reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" else: reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x in {'a', 'b'}: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x in {'a', 'b', *[]}: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + +def narrow_dict(x: Literal['a', 'b', 'c'], t: dict[Literal['a', 'b'], int]): + if x in t: # E: Unsupported operand types for in ("Literal['a', 'b', 'c']" and "dict[Literal['a', 'b'], int]") + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x in {'a': 0, 'b': 1}: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + + if x in {'a': 0, 'b': 1, **{}}: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" + else: + reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']" [builtins fixtures/primitives.pyi] From 9b4e31c2738d1dd1dcc84e9d6e30b48e99299793 Mon Sep 17 00:00:00 2001 From: lphuc2250gma Date: Wed, 13 May 2026 11:43:41 -0500 Subject: [PATCH 021/127] docs: fix duplicated word in overload docs (#21482) Fixes a duplicated word in the overload implementation compatibility docs. No code changes. Co-authored-by: Noa Levi <275430404+lphuc2250gma@users.noreply.github.com> --- docs/source/more_types.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 1febdcac9920a..444753757aad9 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -573,7 +573,7 @@ implementation, then the body is not type checked. If you want to force mypy to check the body anyways, use the :option:`--check-untyped-defs ` flag (:ref:`more details here `). -The variants must also also be compatible with the implementation +The variants must also be compatible with the implementation type hints. In the ``MyList`` example, mypy will check that the parameter type ``int`` and the return type ``T`` are compatible with ``int | slice`` and ``T | Sequence`` for the From d7f703625bcde2b26a3b16d1fe2919b093933e57 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 14 May 2026 15:10:31 +0300 Subject: [PATCH 022/127] [mypyc] Add `librt.strings.isspace` char primitive (#21462) This PR serves as the foundation for the smaller alternative of the [`char` proposal request](https://github.com/python/mypy/issues/21418). It builds on top of the existing `librt.strings` and `ord(char)` specialization, so code like the following can lower to a direct codepoint check i.e without materializing a 1-character str: ```py from librt.strings import isspace from mypy_extensions import i32 c: i32 = ... if (isspace(c)): ... ```
Semantics: - Input type is `i32` - Negative inputs return `False` - For all valid Unicode codepoints, behavior matches `str.isspace()` on the corresponding 1-character string (ensured by exhaustive test too) This PR adds only `isspace`; It does not add any new type-system surface, and it does not introduce the rest of the codepoint helperfamily. I'll be contributing these next if this direction looks good. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypy/typeshed/stubs/librt/librt/strings.pyi | 4 ++ mypyc/ir/deps.py | 1 + mypyc/lib-rt/codepoint_extra_ops.c | 8 +++ mypyc/lib-rt/codepoint_extra_ops.h | 16 +++++ mypyc/lib-rt/strings/librt_strings.c | 42 ++++++++++++ mypyc/primitives/librt_strings_ops.py | 19 +++++- mypyc/test-data/irbuild-librt-strings.test | 75 +++++++++++++++++++++ mypyc/test-data/run-librt-strings.test | 17 +++++ 8 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 mypyc/lib-rt/codepoint_extra_ops.c create mode 100644 mypyc/lib-rt/codepoint_extra_ops.h diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 711e52c2e3700..46e9eac68b24f 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -40,3 +40,7 @@ def write_f64_le(b: BytesWriter, n: float, /) -> None: ... def write_f64_be(b: BytesWriter, n: float, /) -> None: ... def read_f64_le(b: bytes, index: i64, /) -> float: ... def read_f64_be(b: bytes, index: i64, /) -> float: ... + +# Codepoint classification helpers operating on i32 codepoints (typically +# obtained via ord(s[i])). Negative inputs return False. +def isspace(c: i32, /) -> bool: ... diff --git a/mypyc/ir/deps.py b/mypyc/ir/deps.py index 751845d3a324c..0cf58c83c27bf 100644 --- a/mypyc/ir/deps.py +++ b/mypyc/ir/deps.py @@ -116,4 +116,5 @@ def get_header(self) -> str: STRING_WRITER_EXTRA_OPS: Final = SourceDep("stringwriter_extra_ops.c") BYTEARRAY_EXTRA_OPS: Final = SourceDep("bytearray_extra_ops.c") STR_EXTRA_OPS: Final = SourceDep("str_extra_ops.c") +CODEPOINT_EXTRA_OPS: Final = SourceDep("codepoint_extra_ops.c") VECS_EXTRA_OPS: Final = SourceDep("vecs_extra_ops.c") diff --git a/mypyc/lib-rt/codepoint_extra_ops.c b/mypyc/lib-rt/codepoint_extra_ops.c new file mode 100644 index 0000000000000..ca03eba4e6f51 --- /dev/null +++ b/mypyc/lib-rt/codepoint_extra_ops.c @@ -0,0 +1,8 @@ +#include "codepoint_extra_ops.h" + +// Out-of-line bodies for codepoint helpers that are too large to inline. +// The classification helpers and the ASCII fast paths for case conversion +// stay inline in codepoint_extra_ops.h; this file holds the slow paths +// that round-trip through PyUnicode_FromOrdinal and CPython's Unicode +// machinery. Currently empty; populated as later commits add +// isidentifier, toupper, and tolower. diff --git a/mypyc/lib-rt/codepoint_extra_ops.h b/mypyc/lib-rt/codepoint_extra_ops.h new file mode 100644 index 0000000000000..5633efb0987ee --- /dev/null +++ b/mypyc/lib-rt/codepoint_extra_ops.h @@ -0,0 +1,16 @@ +#ifndef MYPYC_CODEPOINT_EXTRA_OPS_H +#define MYPYC_CODEPOINT_EXTRA_OPS_H + +#include +#include +#include + +// Codepoint helpers for librt.strings. +// Inputs are signed int32_t for compatibility with mypyc's i32 type. +// Negative values are treated as non-codepoints and return false. + +static inline bool LibRTStrings_IsSpace(int32_t c) { + return c >= 0 && Py_UNICODE_ISSPACE((Py_UCS4)c); +} + +#endif // MYPYC_CODEPOINT_EXTRA_OPS_H diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index 3f08b5ef43766..ecde8c527f9d4 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -4,6 +4,7 @@ #include #include #include "CPy.h" +#include "codepoint_extra_ops.h" #include "librt_strings.h" #define CPY_BOOL_ERROR 2 @@ -1153,6 +1154,44 @@ read_f64_be(PyObject *module, PyObject *const *args, size_t nargs) { return PyFloat_FromDouble(CPyBytes_ReadF64BEUnsafe(data + index)); } +// Codepoint classification helpers exposed to interpreted callers. +// The C-side names are prefixed `cp_` to avoid colliding with libc's +// isspace / isdigit / etc. Compiled callers go through the +// LibRTStrings_* static inlines in codepoint_extra_ops.h instead. +// +// All wrappers parse a single int argument as i32 (codepoint) and +// dispatch to the corresponding LibRTStrings_* function. The parse +// step accepts any int but rejects values outside the i32 range with +// OverflowError, matching the input domain of the compiled fast path. + +// Parse a Python int as i32 codepoint. Returns 0 on success and writes +// the value to *out; returns -1 on error with a Python exception set. +static int +cp_parse_i32(PyObject *arg, int32_t *out) { + int overflow; + long c = PyLong_AsLongAndOverflow(arg, &overflow); + if (c == -1 && PyErr_Occurred()) + return -1; + if (overflow != 0 || c < INT32_MIN || c > INT32_MAX) { + PyErr_SetString(PyExc_OverflowError, + "codepoint out of i32 range"); + return -1; + } + *out = (int32_t)c; + return 0; +} + +#define DEFINE_CP_BOOL_WRAPPER(name, fn) \ + static PyObject* \ + cp_##name(PyObject *module, PyObject *arg) { \ + int32_t c; \ + if (cp_parse_i32(arg, &c) < 0) \ + return NULL; \ + return PyBool_FromLong(fn(c)); \ + } + +DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace) + static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, PyDoc_STR("Write a 16-bit signed integer to BytesWriter in little-endian format") @@ -1214,6 +1253,9 @@ static PyMethodDef librt_strings_module_methods[] = { {"read_f64_be", (PyCFunction) read_f64_be, METH_FASTCALL, PyDoc_STR("Read a 64-bit float from bytes in big-endian format") }, + {"isspace", cp_isspace, METH_O, + PyDoc_STR("Test whether a codepoint (i32) is Unicode whitespace.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index 502ab8269e8c4..0a4b2515c1ea8 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -1,4 +1,9 @@ -from mypyc.ir.deps import BYTES_WRITER_EXTRA_OPS, LIBRT_STRINGS, STRING_WRITER_EXTRA_OPS +from mypyc.ir.deps import ( + BYTES_WRITER_EXTRA_OPS, + CODEPOINT_EXTRA_OPS, + LIBRT_STRINGS, + STRING_WRITER_EXTRA_OPS, +) from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER from mypyc.ir.rtypes import ( bool_rprimitive, @@ -387,3 +392,15 @@ error_kind=ERR_NEVER, dependencies=[LIBRT_STRINGS, STRING_WRITER_EXTRA_OPS], ) + + +# Codepoint classification helpers operating on i32 codepoints +# (typically obtained via ord(s[i])). Negative inputs return False. +function_op( + name="librt.strings.isspace", + arg_types=[int32_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTStrings_IsSpace", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], +) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index 460a109d1d5ac..9bb2312b0d88a 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -270,3 +270,78 @@ L1: L2: r3 = CPyStringWriter_GetItem(s, r0) return r3 + +[case testLibrtStringsIsSpaceIR] +from librt.strings import isspace +from mypy_extensions import i32 + +def is_ws(c: i32) -> bool: + return isspace(c) +[out] +def is_ws(c): + c :: i32 + r0 :: bool +L0: + r0 = LibRTStrings_IsSpace(c) + return r0 + +[case testLibrtStringsIsSpaceFromStrIndexIR_64bit] +from librt.strings import isspace + +def is_ws_at(s: str, i: int) -> bool: + return isspace(ord(s[i])) +[out] +def is_ws_at(s, i): + s :: str + i :: int + r0 :: native_int + r1 :: bit + r2, r3 :: i64 + r4 :: ptr + r5 :: c_ptr + r6, r7 :: i64 + r8, r9 :: bool + r10 :: short_int + r11, r12 :: bit + r13 :: native_int + r14, r15 :: i32 + r16 :: bool +L0: + r0 = i & 1 + r1 = r0 == 0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = i >> 1 + r3 = r2 + goto L3 +L2: + r4 = i ^ 1 + r5 = r4 + r6 = CPyLong_AsInt64(r5) + r3 = r6 + keep_alive i +L3: + r7 = CPyStr_AdjustIndex(s, r3) + r8 = CPyStr_RangeCheck(s, r7) + if r8 goto L5 else goto L4 :: bool +L4: + r9 = raise IndexError('index out of range') + unreachable +L5: + r10 = CPyStr_GetItemUnsafeAsInt(s, r7) + r11 = r10 < 4294967296 :: signed + if r11 goto L6 else goto L8 :: bool +L6: + r12 = r10 >= -4294967296 :: signed + if r12 goto L7 else goto L8 :: bool +L7: + r13 = r10 >> 1 + r14 = truncate r13: native_int to i32 + r15 = r14 + goto L9 +L8: + CPyInt32_Overflow() + unreachable +L9: + r16 = LibRTStrings_IsSpace(r15) + return r16 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 909766d5c8e74..3c1f686867fbd 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1439,3 +1439,20 @@ def test_new_without_init_is_usable() -> None: assert sw.getvalue() == "" sw.write("hello") assert sw.getvalue() == "hello" + +[case testLibrtStringsIsSpace_librt] +from typing import Any +from mypy_extensions import i32 +from librt.strings import isspace + + +def test_isspace() -> None: + assert not isspace(i32(-1)) + assert not isspace(i32(-113)) + # Verify our codepoint primitive agrees with str.isspace() across all + # Unicode codepoints, including the ord(chr(i)) round-trip. Any + # forces generic dispatch on the str side. + for i in range(0x110000): + c = chr(i) + a: Any = c + assert isspace(ord(c)) == isspace(i) == a.isspace() From e159ee953764872286bfa9cc31b008929921d7cb Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 May 2026 23:32:59 +0100 Subject: [PATCH 023/127] Ignore num_workers in the daemon while it is not supported (#21483) Fixes https://github.com/python/mypy/issues/21475 Avoid the crash while we don't support this. The actual feature is tracked in https://github.com/python/mypy/issues/21346 --- mypy/dmypy_server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 614b757c774d2..066fbf9bed2d6 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -192,13 +192,17 @@ def __init__(self, options: Options, status_file: str, timeout: int | None = Non options.show_traceback = True if options.use_fine_grained_cache: # Using fine_grained_cache implies generating and caring - # about the fine grained cache + # about the fine-grained cache. options.cache_fine_grained = True else: options.cache_dir = os.devnull # Fine-grained incremental doesn't support general partial types # (details in https://github.com/python/mypy/issues/4492) options.local_partial_types = True + # We don't support parallel checking in fine-grained mode yet. + # Ignore it for now, so that same config file can be used with + # mypy and with the daemon. + options.num_workers = 0 self.status_file = status_file # Since the object is created in the parent process we can check From e27179372e28cfade372de712bb34b796d2c9514 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 00:21:23 +0100 Subject: [PATCH 024/127] Bump ast-serialize to 0.4.0 (#21487) Ref https://github.com/python/mypy/issues/20978 --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index 0216f47852baa..7fe111ac7da59 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -6,4 +6,4 @@ mypy_extensions>=1.0.0 pathspec>=1.0.0 tomli>=1.1.0; python_version<'3.11' librt>=0.11.0; platform_python_implementation != 'PyPy' -ast-serialize>=0.3.0,<1.0.0 +ast-serialize>=0.4.0,<1.0.0 diff --git a/pyproject.toml b/pyproject.toml index e5dd37644e2d6..123b7a8f4880e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ requires = [ "types-psutil", "types-setuptools", # required to work around a mypyc import bug - "ast-serialize>=0.3.0,<1.0.0", + "ast-serialize>=0.4.0,<1.0.0", ] build-backend = "setuptools.build_meta" @@ -59,7 +59,7 @@ dependencies = [ "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", "librt>=0.11.0; platform_python_implementation != 'PyPy'", - "ast-serialize>=0.3.0,<1.0.0", + "ast-serialize>=0.4.0,<1.0.0", ] dynamic = ["version"] diff --git a/test-requirements.txt b/test-requirements.txt index ec04e52e8a495..55e4b109346c9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=test-requirements.txt --strip-extras test-requirements.in # -ast-serialize==0.3.0 +ast-serialize==0.4.0 # via -r mypy-requirements.txt attrs==26.1.0 # via -r test-requirements.in From 1bbd6da6ab8f1b36d5cb9ab3358da0cfa9714f6e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 16 May 2026 01:41:39 +0100 Subject: [PATCH 025/127] Describe our LLM stance in CONTRIBUTING.md (#21488) --- CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28828482706da..f4c1dcc89c16f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,6 +123,18 @@ tox -e dev --override testenv:dev.allowlist_externals+=env -- env # inspect the If you don't already have `tox` installed, you can use a virtual environment as described above to install `tox` via `pip` (e.g., ``python -m pip install tox``). +## LLM-assisted contributions + +In general, mypy takes a neutral stance on using various LLM code assistants to +make contributions. LLMs are just another tool, and contributors bear full +responsibility for the code they are submitting. Disclosing the use of LLMs in +pull request description is recommended, but not required. + +However, we discourage use of LLMs by *new* contributors. We are interested in +growing long-term contributors who have good understanding of mypy code. +Pull requests from new contributors that are mostly generated by LLMs +with little human input will be closed. + ## First time contributors If you're looking for things to help with, browse our [issue tracker](https://github.com/python/mypy/issues)! From 11008008752aa10000a6230322fc4ebe3eb2b456 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 16 May 2026 22:37:54 +0100 Subject: [PATCH 026/127] [mypyc] Document free threading and other doc updates (#21494) Free threading support and limitations weren't documented at all. Some features that we currently support were still documented as unsupported. This includes some dunders and numeric use cases. We now support simple numeric use cases via `vec[float]`. --- mypyc/doc/differences_from_python.rst | 44 ++++++++++++++++++++++++--- mypyc/doc/introduction.rst | 16 +++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/mypyc/doc/differences_from_python.rst b/mypyc/doc/differences_from_python.rst index b910e3b3c9290..414476723125b 100644 --- a/mypyc/doc/differences_from_python.rst +++ b/mypyc/doc/differences_from_python.rst @@ -260,6 +260,45 @@ When run as interpreted, the first example will execute slower due to the extra namespace lookups. In interpreted code final attributes can also be modified. +.. _free-threading: + +Free threading +-------------- + +Mypyc has basic support for free threading, but it doesn't provide the +same memory safety guarantees as Python in compiled modules, since +in current Python versions this would cause an unacceptable performance +impact. + +The exact details of the memory safety in the presence of data races +are likely to evolve in the future. Currently, compiled code must +ensure that proper synchronization is used to prevent data races. In +particular, these operations require explicit synchronization, such as +via ``threading.Lock``, if there is a possibility of data races +(the list is not exhaustive): + +* Reads or writes of non-final instance data attributes of native + classes. +* List item access or iteration using a ``list`` static type (using + ``Sequence`` or ``MutableSequence`` as the type ensure correct + implicit synchronization). +* Dict item access using a static ``dict`` type (using ``Mapping`` + or ``MutableMapping`` ensures correct implicit synchronization). + +As libraries often won't be able to control the concurrent access by +user code, we recommend that modules document that multi-threaded +access is only supported via public interfaces that ensure correct +synchronization. Marking attributes as internal using an underscore +attribute prefix is another possibility, but this is not enforced at +runtime. Another option is to document that multithreaded access is +not supported, or that particular objects should not be used from +multiple threads concurrently. + +It's always safe to perform read-only operations concurrently. Using +objects with final attributes and tuple objects can help prevent +race conditions without introducing extra overhead from explicit +synchronization operations. + Unsupported features -------------------- @@ -284,10 +323,8 @@ Dunder methods Native classes **cannot** use these dunders. If defined, they will not work as expected. -* ``__del__`` * ``__index__`` -* ``__getattr__``, ``__getattribute__`` -* ``__setattr__`` +* ``__getattribute__`` * ``__delattr__`` Generator expressions @@ -318,7 +355,6 @@ non-exhaustive list of what won't work: - Compiled methods aren't considered methods by ``inspect.ismethod`` - ``inspect.signature`` chokes on compiled functions with default arguments that are not simple literals -- ``inspect.iscoroutinefunction`` and ``asyncio.iscoroutinefunction`` will always return False for compiled functions, even those defined with `async def` Profiling hooks and tracing *************************** diff --git a/mypyc/doc/introduction.rst b/mypyc/doc/introduction.rst index 785316d959050..c14ef53ed1473 100644 --- a/mypyc/doc/introduction.rst +++ b/mypyc/doc/introduction.rst @@ -23,8 +23,8 @@ also as normal, interpreted Python modules. Existing code with type annotations is often **1.5x to 5x** faster when compiled. Code tuned for mypyc can be **5x to 10x** faster. -Mypyc currently aims to speed up non-numeric code, such as server -applications. Mypyc is also used to compile itself (and mypy). +Mypyc aims to be effective in many common use cases, including +server applications. Mypyc is also used to compile itself (and mypy). Why mypyc? ---------- @@ -54,10 +54,11 @@ requires only minor changes to compile using mypyc. normal Python code. You can use interpreted Python during development, with familiar and fast workflows. -**Runtime type safety.** Mypyc protects you from segfaults and memory -corruption. Any unexpected runtime type safety violation is a bug in -mypyc. Runtime values are checked against type annotations. (Without -mypyc, type annotations are ignored at runtime.) +**Runtime type safety.** Mypyc partially protects you from segfaults and +memory corruption (see :ref:`free-threading` for limitations). Any +unexpected runtime type safety violation is a bug in mypyc. Runtime +values are checked against type annotations. (Without mypyc, type +annotations are ignored at runtime.) **Find errors statically.** Mypyc uses mypy for static type checking that helps catch many bugs. @@ -110,8 +111,7 @@ differently, however: * Mypyc performs strict enforcement of type annotations at runtime, resulting in better runtime type safety and easier debugging. -Unlike Cython, mypyc doesn't directly support interfacing with C libraries -or speeding up numeric code. +Unlike Cython, mypyc doesn't directly support interfacing with C libraries. How does it work ---------------- From 488a64692019752becae1f721901d7548c4c74c1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 17 May 2026 19:23:28 +0100 Subject: [PATCH 027/127] Bump ast-serialize to 0.5.0 (#21501) This is mostly needed for https://github.com/python/mypy/pull/21260 --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index 7fe111ac7da59..04381db913521 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -6,4 +6,4 @@ mypy_extensions>=1.0.0 pathspec>=1.0.0 tomli>=1.1.0; python_version<'3.11' librt>=0.11.0; platform_python_implementation != 'PyPy' -ast-serialize>=0.4.0,<1.0.0 +ast-serialize>=0.5.0,<1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 123b7a8f4880e..a89bac5c356eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ requires = [ "types-psutil", "types-setuptools", # required to work around a mypyc import bug - "ast-serialize>=0.4.0,<1.0.0", + "ast-serialize>=0.5.0,<1.0.0", ] build-backend = "setuptools.build_meta" @@ -59,7 +59,7 @@ dependencies = [ "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", "librt>=0.11.0; platform_python_implementation != 'PyPy'", - "ast-serialize>=0.4.0,<1.0.0", + "ast-serialize>=0.5.0,<1.0.0", ] dynamic = ["version"] diff --git a/test-requirements.txt b/test-requirements.txt index 55e4b109346c9..254c6b8aaaa28 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=test-requirements.txt --strip-extras test-requirements.in # -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via -r mypy-requirements.txt attrs==26.1.0 # via -r test-requirements.in From d9f17fba251699162640b74cec74931b87c0c443 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 18 May 2026 21:14:58 +0300 Subject: [PATCH 028/127] [mypyc] Add `librt.strings.isdigit` codepoint primitive (#21504) 2nd PR building https://github.com/python/mypy/issues/21418 Wraps `Py_UNICODE_ISDIGIT` for the codepoint fast path, mirroring the already-merged `librt.strings.isspace`. I've microbenchmarked both codepoint primitives and they're ~1.6x faster than their counterpart `str` specialized primitives. --- mypy/typeshed/stubs/librt/librt/strings.pyi | 1 + mypyc/lib-rt/codepoint_extra_ops.h | 4 ++++ mypyc/lib-rt/strings/librt_strings.c | 4 ++++ mypyc/primitives/librt_strings_ops.py | 9 +++++++++ mypyc/test-data/irbuild-librt-strings.test | 14 +++++++++++++ mypyc/test-data/run-librt-strings.test | 22 ++++++++++++--------- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 46e9eac68b24f..215fdf6fd56b9 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -44,3 +44,4 @@ def read_f64_be(b: bytes, index: i64, /) -> float: ... # Codepoint classification helpers operating on i32 codepoints (typically # obtained via ord(s[i])). Negative inputs return False. def isspace(c: i32, /) -> bool: ... +def isdigit(c: i32, /) -> bool: ... diff --git a/mypyc/lib-rt/codepoint_extra_ops.h b/mypyc/lib-rt/codepoint_extra_ops.h index 5633efb0987ee..13e530e1d90f3 100644 --- a/mypyc/lib-rt/codepoint_extra_ops.h +++ b/mypyc/lib-rt/codepoint_extra_ops.h @@ -13,4 +13,8 @@ static inline bool LibRTStrings_IsSpace(int32_t c) { return c >= 0 && Py_UNICODE_ISSPACE((Py_UCS4)c); } +static inline bool LibRTStrings_IsDigit(int32_t c) { + return c >= 0 && Py_UNICODE_ISDIGIT((Py_UCS4)c); +} + #endif // MYPYC_CODEPOINT_EXTRA_OPS_H diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index ecde8c527f9d4..97a1c67f46237 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -1191,6 +1191,7 @@ cp_parse_i32(PyObject *arg, int32_t *out) { } DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace) +DEFINE_CP_BOOL_WRAPPER(isdigit, LibRTStrings_IsDigit) static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, @@ -1256,6 +1257,9 @@ static PyMethodDef librt_strings_module_methods[] = { {"isspace", cp_isspace, METH_O, PyDoc_STR("Test whether a codepoint (i32) is Unicode whitespace.") }, + {"isdigit", cp_isdigit, METH_O, + PyDoc_STR("Test whether a codepoint (i32) is a Unicode digit.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index 0a4b2515c1ea8..968aeb6014c44 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -404,3 +404,12 @@ error_kind=ERR_NEVER, dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], ) + +function_op( + name="librt.strings.isdigit", + arg_types=[int32_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTStrings_IsDigit", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], +) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index 9bb2312b0d88a..bffa96dd50098 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -345,3 +345,17 @@ L8: L9: r16 = LibRTStrings_IsSpace(r15) return r16 + +[case testLibrtStringsIsDigitIR] +from librt.strings import isdigit +from mypy_extensions import i32 + +def is_d(c: i32) -> bool: + return isdigit(c) +[out] +def is_d(c): + c :: i32 + r0 :: bool +L0: + r0 = LibRTStrings_IsDigit(c) + return r0 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 3c1f686867fbd..211f88f72e085 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1440,19 +1440,23 @@ def test_new_without_init_is_usable() -> None: sw.write("hello") assert sw.getvalue() == "hello" -[case testLibrtStringsIsSpace_librt] +[case testLibrtStringsCodepointClassifiers_librt] from typing import Any from mypy_extensions import i32 -from librt.strings import isspace +from librt.strings import isspace, isdigit -def test_isspace() -> None: - assert not isspace(i32(-1)) - assert not isspace(i32(-113)) - # Verify our codepoint primitive agrees with str.isspace() across all - # Unicode codepoints, including the ord(chr(i)) round-trip. Any - # forces generic dispatch on the str side. +def test_codepoint_classifiers() -> None: + # Negative values are not codepoints. + for bad in (i32(-1), i32(-113)): + assert not isspace(bad) + assert not isdigit(bad) + # Verify each codepoint primitive agrees with the matching str method + # across all Unicode codepoints, including the ord(chr(i)) round-trip. + # Any forces generic dispatch on the str side. for i in range(0x110000): c = chr(i) a: Any = c - assert isspace(ord(c)) == isspace(i) == a.isspace() + o = ord(c) + assert isspace(o) == isspace(i) == a.isspace() + assert isdigit(o) == isdigit(i) == a.isdigit() From 93eff8370a05c4a4be2475649370502865247d2f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 19 May 2026 00:00:11 +0100 Subject: [PATCH 029/127] Use explicit Never for type inference (#21497) Fixes https://github.com/python/mypy/issues/20391 The problem became more prominent with type variables with defaults, so better be consistent. --- mypy/applytype.py | 2 +- mypy/checker.py | 2 +- mypy/checkexpr.py | 6 +++--- mypy/types.py | 4 ++-- test-data/unit/check-typevar-defaults.test | 9 +++++++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index c8003795ba0b1..731200a06b651 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -39,7 +39,7 @@ def get_target_type( skip_unsatisfied: bool, ) -> Type | None: p_type = get_proper_type(type) - if isinstance(p_type, UninhabitedType) and tvar.has_default(): + if isinstance(p_type, UninhabitedType) and p_type.ambiguous and tvar.has_default(): return tvar.default if isinstance(tvar, ParamSpecType): return type diff --git a/mypy/checker.py b/mypy/checker.py index 80402e71dce69..2cb3d69b2d770 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4552,7 +4552,7 @@ def check_lvalue( self.check_lvalue(sub_expr)[0] or # This type will be used as a context for further inference of rvalue, # we put Uninhabited if there is no information available from lvalue. - UninhabitedType() + UninhabitedType(ambiguous=True) for sub_expr in lvalue.items ] lvalue_type = TupleType(types, self.named_type("builtins.tuple")) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 48ea7ab51f61b..c968e5cc6923b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2085,7 +2085,7 @@ def infer_function_type_arguments_using_context( # Only substitute non-Uninhabited and non-erased types. new_args: list[Type | None] = [] for arg in args: - if has_uninhabited_component(arg) or has_erased_component(arg): + if has_ambiguous_uninhabited_component(arg) or has_erased_component(arg): new_args.append(None) else: new_args.append(arg) @@ -6719,8 +6719,8 @@ def visit_uninhabited_type(self, t: UninhabitedType) -> bool: return True -def has_ambiguous_uninhabited_component(t: Type) -> bool: - return t.accept(HasAmbiguousUninhabitedComponentsQuery()) +def has_ambiguous_uninhabited_component(t: Type | None) -> bool: + return t is not None and t.accept(HasAmbiguousUninhabitedComponentsQuery()) class HasAmbiguousUninhabitedComponentsQuery(types.BoolTypeQuery): diff --git a/mypy/types.py b/mypy/types.py index 40c3839e2efca..898a3344a3a56 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1389,9 +1389,9 @@ class UninhabitedType(ProperType): ambiguous: bool # Is this a result of inference for a variable without constraints? - def __init__(self, line: int = -1, column: int = -1) -> None: + def __init__(self, line: int = -1, column: int = -1, *, ambiguous: bool = False) -> None: super().__init__(line, column) - self.ambiguous = False + self.ambiguous = ambiguous def can_be_true_default(self) -> bool: return False diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 535d882ccf3c4..7987178d1fe02 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -150,6 +150,15 @@ def func_error_alias2( reveal_type(c) # N: Revealed type is "builtins.dict[builtins.int, builtins.float]" [builtins fixtures/dict.pyi] +[case testTypeVarDefaultsExplicitNever] +from typing import Generic, Never, TypeVar + +D = TypeVar("D", default=int) +class WithDefault(Generic[D]): pass + +never_with_default: WithDefault[Never] = WithDefault() +explicit_with_default: WithDefault[Never] = WithDefault[Never]() + [case testTypeVarDefaultsFunctions] from typing import TypeVar, ParamSpec, List, Union, Callable, Tuple from typing_extensions import TypeVarTuple, Unpack From 739a65244eb085703bc52e26326bd3a034e8076c Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Tue, 19 May 2026 16:52:35 +0300 Subject: [PATCH 030/127] [mypyc] Add `librt.strings.isalnum` codepoint primitive (#21509) 3rd PR for https://github.com/python/mypy/issues/21418, mirroring `librt.strings.isdigit`. Measured on a microbenchmark this is roughly 30-40% faster for a char --- mypy/typeshed/stubs/librt/librt/strings.pyi | 1 + mypyc/lib-rt/codepoint_extra_ops.h | 4 ++++ mypyc/lib-rt/strings/librt_strings.c | 4 ++++ mypyc/primitives/librt_strings_ops.py | 9 +++++++ mypyc/test-data/irbuild-librt-strings.test | 14 +++++++++++ mypyc/test-data/run-librt-strings.test | 26 ++++++++++++++++++++- 6 files changed, 57 insertions(+), 1 deletion(-) diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 215fdf6fd56b9..5ab1e978d465b 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -45,3 +45,4 @@ def read_f64_be(b: bytes, index: i64, /) -> float: ... # obtained via ord(s[i])). Negative inputs return False. def isspace(c: i32, /) -> bool: ... def isdigit(c: i32, /) -> bool: ... +def isalnum(c: i32, /) -> bool: ... diff --git a/mypyc/lib-rt/codepoint_extra_ops.h b/mypyc/lib-rt/codepoint_extra_ops.h index 13e530e1d90f3..a4f4c6880cafe 100644 --- a/mypyc/lib-rt/codepoint_extra_ops.h +++ b/mypyc/lib-rt/codepoint_extra_ops.h @@ -17,4 +17,8 @@ static inline bool LibRTStrings_IsDigit(int32_t c) { return c >= 0 && Py_UNICODE_ISDIGIT((Py_UCS4)c); } +static inline bool LibRTStrings_IsAlnum(int32_t c) { + return c >= 0 && Py_UNICODE_ISALNUM((Py_UCS4)c); +} + #endif // MYPYC_CODEPOINT_EXTRA_OPS_H diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index 97a1c67f46237..ce15107f7e09d 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -1192,6 +1192,7 @@ cp_parse_i32(PyObject *arg, int32_t *out) { DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace) DEFINE_CP_BOOL_WRAPPER(isdigit, LibRTStrings_IsDigit) +DEFINE_CP_BOOL_WRAPPER(isalnum, LibRTStrings_IsAlnum) static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, @@ -1260,6 +1261,9 @@ static PyMethodDef librt_strings_module_methods[] = { {"isdigit", cp_isdigit, METH_O, PyDoc_STR("Test whether a codepoint (i32) is a Unicode digit.") }, + {"isalnum", cp_isalnum, METH_O, + PyDoc_STR("Test whether a codepoint (i32) is alphanumeric.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index 968aeb6014c44..0432cade7b9af 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -413,3 +413,12 @@ error_kind=ERR_NEVER, dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], ) + +function_op( + name="librt.strings.isalnum", + arg_types=[int32_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTStrings_IsAlnum", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], +) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index bffa96dd50098..8b27f6a672568 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -359,3 +359,17 @@ def is_d(c): L0: r0 = LibRTStrings_IsDigit(c) return r0 + +[case testLibrtStringsIsAlnumIR] +from librt.strings import isalnum +from mypy_extensions import i32 + +def is_an(c: i32) -> bool: + return isalnum(c) +[out] +def is_an(c): + c :: i32 + r0 :: bool +L0: + r0 = LibRTStrings_IsAlnum(c) + return r0 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 211f88f72e085..141d830d3d0b9 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1443,7 +1443,9 @@ def test_new_without_init_is_usable() -> None: [case testLibrtStringsCodepointClassifiers_librt] from typing import Any from mypy_extensions import i32 -from librt.strings import isspace, isdigit +from librt.strings import isspace, isdigit, isalnum + +from testutil import assertRaises def test_codepoint_classifiers() -> None: @@ -1451,6 +1453,7 @@ def test_codepoint_classifiers() -> None: for bad in (i32(-1), i32(-113)): assert not isspace(bad) assert not isdigit(bad) + assert not isalnum(bad) # Verify each codepoint primitive agrees with the matching str method # across all Unicode codepoints, including the ord(chr(i)) round-trip. # Any forces generic dispatch on the str side. @@ -1460,3 +1463,24 @@ def test_codepoint_classifiers() -> None: o = ord(c) assert isspace(o) == isspace(i) == a.isspace() assert isdigit(o) == isdigit(i) == a.isdigit() + assert isalnum(o) == isalnum(i) == a.isalnum() + + +def test_codepoint_classifiers_via_any() -> None: + # Iterate so the callee is opaque to mypyc and dispatch falls back to + # the PyMethodDef wrapper, exercising the i32 range check. + for fn, true_input, false_input in ( + (isspace, " ", "a"), + (isdigit, "5", "a"), + (isalnum, "A", " "), + ): + f: Any = fn + assert f(ord(true_input)) is True + assert f(ord(false_input)) is False + # Negative values are valid i32, just not codepoints. + assert f(-1) is False + # Inputs outside i32 range raise OverflowError through the wrapper. + with assertRaises(OverflowError, "codepoint out of i32 range"): + f(1 << 40) + with assertRaises(OverflowError, "codepoint out of i32 range"): + f(-(1 << 40)) From 5e0c27498cad785997588579d819a3885608a7eb Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 19 May 2026 17:27:30 +0300 Subject: [PATCH 031/127] [mypyc] Fix cross-group call to inherited __mypyc_defaults_setup (#21481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `separate=True` and cross-module inheritance, when only the subclass module is recompiled incrementally and the parent is loaded from mypy's incremental cache, `find_attr_initializers` gathers no defaults from the parent. The subclass therefore has no `__mypyc_defaults_setup` of its own, and `ClassIR.get_method` walks the MRO and returns the parent's. Without this fix, `emit_attr_defaults_func_call` emits a raw `CPyDef____...(...)` call. The parent's header only declares that function as a pointer inside `struct export_table_`, so the symbol isn't reachable as a free function from the subclass's compilation unit and clang/gcc fail with: ``` error: call to undeclared function 'CPyDef_________mypyc_defaults_setup'; ISO C99 and later do not support implicit function declarations ``` A cold build doesn't hit this because the parent's `defs.body` is populated (everything is freshly parsed), so the subclass gets its own `__mypyc_defaults_setup` and the call is intra-group. Likewise, an incremental change that propagates through interface-hash deps to the parent makes it get reparsed too, avoiding the trigger. The bug requires an invalidation pattern that touches the subclass but **not** the parent. ## Fix `emit_attr_defaults_func_call` in `mypyc/codegen/emitclass.py` now applies `emitter.get_group_prefix(defaults_fn.decl)` when emitting the call, matching the pattern already used by the other cross-group call sites in this file (`emit_setup_or_dunder_new_call`, `generate_constructor_for_class`, etc.). `get_group_prefix` returns `""` for same-group calls (so intra-group behaviour is unchanged) and `"exports_."` when the target lives in a different group. It also registers the target group in `context.group_deps` so the right header gets `#include`d. ## Tests Added `testIncrementalCrossModuleInheritedAttrDefaults` in `mypyc/test-data/run-multimodule.test`, a two-step test that reproduces the bug under `TestRunSeparate`: parent in `other_b.py` with attribute defaults, empty subclass in `other_a.py`, step 2 modifies `other_a.py` to trigger a recompile without touching the parent. Verified to fail under `TestRunSeparate` (with the implicit-declaration error) on the unpatched tree and to pass under all three modes (`TestRun`, `TestRunMultiFile`, `TestRunSeparate`) with the fix. Local checks: - `pre-commit run --all-files` — pass - `python runtests.py self` — pass - mypyc unit tests (1077 + 1 skipped) — pass - `TestRunSeparate` + `TestRunMultiFile` run tests (110) — pass ## Real-world impact Surfaced first against `sqlglot-mypy==1.20.0.post6` (a downstream of mypy used to compile sqlglot) in CI when `build/` and `.mypy_cache/` were preserved across GitHub Actions runs and a PR happened to edit only subclass modules. The incremental compile produced malformed C and failed with the implicit-declaration error. Stripped of sqlglot specifics, the bug requires only `separate=True` + cross-module inheritance + parent with attribute defaults + subclass without its own defaults + an invalidation pattern where the subclass is rechecked but the parent is not. These conditions are common in any non-trivial codebase using mypyc with `separate=True`, so this should be considered a latent issue affecting incremental builds of such codebases. --------- Co-authored-by: Claude Opus 4.7 (1M context) --- mypyc/codegen/emitclass.py | 4 +-- mypyc/test-data/run-multimodule.test | 39 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index a312311b21a04..ea3e2ddd74fea 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -683,8 +683,8 @@ def emit_attr_defaults_func_call(defaults_fn: FuncIR, self_name: str, emitter: E The code returns NULL on a raised exception. """ emitter.emit_lines( - "if ({}{}((PyObject *){}) == 0) {{".format( - NATIVE_PREFIX, defaults_fn.cname(emitter.names), self_name + "if ({}((PyObject *){}) == 0) {{".format( + emitter.native_function_call(defaults_fn.decl), self_name ), "Py_DECREF(self);", "return NULL;", diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index 3ab589ab1530a..aa9dd7eb5ef1c 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1423,6 +1423,45 @@ class Parent: from native import test test() +[case testIncrementalCrossModuleInheritedAttrDefaults] +# Regression: under separate=True, when only the subclass module is +# recompiled (parent loaded from mypy's incremental cache, so its +# ClassDef.defs.body is empty), the subclass produces no +# __mypyc_defaults_setup of its own and ClassIR.get_method returns +# the parent's. The emitted call must use the cross-group +# exports_. prefix, otherwise the generated C references an +# undeclared symbol and clang/gcc fail to compile. +import other_a + +def test() -> None: + c = other_a.Child() + assert c.x == 1 + assert c.y == "hello" + +[file other_b.py] +class Parent: + x: int = 1 + y: str = "hello" + +[file other_a.py] +from other_b import Parent + +class Child(Parent): + pass + +[file other_a.py.2] +from other_b import Parent + +class Child(Parent): + pass + +def _force_recompile() -> int: + return 1 + +[file driver.py] +from native import test +test() + [case testExtraOpsFunction_experimental_librt] from librt.strings import BytesWriter From d23f5f0026eecbdb3fda97ebe3155867e78f0924 Mon Sep 17 00:00:00 2001 From: Zakir Jiwani Date: Tue, 19 May 2026 17:22:11 -0400 Subject: [PATCH 032/127] Fix false positive "Expected TypedDict key to be string literal" for Union[TypedDict, dict[K, V]] (#21511) When a variable is typed as `Union[TypedDict, dict[int, float]]`, mypy incorrectly raised `Expected TypedDict key to be string literal` when assigning a dict literal with non-string keys like `{1: 5.2}`. The plain `dict[int, float]` alternative makes the assignment valid, so this was a false positive. The root cause was that `match_typeddict_call_with_dict`, which is used as a probe to check whether a dict literal could match a TypedDict, was emitting errors from `validate_typeddict_kwargs` during the matching phase rather than silently returning `False`. Wrapping the call in `filter_errors()` suppresses those spurious diagnostics. Fixes #21510 Co-authored-by: Claude Agent --- mypy/checkexpr.py | 3 ++- test-data/unit/check-typeddict.test | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c968e5cc6923b..882d748afaead 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -932,7 +932,8 @@ def match_typeddict_call_with_dict( kwargs: list[tuple[Expression | None, Expression]], context: Context, ) -> bool: - result = self.validate_typeddict_kwargs(kwargs=kwargs, callee=callee) + with self.msg.filter_errors(): + result = self.validate_typeddict_kwargs(kwargs=kwargs, callee=callee) if result is not None: validated_kwargs, _ = result return callee.required_keys <= set(validated_kwargs.keys()) <= set(callee.items.keys()) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index c74ae96d7763e..17a1fea22ef04 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -941,6 +941,18 @@ c: Union[A, B] = {'@type': 'a-type', 'value': 'Test'} # E: Type of TypedDict is [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testTypedDictUnionWithNonTypedDictLiteralKey] +from typing import Union, TypedDict + +class Foo(TypedDict): + some: str + name: str + +baz: Union[Foo, dict[int, float]] = {1: 5.2} +baz2: Union[Foo, dict[int, float]] = {'some': 'x', 'name': 'y'} +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + -- Use dict literals [case testTypedDictDictLiterals] From b44b2ca7d3d2b9010a7a14881458fb485062ac03 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 19 May 2026 22:47:10 +0100 Subject: [PATCH 033/127] Special-case constructor for tuple types (#21502) Fixes https://github.com/python/mypy/issues/19106 Tuple constructor is quite imprecise, and I don't think we can get anything decent without some special-casing, so I am doing just that, adding one more `special_sig`. --- mypy/checkexpr.py | 22 ++++++++++++++++++ mypy/constraints.py | 16 ++++++++----- mypy/nodes.py | 6 +++++ mypy/typeops.py | 6 +++++ mypy/types.py | 2 +- mypyc/test-data/run-tuples.test | 2 +- test-data/unit/check-typevar-tuple.test | 30 +++++++++++++++++++++++++ 7 files changed, 76 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 882d748afaead..dd914498df87d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1748,6 +1748,28 @@ def check_callable_call( lambda i: self.accept(args[i]), ) + if callee.special_sig == "tuple" and len(args) == 1: + with self.msg.filter_errors(): + arg_type = get_proper_type(self.accept(args[0])) + # Give precise constructor signature for situations like this: + # class Shape[*Ts](tuple[*Ts]): ... + # Shape((1, 2)) + # The argument type is the same as return type, but with builtins.tuple fallback. + if isinstance(arg_type, TupleType): + assert isinstance(callee.ret_type, ProperType) + if isinstance(callee.ret_type, TupleType): + # Actual type argument is ignored by tuple_fallback() in this case. + any_type = AnyType(TypeOfAny.special_form) + new_arg_type = callee.ret_type.copy_modified( + fallback=self.chk.named_generic_type("builtins.tuple", [any_type]) + ) + callee = callee.copy_modified(arg_types=[new_arg_type]) + elif isinstance(callee.ret_type, Instance): + new_arg_type = map_instance_to_supertype( + callee.ret_type, self.chk.lookup_typeinfo("builtins.tuple") + ) + callee = callee.copy_modified(arg_types=[new_arg_type]) + if callee.is_generic(): need_refresh = any( isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables diff --git a/mypy/constraints.py b/mypy/constraints.py index df79fdae5456c..a58af222b1ea8 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1224,8 +1224,9 @@ def infer_against_overloaded( def visit_tuple_type(self, template: TupleType) -> list[Constraint]: actual = self.actual unpack_index = find_unpack_in_list(template.items) - is_varlength_tuple = ( - isinstance(actual, Instance) and actual.type.fullname == "builtins.tuple" + # TODO: we may need to be careful with direction to avoid spurious constraints. + is_varlength_tuple = isinstance(actual, Instance) and actual.type.has_base( + "builtins.tuple" ) if isinstance(actual, TupleType) or is_varlength_tuple: @@ -1237,23 +1238,26 @@ def visit_tuple_type(self, template: TupleType) -> list[Constraint]: unpack_type = template.items[unpack_index] assert isinstance(unpack_type, UnpackType) unpacked_type = get_proper_type(unpack_type.type) + assert isinstance(actual, Instance) # ensured by is_varlength_tuple == True + mapped = map_instance_to_supertype( + actual, actual.type.get_base("builtins.tuple") + ) if isinstance(unpacked_type, TypeVarTupleType): res = [ - Constraint(type_var=unpacked_type, op=self.direction, target=actual) + Constraint(type_var=unpacked_type, op=self.direction, target=mapped) ] else: assert ( isinstance(unpacked_type, Instance) and unpacked_type.type.fullname == "builtins.tuple" ) - res = infer_constraints(unpacked_type, actual, self.direction) - assert isinstance(actual, Instance) # ensured by is_varlength_tuple == True + res = infer_constraints(unpacked_type, mapped, self.direction) for i, ti in enumerate(template.items): if i == unpack_index: # This one we just handled above. continue # For Tuple[T, *Ts, S] <: tuple[X, ...] infer also T <: X and S <: X. - res.extend(infer_constraints(ti, actual.args[0], self.direction)) + res.extend(infer_constraints(ti, mapped.args[0], self.direction)) return res else: assert isinstance(actual, TupleType) diff --git a/mypy/nodes.py b/mypy/nodes.py index 32a694560b24b..bcaf7f7b239cf 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -4103,6 +4103,12 @@ def has_base(self, fullname: str) -> bool: return True return False + def get_base(self, fullname: str) -> TypeInfo: + for cls in self.mro: + if cls.fullname == fullname: + return cls + assert False, f"Missing base {fullname} for {self.fullname}" + def direct_base_classes(self) -> list[TypeInfo]: """Return a direct base classes. diff --git a/mypy/typeops.py b/mypy/typeops.py index e13d6dd0b1732..e777bec191f91 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -240,6 +240,12 @@ def type_object_type( assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this t = method.type result = type_object_type_from_function(t, info, method.info, fallback, is_new) + # Tuple constructor in typeshed is imprecise (and precise one is impossible to express), + # so we special-case constructors for tuple types. Note we skip the tuple class itself + # as a micro-optimization, since it is unlikely one would write tuple((1, 2)). + if method.info.fullname == "builtins.tuple" and info.fullname != "builtins.tuple": + assert isinstance(result, CallableType) + result = result.copy_modified(special_sig="tuple") # Only write cached result is strict_optional=True, otherwise we may get # inconsistent behaviour because of union simplification. if allow_cache and state.strict_optional: diff --git a/mypy/types.py b/mypy/types.py index 898a3344a3a56..5a05962dc4802 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2149,7 +2149,7 @@ class CallableType(FunctionLike): # specified by the user? "special_sig", # Non-None for signatures that require special handling # (currently only values are 'dict' for a signature similar to - # 'dict' and 'partial' for a `functools.partial` evaluation) + # 'dict' and 'partial' for a `functools.partial` evaluation, and 'tuple') "from_type_type", # Was this callable generated by analyzing Type[...] # instantiation? "is_bound", # Is this a bound method? diff --git a/mypyc/test-data/run-tuples.test b/mypyc/test-data/run-tuples.test index abbba46386040..58220a1af7efd 100644 --- a/mypyc/test-data/run-tuples.test +++ b/mypyc/test-data/run-tuples.test @@ -432,7 +432,7 @@ def test_user_defined() -> None: [file copysubclass.py] from typing import Any -class subc(tuple[Any]): +class subc(tuple[Any, ...]): pass [file userdefinedtuple.py] diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index ff5cdc8719bc6..45bb58e1a5972 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2773,3 +2773,33 @@ from typing import Protocol, Unpack class C(Protocol[Unpack]): # E: Free type variable expected in Protocol[...] pass [builtins fixtures/tuple.pyi] + +[case testGenericTupleSubclassConstructor] +from typing import TypeVar, TypeVarTuple, Unpack + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +class S1(tuple[T, S]): ... +class S2(tuple[Unpack[Ts]]): ... + +reveal_type(S1((1, ""))) # N: Revealed type is "tuple[builtins.int, builtins.str, fallback=__main__.S1[builtins.int, builtins.str]]" +reveal_type(S1((1, "", 2))) # N: Revealed type is "tuple[Never, Never, fallback=__main__.S1[Never, Never]]" \ + # E: Argument 1 to "S1" has incompatible type "tuple[int, str, int]"; expected "tuple[Never, Never]" + +a1: S1[int, str] = S1((1, "")) +b1: S1[int, str] = S1((1, 1)) # E: Argument 1 to "S1" has incompatible type "tuple[int, int]"; expected "tuple[int, str]" +c1: S1[int, str] = S1((1, "", 2)) # E: Argument 1 to "S1" has incompatible type "tuple[int, str, int]"; expected "tuple[int, str]" + +reveal_type(S2(())) # N: Revealed type is "tuple[(), fallback=__main__.S2[()]]" +reveal_type(S2((1, ""))) # N: Revealed type is "tuple[Literal[1]?, Literal['']?, fallback=__main__.S2[Literal[1]?, Literal['']?]]" +reveal_type(S2((1, "", 2))) # N: Revealed type is "tuple[Literal[1]?, Literal['']?, Literal[2]?, fallback=__main__.S2[Literal[1]?, Literal['']?, Literal[2]?]]" + +a2: S2[int, str] = S2((1, "")) +b2: S2[int, str] = S2((1, 1)) # E: Argument 1 to "S2" has incompatible type "tuple[int, int]"; expected "tuple[int, str]" +c2: S2[int, str] = S2((1, "", 2)) # E: Argument 1 to "S2" has incompatible type "tuple[int, str, int]"; expected "tuple[int, str]" + +t: S2[Unpack[tuple[int, ...]]] = S2((1, 2)) +t_bad: S2[Unpack[tuple[int, ...]]] = S2((1, "")) # E: Argument 1 to "S2" has incompatible type "tuple[int, str]"; expected "tuple[int, ...]" +[builtins fixtures/tuple.pyi] From 3a969d1af84cdda459f5403e15a978d4ed67b46c Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Wed, 20 May 2026 03:47:03 +0300 Subject: [PATCH 034/127] Fix crash for empty Annotated type application (#21503) Fixes #21471. ## Summary - Avoid special-casing `Annotated[...]` as its inner type when the subscript has no arguments. - Let the normal type analyzer report the existing `Annotated[...]` argument error instead of crashing. - Add a regression test for `list[Annotated[()]]` in type application context. Prepared with AI assistance; I reviewed and verified the changes. --------- Co-authored-by: cyphercodes Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypy/exprtotype.py | 2 +- test-data/unit/check-annotated.test | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 5a22b9c3c759a..1c9323be056dd 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -122,7 +122,7 @@ def expr_to_unanalyzed_type( else: base_fullname = expr.base.fullname - if base_fullname is not None and base_fullname in ANNOTATED_TYPE_NAMES: + if base_fullname is not None and base_fullname in ANNOTATED_TYPE_NAMES and args: # TODO: this is not the optimal solution as we are basically getting rid # of the Annotation definition and only returning the type information, # losing all the annotations. diff --git a/test-data/unit/check-annotated.test b/test-data/unit/check-annotated.test index d4de3f7b5043f..03ae8906f1d32 100644 --- a/test-data/unit/check-annotated.test +++ b/test-data/unit/check-annotated.test @@ -41,6 +41,11 @@ x: Annotated[int] # E: Annotated[...] must have exactly one type argument and a reveal_type(x) # N: Revealed type is "Any" [builtins fixtures/tuple.pyi] +[case testAnnotatedBadEmptyTupleIndexInTypeApplication] +from typing_extensions import Annotated +list[Annotated[()]] # E: Annotated[...] must have exactly one type argument and at least one annotation +[builtins fixtures/list.pyi] + [case testAnnotatedNested0] from typing_extensions import Annotated x: Annotated[Annotated[int, ...], ...] From 9b96cbe2a81e2844be5fc329f28afaf863680810 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 20 May 2026 10:55:02 +0100 Subject: [PATCH 035/127] Fix edge cases in variadic tuple subclasses (#21518) Fixes https://github.com/python/mypy/issues/19110 Two (mostly unrelated) things here: * Handle empty tuple index in tuple subclasses * Move tuple special-casing in map type to inner function to handle situations with indirect tuple subclasses. --- mypy/maptype.py | 41 +++++++++++++------------ mypy/typeanal.py | 1 + test-data/unit/check-typevar-tuple.test | 35 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/mypy/maptype.py b/mypy/maptype.py index 59ecb2bc99930..d9bb6da8080a0 100644 --- a/mypy/maptype.py +++ b/mypy/maptype.py @@ -16,25 +16,6 @@ def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Insta # Fast path: `instance` already belongs to `superclass`. return instance - if superclass.fullname == "builtins.tuple" and instance.type.tuple_type: - if has_type_vars(instance.type.tuple_type): - # We special case mapping generic tuple types to tuple base, because for - # such tuples fallback can't be calculated before applying type arguments. - alias = instance.type.special_alias - assert alias is not None - if not alias._is_recursive: - # Unfortunately we can't support this for generic recursive tuples. - # If we skip this special casing we will fall back to tuple[Any, ...]. - tuple_type = expand_type_by_instance(instance.type.tuple_type, instance) - if isinstance(tuple_type, TupleType): - # Make the import here to avoid cyclic imports. - import mypy.typeops - - return mypy.typeops.tuple_fallback(tuple_type) - elif isinstance(tuple_type, Instance): - # This can happen after normalizing variadic tuples. - return tuple_type - if not superclass.type_vars: # Fast path: `superclass` has no type variables to map to. return Instance(superclass, []) @@ -93,6 +74,28 @@ def map_instance_to_direct_supertypes(instance: Instance, supertype: TypeInfo) - for b in typ.bases: if b.type == supertype: + + if supertype.fullname == "builtins.tuple" and instance.type.tuple_type: + if has_type_vars(instance.type.tuple_type): + # We special case mapping generic tuple types to tuple base, because for + # such tuples fallback can't be calculated before applying type arguments. + alias = instance.type.special_alias + assert alias is not None + if not alias._is_recursive: + # Unfortunately we can't support this for generic recursive tuples. + # If we skip this special casing we will fall back to tuple[Any, ...]. + tuple_type = expand_type_by_instance(instance.type.tuple_type, instance) + if isinstance(tuple_type, TupleType): + # Make the import here to avoid cyclic imports. + import mypy.typeops + + result.append(mypy.typeops.tuple_fallback(tuple_type)) + continue + elif isinstance(tuple_type, Instance): + # This can happen after normalizing variadic tuples. + result.append(tuple_type) + continue + t = expand_type_by_instance(b, instance) assert isinstance(t, Instance) result.append(t) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index db56256192625..0c0399d91ecca 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -899,6 +899,7 @@ def analyze_type_with_type_info( ctx, self.options, use_standard_error=True, + empty_tuple_index=empty_tuple_index, ) return tup.copy_modified( items=self.anal_array(tup.items, allow_unpack=True), fallback=instance diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 45bb58e1a5972..8d3f3cd805315 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2803,3 +2803,38 @@ c2: S2[int, str] = S2((1, "", 2)) # E: Argument 1 to "S2" has incompatible type t: S2[Unpack[tuple[int, ...]]] = S2((1, 2)) t_bad: S2[Unpack[tuple[int, ...]]] = S2((1, "")) # E: Argument 1 to "S2" has incompatible type "tuple[int, str]"; expected "tuple[int, ...]" [builtins fixtures/tuple.pyi] + +[case testVariadicTupleSubclassEmptyIndex] +from typing import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +class Shape(tuple[Unpack[Ts]]): ... + +class Shape0(Shape[()]): ... + +x: Shape[()] +y: Shape0 + +reveal_type(x) # N: Revealed type is "tuple[(), fallback=__main__.Shape[()]]" +reveal_type(y) # N: Revealed type is "tuple[(), fallback=__main__.Shape0]" + +def test(t: tuple[int, ...]) -> None: ... +test(x) +test(y) +[builtins fixtures/tuple.pyi] + +[case testVariadicTupleSubclassVariadicIndex] +from typing import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +class Shape(tuple[Unpack[Ts]]): ... + +class ShapeN(Shape[Unpack[tuple[int, ...]]]): ... + +x: Shape[Unpack[tuple[int, ...]]] +y: ShapeN + +def test(t: tuple[int, ...]) -> None: ... +test(x) +test(y) +[builtins fixtures/tuple.pyi] From a9d2d1476a8ccad967a19f9d562f1703f51aae73 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Wed, 20 May 2026 14:25:34 +0300 Subject: [PATCH 036/127] [mypyc] Add `librt.strings.isalpha` codepoint primitive (#21521) 4th PR of https://github.com/python/mypy/issues/21418 This is 5x faster than `str.isalpha()` for ASCII. --- mypy/typeshed/stubs/librt/librt/strings.pyi | 1 + mypyc/lib-rt/codepoint_extra_ops.h | 4 ++++ mypyc/lib-rt/strings/librt_strings.c | 4 ++++ mypyc/primitives/librt_strings_ops.py | 9 +++++++++ mypyc/test-data/irbuild-librt-strings.test | 14 ++++++++++++++ mypyc/test-data/run-librt-strings.test | 5 ++++- 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 5ab1e978d465b..01aee3ff758d3 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -46,3 +46,4 @@ def read_f64_be(b: bytes, index: i64, /) -> float: ... def isspace(c: i32, /) -> bool: ... def isdigit(c: i32, /) -> bool: ... def isalnum(c: i32, /) -> bool: ... +def isalpha(c: i32, /) -> bool: ... diff --git a/mypyc/lib-rt/codepoint_extra_ops.h b/mypyc/lib-rt/codepoint_extra_ops.h index a4f4c6880cafe..bb83f92e4b87c 100644 --- a/mypyc/lib-rt/codepoint_extra_ops.h +++ b/mypyc/lib-rt/codepoint_extra_ops.h @@ -21,4 +21,8 @@ static inline bool LibRTStrings_IsAlnum(int32_t c) { return c >= 0 && Py_UNICODE_ISALNUM((Py_UCS4)c); } +static inline bool LibRTStrings_IsAlpha(int32_t c) { + return c >= 0 && Py_UNICODE_ISALPHA((Py_UCS4)c); +} + #endif // MYPYC_CODEPOINT_EXTRA_OPS_H diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index ce15107f7e09d..cbc3e5f753fa6 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -1193,6 +1193,7 @@ cp_parse_i32(PyObject *arg, int32_t *out) { DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace) DEFINE_CP_BOOL_WRAPPER(isdigit, LibRTStrings_IsDigit) DEFINE_CP_BOOL_WRAPPER(isalnum, LibRTStrings_IsAlnum) +DEFINE_CP_BOOL_WRAPPER(isalpha, LibRTStrings_IsAlpha) static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, @@ -1264,6 +1265,9 @@ static PyMethodDef librt_strings_module_methods[] = { {"isalnum", cp_isalnum, METH_O, PyDoc_STR("Test whether a codepoint (i32) is alphanumeric.") }, + {"isalpha", cp_isalpha, METH_O, + PyDoc_STR("Test whether a codepoint (i32) is a Unicode letter.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index 0432cade7b9af..93fa717cf5290 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -422,3 +422,12 @@ error_kind=ERR_NEVER, dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], ) + +function_op( + name="librt.strings.isalpha", + arg_types=[int32_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTStrings_IsAlpha", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], +) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index 8b27f6a672568..e5d18b6eb8522 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -373,3 +373,17 @@ def is_an(c): L0: r0 = LibRTStrings_IsAlnum(c) return r0 + +[case testLibrtStringsIsAlphaIR] +from librt.strings import isalpha +from mypy_extensions import i32 + +def is_a(c: i32) -> bool: + return isalpha(c) +[out] +def is_a(c): + c :: i32 + r0 :: bool +L0: + r0 = LibRTStrings_IsAlpha(c) + return r0 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 141d830d3d0b9..aa38c713d3841 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1443,7 +1443,7 @@ def test_new_without_init_is_usable() -> None: [case testLibrtStringsCodepointClassifiers_librt] from typing import Any from mypy_extensions import i32 -from librt.strings import isspace, isdigit, isalnum +from librt.strings import isspace, isdigit, isalnum, isalpha from testutil import assertRaises @@ -1454,6 +1454,7 @@ def test_codepoint_classifiers() -> None: assert not isspace(bad) assert not isdigit(bad) assert not isalnum(bad) + assert not isalpha(bad) # Verify each codepoint primitive agrees with the matching str method # across all Unicode codepoints, including the ord(chr(i)) round-trip. # Any forces generic dispatch on the str side. @@ -1464,6 +1465,7 @@ def test_codepoint_classifiers() -> None: assert isspace(o) == isspace(i) == a.isspace() assert isdigit(o) == isdigit(i) == a.isdigit() assert isalnum(o) == isalnum(i) == a.isalnum() + assert isalpha(o) == isalpha(i) == a.isalpha() def test_codepoint_classifiers_via_any() -> None: @@ -1473,6 +1475,7 @@ def test_codepoint_classifiers_via_any() -> None: (isspace, " ", "a"), (isdigit, "5", "a"), (isalnum, "A", " "), + (isalpha, "A", " "), ): f: Any = fn assert f(ord(true_input)) is True From ab8e4bf1305416372ea70076193a766800f26410 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 20 May 2026 19:49:36 +0300 Subject: [PATCH 037/127] [mypyc] Fix missing cross-group header deps in incremental builds (#21490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem In `separate=True` mode, each group's `__native_internal_.h` reaches into a sibling group's export-table header via an angled include: ```c // __native_internal_caller.h #include ``` The consumer's `.o` file bakes in byte offsets from that struct at C compile time. If the sibling group's struct layout shifts (e.g. a class is inserted earlier in the source, shifting all subsequent offsets), the consumer's `.o` must be recompiled, otherwise it silently resolves offsets to the wrong object at runtime. Two bugs in the old `get_header_deps` combined to hide this dep from setuptools' `Extension.depends`: 1. **No transitive walk.** The cross-group include lives inside `__native_internal_.h`, not in the `.c` file itself. The old code only scanned the `.c` file's direct includes and never opened the resolved headers. 2. **Single-pass resolution.** Deps were resolved per-group before sibling groups had written their headers to disk, so the cross-group header didn't yet exist even if we had looked. ## Fix - **`_extract_includes` / `_INCLUDE_RE`**: distinguishes `#include "foo"` (quoted) from `#include ` (angled), matching the preprocessor's own search rules — quoted form searches the includer's directory first; angled form searches `-I` paths only. - **`resolve_cfile_deps`**: transitive walker that opens each resolved header and follows its `#include` directives, bounded by a visited set. Headers that don't exist under `target_dir` (lib-rt headers like ``) are dropped since they never change between builds. - **Two-pass loop in `mypyc_build`**: first pass writes all groups' files to disk; second pass resolves deps so every group's headers are present when cross-group includes are looked up. ## Impact on non-incremental builds None. `Extension.depends` is only consulted by setuptools when a `.o` already exists. Cold builds compile everything unconditionally. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypyc/build.py | 154 +++++++++++++++++++++++---- mypyc/test-data/run-multimodule.test | 54 ++++++++++ mypyc/test/test_misc.py | 130 ++++++++++++++++++++++ mypyc/test/testutil.py | 6 ++ 4 files changed, 323 insertions(+), 21 deletions(-) diff --git a/mypyc/build.py b/mypyc/build.py index 84633086d2724..13bd50fef3b1a 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -566,22 +566,115 @@ def construct_groups( return groups -def get_header_deps(cfiles: list[tuple[str, str]]) -> list[str]: - """Find all the headers used by a group of cfiles. +# Single regex that captures both `#include "foo"` and `#include `. The +# alternation lets us tell the two forms apart: the quoted-form match populates +# group 1 and the angle-form match populates group 2. The C preprocessor +# applies different search rules to each kind (see `_extract_includes`), so we +# carry the kind through resolution rather than collapsing them up front. +_INCLUDE_RE = re.compile(r'#\s*include\s+(?:"([^"]+)"|<([^>]+)>)') + + +def _extract_includes(contents: str) -> list[tuple[bool, str]]: + """Return each `#include` directive's (is_angled, name) from `contents`. + + is_angled=False for `#include "foo"`, True for `#include `. + """ + out: list[tuple[bool, str]] = [] + for quoted, angled in _INCLUDE_RE.findall(contents): + if quoted: + out.append((False, quoted)) + else: + out.append((True, angled)) + return out + + +def get_header_deps(cfiles: list[tuple[str, str]]) -> list[tuple[bool, str]]: + """Find all the headers directly included by a group of cfiles. + + Returns a sorted, deduplicated list of `(is_angled, header_name)` pairs. + Callers that only need the names can ignore the bool, but it is needed by + `resolve_cfile_deps` to apply the correct preprocessor search order. We do this by just regexping the source, which is a bit simpler than - properly plumbing the data through. + properly plumbing the data through. Transitive header-to-header includes + are picked up by `resolve_cfile_deps` in `mypyc_build`, which can read + the on-disk headers after every group has written its files. Arguments: - cfiles: A list of (file name, file contents) pairs. + cfiles: A list of (file name, file contents) pairs. Contents must be + non-empty; callers handling cached groups must re-read the .c + from disk before calling, otherwise direct includes are missed + and Extension.depends ends up empty. """ - headers: set[str] = set() + assert all( + contents for _, contents in cfiles + ), "get_header_deps requires non-empty file contents" + headers: set[tuple[bool, str]] = set() for _, contents in cfiles: - headers.update(re.findall(r'#include [<"]([^>"]+)[>"]', contents)) + headers.update(_extract_includes(contents)) return sorted(headers) +def resolve_cfile_deps( + cfile_dir: str, direct_includes: list[tuple[bool, str]], target_dir: str +) -> set[str]: + """ + Resolve a .c file's `#include`s to on-disk paths, walking transitively through resolved headers. + + The C preprocessor resolves `#include "foo"` against the includer's directory first, then via + -I, while `#include ` only uses -I. We mirror that exactly: quoted includes are searched + in (includer_dir, target_dir) order, and angled includes are searched in target_dir only. + `target_dir` is the only -I path that holds files we generate; anything we cannot resolve under + it (or, for quoted form, the includer's dir) is dropped. Other headers like `` and + `` live elsewhere and do not change between builds, so they are not real dependencies + for incremental purposes. + + The walk is transitive: each resolved header is opened and scanned for its own `#include` + directives. Without this, cross-group export-table headers reached via `__native_internal_.h` + (which includes ``) would be missed, and edits that shift struct + offsets in `other_group` would not trigger a recompile of the consumer's .o file. Its baked-in + offsets would then resolve to whatever class/function now occupies that slot => runtime corruption. + + Returns a set of resolved paths suitable for use as an Extension.depends list. + """ + resolved: set[str] = set() + + # Worklist of (search_dir, is_angled, header_name). search_dir is the includer's directory; for the + # initial cfile it is the cfile's dir, for a transitively-included header it is that header's dir. + # It is only consulted for quoted-form includes. + worklist: list[tuple[str, bool, str]] = [ + (cfile_dir, is_angled, dep) for is_angled, dep in direct_includes + ] + + while worklist: + search_dir, is_angled, dep = worklist.pop() + # Quoted form: includer's dir first, then -I (target_dir). + # Angled form: -I only (skips the includer's dir). + search_bases = (target_dir,) if is_angled else (search_dir, target_dir) + for base in search_bases: + candidate = os.path.normpath(os.path.join(base, dep)) + if not os.path.exists(candidate): + continue + if candidate in resolved: + break + resolved.add(candidate) + # Recurse only into headers. Some lib-rt sources are pulled in as `#include "init.c"` etc.; + # those do not resolve under target_dir so they get filtered out before we would try to scan + # them, but the .h guard is a cheap belt-and-braces. + if candidate.endswith(".h"): + try: + with open(candidate, encoding="utf-8") as f: + header_contents = f.read() + except OSError: + header_contents = "" + sub_dir = os.path.dirname(candidate) + for sub_angled, sub in _extract_includes(header_contents): + worklist.append((sub_dir, sub_angled, sub)) + break + return resolved + + def mypyc_build( paths: list[str], compiler_options: CompilerOptions, @@ -630,29 +723,48 @@ def mypyc_build( for (path, dirs, internal) in skip_cgen_input[1] ] - # Write out the generated C and collect the files for each group + # Write out the generated C and collect the files for each group. # Should this be here?? - group_cfilenames: list[tuple[list[str], list[str]]] = [] + # + # Header resolution is deferred to a second pass: a header in one group may include a header + # generated by another group, so resolving here misses cross-group deps for groups processed first. + pending: list[list[tuple[str, list[tuple[bool, str]]]]] = [] for cfiles in group_cfiles: - cfilenames = [] + per_cfile_deps: list[tuple[str, list[tuple[bool, str]]]] = [] for cfile, ctext in cfiles: cfile = os.path.join(compiler_options.target_dir, cfile) + # Empty contents marks a file the previous run already wrote # (fully-cached group): skip the rewrite and just reuse it. if ctext and not options.mypyc_skip_c_generation: write_file(cfile, ctext) - if os.path.splitext(cfile)[1] == ".c": - cfilenames.append(cfile) - - # The header regex matches both quote styles, so the result can - # include system headers like `` that don't live under - # target_dir. Joining those produces non-existent paths which - # would force a full rebuild on every run via Extension.depends. - candidate_deps = ( - os.path.join(compiler_options.target_dir, dep) for dep in get_header_deps(cfiles) - ) - deps = [d for d in candidate_deps if os.path.exists(d)] - group_cfilenames.append((cfilenames, deps)) + + # For fully-cached groups ctext is empty; read the on-disk .c so the dep resolver + # can walk its transitive header chain and populate Extension.depends. Otherwise, + # cross-group export-table header changes (e.g. a new class shifting struct offsets) + # won't trigger a recompile of this cached consumer's .o. + if not ctext and os.path.exists(cfile): + try: + with open(cfile, encoding="utf-8") as _f: + ctext = _f.read() + except OSError: + pass + per_cfile_deps.append((cfile, get_header_deps([(cfile, ctext)]))) + pending.append(per_cfile_deps) + + # Second pass: assemble each group's .c filenames and resolve transitive deps now that every group's + # headers are on disk. See resolve_cfile_deps for the rules. + group_cfilenames: list[tuple[list[str], list[str]]] = [] + for per_cfile in pending: + cfilenames = [cf for cf, _ in per_cfile if os.path.splitext(cf)[1] == ".c"] + deps_set: set[str] = set() + for cfile_full, dep_names in per_cfile: + deps_set.update( + resolve_cfile_deps( + os.path.dirname(cfile_full), dep_names, compiler_options.target_dir + ) + ) + group_cfilenames.append((cfilenames, sorted(deps_set))) return groups, group_cfilenames, source_deps diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index aa9dd7eb5ef1c..92b9fa6623fcc 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1391,6 +1391,60 @@ def translate(b: bytes) -> bytes: import native assert native.translate(b'ABCD') == b'BBCD' +[case testIncrementalCrossGroupExportTableOffsets] +# Regression: under separate=True, each consumer module's IR is +# compiled against the positional layout of its deps' +# `exports_` struct. Reordering the dep's classes keeps the +# same set of public names, so mypy's interface hash for the dep is +# unchanged -- the consumer is not invalidated and stays fully +# cached, which causes `_load_cached_group_files` to return empty +# cfile content for the consumer's group. +# +# Before the fix, `get_header_deps` over empty content returned no +# includes, so `Extension.depends` for the consumer ended up empty +# and setuptools never recompiled the consumer's .o when the dep's +# `__native_.h` shifted struct offsets. The stale .o kept the +# old offsets and silently resolved cross-group calls to the wrong +# class. +from other_classes import Gamma, Delta + +def make_gamma() -> Gamma: + return Gamma() + +def make_delta() -> Delta: + return Delta() + +[file other_classes.py] +class Alpha: + a: int = 1 + +class Beta: + b: int = 2 + +class Gamma: + g: int = 3 + +class Delta: + d: int = 4 + +[file other_classes.py.2] +class Delta: + d: int = 4 + +class Alpha: + a: int = 1 + +class Beta: + b: int = 2 + +class Gamma: + g: int = 3 + +[file driver.py] +import native +assert type(native.make_gamma()).__name__ == "Gamma" +assert type(native.make_delta()).__name__ == "Delta" + [case testCrossModuleAttrDefaults] from other import Parent diff --git a/mypyc/test/test_misc.py b/mypyc/test/test_misc.py index 4b0bbe5988afb..816875fcc23db 100644 --- a/mypyc/test/test_misc.py +++ b/mypyc/test/test_misc.py @@ -1,7 +1,10 @@ from __future__ import annotations +import os +import tempfile import unittest +from mypyc.build import get_header_deps, resolve_cfile_deps from mypyc.ir.ops import BasicBlock from mypyc.ir.pprint import format_blocks, generate_names_for_ir from mypyc.irbuild.ll_builder import LowLevelIRBuilder @@ -20,3 +23,130 @@ def test_debug_op(self) -> None: names = generate_names_for_ir([], [block]) code = format_blocks([block], names, {}) assert code[:-1] == ["L0:", " r0 = 'foo'", " CPyDebug_PrintObject(r0)"] + + +class TestHeaderDeps(unittest.TestCase): + """ + Tests for the header-dependency tracking used to build `Extension.depends`, which drives + setuptools' `newer_group` decision about whether to recompile a .o file on incremental builds. + """ + + def test_get_header_deps_quoted_includes(self) -> None: + # Quoted includes: the historical form. Used by the .c file to reach its own __native_.h / + # __native_internal_.h. The `False` in each tuple marks the include as non-angled, which + # `resolve_cfile_deps` uses to search the includer's directory. + cfile = '#include "__native_caller.h"\n#include "__native_internal_caller.h"\n' + assert get_header_deps([("caller.c", cfile)]) == [ + (False, "__native_caller.h"), + (False, "__native_internal_caller.h"), + ] + + def test_get_header_deps_angle_bracket_includes(self) -> None: + # Angle-bracket includes are also matched, and reported with is_angled=True so that the resolver + # skips the includer's dir for them (matching the C preprocessor). The cross-group export header + # is reached via `#include ` in __native_internal_.h. Before + # this was matched the dep was missed entirely and the consumer's .o was never invalidated when + # the other group's struct layout shifted. + cfile = "#include \n#include \n" + assert get_header_deps([("caller.c", cfile)]) == [ + (True, "Python.h"), + (True, "lib/__native_functions.h"), + ] + + def test_get_header_deps_mixed_and_whitespace(self) -> None: + # The preprocessor tolerates whitespace and the leading-hash form. `get_header_deps` returns sorted + # tuples — non-angled (False) sorts before angled (True), then alphabetical within each kind. + cfile = '# include "a.h"\n# include \n#include\t"c.h"\n' + assert get_header_deps([("x.c", cfile)]) == [(False, "a.h"), (False, "c.h"), (True, "b.h")] + + def test_resolve_walks_transitively_through_headers(self) -> None: + # Reproduces the bug scenario: caller's .c only directly includes caller's own headers, but + # caller's __native_internal_caller.h includes the cross-group export header. The resolver + # must follow that chain so setuptools sees the cross-group header as a dep. + with tempfile.TemporaryDirectory() as tmp: + build_dir = tmp + os.makedirs(os.path.join(build_dir, "lib")) + os.makedirs(os.path.join(build_dir, "other_group")) + + # caller.c's directly-included headers, both live alongside + # caller.c under build/ (resolved via target_dir). + internal_h = os.path.join(build_dir, "__native_internal_caller.h") + caller_h = os.path.join(build_dir, "__native_caller.h") + cross_group_h = os.path.join(build_dir, "lib", "__native_functions.h") + unrelated_h = os.path.join(build_dir, "other_group", "__native_other.h") + + with open(caller_h, "w") as f: + # Headers outside build/ (CPython's , lib-rt's ) don't resolve under + # target_dir, so they get dropped during resolution and aren't recursed into. + f.write("#include \n#include \n") + with open(internal_h, "w") as f: + # This header includes a header in another group via angle brackets. Pre-fix, this dep + # was invisible to setuptools. + f.write( + "#include \n" + '#include "__native_caller.h"\n' + "#include \n" + ) + with open(cross_group_h, "w") as f: + f.write("struct export_table_lib___functions { int x; };\n") + with open(unrelated_h, "w") as f: + # Sibling group not reached from caller's chain => must NOT appear in the resolved set. + f.write("struct unrelated { int x; };\n") + + # caller.c is in build_dir, so its includer-dir is build_dir. Both directly-included headers + # are quoted (`False`); the cross-group header that __native_internal_caller.h reaches via + # `` is found by the recursive walk re-reading the on-disk header. + deps = resolve_cfile_deps( + cfile_dir=build_dir, + direct_includes=[ + (False, "__native_caller.h"), + (False, "__native_internal_caller.h"), + ], + target_dir=build_dir, + ) + + assert deps == {caller_h, internal_h, cross_group_h}, ( + f"expected the cross-group header to be reached transitively; " + f"got {sorted(deps)!r}" + ) + + def test_resolve_drops_unresolvable_includes(self) -> None: + # ``, ``, etc. don't live under target_dir, so they're dropped from depends. They + # never change between builds, so this is the right behavior. Crucially, it stops setuptools' + # `missing="newer"` from treating them as always-newer and force-rebuilding every translation unit. + with tempfile.TemporaryDirectory() as tmp: + cfile_dir = tmp + deps = resolve_cfile_deps( + cfile_dir=cfile_dir, + direct_includes=[(True, "Python.h"), (True, "CPy.h"), (False, "init.c")], + target_dir=cfile_dir, + ) + assert deps == set() + + def test_resolve_search_order_matches_preprocessor(self) -> None: + # When the same header name exists both next to the includer and under target_dir, the C preprocessor + # picks the includer-dir copy for `#include "shared.h"` and the target_dir copy for `#include `. + # The resolver must record the same path the compiler will actually consume, otherwise mtimes of the + # wrong file drive incremental rebuild decisions. + with tempfile.TemporaryDirectory() as tmp: + includer = os.path.join(tmp, "groupA") + target = os.path.join(tmp, "build") + os.makedirs(includer) + os.makedirs(target) + + local_h = os.path.join(includer, "shared.h") + global_h = os.path.join(target, "shared.h") + with open(local_h, "w") as f: + f.write("/* local */\n") + with open(global_h, "w") as f: + f.write("/* global */\n") + + # Quoted form: resolves to the includer-dir copy. + assert resolve_cfile_deps( + cfile_dir=includer, direct_includes=[(False, "shared.h")], target_dir=target + ) == {local_h} + + # Angled form: skips the includer-dir copy, resolves under -I. + assert resolve_cfile_deps( + cfile_dir=includer, direct_includes=[(True, "shared.h")], target_dir=target + ) == {global_h} diff --git a/mypyc/test/testutil.py b/mypyc/test/testutil.py index 0a558d0d0b8ec..9d59993c03402 100644 --- a/mypyc/test/testutil.py +++ b/mypyc/test/testutil.py @@ -232,8 +232,14 @@ def show_c(cfiles: list[list[tuple[str, str]]]) -> None: def fudge_dir_mtimes(dir: str, delta: int) -> None: + # Skip linker outputs. Pushing them back combines with write_file's + # +1 sec bump on .c files to make .c always newer than .so, forcing + # an unconditional rebuild that would mask Extension.depends bugs. + # See setuptools/_distutils/command/build_ext.py:`build_extension`. for dirpath, _, filenames in os.walk(dir): for name in filenames: + if name.endswith((".so", ".pyd", ".o", ".obj")): + continue path = os.path.join(dirpath, name) new_mtime = os.stat(path).st_mtime + delta os.utime(path, times=(new_mtime, new_mtime)) From 6f0e77b85910f0c64b6a1d5f080c944243cf8947 Mon Sep 17 00:00:00 2001 From: bzoracler <50305397+bzoracler@users.noreply.github.com> Date: Thu, 21 May 2026 10:31:25 +1000 Subject: [PATCH 038/127] Allow nativeparse to parse source code directly (#21260) This is the mypy counterpart of https://github.com/mypyc/ast_serialize/pull/54 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ivan Levkivskyi --- misc/dump-ast.py | 2 +- mypy/build.py | 146 +++++++++++++++------------------- mypy/checkstrformat.py | 1 - mypy/nativeparse.py | 13 ++- mypy/parse.py | 44 +++++----- mypy/stubgen.py | 8 +- mypy/test/test_nativeparse.py | 30 +++++-- mypy/test/testparse.py | 2 - 8 files changed, 123 insertions(+), 123 deletions(-) diff --git a/misc/dump-ast.py b/misc/dump-ast.py index 68ea8bc0dc61e..7fdf905bae0b4 100755 --- a/misc/dump-ast.py +++ b/misc/dump-ast.py @@ -19,7 +19,7 @@ def dump(fname: str, python_version: tuple[int, int], quiet: bool = False) -> No options.python_version = python_version with open(fname, "rb") as f: s = f.read() - tree = parse(s, fname, None, errors=Errors(options), options=options, file_exists=True) + tree = parse(s, fname, None, errors=Errors(options), options=options) if not quiet: print(tree) diff --git a/mypy/build.py b/mypy/build.py index 8d5db0bab8dfa..09e739f7fb991 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1024,85 +1024,76 @@ def parse_all(self, states: list[State], post_parse: bool = True) -> None: self.post_parse_all(states) return - sequential_states = [] parallel_states = [] for state in states: + if not self.fscache.exists(state.xpath, real_only=True): + state.source = state.get_source() if state.tree is not None: # The file was already parsed. - continue - if not self.fscache.exists(state.xpath, real_only=True): - # New parser only supports parsing on-disk files. - sequential_states.append(state) + state.needs_parse = False continue parallel_states.append(state) + if len(parallel_states) > 1: - self.parse_parallel(sequential_states, parallel_states) - else: - # Avoid using executor when there is no parallelism. - for state in states: - state.parse_file() - if post_parse: - self.post_parse_all(states) + # This duplicates a bit of logic from State.parse_file(). This is done as an + # optimization to parallelize only those parts of the code that can be + # parallelized efficiently. - def parse_parallel(self, sequential_states: list[State], parallel_states: list[State]) -> None: - """Perform parallel parsing of states. + parallel_parsed_states, parallel_parsed_states_set = self.parse_files_threaded_raw( + parallel_states + ) - Note: this duplicates a bit of logic from State.parse_file(). This is done - as an optimization to parallelize only those parts of the code that can be - parallelized efficiently. - """ - parallel_parsed_states, parallel_parsed_states_set = self.parse_files_threaded_raw( - sequential_states, parallel_states - ) + for state in parallel_parsed_states: + # New parser only returns serialized ASTs + with state.wrap_context(): + assert state.tree is not None + raw_data = state.tree.raw_data + if raw_data is not None: + # Apply inline mypy config before deserialization, since + # some options (e.g. implicit_optional) affect how the + # AST is built during deserialization. + state.source_hash = raw_data.source_hash + state.apply_inline_configuration(raw_data.mypy_comments) + state.tree = load_from_raw( + state.xpath, + state.id, + raw_data, + self.errors, + state.options, + imports_only=bool(self.workers), + ) + if self.errors.is_blockers(): + self.log("Bailing due to parse errors") + self.errors.raise_error() - for state in parallel_parsed_states: - # New parser returns serialized ASTs. Deserialize full trees only if not using - # parallel workers. - with state.wrap_context(): + for state in parallel_states: assert state.tree is not None - raw_data = state.tree.raw_data - if raw_data is not None: - # Apply inline mypy config before deserialization, since - # some options (e.g. implicit_optional) affect deserialization - state.source_hash = raw_data.source_hash - state.apply_inline_configuration(raw_data.mypy_comments) - state.tree = load_from_raw( - state.xpath, - state.id, - raw_data, - self.errors, - state.options, - imports_only=bool(self.workers), - ) - if self.errors.is_blockers(): - self.log("Bailing due to parse errors") - self.errors.raise_error() - - for state in parallel_states: - assert state.tree is not None - if state in parallel_parsed_states_set: + if state in parallel_parsed_states_set: + if state.tree.raw_data is not None: + # source_hash was already extracted above, but raw_data + # may have been preserved for workers (imports_only=True). + pass + elif state.source_hash is None: + # At least namespace packages may not have source. + state.get_source() + state.early_errors = list(self.errors.error_info_map.get(state.xpath, [])) + state.semantic_analysis_pass1() + self.ast_cache[state.id] = (state.tree, state.early_errors, state.source_hash) + self.modules[state.id] = state.tree if state.tree.raw_data is not None: - # source_hash was already extracted above, but raw_data - # may have been preserved for workers (imports_only=True). - pass - elif state.source_hash is None: - # At least namespace packages may not have source. - state.get_source() - state.early_errors = list(self.errors.error_info_map.get(state.xpath, [])) - state.semantic_analysis_pass1() - self.ast_cache[state.id] = (state.tree, state.early_errors, state.source_hash) - self.modules[state.id] = state.tree - if state.tree.raw_data is not None: - state.size_hint = len(state.tree.raw_data.defs) + MIN_SIZE_HINT - state.check_blockers() - state.setup_errors() - - def parse_files_threaded_raw( - self, sequential_states: list[State], parallel_states: list[State] - ) -> tuple[list[State], set[State]]: - """Parse files using a thread pool. - - Also parse sequential states while waiting for the parallel results. + state.size_hint = len(state.tree.raw_data.defs) + MIN_SIZE_HINT + state.check_blockers() + state.setup_errors() + elif len(parallel_states) == 1: + # Avoid using executor when there is no parallelism. + parallel_states[0].parse_file() + + if post_parse: + self.post_parse_all(states) + + def parse_files_threaded_raw(self, states: list[State]) -> tuple[list[State], set[State]]: + """Parse files in parallel using a thread pool. + Trees from the new parser are left in raw (serialized) form. Return (list, set) of states that were actually parsed (not cached). @@ -1118,14 +1109,14 @@ def parse_files_threaded_raw( # parse_file_inner() results in no visible improvement with more than 8 threads. # TODO: reuse thread pool and/or batch small files in single submit() call. with ThreadPoolExecutor(max_workers=min(available_threads, 8)) as executor: - for state in parallel_states: + for state in states: state.needs_parse = False if state.id not in self.ast_cache: self.log(f"Parsing {state.xpath} ({state.id})") ignore_errors = state.ignore_all or state.options.ignore_errors if ignore_errors: self.errors.ignored_files.add(state.xpath) - futures.append(executor.submit(state.parse_file_inner, "")) + futures.append(executor.submit(state.parse_file_inner, state.source)) parallel_parsed_states.append(state) parallel_parsed_states_set.add(state) else: @@ -1133,10 +1124,6 @@ def parse_files_threaded_raw( state.tree, state.early_errors, source_hash = self.ast_cache[state.id] state.source_hash = source_hash - # Parse sequential before waiting on parallel. - for state in sequential_states: - state.parse_file() - for fut in wait(futures).done: fut.result() @@ -1279,7 +1266,7 @@ def parse_file( self, id: str, path: str, - source: str, + source: str | None, options: Options, raw_data: FileRawData | None = None, ) -> MypyFile: @@ -1287,13 +1274,12 @@ def parse_file( Raise CompileError if there is a parse error. """ - file_exists = self.fscache.exists(path, real_only=True) t0 = time.time() if raw_data: # If possible, deserialize from known binary data instead of parsing from scratch. tree = load_from_raw(path, id, raw_data, self.errors, options) else: - tree = parse(source, path, id, self.errors, options=options, file_exists=file_exists) + tree = parse(source, path, id, self.errors, options=options) tree._fullname = id if self.stats_enabled: with self.stats_lock: @@ -3179,7 +3165,7 @@ def get_source(self) -> str: else: err = f"{self.path}: error: Cannot decode file: {str(decodeerr)}" raise CompileError([err], module_with_blocker=self.id) from decodeerr - elif self.path and self.manager.fscache.isdir(self.path): + elif self.path and manager.fscache.isdir(self.path): source = "" self.source_hash = "" else: @@ -3192,7 +3178,7 @@ def get_source(self) -> str: self.time_spent_us += time_spent_us(t0) return source - def parse_file_inner(self, source: str, raw_data: FileRawData | None = None) -> None: + def parse_file_inner(self, source: str | None, raw_data: FileRawData | None = None) -> None: t0 = time_ref() self.tree = self.manager.parse_file( self.id, self.xpath, source, options=self.options, raw_data=raw_data @@ -3319,9 +3305,7 @@ def semantic_analysis_pass1(self) -> None: # # TODO: This should not be considered as a semantic analysis # pass -- it's an independent pass. - if not options.native_parser or not self.manager.fscache.exists( - self.xpath, real_only=True - ): + if not options.native_parser: analyzer = SemanticAnalyzerPreAnalysis() with self.wrap_context(): analyzer.visit_file(self.tree, self.xpath, self.id, options) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index e96af007e29c9..aba49d71b77ec 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -587,7 +587,6 @@ def apply_field_accessors( module=None, options=self.chk.options, errors=temp_errors, - file_exists=False, eager=True, ) if temp_errors.is_errors(): diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index d048e9bce65e2..414426580fa73 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -182,7 +182,10 @@ def add_error( def native_parse( - filename: str, options: Options, skip_function_bodies: bool = False + filename: str, + options: Options, + source: str | bytes | None = None, + skip_function_bodies: bool = False, ) -> tuple[MypyFile, list[ParseError], TypeIgnores]: """Parse a Python file using the native Rust-based parser. @@ -211,7 +214,7 @@ def native_parse( uses_template_strings, source_hash, mypy_comments, - ) = parse_to_binary_ast(filename, options, skip_function_bodies) + ) = parse_to_binary_ast(filename, options, source, skip_function_bodies) node = MypyFile([], []) node.path = filename node.raw_data = FileRawData( @@ -248,7 +251,10 @@ def read_statements(state: State, data: ReadBuffer, n: int) -> list[Statement]: def parse_to_binary_ast( - filename: str, options: Options, skip_function_bodies: bool = False + filename: str, + options: Options, + source: str | bytes | None = None, + skip_function_bodies: bool = False, ) -> tuple[bytes, list[ParseError], TypeIgnores, bytes, bool, bool, str, list[tuple[int, str]]]: # This is a horrible hack to work around a mypyc bug where imported # module may be not ready in a thread sometimes. @@ -259,6 +265,7 @@ def parse_to_binary_ast( raise ImportError("Cannot import ast_serialize") ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse( filename, + source, skip_function_bodies=skip_function_bodies, python_version=options.python_version, platform=options.platform, diff --git a/mypy/parse.py b/mypy/parse.py index b0901a3a24552..a8fb5542a7049 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -12,12 +12,11 @@ def parse( - source: str | bytes, + source: str | bytes | None, fnam: str, module: str | None, errors: Errors, options: Options, - file_exists: bool, eager: bool = False, ) -> MypyFile: """Parse a source file, without doing any semantic analysis. @@ -27,28 +26,29 @@ def parse( New parser returns empty tree with serialized data. To get the full tree and the parse errors, use eager=True. + + `source` must not be `None` if the old parser is used. The new parser will read and + parse contents from path `fnam` if `source` is `None`. """ if options.native_parser: - # Native parser only works with actual files on disk - # Fall back to fastparse for in-memory source or non-existent files - if file_exists: - import mypy.nativeparse - - ignore_errors = options.ignore_errors or fnam in errors.ignored_files - # If errors are ignored, we can drop many function bodies to speed up type checking. - strip_function_bodies = ignore_errors and not options.preserve_asts - tree, _, _ = mypy.nativeparse.native_parse( - fnam, options, skip_function_bodies=strip_function_bodies - ) - # Set is_stub based on file extension - tree.is_stub = fnam.endswith(".pyi") - # Note: tree.imports is populated directly by load_from_raw() with deserialized - # import metadata, so we don't need to collect imports via AST traversal - if eager and tree.raw_data is not None: - tree = load_from_raw(fnam, module, tree.raw_data, errors, options) - return tree - # Fall through to fastparse for non-existent files - + import mypy.nativeparse + + ignore_errors = options.ignore_errors or fnam in errors.ignored_files + # If errors are ignored, we can drop many function bodies to speed up type checking. + strip_function_bodies = ignore_errors and not options.preserve_asts + tree, _, _ = mypy.nativeparse.native_parse( + fnam, options, source, skip_function_bodies=strip_function_bodies + ) + # Set is_stub based on file extension + tree.is_stub = fnam.endswith(".pyi") + # Note: tree.imports is populated directly by load_from_raw() with deserialized + # import metadata, so we don't need to collect imports via AST traversal + if eager and tree.raw_data is not None: + tree = load_from_raw(fnam, module, tree.raw_data, errors, options) + return tree + + if source is None: + raise ValueError("Source cannot be `None` when using the old parser") if options.transform_source is not None: source = options.transform_source(source) import mypy.fastparse diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 9c682ba4b8201..9b0089b6aec0f 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1745,13 +1745,7 @@ def parse_source_file(mod: StubSource, mypy_options: MypyOptions) -> None: source = mypy.util.decode_python_encoding(data) errors = Errors(mypy_options) mod.ast = mypy.parse.parse( - source, - fnam=mod.path, - module=mod.module, - errors=errors, - options=mypy_options, - file_exists=True, - eager=True, + source, fnam=mod.path, module=mod.module, errors=errors, options=mypy_options, eager=True ) mod.ast._fullname = mod.module if errors.is_blockers(): diff --git a/mypy/test/test_nativeparse.py b/mypy/test/test_nativeparse.py index b50da5f5d02c7..e0a0da29166b9 100644 --- a/mypy/test/test_nativeparse.py +++ b/mypy/test/test_nativeparse.py @@ -98,7 +98,7 @@ def test_parser(testcase: DataDrivenTestCase) -> None: try: with temp_source(source) as fnam: - node, errors, type_ignores = native_parse(fnam, options, skip_function_bodies) + node, errors, type_ignores = native_parse(fnam, options, None, skip_function_bodies) errors += load_tree(node, options) node.path = "main" a = node.str_with_options(options).split("\n") @@ -234,7 +234,7 @@ def format_reachable_imports(node: MypyFile) -> list[str]: @unittest.skipUnless(has_nativeparse, "nativeparse not available") class TestNativeParserBinaryFormat(unittest.TestCase): - def test_trivial_binary_data(self) -> None: + def _assert_trivial_binary_data(self, b: bytes, /) -> None: # A quick sanity check to ensure the serialized data looks as expected. Only covers # a few AST nodes. @@ -250,9 +250,9 @@ def locs(start_line: int, start_column: int, end_line: int, end_column: int) -> int_enc(end_column - start_column), ] - with temp_source("print('hello')") as fnam: - b, _, _, _, _, _, _, _ = parse_to_binary_ast(fnam, Options()) - assert list(b) == ( + self.assertEqual( + list(b), + ( [LITERAL_INT, 22, nodes.EXPR_STMT, nodes.CALL_EXPR] + [nodes.NAME_EXPR, LITERAL_STR] + [int_enc(5)] @@ -269,7 +269,25 @@ def locs(start_line: int, start_column: int, end_line: int, end_column: int) -> + [LIST_GEN, 22, LITERAL_NONE] + locs(1, 0, 1, 14) + [END_TAG, END_TAG] - ) + ), + ) + + def test_trivial_binary_data_from_file(self) -> None: + with temp_source("print('hello')") as fnam: + b, _, _, _, _, _, _, _ = parse_to_binary_ast(fnam, Options()) + self._assert_trivial_binary_data(b) + + def test_trivial_binary_data_from_string_source(self) -> None: + b, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), "print('hello')") + self._assert_trivial_binary_data(b) + + def test_trivial_binary_data_from_bytes_source(self) -> None: + b, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), b"print('hello')") + self._assert_trivial_binary_data(b) + + def test_invalid_bytes_raises(self) -> None: + with self.assertRaises(UnicodeDecodeError): + parse_to_binary_ast("", Options(), b"\xff") @contextlib.contextmanager diff --git a/mypy/test/testparse.py b/mypy/test/testparse.py index 6d00f5b5710f9..8f4de5bc7412b 100644 --- a/mypy/test/testparse.py +++ b/mypy/test/testparse.py @@ -66,7 +66,6 @@ def test_parser(testcase: DataDrivenTestCase) -> None: module="__main__", errors=errors, options=options, - file_exists=False, eager=True, ) if errors.is_errors(): @@ -108,7 +107,6 @@ def test_parse_error(testcase: DataDrivenTestCase) -> None: "__main__", errors=errors, options=options, - file_exists=False, eager=True, ) if errors.is_errors(): From cde4779e2347e7fc665717d288591b03138467fd Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 21 May 2026 18:59:17 +0300 Subject: [PATCH 039/127] [mypyc] Fix non-deterministic class struct layout under `separate=True` (#21530) The helper function `detect_undefined_bitmap` builds the list of attributes that need a per-instance "is set?" bit (`cl.bitmap_attrs`). It walks from a subclass up into its base and `.append()`s entries. The walk dedupes within one call via `seen`, but the function is called once per SCC; Under `separate=True`, every subclass of a shared base lives in its own SCC, so the base is visited multiple times and gets the same entries re-appended on every pass. After N visits, `base.bitmap_attrs` contains N duplicate copies of the same names, so attribute offsets shift between builds and not-rebuilt subclasses end up reading the wrong bytes. The added test case shows that on master branch `base.bitmap_attrs` has been populated with `["i"] * 11` The fix: Build a fresh local list and assign once at the end. The function becomes idempotent and the struct layout remains identical after each incremental build. --- mypyc/analysis/attrdefined.py | 14 ++++++++++---- mypyc/test/test_emitclass.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/mypyc/analysis/attrdefined.py b/mypyc/analysis/attrdefined.py index 1dfd33630f1c0..bfb9e7652a4f8 100644 --- a/mypyc/analysis/attrdefined.py +++ b/mypyc/analysis/attrdefined.py @@ -424,14 +424,20 @@ def detect_undefined_bitmap(cl: ClassIR, seen: set[ClassIR]) -> None: for base in cl.base_mro[1:]: detect_undefined_bitmap(base, seen) + # Build fresh and assign once. This function is called per SCC and `seen` + # only dedupes within a single call, so appending in place to a shared base + # would accumulate duplicates across SCCs and produce non-deterministic + # struct layouts under separate=True. + new_attrs: list[str] = [] if len(cl.base_mro) > 1: - cl.bitmap_attrs.extend(cl.base_mro[1].bitmap_attrs) + new_attrs.extend(cl.base_mro[1].bitmap_attrs) for n, t in cl.attributes.items(): if t.error_overlap and not cl.is_always_defined(n): - cl.bitmap_attrs.append(n) + new_attrs.append(n) for base in cl.mro[1:]: if base.is_trait: for n, t in base.attributes.items(): - if t.error_overlap and not cl.is_always_defined(n) and n not in cl.bitmap_attrs: - cl.bitmap_attrs.append(n) + if t.error_overlap and not cl.is_always_defined(n) and n not in new_attrs: + new_attrs.append(n) + cl.bitmap_attrs = new_attrs diff --git a/mypyc/test/test_emitclass.py b/mypyc/test/test_emitclass.py index eb04b22495de6..9c3cd02d1100c 100644 --- a/mypyc/test/test_emitclass.py +++ b/mypyc/test/test_emitclass.py @@ -2,8 +2,10 @@ import unittest +from mypyc.analysis.attrdefined import detect_undefined_bitmap from mypyc.codegen.emitclass import getter_name, setter_name, slot_key from mypyc.ir.class_ir import ClassIR +from mypyc.ir.rtypes import int32_rprimitive from mypyc.namegen import NameGenerator @@ -33,3 +35,22 @@ def test_getter_name(self) -> None: generator = NameGenerator([["mod"]]) assert getter_name(cls, "down", generator) == "testing___SomeClass_get_down" + + def test_bitmap_attrs_stable_across_repeat_analysis(self) -> None: + # Regression: detect_undefined_bitmap used to mutate cl.bitmap_attrs + # in place, so under separate=True (one SCC per group) a shared base + # class would accumulate duplicate entries as each subclass's SCC + # walked into it, growing the emitted struct between builds. + base = ClassIR("Base", "mod") + base.attributes = {"i": int32_rprimitive} + sub = ClassIR("Sub", "mod") + sub.attributes = {"j": int32_rprimitive} + base.mro = base.base_mro = [base] + sub.mro = sub.base_mro = [sub, base] + base.children = [sub] + + detect_undefined_bitmap(sub, seen=set()) + for _ in range(10): + detect_undefined_bitmap(sub, seen=set()) + assert base.bitmap_attrs == ["i"] + assert sub.bitmap_attrs == ["i", "j"] From 55ee0bd397e22361aae5fa6d8feb1f2bc140ba5e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 21 May 2026 17:55:48 +0100 Subject: [PATCH 040/127] Fix type variable defaults depending on previous variables (#21526) Fixes https://github.com/python/mypy/issues/19192 Fixes https://github.com/python/mypy/issues/20027 Fixes few TODOs in tests (don't know if there are relevant issues) Closes https://github.com/python/mypy/pull/19382 Core idea is simple: don't do bizarre things :-) More precisely, instead of various in-place modifications (which is a big no-no for types), simply expand defaults iteratively as we build the environment for the type. Couple additional things: * Update defaults in `freshen_function_type_vars()` this is required for consistency when updating `TypeVarId`s. * Use `fix_instance()` in `checkexpr.py` for type applications. This will apply _all_ type arguments in cases like `x = Foo[int](...)`. The PEP and the spec say this is how it should be. --- mypy/applytype.py | 12 +++- mypy/checkexpr.py | 9 ++- mypy/expandtype.py | 14 +---- mypy/semanal.py | 8 ++- mypy/tvar_scope.py | 68 +--------------------- mypy/typeanal.py | 28 ++++----- test-data/unit/check-python313.test | 34 ++++++++++- test-data/unit/check-typevar-defaults.test | 32 +++++++--- 8 files changed, 94 insertions(+), 111 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index 731200a06b651..cde97a4e712fc 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -37,10 +37,12 @@ def get_target_type( report_incompatible_typevar_value: Callable[[CallableType, Type, str, Context], None], context: Context, skip_unsatisfied: bool, + id_to_type: dict[TypeVarId, Type], ) -> Type | None: p_type = get_proper_type(type) if isinstance(p_type, UninhabitedType) and p_type.ambiguous and tvar.has_default(): - return tvar.default + # Gradually expand defaults, as they may depend on previous type variables. + return expand_type(tvar.default, id_to_type) if isinstance(tvar, ParamSpecType): return type if isinstance(tvar, TypeVarTupleType): @@ -113,7 +115,13 @@ def apply_generic_arguments( continue target_type = get_target_type( - tvar, type, callable, report_incompatible_typevar_value, context, skip_unsatisfied + tvar, + type, + callable, + report_incompatible_typevar_value, + context, + skip_unsatisfied, + id_to_type, ) if target_type is not None: id_to_type[tvar.id] = target_type diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index dd914498df87d..5f1aeac8a7255 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5079,11 +5079,7 @@ class C(Generic[T, Unpack[Ts]]): ... return [AnyType(TypeOfAny.from_error)] * len(vars) # TODO: in future we may want to support type application to variadic functions. - if ( - not vars - or not any(isinstance(v, TypeVarTupleType) for v in vars) - or not t.is_type_obj() - ): + if not vars or not t.is_type_obj() or t.type_object().fullname == "builtins.tuple": return list(args) info = t.type_object() # We reuse the logic from semanal phase to reduce code duplication. @@ -5097,6 +5093,9 @@ class C(Generic[T, Unpack[Ts]]): ... ) args = list(fake.args) + if not any(isinstance(v, TypeVarTupleType) for v in vars): + return args + prefix = next(i for (i, v) in enumerate(vars) if isinstance(v, TypeVarTupleType)) suffix = len(vars) - prefix - 1 tvt = vars[prefix] diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 5790b717172ac..33c6c6d4ae717 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -129,6 +129,9 @@ def freshen_function_type_vars(callee: F) -> F: tv = v.new_unification_variable(v) tvs.append(tv) tvmap[v.id] = tv + if tv.has_default(): + # Point to fresh ids in case defaults depend on previous variables. + tv.default = expand_type(tv.default, tvmap) fresh = expand_type(callee, tvmap).copy_modified(variables=tvs) return cast(F, fresh) else: @@ -182,7 +185,6 @@ class ExpandTypeVisitor(TrivialSyntheticTypeTranslator): def __init__(self, variables: Mapping[TypeVarId, Type]) -> None: super().__init__() self.variables = variables - self.recursive_tvar_guard: dict[TypeVarId, Type | None] | None = None def visit_unbound_type(self, t: UnboundType) -> Type: return t @@ -245,16 +247,6 @@ def visit_type_var(self, t: TypeVarType) -> Type: # TODO: do we really need to do this? # If I try to remove this special-casing ~40 tests fail on reveal_type(). return repl.copy_modified(last_known_value=None) - if isinstance(repl, TypeVarType) and repl.has_default(): - if self.recursive_tvar_guard is None: - self.recursive_tvar_guard = {} - if (tvar_id := repl.id) in self.recursive_tvar_guard: - return self.recursive_tvar_guard[tvar_id] or repl - self.recursive_tvar_guard[tvar_id] = None - repl.default = repl.default.accept(self) - expanded = repl.accept(self) # Note: `expanded is repl` may be true. - repl = repl if isinstance(expanded, TypeVarType) else expanded - self.recursive_tvar_guard[tvar_id] = repl return repl def visit_param_spec(self, t: ParamSpecType) -> Type: diff --git a/mypy/semanal.py b/mypy/semanal.py index da58c95869667..b30958ec7c719 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2466,9 +2466,6 @@ def tvar_defs_from_tvars( tvar_defs: list[TypeVarLikeType] = [] last_tvar_name_with_default: str | None = None for name, tvar_expr in tvars: - tvar_expr.default = tvar_expr.default.accept( - TypeVarDefaultTranslator(self, tvar_expr.name, context) - ) # PEP-695 type variables that are redeclared in an inner scope are warned # about elsewhere. if not tvar_expr.is_new_style and not self.tvar_scope.allow_binding( @@ -2478,6 +2475,11 @@ def tvar_defs_from_tvars( message_registry.TYPE_VAR_REDECLARED_IN_NESTED_CLASS.format(name), context ) tvar_def = self.tvar_scope.bind_new(name, tvar_expr, self.fail, context) + # Fix any residual UnboundTypes in the generated TypeVarLike, keep + # TypeVarLikeExpr untouched as it may be shared by multiple classes. + tvar_def.default = tvar_def.default.accept( + TypeVarDefaultTranslator(self, tvar_expr.name, context) + ) if last_tvar_name_with_default is not None and not tvar_def.has_default(): self.msg.tvar_without_default_type( tvar_def.name, last_tvar_name_with_default, context diff --git a/mypy/tvar_scope.py b/mypy/tvar_scope.py index e65f9f5ee3a42..1ff93fd891ef9 100644 --- a/mypy/tvar_scope.py +++ b/mypy/tvar_scope.py @@ -12,13 +12,8 @@ TypeVarTupleExpr, ) from mypy.types import ( - AnyType, ParamSpecFlavor, ParamSpecType, - TrivialSyntheticTypeTranslator, - Type, - TypeAliasType, - TypeOfAny, TypeVarId, TypeVarLikeType, TypeVarTupleType, @@ -28,54 +23,6 @@ FailFunc: _TypeAlias = Callable[[str, Context], None] -class TypeVarLikeDefaultFixer(TrivialSyntheticTypeTranslator): - """Set namespace for all TypeVarLikeTypes types.""" - - def __init__( - self, - scope: TypeVarLikeScope, - fail_func: FailFunc, - source_tv: TypeVarLikeExpr, - context: Context, - ) -> None: - self.scope = scope - self.fail_func = fail_func - self.source_tv = source_tv - self.context = context - super().__init__() - - def visit_type_var(self, t: TypeVarType) -> Type: - existing = self.scope.get_binding(t.fullname) - if existing is None: - self._report_unbound_tvar(t) - return AnyType(TypeOfAny.from_error) - return existing - - def visit_param_spec(self, t: ParamSpecType) -> Type: - existing = self.scope.get_binding(t.fullname) - if existing is None: - self._report_unbound_tvar(t) - return AnyType(TypeOfAny.from_error) - return existing - - def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: - existing = self.scope.get_binding(t.fullname) - if existing is None: - self._report_unbound_tvar(t) - return AnyType(TypeOfAny.from_error) - return existing - - def visit_type_alias_type(self, t: TypeAliasType) -> Type: - return t - - def _report_unbound_tvar(self, tvar: TypeVarLikeType) -> None: - self.fail_func( - f"Type variable {tvar.name} referenced in the default" - f" of {self.source_tv.name} is unbound", - self.context, - ) - - class TypeVarLikeScope: """Scope that holds bindings for type variables and parameter specifications. @@ -148,15 +95,6 @@ def bind_new( i = self.func_id namespace = self.namespace - # Defaults may reference other type variables. That is only valid when the - # referenced variable is already in scope (textually precedes the definition we're - # processing now). - default = tvar_expr.default.accept( - TypeVarLikeDefaultFixer( - self, fail_func=fail_func, source_tv=tvar_expr, context=context - ) - ) - if isinstance(tvar_expr, TypeVarExpr): tvar_def: TypeVarLikeType = TypeVarType( name=name, @@ -164,7 +102,7 @@ def bind_new( id=TypeVarId(i, namespace=namespace), values=tvar_expr.values, upper_bound=tvar_expr.upper_bound, - default=default, + default=tvar_expr.default, variance=tvar_expr.variance, line=tvar_expr.line, column=tvar_expr.column, @@ -176,7 +114,7 @@ def bind_new( id=TypeVarId(i, namespace=namespace), flavor=ParamSpecFlavor.BARE, upper_bound=tvar_expr.upper_bound, - default=default, + default=tvar_expr.default, line=tvar_expr.line, column=tvar_expr.column, ) @@ -187,7 +125,7 @@ def bind_new( id=TypeVarId(i, namespace=namespace), upper_bound=tvar_expr.upper_bound, tuple_fallback=tvar_expr.tuple_fallback, - default=default, + default=tvar_expr.default, line=tvar_expr.line, column=tvar_expr.column, ) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0c0399d91ecca..02b96afa8c170 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -2098,15 +2098,16 @@ def fix_instance( disallow_any, fail, note, t, options, fullname, unexpanded_type ) arg = any_type + with state.strict_optional_set(options.strict_optional): + # Gradually expand defaults, as they may depend on previous variables. + if tv.has_default(): + arg = expand_type(arg, env) + env[tv.id] = arg args.append(arg) - env[tv.id] = arg + else: + env[tv.id] = arg t.args = tuple(args) fix_type_var_tuple_argument(t) - if not t.type.has_type_var_tuple_type: - with state.strict_optional_set(True): - fixed = expand_type(t, env) - assert isinstance(fixed, Instance) - t.args = fixed.args def instantiate_type_alias( @@ -2288,7 +2289,6 @@ def set_any_tvars( env: dict[TypeVarId, Type] = {} used_any_type = False - has_type_var_tuple_type = False for tv, arg in itertools.zip_longest(node.alias_tvars, args, fillvalue=None): if tv is None: continue @@ -2300,16 +2300,16 @@ def set_any_tvars( used_any_type = True if isinstance(tv, TypeVarTupleType): # TODO Handle TypeVarTuple defaults - has_type_var_tuple_type = True arg = UnpackType(Instance(tv.tuple_fallback.type, [any_type])) + with state.strict_optional_set(options.strict_optional): + # Gradually expand defaults, as they may depend on previous variables. + if tv.has_default(): + arg = expand_type(arg, env) + env[tv.id] = arg args.append(arg) - env[tv.id] = arg + else: + env[tv.id] = arg t = TypeAliasType(node, args, newline, newcolumn) - if not has_type_var_tuple_type: - with state.strict_optional_set(options.strict_optional): - fixed = expand_type(t, env) - assert isinstance(fixed, TypeAliasType) - t.args = fixed.args if used_any_type and disallow_any and node.alias_tvars: assert fail is not None diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 8a80977fb22a9..9f9122b2dfb1b 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -291,7 +291,7 @@ reveal_type(A2().x) # N: Revealed type is "builtins.int" reveal_type(A3().x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeVarDefaultToAnotherTypeVar] +[case testTypeVarDefaultToAnotherTypeVarClass] class A[X, Y = X, Z = Y]: x: X y: Y @@ -300,8 +300,7 @@ class A[X, Y = X, Z = Y]: a1: A[int] reveal_type(a1.x) # N: Revealed type is "builtins.int" reveal_type(a1.y) # N: Revealed type is "builtins.int" -# TODO: this must reveal `int` as well: -reveal_type(a1.z) # N: Revealed type is "X`1" +reveal_type(a1.z) # N: Revealed type is "builtins.int" a2: A[int, str] reveal_type(a2.x) # N: Revealed type is "builtins.int" @@ -314,6 +313,35 @@ reveal_type(a3.y) # N: Revealed type is "builtins.str" reveal_type(a3.z) # N: Revealed type is "builtins.bool" [builtins fixtures/tuple.pyi] +[case testTypeVarDefaultToAnotherTypeVarAlias] +type A[X, Y = X, Z = Y] = tuple[X, Y, Z] + +a1: A[int] +reveal_type(a1[0]) # N: Revealed type is "builtins.int" +reveal_type(a1[1]) # N: Revealed type is "builtins.int" +reveal_type(a1[2]) # N: Revealed type is "builtins.int" + +a2: A[int, str] +reveal_type(a2[0]) # N: Revealed type is "builtins.int" +reveal_type(a2[1]) # N: Revealed type is "builtins.str" +reveal_type(a2[2]) # N: Revealed type is "builtins.str" + +a3: A[int, str, bool] +reveal_type(a3[0]) # N: Revealed type is "builtins.int" +reveal_type(a3[1]) # N: Revealed type is "builtins.str" +reveal_type(a3[2]) # N: Revealed type is "builtins.bool" +[builtins fixtures/tuple.pyi] + +[case testTypeVarDefaultAssertTypeType] +from typing import assert_type + +class NoNonDefaults[T = str, S = int]: ... + +assert_type(NoNonDefaults[()], type[NoNonDefaults[str, int]]) +assert_type(NoNonDefaults[str], type[NoNonDefaults[str, int]]) +assert_type(NoNonDefaults[str, int], type[NoNonDefaults[str, int]]) +[builtins fixtures/tuple.pyi] + [case testTypeVarDefaultToAnotherTypeVarWrong] class A[Y = X, X = int]: ... # E: Name "X" is not defined diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 7987178d1fe02..bde73ab533510 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -137,8 +137,7 @@ def func_error_alias1( reveal_type(b) # N: Revealed type is "builtins.dict[builtins.int, Any]" reveal_type(c) # N: Revealed type is "builtins.dict[builtins.int, builtins.float]" -TERR2 = Dict[T4, T3] # TODO should be an error \ - # Type parameter "T4" has a default type that refers to one or more type variables that are out of scope +TERR2 = Dict[T4, T3] # E: Type parameter "T4" has a default type that refers to one or more type variables that are out of scope def func_error_alias2( a: TERR2, @@ -518,8 +517,8 @@ def func_d3( reveal_type(b) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.list[builtins.int]]" reveal_type(c) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.float]" - # k = ClassD3() - # reveal_type(k) # Revealed type is "__main__.ClassD3[builtins.str, builtins.list[builtins.str]]" # TODO + k = ClassD3() + reveal_type(k) # N: Revealed type is "__main__.ClassD3[builtins.str, builtins.list[builtins.str]]" l = ClassD3[int]() reveal_type(l) # N: Revealed type is "__main__.ClassD3[builtins.int, builtins.list[builtins.int]]" m = ClassD3[int, float]() @@ -904,6 +903,19 @@ Alias: TypeAlias = "MyClass[T1, T2]" class MyClass(Generic["T1", "T2"]): ... [builtins fixtures/tuple.pyi] +[case testTypeVariableDefaultNotSticky] +from typing import Generic, TypeVar + +T1 = TypeVar("T1") +T2 = TypeVar("T2", default=T1) + +class MyClass(Generic[T1, T2]): ... + +reveal_type(MyClass[int]) # N: Revealed type is "def () -> __main__.MyClass[builtins.int, builtins.int]" +reveal_type(MyClass[str]) # N: Revealed type is "def () -> __main__.MyClass[builtins.str, builtins.str]" +reveal_type(MyClass[bytes]) # N: Revealed type is "def () -> __main__.MyClass[builtins.bytes, builtins.bytes]" +[builtins fixtures/tuple.pyi] + [case testDefaultsApplicationInAliasNoCrashNested] from typing import Generic, TypeVar from typing_extensions import TypeAlias @@ -928,19 +940,23 @@ T3 = TypeVar("T3", default=T2) class A(Generic[T1, T2, T3]): ... reveal_type(A) # N: Revealed type is "def [T1, T2 = T1`1, T3 = T2`2 = T1`1] () -> __main__.A[T1`1, T2`2 = T1`1, T3`3 = T2`2 = T1`1]" a: A[int] -reveal_type(a) # N: Revealed type is "__main__.A[builtins.int, builtins.int, T1`1]" +reveal_type(a) # N: Revealed type is "__main__.A[builtins.int, builtins.int, builtins.int]" + +AA = tuple[T1, T2, T3] +aa: AA[str] +reveal_type(aa) # N: Revealed type is "tuple[builtins.str, builtins.str, builtins.str]" -class B(Generic[T1, T3]): ... # E: Type variable T2 referenced in the default of T3 is unbound +class B(Generic[T1, T3]): ... # E: Type parameter "T3" has a default type that refers to one or more type variables that are out of scope reveal_type(B) # N: Revealed type is "def [T1, T3 = Any] () -> __main__.B[T1`1, T3`2 = Any]" b: B[int] reveal_type(b) # N: Revealed type is "__main__.B[builtins.int, Any]" -class C(Generic[T2]): ... # E: Type variable T1 referenced in the default of T2 is unbound +class C(Generic[T2]): ... # E: Type parameter "T2" has a default type that refers to one or more type variables that are out of scope reveal_type(C) # N: Revealed type is "def [T2 = Any] () -> __main__.C[T2`1 = Any]" c: C reveal_type(c) # N: Revealed type is "__main__.C[Any]" -class D(Generic[T2, T1]): ... # E: Type variable T1 referenced in the default of T2 is unbound \ +class D(Generic[T2, T1]): ... # E: Type parameter "T2" has a default type that refers to one or more type variables that are out of scope \ # E: "T1" cannot appear after "T2" in type parameter list because it has no default type reveal_type(D) # N: Revealed type is "def [T2 = Any, T1 = Any] () -> __main__.D[T2`1 = Any, T1`2 = Any]" d: D From e61adeaa67d0cdf4b177b835aa17a57de8e1f18e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 22 May 2026 12:54:44 +0300 Subject: [PATCH 041/127] [mypyc] Add test for incremental builtin_base class construction across groups (#21524) The fix for this was included in #21369, but no dedicated test was added. This adds `testIncrementalBuiltinBaseClassConstruction` to `run-multimodule.test`: three modules compiled with `separate=True`, where step 2 changes a helper module's signature to force the caller to be recompiled while the exception module is only loaded from cache. --- mypyc/test-data/run-multimodule.test | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index 92b9fa6623fcc..6ae6c0f2cab9b 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1734,3 +1734,47 @@ class Base: from native import make_child assert make_child(7) == "child(7)" assert make_child(-1) == "child(-1)" + +[case testIncrementalBuiltinBaseClassConstruction] +# Regression: builtin_base classes (Exception subclasses) were unconditionally +# added to func_to_decl in load_type_map, causing cross-group call sites to +# emit CPyDef instead of CPyType for the constructor. +from other_errors import MyError +from other_util import process + +def run(value: str) -> None: + if not value: + raise MyError("empty") + +def compute(x: str) -> str: + result = process(x) + if not result: + raise MyError("no result") + return result + +[file other_errors.py] +class MyError(Exception): + pass + +[file other_util.py] +def process(x: str) -> str: + return x + +[file other_util.py.2] +def process(x: str, flag: bool = False) -> str: + return x.strip() + +[file driver.py] +from native import run, compute +try: + run("") +except Exception as e: + print(str(e)) +print(compute("hello")) + +[out] +empty +hello +[out2] +empty +hello From 21e2859b7f7956f8587a172e517ce7b265edb563 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 22 May 2026 12:20:59 +0100 Subject: [PATCH 042/127] Support protocol checks for self-types in tuple types (#21535) Fixes https://github.com/python/mypy/issues/21528 This fixes a regression caused by a typeshed PR exposing this missing feature. The implementation is somewhat non-trivial, but I don't see a simpler way to do it in a robust manner. --- mypy/constraints.py | 21 ++++++++++++++++++-- mypy/messages.py | 23 ++++++++++++++++------ mypy/subtypes.py | 27 +++++++++++++++++++------- test-data/unit/check-protocols.test | 30 +++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index a58af222b1ea8..d20acccd09a80 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -797,7 +797,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: if isinstance(actual, Instance): instance = actual erased = erase_typevars(template) - assert isinstance(erased, Instance) # type: ignore[misc] + assert isinstance(erased, ProperType) and isinstance(erased, Instance) # We always try nominal inference if possible, # it is much faster than the structural one. if self.direction == SUBTYPE_OF and template.type.has_base(instance.type.fullname): @@ -996,7 +996,24 @@ def visit_instance(self, template: Instance) -> list[Constraint]: res.extend(cb) return res elif isinstance(actual, TupleType) and self.direction == SUPERTYPE_OF: - return infer_constraints(template, mypy.typeops.tuple_fallback(actual), self.direction) + instance = mypy.typeops.tuple_fallback(actual) + erased = erase_typevars(template) + assert isinstance(erased, ProperType) and isinstance(erased, Instance) + # Special-case protocols before using fallback to get more precise constraints + # for custom tuple types like NamedTuples. + if ( + template.type.is_protocol + and self.direction == SUPERTYPE_OF + and not any(template == t for t in reversed(template.type.inferring)) + and mypy.subtypes.is_protocol_implementation(instance, erased, skip=["__call__"]) + ): + template.type.inferring.append(template) + res = self.infer_constraints_from_protocol_members( + instance, template, original_actual, template + ) + template.type.inferring.pop() + return res + return infer_constraints(template, instance, self.direction) elif isinstance(actual, TypeVarType): if not actual.values and not actual.id.is_meta_var(): return infer_constraints(template, actual.upper_bound, self.direction) diff --git a/mypy/messages.py b/mypy/messages.py index 3de66c7c6082c..1a6abcc853fef 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2220,8 +2220,9 @@ def report_protocol_problems( class_obj = False is_module = False skip = [] + original_subtype = subtype if isinstance(subtype, TupleType): - subtype = subtype.partial_fallback + subtype = mypy.typeops.tuple_fallback(subtype) elif isinstance(subtype, TypedDictType): subtype = subtype.fallback elif isinstance(subtype, TypeType): @@ -2233,7 +2234,7 @@ def report_protocol_problems( if subtype.is_type_obj(): ret_type = get_proper_type(subtype.ret_type) if isinstance(ret_type, TupleType): - ret_type = ret_type.partial_fallback + ret_type = mypy.typeops.tuple_fallback(ret_type) if not isinstance(ret_type, Instance): return class_obj = True @@ -2243,6 +2244,10 @@ def report_protocol_problems( skip = ["__call__"] if subtype.extra_attrs and subtype.extra_attrs.mod_name: is_module = True + if not isinstance(original_subtype, TupleType): + # Apart from instances, only tuples are supported by + # is_protocol_implementation() for now. + original_subtype = subtype # Report missing members missing = get_missing_protocol_members(subtype, supertype, skip=skip) @@ -2274,7 +2279,7 @@ def report_protocol_problems( # Report member type conflicts conflict_types = get_conflict_protocol_types( - subtype, supertype, class_obj=class_obj, options=self.options + subtype, original_subtype, supertype, class_obj=class_obj, options=self.options ) if conflict_types and ( not is_subtype(subtype, erase_type(supertype), options=self.options) @@ -3191,7 +3196,11 @@ def get_missing_protocol_members(left: Instance, right: Instance, skip: list[str def get_conflict_protocol_types( - left: Instance, right: Instance, class_obj: bool = False, options: Options | None = None + left: Instance, + original_left: Type, + right: Instance, + class_obj: bool = False, + options: Options | None = None, ) -> list[tuple[str, Type, Type, bool]]: """Find members that are defined in 'left' but have incompatible types. Return them as a list of ('member', 'got', 'expected', 'is_lvalue'). @@ -3203,7 +3212,7 @@ def get_conflict_protocol_types( continue supertype = find_member(member, right, left) assert supertype is not None - subtype = get_protocol_member(left, member, class_obj) + subtype = get_protocol_member(left, original_left, member, class_obj) if not subtype: continue is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True, options=options) @@ -3219,7 +3228,9 @@ def get_conflict_protocol_types( different_setter = True supertype = set_supertype if IS_EXPLICIT_SETTER in get_member_flags(member, left): - set_subtype = get_protocol_member(left, member, class_obj, is_lvalue=True) + set_subtype = get_protocol_member( + left, original_left, member, class_obj, is_lvalue=True + ) if set_subtype and not is_same_type(set_subtype, subtype): different_setter = True subtype = set_subtype diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 5733797326e88..2e3ded8460b3e 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -811,6 +811,12 @@ def visit_tuple_type(self, left: TupleType) -> bool: mypy.typeops.tuple_fallback(left), right ): return True + elif right.type.is_protocol and is_protocol_implementation( + left, right, proper_subtype=self.proper_subtype + ): + # Special-case protocols to get precise binding of self type for + # custom tuple types like NamedTuples. + return True return False elif isinstance(right, TupleType): # If right has a variadic unpack this needs special handling. If there is a TypeVarTuple @@ -1185,7 +1191,7 @@ def pop_on_exit(stack: list[tuple[T, T]], left: T, right: T) -> Iterator[None]: def is_protocol_implementation( - left: Instance, + left: Instance | TupleType, right: Instance, proper_subtype: bool = False, class_obj: bool = False, @@ -1212,6 +1218,11 @@ def f(self) -> A: ... assert right.type.is_protocol if skip is None: skip = [] + # Preserve original left type for precise self-type binding. Only tuple types are + # supported for now. + original_left = left + if isinstance(left, TupleType): + left = mypy.typeops.tuple_fallback(left) # We need to record this check to generate protocol fine-grained dependencies. type_state.record_protocol_subtype_check(left.type, right.type) # nominal subtyping currently ignores '__init__' and '__new__' signatures @@ -1234,10 +1245,10 @@ def f(self) -> A: ... ignore_names = member != "__call__" # __call__ can be passed kwargs # The third argument below indicates to what self type is bound. # We always bind self to the subtype. (Similarly to nominal types). - supertype = find_member(member, right, left) + supertype = find_member(member, right, original_left) assert supertype is not None - subtype = get_protocol_member(left, member, class_obj) + subtype = get_protocol_member(left, original_left, member, class_obj) # Useful for debugging: # print(member, 'of', left, 'has type', subtype) # print(member, 'of', right, 'has type', supertype) @@ -1264,9 +1275,11 @@ def f(self) -> A: ... if IS_SETTABLE in superflags: # Check opposite direction for settable attributes. if IS_EXPLICIT_SETTER in superflags: - supertype = find_member(member, right, left, is_lvalue=True) + supertype = find_member(member, right, original_left, is_lvalue=True) if IS_EXPLICIT_SETTER in subflags: - subtype = get_protocol_member(left, member, class_obj, is_lvalue=True) + subtype = get_protocol_member( + left, original_left, member, class_obj, is_lvalue=True + ) # At this point we know attribute is present on subtype, otherwise we # would return False above. assert supertype is not None and subtype is not None @@ -1305,7 +1318,7 @@ def f(self) -> A: ... def get_protocol_member( - left: Instance, member: str, class_obj: bool, is_lvalue: bool = False + left: Instance, original_left: Type, member: str, class_obj: bool, is_lvalue: bool = False ) -> Type | None: if member == "__call__" and class_obj: # Special case: class objects always have __call__ that is just the constructor. @@ -1316,7 +1329,7 @@ def get_protocol_member( # if constructor signature didn't match, this can cause many false negatives. return None - subtype = find_member(member, left, left, class_obj=class_obj, is_lvalue=is_lvalue) + subtype = find_member(member, left, original_left, class_obj=class_obj, is_lvalue=is_lvalue) if isinstance(subtype, PartialType): subtype = ( NoneType() diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 87fd30e437170..6e86507eb48d7 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4779,3 +4779,33 @@ class A(Protocol): pass [builtins fixtures/tuple.pyi] + +[case testTupleTypeSelfTypeProto] +from typing import Protocol, TypeVar + +R = TypeVar("R", covariant=True) + +class P(Protocol[R]): + def rep(self) -> R: ... + +T = TypeVar("T") +def rep(x: P[T]) -> T: ... + +class C(tuple[int, str]): + def rep(self: T) -> T: ... + +t: C +reveal_type(t) # N: Revealed type is "tuple[builtins.int, builtins.str, fallback=__main__.C]" +reveal_type(rep(t)) # N: Revealed type is "tuple[builtins.int, builtins.str, fallback=__main__.C]" + +def ok_rep(x: P[tuple[int, str]]) -> None: ... +ok_rep(t) + +def bad_rep(x: P[tuple[str, int]]) -> None: ... +bad_rep(t) # E: Argument 1 to "bad_rep" has incompatible type "C"; expected "P[tuple[str, int]]" \ + # N: Following member(s) of "C" have conflicts: \ + # N: Expected: \ + # N: def rep(self) -> tuple[str, int] \ + # N: Got: \ + # N: def rep(self) -> C +[builtins fixtures/tuple.pyi] From 37ee4323e574668c083c5b4d9d1f272591e3a3aa Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 23 May 2026 01:41:37 +0100 Subject: [PATCH 043/127] Clean-up classes nested in functions (#21478) Fixes https://github.com/python/mypy/issues/6422 Fixes https://github.com/python/mypy/issues/13024 TBH the current situation is embarrassing. I decided to _finally_ clean this all up and unify various cases. Now we have same simple rules used by all classes, both regular (first three) and magic: * Use the name mangling for everything inside a top-level function. * Do not mangle `defn.name` only `defn.fullname`. * Always store classes nested in functions in global symbol table (using enclosing class was only adding unnecessary complications) * In cases of name mismatch (like `One = NamedTuple("Other", ...)` etc) always use the `var_name` for all purposes. * In cases of inline base classes use different mangling mechanism (since those may be nested in functions as well). --- mypy/checker.py | 3 + mypy/nodes.py | 10 +++ mypy/semanal.py | 119 ++++++++++--------------- mypy/semanal_enum.py | 26 ++---- mypy/semanal_namedtuple.py | 78 ++++------------ mypy/semanal_newtype.py | 7 +- mypy/semanal_shared.py | 13 ++- mypy/semanal_typeddict.py | 37 +++----- test-data/unit/check-classes.test | 9 ++ test-data/unit/check-enum.test | 7 +- test-data/unit/check-incremental.test | 79 +++++++++++++++- test-data/unit/check-newsemanal.test | 10 +-- test-data/unit/check-newtype.test | 3 +- test-data/unit/check-serialize.test | 24 ++--- test-data/unit/semanal-namedtuple.test | 12 +-- 15 files changed, 219 insertions(+), 218 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2cb3d69b2d770..c35e78d83a604 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8577,6 +8577,9 @@ def is_func_scope(self) -> bool: # message types are ignored. return False + def is_nested_within_func_scope(self) -> bool: + return self._chk.scope.top_level_function() is not None + @property def type(self) -> TypeInfo | None: return self._chk.scope.current_class() diff --git a/mypy/nodes.py b/mypy/nodes.py index bcaf7f7b239cf..a1a72bf885801 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -5396,6 +5396,16 @@ def set_info(node: SymbolNode, info: TypeInfo) -> None: set_info(node.impl, info) +def func_scoped_name(name: str, line: int) -> str: + """Mangled name to use when storing function-scoped symbols in global symbol tables.""" + return f"{name}@{line}" + + +def inline_base(name: str, index: int) -> str: + """Synthetic name to use when storing inlined base classes in symbol tables.""" + return f"{name}@base{index + 1}" + + # See docstring for mypy/cache.py for reserved tag ranges. MYPY_FILE: Final[Tag] = 50 OVERLOADED_FUNC_DEF: Final[Tag] = 51 diff --git a/mypy/semanal.py b/mypy/semanal.py index b30958ec7c719..57a0a654f8f38 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -188,8 +188,10 @@ WithStmt, YieldExpr, YieldFromExpr, + func_scoped_name, get_member_expr_fullname, implicit_module_attrs, + inline_base, is_final_node, type_aliases, type_aliases_source_versions, @@ -1992,7 +1994,7 @@ def analyze_class(self, defn: ClassDef) -> None: return self.analyze_class_keywords(defn) - bases_result = self.analyze_base_classes(bases) + bases_result = self.analyze_base_classes(defn.name, bases) if bases_result is None or self.found_incomplete_ref(tag): # Something was incomplete. Defer current target. self.mark_incomplete(defn.name, defn) @@ -2112,7 +2114,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: if info is None: self.mark_incomplete(defn.name, defn) else: - self.prepare_class_def(defn, info, custom_names=True) + self.prepare_class_def(defn, info) for decorator in defn.decorators: decorator.accept(self) if defn.info: @@ -2136,13 +2138,13 @@ def analyze_namedtuple_classdef( info: TypeInfo | None = defn.info else: is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef( - defn, self.is_stub_file, self.is_func_scope() + defn, self.is_stub_file ) if is_named_tuple: if info is None: self.mark_incomplete(defn.name, defn) else: - self.prepare_class_def(defn, info, custom_names=True) + self.prepare_class_def(defn, info) self.setup_type_vars(defn, tvar_defs) self.setup_alias_type_vars(defn) with self.scope.class_scope(defn.info): @@ -2512,51 +2514,26 @@ def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLi tvar_defs.append(tvar_def) return tvar_defs - def prepare_class_def( - self, defn: ClassDef, info: TypeInfo | None = None, custom_names: bool = False - ) -> None: + def class_fullname(self, name: str, line: int) -> str: + if not self.is_nested_within_func_scope(): + return self.qualified_name(name) + name = func_scoped_name(name, line) + return f"{self.cur_mod_id}.{name}" + + def prepare_class_def(self, defn: ClassDef, info: TypeInfo | None = None) -> None: """Prepare for the analysis of a class definition. Create an empty TypeInfo and store it in a symbol table, or if the 'info' argument is provided, store it instead (used for magic type definitions). """ if not defn.info: - defn.fullname = self.qualified_name(defn.name) - # TODO: Nested classes + defn.fullname = self.class_fullname(defn.name, defn.line) info = info or self.make_empty_type_info(defn) defn.info = info info.defn = defn - if not custom_names: - # Some special classes (in particular NamedTuples) use custom fullname logic. - # Don't override it here (also see comment below, this needs cleanup). - if not self.is_func_scope(): - info._fullname = self.qualified_name(defn.name) - else: - info._fullname = info.name - local_name = defn.name - if "@" in local_name: - local_name = local_name.split("@")[0] - self.add_symbol(local_name, defn.info, defn) + self.add_symbol(defn.name, defn.info, defn) if self.is_nested_within_func_scope(): - # We need to preserve local classes, let's store them - # in globals under mangled unique names - # - # TODO: Putting local classes into globals breaks assumptions in fine-grained - # incremental mode and we should avoid it. In general, this logic is too - # ad-hoc and needs to be removed/refactored. - if "@" not in defn.info._fullname: - global_name = defn.info.name + "@" + str(defn.line) - defn.info._fullname = self.cur_mod_id + "." + global_name - else: - # Preserve name from previous fine-grained incremental run. - global_name = defn.info.name - defn.fullname = defn.info._fullname - if defn.info.is_named_tuple or defn.info.typeddict_type: - # Named tuples and Typed dicts nested within a class are stored - # in the class symbol table. - self.add_symbol_skip_local(global_name, defn.info) - else: - self.globals[global_name] = SymbolTableNode(GDEF, defn.info) + self.add_global_symbol(defn.name, defn, defn.info) def make_empty_type_info(self, defn: ClassDef) -> TypeInfo: if ( @@ -2587,7 +2564,7 @@ def get_name_repr_of_expr(self, expr: Expression) -> str | None: return None def analyze_base_classes( - self, base_type_exprs: list[Expression] + self, cls_name: str, base_type_exprs: list[Expression] ) -> tuple[list[tuple[ProperType, Expression]], bool] | None: """Analyze base class types. @@ -2599,7 +2576,7 @@ def analyze_base_classes( """ is_error = False bases = [] - for base_expr in base_type_exprs: + for i, base_expr in enumerate(base_type_exprs): if ( isinstance(base_expr, RefExpr) and base_expr.fullname in TYPED_NAMEDTUPLE_NAMES + TPDICT_NAMES @@ -2617,7 +2594,10 @@ def analyze_base_classes( try: base = self.expr_to_analyzed_type( - base_expr, allow_placeholder=True, allow_type_any=True + base_expr, + allow_placeholder=True, + allow_type_any=True, + unique_name=inline_base(cls_name, i), ) except TypeTranslationError: name = self.get_name_repr_of_expr(base_expr) @@ -3594,7 +3574,7 @@ def analyze_enum_assign(self, s: AssignmentStmt) -> bool: # This is an analyzed enum definition. # It is valid iff it can be stored correctly, failures were already reported. return self._is_single_name_assignment(s) - return self.enum_call_analyzer.process_enum_call(s, self.is_func_scope()) + return self.enum_call_analyzer.process_enum_call(s) def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool: """Check if s defines a namedtuple.""" @@ -3618,7 +3598,7 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool: namespace = self.qualified_name(name) with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple( - s.rvalue, name, self.is_func_scope() + s.rvalue, name ) if internal_name is None: return False @@ -3655,7 +3635,7 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool: namespace = self.qualified_name(name) with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict( - s.rvalue, name, self.is_func_scope() + s.rvalue, name ) if not is_typed_dict: return False @@ -5161,17 +5141,18 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: return True def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo: - if self.is_func_scope() and not self.type and "@" not in name: - name += "@" + str(line) class_def = ClassDef(name, Block([])) - if self.is_func_scope() and not self.type: - # Full names of generated classes should always be prefixed with the module names - # even if they are nested in a function, since these classes will be (de-)serialized. - # (Note that the caller should append @line to the name to avoid collisions.) - # TODO: clean this up, see #6422. - class_def.fullname = self.cur_mod_id + "." + self.qualified_name(name) - else: - class_def.fullname = self.qualified_name(name) + # Ground rules for classes nested in functions: + # * Use is_nested_within_func_scope(), not is_func_scope(), to determine whether + # to use any special logic, because nothing inside top-level functions is serialized. + # * ClassDef.name is not mangled (i.e. @line suffix is not appended). + # * ClassDef.fullname, and thus TypeInfo.fullname are always pkg.mod.Name@line, any + # "intermediate" classes are not included in the fullname. + # * The caller is responsible for storing the generated TypeInfo twice: once as usual + # with add_symbol(), and once using add_global_symbol() using the mangled name. + # The second one is needed to properly serialize any classes nested in functions. + # TODO: make sure the daemon works well with these rules. + class_def.fullname = self.class_fullname(name, line) info = TypeInfo(SymbolTable(), class_def, self.cur_mod_id) class_def.info = info @@ -7030,27 +7011,18 @@ def add_symbol( name, symbol, context, can_defer, escape_comprehensions, no_progress, type_param ) - def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None: - """Same as above, but skipping the local namespace. + def add_global_symbol(self, name: str, ctx: Context, node: SymbolNode) -> None: + """Add symbol to a global namespace. This doesn't check for previous definition and is only used - for serialization of method-level classes. + for serialization of classes nested in functions/methods. Classes defined within methods can be exposed through an attribute type, but method-level symbol tables aren't serialized. This method can be used to add such classes to an enclosing, serialized symbol table. """ - # TODO: currently this is only used by named tuples and typed dicts. - # Use this method also by normal classes, see issue #6422. - if self.type is not None: - names = self.type.names - kind = MDEF - else: - names = self.globals - kind = GDEF - symbol = SymbolTableNode(kind, node) - names[name] = symbol + self.globals[func_scoped_name(name, ctx.line)] = SymbolTableNode(GDEF, node) def add_symbol_table_node( self, @@ -7111,8 +7083,10 @@ def add_symbol_table_node( if isinstance(old, Var) and is_init_only(old): if old.has_explicit_value: self.fail("InitVar with default value cannot be redefined", context) - elif not ( - isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new) + elif ( + not (isinstance(new, (FuncDef, Decorator)) and self.set_original_def(old, new)) + # Avoid (additional) errors for internal symbols. + and "@" not in name ): self.name_already_defined(name, context, existing) elif type_param or ( @@ -7710,14 +7684,15 @@ def expr_to_analyzed_type( allow_unbound_tvars: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, + unique_name: str | None = None, ) -> Type | None: - if isinstance(expr, CallExpr): + if unique_name is not None and isinstance(expr, CallExpr): # This is a legacy syntax intended mostly for Python 2, we keep it for # backwards compatibility, but new features like generic named tuples # and recursive named tuples will be not supported. expr.accept(self) internal_name, info, tvar_defs = self.named_tuple_analyzer.check_namedtuple( - expr, None, self.is_func_scope() + expr, unique_name ) if tvar_defs: self.fail("Generic named tuples are not supported for legacy class syntax", expr) diff --git a/mypy/semanal_enum.py b/mypy/semanal_enum.py index b1e267b4c781f..4074a586a29f7 100644 --- a/mypy/semanal_enum.py +++ b/mypy/semanal_enum.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Final, cast +from typing import Final from mypy.nodes import ( ARG_NAMED, @@ -60,7 +60,7 @@ def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None: self.options = options self.api = api - def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool: + def process_enum_call(self, s: AssignmentStmt) -> bool: """Check if s defines an Enum; if yes, store the definition in symbol table. Return True if this looks like an Enum definition (but maybe with errors), @@ -70,7 +70,7 @@ def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool: return False lvalue = s.lvalues[0] name = lvalue.name - enum_call = self.check_enum_call(s.rvalue, name, is_func_scope) + enum_call = self.check_enum_call(s.rvalue, name) if enum_call is None: return False if isinstance(lvalue, MemberExpr): @@ -80,9 +80,7 @@ def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool: self.api.add_symbol(name, enum_call, s) return True - def check_enum_call( - self, node: Expression, var_name: str, is_func_scope: bool - ) -> TypeInfo | None: + def check_enum_call(self, node: Expression, var_name: str) -> TypeInfo | None: """Check if a call defines an Enum. Example: @@ -110,23 +108,15 @@ class A(enum.Enum): ) if not ok: # Error. Construct dummy return value. - name = var_name - if is_func_scope: - name += "@" + str(call.line) - info = self.build_enum_call_typeinfo(name, [], fullname, node.line) + info = self.build_enum_call_typeinfo(var_name, [], fullname, node.line) else: if new_class_name != var_name: msg = f'String argument 1 "{new_class_name}" to {fullname}(...) does not match variable name "{var_name}"' self.fail(msg, call) - - name = cast(StrExpr, call.args[0]).value - if name != var_name or is_func_scope: - # Give it a unique name derived from the line number. - name += "@" + str(call.line) - info = self.build_enum_call_typeinfo(name, items, fullname, call.line) + info = self.build_enum_call_typeinfo(var_name, items, fullname, call.line) # Store generated TypeInfo under both names, see semanal_namedtuple for more details. - if name != var_name or is_func_scope: - self.api.add_symbol_skip_local(name, info) + if self.api.is_nested_within_func_scope(): + self.api.add_global_symbol(var_name, node, info) call.analyzed = EnumCallExpr(info, items, values) call.analyzed.set_line(call) info.line = node.line diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 08fc3e2bb7725..6244cb8150907 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -105,7 +105,7 @@ def __init__( self.msg = msg def analyze_namedtuple_classdef( - self, defn: ClassDef, is_stub_file: bool, is_func_scope: bool + self, defn: ClassDef, is_stub_file: bool ) -> tuple[bool, TypeInfo | None]: """Analyze if given class definition can be a named tuple definition. @@ -122,8 +122,6 @@ def analyze_namedtuple_classdef( # This is a valid named tuple, but some types are incomplete. return True, None items, types, default_items, statements = result - if is_func_scope and "@" not in defn.name: - defn.name += "@" + str(defn.line) existing_info = None if isinstance(defn.analyzed, NamedTupleExpr): existing_info = defn.analyzed.info @@ -221,12 +219,13 @@ def check_namedtuple_classdef( return items, types, default_items, statements def check_namedtuple( - self, node: Expression, var_name: str | None, is_func_scope: bool + self, node: Expression, name: str ) -> tuple[str | None, TypeInfo | None, list[TypeVarLikeType]]: """Check if a call defines a namedtuple. - The optional var_name argument is the name of the variable to - which this is assigned, if any. + The name argument is the name of the variable to which this is assigned. + For an inlined base class this is a unique name generated from class name + base number. Return a tuple of two items: * Internal name of the named tuple (e.g. the name passed as an argument to namedtuple) @@ -254,42 +253,17 @@ def check_namedtuple( items, types, defaults, typename, tvar_defs, ok = result else: # Error. Construct dummy return value. - if var_name: - name = var_name - if is_func_scope: - name += "@" + str(call.line) - else: - name = var_name = "namedtuple@" + str(call.line) info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line, None) - self.store_namedtuple_info(info, var_name, call, is_typed) - if name != var_name or is_func_scope: - # NOTE: we skip local namespaces since they are not serialized. - self.api.add_symbol_skip_local(name, info) - return var_name, info, [] + self.store_namedtuple_info(info, name, call, is_typed) + if self.api.is_nested_within_func_scope(): + # NOTE: we always serialize in global namespace for convenience, + # because local namespaces are never serialized. + self.api.add_global_symbol(name, call, info) + return name, info, [] if not ok: # This is a valid named tuple but some types are not ready. - return typename, None, [] - - # We use the variable name as the class name if it exists. If - # it doesn't, we use the name passed as an argument. We prefer - # the variable name because it should be unique inside a - # module, and so we don't need to disambiguate it with a line - # number. - if var_name: - name = var_name - else: - name = typename - - if var_name is None or is_func_scope: - # There are two special cases where need to give it a unique name derived - # from the line number: - # * This is a base class expression, since it often matches the class name: - # class NT(NamedTuple('NT', [...])): - # ... - # * This is a local (function or method level) named tuple, since - # two methods of a class can define a named tuple with the same name, - # and they will be stored in the same namespace (see below). - name += "@" + str(call.line) + return name, None, [] + if defaults: default_items = { arg_name: default for arg_name, default in zip(items[-len(defaults) :], defaults) @@ -304,29 +278,9 @@ def check_namedtuple( name, items, types, default_items, node.line, existing_info ) - # If var_name is not None (i.e. this is not a base class expression), we always - # store the generated TypeInfo under var_name in the current scope, so that - # other definitions can use it. - if var_name: - self.store_namedtuple_info(info, var_name, call, is_typed) - else: - call.analyzed = NamedTupleExpr(info, is_typed=is_typed) - call.analyzed.set_line(call) - # There are three cases where we need to store the generated TypeInfo - # second time (for the purpose of serialization): - # * If there is a name mismatch like One = NamedTuple('Other', [...]) - # we also store the info under name 'Other@lineno', this is needed - # because classes are (de)serialized using their actual fullname, not - # the name of l.h.s. - # * If this is a method level named tuple. It can leak from the method - # via assignment to self attribute and therefore needs to be serialized - # (local namespaces are not serialized). - # * If it is a base class expression. It was not stored above, since - # there is no var_name (but it still needs to be serialized - # since it is in MRO of some class). - if name != var_name or is_func_scope: - # NOTE: we skip local namespaces since they are not serialized. - self.api.add_symbol_skip_local(name, info) + self.store_namedtuple_info(info, name, call, is_typed) + if self.api.is_nested_within_func_scope(): + self.api.add_global_symbol(name, call, info) return typename, info, tvar_defs def store_namedtuple_info( diff --git a/mypy/semanal_newtype.py b/mypy/semanal_newtype.py index e1d62c4410c9f..2735832cc0fe7 100644 --- a/mypy/semanal_newtype.py +++ b/mypy/semanal_newtype.py @@ -67,9 +67,6 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool: name = var_name # OK, now we know this is a NewType. But the base type may be not ready yet, # add placeholder as we do for ClassDef. - - if self.api.is_func_scope(): - name += "@" + str(s.line) fullname = self.api.qualified_name(name) if not call.analyzed or isinstance(call.analyzed, NewTypeExpr) and not call.analyzed.info: @@ -134,8 +131,8 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool: else: call.analyzed.info.bases = newtype_class_info.bases self.api.add_symbol(var_name, call.analyzed.info, s) - if self.api.is_func_scope(): - self.api.add_symbol_skip_local(name, call.analyzed.info) + if self.api.is_nested_within_func_scope(): + self.api.add_global_symbol(var_name, s, call.analyzed.info) newtype_class_info.line = s.line return True diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index a85d4ed00b5e6..6261c7f011eb3 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -137,6 +137,10 @@ def is_stub_file(self) -> bool: def is_func_scope(self) -> bool: raise NotImplementedError + @abstractmethod + def is_nested_within_func_scope(self) -> bool: + raise NotImplementedError + @property @abstractmethod def type(self) -> TypeInfo | None: @@ -230,13 +234,8 @@ def add_symbol( raise NotImplementedError @abstractmethod - def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None: - """Add symbol to the current symbol table, skipping locals. - - This is used to store symbol nodes in a symbol table that - is going to be serialized (local namespaces are not serialized). - See implementation docstring for more details. - """ + def add_global_symbol(self, name: str, ctx: Context, node: SymbolNode) -> None: + """Add symbol directly to the global symbol table.""" raise NotImplementedError @abstractmethod diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 3655e4c89dd4b..593bbbbb25672 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -33,6 +33,7 @@ TypeAlias, TypedDictExpr, TypeInfo, + inline_base, ) from mypy.options import Options from mypy.semanal_shared import ( @@ -113,8 +114,6 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N ) if field_types is None: return True, None # Defer - if self.api.is_func_scope() and "@" not in defn.name: - defn.name += "@" + str(defn.line) info = self.build_typeddict_typeinfo( defn.name, field_types, required_keys, readonly_keys, defn.line, existing_info ) @@ -127,8 +126,8 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N # Extending/merging existing TypedDicts typeddict_bases: list[Expression] = [] typeddict_bases_set = set() - for expr in defn.base_type_exprs: - ok, maybe_type_info, _ = self.check_typeddict(expr, None, False) + for i, expr in enumerate(defn.base_type_exprs): + ok, maybe_type_info, _ = self.check_typeddict(expr, inline_base(defn.name, i)) if ok and maybe_type_info is not None: # expr is a CallExpr info = maybe_type_info @@ -406,12 +405,13 @@ def extract_meta_info( return typ, is_required, readonly def check_typeddict( - self, node: Expression, var_name: str | None, is_func_scope: bool + self, node: Expression, name: str ) -> tuple[bool, TypeInfo | None, list[TypeVarLikeType]]: """Check if a call defines a TypedDict. - The optional var_name argument is the name of the variable to - which this is assigned, if any. + The name argument is the name of the variable to which this is assigned. + For an inlined base class this is a unique name generated from class name + base number. Return a pair (is it a typed dict, corresponding TypeInfo). @@ -433,28 +433,19 @@ def check_typeddict( # This is a valid typed dict, but some type is not ready. # The caller should defer this until next iteration. return True, None, [] - name, items, types, total, tvar_defs, ok = res + typename, items, types, total, tvar_defs, ok = res if not ok: # Error. Construct dummy return value. - if var_name: - name = var_name - if is_func_scope: - name += "@" + str(call.line) - else: - name = var_name = "TypedDict@" + str(call.line) info = self.build_typeddict_typeinfo(name, {}, set(), set(), call.line, None) else: - if var_name is not None and name != var_name: + if "@" not in name and name != typename: self.fail( 'First argument "{}" to TypedDict() does not match variable name "{}"'.format( - name, var_name + typename, name ), node, code=codes.NAME_MATCH, ) - if name != var_name or is_func_scope: - # Give it a unique name derived from the line number. - name += "@" + str(call.line) required_keys = { field for (field, t) in zip(items, types) @@ -481,6 +472,7 @@ def check_typeddict( existing_info = None if isinstance(node.analyzed, TypedDictExpr): existing_info = node.analyzed.info + info = self.build_typeddict_typeinfo( name, dict(zip(items, types)), @@ -491,10 +483,9 @@ def check_typeddict( ) info.line = node.line # Store generated TypeInfo under both names, see semanal_namedtuple for more details. - if name != var_name or is_func_scope: - self.api.add_symbol_skip_local(name, info) - if var_name: - self.api.add_symbol(var_name, info, node) + self.api.add_symbol(name, info, node) + if self.api.is_nested_within_func_scope(): + self.api.add_global_symbol(name, node, info) call.analyzed = TypedDictExpr(info) call.analyzed.set_line(call) return True, info, tvar_defs diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 5a66eff2bd3b7..8f3407c954e00 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9412,3 +9412,12 @@ from typ import NT def f() -> NT: return NT(x='') [builtins fixtures/tuple.pyi] + +[case testClassNestedInFunctionNotLeaking] +from a import X # E: Module "a" has no attribute "X" +reveal_type(X) # N: Revealed type is "Any" +[file a.py] +def f() -> None: + class X: + ... + undefined # E: Name "undefined" is not defined diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 55a20e2b3fa2c..20cf3b5aebfbc 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -604,8 +604,9 @@ V = Enum('U', **{'a': 1}) # E: Unexpected arguments to Enum() W = Enum('W', 'a b') W.c # E: "type[W]" has no attribute "c" X = Enum('Something', 'a b') # E: String argument 1 "Something" to enum.Enum(...) does not match variable name "X" -reveal_type(X.a) # N: Revealed type is "Literal[__main__.Something@23.a]?" -X.asdf # E: "type[Something@23]" has no attribute "asdf" +reveal_type(X.a) # N: Revealed type is "Literal[__main__.X.a]?" +X.asdf # E: "type[X]" has no attribute "asdf" + [builtins fixtures/tuple.pyi] [typing fixtures/typing-medium.pyi] @@ -633,7 +634,7 @@ a = A() reveal_type(a.x) [builtins fixtures/enum.pyi] [out] -main:7: note: Revealed type is "__main__.A.E@4" +main:7: note: Revealed type is "__main__.E@4" [case testFunctionalEnumInClassBody] from enum import Enum diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index db15b73419109..07d70ba7cfb12 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -5188,7 +5188,7 @@ class C: [builtins fixtures/tuple.pyi] [out] [out2] -tmp/a.py:3: note: Revealed type is "tuple[builtins.int, fallback=b.C.Hidden@5]" +tmp/a.py:3: note: Revealed type is "tuple[builtins.int, fallback=b.Hidden@5]" [case testIncrementalNodeCreatedFromGetattr] import a @@ -6892,7 +6892,7 @@ class C: [typing fixtures/typing-typeddict.pyi] [out] [out2] -tmp/a.py:3: note: Revealed type is "TypedDict('b.C.Hidden@5', {'x': builtins.int})" +tmp/a.py:3: note: Revealed type is "TypedDict('b.Hidden@5', {'x': builtins.int})" [case testNoIncrementalCrashOnInvalidEnumMethod] import a @@ -6916,7 +6916,7 @@ class TheClass: [builtins fixtures/tuple.pyi] [out] [out2] -tmp/a.py:3: note: Revealed type is "def (value: builtins.object) -> lib.TheClass.pyenum@6" +tmp/a.py:3: note: Revealed type is "def (value: builtins.object) -> lib.pyenum@6" -- Note: do not use _no_parallel unless really needed! [case testIncrementalFunctoolsPartial_no_parallel] @@ -8011,3 +8011,76 @@ x = "hello" [builtins fixtures/module.pyi] [stale b] [rechecked b] + +[case testNamedTupleInlineBaseClassNoNameCollision] +import m +[file m.py] +from lib import A, B +a: A +b: B +reveal_type(a) +reveal_type(b) +[file m.py.2] +from lib import A, B +a: A +b: B +reveal_type(a) # touch +reveal_type(b) +[file lib.py] +from typing import NamedTuple +class A(NamedTuple("Same", [('x', int)])): pass +class B(NamedTuple("Same", [('x', str)])): pass +[builtins fixtures/tuple.pyi] +[out] +tmp/m.py:4: note: Revealed type is "tuple[builtins.int, fallback=lib.A]" +tmp/m.py:5: note: Revealed type is "tuple[builtins.str, fallback=lib.B]" +[out2] +tmp/m.py:4: note: Revealed type is "tuple[builtins.int, fallback=lib.A]" +tmp/m.py:5: note: Revealed type is "tuple[builtins.str, fallback=lib.B]" + +[case testNamedTupleInlineBaseClassRedefined] +import m +[file m.py] +from lib import A +a: A +reveal_type(a) +[file m.py.2] +from lib import A +a: A # touch +reveal_type(a) +[file lib.py] +from typing import NamedTuple +class A(NamedTuple("N", [('x', int)])): pass +class A(NamedTuple("N", [('x', str)])): pass +[builtins fixtures/tuple.pyi] +[out] +tmp/lib.py:3: error: Name "A" already defined on line 2 +tmp/m.py:3: note: Revealed type is "tuple[builtins.int, fallback=lib.A]" +[out2] +tmp/lib.py:3: error: Name "A" already defined on line 2 +tmp/m.py:3: note: Revealed type is "tuple[builtins.int, fallback=lib.A]" + +[case testTypedDictInlineBaseClassRedefined] +import m +[file m.py] +from lib import A +a: A +reveal_type(a) +[file m.py.2] +from lib import A +a: A # touch +reveal_type(a) +[file lib.py] +from typing import TypedDict +class A(TypedDict("TD", {'x': int})): + pass +class A(TypedDict("TD", {'x': str})): + pass +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[out] +tmp/lib.py:4: error: Name "A" already defined (possibly by an import) +tmp/m.py:3: note: Revealed type is "TypedDict('lib.A', {'x': builtins.int})" +[out2] +tmp/lib.py:4: error: Name "A" already defined (possibly by an import) +tmp/m.py:3: note: Revealed type is "TypedDict('lib.A', {'x': builtins.int})" diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test index 4148d04014a81..b0b01ad3a593a 100644 --- a/test-data/unit/check-newsemanal.test +++ b/test-data/unit/check-newsemanal.test @@ -936,8 +936,8 @@ class C: self.o: Out c = C() -reveal_type(c.o) # N: Revealed type is "tuple[tuple[builtins.str, __main__.Other@7, fallback=__main__.C.In@6], __main__.Other@7, fallback=__main__.C.Out@5]" -reveal_type(c.o.x) # N: Revealed type is "tuple[builtins.str, __main__.Other@7, fallback=__main__.C.In@6]" +reveal_type(c.o) # N: Revealed type is "tuple[tuple[builtins.str, __main__.Other@7, fallback=__main__.In@6], __main__.Other@7, fallback=__main__.Out@5]" +reveal_type(c.o.x) # N: Revealed type is "tuple[builtins.str, __main__.Other@7, fallback=__main__.In@6]" [builtins fixtures/tuple.pyi] [case testNewAnalyzerNamedTupleClassNestedMethod] @@ -956,9 +956,9 @@ class C: self.o: Out c = C() -reveal_type(c.o) # N: Revealed type is "tuple[tuple[builtins.str, __main__.Other@12, fallback=__main__.C.In@9], __main__.Other@12, fallback=__main__.C.Out@5]" -reveal_type(c.o.x) # N: Revealed type is "tuple[builtins.str, __main__.Other@12, fallback=__main__.C.In@9]" -reveal_type(c.o.method()) # N: Revealed type is "tuple[builtins.str, __main__.Other@12, fallback=__main__.C.In@9]" +reveal_type(c.o) # N: Revealed type is "tuple[tuple[builtins.str, __main__.Other@12, fallback=__main__.In@9], __main__.Other@12, fallback=__main__.Out@5]" +reveal_type(c.o.x) # N: Revealed type is "tuple[builtins.str, __main__.Other@12, fallback=__main__.In@9]" +reveal_type(c.o.method()) # N: Revealed type is "tuple[builtins.str, __main__.Other@12, fallback=__main__.In@9]" [builtins fixtures/tuple.pyi] [case testNewAnalyzerNamedTupleClassForwardMethod] diff --git a/test-data/unit/check-newtype.test b/test-data/unit/check-newtype.test index a0789596a4798..95042d1393ab1 100644 --- a/test-data/unit/check-newtype.test +++ b/test-data/unit/check-newtype.test @@ -194,7 +194,7 @@ def func() -> None: A = NewType('A', str) B = NewType('B', str) - a = A(3) # E: Argument 1 to "A@6" has incompatible type "int"; expected "str" + a = A(3) # E: Argument 1 to "A" has incompatible type "int"; expected "str" a = A('xyz') b = B('xyz') @@ -206,7 +206,6 @@ class MyClass: b = A(3) c = MyClass.C(3.5) -[out] [case testNewTypeInMultipleFiles] import a diff --git a/test-data/unit/check-serialize.test b/test-data/unit/check-serialize.test index 7a257bc017c36..7bc7f78bd961e 100644 --- a/test-data/unit/check-serialize.test +++ b/test-data/unit/check-serialize.test @@ -729,13 +729,13 @@ class C: self.c = A [builtins fixtures/tuple.pyi] [out1] -main:2: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.C.A@4]" -main:3: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.C.A@4]" -main:4: note: Revealed type is "def (x: builtins.int) -> tuple[builtins.int, fallback=ntcrash.C.A@4]" +main:2: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.A@4]" +main:3: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.A@4]" +main:4: note: Revealed type is "def (x: builtins.int) -> tuple[builtins.int, fallback=ntcrash.A@4]" [out2] -main:2: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.C.A@4]" -main:3: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.C.A@4]" -main:4: note: Revealed type is "def (x: builtins.int) -> tuple[builtins.int, fallback=ntcrash.C.A@4]" +main:2: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.A@4]" +main:3: note: Revealed type is "tuple[builtins.int, fallback=ntcrash.A@4]" +main:4: note: Revealed type is "def (x: builtins.int) -> tuple[builtins.int, fallback=ntcrash.A@4]" -- -- Strict optional @@ -1066,13 +1066,13 @@ class C: [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] [out1] -main:2: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" -main:3: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" -main:4: note: Revealed type is "def (*, x: builtins.int) -> TypedDict('ntcrash.C.A@4', {'x': builtins.int})" +main:2: note: Revealed type is "TypedDict('ntcrash.A@4', {'x': builtins.int})" +main:3: note: Revealed type is "TypedDict('ntcrash.A@4', {'x': builtins.int})" +main:4: note: Revealed type is "def (*, x: builtins.int) -> TypedDict('ntcrash.A@4', {'x': builtins.int})" [out2] -main:2: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" -main:3: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" -main:4: note: Revealed type is "def (*, x: builtins.int) -> TypedDict('ntcrash.C.A@4', {'x': builtins.int})" +main:2: note: Revealed type is "TypedDict('ntcrash.A@4', {'x': builtins.int})" +main:3: note: Revealed type is "TypedDict('ntcrash.A@4', {'x': builtins.int})" +main:4: note: Revealed type is "def (*, x: builtins.int) -> TypedDict('ntcrash.A@4', {'x': builtins.int})" [case testSerializeNonTotalTypedDict] from m import d diff --git a/test-data/unit/semanal-namedtuple.test b/test-data/unit/semanal-namedtuple.test index 62bd87f1995a6..a4b8df00b9a3f 100644 --- a/test-data/unit/semanal-namedtuple.test +++ b/test-data/unit/semanal-namedtuple.test @@ -121,9 +121,9 @@ MypyFile:1( ClassDef:2( A TupleType( - tuple[Any, fallback=__main__.N@2]) + tuple[Any, fallback=__main__.A@base1]) BaseType( - __main__.N@2) + __main__.A@base1) PassStmt:2())) [case testNamedTupleBaseClassWithItemTypes] @@ -136,9 +136,9 @@ MypyFile:1( ClassDef:2( A TupleType( - tuple[builtins.int, fallback=__main__.N@2]) + tuple[builtins.int, fallback=__main__.A@base1]) BaseType( - __main__.N@2) + __main__.A@base1) PassStmt:2())) -- Errors @@ -239,9 +239,9 @@ MypyFile:1( ClassDef:4( A TupleType( - tuple[builtins.int, fallback=__main__.N@4]) + tuple[builtins.int, fallback=__main__.A@base1]) Decorators( NameExpr(final [typing.final])) BaseType( - __main__.N@4) + __main__.A@base1) PassStmt:5())) From 47ca4c247c38b77a1267b12127db1eee6c89487a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 26 May 2026 13:11:09 +0100 Subject: [PATCH 044/127] Fix various crashes on recursive type variable defaults (#21491) Fixes https://github.com/python/mypy/issues/17716 Fixes https://github.com/python/mypy/issues/20698 Fixes https://github.com/python/mypy/issues/21128 Fixes https://github.com/python/mypy/issues/21269 Although the main idea is relatively simple (replace explicit type variable defaults with `Any` in cases where they would cause infinite recursion), this required a lot of "plumbing", because: * We always call `fix_instance()` eagerly and early. * There are four different cases to handle (classes vs aliases, and old style vs new style). * Relevant pieces of information are available in "distant" places (and I don't want to add any global state). Some additional notes: * I clean-up some weirdness with deferral on type variable defaults with placeholders (especially in type aliases). * I remove an unwarranted `get_proper_type()` call during semantic analysis. * Now we handle rare edge case of unused alias type variables in `BoolTypeQuery` (possible with new style aliases). * Type variable defaults should be considered in `__eq__()` to correctly detect progress. --- mypy/checker.py | 3 + mypy/checkexpr.py | 22 +-- mypy/message_registry.py | 1 + mypy/nodes.py | 29 +++- mypy/semanal.py | 183 ++++++++++++--------- mypy/semanal_shared.py | 5 + mypy/type_visitor.py | 10 +- mypy/typeanal.py | 137 +++++++++++++-- mypy/types.py | 12 +- test-data/unit/check-flags.test | 31 ++++ test-data/unit/check-python313.test | 86 ++++++++++ test-data/unit/check-typevar-defaults.test | 104 ++++++++++++ 12 files changed, 513 insertions(+), 110 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c35e78d83a604..b4ff39d49b80b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8529,6 +8529,9 @@ def lookup_fully_qualified_or_none(self, fullname: str, /) -> SymbolTableNode | except KeyError: return None + def record_fixed_type(self, fixed: TypeInfo | TypeAlias) -> None: + pass + def fail( self, msg: str, diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 5f1aeac8a7255..6cdccd912055f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4938,10 +4938,11 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: if tapp.expr.node.python_3_12_type_alias: return self.type_alias_type_type() # Subscription of a (generic) alias in runtime context, expand the alias. - item = instantiate_type_alias( + item, _ = instantiate_type_alias( tapp.expr.node, tapp.types, self.chk.fail, + self.chk.note, tapp.expr.node.no_args, tapp, self.chk.options, @@ -5006,17 +5007,16 @@ class LongName(Generic[T]): ... # A = List[Tuple[T, T]] # x = A() <- same as List[Tuple[Any, Any]], see PEP 484. disallow_any = self.chk.options.disallow_any_generics and self.is_callee - item = get_proper_type( - set_any_tvars( - alias, - [], - ctx.line, - ctx.column, - self.chk.options, - disallow_any=disallow_any, - fail=self.msg.fail, - ) + item, _ = set_any_tvars( + alias, + [], + ctx.line, + ctx.column, + self.chk.options, + disallow_any=disallow_any, + fail=self.msg.fail, ) + item = get_proper_type(item) if isinstance(item, Instance): # Normally we get a callable type (or overloaded) with .is_type_obj() true # representing the class's constructor diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 30ced27aef22f..82885065934f1 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -180,6 +180,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage: IMPLICIT_GENERIC_ANY_BUILTIN: Final = ( 'Implicit generic "Any". Use "{}" and specify generic parameters' ) +NO_CYCLIC_DEFAULT: Final = "Cyclic type variable defaults are not supported" INVALID_UNPACK: Final = "{} cannot be unpacked (must be tuple or TypeVarTuple)" INVALID_UNPACK_POSITION: Final = "Unpack is only valid in a variadic position" INVALID_PARAM_SPEC_LOCATION: Final = "Invalid location for ParamSpec {}" diff --git a/mypy/nodes.py b/mypy/nodes.py index a1a72bf885801..13a8a0fbf4e32 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3145,7 +3145,15 @@ class TypeVarLikeExpr(SymbolNode, Expression): Note that they are constructed by the semantic analyzer. """ - __slots__ = ("_name", "_fullname", "upper_bound", "default", "variance", "is_new_style") + __slots__ = ( + "_name", + "_fullname", + "upper_bound", + "default", + "variance", + "is_new_style", + "default_depends", + ) _name: str _fullname: str @@ -3160,6 +3168,9 @@ class TypeVarLikeExpr(SymbolNode, Expression): # TypeVar(..., contravariant=True) defines a contravariant type # variable. variance: int + # Record instances and type aliases that appear bare/implicit in the default value + # of this type variable. This is needed to detect recursive type variable defaults. + default_depends: set[TypeInfo | TypeAlias] | None def __init__( self, @@ -3178,6 +3189,7 @@ def __init__( self.default = default self.variance = variance self.is_new_style = is_new_style + self.default_depends = None @property def name(self) -> str: @@ -3655,6 +3667,7 @@ class is generic then it will be a type constructor of higher kind. "is_type_check_only", "deprecated", "type_object_type", + "default_depends", ) _fullname: str # Fully qualified name @@ -3816,6 +3829,16 @@ class is generic then it will be a type constructor of higher kind. # appears in runtime context. type_object_type: mypy.types.FunctionLike | None + # Type variables whose defaults depend on defaults of type variables in other classes + # and type aliases. We keep track of this to safely handle situations like this one: + # class C[T = D]: ... + # class D[S = C]: ... + # x: C + # Since we apply fix_instance() eagerly, inferring a precise type is quite tricky. + # Therefore, we infer the type of `x` as `C[D[Any]]` to avoid infinite recursion. + # Keys are type variable full names. + default_depends: dict[str, set[TypeAlias | TypeInfo]] + FLAGS: Final = [ "is_abstract", "is_enum", @@ -3877,6 +3900,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.is_type_check_only = False self.deprecated = None self.type_object_type = None + self.default_depends = {} def add_type_vars(self) -> None: self.has_type_var_tuple_type = False @@ -4548,6 +4572,7 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here "eager", "tvar_tuple_index", "python_3_12_type_alias", + "default_depends", ) __match_args__ = ("name", "target", "alias_tvars", "no_args") @@ -4580,6 +4605,8 @@ def __init__( self.eager = eager self.python_3_12_type_alias = python_3_12_type_alias self.tvar_tuple_index = None + # This plays the same role as TypeInfo.default_depends attribute. + self.default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} for i, t in enumerate(alias_tvars): if isinstance(t, mypy.types.TypeVarTupleType): self.tvar_tuple_index = i diff --git a/mypy/semanal.py b/mypy/semanal.py index 57a0a654f8f38..84ac54bfba724 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -541,6 +541,11 @@ def __init__( # import foo.bar self.transitive_submodule_imports: dict[str, set[str]] = {} + # Instances and type aliases that were fixed using default values of type + # variables. This can be used on-demand by type analyzer. Use record_fixed_type() + # to create the set lazily. + self.types_fixed: set[TypeInfo | TypeAlias] | None = None + # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @property @@ -1885,6 +1890,9 @@ def analyze_type_param( upper_bound = self.named_type("builtins.tuple", [self.object_type()]) else: upper_bound = self.object_type() + # Reset fixed types both before and after each collection just in case. + if self.types_fixed is not None: + self.types_fixed.clear() if type_param.default: default = self.anal_type( type_param.default, @@ -1894,6 +1902,7 @@ def analyze_type_param( allow_param_spec_literals=type_param.kind == PARAM_SPEC_KIND, allow_tuple_literal=type_param.kind == PARAM_SPEC_KIND, allow_unpack=type_param.kind == TYPE_VAR_TUPLE_KIND, + analyzing_tvar_def=True, ) if default is None: default = PlaceholderType(None, [], context.line) @@ -1905,6 +1914,8 @@ def analyze_type_param( default = self.check_typevartuple_default(default, type_param.default) else: default = AnyType(TypeOfAny.from_omitted_generics) + default_depends = self.types_fixed + self.types_fixed = None if type_param.kind == TYPE_VAR_KIND: values: list[Type] = [] if type_param.values: @@ -1917,7 +1928,7 @@ def analyze_type_param( values.append(AnyType(TypeOfAny.from_error)) else: values.append(analyzed) - return TypeVarExpr( + tv = TypeVarExpr( name=type_param.name, fullname=fullname, values=values, @@ -1928,7 +1939,7 @@ def analyze_type_param( line=context.line, ) elif type_param.kind == PARAM_SPEC_KIND: - return ParamSpecExpr( + tv = ParamSpecExpr( name=type_param.name, fullname=fullname, upper_bound=upper_bound, @@ -1939,7 +1950,7 @@ def analyze_type_param( else: assert type_param.kind == TYPE_VAR_TUPLE_KIND tuple_fallback = self.named_type("builtins.tuple", [self.object_type()]) - return TypeVarTupleExpr( + tv = TypeVarTupleExpr( name=type_param.name, fullname=fullname, upper_bound=upper_bound, @@ -1948,6 +1959,8 @@ def analyze_type_param( is_new_style=True, line=context.line, ) + tv.default_depends = default_depends + return tv def pop_type_args(self, type_args: list[TypeParam] | None) -> None: if not type_args: @@ -1974,24 +1987,22 @@ def analyze_class(self, defn: ClassDef) -> None: self.infer_metaclass_and_bases_from_compat_helpers(defn) bases = defn.base_type_exprs - bases, tvar_defs, is_protocol = self.clean_up_bases_and_infer_type_variables( - defn, bases, context=defn + bases, tvar_defs, is_protocol, declared_tvars = ( + self.clean_up_bases_and_infer_type_variables(defn, bases, context=defn) ) self.check_type_alias_bases(bases) - - for tvd in tvar_defs: - if isinstance(tvd, TypeVarType) and any( - has_placeholder(t) for t in [tvd.upper_bound] + tvd.values - ): - # Some type variable bounds or values are not ready, we need - # to re-analyze this class. - self.defer() - if has_placeholder(tvd.default): - # Placeholder values in TypeVarLikeTypes may get substituted in. - # Defer current target until they are ready. - self.mark_incomplete(defn.name, defn) - return + default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} + for _, tv in declared_tvars: + if tv.default_depends is not None: + default_depends[tv.fullname] = tv.default_depends + + if any(has_placeholder(tvd) for tvd in tvar_defs): + # Some type variable bounds or values are not ready, we need to + # re-analyze this class. Note we force progress to handle cases like + # class C[T = C], this matches logic in process_typevar_parameters() + # for "old style" type variables. + self.defer(force_progress=tvar_defs != defn.type_vars) self.analyze_class_keywords(defn) bases_result = self.analyze_base_classes(defn.name, bases) @@ -2006,6 +2017,10 @@ def analyze_class(self, defn: ClassDef) -> None: # are okay in nested positions, since they can't affect the MRO. self.mark_incomplete(defn.name, defn) return + if any(has_placeholder(base) for base, _ in base_types): + # We need to manually call defer() in case a placeholder was brought by a + # type variable default, so that type analyzer didn't call it. + self.defer() declared_metaclass, should_defer, any_meta = self.get_declared_metaclass( defn.name, defn.metaclass @@ -2019,14 +2034,18 @@ def analyze_class(self, defn: ClassDef) -> None: if defn.info: self.setup_type_vars(defn, tvar_defs) self.setup_alias_type_vars(defn) + defn.info.default_depends = default_depends return if self.analyze_namedtuple_classdef(defn, tvar_defs): + if defn.info: + defn.info.default_depends = default_depends return # Create TypeInfo for class now that base classes and the MRO can be calculated. self.prepare_class_def(defn) self.setup_type_vars(defn, tvar_defs) + defn.info.default_depends = default_depends if base_error: defn.info.fallback_to_any = True if any_meta: @@ -2266,7 +2285,7 @@ def analyze_class_decorator_common(self, defn: ClassDef, decorator: Expression) def clean_up_bases_and_infer_type_variables( self, defn: ClassDef, base_type_exprs: list[Expression], context: Context - ) -> tuple[list[Expression], list[TypeVarLikeType], bool]: + ) -> tuple[list[Expression], list[TypeVarLikeType], bool, list[tuple[str, TypeVarLikeExpr]]]: """Remove extra base classes such as Generic and infer type vars. For example, consider this class: @@ -2278,7 +2297,8 @@ class Foo(Bar, Generic[T]): ... Note that this is performed *before* semantic analysis. - Returns (remaining base expressions, inferred type variables, is protocol). + Returns a tuple: + (remaining base expressions, type variables, is protocol, type variable expressions). """ removed: list[int] = [] declared_tvars: TypeVarLikeList = [] @@ -2358,7 +2378,7 @@ class Foo(Bar, Generic[T]): ... defn.removed_base_type_exprs.append(defn.base_type_exprs[i]) del base_type_exprs[i] tvar_defs = self.tvar_defs_from_tvars(declared_tvars, context) - return base_type_exprs, tvar_defs, is_protocol + return base_type_exprs, tvar_defs, is_protocol, declared_tvars def analyze_class_typevar_declaration( self, base: Type, has_type_var_tuple: bool @@ -3962,11 +3982,14 @@ def analyze_alias( declared_type_vars: TypeVarLikeList | None = None, all_declared_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, - ) -> tuple[Type | None, list[TypeVarLikeType], set[str], bool]: + ) -> tuple[ + Type | None, list[TypeVarLikeType], set[str], bool, dict[str, set[TypeAlias | TypeInfo]] + ]: """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type, a list of type variables for generic aliases, - a set of names the alias depends on, and True if the original type has empty tuple index. + a set of names the alias depends on, whether the original type has empty tuple index, + and any type variables whose defaults depend on other classes or type aliases. An example for the dependencies: A = int B = str @@ -3982,7 +4005,7 @@ def analyze_alias( self.fail( "Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE ) - return None, [], set(), False + return None, [], set(), False, {} found_type_vars = self.find_type_var_likes(typ) namespace = self.qualified_name(name) @@ -4021,7 +4044,11 @@ def analyze_alias( new_tvar_defs.append(td) indexed = bool(isinstance(typ, UnboundType) and (typ.args or typ.empty_tuple_index)) - return analyzed, new_tvar_defs, depends_on, indexed + default_depends = {} + for _, tv in alias_type_vars: + if tv.default_depends is not None: + default_depends[tv.fullname] = tv.default_depends + return analyzed, new_tvar_defs, depends_on, indexed, default_depends def is_pep_613(self, s: AssignmentStmt) -> bool: if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): @@ -4121,9 +4148,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: alias_tvars: list[TypeVarLikeType] = [] depends_on: set[str] = set() indexed = False + default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} else: tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, indexed = self.analyze_alias( + res, alias_tvars, depends_on, indexed, default_depends = self.analyze_alias( lvalue.name, rvalue, allow_placeholder=True, @@ -4146,6 +4174,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # may appear in nested positions), therefore use becomes_typeinfo=True. self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) return True + self.add_type_alias_deps(depends_on) check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, context=s) # When this type alias gets "inlined", the Any is not explicit anymore, @@ -4184,6 +4213,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: eager=eager, python_3_12_type_alias=pep_695, ) + alias_node.default_depends = default_depends if isinstance(s.rvalue, (IndexExpr, CallExpr, OpExpr)): # Note: CallExpr is for "void = type(None)" and OpExpr is for "X | Y" union syntax. if not isinstance(s.rvalue.analyzed, TypeAliasExpr): @@ -4203,6 +4233,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res + existing.node.default_depends = default_depends existing.node.alias_tvars = alias_tvars existing.node.no_args = no_args updated = True @@ -4212,6 +4243,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # Otherwise just replace existing placeholder with type alias *in place*. existing._node = alias_node updated = True + # TODO: switch type aliases to if has_placeholder(): process_placeholder() pattern. + # Type aliases are last notable exception from this logic. if updated: if self.final_iteration: self.cannot_resolve_name(lvalue.name, "name", s) @@ -4735,6 +4768,8 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values = call.arg_kinds[1:].count(ARG_POS) values = self.analyze_value_types(call.args[1 : 1 + n_values]) + if self.types_fixed is not None: + self.types_fixed.clear() res = self.process_typevar_parameters( call.args[1 + n_values :], call.arg_names[1 + n_values :], @@ -4742,6 +4777,8 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values, s, ) + default_depends = self.types_fixed + self.types_fixed = None if res is None: return False variance, upper_bound, default = res @@ -4782,6 +4819,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: type_var = TypeVarExpr( name, self.qualified_name(name), values, upper_bound, default, variance ) + type_var.default_depends = default_depends type_var.line = call.line call.analyzed = type_var updated = True @@ -4795,6 +4833,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: call.analyzed.upper_bound = upper_bound call.analyzed.values = values call.analyzed.default = default + call.analyzed.default_depends = default_depends if any(has_placeholder(v) for v in values): self.process_placeholder(None, "TypeVar values", s, force_progress=updated) elif has_placeholder(upper_bound): @@ -4947,6 +4986,11 @@ def process_typevar_parameters( variance = INVARIANT return variance, upper_bound, default + def record_fixed_type(self, fixed: TypeInfo | TypeAlias) -> None: + if self.types_fixed is None: + self.types_fixed = set() + self.types_fixed.add(fixed) + def get_typevarlike_argument( self, typevarlike_name: str, @@ -4958,7 +5002,7 @@ def get_typevarlike_argument( allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_typevar_arg: bool = True, - ) -> ProperType | None: + ) -> Type | None: try: # We want to use our custom error message below, so we suppress # the default error message for invalid types here. @@ -4969,6 +5013,7 @@ def get_typevarlike_argument( allow_unbound_tvars=allow_unbound_tvars, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, + analyzing_tvar_def=param_name == "default", ) if analyzed is None: # Type variables are special: we need to place them in the symbol table @@ -4978,15 +5023,19 @@ def get_typevarlike_argument( # class Custom(Generic[T]): # ... analyzed = PlaceholderType(None, [], context.line) - typ = get_proper_type(analyzed) - if report_invalid_typevar_arg and isinstance(typ, AnyType) and typ.is_from_error: + if ( + report_invalid_typevar_arg + and isinstance(analyzed, ProperType) + and isinstance(analyzed, AnyType) + and analyzed.is_from_error + ): self.fail( message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name), param_value, ) # Note: we do not return 'None' here -- we want to continue # using the AnyType. - return typ + return analyzed except TypeTranslationError: if report_invalid_typevar_arg: self.fail( @@ -5031,6 +5080,8 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: if n_values != 0: self.fail('Too many positional arguments for "ParamSpec"', s) + if self.types_fixed is not None: + self.types_fixed.clear() default: Type = AnyType(TypeOfAny.from_omitted_generics) for param_value, param_name in zip( call.args[1 + n_values :], call.arg_names[1 + n_values :] @@ -5055,6 +5106,8 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: "The variance and bound arguments to ParamSpec do not have defined semantics yet", s, ) + default_depends = self.types_fixed + self.types_fixed = None # PEP 612 reserves the right to define bound, covariant and contravariant arguments to # ParamSpec in a later PEP. If and when that happens, we should do something @@ -5065,12 +5118,14 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: name, self.qualified_name(name), self.object_type(), default, INVARIANT ) paramspec_var.line = call.line + paramspec_var.default_depends = default_depends call.analyzed = paramspec_var updated = True else: assert isinstance(call.analyzed, ParamSpecExpr) updated = default != call.analyzed.default call.analyzed.default = default + call.analyzed.default_depends = default_depends if has_placeholder(default): self.process_placeholder(None, "ParamSpec default", s, force_progress=updated) @@ -5093,6 +5148,8 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: self.fail('Too many positional arguments for "TypeVarTuple"', s) default: Type = AnyType(TypeOfAny.from_omitted_generics) + if self.types_fixed is not None: + self.types_fixed.clear() for param_value, param_name in zip( call.args[1 + n_values :], call.arg_names[1 + n_values :] ): @@ -5111,6 +5168,9 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: else: self.fail(f'Unexpected keyword argument "{param_name}" for "TypeVarTuple"', s) + default_depends = self.types_fixed + self.types_fixed = None + name = self.extract_typevarlike_name(s, call) if name is None: return False @@ -5128,12 +5188,14 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: INVARIANT, ) typevartuple_var.line = call.line + typevartuple_var.default_depends = default_depends call.analyzed = typevartuple_var updated = True else: assert isinstance(call.analyzed, TypeVarTupleExpr) updated = default != call.analyzed.default call.analyzed.default = default + call.analyzed.default_depends = default_depends if has_placeholder(default): self.process_placeholder(None, "TypeVarTuple default", s, force_progress=updated) @@ -5665,7 +5727,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: return tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, indexed = self.analyze_alias( + res, alias_tvars, depends_on, indexed, default_depends = self.analyze_alias( s.name.name, s.value.expr(), allow_placeholder=True, @@ -5692,13 +5754,10 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: self.mark_incomplete(s.name.name, s.value, becomes_typeinfo=True) return - # Now go through all new variables and temporary replace all tvars that still - # refer to some placeholders. We defer the whole alias and will revisit it again, - # as well as all its dependents. - for i, tv in enumerate(alias_tvars): - if has_placeholder(tv): - self.mark_incomplete(s.name.name, s.value, becomes_typeinfo=True) - alias_tvars[i] = self._trivial_typevarlike_like(tv) + if any(has_placeholder(tv) for tv in alias_tvars): + # Defer the alias if some type variables are not ready, same as for classes. + # Note: progress is forced below (if needed). + self.defer() self.add_type_alias_deps(depends_on) check_for_explicit_any( @@ -5724,6 +5783,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: eager=eager, python_3_12_type_alias=True, ) + alias_node.default_depends = default_depends s.alias_node = alias_node if ( @@ -5740,6 +5800,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res + existing.node.default_depends = default_depends existing.node.alias_tvars = alias_tvars updated = True # Invalidate recursive status cache in case it was previously set. @@ -5766,46 +5827,6 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: finally: self.pop_type_args(s.type_args) - def _trivial_typevarlike_like(self, tv: TypeVarLikeType) -> TypeVarLikeType: - object_type = self.named_type("builtins.object") - if isinstance(tv, TypeVarType): - return TypeVarType( - tv.name, - tv.fullname, - tv.id, - values=[], - upper_bound=object_type, - default=AnyType(TypeOfAny.from_omitted_generics), - variance=tv.variance, - line=tv.line, - column=tv.column, - ) - elif isinstance(tv, TypeVarTupleType): - tuple_type = self.named_type("builtins.tuple", [object_type]) - return TypeVarTupleType( - tv.name, - tv.fullname, - tv.id, - upper_bound=tuple_type, - tuple_fallback=tuple_type, - default=AnyType(TypeOfAny.from_omitted_generics), - line=tv.line, - column=tv.column, - ) - elif isinstance(tv, ParamSpecType): - return ParamSpecType( - tv.name, - tv.fullname, - tv.id, - flavor=tv.flavor, - upper_bound=object_type, - default=AnyType(TypeOfAny.from_omitted_generics), - line=tv.line, - column=tv.column, - ) - else: - assert False, f"Unknown TypeVarLike: {tv!r}" - # # Expressions # @@ -7685,6 +7706,7 @@ def expr_to_analyzed_type( allow_param_spec_literals: bool = False, allow_unpack: bool = False, unique_name: str | None = None, + analyzing_tvar_def: bool = False, ) -> Type | None: if unique_name is not None and isinstance(expr, CallExpr): # This is a legacy syntax intended mostly for Python 2, we keep it for @@ -7716,6 +7738,7 @@ def expr_to_analyzed_type( allow_unbound_tvars=allow_unbound_tvars, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, + analyzing_tvar_def=analyzing_tvar_def, ) def analyze_type_expr(self, expr: Expression) -> None: @@ -7743,6 +7766,7 @@ def type_analyzer( prohibit_self_type: str | None = None, prohibit_special_class_field_types: str | None = None, allow_type_any: bool = False, + analyzing_tvar_def: bool = False, ) -> TypeAnalyser: if tvar_scope is None: tvar_scope = self.tvar_scope @@ -7764,6 +7788,7 @@ def type_analyzer( prohibit_self_type=prohibit_self_type, prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, + analyzing_tvar_def=analyzing_tvar_def, ) tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic()) tpan.global_scope = not self.type and not self.function_stack @@ -7790,6 +7815,7 @@ def anal_type( prohibit_self_type: str | None = None, prohibit_special_class_field_types: str | None = None, allow_type_any: bool = False, + analyzing_tvar_def: bool = False, ) -> Type | None: """Semantically analyze a type. @@ -7827,6 +7853,7 @@ def anal_type( prohibit_self_type=prohibit_self_type, prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, + analyzing_tvar_def=analyzing_tvar_def, ) tag = self.track_incomplete_refs() typ = typ.accept(a) diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index 6261c7f011eb3..f4ec7eb2c5d55 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -24,6 +24,7 @@ SymbolNode, SymbolTable, SymbolTableNode, + TypeAlias, TypeInfo, ) from mypy.plugin import SemanticAnalyzerPluginInterface @@ -84,6 +85,10 @@ def lookup_fully_qualified(self, fullname: str, /) -> SymbolTableNode: def lookup_fully_qualified_or_none(self, fullname: str, /) -> SymbolTableNode | None: raise NotImplementedError + @abstractmethod + def record_fixed_type(self, fixed: TypeInfo | TypeAlias) -> None: + raise NotImplementedError + @abstractmethod def fail( self, diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 1b38481ba0004..d668121bc5b9c 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -595,7 +595,15 @@ def visit_type_alias_type(self, t: TypeAliasType, /) -> bool: elif t in self.seen_aliases: return self.default self.seen_aliases.add(t) - return get_proper_type(t).accept(self) + res = get_proper_type(t).accept(self) + # This is a weird edge case: if a type alias has unused type variables, we + # should visit arguments even if we didn't find anything in the expansion. + # As an optimization, do this only for new style type aliases. + assert t.alias is not None + if self.strategy == ANY_STRATEGY: + return res or (t.alias.python_3_12_type_alias and self.query_types(t.args)) + else: + return res and (not t.alias.python_3_12_type_alias or self.query_types(t.args)) def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool: """Perform a query for a sequence of types using the strategy to combine the results.""" diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 02b96afa8c170..3d0bda77fe392 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -222,6 +222,7 @@ def __init__( allowed_alias_tvars: list[TypeVarLikeType] | None = None, allow_type_any: bool = False, alias_type_params_names: list[str] | None = None, + analyzing_tvar_def: bool = False, ) -> None: self.api = api self.fail_func = api.fail @@ -268,6 +269,8 @@ def __init__( self.allow_type_any = allow_type_any self.allow_type_var_tuple = False self.allow_unpack = allow_unpack + # Set when we are analyzing a default of a type variable. + self.analyzing_tvar_def = analyzing_tvar_def def lookup_qualified( self, name: str, ctx: Context, suppress_errors: bool = False @@ -473,17 +476,22 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) an_args = self.pack_paramspec_args(an_args, t.empty_tuple_index) disallow_any = self.options.disallow_any_generics and not self.is_typeshed_stub - res = instantiate_type_alias( + res, used_default = instantiate_type_alias( node, an_args, self.fail, + self.note, node.no_args, t, self.options, unexpanded_type=t, disallow_any=disallow_any, empty_tuple_index=t.empty_tuple_index, + analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default and isinstance(res, TypeAliasType): + assert res.alias is not None + self.api.record_fixed_type(res.alias) # The only case where instantiate_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. if ( @@ -492,7 +500,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) and not (self.defining_alias and self.nesting_level == 0) and not validate_instance(res, self.fail, t.empty_tuple_index) ): - fix_instance( + used_default = fix_instance( res, self.fail, self.note, @@ -500,7 +508,10 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) options=self.options, use_generic_error=True, unexpanded_type=t, + analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + self.api.record_fixed_type(res.type) if node.eager: res = get_proper_type(res) return res @@ -877,30 +888,40 @@ def analyze_type_with_type_info( if not (self.defining_alias and self.nesting_level == 0) and not validate_instance( instance, self.fail, empty_tuple_index ): - fix_instance( + used_default = fix_instance( instance, self.fail, self.note, disallow_any=self.options.disallow_any_generics and not self.is_typeshed_stub, options=self.options, + analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + self.api.record_fixed_type(info) tup = info.tuple_type if tup is not None: # The class has a Tuple[...] base class so it will be # represented as a tuple type. if info.special_alias: - return instantiate_type_alias( + res, used_default = instantiate_type_alias( info.special_alias, # TODO: should we allow NamedTuples generic in ParamSpec? self.anal_array(args, allow_unpack=True), self.fail, + self.note, False, ctx, self.options, use_standard_error=True, empty_tuple_index=empty_tuple_index, + analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + # For convenience, we make default depend on the original TypeInfo, + # *not* on the special alias. + self.api.record_fixed_type(info) + return res return tup.copy_modified( items=self.anal_array(tup.items, allow_unpack=True), fallback=instance ) @@ -909,16 +930,23 @@ def analyze_type_with_type_info( # The class has a TypedDict[...] base class so it will be # represented as a typeddict type. if info.special_alias: - return instantiate_type_alias( + res, used_default = instantiate_type_alias( info.special_alias, # TODO: should we allow TypedDicts generic in ParamSpec? self.anal_array(args, allow_unpack=True), self.fail, + self.note, False, ctx, self.options, use_standard_error=True, + analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + # For convenience, we make default depend on the original TypeInfo, + # *not* on the special alias. + self.api.record_fixed_type(info) + return res # Create a named TypedDictType return td.copy_modified( item_types=self.anal_array(list(td.items.values())), fallback=instance @@ -2027,6 +2055,7 @@ def get_omitted_any( options: Options, fullname: str | None = None, unexpanded_type: Type | None = None, + used_default: bool = False, ) -> AnyType: if disallow_any: typ = unexpanded_type or orig_type @@ -2037,6 +2066,8 @@ def get_omitted_any( typ, code=codes.TYPE_ARG, ) + if used_default: + note(message_registry.NO_CYCLIC_DEFAULT, typ, code=codes.TYPE_ARG) any_type = AnyType(TypeOfAny.from_error, line=typ.line, column=typ.column) else: @@ -2066,11 +2097,13 @@ def fix_instance( options: Options, use_generic_error: bool = False, unexpanded_type: Type | None = None, -) -> None: + analyzing_tvar_def: bool = False, +) -> bool: """Fix a malformed instance by replacing all type arguments with TypeVar default or Any. Also emit a suitable error if this is not due to implicit Any's. """ + used_default = False arg_count = len(t.args) min_tv_count = sum(not tv.has_default() for tv in t.type.defn.type_vars) max_tv_count = len(t.type.type_vars) @@ -2089,15 +2122,33 @@ def fix_instance( if tv is None: continue if arg is None: + use_any = False if tv.has_default(): arg = tv.default + if analyzing_tvar_def: + # Record the use of default only when analyzing another default. + used_default = True + if is_typevar_default_recursive(tv.fullname, t.type): + # If this results in infinite recursion, use Any instead. + use_any = True else: + use_any = True + if use_any: if any_type is None: fullname = None if use_generic_error else t.type.fullname any_type = get_omitted_any( - disallow_any, fail, note, t, options, fullname, unexpanded_type + disallow_any, + fail, + note, + t, + options, + fullname, + unexpanded_type, + used_default, ) arg = any_type + else: + assert arg is not None with state.strict_optional_set(options.strict_optional): # Gradually expand defaults, as they may depend on previous variables. if tv.has_default(): @@ -2108,12 +2159,14 @@ def fix_instance( env[tv.id] = arg t.args = tuple(args) fix_type_var_tuple_argument(t) + return used_default def instantiate_type_alias( node: TypeAlias, args: list[Type], fail: MsgCallback, + note: MsgCallback, no_args: bool, ctx: Context, options: Options, @@ -2122,7 +2175,8 @@ def instantiate_type_alias( disallow_any: bool = False, use_standard_error: bool = False, empty_tuple_index: bool = False, -) -> Type: + analyzing_tvar_def: bool = False, +) -> tuple[Type, bool]: """Create an instance of a (generic) type alias from alias node and type arguments. We are following the rules outlined in TypeAlias docstring. @@ -2169,15 +2223,17 @@ def instantiate_type_alias( options, disallow_any=disallow_any, fail=fail, + note=note, unexpanded_type=unexpanded_type, + analyzing_tvar_def=analyzing_tvar_def, ) if max_tv_count == 0 and act_len == 0: if no_args: assert isinstance(node.target, Instance) # type: ignore[misc] # Note: this is the only case where we use an eager expansion. See more info about # no_args aliases like L = List in the docstring for TypeAlias class. - return Instance(node.target.type, [], line=ctx.line, column=ctx.column) - return TypeAliasType(node, [], line=ctx.line, column=ctx.column) + return Instance(node.target.type, [], line=ctx.line, column=ctx.column), False + return TypeAliasType(node, [], line=ctx.line, column=ctx.column), False if ( max_tv_count == 0 and act_len > 0 @@ -2189,7 +2245,7 @@ def instantiate_type_alias( tp.column = ctx.column tp.end_line = ctx.end_line tp.end_column = ctx.end_column - return tp + return tp, False if node.tvar_tuple_index is None: if any(isinstance(a, UnpackType) for a in args): # A variadic unpack in fixed size alias (fixed unpacks must be flattened by the caller) @@ -2235,7 +2291,18 @@ def instantiate_type_alias( ) fail(msg, ctx, code=codes.TYPE_ARG) args = [] - return set_any_tvars(node, args, ctx.line, ctx.column, options, from_error=True) + return set_any_tvars( + node, + args, + ctx.line, + ctx.column, + options, + disallow_any=disallow_any, + fail=fail, + note=note, + from_error=not correct, + analyzing_tvar_def=analyzing_tvar_def, + ) elif node.tvar_tuple_index is not None: # We also need to check if we are not performing a type variable tuple split. unpack = find_unpack_in_list(args) @@ -2262,8 +2329,8 @@ def instantiate_type_alias( ): exp = get_proper_type(typ) assert isinstance(exp, Instance) - return exp.args[-1] - return typ + return exp.args[-1], False + return typ, False def set_any_tvars( @@ -2277,8 +2344,11 @@ def set_any_tvars( disallow_any: bool = False, special_form: bool = False, fail: MsgCallback | None = None, + note: MsgCallback | None = None, unexpanded_type: Type | None = None, -) -> TypeAliasType: + analyzing_tvar_def: bool = False, +) -> tuple[TypeAliasType, bool]: + used_default = False if from_error or disallow_any: type_of_any = TypeOfAny.from_error elif special_form: @@ -2295,6 +2365,12 @@ def set_any_tvars( if arg is None: if tv.has_default(): arg = tv.default + # Same as for instances, record and avoid infinite recursion. + if analyzing_tvar_def: + used_default = True + if is_typevar_default_recursive(tv.fullname, node): + arg = any_type + used_any_type = True else: arg = any_type used_any_type = True @@ -2311,7 +2387,7 @@ def set_any_tvars( env[tv.id] = arg t = TypeAliasType(node, args, newline, newcolumn) - if used_any_type and disallow_any and node.alias_tvars: + if used_any_type and disallow_any and node.alias_tvars and not from_error: assert fail is not None if unexpanded_type: type_str = ( @@ -2327,7 +2403,34 @@ def set_any_tvars( Context(newline, newcolumn), code=codes.TYPE_ARG, ) - return t + if used_default: + assert note is not None + note( + message_registry.NO_CYCLIC_DEFAULT, + Context(newline, newcolumn), + code=codes.TYPE_ARG, + ) + return t, used_default + + +def is_typevar_default_recursive(tv_fname: str, start: TypeInfo | TypeAlias) -> bool: + """Check if the type variable can lead to infinite recursion via defaults.""" + if tv_fname not in start.default_depends: + return False + todo = start.default_depends[tv_fname].copy() + seen: set[TypeAlias | TypeInfo] = set() + while todo: + node = todo.pop() + if node is start: + return True + if node in seen: + # We don't return True here, since we are interested only in + # recursion via the original type variable. + continue + seen.add(node) + for dep_nodes in node.default_depends.values(): + todo |= dep_nodes + return False class DivergingAliasDetector(TrivialSyntheticTypeTranslator): diff --git a/mypy/types.py b/mypy/types.py index 5a05962dc4802..07b0a4f64ae11 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -699,6 +699,7 @@ def __eq__(self, other: object) -> bool: self.id == other.id and self.upper_bound == other.upper_bound and self.values == other.values + and self.default == other.default ) def serialize(self) -> JsonDict: @@ -854,7 +855,12 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, ParamSpecType): return NotImplemented # Upper bound can be ignored, since it's determined by flavor. - return self.id == other.id and self.flavor == other.flavor and self.prefix == other.prefix + return ( + self.id == other.id + and self.flavor == other.flavor + and self.prefix == other.prefix + and self.default == other.default + ) def serialize(self) -> JsonDict: assert not self.id.is_meta_var() @@ -1003,7 +1009,9 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: if not isinstance(other, TypeVarTupleType): return NotImplemented - return self.id == other.id and self.min_len == other.min_len + return ( + self.id == other.id and self.min_len == other.min_len and self.default == other.default + ) def copy_modified( self, diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index a281218af58e0..dd4687181ca41 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2717,3 +2717,34 @@ if ( or z is None ): pass + +[case testRecursiveTypeVarDefaultMutualDisallow] +# flags: --disallow-any-generic +from typing import TypeVar, Generic + +class C(Generic["T"]): + pass + +class D(Generic["S"]): + pass + +T = TypeVar("T", default=D) # E: Missing type arguments for generic type "D" \ + # N: Cyclic type variable defaults are not supported +S = TypeVar("S", default=C) # E: Missing type arguments for generic type "C" \ + # N: Cyclic type variable defaults are not supported + +c: C +d: D +reveal_type(c) # N: Revealed type is "__main__.C[__main__.D[Any]]" +reveal_type(d) # N: Revealed type is "__main__.D[__main__.C[Any]]" + +[case testRecursiveTypeVarDefaultOnlyAliasDisallow] +# flags: --disallow-any-generic +from typing import TypeVar + +T = TypeVar("T", default="A") # E: Missing type arguments for generic type "A" \ + # N: Cyclic type variable defaults are not supported +A = list[T] +a: A +reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 9f9122b2dfb1b..497b293e48a3a 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -365,3 +365,89 @@ type Result[T, E] = Ok[T, E] | Err[E, T] class Bar[U]: def foo(data: U, cond: bool) -> Result[U, str]: return Ok(data) if cond else Err("Error") + +[case testRecursiveTypeVarDefaultBasicNewStyle] +class C[T: C = C]: + pass + +c: C +reveal_type(c) # N: Revealed type is "__main__.C[__main__.C[Any]]" + +[case testRecursiveTypeVarDefaultMutualNewStyle] +class C[T = D]: + pass + +class D[S = C]: + pass + +c: C +d: D +reveal_type(c) # N: Revealed type is "__main__.C[__main__.D[Any]]" +reveal_type(d) # N: Revealed type is "__main__.D[__main__.C[Any]]" + +[case testNonRecursiveSimpleTypeVarDefaultNewStyle] +class Child[S = Parent]: ... + +class Parent[T = int]: ... + +reveal_type(Child()) # N: Revealed type is "__main__.Child[__main__.Parent[builtins.int]]" + +[case testNonRecursiveTypeVarDefaultImportCycleClassNewStyle] +import exp +[file exp.pyi] +import ind + +class F[T: D = D]: + x: T + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +class Ind(F): ... +x: Ind +reveal_type(x.x) # N: Revealed type is "exp.D" + +[case testNonRecursiveTypeVarDefaultImportCycleAliasNewStyle] +import exp +[file exp.pyi] +import ind + +type F[T: D = D] = list[T] + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +type Ind = list[F] +x: Ind +reveal_type(x) # N: Revealed type is "builtins.list[builtins.list[exp.D]]" + +[case testRecursiveTypeVarDefaultClassAndAliasNewStyle] +class Trait[T = Pattern]: + pass + +class Pattern1(Trait): + pass +class Pattern2(Trait): + pass + +type Pattern = Pattern1 | Pattern2 + +reveal_type(Trait()) # N: Revealed type is "__main__.Trait[__main__.Pattern1 | __main__.Pattern2]" +[builtins fixtures/tuple.pyi] + +[case testRecursiveTypeVarDefaultOnlyAliasNewStyle] +type A[T = A] = list[T] +a: A +reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" +[builtins fixtures/tuple.pyi] + +[case testRecursiveAliasTypeVarDefaultNewStyle] +type A[T = A] = int +a: A +reveal_type(a) # N: Revealed type is "builtins.int" diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index bde73ab533510..2f454b70747f9 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -962,3 +962,107 @@ reveal_type(D) # N: Revealed type is "def [T2 = Any, T1 = Any] () -> __main__.D d: D reveal_type(d) # N: Revealed type is "__main__.D[Any, Any]" [builtins fixtures/tuple.pyi] + +[case testRecursiveTypeVarDefaultBasic] +from typing import TypeVar, Generic + +class C(Generic["T"]): + pass + +T = TypeVar("T", bound=C, default=C) + +c: C +reveal_type(c) # N: Revealed type is "__main__.C[__main__.C[Any]]" + +[case testRecursiveTypeVarDefaultMutual] +from typing import TypeVar, Generic + +class C(Generic["T"]): + pass + +class D(Generic["S"]): + pass + +T = TypeVar("T", default=D) +S = TypeVar("S", default=C) + +c: C +d: D +reveal_type(c) # N: Revealed type is "__main__.C[__main__.D[Any]]" +reveal_type(d) # N: Revealed type is "__main__.D[__main__.C[Any]]" + +[case testNonRecursiveSimpleTypeVarDefault] +from typing import TypeVar, Generic + +S = TypeVar("S", default="Parent") +class Child(Generic[S]): ... + +T = TypeVar("T", default=int) +class Parent(Generic[T]): ... + +reveal_type(Child()) # N: Revealed type is "__main__.Child[__main__.Parent[builtins.int]]" + +[case testNonRecursiveTypeVarDefaultImportCycleClass] +import exp +[file exp.pyi] +from typing import Generic, TypeVar +import ind + +T = TypeVar("T", bound=D, default=D) +class F(Generic[T]): + x: T + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +class Ind(F): ... +x: Ind +reveal_type(x.x) # N: Revealed type is "exp.D" + +[case testNonRecursiveTypeVarDefaultImportCycleAlias] +import exp +[file exp.pyi] +from typing import TypeVar +import ind + +T = TypeVar("T", bound=D, default=D) +F = list[T] + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +Ind = list[F] +x: Ind +reveal_type(x) # N: Revealed type is "builtins.list[builtins.list[exp.D]]" + +[case testRecursiveTypeVarDefaultClassAndAlias] +from typing import Generic, TypeVar, Union + +T = TypeVar("T", default="Pattern") + +class Trait(Generic[T]): + pass + +class Pattern1(Trait): + pass + +Pattern = Union[Pattern1, None] + +reveal_type(Trait()) # N: Revealed type is "__main__.Trait[__main__.Pattern1 | None]" +[builtins fixtures/tuple.pyi] + +[case testRecursiveTypeVarDefaultOnlyAlias] +from typing import TypeVar + +T = TypeVar("T", default="A") + +A = list[T] +a: A +reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" +[builtins fixtures/tuple.pyi] From 965dd31224bc2a0694e7343f927ff9c164b4b673 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Tue, 26 May 2026 19:36:10 +0300 Subject: [PATCH 045/127] [mypyc] Add `librt.strings.isidentifier` codepoint primitive (#21522) 5th PR of https://github.com/python/mypy/issues/21418 --- mypy/typeshed/stubs/librt/librt/strings.pyi | 1 + mypyc/ir/deps.py | 1 - mypyc/lib-rt/codepoint_extra_ops.c | 8 ---- mypyc/lib-rt/codepoint_extra_ops.h | 28 ------------- mypyc/lib-rt/strings/librt_strings.c | 19 +++++---- mypyc/lib-rt/strings/librt_strings.h | 45 +++++++++++++++++++++ mypyc/primitives/librt_strings_ops.py | 27 ++++++++----- mypyc/test-data/irbuild-librt-strings.test | 14 +++++++ mypyc/test-data/run-librt-strings.test | 5 ++- 9 files changed, 90 insertions(+), 58 deletions(-) delete mode 100644 mypyc/lib-rt/codepoint_extra_ops.c delete mode 100644 mypyc/lib-rt/codepoint_extra_ops.h diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 01aee3ff758d3..7a028f9e7859e 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -47,3 +47,4 @@ def isspace(c: i32, /) -> bool: ... def isdigit(c: i32, /) -> bool: ... def isalnum(c: i32, /) -> bool: ... def isalpha(c: i32, /) -> bool: ... +def isidentifier(c: i32, /) -> bool: ... diff --git a/mypyc/ir/deps.py b/mypyc/ir/deps.py index 0cf58c83c27bf..751845d3a324c 100644 --- a/mypyc/ir/deps.py +++ b/mypyc/ir/deps.py @@ -116,5 +116,4 @@ def get_header(self) -> str: STRING_WRITER_EXTRA_OPS: Final = SourceDep("stringwriter_extra_ops.c") BYTEARRAY_EXTRA_OPS: Final = SourceDep("bytearray_extra_ops.c") STR_EXTRA_OPS: Final = SourceDep("str_extra_ops.c") -CODEPOINT_EXTRA_OPS: Final = SourceDep("codepoint_extra_ops.c") VECS_EXTRA_OPS: Final = SourceDep("vecs_extra_ops.c") diff --git a/mypyc/lib-rt/codepoint_extra_ops.c b/mypyc/lib-rt/codepoint_extra_ops.c deleted file mode 100644 index ca03eba4e6f51..0000000000000 --- a/mypyc/lib-rt/codepoint_extra_ops.c +++ /dev/null @@ -1,8 +0,0 @@ -#include "codepoint_extra_ops.h" - -// Out-of-line bodies for codepoint helpers that are too large to inline. -// The classification helpers and the ASCII fast paths for case conversion -// stay inline in codepoint_extra_ops.h; this file holds the slow paths -// that round-trip through PyUnicode_FromOrdinal and CPython's Unicode -// machinery. Currently empty; populated as later commits add -// isidentifier, toupper, and tolower. diff --git a/mypyc/lib-rt/codepoint_extra_ops.h b/mypyc/lib-rt/codepoint_extra_ops.h deleted file mode 100644 index bb83f92e4b87c..0000000000000 --- a/mypyc/lib-rt/codepoint_extra_ops.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef MYPYC_CODEPOINT_EXTRA_OPS_H -#define MYPYC_CODEPOINT_EXTRA_OPS_H - -#include -#include -#include - -// Codepoint helpers for librt.strings. -// Inputs are signed int32_t for compatibility with mypyc's i32 type. -// Negative values are treated as non-codepoints and return false. - -static inline bool LibRTStrings_IsSpace(int32_t c) { - return c >= 0 && Py_UNICODE_ISSPACE((Py_UCS4)c); -} - -static inline bool LibRTStrings_IsDigit(int32_t c) { - return c >= 0 && Py_UNICODE_ISDIGIT((Py_UCS4)c); -} - -static inline bool LibRTStrings_IsAlnum(int32_t c) { - return c >= 0 && Py_UNICODE_ISALNUM((Py_UCS4)c); -} - -static inline bool LibRTStrings_IsAlpha(int32_t c) { - return c >= 0 && Py_UNICODE_ISALPHA((Py_UCS4)c); -} - -#endif // MYPYC_CODEPOINT_EXTRA_OPS_H diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index cbc3e5f753fa6..d95d5afb48600 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -4,7 +4,6 @@ #include #include #include "CPy.h" -#include "codepoint_extra_ops.h" #include "librt_strings.h" #define CPY_BOOL_ERROR 2 @@ -1154,15 +1153,11 @@ read_f64_be(PyObject *module, PyObject *const *args, size_t nargs) { return PyFloat_FromDouble(CPyBytes_ReadF64BEUnsafe(data + index)); } -// Codepoint classification helpers exposed to interpreted callers. -// The C-side names are prefixed `cp_` to avoid colliding with libc's -// isspace / isdigit / etc. Compiled callers go through the -// LibRTStrings_* static inlines in codepoint_extra_ops.h instead. -// -// All wrappers parse a single int argument as i32 (codepoint) and -// dispatch to the corresponding LibRTStrings_* function. The parse -// step accepts any int but rejects values outside the i32 range with -// OverflowError, matching the input domain of the compiled fast path. +// Python-level wrappers (`cp_*`) for interpreted callers. The C-side names +// are prefixed `cp_` to avoid colliding with libc's isspace etc. +// The LibRTStrings_Is* helpers themselves are static inline in librt_strings.h +// so they compile directly into mypyc-emitted code with no capsule +// indirection. // Parse a Python int as i32 codepoint. Returns 0 on success and writes // the value to *out; returns -1 on error with a Python exception set. @@ -1194,6 +1189,7 @@ DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace) DEFINE_CP_BOOL_WRAPPER(isdigit, LibRTStrings_IsDigit) DEFINE_CP_BOOL_WRAPPER(isalnum, LibRTStrings_IsAlnum) DEFINE_CP_BOOL_WRAPPER(isalpha, LibRTStrings_IsAlpha) +DEFINE_CP_BOOL_WRAPPER(isidentifier, LibRTStrings_IsIdentifier) static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, @@ -1268,6 +1264,9 @@ static PyMethodDef librt_strings_module_methods[] = { {"isalpha", cp_isalpha, METH_O, PyDoc_STR("Test whether a codepoint (i32) is a Unicode letter.") }, + {"isidentifier", cp_isidentifier, METH_O, + PyDoc_STR("Test whether a codepoint (i32) is a valid identifier start (XID_Start).") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/lib-rt/strings/librt_strings.h b/mypyc/lib-rt/strings/librt_strings.h index e6236f7950929..c3cbd2f2237a6 100644 --- a/mypyc/lib-rt/strings/librt_strings.h +++ b/mypyc/lib-rt/strings/librt_strings.h @@ -3,6 +3,8 @@ #include #include +#include +#include "CPy.h" #include "librt_strings_common.h" // ABI version -- only an exact match is compatible. This will only be changed in @@ -28,4 +30,47 @@ typedef struct { char data[WRITER_EMBEDDED_BUF_LEN]; // Default buffer } StringWriterObject; +// Codepoint classification helpers. Inputs are signed i32 for compatibility +// with mypyc's int32_rprimitive; negative values are non-codepoints and +// return false. Defined `static inline` so they compile statically into +// both the librt.strings module and any mypyc-compiled extension that +// includes this header, avoiding the capsule indirection that would dwarf +// the work of a single Py_UNICODE_IS* macro call. + +static inline bool LibRTStrings_IsSpace(int32_t c) { + return c >= 0 && Py_UNICODE_ISSPACE((Py_UCS4)c); +} + +static inline bool LibRTStrings_IsDigit(int32_t c) { + return c >= 0 && Py_UNICODE_ISDIGIT((Py_UCS4)c); +} + +static inline bool LibRTStrings_IsAlnum(int32_t c) { + return c >= 0 && Py_UNICODE_ISALNUM((Py_UCS4)c); +} + +static inline bool LibRTStrings_IsAlpha(int32_t c) { + return c >= 0 && Py_UNICODE_ISALPHA((Py_UCS4)c); +} + +// True if c could start a valid identifier (XID_Start, per PEP 3131). +// ASCII fast path covers `[A-Za-z_]`; non-ASCII delegates to CPython's +// PyUnicode_IsIdentifier on a 1-character string. Aborts via +// CPyError_OutOfMemory on allocation failure to keep this ERR_NEVER. +static inline bool LibRTStrings_IsIdentifier(int32_t c) { + if (c < 0) return false; + if (c < 128) { + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_'; + } + PyObject *s = PyUnicode_FromOrdinal((int)c); + if (s == NULL) { + CPyError_OutOfMemory(); + } + int r = PyUnicode_IsIdentifier(s); + Py_DECREF(s); + return r == 1; +} + #endif // LIBRT_STRINGS_H diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index 93fa717cf5290..f025c6e95b718 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -1,9 +1,4 @@ -from mypyc.ir.deps import ( - BYTES_WRITER_EXTRA_OPS, - CODEPOINT_EXTRA_OPS, - LIBRT_STRINGS, - STRING_WRITER_EXTRA_OPS, -) +from mypyc.ir.deps import BYTES_WRITER_EXTRA_OPS, LIBRT_STRINGS, STRING_WRITER_EXTRA_OPS from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER from mypyc.ir.rtypes import ( bool_rprimitive, @@ -402,7 +397,7 @@ return_type=bool_rprimitive, c_function_name="LibRTStrings_IsSpace", error_kind=ERR_NEVER, - dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], + dependencies=[LIBRT_STRINGS], ) function_op( @@ -411,7 +406,7 @@ return_type=bool_rprimitive, c_function_name="LibRTStrings_IsDigit", error_kind=ERR_NEVER, - dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], + dependencies=[LIBRT_STRINGS], ) function_op( @@ -420,7 +415,7 @@ return_type=bool_rprimitive, c_function_name="LibRTStrings_IsAlnum", error_kind=ERR_NEVER, - dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], + dependencies=[LIBRT_STRINGS], ) function_op( @@ -429,5 +424,17 @@ return_type=bool_rprimitive, c_function_name="LibRTStrings_IsAlpha", error_kind=ERR_NEVER, - dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], + dependencies=[LIBRT_STRINGS], +) + +# isidentifier checks XID_Start semantics for a single codepoint, matching +# str.isidentifier() on a 1-character string. The non-ASCII path allocates +# and aborts via CPyError_OutOfMemory on failure, so this stays ERR_NEVER. +function_op( + name="librt.strings.isidentifier", + arg_types=[int32_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTStrings_IsIdentifier", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS], ) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index e5d18b6eb8522..e3aaa49bd6f90 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -387,3 +387,17 @@ def is_a(c): L0: r0 = LibRTStrings_IsAlpha(c) return r0 + +[case testLibrtStringsIsIdentifierIR] +from librt.strings import isidentifier +from mypy_extensions import i32 + +def is_id(c: i32) -> bool: + return isidentifier(c) +[out] +def is_id(c): + c :: i32 + r0 :: bool +L0: + r0 = LibRTStrings_IsIdentifier(c) + return r0 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index aa38c713d3841..0a3320ff6522e 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1443,7 +1443,7 @@ def test_new_without_init_is_usable() -> None: [case testLibrtStringsCodepointClassifiers_librt] from typing import Any from mypy_extensions import i32 -from librt.strings import isspace, isdigit, isalnum, isalpha +from librt.strings import isspace, isdigit, isalnum, isalpha, isidentifier from testutil import assertRaises @@ -1455,6 +1455,7 @@ def test_codepoint_classifiers() -> None: assert not isdigit(bad) assert not isalnum(bad) assert not isalpha(bad) + assert not isidentifier(bad) # Verify each codepoint primitive agrees with the matching str method # across all Unicode codepoints, including the ord(chr(i)) round-trip. # Any forces generic dispatch on the str side. @@ -1466,6 +1467,7 @@ def test_codepoint_classifiers() -> None: assert isdigit(o) == isdigit(i) == a.isdigit() assert isalnum(o) == isalnum(i) == a.isalnum() assert isalpha(o) == isalpha(i) == a.isalpha() + assert isidentifier(o) == isidentifier(i) == a.isidentifier() def test_codepoint_classifiers_via_any() -> None: @@ -1476,6 +1478,7 @@ def test_codepoint_classifiers_via_any() -> None: (isdigit, "5", "a"), (isalnum, "A", " "), (isalpha, "A", " "), + (isidentifier, "A", "0"), ): f: Any = fn assert f(ord(true_input)) is True From 668733de95aa08fd5dbaa2760edbd0cffe859001 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 27 May 2026 01:00:44 +0100 Subject: [PATCH 046/127] Correctly handle empty tuple index when unpacked (#21545) Fixes https://github.com/python/mypy/issues/18390 Closes https://github.com/python/mypy/pull/18395 This is a more targeted fix than the original PR. Note we don't need to worry about other places with `flatten_nested_tuples()`, since the issue is only specific to (early) `UnboundType`. --- mypy/typeanal.py | 6 +++++ test-data/unit/check-python313.test | 29 ++++++++++++++++++++ test-data/unit/check-typevar-tuple.test | 35 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 3d0bda77fe392..014681d9132ec 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -884,7 +884,10 @@ def analyze_type_with_type_info( return AnyType(TypeOfAny.from_error) # Check type argument count. + old_args = instance.args instance.args = tuple(flatten_nested_tuples(instance.args)) + if old_args and not instance.args: + empty_tuple_index = True if not (self.defining_alias and self.nesting_level == 0) and not validate_instance( instance, self.fail, empty_tuple_index ): @@ -2192,7 +2195,10 @@ def instantiate_type_alias( # Type aliases are special, since they can be expanded during semantic analysis, # so we need to normalize them as soon as possible. # TODO: can this cause an infinite recursion? + old_args = args args = flatten_nested_tuples(args) + if old_args and not args: + empty_tuple_index = True if any(unknown_unpack(a) for a in args): # This type is not ready to be validated, because of unknown total count. # Note that we keep the kind of Any for consistency. diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 497b293e48a3a..38fa39d39493a 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -451,3 +451,32 @@ reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" type A[T = A] = int a: A reveal_type(a) # N: Revealed type is "builtins.int" + +[case testPEP695VariadicGenericClassEmptyTupleUnpackIndex] +class Blah[*Ts]: + pass + +x: Blah[()] +y: Blah[*tuple[()]] + +type Empty = tuple[()] +z: Blah[*Empty] + +reveal_type(x) # N: Revealed type is "__main__.Blah[()]" +reveal_type(y) # N: Revealed type is "__main__.Blah[()]" +reveal_type(z) # N: Revealed type is "__main__.Blah[()]" +[builtins fixtures/tuple.pyi] + +[case testPEP695VariadicGenericAliasEmptyTupleUnpackIndex] +type Blah[*Ts] = list[tuple[*Ts]] + +x: Blah[()] +y: Blah[*tuple[()]] + +type Empty = tuple[()] +z: Blah[*Empty] + +reveal_type(x) # N: Revealed type is "builtins.list[tuple[()]]" +reveal_type(y) # N: Revealed type is "builtins.list[tuple[()]]" +reveal_type(z) # N: Revealed type is "builtins.list[tuple[()]]" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 8d3f3cd805315..0119728e5834e 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2838,3 +2838,38 @@ def test(t: tuple[int, ...]) -> None: ... test(x) test(y) [builtins fixtures/tuple.pyi] + +[case testVariadicGenericClassEmptyTupleUnpackIndex] +from typing import Generic, TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +class Blah(Generic[Unpack[Ts]]): + pass + +x: Blah[()] +y: Blah[Unpack[tuple[()]]] + +Empty = tuple[()] +z: Blah[Unpack[Empty]] + +reveal_type(x) # N: Revealed type is "__main__.Blah[()]" +reveal_type(y) # N: Revealed type is "__main__.Blah[()]" +reveal_type(z) # N: Revealed type is "__main__.Blah[()]" +[builtins fixtures/tuple.pyi] + +[case testVariadicGenericAliasEmptyTupleUnpackIndex] +from typing import Generic, TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +Blah = list[tuple[Unpack[Ts]]] + +x: Blah[()] +y: Blah[Unpack[tuple[()]]] + +Empty = tuple[()] +z: Blah[Unpack[Empty]] + +reveal_type(x) # N: Revealed type is "builtins.list[tuple[()]]" +reveal_type(y) # N: Revealed type is "builtins.list[tuple[()]]" +reveal_type(z) # N: Revealed type is "builtins.list[tuple[()]]" +[builtins fixtures/tuple.pyi] From 938dbe2fe79359e6d97ff5319a40b61f7af446fd Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 27 May 2026 02:13:09 +0100 Subject: [PATCH 047/127] Respect explicit return type of __new__() (#21441) Fixes https://github.com/python/mypy/issues/8330 Fixes https://github.com/python/mypy/issues/15182 Closes https://github.com/python/mypy/pull/16020 Closes https://github.com/python/mypy/pull/14471 Fixes https://github.com/python/mypy/issues/13824 Fixes https://github.com/python/mypy/issues/14502 With this PR will still give an error if the explicit return `__new__()` is not a subtype of current class, but now we will actually use it. There are _two exceptions_ (to preserve backwards compatibility): * If the return type is `Any` with still use current class as the return type. * If the explicit return type comes from a superclass and is a supertype of implicit return type. ```python class A: def __new__(cls): ... reveal_type(A()) # still __main__.A class B: def __new__(cls) -> B: return cls() class C(B): ... reveal_type(C()) # still __main__.C ``` This uses a more principled implementation than some earlier attempts: adding a new dedicated attribute to `CallableType` for this purpose. Some comments: * This PR has a bit for boilerplate, but this is expected. When adding a new attribute to a type, one needs to update most visitors. * While doing the above I noticed that `CallableType.type_guard` and `CallableType.type_is` were not handled in dependency visitors (neither coarse-grained nor fine-grained). IMO they definitely should be handled there, so I now handle them. * I try to reduce the size of `CallableType` a bit to compensate for new attribute by removing (rarely used) `min_args` attribute. I also use compact flags serialization. * I skimmed the code base and updated all places where we "casually" use `CallableType.ret_type` for various type object edge cases. * Note that I don't assert that `instance_type` is set when `is_type_obj()` returns `True`. This is mostly to avoid breaking 3rd party plugins. * I didn't add a test case for each edge case, but I added (improved) test cases from https://github.com/python/mypy/pull/16020 plus some more. Suggestions for more test cases are welcome. * It looks like this exposes a pre-existing bug where we leaked type variables in `type[T]`. It is relatively niche edge case, but it looks important for NumPy stubs, so I am trying to fix it here. --- mypy/applytype.py | 6 + mypy/cache.py | 16 +- mypy/checker.py | 2 +- mypy/checkexpr.py | 18 +- mypy/checkmember.py | 21 +- mypy/constraints.py | 59 +++-- mypy/expandtype.py | 9 +- mypy/fixup.py | 2 + mypy/indirection.py | 6 + mypy/infer.py | 5 +- mypy/join.py | 8 + mypy/meet.py | 9 +- mypy/messages.py | 12 +- mypy/nodes.py | 16 +- mypy/plugins/singledispatch.py | 2 +- mypy/server/astdiff.py | 3 + mypy/server/astmerge.py | 6 + mypy/server/deps.py | 6 + mypy/subtypes.py | 25 ++- mypy/type_visitor.py | 19 +- mypy/typeops.py | 53 +++-- mypy/types.py | 95 +++++++-- mypy/typetraverser.py | 3 + test-data/unit/check-classes.test | 296 +++++++++++++++++++++++++- test-data/unit/check-generics.test | 9 + test-data/unit/check-incremental.test | 32 +++ 26 files changed, 627 insertions(+), 111 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index cde97a4e712fc..fbc93492f87e9 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -179,11 +179,17 @@ def apply_generic_arguments( assert isinstance(typ, TypeVarLikeType) remaining_tvars.append(typ) + instance_type = None + if callable.instance_type is not None: + instance_type = expand_type(callable.instance_type, id_to_type) + assert isinstance(instance_type, ProperType) + return callable.copy_modified( ret_type=expand_type(callable.ret_type, id_to_type), variables=remaining_tvars, type_guard=type_guard, type_is=type_is, + instance_type=instance_type, ) diff --git a/mypy/cache.py b/mypy/cache.py index b9cd8ad7a9050..e90c933fdab9a 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -69,7 +69,7 @@ from mypy_extensions import u8 # High-level cache layout format -CACHE_VERSION: Final = 8 +CACHE_VERSION: Final = 9 # Type used internally to represent errors: # (path, line, column, end_line, end_column, severity, message, code) @@ -558,6 +558,20 @@ def write_json(data: WriteBuffer, value: dict[str, Any]) -> None: write_json_value(data, value[key]) +def write_flags(data: WriteBuffer, flags: list[bool]) -> None: + assert len(flags) <= 26, "This many flags not supported yet" + packed = 0 + for i, flag in enumerate(flags): + if flag: + packed |= 1 << i + write_int(data, packed) + + +def read_flags(data: ReadBuffer, num_flags: int) -> list[bool]: + packed = read_int(data) + return [(packed & (1 << i)) != 0 for i in range(num_flags)] + + def write_errors(data: WriteBuffer, errs: list[ErrorTuple]) -> None: write_tag(data, LIST_GEN) write_int_bare(data, len(errs)) diff --git a/mypy/checker.py b/mypy/checker.py index b4ff39d49b80b..7dfdcb83a90b7 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5515,7 +5515,7 @@ def check_except_handler_test(self, n: Expression, is_star: bool) -> Type: if not item.is_type_obj(): self.fail(message_registry.INVALID_EXCEPTION_TYPE, n) return self.default_exception_type(is_star) - exc_type = erase_typevars(item.ret_type) + exc_type = erase_typevars(item.get_instance_type()) elif isinstance(ttype, TypeType): exc_type = ttype.item else: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6cdccd912055f..714ae12310094 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -124,7 +124,6 @@ from mypy.subtypes import ( covers_at_runtime, find_member, - is_equivalent, is_same_type, is_subtype, non_method_protocol_members, @@ -689,7 +688,7 @@ def method_fullname(self, object_type: Type, method_name: str) -> str | None: # For class method calls, object_type is a callable representing the class object. # We "unwrap" it to a regular type, as the class/instance method difference doesn't # affect the fully qualified name. - object_type = get_proper_type(object_type.ret_type) + object_type = object_type.get_instance_type() elif isinstance(object_type, TypeType): object_type = object_type.item @@ -717,9 +716,9 @@ def always_returns_none(self, node: Expression) -> bool: if isinstance(typ, Instance): info = typ.type elif isinstance(typ, CallableType) and typ.is_type_obj(): - ret_type = get_proper_type(typ.ret_type) - if isinstance(ret_type, Instance): - info = ret_type.type + instance_type = typ.get_instance_type(force_fallback=True) + if isinstance(instance_type, Instance): + info = instance_type.type else: return False else: @@ -1668,9 +1667,10 @@ def check_callable_call( callee = callee.with_unpacked_kwargs().with_normalized_var_args() if callable_name is None and callee.name: callable_name = callee.name - ret_type = get_proper_type(callee.ret_type) - if callee.is_type_obj() and isinstance(ret_type, Instance): - callable_name = ret_type.type.fullname + if callee.is_type_obj(): + instance_type = callee.get_instance_type(force_fallback=True) + if isinstance(instance_type, Instance): + callable_name = instance_type.type.fullname if isinstance(callable_node, RefExpr) and callable_node.fullname in ENUM_BASES: # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call(). return callee.ret_type, callee @@ -1867,7 +1867,7 @@ def check_callable_call( if ( callee.is_type_obj() and (len(arg_types) == 1) - and is_equivalent(callee.ret_type, self.named_type("builtins.type")) + and is_named_instance(callee.get_instance_type(), "builtins.type") ): callee = callee.copy_modified(ret_type=TypeType.make_normalized(arg_types[0])) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index b5dcf94a0b206..e75a8ed7a5b03 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -407,15 +407,8 @@ def validate_super_call(node: FuncBase, mx: MemberContext) -> None: def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: MemberContext) -> Type: # Class attribute. # TODO super? - ret_type = typ.items[0].ret_type - assert isinstance(ret_type, ProperType) - if isinstance(ret_type, TupleType): - ret_type = tuple_fallback(ret_type) - if isinstance(ret_type, TypedDictType): - ret_type = ret_type.fallback - if isinstance(ret_type, LiteralType): - ret_type = ret_type.fallback - if isinstance(ret_type, Instance): + instance_type = typ.items[0].get_instance_type(force_fallback=True) + if isinstance(instance_type, Instance): if not mx.is_operator: # When Python sees an operator (eg `3 == 4`), it automatically translates that # into something like `int.__eq__(3, 4)` instead of `(3).__eq__(4)` as an @@ -432,14 +425,18 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member # See https://github.com/python/mypy/pull/1787 for more info. # TODO: do not rely on same type variables being present in all constructor overloads. result = analyze_class_attribute_access( - ret_type, name, mx, original_vars=typ.items[0].variables, mcs_fallback=typ.fallback + instance_type, + name, + mx, + original_vars=typ.items[0].variables, + mcs_fallback=typ.fallback, ) if result: return result # Look up from the 'type' type. return _analyze_member_access(name, typ.fallback, mx) else: - assert False, f"Unexpected type {ret_type!r}" + assert False, f"Unexpected type {instance_type!r}" def analyze_type_type_member_access( @@ -721,7 +718,7 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: dunder_get_type = expand_type_by_instance(bound_method, typ) if isinstance(instance_type, FunctionLike) and instance_type.is_type_obj(): - owner_type = instance_type.items[0].ret_type + owner_type = instance_type.items[0].get_instance_type() instance_type = NoneType() elif isinstance(instance_type, TypeType): owner_type = instance_type.item diff --git a/mypy/constraints.py b/mypy/constraints.py index d20acccd09a80..48cc23f742227 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -9,6 +9,7 @@ import mypy.typeops from mypy.argmap import ArgTypeExpander from mypy.erasetype import erase_typevars +from mypy.expandtype import expand_type_by_instance from mypy.maptype import map_instance_to_supertype from mypy.nodes import ( ARG_OPT, @@ -275,7 +276,11 @@ def infer_constraints_for_callable( def infer_constraints( - template: Type, actual: Type, direction: int, skip_neg_op: bool = False + template: Type, + actual: Type, + direction: int, + skip_neg_op: bool = False, + erase_types: bool = True, ) -> list[Constraint]: """Infer type constraints. @@ -312,14 +317,14 @@ def infer_constraints( # Return early on an empty branch. return [] type_state.inferring.append((template, actual)) - res = _infer_constraints(template, actual, direction, skip_neg_op) + res = _infer_constraints(template, actual, direction, skip_neg_op, erase_types) type_state.inferring.pop() return res - return _infer_constraints(template, actual, direction, skip_neg_op) + return _infer_constraints(template, actual, direction, skip_neg_op, erase_types) def _infer_constraints( - template: Type, actual: Type, direction: int, skip_neg_op: bool + template: Type, actual: Type, direction: int, skip_neg_op: bool, erase_types: bool ) -> list[Constraint]: orig_template = template template = get_proper_type(template) @@ -424,7 +429,7 @@ def _infer_constraints( return [] # Remaining cases are handled by ConstraintBuilderVisitor. - return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op)) + return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op, erase_types)) def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]: @@ -659,7 +664,9 @@ class ConstraintBuilderVisitor(TypeVisitor[list[Constraint]]): # TODO: The value may be None. Is that actually correct? actual: ProperType - def __init__(self, actual: ProperType, direction: int, skip_neg_op: bool) -> None: + def __init__( + self, actual: ProperType, direction: int, skip_neg_op: bool, erase_types: bool + ) -> None: # Direction must be SUBTYPE_OF or SUPERTYPE_OF. self.actual = actual self.direction = direction @@ -667,6 +674,10 @@ def __init__(self, actual: ProperType, direction: int, skip_neg_op: bool) -> Non # this is used to prevent infinite recursion when both template and actual are # generic callables. self.skip_neg_op = skip_neg_op + # Normally we should erase generic actual type when inferring against type[T] + # to avoid leaking type variables, see testGenericClassAsArgumentToType. + # The only exception is self-types in generic classes, where we set this to False. + self.erase_types = erase_types # Trivial leaf types @@ -759,13 +770,11 @@ def visit_instance(self, template: Instance) -> list[Constraint]: and template.type.is_protocol and self.direction == SUPERTYPE_OF ): - ret_type = get_proper_type(actual.ret_type) - if isinstance(ret_type, TupleType): - ret_type = mypy.typeops.tuple_fallback(ret_type) - if isinstance(ret_type, Instance): + instance_type = actual.get_instance_type(force_fallback=True) + if isinstance(instance_type, Instance): res.extend( self.infer_constraints_from_protocol_members( - ret_type, template, ret_type, template, class_obj=True + instance_type, template, instance_type, template, class_obj=True ) ) actual = actual.fallback @@ -1213,6 +1222,20 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: elif isinstance(self.actual, Overloaded): return self.infer_against_overloaded(self.actual, template) elif isinstance(self.actual, TypeType): + # This matches the corresponding logic in subtypes.py. + item = self.actual.item + if isinstance(item, TupleType): + item = mypy.typeops.tuple_fallback(item) + if isinstance(item, Instance): + constructor = mypy.typeops.type_object_type(item.type) + constructor = expand_type_by_instance(constructor, item) + # Only consider return type to match historic behavior (see below). + if isinstance(constructor, CallableType): + return infer_constraints( + template.ret_type, constructor.ret_type, self.direction + ) + elif isinstance(constructor, Overloaded): + return self.infer_against_overloaded(constructor, template, ret_only=True) return infer_constraints(template.ret_type, self.actual.item, self.direction) elif isinstance(self.actual, Instance): # Instances with __call__ method defined are considered structural @@ -1228,7 +1251,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: return [] def infer_against_overloaded( - self, overloaded: Overloaded, template: CallableType + self, overloaded: Overloaded, template: CallableType, ret_only: bool = False ) -> list[Constraint]: # Create constraints by matching an overloaded type against a template. # This is tricky to do in general. We cheat by only matching against @@ -1236,6 +1259,8 @@ def infer_against_overloaded( # seems to work somewhat well, but we should really use a more # reliable technique. item = find_matching_overload_item(overloaded, template) + if ret_only: + return infer_constraints(template.ret_type, item.ret_type, self.direction) return infer_constraints(template, item, self.direction) def visit_tuple_type(self, template: TupleType) -> list[Constraint]: @@ -1398,8 +1423,18 @@ def visit_overloaded(self, template: Overloaded) -> list[Constraint]: def visit_type_type(self, template: TypeType) -> list[Constraint]: if isinstance(self.actual, CallableType): + if self.actual.is_type_obj(): + instance_type = self.actual.get_instance_type() + if self.erase_types: + instance_type = erase_typevars(instance_type) + return infer_constraints(template.item, instance_type, self.direction) return infer_constraints(template.item, self.actual.ret_type, self.direction) elif isinstance(self.actual, Overloaded): + if self.actual.is_type_obj(): + instance_type = self.actual.items[0].get_instance_type() + if self.erase_types: + instance_type = erase_typevars(instance_type) + return infer_constraints(template.item, instance_type, self.direction) return infer_constraints(template.item, self.actual.items[0].ret_type, self.direction) elif isinstance(self.actual, TypeType): return infer_constraints(template.item, self.actual.item, self.direction) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 33c6c6d4ae717..6aa18fb72c2f4 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -485,11 +485,16 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_types = self.interpolate_args_for_unpack(t, var_arg.typ) else: arg_types = self.expand_types(t.arg_types) + instance_type = None + if t.instance_type is not None: + instance_type = t.instance_type.accept(self) + assert isinstance(instance_type, ProperType) expanded = t.copy_modified( arg_types=arg_types, ret_type=t.ret_type.accept(self), - type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), - type_is=(t.type_is.accept(self) if t.type_is is not None else None), + type_guard=t.type_guard.accept(self) if t.type_guard is not None else None, + type_is=t.type_is.accept(self) if t.type_is is not None else None, + instance_type=instance_type, ) if needs_normalization: return expanded.with_normalized_var_args() diff --git a/mypy/fixup.py b/mypy/fixup.py index c0782610e8f40..48ed7c26d57ba 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -279,6 +279,8 @@ def visit_callable_type(self, ct: CallableType) -> None: ct.type_guard.accept(self) if ct.type_is is not None: ct.type_is.accept(self) + if ct.instance_type is not None: + ct.instance_type.accept(self) def visit_overloaded(self, t: Overloaded) -> None: for ct in t.items: diff --git a/mypy/indirection.py b/mypy/indirection.py index c5f3fa89b8c4a..6bbda859de8f9 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -134,6 +134,12 @@ def visit_instance(self, t: types.Instance) -> None: def visit_callable_type(self, t: types.CallableType) -> None: self._visit_type_list(t.arg_types) self._visit(t.ret_type) + if t.type_guard is not None: + self._visit(t.type_guard) + if t.type_is is not None: + self._visit(t.type_is) + if t.instance_type is not None: + self._visit(t.instance_type) self._visit_type_tuple(t.variables) def visit_overloaded(self, t: types.Overloaded) -> None: diff --git a/mypy/infer.py b/mypy/infer.py index 56f4af753db82..2c155ee2456b3 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -70,8 +70,11 @@ def infer_type_arguments( actual: Type, is_supertype: bool = False, skip_unsatisfied: bool = False, + erase_types: bool = True, ) -> list[Type | None]: # Like infer_function_type_arguments, but only match a single type # against a generic type. - constraints = infer_constraints(template, actual, SUPERTYPE_OF if is_supertype else SUBTYPE_OF) + constraints = infer_constraints( + template, actual, SUPERTYPE_OF if is_supertype else SUBTYPE_OF, erase_types=erase_types + ) return solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)[0] diff --git a/mypy/join.py b/mypy/join.py index a8c9910e60bb7..3b6c9cc23f6f3 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -773,10 +773,14 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType: fallback = t.fallback else: fallback = s.fallback + instance_type = None + if t.instance_type is not None and s.instance_type is not None: + instance_type = join_types(t.instance_type, s.instance_type) return t.copy_modified( arg_types=arg_types, arg_names=combine_arg_names(t, s), ret_type=join_types(t.ret_type, s.ret_type), + instance_type=instance_type, fallback=fallback, name=None, ) @@ -827,10 +831,14 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType: fallback = t.fallback else: fallback = s.fallback + instance_type = None + if t.instance_type is not None and s.instance_type is not None: + instance_type = join_types(t.instance_type, s.instance_type) return t.copy_modified( arg_types=arg_types, arg_names=combine_arg_names(t, s), ret_type=join_types(t.ret_type, s.ret_type), + instance_type=instance_type, fallback=fallback, name=None, ) diff --git a/mypy/meet.py b/mypy/meet.py index cb8ad75f6013d..18b2732c55932 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -973,7 +973,7 @@ def visit_callable_type(self, t: CallableType) -> ProperType: return result elif isinstance(self.s, TypeType) and t.is_type_obj() and not t.is_generic(): # In this case we are able to potentially produce a better meet. - res = meet_types(self.s.item, t.ret_type) + res = meet_types(self.s.item, t.get_instance_type()) if not isinstance(res, (NoneType, UninhabitedType)): return TypeType.make_normalized(res) return self.default(self.s) @@ -1182,9 +1182,16 @@ def meet_similar_callables(t: CallableType, s: CallableType) -> CallableType: fallback = t.fallback else: fallback = s.fallback + if t.instance_type is None: + instance_type = s.instance_type + elif s.instance_type is None: + instance_type = t.instance_type + else: + instance_type = meet_types(t.instance_type, s.instance_type) return t.copy_modified( arg_types=arg_types, ret_type=meet_types(t.ret_type, s.ret_type), + instance_type=instance_type, fallback=fallback, name=None, ) diff --git a/mypy/messages.py b/mypy/messages.py index 1a6abcc853fef..93d5f2c212d0e 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2232,13 +2232,11 @@ def report_protocol_problems( subtype = subtype.item elif isinstance(subtype, CallableType): if subtype.is_type_obj(): - ret_type = get_proper_type(subtype.ret_type) - if isinstance(ret_type, TupleType): - ret_type = mypy.typeops.tuple_fallback(ret_type) - if not isinstance(ret_type, Instance): + instance_type = subtype.get_instance_type(force_fallback=True) + if not isinstance(instance_type, Instance): return class_obj = True - subtype = ret_type + subtype = instance_type else: subtype = subtype.fallback skip = ["__call__"] @@ -2832,9 +2830,7 @@ def format_literal_value(typ: LiteralType) -> str: elif isinstance(typ, FunctionLike): func = typ if func.is_type_obj(): - # The type of a type object type can be derived from the - # return type (this always works). - return format(TypeType.make_normalized(func.items[0].ret_type)) + return format(TypeType.make_normalized(func.items[0].get_instance_type())) elif isinstance(func, CallableType): if func.type_guard is not None: return_type = f"TypeGuard[{format(func.type_guard)}]" diff --git a/mypy/nodes.py b/mypy/nodes.py index 13a8a0fbf4e32..a0342b0e94958 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -49,6 +49,7 @@ WriteBuffer, read_bool, read_bytes, + read_flags, read_int, read_int_list, read_int_opt, @@ -61,6 +62,7 @@ read_tag, write_bool, write_bytes, + write_flags, write_int, write_int_list, write_int_opt, @@ -5259,20 +5261,6 @@ def set_flags(node: Node, flags: list[str]) -> None: setattr(node, name, True) -def write_flags(data: WriteBuffer, flags: list[bool]) -> None: - assert len(flags) <= 26, "This many flags not supported yet" - packed = 0 - for i, flag in enumerate(flags): - if flag: - packed |= 1 << i - write_int(data, packed) - - -def read_flags(data: ReadBuffer, num_flags: int) -> list[bool]: - packed = read_int(data) - return [(packed & (1 << i)) != 0 for i in range(num_flags)] - - def get_member_expr_fullname(expr: MemberExpr) -> str | None: """Return the qualified name representation of a member expression. diff --git a/mypy/plugins/singledispatch.py b/mypy/plugins/singledispatch.py index a513b91ff309b..9a5576c17e82c 100644 --- a/mypy/plugins/singledispatch.py +++ b/mypy/plugins/singledispatch.py @@ -126,7 +126,7 @@ def singledispatch_register_callback(ctx: MethodContext) -> Type: # is_subtype doesn't work when the right type is Overloaded, so we need the # actual type - register_type = first_arg_type.items[0].ret_type + register_type = first_arg_type.items[0].get_instance_type() type_args = RegisterCallableInfo(register_type, ctx.type) register_callable = make_fake_register_class_instance(ctx.api, type_args) return register_callable diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index 9bbc3077ec512..ecff546049f92 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -470,6 +470,9 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem: typ.is_ellipsis_args, snapshot_types(typ.variables), typ.is_bound, + snapshot_optional_type(typ.type_guard), + snapshot_optional_type(typ.type_is), + snapshot_optional_type(typ.instance_type), ) def normalize_callable_variables(self, typ: CallableType) -> CallableType: diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index aaf388b6665d6..075bf7cb540bf 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -452,6 +452,12 @@ def visit_callable_type(self, typ: CallableType) -> None: # Fallback can be None for callable types that haven't been semantically analyzed. if typ.fallback is not None: typ.fallback.accept(self) + if typ.type_guard is not None: + typ.type_guard.accept(self) + if typ.type_is is not None: + typ.type_is.accept(self) + if typ.instance_type is not None: + typ.instance_type.accept(self) for tv in typ.variables: if isinstance(tv, TypeVarType): tv.upper_bound.accept(self) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index ba622329665ea..b2c91d8db4888 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -1003,6 +1003,12 @@ def visit_callable_type(self, typ: CallableType) -> list[str]: for arg in typ.arg_types: triggers.extend(self.get_type_triggers(arg)) triggers.extend(self.get_type_triggers(typ.ret_type)) + if typ.type_guard is not None: + triggers.extend(self.get_type_triggers(typ.type_guard)) + if typ.type_is is not None: + triggers.extend(self.get_type_triggers(typ.type_is)) + if typ.instance_type is not None: + triggers.extend(self.get_type_triggers(typ.instance_type)) # fallback is a metaclass type for class objects, and is # processed separately. return triggers diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2e3ded8460b3e..305cfa9de5f22 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -762,17 +762,15 @@ def visit_callable_type(self, left: CallableType) -> bool: if is_protocol_implementation(left.fallback, right, skip=["__call__"]): return True if right.type.is_protocol and left.is_type_obj(): - ret_type = get_proper_type(left.ret_type) - if isinstance(ret_type, TupleType): - ret_type = mypy.typeops.tuple_fallback(ret_type) - if isinstance(ret_type, Instance) and is_protocol_implementation( - ret_type, right, proper_subtype=self.proper_subtype, class_obj=True + instance_type = left.get_instance_type(force_fallback=True) + if isinstance(instance_type, Instance) and is_protocol_implementation( + instance_type, right, proper_subtype=self.proper_subtype, class_obj=True ): return True return self._is_subtype(left.fallback, right) elif isinstance(right, TypeType): # This is unsound, we don't check the __init__ signature. - return left.is_type_obj() and self._is_subtype(left.ret_type, right.item) + return left.is_type_obj() and self._is_subtype(left.get_instance_type(), right.item) else: return False @@ -1154,6 +1152,21 @@ def visit_type_type(self, left: TypeType) -> bool: # We can't accept `Type[X]` as a *proper* subtype of Callable[P, X] # since this will break transitivity of subtyping. return False + item = left.item + # Note: self-types are special since returning `Self` is more precise + # than its upper bound, so we don't unwrap TypeVar to its bound here. + if isinstance(item, TupleType): + item = mypy.typeops.tuple_fallback(item) + if isinstance(item, Instance): + constructor = mypy.typeops.type_object_type(item.type) + constructor = expand_type_by_instance(constructor, item) + if isinstance(constructor, CallableType): + # Only consider return type to match historic behavior (see below). + return self._is_subtype(constructor.ret_type, right.ret_type) + elif isinstance(constructor, Overloaded): + return all( + self._is_subtype(c.ret_type, right.ret_type) for c in constructor.items + ) # This is unsound, we don't check the __init__ signature. return self._is_subtype(left.item, right.ret_type) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index d668121bc5b9c..8381c9a1f7508 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -34,6 +34,7 @@ ParamSpecType, PartialType, PlaceholderType, + ProperType, RawExpressionType, TupleType, Type, @@ -254,10 +255,15 @@ def visit_unpack_type(self, t: UnpackType, /) -> Type: return UnpackType(t.type.accept(self)) def visit_callable_type(self, t: CallableType, /) -> Type: + instance_type = None + if t.instance_type is not None: + instance_type = t.instance_type.accept(self) + assert isinstance(instance_type, ProperType) return t.copy_modified( arg_types=self.translate_type_list(t.arg_types), ret_type=t.ret_type.accept(self), variables=self.translate_variables(t.variables), + instance_type=instance_type, ) def visit_tuple_type(self, t: TupleType, /) -> Type: @@ -415,7 +421,11 @@ def visit_instance(self, t: Instance, /) -> T: def visit_callable_type(self, t: CallableType, /) -> T: # FIX generics - return self.query_types(t.arg_types + [t.ret_type]) + types = t.arg_types + [t.ret_type] + # Avoid double-counting when using queries in reports. + if t.instance_type is not None and t.instance_type != t.ret_type: + types.append(t.instance_type) + return self.query_types(types) def visit_tuple_type(self, t: TupleType, /) -> T: return self.query_types([t.partial_fallback] + t.items) @@ -551,12 +561,11 @@ def visit_instance(self, t: Instance, /) -> bool: def visit_callable_type(self, t: CallableType, /) -> bool: # FIX generics # Avoid allocating any objects here as an optimization. - args = self.query_types(t.arg_types) - ret = t.ret_type.accept(self) + inst = t.instance_type.accept(self) if t.instance_type is not None else False if self.strategy == ANY_STRATEGY: - return args or ret + return self.query_types(t.arg_types) or t.ret_type.accept(self) or inst else: - return args and ret + return self.query_types(t.arg_types) and t.ret_type.accept(self) and inst def visit_tuple_type(self, t: TupleType, /) -> bool: return self.query_types([t.partial_fallback] + t.items) diff --git a/mypy/typeops.py b/mypy/typeops.py index e777bec191f91..78448c60927f9 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -217,7 +217,9 @@ def type_object_type( is_bound=True, fallback=instance_cache.function_type, ) - result: FunctionLike = class_callable(sig, info, fallback, None, is_new=False) + result: FunctionLike = class_callable( + sig, info, None, fallback, None, is_new=False + ) if allow_cache and state.strict_optional: info.type_object_type = result return result @@ -305,19 +307,24 @@ def type_object_type_from_function( special_sig = "dict" if isinstance(signature, CallableType): - return class_callable(signature, info, fallback, special_sig, is_new, orig_self_types[0]) + return class_callable( + signature, info, def_info, fallback, special_sig, is_new, orig_self_types[0] + ) else: # Overloaded __init__/__new__. assert isinstance(signature, Overloaded) items: list[CallableType] = [] for item, orig_self in zip(signature.items, orig_self_types): - items.append(class_callable(item, info, fallback, special_sig, is_new, orig_self)) + items.append( + class_callable(item, info, def_info, fallback, special_sig, is_new, orig_self) + ) return Overloaded(items) def class_callable( init_type: CallableType, info: TypeInfo, + def_info: TypeInfo | None, type_type: Instance, special_sig: str | None, is_new: bool, @@ -328,35 +335,51 @@ def class_callable( variables.extend(info.defn.type_vars) variables.extend(init_type.variables) - from mypy.subtypes import is_subtype + from mypy.subtypes import is_equivalent, is_subtype init_ret_type = get_proper_type(init_type.ret_type) orig_self_type = get_proper_type(orig_self_type) default_ret_type = fill_typevars(info) + # Default return type in the class where constructor method was defined. + default_def_ret_type = fill_typevars(def_info) if def_info is not None else default_ret_type explicit_type = init_ret_type if is_new else orig_self_type if ( + is_new + and explicit_type is not None + # We used to only use the explicit return type of __new__() when it was a subtype + # of the current class. As a result, we may now have a situation like this: + # class C: + # def __new__(cls) -> C: ... + # class D(C): ... + # So we need to ignore the explicit annotation when creating constructor type for D. + and ( + isinstance(explicit_type, AnyType) + and explicit_type.type_of_any != TypeOfAny.unannotated + or not is_equivalent(default_def_ret_type, explicit_type, ignore_type_params=True) + ) + ): + ret_type = explicit_type + elif ( isinstance(explicit_type, (Instance, TupleType, UninhabitedType, LiteralType)) # We have to skip protocols, because it can be a subtype of a return type # by accident. Like `Hashable` is a subtype of `object`. See #11799 and isinstance(default_ret_type, Instance) and not default_ret_type.type.is_protocol - # Only use the declared return type from __new__ or declared self in __init__ - # if it is actually returning a subtype of what we would return otherwise. + # Use the declared self in __init__ if it is a subtype of what we would use otherwise. and is_subtype(explicit_type, default_ret_type, ignore_type_params=True) ): - ret_type: Type = explicit_type + ret_type = explicit_type else: ret_type = default_ret_type - callable_type = init_type.copy_modified( + return init_type.copy_modified( ret_type=ret_type, fallback=type_type, - name=None, + name=info.name, variables=variables, special_sig=special_sig, + instance_type=default_ret_type, ) - c = callable_type.with_name(info.name) - return c def map_type_from_supertype(typ: Type, sub_info: TypeInfo, super_info: TypeInfo) -> Type: @@ -480,7 +503,7 @@ class B(A): pass # Solve for these type arguments using the actual class or instance type. typeargs = infer_type_arguments( - self_vars, self_param_type, original_type, is_supertype=True + self_vars, self_param_type, original_type, is_supertype=True, erase_types=False ) if ( is_classmethod @@ -489,7 +512,11 @@ class B(A): pass ): # In case we call a classmethod through an instance x, fallback to type(x). typeargs = infer_type_arguments( - self_vars, self_param_type, TypeType(original_type), is_supertype=True + self_vars, + self_param_type, + TypeType(original_type), + is_supertype=True, + erase_types=False, ) # Update the method signature with the solutions found. diff --git a/mypy/types.py b/mypy/types.py index 07b0a4f64ae11..bc06e36d7a47e 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -37,6 +37,7 @@ Tag, WriteBuffer, read_bool, + read_flags, read_int, read_int_list, read_literal, @@ -46,6 +47,7 @@ read_str_opt_list, read_tag, write_bool, + write_flags, write_int, write_int_list, write_literal, @@ -2147,7 +2149,6 @@ class CallableType(FunctionLike): "arg_types", # Types of function arguments "arg_kinds", # ARG_ constants "arg_names", # Argument names; None if not a keyword argument - "min_args", # Minimum number of arguments; derived from arg_kinds "ret_type", # Return value type "name", # Name (may be None; for error messages and plugins) "definition", # For error messages. May be None. @@ -2167,6 +2168,8 @@ class CallableType(FunctionLike): # (this is used for error messages) "imprecise_arg_kinds", "unpack_kwargs", # Was an Unpack[...] with **kwargs used to define this callable? + "instance_type", # Real underlying type of a type object. This is different from + # ret_type in case we have e.g. a custom __new__() return annotation. ) def __init__( @@ -2192,6 +2195,7 @@ def __init__( from_concatenate: bool = False, imprecise_arg_kinds: bool = False, unpack_kwargs: bool = False, + instance_type: ProperType | None = None, ) -> None: super().__init__(line, column) assert len(arg_types) == len(arg_kinds) == len(arg_names) @@ -2203,7 +2207,6 @@ def __init__( # See testParamSpecJoin, that relies on passing e.g `P.args` as plain argument. self.arg_kinds = arg_kinds self.arg_names = list(arg_names) - self.min_args = arg_kinds.count(ARG_POS) self.ret_type = ret_type self.fallback = fallback assert not name or " CT: modified = CallableType( arg_types=arg_types if arg_types is not _dummy else self.arg_types, @@ -2280,6 +2285,7 @@ def copy_modified( else self.imprecise_arg_kinds ), unpack_kwargs=unpack_kwargs if unpack_kwargs is not _dummy else self.unpack_kwargs, + instance_type=instance_type if instance_type is not _dummy else self.instance_type, ) # Optimization: Only NewTypes are supported as subtypes since # the class is effectively final, so we can use a cast safely. @@ -2299,6 +2305,10 @@ def kw_arg(self) -> FormalArgument | None: return FormalArgument(None, position, type, False) return None + @property + def min_args(self) -> int: + return self.arg_kinds.count(ARG_POS) + @property def is_var_arg(self) -> bool: """Does this callable have a *args argument?""" @@ -2314,9 +2324,23 @@ def is_type_obj(self) -> bool: get_proper_type(self.ret_type), UninhabitedType ) - def type_object(self) -> mypy.nodes.TypeInfo: + def get_instance_type(self, *, force_fallback: bool = False) -> ProperType: + """Get underlying type of a type object. + + By default, this will return a precise self-type, essentially whatever is + returned by fill_typevars(). Most notably this is a TupleType for named tuples. + If an Instance fallback is required, use force_fallback=True. + """ assert self.is_type_obj() - ret = get_proper_type(self.ret_type) + if self.instance_type is not None: + ret = self.instance_type + else: + # Fall back to historic behavior in case instance_type is not set. This + # will avoid crashes on type objects generated by plugins, and on (unknown) + # corner cases where is_type_obj() may "accidentally" return True. + ret = get_proper_type(self.ret_type) + if not force_fallback: + return ret if isinstance(ret, TypeVarType): ret = get_proper_type(ret.upper_bound) if isinstance(ret, TupleType): @@ -2325,15 +2349,19 @@ def type_object(self) -> mypy.nodes.TypeInfo: ret = ret.fallback if isinstance(ret, LiteralType): ret = ret.fallback - assert isinstance(ret, Instance) - return ret.type + return ret + + def type_object(self) -> mypy.nodes.TypeInfo: + instance_type = self.get_instance_type(force_fallback=True) + assert isinstance(instance_type, Instance) + return instance_type.type def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_callable_type(self) def with_name(self, name: str) -> CallableType: """Return a copy of this type with the specified name.""" - return self.copy_modified(ret_type=self.ret_type, name=name) + return self.copy_modified(name=name) def get_name(self) -> str | None: return self.name @@ -2591,10 +2619,13 @@ def serialize(self) -> JsonDict: "implicit": self.implicit, "is_bound": self.is_bound, "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, - "type_is": (self.type_is.serialize() if self.type_is is not None else None), + "type_is": self.type_is.serialize() if self.type_is is not None else None, "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, + "instance_type": ( + self.instance_type.serialize() if self.instance_type is not None else None + ), } @classmethod @@ -2615,35 +2646,56 @@ def deserialize(cls, data: JsonDict) -> CallableType: type_guard=( deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None ), - type_is=(deserialize_type(data["type_is"]) if data["type_is"] is not None else None), + type_is=deserialize_type(data["type_is"]) if data["type_is"] is not None else None, from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], unpack_kwargs=data["unpack_kwargs"], + instance_type=( + cast(ProperType, deserialize_type(data["instance_type"])) + if data["instance_type"] is not None + else None + ), ) def write(self, data: WriteBuffer) -> None: write_tag(data, CALLABLE_TYPE) self.fallback.write(data) + write_type_opt(data, self.instance_type) + write_flags( + data, + [ + self.is_ellipsis_args, + self.implicit, + self.is_bound, + self.from_concatenate, + self.imprecise_arg_kinds, + self.unpack_kwargs, + ], + ) write_type_list(data, self.arg_types) write_int_list(data, [int(x.value) for x in self.arg_kinds]) write_str_opt_list(data, self.arg_names) self.ret_type.write(data) write_str_opt(data, self.name) write_type_list(data, self.variables) - write_bool(data, self.is_ellipsis_args) - write_bool(data, self.implicit) - write_bool(data, self.is_bound) write_type_opt(data, self.type_guard) write_type_opt(data, self.type_is) - write_bool(data, self.from_concatenate) - write_bool(data, self.imprecise_arg_kinds) - write_bool(data, self.unpack_kwargs) write_tag(data, END_TAG) @classmethod def read(cls, data: ReadBuffer) -> CallableType: assert read_tag(data) == INSTANCE fallback = Instance.read(data) + instance_type = read_type_opt(data) + assert instance_type is None or isinstance(instance_type, ProperType) + ( + is_ellipsis_args, + implicit, + is_bound, + from_concatenate, + imprecise_arg_kinds, + unpack_kwargs, + ) = read_flags(data, num_flags=6) ret = CallableType( read_type_list(data), [ARG_KINDS[ak] for ak in read_int_list(data)], @@ -2652,14 +2704,15 @@ def read(cls, data: ReadBuffer) -> CallableType: fallback, name=read_str_opt(data), variables=read_type_var_likes(data), - is_ellipsis_args=read_bool(data), - implicit=read_bool(data), - is_bound=read_bool(data), + is_ellipsis_args=is_ellipsis_args, + implicit=implicit, + is_bound=is_bound, type_guard=read_type_opt(data), type_is=read_type_opt(data), - from_concatenate=read_bool(data), - imprecise_arg_kinds=read_bool(data), - unpack_kwargs=read_bool(data), + from_concatenate=from_concatenate, + imprecise_arg_kinds=imprecise_arg_kinds, + unpack_kwargs=unpack_kwargs, + instance_type=instance_type, ) assert read_tag(data) == END_TAG return ret diff --git a/mypy/typetraverser.py b/mypy/typetraverser.py index abd0f6bf3bdfe..2a7f41bd97f21 100644 --- a/mypy/typetraverser.py +++ b/mypy/typetraverser.py @@ -93,6 +93,9 @@ def visit_callable_type(self, t: CallableType, /) -> None: if t.type_is is not None: t.type_is.accept(self) + if t.instance_type is not None: + t.instance_type.accept(self) + def visit_tuple_type(self, t: TupleType, /) -> None: self.traverse_type_list(t.items) t.partial_fallback.accept(self) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 8f3407c954e00..b830b99465e07 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -7317,18 +7317,25 @@ reveal_type(B()) # N: Revealed type is "__main__.B" [case testNewReturnType2] from typing import Any -# make sure that __new__ method that return Any are ignored when +# make sure that __new__ method that return implicit Any are ignored when # determining the return type class A: def __new__(cls): pass class B: - def __new__(cls) -> Any: + def __new__(cls, x: int = 0) -> Any: + pass + +class C: + def __new__(cls, x: int = 0): pass reveal_type(A()) # N: Revealed type is "__main__.A" -reveal_type(B()) # N: Revealed type is "__main__.B" +reveal_type(B()) # N: Revealed type is "Any" +reveal_type(B(1)) # N: Revealed type is "Any" +reveal_type(C()) # N: Revealed type is "__main__.C" +reveal_type(C(1)) # N: Revealed type is "__main__.C" [case testNewReturnType3] @@ -7338,7 +7345,7 @@ class A: def __new__(cls) -> int: # E: Incompatible return type for "__new__" (returns "int", but must return a subtype of "A") pass -reveal_type(A()) # N: Revealed type is "__main__.A" +reveal_type(A()) # N: Revealed type is "builtins.int" [case testNewReturnType4] from typing import TypeVar, Type @@ -7448,6 +7455,287 @@ class MyMetaClass(type): class MyClass(metaclass=MyMetaClass): pass +[case testNewReturnType13] +from typing import Protocol + +class Foo(Protocol): + def foo(self) -> str: ... + +class A: + def __new__(cls) -> Foo: ... # E: Incompatible return type for "__new__" (returns "Foo", but must return a subtype of "A") + +reveal_type(A()) # N: Revealed type is "__main__.Foo" +reveal_type(A().foo()) # N: Revealed type is "builtins.str" + +[case testNewReturnType14] +from __future__ import annotations + +class A: + def __new__(cls) -> int: raise # E: Incompatible return type for "__new__" (returns "int", but must return a subtype of "A") + +class B(A): + @classmethod + def foo(cls) -> int: raise + +reveal_type(B.foo()) # N: Revealed type is "builtins.int" +[builtins fixtures/classmethod.pyi] + +[case testNewReturnType15] +from typing import Generic, Type, TypeVar + +T = TypeVar("T") + +class A(Generic[T]): + def __new__(cls) -> B[int]: ... + @classmethod + def foo(cls: Type[A[T]]) -> T: ... + +class B(A[T]): ... + +# The Never without error is not ideal, but matches the behavior without custom __new__(). +reveal_type(B.foo()) # N: Revealed type is "Never" +reveal_type(B[str].foo()) # N: Revealed type is "builtins.str" + +class C(A[str]): ... + +reveal_type(C.foo()) # N: Revealed type is "builtins.str" +[builtins fixtures/classmethod.pyi] + +[case testNewReturnType16] +from typing import Generic, TypeVar + +T = TypeVar("T") +class A(Generic[T]): + def __new__(cls, *args, **kwargs) -> T: # E: "__new__" must return a class instance (got "T") + ... + +class Model: + pass + +reveal_type(A[Model]()) # N: Revealed type is "__main__.Model" + +class B(A[Model]): + pass + +reveal_type(B()) # N: Revealed type is "__main__.Model" +[builtins fixtures/dict.pyi] + +[case testNewReturnType17] +class C: + def __new__(self) -> D: + return D() + +class D(C): + x: int + +C.x # E: "type[C]" has no attribute "x" + +[case testNewReturnType18] +class A: + def __new__(cls) -> A: + return A() + +class B(A): + def __new__(cls) -> A: # E: Incompatible return type for "__new__" (returns "A", but must return a subtype of "B") + return super().__new__(cls) + +class C(B): ... + +# Always respect explicit return type after giving an error. +reveal_type(B()) # N: Revealed type is "__main__.A" +reveal_type(C()) # N: Revealed type is "__main__.A" + +# Ignore "implicit" return type to preserve backwards compatibility. +class D(A): ... +reveal_type(D()) # N: Revealed type is "__main__.D" + +[case testNewReturnType19] +from typing import TypeVar + +T = TypeVar("T") + +def f(tp: type[T]) -> T: ... + +class C: + def __new__(cls) -> int: ... # type: ignore[misc] + +reveal_type(f(C)) # N: Revealed type is "__main__.C" + +[case testNewReturnTypeCallableSubtyping] +from typing import Callable, Protocol + +class Foo: + def foo(self) -> None: ... + +class A: + def __new__(cls) -> Foo: ... # type: ignore + +ta: type[A] +tf: type[Foo] + +class PA(Protocol): + def __call__(self) -> A: ... + +class PFoo(Protocol): + def __call__(self) -> Foo: ... + +def f(x: Callable[[], A]) -> None: ... +def g(x: Callable[[], Foo]) -> None: ... +def fp(x: PA) -> None: ... +def gp(x: PFoo) -> None: ... +def h(x: type[A]) -> None: ... +def i(x: type[Foo]) -> None: ... + +f(A) # E: Argument 1 to "f" has incompatible type "type[A]"; expected "Callable[[], A]" +g(A) +fp(A) # E: Argument 1 to "fp" has incompatible type "type[A]"; expected "PA" \ + # N: "A" has constructor incompatible with "__call__" of "PA" \ + # N: Following member(s) of "A" have conflicts: \ + # N: Expected: \ + # N: def __call__() -> A \ + # N: Got: \ + # N: def A() -> Foo \ + # N: "PA.__call__" has type "Callable[[], A]" +gp(A) +h(A) +i(A) # E: Argument 1 to "i" has incompatible type "type[A]"; expected "type[Foo]" + +f(Foo) # E: Argument 1 to "f" has incompatible type "type[Foo]"; expected "Callable[[], A]" +g(Foo) +fp(Foo) # E: Argument 1 to "fp" has incompatible type "type[Foo]"; expected "PA" \ + # N: "Foo" has constructor incompatible with "__call__" of "PA" \ + # N: Following member(s) of "Foo" have conflicts: \ + # N: Expected: \ + # N: def __call__() -> A \ + # N: Got: \ + # N: def Foo() -> Foo \ + # N: "PA.__call__" has type "Callable[[], A]" +gp(Foo) +h(Foo) # E: Argument 1 to "h" has incompatible type "type[Foo]"; expected "type[A]" +i(Foo) + +f(ta) # E: Argument 1 to "f" has incompatible type "type[A]"; expected "Callable[[], A]" +g(ta) +fp(ta) # E: Argument 1 to "fp" has incompatible type "type[A]"; expected "PA" \ + # N: "A" has constructor incompatible with "__call__" of "PA" \ + # N: Following member(s) of "A" have conflicts: \ + # N: Expected: \ + # N: def __call__() -> A \ + # N: Got: \ + # N: def A() -> Foo \ + # N: "PA.__call__" has type "Callable[[], A]" +gp(ta) +h(ta) +i(ta) # E: Argument 1 to "i" has incompatible type "type[A]"; expected "type[Foo]" + +f(tf) # E: Argument 1 to "f" has incompatible type "type[Foo]"; expected "Callable[[], A]" +g(tf) +fp(tf) # E: Argument 1 to "fp" has incompatible type "type[Foo]"; expected "PA" \ + # N: "Foo" has constructor incompatible with "__call__" of "PA" \ + # N: Following member(s) of "Foo" have conflicts: \ + # N: Expected: \ + # N: def __call__() -> A \ + # N: Got: \ + # N: def Foo() -> Foo \ + # N: "PA.__call__" has type "Callable[[], A]" +gp(tf) +h(tf) # E: Argument 1 to "h" has incompatible type "type[Foo]"; expected "type[A]" +i(tf) + +[case testNewReturnTypeCallableJoins] +from typing import TypeVar + +class Base: ... +class Foo(Base): ... +class Bar(Base): ... + +class Base2: ... + +class A(Base2): + def __new__(cls) -> Foo: ... # type: ignore + +class B(Base2): + def __new__(cls) -> Bar: ... # type: ignore + +T = TypeVar("T") +def join(x: T, y: T) -> T: ... + +reveal_type(join(A, Foo)) # N: Revealed type is "def () -> __main__.Foo" +reveal_type(join(Foo, A)) # N: Revealed type is "def () -> __main__.Foo" +reveal_type(join(A, B)) # N: Revealed type is "def () -> __main__.Base" +reveal_type(join(B, A)) # N: Revealed type is "def () -> __main__.Base" + +# This tests the type repr in error messages. +j = join(A, B) +x: int = j # E: Incompatible types in assignment (expression has type "type[Base2]", variable has type "int") + +[case testNewReturnTypeCallableInference] +from typing import Callable, TypeVar + +class A: + def __new__(cls) -> int: ... # type: ignore + +T = TypeVar("T") +def foo(f: Callable[[], T]) -> T: ... +def bar(t: type[T]) -> T: ... + +ta: type[A] +reveal_type(foo(A)) # N: Revealed type is "builtins.int" +reveal_type(foo(ta)) # N: Revealed type is "builtins.int" +reveal_type(bar(A)) # N: Revealed type is "__main__.A" +reveal_type(bar(ta)) # N: Revealed type is "__main__.A" + +[case testNewReturnTypeCallableNarrowing] +from typing import Callable, TypeVar + +class A: ... + +class B(A): + more = 1 + def __new__(cls) -> A: ... # type: ignore + +x: A +if isinstance(x, B): + reveal_type(x) # N: Revealed type is "__main__.B" + +tx: type[A] +if issubclass(tx, B): + reveal_type(tx) # N: Revealed type is "type[__main__.B]" +[builtins fixtures/isinstancelist.pyi] + +[case testNewReturnTypeOverloadNarrowing] +from typing import TypeVar, Generic, Union, overload + +T = TypeVar("T", int, str) + +class Series(Generic[T]): + @overload + def __new__(cls, dtype: int) -> IntSeries: ... + @overload + def __new__(cls, dtype: type[T]) -> Series[T]: ... + @overload + def __new__(cls, dtype=...) -> Series: ... + def __new__(cls, dtype=...) -> Series: + ... + +class IntSeries(Series[int]): ... + +class Index: + @overload + def __new__(cls, dtype: int) -> IntIndex: ... + @overload + def __new__(cls, dtype=...) -> Index: ... + def __new__(cls, dtype=...) -> Index: + ... + +class IntIndex(Index): ... + +def foo(x: Union[Series, int, Index]): + if isinstance(x, (Series, Index)): + reveal_type(x) # N: Revealed type is "__main__.Series[Any] | __main__.Index" + else: + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/isinstancelist.pyi] [case testMetaclassPlaceholderNode] from sympy.assumptions import ManagedProperties diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index b6a97c70f4950..b8f7a5699e199 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -3728,3 +3728,12 @@ reveal_type(ok3) # N: Revealed type is "tuple[()]" bad1: list[()] = [] # E: "list" expects 1 type argument, but none given \ # E: Missing type arguments for generic type "list" [builtins fixtures/tuple.pyi] + +[case testGenericClassAsArgumentToType] +from typing import TypeVar, Generic + +T = TypeVar("T") +def test(tp: type[T]) -> T: ... + +class C(Generic[T]): ... +reveal_type(test(C)) # N: Revealed type is "__main__.C[Any]" diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 07d70ba7cfb12..22f0c805799bb 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -8084,3 +8084,35 @@ tmp/m.py:3: note: Revealed type is "TypedDict('lib.A', {'x': builtins.int})" [out2] tmp/lib.py:4: error: Name "A" already defined (possibly by an import) tmp/m.py:3: note: Revealed type is "TypedDict('lib.A', {'x': builtins.int})" + +[case testNewReturnTypeIncremental] +import m +[file m.py] +from lib import C + +reveal_type(C().ta) +x: int = C().ta +reveal_type(C().ta.x) +[file m.py.2] +from lib import C + +# touch +reveal_type(C().ta) +x: int = C().ta +reveal_type(C().ta.x) +[file lib.py] +class A: + x = "test" + def __new__(cls) -> int: ... # type: ignore + +class C: + def __init__(self) -> None: + self.ta = A +[out] +tmp/m.py:3: note: Revealed type is "def () -> builtins.int" +tmp/m.py:4: error: Incompatible types in assignment (expression has type "type[A]", variable has type "int") +tmp/m.py:5: note: Revealed type is "builtins.str" +[out2] +tmp/m.py:4: note: Revealed type is "def () -> builtins.int" +tmp/m.py:5: error: Incompatible types in assignment (expression has type "type[A]", variable has type "int") +tmp/m.py:6: note: Revealed type is "builtins.str" From 8f4c84052cbb4cda3042287da0039754436eb49f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 28 May 2026 02:02:17 +0100 Subject: [PATCH 048/127] Fix crash in new-style type alias with variadic unpack (#21551) Fixes https://github.com/python/mypy/issues/20913 Closes https://github.com/python/mypy/pull/20931 Fix is trivial, don't fix what is already valid (since `fix_instance()` has some implicit assumptions) like we already do for old-style aliases. --- mypy/semanal.py | 5 ++++- test-data/unit/check-python312.test | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 84ac54bfba724..91e0a5e79a7e9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5771,7 +5771,10 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: res = make_any_non_unimported(res) eager = self.is_func_scope() if isinstance(res, ProperType) and isinstance(res, Instance): - fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options) + if not validate_instance(res, self.fail, indexed): + fix_instance( + res, self.fail, self.note, disallow_any=False, options=self.options + ) alias_node = TypeAlias( res, self.qualified_name(s.name.name), diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index b51186796b8f0..c4a2320797464 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2280,3 +2280,16 @@ class D[*Ts](Generic[Unpack[Us]]): # E: Generic[...] base class is redundant \ # E: Can only use one type var tuple in a class def pass [builtins fixtures/tuple.pyi] + +[case testPEP695VariadicAliasUnpack] +class C[*Ts]: + pass + +type T[T, *Ts] = C[*Ts] + +x: T[bool, *tuple[()]] +reveal_type(x) # N: Revealed type is "__main__.C[()]" + +y: T[bool] +reveal_type(y) # N: Revealed type is "__main__.C[()]" +[builtins fixtures/tuple.pyi] From 1d6905a7ddec3ad3d0d947f3ed8736587e0297e4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 28 May 2026 16:39:00 +0100 Subject: [PATCH 049/127] Fix various bugs in TypeVarTuples with defaults (#21544) Fixes a bunch of TODOs in tests. Fixes https://github.com/python/mypy/issues/20449 Couple non-trivial things here: First, I prohibit (regular) type variables with defaults that appear after a `TypeVarTuple`. The PEP and the spec are clear that these are ambiguous and invalid. But, because they are ambiguous, we can't simply give the error, since otherwise various parts of code will behave in weirdly inconsistent ways. Therefore, I completely erase ambiguous type variables out of existence, 1984 style. Second, couple commented-out tests cases suggested that `TypeVarTuple` defaults should be used in cases where `*tuple[()]` would be used as a value otherwise, plus `Foo[X, Y]` and `Foo[X, Y, *tuple[()]]` should be interpreted differently. I don't see anything about this in the PEP/spec, and I don't like this. I think the default should be used _only_ in cases where `Any` or `Never` would be used otherwise. --- mypy/message_registry.py | 1 + mypy/semanal.py | 60 ++++++++++++++---- mypy/typeanal.py | 71 ++++++++++++++++------ test-data/unit/check-python313.test | 6 +- test-data/unit/check-typevar-defaults.test | 43 ++++++------- 5 files changed, 125 insertions(+), 56 deletions(-) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 82885065934f1..2e40048cbb58b 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -181,6 +181,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage: 'Implicit generic "Any". Use "{}" and specify generic parameters' ) NO_CYCLIC_DEFAULT: Final = "Cyclic type variable defaults are not supported" +NO_DEFAULT_AFTER_TYPEVAR_TUPLE: Final = "A type variable with default cannot follow TypeVarTuple" INVALID_UNPACK: Final = "{} cannot be unpacked (must be tuple or TypeVarTuple)" INVALID_UNPACK_POSITION: Final = "Unpack is only valid in a variadic position" INVALID_PARAM_SPEC_LOCATION: Final = "Invalid location for ParamSpec {}" diff --git a/mypy/semanal.py b/mypy/semanal.py index 91e0a5e79a7e9..e3524f2c7db68 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -546,8 +546,13 @@ def __init__( # to create the set lazily. self.types_fixed: set[TypeInfo | TypeAlias] | None = None + # Stack of type variables that have been removed from current class because they + # cannot be bound unambiguously. This can happen if a (regular) type variable + # with a default follows a type variable tuple. + self.removed_type_vars: list[list[TypeVarType]] = [[]] + # mypyc doesn't properly handle implementing an abstractproperty - # with a regular attribute so we make them properties + # with a regular attribute, so we make them properties @property def type(self) -> TypeInfo | None: return self._type @@ -1836,8 +1841,9 @@ def visit_class_def(self, defn: ClassDef) -> None: if self.push_type_args(defn.type_args, defn) is None: self.mark_incomplete(defn.name, defn) return - + self.removed_type_vars.append([]) self.analyze_class(defn) + self.removed_type_vars.pop() self.pop_type_args(defn.type_args) self.incomplete_type_stack.pop() @@ -2084,7 +2090,21 @@ def check_type_alias_bases(self, bases: list[Expression]) -> None: ) def setup_type_vars(self, defn: ClassDef, tvar_defs: list[TypeVarLikeType]) -> None: - defn.type_vars = tvar_defs + seen_tvt = False + valid_tvar_defs = [] + for tv in tvar_defs: + if seen_tvt and isinstance(tv, TypeVarType) and tv.has_default(): + self.fail( + message_registry.NO_DEFAULT_AFTER_TYPEVAR_TUPLE, defn, code=codes.TYPE_VAR + ) + # Remove the ambiguous type variable, and record it, so that we can replace + # all its uses with Any. + self.removed_type_vars[-1].append(tv) + continue + if isinstance(tv, TypeVarTupleType): + seen_tvt = True + valid_tvar_defs.append(tv) + defn.type_vars = valid_tvar_defs defn.info.type_vars = [] # we want to make sure any additional logic in add_type_vars gets run defn.info.add_type_vars() @@ -4017,6 +4037,28 @@ def analyze_alias( with self.allow_unbound_tvars_set(): rvalue.accept(self) + new_tvar_defs = [] + erase_tvar_defs = [] + variadic = False + for td in tvar_defs: + if variadic and isinstance(td, TypeVarType) and td.has_default(): + self.fail( + message_registry.NO_DEFAULT_AFTER_TYPEVAR_TUPLE, + rvalue, + code=codes.TYPE_VAR, + ) + # Remove the ambiguous type variable, and record it, so that we can + # replace all its uses with Any. + erase_tvar_defs.append(td) + continue + if isinstance(td, TypeVarTupleType): + # There can be only one variadic variable at most, + # the error is reported elsewhere. + if variadic: + continue + variadic = True + new_tvar_defs.append(td) + analyzed, depends_on = analyze_type_alias( typ, self, @@ -4029,20 +4071,11 @@ def analyze_alias( in_dynamic_func=dynamic, global_scope=global_scope, allowed_alias_tvars=tvar_defs, + erase_tvar_defs=erase_tvar_defs, alias_type_params_names=all_declared_type_params_names, python_3_12_type_alias=python_3_12_type_alias, ) - # There can be only one variadic variable at most, the error is reported elsewhere. - new_tvar_defs = [] - variadic = False - for td in tvar_defs: - if isinstance(td, TypeVarTupleType): - if variadic: - continue - variadic = True - new_tvar_defs.append(td) - indexed = bool(isinstance(typ, UnboundType) and (typ.args or typ.empty_tuple_index)) default_depends = {} for _, tv in alias_type_vars: @@ -7792,6 +7825,7 @@ def type_analyzer( prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, analyzing_tvar_def=analyzing_tvar_def, + erase_tvar_defs=self.removed_type_vars[-1], ) tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic()) tpan.global_scope = not self.type and not self.function_stack diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 014681d9132ec..51d26afd55e46 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -154,6 +154,7 @@ def analyze_type_alias( in_dynamic_func: bool = False, global_scope: bool = True, allowed_alias_tvars: list[TypeVarLikeType] | None = None, + erase_tvar_defs: list[TypeVarType] | None = None, alias_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, ) -> tuple[Type, set[str]]: @@ -174,6 +175,7 @@ def analyze_type_alias( allow_placeholder=allow_placeholder, prohibit_self_type="type alias target", allowed_alias_tvars=allowed_alias_tvars, + erase_tvar_defs=erase_tvar_defs, alias_type_params_names=alias_type_params_names, python_3_12_type_alias=python_3_12_type_alias, ) @@ -220,6 +222,7 @@ def __init__( prohibit_self_type: str | None = None, prohibit_special_class_field_types: str | None = None, allowed_alias_tvars: list[TypeVarLikeType] | None = None, + erase_tvar_defs: list[TypeVarType] | None = None, allow_type_any: bool = False, alias_type_params_names: list[str] | None = None, analyzing_tvar_def: bool = False, @@ -240,6 +243,11 @@ def __init__( if allowed_alias_tvars is None: allowed_alias_tvars = [] self.allowed_alias_tvars = allowed_alias_tvars + # Should we erase some type variables? This can be used to mass-erase type + # variables that were found to be invalid at the class/alias definition. + if erase_tvar_defs is None: + erase_tvar_defs = [] + self.erase_tvar_defs = erase_tvar_defs self.alias_type_params_names = alias_type_params_names # If false, record incomplete ref if we generate PlaceholderType. self.allow_placeholder = allow_placeholder @@ -403,6 +411,13 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) msg = f'Can\'t use bound type variable "{t.name}" to define generic alias' self.fail(msg, t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) + if ( + isinstance(sym.node, TypeVarExpr) + and tvar_def is not None + and tvar_def in self.erase_tvar_defs + ): + # The caller should have already given a relevant error. + return AnyType(TypeOfAny.from_error) if isinstance(sym.node, TypeVarExpr) and tvar_def is not None: assert isinstance(tvar_def, TypeVarType) if len(t.args) > 0: @@ -2108,7 +2123,10 @@ def fix_instance( """ used_default = False arg_count = len(t.args) - min_tv_count = sum(not tv.has_default() for tv in t.type.defn.type_vars) + min_tv_count = sum( + not tv.has_default() and not isinstance(tv, TypeVarTupleType) + for tv in t.type.defn.type_vars + ) max_tv_count = len(t.type.type_vars) if arg_count < min_tv_count or arg_count > max_tv_count: # Don't use existing args if arg_count doesn't match @@ -2117,9 +2135,10 @@ def fix_instance( disallow_any = False t.args = () - args: list[Type] = [*(t.args[:max_tv_count])] + args: list[Type] = list(t.args) any_type: AnyType | None = None env: dict[TypeVarId, Type] = {} + tvt_no_default = False for tv, arg in itertools.zip_longest(t.type.defn.type_vars, t.args, fillvalue=None): if tv is None: @@ -2152,16 +2171,27 @@ def fix_instance( arg = any_type else: assert arg is not None - with state.strict_optional_set(options.strict_optional): - # Gradually expand defaults, as they may depend on previous variables. - if tv.has_default(): - arg = expand_type(arg, env) - env[tv.id] = arg - args.append(arg) + if use_any and isinstance(tv, TypeVarTupleType): + tvt_no_default = True + # Default such as *tuple[int, str] should be unpacked into individual items. + if isinstance(arg, UnpackType) and isinstance( + unpack := get_proper_type(arg.type), TupleType + ): + unpacked = unpack.items + else: + unpacked = [arg] + for arg in unpacked: + with state.strict_optional_set(options.strict_optional): + # Gradually expand defaults, as they may depend on previous variables. + if tv.has_default(): + arg = expand_type(arg, env) + env[tv.id] = arg + args.append(arg) else: env[tv.id] = arg t.args = tuple(args) - fix_type_var_tuple_argument(t) + if tvt_no_default: + fix_type_var_tuple_argument(t) return used_default @@ -2380,15 +2410,22 @@ def set_any_tvars( else: arg = any_type used_any_type = True - if isinstance(tv, TypeVarTupleType): - # TODO Handle TypeVarTuple defaults + if used_any_type and isinstance(tv, TypeVarTupleType): arg = UnpackType(Instance(tv.tuple_fallback.type, [any_type])) - with state.strict_optional_set(options.strict_optional): - # Gradually expand defaults, as they may depend on previous variables. - if tv.has_default(): - arg = expand_type(arg, env) - env[tv.id] = arg - args.append(arg) + # Default such as *tuple[int, str] should be unpacked into individual items. + if isinstance(arg, UnpackType) and isinstance( + unpack := get_proper_type(arg.type), TupleType + ): + unpacked = unpack.items + else: + unpacked = [arg] + for arg in unpacked: + with state.strict_optional_set(options.strict_optional): + # Gradually expand defaults, as they may depend on previous variables. + if tv.has_default(): + arg = expand_type(arg, env) + env[tv.id] = arg + args.append(arg) else: env[tv.id] = arg t = TypeAliasType(node, args, newline, newcolumn) diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 38fa39d39493a..d52cb575bc916 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -140,7 +140,7 @@ reveal_type(func_b1(callback1)) # N: Revealed type is "def (x: builtins.str)" reveal_type(func_b1(2)) # N: Revealed type is "def (builtins.int, builtins.str)" def func_c1[*Ts = *tuple[int, str]](x: int | Callable[[*Ts], None]) -> tuple[*Ts]: ... -# reveal_type(func_c1(callback1)) # Revealed type is "Tuple[str]" # TODO +reveal_type(func_c1(callback1)) # N: Revealed type is "tuple[builtins.str]" reveal_type(func_c1(2)) # N: Revealed type is "tuple[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] @@ -195,7 +195,7 @@ def func_c1( a: ClassC1, b: ClassC1[float], ) -> None: - # reveal_type(a) # Revealed type is "__main__.ClassC1[builtins.int, builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "__main__.ClassC1[builtins.int, builtins.str]" reveal_type(b) # N: Revealed type is "__main__.ClassC1[builtins.float]" k = ClassC1() @@ -250,7 +250,7 @@ def func_c1( a: TC1, b: TC1[float], ) -> None: - # reveal_type(a) # Revealed type is "Tuple[builtins.int, builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "tuple[builtins.int, builtins.str]" reveal_type(b) # N: Revealed type is "tuple[builtins.float]" [builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 2f454b70747f9..a5cdaea16105c 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -362,7 +362,7 @@ def func_c1( a: ClassC1, b: ClassC1[float], ) -> None: - # reveal_type(a) # Revealed type is "__main__.ClassC1[builtins.int, builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "__main__.ClassC1[builtins.int, builtins.str]" reveal_type(b) # N: Revealed type is "__main__.ClassC1[builtins.float]" k = ClassC1() @@ -375,18 +375,14 @@ class ClassC2(Generic[T3, Unpack[Ts3]]): ... def func_c2( a: ClassC2, b: ClassC2[int], - c: ClassC2[int, Unpack[Tuple[()]]], ) -> None: reveal_type(a) # N: Revealed type is "__main__.ClassC2[builtins.str, Unpack[builtins.tuple[builtins.float, ...]]]" - # reveal_type(b) # Revealed type is "__main__.ClassC2[builtins.int, Unpack[builtins.tuple[builtins.float, ...]]]" # TODO - reveal_type(c) # N: Revealed type is "__main__.ClassC2[builtins.int]" + reveal_type(b) # N: Revealed type is "__main__.ClassC2[builtins.int]" k = ClassC2() reveal_type(k) # N: Revealed type is "__main__.ClassC2[builtins.str, Unpack[builtins.tuple[builtins.float, ...]]]" l = ClassC2[int]() - # reveal_type(l) # Revealed type is "__main__.ClassC2[builtins.int, Unpack[builtins.tuple[builtins.float, ...]]]" # TODO - m = ClassC2[int, Unpack[Tuple[()]]]() - reveal_type(m) # N: Revealed type is "__main__.ClassC2[builtins.int]" + reveal_type(l) # N: Revealed type is "__main__.ClassC2[builtins.int]" class ClassC3(Generic[T3, Unpack[Ts4]]): ... @@ -395,7 +391,7 @@ def func_c3( b: ClassC3[int], c: ClassC3[int, Unpack[Tuple[float]]] ) -> None: - # reveal_type(a) # Revealed type is "__main__.ClassC3[builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "__main__.ClassC3[builtins.str]" reveal_type(b) # N: Revealed type is "__main__.ClassC3[builtins.int]" reveal_type(c) # N: Revealed type is "__main__.ClassC3[builtins.int, builtins.float]" @@ -406,21 +402,24 @@ def func_c3( m = ClassC3[int, Unpack[Tuple[float]]]() reveal_type(m) # N: Revealed type is "__main__.ClassC3[builtins.int, builtins.float]" -class ClassC4(Generic[T1, Unpack[Ts1], T3]): ... +class ClassC4(Generic[T1, Unpack[Ts1], T3]): # E: A type variable with default cannot follow TypeVarTuple + x: T3 + +reveal_type(ClassC4().x) # N: Revealed type is "Any" def func_c4( a: ClassC4, # E: Missing type arguments for generic type "ClassC4" b: ClassC4[int], c: ClassC4[int, float], ) -> None: - reveal_type(a) # N: Revealed type is "__main__.ClassC4[Any, Unpack[builtins.tuple[Any, ...]], builtins.str]" - # reveal_type(b) # Revealed type is "__main__.ClassC4[builtins.int, builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "__main__.ClassC4[Any, Unpack[builtins.tuple[Any, ...]]]" + reveal_type(b) # N: Revealed type is "__main__.ClassC4[builtins.int]" reveal_type(c) # N: Revealed type is "__main__.ClassC4[builtins.int, builtins.float]" k = ClassC4() # E: Need type annotation for "k" - reveal_type(k) # N: Revealed type is "__main__.ClassC4[Any, Unpack[builtins.tuple[Any, ...]], builtins.str]" + reveal_type(k) # N: Revealed type is "__main__.ClassC4[Any, Unpack[builtins.tuple[Any, ...]]]" l = ClassC4[int]() - # reveal_type(l) # Revealed type is "__main__.ClassC4[builtins.int, builtins.str]" # TODO + reveal_type(l) # N: Revealed type is "__main__.ClassC4[builtins.int]" m = ClassC4[int, float]() reveal_type(m) # N: Revealed type is "__main__.ClassC4[builtins.int, builtins.float]" [builtins fixtures/tuple.pyi] @@ -678,7 +677,7 @@ def func_c1( a: TC1, b: TC1[float], ) -> None: - # reveal_type(a) # Revealed type is "Tuple[builtins.int, builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "tuple[builtins.int, builtins.str]" reveal_type(b) # N: Revealed type is "tuple[builtins.float]" TC2 = Tuple[T3, Unpack[Ts3]] @@ -686,11 +685,9 @@ TC2 = Tuple[T3, Unpack[Ts3]] def func_c2( a: TC2, b: TC2[int], - c: TC2[int, Unpack[Tuple[()]]], ) -> None: - # reveal_type(a) # Revealed type is "Tuple[builtins.str, Unpack[builtins.tuple[builtins.float, ...]]]" # TODO - # reveal_type(b) # Revealed type is "Tuple[builtins.int, Unpack[builtins.tuple[builtins.float, ...]]]" # TODO - reveal_type(c) # N: Revealed type is "tuple[builtins.int]" + reveal_type(a) # N: Revealed type is "tuple[builtins.str, Unpack[builtins.tuple[builtins.float, ...]]]" + reveal_type(b) # N: Revealed type is "tuple[builtins.int]" TC3 = Tuple[T3, Unpack[Ts4]] @@ -699,20 +696,20 @@ def func_c3( b: TC3[int], c: TC3[int, Unpack[Tuple[float]]], ) -> None: - # reveal_type(a) # Revealed type is "Tuple[builtins.str]" # TODO + reveal_type(a) # N: Revealed type is "tuple[builtins.str]" reveal_type(b) # N: Revealed type is "tuple[builtins.int]" reveal_type(c) # N: Revealed type is "tuple[builtins.int, builtins.float]" -TC4 = Tuple[T1, Unpack[Ts1], T3] +TC4 = Tuple[T1, Unpack[Ts1], T3] # E: A type variable with default cannot follow TypeVarTuple def func_c4( a: TC4, # E: Missing type arguments for generic type "TC4" b: TC4[int], c: TC4[int, float], ) -> None: - reveal_type(a) # N: Revealed type is "tuple[Any, Unpack[builtins.tuple[Any, ...]], builtins.str]" - # reveal_type(b) # Revealed type is "Tuple[builtins.int, builtins.str]" # TODO - reveal_type(c) # N: Revealed type is "tuple[builtins.int, builtins.float]" + reveal_type(a) # N: Revealed type is "tuple[Any, Unpack[builtins.tuple[Any, ...]], Any]" + reveal_type(b) # N: Revealed type is "tuple[builtins.int, Any]" + reveal_type(c) # N: Revealed type is "tuple[builtins.int, builtins.float, Any]" [builtins fixtures/tuple.pyi] [case testTypeVarDefaultsTypeAliasRecursive1] From 5f9371e7bd79428c1565f5841aa4859cb148fe24 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 29 May 2026 00:52:52 +0100 Subject: [PATCH 050/127] Fix crash on deferred generic class nested in function (#21557) Fixes https://github.com/python/mypy/issues/21550 Fix is straightforward: do what we do in similar cases elsewhere -- do not store type until it has no placeholders. I am quite sure we don't need to defer in this case, as the only way this can happen is when the enclosing class will call `defer()` anyway. --- mypy/semanal.py | 3 ++- test-data/unit/check-newsemanal.test | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e3524f2c7db68..92f5c3d44333b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1156,7 +1156,8 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type: leading_type = fill_typevars(info) if func.is_class or func.name == "__new__": leading_type = self.class_type(leading_type) - func.type = replace_implicit_first_type(functype, leading_type) + if not has_placeholder(leading_type): + func.type = replace_implicit_first_type(functype, leading_type) elif has_self_type and isinstance(func.unanalyzed_type, CallableType): if not isinstance(get_proper_type(func.unanalyzed_type.arg_types[0]), AnyType): if self.is_expected_self_type( diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test index b0b01ad3a593a..cfb59a9f8e9ba 100644 --- a/test-data/unit/check-newsemanal.test +++ b/test-data/unit/check-newsemanal.test @@ -3228,3 +3228,16 @@ def deco(fn: Callable[[], T]) -> Callable[[], T]: ... @deco def defer() -> int: ... + +[case testNestedClassSelfNoPlaceholder] +from typing import Generic, TypeVar + +def test() -> None: + T = TypeVar("T", bound="Model") + + class Query(Generic[T]): + def filter(self, value: T) -> None: + ... + + class Model: + pass From c2fd9d333127b3f39cdb9ef53288b97bb24d58c4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 29 May 2026 01:38:41 +0100 Subject: [PATCH 051/127] Fix crash on unhandled meet variadic tuple vs indtance (#21558) Fixes https://github.com/python/mypy/issues/20526 Fix is straightforward: handle previously unhandled scenario. --- mypy/meet.py | 23 ++++++++++++++++++++++- test-data/unit/check-typeis.test | 3 +-- test-data/unit/check-typevar-tuple.test | 12 ++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index 18b2732c55932..6c05d449abe5c 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1090,7 +1090,28 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: elif isinstance(self.s, Instance): # meet(Tuple[t1, t2, <...>], Tuple[s, ...]) == Tuple[meet(t1, s), meet(t2, s), <...>]. if self.s.type.fullname in TUPLE_LIKE_INSTANCE_NAMES and self.s.args: - return t.copy_modified(items=[meet_types(it, self.s.args[0]) for it in t.items]) + arg = self.s.args[0] + new_items: list[Type] = [] + for it in t.items: + # Unpack items need to be handled by the caller. + if isinstance(it, UnpackType): + unpacked = get_proper_type(it.type) + if isinstance(unpacked, TypeVarTupleType): + # We can't infer anything in this case. + new_arg = UninhabitedType() + instance = unpacked.tuple_fallback + else: + assert ( + isinstance(unpacked, Instance) + and unpacked.type.fullname == "builtins.tuple" + ) + new_arg = meet_types(unpacked.args[0], arg) + instance = unpacked + new_items.append(UnpackType(instance.copy_modified(args=[new_arg]))) + else: + # All other items can be processed in a regular way. + new_items.append(meet_types(it, arg)) + return t.copy_modified(items=new_items) elif is_proper_subtype(t, self.s): # A named tuple that inherits from a normal class return t diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index b6c6b5ce0011e..65ee837452f5b 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -213,8 +213,7 @@ def test5(t: tuple[A | B, ...]) -> None: def test6(t: tuple[B, Unpack[tuple[A | B, ...]], B]) -> None: if is_tuple_of_B(t): - # Should this be tuple[B, *tuple[B, ...], B] - reveal_type(t) # N: Revealed type is "tuple[__main__.B, Never, __main__.B]" + reveal_type(t) # N: Revealed type is "tuple[__main__.B, Unpack[builtins.tuple[__main__.B, ...]], __main__.B]" else: reveal_type(t) # N: Revealed type is "tuple[__main__.B, Unpack[builtins.tuple[__main__.A | __main__.B, ...]], __main__.B]" [builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 0119728e5834e..3f0765ba5c770 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2873,3 +2873,15 @@ reveal_type(x) # N: Revealed type is "builtins.list[tuple[()]]" reveal_type(y) # N: Revealed type is "builtins.list[tuple[()]]" reveal_type(z) # N: Revealed type is "builtins.list[tuple[()]]" [builtins fixtures/tuple.pyi] + +[case testMeetTupleWithUnpackVsInstanceWorks_no_verbose_reveal] +from typing import TypeVar, Unpack, Union + +K = TypeVar("K") +Prefix = tuple[Union[K, list[int]], ...] + +def g(x: K) -> None: + d: Union[Prefix[K], tuple[Unpack[Prefix[int]], int], None] = None + assert d is not None + reveal_type(d) # N: Revealed type is "tuple[K | list[int], ...] | tuple[*tuple[int | list[int], ...], int]" +[builtins fixtures/tuple.pyi] From 94838b0f9c3b37af02e1924b60254a49bb138eb7 Mon Sep 17 00:00:00 2001 From: Jingchen Ye <11172084+97littleleaf11@users.noreply.github.com> Date: Fri, 29 May 2026 19:19:31 +0800 Subject: [PATCH 052/127] Fix TypedDict indexing with literal keys in comprehensions (#21556) Fixes #19317 Uses exsiting index narrowing in for-loop. Previous PR: https://github.com/python/mypy/pull/18014 --- mypy/semanal.py | 2 +- test-data/unit/check-typeddict.test | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 92f5c3d44333b..b0294c782ab98 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6433,7 +6433,7 @@ def analyze_comp_for(self, expr: GeneratorExpr | DictionaryComprehension) -> Non if i > 0: sequence.accept(self) # Bind index variables. - self.analyze_lvalue(index) + self.analyze_lvalue(index, is_index_var=True) for cond in conditions: cond.accept(self) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 17a1fea22ef04..a2fdb5054829f 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -779,6 +779,17 @@ def get_coordinate(p: TaggedPoint, key: str) -> Union[str, int]: [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testCanGetItemOfTypedDictWithStringLiteralKeyInComprehension] +from typing import TypedDict +Data = TypedDict('Data', {'field1': int, 'field2': str, 'field3': str}) +data: Data +def is_str(value: object) -> bool: ... +filtered_keys1 = [data[key] for key in ("field1", "field2", "field3")] +filtered_keys2 = [key for key in ("field1", "field2", "field3") if is_str(data[key])] +reveal_type(filtered_keys1) # N: Revealed type is "builtins.list[builtins.int | builtins.str]" +reveal_type(filtered_keys2) # N: Revealed type is "builtins.list[builtins.str]" +[builtins fixtures/for.pyi] +[typing fixtures/typing-full.pyi] -- Special Method: __setitem__ From 49ccd26eb29e8937b70e4991cd2fe41835dc2ad7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 29 May 2026 19:17:13 +0100 Subject: [PATCH 053/127] Fix crashes on variadic unpacking in synthetic types (#21555) Fixes https://github.com/python/mypy/issues/21237 (and similar crashes). The problem is that we cannot do some of the type argument normalization early in `typeanal.py` (since that will cause crashes on some pathological recursive aliases), so we do it in `semanal_typeargs.py` later. However, the mixed traverser visitor is somewhat incomplete (most notably w.r.t. generated methods). I add (most of) the missing parts. --- mypy/mixedtraverser.py | 38 ++++++++++++++++++++--------- mypy/typetraverser.py | 6 ++++- test-data/unit/check-python312.test | 24 ++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/mypy/mixedtraverser.py b/mypy/mixedtraverser.py index 535391886b020..40f3e640ef032 100644 --- a/mypy/mixedtraverser.py +++ b/mypy/mixedtraverser.py @@ -16,6 +16,7 @@ TypeApplication, TypedDictExpr, TypeFormExpr, + TypeInfo, TypeVarExpr, Var, WithStmt, @@ -41,15 +42,29 @@ def visit_func(self, o: FuncItem, /) -> None: self.visit_optional_type(o.type) def visit_class_def(self, o: ClassDef, /) -> None: - # TODO: Should we visit generated methods/variables as well, either here or in - # TraverserVisitor? super().visit_class_def(o) - info = o.info - if info: - for base in info.bases: - base.accept(self) - if info.special_alias: - info.special_alias.accept(self) + if o.info: + self.process_type_info(o.info) + + def process_type_info(self, info: TypeInfo) -> None: + # TODO: Should we visit generated methods/variables as well? + # We should for methods generated by us (see below). But it is less clear for + # 3rd party plugin generated methods (since we don't want to emit errors there). + for base in info.bases: + base.accept(self) + if info.special_alias: + # We need to accept all types that are conceptually identical like special + # alias target and corresponding tuple_type or typeddict_type, since those + # may be copies, and not the same object. + info.special_alias.accept(self) + if info.tuple_type: + info.tuple_type.accept(self) + if info.typeddict_type: + info.typeddict_type.accept(self) + if info.is_named_tuple or info.is_newtype: + for sym in info.names.values(): + if sym.plugin_generated and sym.node: + sym.node.accept(self) def visit_type_alias_expr(self, o: TypeAliasExpr, /) -> None: super().visit_type_alias_expr(o) @@ -64,12 +79,11 @@ def visit_type_var_expr(self, o: TypeVarExpr, /) -> None: def visit_typeddict_expr(self, o: TypedDictExpr, /) -> None: super().visit_typeddict_expr(o) - self.visit_optional_type(o.info.typeddict_type) + self.process_type_info(o.info) def visit_namedtuple_expr(self, o: NamedTupleExpr, /) -> None: super().visit_namedtuple_expr(o) - assert o.info.tuple_type - o.info.tuple_type.accept(self) + self.process_type_info(o.info) def visit__promote_expr(self, o: PromoteExpr, /) -> None: super().visit__promote_expr(o) @@ -77,6 +91,8 @@ def visit__promote_expr(self, o: PromoteExpr, /) -> None: def visit_newtype_expr(self, o: NewTypeExpr, /) -> None: super().visit_newtype_expr(o) + if o.info: + self.process_type_info(o.info) self.visit_optional_type(o.old_type) # Statements diff --git a/mypy/typetraverser.py b/mypy/typetraverser.py index 2a7f41bd97f21..9cbf9d95067a9 100644 --- a/mypy/typetraverser.py +++ b/mypy/typetraverser.py @@ -82,7 +82,11 @@ def visit_instance(self, t: Instance, /) -> None: self.traverse_type_tuple(t.args) def visit_callable_type(self, t: CallableType, /) -> None: - # FIX generics + for tv in t.variables: + tv.upper_bound.accept(self) + if isinstance(tv, TypeVarType): + for v in tv.values: + v.accept(self) self.traverse_type_list(t.arg_types) t.ret_type.accept(self) t.fallback.accept(self) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index c4a2320797464..23626ccd0a938 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2293,3 +2293,27 @@ reveal_type(x) # N: Revealed type is "__main__.C[()]" y: T[bool] reveal_type(y) # N: Revealed type is "__main__.C[()]" [builtins fixtures/tuple.pyi] + +[case testTupleBaseTupleTypeUnpack] +class X(tuple[*tuple[int], *tuple[int]]): + pass +x = X((1, 2)) +reveal_type(x) # N: Revealed type is "tuple[builtins.int, builtins.int, fallback=__main__.X]" +[builtins fixtures/tuple.pyi] + +[case testNewTypeTupleTypeUnpack] +from typing import NewType + +T = NewType("T", tuple[*tuple[int], *tuple[int]]) +t = T((1, 2)) +reveal_type(t) # N: Revealed type is "tuple[builtins.int, builtins.int, fallback=__main__.T]" +[builtins fixtures/tuple.pyi] + +[case testNamedTupleTupleTypeUnpack] +from typing import NamedTuple + +class N(NamedTuple): + x: tuple[*tuple[int], *tuple[int]] +n = N((1, 2)) +reveal_type(n) # N: Revealed type is "tuple[tuple[builtins.int, builtins.int], fallback=__main__.N]" +[builtins fixtures/tuple.pyi] From f59f649b4dc54db53c4fa573412074c59c559eb9 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 29 May 2026 16:23:50 -0700 Subject: [PATCH 054/127] Fix changelog for mypy 2.1 (#21565) Fixes #21561 Co-authored-by: Codex --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d01af76edf0e0..ac2cb8036dc71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Contributed by Jukka Lehtosalo (PR [21433](https://github.com/python/mypy/pull/2 ### Mypyc Improvements +- Enable incremental self-compilation (Vaggelis Danias, PR [21369](https://github.com/python/mypy/pull/21369)) - Make compilation order with multiple files consistent (Piotr Sawicki, PR [21419](https://github.com/python/mypy/pull/21419)) - Fix crash on accessing `StopAsyncIteration` (Piotr Sawicki, PR [21406](https://github.com/python/mypy/pull/21406)) - Fix incremental compilation with `separate` flag (Vaggelis Danias, PR [21299](https://github.com/python/mypy/pull/21299)) @@ -54,7 +55,6 @@ Contributed by Jukka Lehtosalo (PR [21433](https://github.com/python/mypy/pull/2 ### Other Notable Fixes and Improvements - Rely on typeshed stubs for `slice` typing (Ivan Levkivskyi, PR [21401](https://github.com/python/mypy/pull/21401)) -- Improve negative narrowing for membership checks on tuples (Shantanu, PR [21456](https://github.com/python/mypy/pull/21456)) - Narrow match captures based on previous cases (Shantanu, PR [21405](https://github.com/python/mypy/pull/21405)) - Fix nondeterminism in overload resolution (Shantanu, PR [21455](https://github.com/python/mypy/pull/21455)) - Respect file config comments for stale modules (Adam Turner, PR [21444](https://github.com/python/mypy/pull/21444)) @@ -62,7 +62,6 @@ Contributed by Jukka Lehtosalo (PR [21433](https://github.com/python/mypy/pull/2 - Fix type variable with values as a supertype (Ivan Levkivskyi, PR [21431](https://github.com/python/mypy/pull/21431)) - Add support for configuring `--num-workers` with an environment variable (Kevin Kannammalil, PR [21407](https://github.com/python/mypy/pull/21407)) - Respect JSON output mode for syntax errors (Adam Turner, PR [21386](https://github.com/python/mypy/pull/21386)) -- Analyze `TypedDict` decorators (Pranav Manglik, PR [21267](https://github.com/python/mypy/pull/21267)) ### Typeshed Updates From 8f2d0f12ff1e475889d3a2f4de9b1e87a18425d2 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 1 Jun 2026 13:41:44 +0300 Subject: [PATCH 055/127] [mypyc] Add `librt.strings.toupper` and `tolower` codepoint primitives (#21553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6th PR of #21418. This PR introduces two `i32 -> i32` case-conversion helpers, alongside the existing classifiers. **The constraint to flag**: A single i32 holds one codepoint, but some Unicode case mappings expand to multiple e.g `'ß'.upper()` becomes `'SS'`, `'fi'.upper()` becomes `'FI'` etc. For those inputs the primitive _returns the input unchanged_; This is the same split CPython makes between `Py_UNICODE_TOUPPER` (codepoint) and `str.upper()` (string), with the former returning the **first codepoint** of the expansion. Users needing full Unicode case conversion should call `s.upper()` / `s.lower()` on the string, for which we already have mypyc primitives (#20948). For ASCII benchmarks, the codepoint primitives are ~5x faster than their `str` counterparts, avoiding the 1-char allocation. --- mypy/typeshed/stubs/librt/librt/strings.pyi | 9 ++++ mypyc/lib-rt/strings/librt_strings.c | 18 ++++++++ mypyc/lib-rt/strings/librt_strings.h | 44 ++++++++++++++++++ mypyc/primitives/librt_strings_ops.py | 23 ++++++++++ mypyc/test-data/irbuild-librt-strings.test | 26 +++++++++++ mypyc/test-data/run-librt-strings.test | 49 +++++++++++++++++++++ 6 files changed, 169 insertions(+) diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 7a028f9e7859e..94e3b69abf243 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -48,3 +48,12 @@ def isdigit(c: i32, /) -> bool: ... def isalnum(c: i32, /) -> bool: ... def isalpha(c: i32, /) -> bool: ... def isidentifier(c: i32, /) -> bool: ... + +# Codepoint case conversion. For the rare codepoints whose Unicode +# uppercase / lowercase expands to multiple codepoints (e.g. U+00DF +# uppercases to "SS", U+FB01 to "FI"), returns the input unchanged so +# the signature stays i32 -> i32. Use str.upper() / str.lower() for full +# Unicode case conversion when those cases matter. Negative inputs are +# returned unchanged. +def toupper(c: i32, /) -> i32: ... +def tolower(c: i32, /) -> i32: ... diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index d95d5afb48600..b79ffcd9f7c89 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -1191,6 +1191,18 @@ DEFINE_CP_BOOL_WRAPPER(isalnum, LibRTStrings_IsAlnum) DEFINE_CP_BOOL_WRAPPER(isalpha, LibRTStrings_IsAlpha) DEFINE_CP_BOOL_WRAPPER(isidentifier, LibRTStrings_IsIdentifier) +#define DEFINE_CP_I32_WRAPPER(name, fn) \ + static PyObject* \ + cp_##name(PyObject *module, PyObject *arg) { \ + int32_t c; \ + if (cp_parse_i32(arg, &c) < 0) \ + return NULL; \ + return PyLong_FromLong((long) fn(c)); \ + } + +DEFINE_CP_I32_WRAPPER(toupper, LibRTStrings_ToUpper) +DEFINE_CP_I32_WRAPPER(tolower, LibRTStrings_ToLower) + static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, PyDoc_STR("Write a 16-bit signed integer to BytesWriter in little-endian format") @@ -1267,6 +1279,12 @@ static PyMethodDef librt_strings_module_methods[] = { {"isidentifier", cp_isidentifier, METH_O, PyDoc_STR("Test whether a codepoint (i32) is a valid identifier start (XID_Start).") }, + {"toupper", cp_toupper, METH_O, + PyDoc_STR("Single-codepoint uppercase mapping for a codepoint (i32). Returns the input unchanged if the Unicode uppercase expands to multiple codepoints (e.g. U+00DF uppercases to \"SS\"); use str.upper() for full Unicode case conversion.") + }, + {"tolower", cp_tolower, METH_O, + PyDoc_STR("Single-codepoint lowercase mapping for a codepoint (i32). Returns the input unchanged if the Unicode lowercase expands to multiple codepoints; use str.lower() for full Unicode case conversion.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/lib-rt/strings/librt_strings.h b/mypyc/lib-rt/strings/librt_strings.h index c3cbd2f2237a6..6c1942667ba44 100644 --- a/mypyc/lib-rt/strings/librt_strings.h +++ b/mypyc/lib-rt/strings/librt_strings.h @@ -73,4 +73,48 @@ static inline bool LibRTStrings_IsIdentifier(int32_t c) { return r == 1; } +// Shared slow path for LibRTStrings_ToUpper / _ToLower. Round-trips the +// codepoint through CPython's str.upper / str.lower on a 1-character +// string. When the conversion expands to multiple codepoints (e.g. +// 'ß'.upper() == 'SS') we return the input unchanged so the public +// helpers stay i32 -> i32. Aborts via CPyError_OutOfMemory on allocation +// failure. +static inline int32_t LibRTStrings_ChangeCase_slow(int32_t c, const char *method) { + PyObject *s = PyUnicode_FromOrdinal((int)c); + if (s == NULL) { + CPyError_OutOfMemory(); + } + PyObject *u = PyObject_CallMethod(s, method, NULL); + Py_DECREF(s); + if (u == NULL) { + CPyError_OutOfMemory(); + } + int32_t result = c; + if (PyUnicode_GET_LENGTH(u) == 1) { + result = (int32_t)PyUnicode_READ_CHAR(u, 0); + } + Py_DECREF(u); + return result; +} + +// Uppercase a codepoint. ASCII fast path is `a..z -> A..Z` (subtract 32); +// non-ASCII delegates to str.upper on a 1-character string. Returns the +// input unchanged when uppercasing expands to multiple codepoints. +static inline int32_t LibRTStrings_ToUpper(int32_t c) { + if (c < 0) return c; + if (c >= 'a' && c <= 'z') return c - 32; + if (c < 128) return c; + return LibRTStrings_ChangeCase_slow(c, "upper"); +} + +// Lowercase a codepoint. ASCII fast path is `A..Z -> a..z` (add 32); +// non-ASCII delegates to str.lower on a 1-character string. Returns the +// input unchanged when lowercasing expands to multiple codepoints. +static inline int32_t LibRTStrings_ToLower(int32_t c) { + if (c < 0) return c; + if (c >= 'A' && c <= 'Z') return c + 32; + if (c < 128) return c; + return LibRTStrings_ChangeCase_slow(c, "lower"); +} + #endif // LIBRT_STRINGS_H diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index f025c6e95b718..f3fceb483f968 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -438,3 +438,26 @@ error_kind=ERR_NEVER, dependencies=[LIBRT_STRINGS], ) + +# Codepoint case conversion. When the Unicode uppercase/lowercase of a +# codepoint expands to multiple codepoints (e.g. U+00DF uppercases to "SS", +# U+FB01 to "FI"), returns the input unchanged so the signature stays +# i32 -> i32; callers needing full Unicode case conversion should use +# str.upper() / .lower() instead. Negative inputs are returned unchanged. +function_op( + name="librt.strings.toupper", + arg_types=[int32_rprimitive], + return_type=int32_rprimitive, + c_function_name="LibRTStrings_ToUpper", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS], +) + +function_op( + name="librt.strings.tolower", + arg_types=[int32_rprimitive], + return_type=int32_rprimitive, + c_function_name="LibRTStrings_ToLower", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS], +) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index e3aaa49bd6f90..83523cb2468e0 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -401,3 +401,29 @@ def is_id(c): L0: r0 = LibRTStrings_IsIdentifier(c) return r0 + +[case testLibrtStringsToUpperIR] +from librt.strings import toupper +from mypy_extensions import i32 + +def up(c: i32) -> i32: + return toupper(c) +[out] +def up(c): + c, r0 :: i32 +L0: + r0 = LibRTStrings_ToUpper(c) + return r0 + +[case testLibrtStringsToLowerIR] +from librt.strings import tolower +from mypy_extensions import i32 + +def lo(c: i32) -> i32: + return tolower(c) +[out] +def lo(c): + c, r0 :: i32 +L0: + r0 = LibRTStrings_ToLower(c) + return r0 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 0a3320ff6522e..7efff12667d87 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1490,3 +1490,52 @@ def test_codepoint_classifiers_via_any() -> None: f(1 << 40) with assertRaises(OverflowError, "codepoint out of i32 range"): f(-(1 << 40)) + +[case testLibrtStringsCodepointCaseConversion_librt] +from typing import Any +from mypy_extensions import i32 +from librt.strings import toupper, tolower + +from testutil import assertRaises + + +def _expect(c: str, method: str) -> int: + # The contract: i32 -> i32 when conversion yields exactly one codepoint, + # else return the input unchanged. + converted = getattr(c, method)() + if len(converted) == 1: + return ord(converted) + return ord(c) + + +def test_codepoint_case_conversion() -> None: + # Negative inputs return unchanged. + for bad in (i32(-1), i32(-113)): + assert toupper(bad) == bad + assert tolower(bad) == bad + # Agree with str.upper / str.lower across the full Unicode range + # whenever the conversion is single-codepoint; otherwise return input. + for i in range(0x110000): + c = chr(i) + o = ord(c) + assert toupper(o) == _expect(c, "upper") + assert tolower(o) == _expect(c, "lower") + + +def test_codepoint_case_conversion_via_any() -> None: + # Iterate to force generic dispatch through the PyMethodDef wrapper. + for fn, in_cp, out_cp in ( + (toupper, ord("a"), ord("A")), + (toupper, ord("A"), ord("A")), + (tolower, ord("Z"), ord("z")), + (tolower, ord("z"), ord("z")), + ): + f: Any = fn + assert f(in_cp) == out_cp + # Negative values are valid i32, returned unchanged. + assert f(-1) == -1 + # Inputs outside i32 range raise OverflowError through the wrapper. + with assertRaises(OverflowError, "codepoint out of i32 range"): + f(1 << 40) + with assertRaises(OverflowError, "codepoint out of i32 range"): + f(-(1 << 40)) From 70b74e1b6225b2beca374e4ae2b03825454b1e6f Mon Sep 17 00:00:00 2001 From: Ryan Heard Date: Mon, 1 Jun 2026 06:49:21 -0400 Subject: [PATCH 056/127] [mypyc] Use `method_sig` to get the method signature (#21567) Fixes #21566 `create_ne_from_eq()` checks whether the class has an `__eq__` method using `cls.has_method("__eq__")`, but `gen_glue_ne_method()` then requires `cls.get_method("__eq__")` to return a concrete `FuncIR`. In this package-cycle case, the inherited method declaration exists, but the concrete `FuncIR` body is not available yet. This makes `cls.has_method("__eq__")` true while `cls.get_method("__eq__")` returns None. --- mypyc/irbuild/classdef.py | 4 +--- mypyc/test-data/run-multimodule.test | 35 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index f5d094d142317..d0690979bf31f 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -862,9 +862,7 @@ def create_ne_from_eq(builder: IRBuilder, cdef: ClassDef) -> None: def gen_glue_ne_method(builder: IRBuilder, cls: ClassIR, line: int) -> None: """Generate a "__ne__" method from a "__eq__" method.""" - func_ir = cls.get_method("__eq__") - assert func_ir - eq_sig = func_ir.decl.sig + eq_sig = cls.method_sig("__eq__") strict_typing = builder.options.strict_dunders_typing with builder.enter_method(cls, "__ne__", eq_sig.ret_type): rhs_type = eq_sig.args[0].type if strict_typing else object_rprimitive diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index 6ae6c0f2cab9b..ace1ab9fb1a12 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1710,6 +1710,41 @@ assert sum_range(0) == 0 assert sum_range(1) == 0 assert sum_range(5) == 10 +[case testPackageCycleInheritedEq] +-- Regression test for generating __ne__ from an inherited __eq__ when the +-- inherited method's declaration is available before its FuncIR body has been +-- generated. This can happen in package import cycles where __init__ imports a +-- child module before the subpackage that defines the base class. +from other.other_child import Child + +def make_child() -> Child: + return Child() + +[file other/__init__.py] +from other.other_child import Child as Child +from other.other_subpkg import Base as Base + +[file other/other_child.py] +from other.other_subpkg import Base + +class Child(Base): + pass + +[file other/other_subpkg/__init__.py] +from other.other_subpkg.other_base import Base as Base + +[file other/other_subpkg/other_base.py] +class Base: + def __eq__(self, other: object) -> bool: + return True + +[file driver.py] +from native import make_child +left = make_child() +right = make_child() +assert left == right +assert not (left != right) + [case testSeparateCrossGroupInheritedInit] -- Under separate=True, a subclass whose __init__ is inherited from a -- different group must call the base's CPyPy_ wrapper through the exports From cd75c4ec10254f4c98964308a0a1437274c7be4f Mon Sep 17 00:00:00 2001 From: Ryan Heard Date: Mon, 1 Jun 2026 11:09:48 -0400 Subject: [PATCH 057/127] [mypyc] Use `other` arg instead of `self` for RHS type (#21569) Fixes #21568 In `gen_glue_ne_method`, mypyc currently chooses the generated RHS argument type like this when strict dunder typing is enabled: ```python rhs_type = eq_sig.args[0].type if strict_typing else object_rprimitive ``` However, the `__eq__` signature includes `self`, so `eq_sig.args[0]` is the type of `self`, not the type of `other`. --------- Co-authored-by: Piotr Sawicki --- mypyc/irbuild/classdef.py | 2 +- mypyc/test-data/run-dunders.test | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index d0690979bf31f..3f95cf82c89c8 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -865,7 +865,7 @@ def gen_glue_ne_method(builder: IRBuilder, cls: ClassIR, line: int) -> None: eq_sig = cls.method_sig("__eq__") strict_typing = builder.options.strict_dunders_typing with builder.enter_method(cls, "__ne__", eq_sig.ret_type): - rhs_type = eq_sig.args[0].type if strict_typing else object_rprimitive + rhs_type = eq_sig.args[1].type rhs_arg = builder.add_argument("rhs", rhs_type) eqval = builder.add(MethodCall(builder.self(), "__eq__", [rhs_arg], line)) diff --git a/mypyc/test-data/run-dunders.test b/mypyc/test-data/run-dunders.test index a3ec06763d75b..d5907d959ccb1 100644 --- a/mypyc/test-data/run-dunders.test +++ b/mypyc/test-data/run-dunders.test @@ -1009,3 +1009,15 @@ def test_equality_with_implicit_ne() -> None: assert not eq(Eq(1), Eq(2)) assert ne(Eq(1), Eq(2)) assert not ne(Eq(1), Eq(1)) + +[case testDundersImplicitNeWithNarrowRhs] +class Accepted: + pass + +class Eq: + def __eq__(self, other: Accepted) -> bool: # type: ignore[override] + return True + +def test_implicit_ne_with_narrow_rhs() -> None: + assert Eq() == Accepted() + assert not (Eq() != Accepted()) From 4c8f9944ead815e264bcb71cd5f0e8eeb2a75689 Mon Sep 17 00:00:00 2001 From: Jingchen Ye <11172084+97littleleaf11@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:55:20 +0800 Subject: [PATCH 058/127] Fix constructor calls for union-bounded TypeVars (#21571) Fixes #21106 --- mypy/checkexpr.py | 24 ++++++++++++++++++------ test-data/unit/check-classes.test | 18 ++++++++++++++++++ test-data/unit/check-python310.test | 2 +- test-data/unit/check-python312.test | 14 ++++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 714ae12310094..9c1444ab96322 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1948,12 +1948,7 @@ def analyze_type_type_callee(self, item: ProperType, context: Context) -> Type: # but better than AnyType...), but replace the return type # with typevar. callee = self.analyze_type_type_callee(get_proper_type(item.upper_bound), context) - callee = get_proper_type(callee) - if isinstance(callee, CallableType): - callee = callee.copy_modified(ret_type=item) - elif isinstance(callee, Overloaded): - callee = Overloaded([c.copy_modified(ret_type=item) for c in callee.items]) - return callee + return self.replace_type_type_callee_ret_type(callee, item) # We support Type of namedtuples but not of tuples in general if isinstance(item, TupleType) and tuple_fallback(item).type.fullname != "builtins.tuple": return self.analyze_type_type_callee(tuple_fallback(item), context) @@ -1963,6 +1958,23 @@ def analyze_type_type_callee(self, item: ProperType, context: Context) -> Type: self.msg.unsupported_type_type(item, context) return AnyType(TypeOfAny.from_error) + def replace_type_type_callee_ret_type(self, callee: Type, ret_type: Type) -> Type: + callee = get_proper_type(callee) + if isinstance(callee, CallableType): + return callee.copy_modified(ret_type=ret_type) + if isinstance(callee, Overloaded): + return Overloaded([c.copy_modified(ret_type=ret_type) for c in callee.items]) + if isinstance(callee, UnionType): + return UnionType( + [ + self.replace_type_type_callee_ret_type(item, ret_type) + for item in callee.relevant_items() + ], + line=callee.line, + column=callee.column, + ) + return callee + def infer_arg_types_in_empty_context(self, args: list[Expression]) -> list[Type]: """Infer argument expression types in an empty context. diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index b830b99465e07..2cd43c74ebb5b 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -3889,6 +3889,24 @@ def process(cls: Type[U]): [builtins fixtures/classmethod.pyi] [out] +[case testTypeUsingTypeCConstructorReturnFromTypeVarUnionBound] +from typing import Optional, Type, TypeVar, Union + +class A: + def __init__(self, value: str = "") -> None: pass +class B: + def __init__(self, value: str = "") -> None: pass + +T = TypeVar("T", bound=Union[A, B]) + +def make(ftype: Type[T], value: Optional[str]) -> T: + if value is None: + return ftype() + return ftype(value) + +reveal_type(make(A, "a")) # N: Revealed type is "__main__.A" +reveal_type(make(B, None)) # N: Revealed type is "__main__.B" + [case testTypeUsingTypeCErrorUnsupportedType] from typing import Type, Tuple def foo(arg: Type[Tuple[int]]): diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 03a0934b662c6..2f5b2d7ce8e86 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -3531,7 +3531,7 @@ def switch(choice: type[T_Choice]) -> None: reveal_type(choice()) # N: Revealed type is "b.Two" case _: reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" - reveal_type(choice()) # N: Revealed type is "b.One | b.Two" + reveal_type(choice()) # N: Revealed type is "T_Choice`-1" [file b.py] class One: ... diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 23626ccd0a938..9d612109d5452 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -824,6 +824,20 @@ f(1, u) f('x', None) # E: Value of type variable "T" of "f" cannot be "str" \ # E: Value of type variable "S" of "f" cannot be "None" +[case testPEP695UpperBoundTypeTypeConstructorReturnType] +class A: + def __init__(self, value: str = "") -> None: pass +class B: + def __init__(self, value: str = "") -> None: pass + +def make[T: A | B](ftype: type[T], value: str | None) -> T: + if value is None: + return ftype() + return ftype(value) + +reveal_type(make(A, "a")) # N: Revealed type is "__main__.A" +reveal_type(make(B, None)) # N: Revealed type is "__main__.B" + [case testPEP695InferVarianceOfTupleType] class Cov[T](tuple[int, str]): def f(self) -> T: pass From 0ce09a170e4b6a57e32080fabeb786c9a1e02003 Mon Sep 17 00:00:00 2001 From: Alice Date: Tue, 2 Jun 2026 16:22:00 +0100 Subject: [PATCH 059/127] Implement support for closed TypedDicts (PEP 728) (#21382) Implement support for the closed keyword on TypedDicts (part of [PEP 728][]). Additionally, fix some preexisting issues that I came across while updating the logic. * Fixes #7435 * Fixes #7981 * Fixes #8714 * Fixes #12143 * Fixes #20401 * Partially addresses #18176 [PEP 728]: https://peps.python.org/pep-0728/ --- docs/source/typed_dict.rst | 116 +- mypy/checker.py | 67 +- mypy/checkexpr.py | 47 +- mypy/copytype.py | 2 +- mypy/expandtype.py | 9 +- mypy/exprtotype.py | 2 +- mypy/fastparse.py | 2 +- mypy/join.py | 64 +- mypy/meet.py | 78 +- mypy/nativeparse.py | 2 +- mypy/nodes.py | 43 + mypy/plugins/default.py | 7 +- mypy/semanal.py | 2 +- mypy/semanal_typeddict.py | 382 +++- mypy/server/astdiff.py | 2 +- mypy/stubgen.py | 15 +- mypy/subtypes.py | 65 +- mypy/type_visitor.py | 1 + mypy/typeanal.py | 7 +- mypy/types.py | 72 +- .../unit/check-parameter-specification.test | 16 +- test-data/unit/check-recursive-types.test | 4 +- test-data/unit/check-serialize.test | 14 + test-data/unit/check-typeddict.test | 1555 ++++++++++++++++- test-data/unit/diff.test | 18 + test-data/unit/fine-grained.test | 77 + test-data/unit/stubgen.test | 18 +- 27 files changed, 2431 insertions(+), 256 deletions(-) diff --git a/docs/source/typed_dict.rst b/docs/source/typed_dict.rst index d42b434b05b2d..a0d2dc81e1648 100644 --- a/docs/source/typed_dict.rst +++ b/docs/source/typed_dict.rst @@ -81,9 +81,9 @@ arbitrarily complex types. For example, you can define nested ``TypedDict``\s and containers with ``TypedDict`` items. Unlike most other types, mypy uses structural compatibility checking (or structural subtyping) with ``TypedDict``\s. A ``TypedDict`` object with -extra items is compatible with (a subtype of) a narrower +extra items can be compatible with (a subtype of) a narrower ``TypedDict``, assuming item types are compatible (*totality* also affects -subtyping, as discussed below). +subtyping, as does *closing*, as discussed below). A ``TypedDict`` object is not a subtype of the regular ``dict[...]`` type (and vice versa), since :py:class:`dict` allows arbitrary keys to be @@ -276,6 +276,84 @@ vary :ref:`covariantly `: m: Movie = {"name": "Jaws", "year": 1975} process_entry(m) # OK +You can override a read-only item with a compatible subtype, make a +read-only item mutable, and inherit from multiple parents with compatible +definitions: + +.. code-block:: python + + from collections.abc import Collection, Sequence + + class Competition(TypedDict): + hosts: ReadOnly[Collection[str]] + entries: ReadOnly[Sequence[Entry]] + + class MovieShow(TypedDict): + entries: list[Movie] + + class Oscars(Competition, MovieShow): + hosts: set[str] + +Defining ``hosts`` as a mutable ``set[str]`` item works as this is compatible +with the read-only ``Collection[str]`` definition in ``Competition``. +``entries`` will be of type ``list[Movie]``, taken from the ``MovieShow`` type, +as it is the only non-readonly definition, and is compatible with the definition +in ``Competition``. + +If an item is only defined in supertypes, and is always read-only, mypy takes +the definition from the first parent in the inheritance order, and raises an +error if any other parent definition is incompatible: + +.. code-block:: python + + class NameIds(TypedDict): + ids: ReadOnly[Collection[str]] + + class OrderedIds(TypedDict): + ids: ReadOnly[Sequence[int | str]] + + class OrderedNameIds(NameIds, OrderedIds): + pass # Error! Parent definitions incompatible + +In this example, the definition of ``ids`` will be taken from ``NameIds``, +which would not be compatible with the definition in ``OrderedIds``; reordering +the parents would not solve the problem. Instead, you will need to make a +compatible definition explicitly: + +.. code-block:: python + + class OrderedNameIds(NameIds, OrderedIds): + ids: ReadOnly[Sequence[str]] + +Closing +------- + +You can use the ``closed`` keyword, introduced to ``TypedDict`` in Python +3.15 (and available via ``typing_extensions.TypedDict`` in older +versions) to prevent structural subtypes from adding extra keys to a +type (:pep:`728`): + +.. code-block:: python + + HasName = TypedDict("HasName", {"name": str}) + HasOnlyName = TypedDict("HasOnlyName", {"name": str}, closed=True) + Movie = TypedDict("Movie", {"name": str, "year": int}) + + movie: Movie = {"name": "Nimona", "year": 2023} + has_name: HasName = movie # OK: type is open + has_only_name: HasOnlyName = movie # Error: type is closed + +This allows the typechecker to determine that certain operations are safe, +when they otherwise wouldn't be due to the potential presence of unknown +keys. + +The ``closed`` keyword can also be used in class-based syntax: + +.. code-block:: python + + class HasOnlyName(TypedDict, closed=True): + name: str + Unions of TypedDicts -------------------- @@ -289,6 +367,40 @@ need to give each TypedDict the same key where each value has a unique :ref:`Literal type `. Then, check that key to distinguish between your TypedDicts. +Alternatively, you can implement tagged unions with single-key wrapper dictionaries: + +.. code-block:: python + + class Book(TypedDict): + name: str + length: int + ... + + class DVD(TypedDict): + name: str + length: int + ... + + TaggedBook = TypedDict('TaggedBook', {'book': Book}, closed=True) + TaggedDVD = TypedDict('TaggedDVD', {'dvd': DVD}, closed=True) + type Inventory = TaggedBook | TaggedDVD + + def print_length(inventory: Inventory) -> None: + if "book" in inventory: + print(inventory["book"]["length"], 'pages') + else: + print(inventory["dvd"]["length"], 'minutes') + +Here, the ``closed`` keyword is necessary to allow the ``if`` guard to safely +narrow the types; without it, there could be a structural subtype of ``TaggedDVD`` +that contains a ``book`` field of arbitrary type. + +.. note:: + + Applying ``@final`` to a TypedDict is a legacy way of marking it as closed + for the purposes of type narrowing. It was never fully implemented and is + now superseded; it may be removed in future. + Inline TypedDict types ---------------------- diff --git a/mypy/checker.py b/mypy/checker.py index 7dfdcb83a90b7..33705c98e10c3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2813,6 +2813,8 @@ def visit_class_def(self, defn: ClassDef) -> None: self.check_multiple_inheritance(typ) self.check_metaclass_compatibility(typ) self.check_final_deletable(typ) + if typ.typeddict_type: + self.check_typeddict_inheritance(defn) if defn.decorators: sig: Type = type_object_type(defn.info) @@ -3219,6 +3221,44 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: if explanation: self.note(explanation, typ, code=codes.METACLASS) + def check_typeddict_inheritance(self, defn: ClassDef) -> None: + """Ensure that the final definition of a TypedDict is compatible with its base classes.""" + assert defn.info.typeddict_type + td = defn.info.typeddict_type + data = defn.info.typeddict_data + if data is None or not data.ready: + return + for base, base_items in data.bases: + assert base.typeddict_type + for field_name, base_type in base_items.items(): + field_type = td.items[field_name] + assert field_type + is_readonly = field_name in base.typeddict_type.readonly_keys + if is_readonly: + is_compatible = is_subtype(field_type, base_type) + else: + is_compatible = is_equivalent(field_type, base_type) + if not is_compatible: + source = data.field_sources[field_name] + if source.base is None: + self.fail( + f'Definition of field "{field_name}" incompatible with base class ' + f'"{base.name}"', + source.ctx, + ) + else: + self.fail( + f'Incompatible definitions of field "{field_name}" in base classes ' + f'"{base.name}" and "{source.base.name}"', + source.ctx, + ) + if field_name in td.readonly_keys: + self.note( + f'This can be resolved by redeclaring the field "{field_name}" ' + f"with a mutually compatible type", + source.ctx, + ) + def visit_import_from(self, node: ImportFrom) -> None: for name, _ in node.names: if (sym := self.globals.get(name)) is not None: @@ -6393,7 +6433,8 @@ def conditional_types_for_iterable( ) -> tuple[Type, Type]: """ Narrows the type of `iterable_type` based on the type of `item_type`. - For now, we only support narrowing unions of TypedDicts based on left operand being literal string(s). + For now, we only support narrowing unions of TypedDicts, and TypeVars with TypedDict + bounds, based on left operand being literal string(s). """ if_types: list[Type] = [] else_types: list[Type] = [] @@ -6407,16 +6448,30 @@ def conditional_types_for_iterable( item_str_literals = try_getting_str_literals_from_type(item_type) for possible_iterable_type in possible_iterable_types: - if item_str_literals and isinstance(possible_iterable_type, TypedDictType): + bound = ( + get_proper_type(possible_iterable_type.upper_bound) + if isinstance(possible_iterable_type, TypeVarType) + else possible_iterable_type + ) + + if item_str_literals and isinstance(bound, TypedDictType): for key in item_str_literals: - if key in possible_iterable_type.required_keys: + if key in bound.required_keys: if_types.append(possible_iterable_type) - elif ( - key in possible_iterable_type.items or not possible_iterable_type.is_final + elif key in bound.items and isinstance( + get_proper_type(bound.items[key]), UninhabitedType ): - if_types.append(possible_iterable_type) + # If an item is explicitly declared uninhabited, we can exclude it from + # if_types; see testOperatorContainsNarrowsTypedDicts_closed + else_types.append(possible_iterable_type) + elif key not in bound.items and (bound.is_closed or bound.is_final): + # If an item is missing and the type is closed, we can exclude it from + # if_types; see testOperatorContainsNarrowsTypedDicts_closed + # We also support "final" as a legacy way of expressing "closed" in this + # specific case; see testOperatorContainsNarrowsTypedDicts_final else_types.append(possible_iterable_type) else: + if_types.append(possible_iterable_type) else_types.append(possible_iterable_type) else: if_types.append(possible_iterable_type) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9c1444ab96322..44855f49afaf9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -818,8 +818,8 @@ def validate_typeddict_kwargs( result = defaultdict(list) # Keys that are guaranteed to be present no matter what (e.g. for all items of a union) always_present_keys = set() - # Indicates latest encountered ** unpack among items. - last_star_found = None + # Indicates latest encountered ** unpack of a non-closed type among items. + last_open_star_found = None for item_name_expr, item_arg in kwargs: if item_name_expr: @@ -843,22 +843,30 @@ def validate_typeddict_kwargs( result[literal_value] = [item_arg] always_present_keys.add(literal_value) else: - last_star_found = item_arg - if not self.validate_star_typeddict_item( + is_valid, is_open = self.validate_star_typeddict_item( item_arg, callee, result, always_present_keys - ): + ) + if not is_valid: return None - if self.chk.options.extra_checks and last_star_found is not None: + if is_open: + last_open_star_found = item_arg + if self.chk.options.extra_checks and last_open_star_found is not None: + if callee.is_closed: + self.chk.fail( + "Cannot unpack item that may contain extra keys into a closed TypedDict", + last_open_star_found, + code=codes.TYPEDDICT_ITEM, + ) absent_keys = [] for key in callee.items: if key not in callee.required_keys and key not in result: absent_keys.append(key) if absent_keys: - # Having an optional key not explicitly declared by a ** unpacked + # Having an optional key not explicitly declared by a ** unpacked open # TypedDict is unsafe, it may be an (incompatible) subtype at runtime. # TODO: catch the cases where a declared key is overridden by a subsequent # ** item without it (and not again overridden with complete ** item). - self.msg.non_required_keys_absent_with_star(absent_keys, last_star_found) + self.msg.non_required_keys_absent_with_star(absent_keys, last_open_star_found) return result, always_present_keys def validate_star_typeddict_item( @@ -867,14 +875,18 @@ def validate_star_typeddict_item( callee: TypedDictType, result: dict[str, list[Expression]], always_present_keys: set[str], - ) -> bool: + ) -> tuple[bool, bool]: """Update keys/expressions from a ** expression in TypedDict constructor. - Note `result` and `always_present_keys` are updated in place. Return true if the - expression `item_arg` may valid in `callee` TypedDict context. + Note `result` and `always_present_keys` are updated in place. + + First tuple item returned is true if the expression `item_arg` may valid + in `callee` TypedDict context. Second tuple item returned is true if the + expression may contain other keys not explicitly declared. """ inferred = get_proper_type(self.accept(item_arg, type_context=callee)) - possible_tds = [] + any_fallback = False + possible_tds: list[TypedDictType] = [] if isinstance(inferred, TypedDictType): possible_tds = [inferred] elif isinstance(inferred, UnionType): @@ -883,10 +895,14 @@ def validate_star_typeddict_item( possible_tds.append(item) elif not self.valid_unpack_fallback_item(item): self.msg.unsupported_target_for_star_typeddict(item, item_arg) - return False + return False, True + else: + any_fallback = True elif not self.valid_unpack_fallback_item(inferred): self.msg.unsupported_target_for_star_typeddict(inferred, item_arg) - return False + return False, True + else: + any_fallback = True all_keys: set[str] = set() for td in possible_tds: all_keys |= td.items.keys() @@ -915,7 +931,8 @@ def validate_star_typeddict_item( # If this key is not required at least in some item of a union # it may not shadow previous item, so we need to type check both. result[key].append(arg) - return True + all_closed = all(t.is_closed for t in possible_tds) + return True, any_fallback or not all_closed def valid_unpack_fallback_item(self, typ: ProperType) -> bool: if isinstance(typ, AnyType): diff --git a/mypy/copytype.py b/mypy/copytype.py index 9a390a01bdbab..3ec512193bece 100644 --- a/mypy/copytype.py +++ b/mypy/copytype.py @@ -107,7 +107,7 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: def visit_typeddict_type(self, t: TypedDictType) -> ProperType: return self.copy_common( - t, TypedDictType(t.items, t.required_keys, t.readonly_keys, t.fallback) + t, TypedDictType(t.items, t.required_keys, t.readonly_keys, t.is_closed, t.fallback) ) def visit_literal_type(self, t: LiteralType) -> ProperType: diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 6aa18fb72c2f4..b576d9f97d8e5 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -336,7 +336,7 @@ def _possible_callable_kwargs(cls, repl: Parameters, dict_type: Instance) -> Pro return dict_type kwargs = {} required_names = set() - extra_items: Type = UninhabitedType() + extra_items: Type | None = None for kind, name, type in zip(repl.arg_kinds, repl.arg_names, repl.arg_types): if kind == ArgKind.ARG_NAMED and name is not None: kwargs[name] = type @@ -346,10 +346,11 @@ def _possible_callable_kwargs(cls, repl: Parameters, dict_type: Instance) -> Pro extra_items = type elif not kind.is_star() and name is not None: kwargs[name] = type - if not kwargs: + if not kwargs and extra_items is not None: return Instance(dict_type.type, [dict_type.args[0], extra_items]) - # TODO: when PEP 728 is implemented, pass extra_items below. - return TypedDictType(kwargs, required_names, set(), fallback=dict_type) + # TODO: when PEP 728 `extra_items` is implemented, pass extra_items below. + is_closed = extra_items is None + return TypedDictType(kwargs, required_names, set(), is_closed, fallback=dict_type) def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: # Sometimes solver may need to expand a type variable with (a copy of) itself diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 1c9323be056dd..5fecf7b6fba9f 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -277,7 +277,7 @@ def expr_to_unanalyzed_type( value, options, allow_new_syntax, expr, lookup_qualified=lookup_qualified ) result = TypedDictType( - items, set(), set(), Instance(MISSING_FALLBACK, ()), expr.line, expr.column + items, set(), set(), False, Instance(MISSING_FALLBACK, ()), expr.line, expr.column ) result.extra_items_from = extra_items_from return result diff --git a/mypy/fastparse.py b/mypy/fastparse.py index d9e2d5df8f4c1..c2d2243d7555e 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2143,7 +2143,7 @@ def visit_Dict(self, n: ast3.Dict) -> Type: continue return self.invalid_type(n) items[item_name.value] = self.visit(value) - result = TypedDictType(items, set(), set(), _dummy_fallback, n.lineno, n.col_offset) + result = TypedDictType(items, set(), set(), False, _dummy_fallback, n.lineno, n.col_offset) result.extra_items_from = extra_items_from return result diff --git a/mypy/join.py b/mypy/join.py index 3b6c9cc23f6f3..b99ed190cc9da 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -35,6 +35,7 @@ TupleType, Type, TypeAliasType, + TypedDictItem, TypedDictType, TypeOfAny, TypeType, @@ -610,24 +611,59 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: else: return join_types(self.s, mypy.typeops.tuple_fallback(t)) + def resolve_typeddict_item( + self, item_name: str, s: TypedDictItem, t: TypedDictItem + ) -> tuple[Type | None, bool, bool]: + """Return the type, requiredness, and readonlyness of a join item. + + If the type is None, the item should be omitted from the join keys. + """ + is_required = s.required and t.required + + if s.typ is None or t.typ is None: + # Key is implicitly NotRequired[ReadOnly[object]] in one type and + # (object join T) == object. Omitting it in the join leaves it implicitly + # of object type. + join_type = None + is_readonly = True + else: + join_type = join_types(s.typ, t.typ) + + if s.required != t.required: + # As one of the input types marks the key as not required, it must + # be not required in the join supertype. However, as the other input + # type does not have a delitem overload for the key, the delitem + # overload must be omitted in the join supertype too. This can only + # be done by marking the key as readonly. + is_readonly = True + elif s.readonly or t.readonly: + # If either type has no setitem overload for this key, + # then the join supertype must also omit it + is_readonly = True + else: + is_readonly = not is_equivalent(s.typ, t.typ) + + return join_type, is_required, is_readonly + def visit_typeddict_type(self, t: TypedDictType) -> ProperType: if isinstance(self.s, TypedDictType): - items = { - item_name: s_item_type - for (item_name, s_item_type, t_item_type) in self.s.zip(t) - if ( - is_equivalent(s_item_type, t_item_type) - and (item_name in t.required_keys) == (item_name in self.s.required_keys) + items = {} + required_keys = set() + readonly_keys = set() + for item_name, s_item, t_item in self.s.zipall(t): + item_type, is_required, is_readonly = self.resolve_typeddict_item( + item_name, s_item, t_item ) - } + if item_type is not None: + items[item_name] = item_type + if is_required: + required_keys.add(item_name) + if is_readonly: + readonly_keys.add(item_name) + fallback = self.s.create_anonymous_fallback() - all_keys = set(items.keys()) - # We need to filter by items.keys() since some required keys present in both t and - # self.s might be missing from the join if the types are incompatible. - required_keys = all_keys & t.required_keys & self.s.required_keys - # If one type has a key as readonly, we mark it as readonly for both: - readonly_keys = (t.readonly_keys | t.readonly_keys) & all_keys - return TypedDictType(items, required_keys, readonly_keys, fallback) + is_closed = self.s.is_closed and t.is_closed + return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) elif isinstance(self.s, Instance): return join_types(self.s, t.fallback) else: diff --git a/mypy/meet.py b/mypy/meet.py index 6c05d449abe5c..57b79c51f8e11 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -35,6 +35,7 @@ TupleType, Type, TypeAliasType, + TypedDictItem, TypedDictType, TypeGuardedType, TypeOfAny, @@ -1121,26 +1122,71 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: return t return self.default(self.s) + def resolve_typeddict_item_type( + self, name: str, s: TypedDictItem, t: TypedDictItem + ) -> tuple[Type | None, bool]: + """Return the type and readonlyness of a meet item. + + If the parent constraints are mutually incompatible, the + returned type will be None; the overall meet type should + be UninhabitedType. + """ + is_readonly = s.readonly and t.readonly + + if t.typ is None: + assert s.typ is not None + meet_type = s.typ + elif s.typ is None: + meet_type = t.typ + else: + meet_type = meet_types(s.typ, t.typ) + + if ( + s.typ is not None + and s.mutable + and (not is_equivalent(meet_type, s.typ) or (t.required and not s.required)) + ): + meet_type = None + elif ( + t.typ is not None + and t.mutable + and (not is_equivalent(meet_type, t.typ) or (s.required and not t.required)) + ): + meet_type = None + elif isinstance(get_proper_type(meet_type), UninhabitedType) and ( + s.required or t.required + ): + meet_type = None + + return (meet_type, is_readonly) + def visit_typeddict_type(self, t: TypedDictType) -> ProperType: if isinstance(self.s, TypedDictType): - for name, l, r in self.s.zip(t): - if not is_equivalent(l, r) or (name in t.required_keys) != ( - name in self.s.required_keys - ): + is_closed = self.s.is_closed or t.is_closed + items: dict[str, Type] = {} + readonly_keys: set[str] = set() + for name, s_item, t_item in self.s.zipall(t): + meet_type, is_readonly = self.resolve_typeddict_item_type(name, s_item, t_item) + + if meet_type is None: return self.default(self.s) - item_list: list[tuple[str, Type]] = [] - for item_name, s_item_type, t_item_type in self.s.zipall(t): - if s_item_type is not None: - item_list.append((item_name, s_item_type)) - else: - # at least one of s_item_type and t_item_type is not None - assert t_item_type is not None - item_list.append((item_name, t_item_type)) - items = dict(item_list) + + if ( + is_closed + and not is_readonly + and isinstance(get_proper_type(meet_type), UninhabitedType) + ): + # Simplify emitted type by omitting redundant Never keys from closed + # TypedDicts + continue + + items[name] = meet_type + if is_readonly: + readonly_keys.add(name) + fallback = self.s.create_anonymous_fallback() - required_keys = t.required_keys | self.s.required_keys - readonly_keys = t.readonly_keys | self.s.readonly_keys - return TypedDictType(items, required_keys, readonly_keys, fallback) + required_keys = self.s.required_keys | t.required_keys + return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) elif isinstance(self.s, Instance) and is_subtype(t, self.s): return t else: diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index 414426580fa73..f371746cab8b8 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -926,7 +926,7 @@ def read_type(state: State, data: ReadBuffer) -> Type: extra_items_from.append(val) else: td_items[key] = val - typeddict_type = TypedDictType(td_items, set(), set(), _dummy_fallback) + typeddict_type = TypedDictType(td_items, set(), set(), False, _dummy_fallback) typeddict_type.extra_items_from = extra_items_from read_loc(data, typeddict_type) expect_end_tag(data) diff --git a/mypy/nodes.py b/mypy/nodes.py index a0342b0e94958..f837185b858a7 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3670,6 +3670,7 @@ class is generic then it will be a type constructor of higher kind. "deprecated", "type_object_type", "default_depends", + "typeddict_data", ) _fullname: str # Fully qualified name @@ -3841,6 +3842,9 @@ class is generic then it will be a type constructor of higher kind. # Keys are type variable full names. default_depends: dict[str, set[TypeAlias | TypeInfo]] + # If defn is TypedDictType, stores information needed for delayed validation of inheritance. + typeddict_data: TypedDictData | None + FLAGS: Final = [ "is_abstract", "is_enum", @@ -3903,6 +3907,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.deprecated = None self.type_object_type = None self.default_depends = {} + self.typeddict_data = None def add_type_vars(self) -> None: self.has_type_var_tuple_type = False @@ -5232,6 +5237,44 @@ def read(cls, data: ReadBuffer) -> DataclassTransformSpec: return ret +class TypedDictFieldSource: + """Source of a TypedDict field definition, used for forming error messages. + + May be defined directly on the type, or on a base class. + """ + + __slots__ = ("base", "ctx") + + base: TypeInfo | None + ctx: Context + + def __init__(self, base: TypeInfo | None, ctx: Context) -> None: + self.base = base + self.ctx = ctx + + +class TypedDictData: + """Stores information needed for delayed validation of TypedDict inheritance.""" + + __slots__ = ("ready", "bases", "field_sources") + + # If False, the type definition referenced a placeholder + ready: bool + + bases: list[tuple[TypeInfo, dict[str, mypy.types.Type]]] + field_sources: dict[str, TypedDictFieldSource] + + def __init__( + self, + ready: bool, + bases: list[tuple[TypeInfo, dict[str, mypy.types.Type]]], + field_sources: dict[str, TypedDictFieldSource], + ) -> None: + self.ready = ready + self.bases = bases + self.field_sources = field_sources + + @trait class SplittingVisitor: # If True, process function definitions. If False, don't. This is used diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 8023397836124..a7d1c9e4426ab 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -293,9 +293,10 @@ def typed_dict_get_callback(ctx: MethodContext) -> Type: for key in keys: value_type: Type | None = ctx.type.items.get(key) if value_type is None: - return ctx.default_return_type - - if key in ctx.type.required_keys: + if not ctx.type.is_closed: + return ctx.default_return_type + output_types.append(default_type) + elif key in ctx.type.required_keys: output_types.append(value_type) else: # HACK to deal with get(key, {}) diff --git a/mypy/semanal.py b/mypy/semanal.py index b0294c782ab98..58c152fa066e7 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2143,7 +2143,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool: if ( defn.info and defn.info.typeddict_type - and not has_placeholder(defn.info.typeddict_type) + and (defn.info.typeddict_data is None or defn.info.typeddict_data.ready) ): # Don't reprocess everything is_typeddict = True diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 593bbbbb25672..5f152ba0e8988 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -3,13 +3,12 @@ from __future__ import annotations from collections.abc import Collection -from typing import Final +from typing import Final, NamedTuple from mypy import errorcodes as codes, message_registry from mypy.errorcodes import ErrorCode from mypy.expandtype import expand_type from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type -from mypy.message_registry import TYPEDDICT_OVERRIDE_MERGE from mypy.messages import MessageBuilder from mypy.nodes import ( ARG_NAMED, @@ -31,7 +30,9 @@ TempNode, TupleExpr, TypeAlias, + TypedDictData, TypedDictExpr, + TypedDictFieldSource, TypeInfo, inline_base, ) @@ -60,6 +61,14 @@ ) +class FieldSource(NamedTuple): + field_type: Type + is_readonly: bool + is_required: bool + base: TypeInfo | None + ctx: Context + + class TypedDictAnalyzer: def __init__( self, options: Options, api: SemanticAnalyzerInterface, msg: MessageBuilder @@ -102,20 +111,32 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N if isinstance(defn.analyzed, TypedDictExpr): existing_info = defn.analyzed.info - field_types: dict[str, Type] | None + is_closed: bool | None = None + if "closed" in defn.keywords: + is_closed = require_bool_literal_argument( + self.api, defn.keywords["closed"], "closed", False + ) + if ( len(defn.base_type_exprs) == 1 and isinstance(defn.base_type_exprs[0], RefExpr) and defn.base_type_exprs[0].fullname in TPDICT_NAMES ): # Building a new TypedDict - field_types, statements, required_keys, readonly_keys = ( - self.analyze_typeddict_classdef_fields(defn) - ) - if field_types is None: + field_sources, statements = self.analyze_typeddict_classdef_fields(defn) + if field_sources is None: return True, None # Defer + field_types = {key: source.field_type for (key, source) in field_sources.items()} + required_keys = {key for (key, source) in field_sources.items() if source.is_required} + readonly_keys = {key for (key, source) in field_sources.items() if source.is_readonly} info = self.build_typeddict_typeinfo( - defn.name, field_types, required_keys, readonly_keys, defn.line, existing_info + defn.name, + field_types, + required_keys, + readonly_keys, + is_closed or False, + defn.line, + existing_info, ) defn.analyzed = TypedDictExpr(info) defn.analyzed.line = defn.line @@ -153,24 +174,27 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N else: self.fail("All bases of a new TypedDict must be TypedDict types", defn) - field_types = {} - required_keys = set() - readonly_keys = set() - # Iterate over bases in reverse order so that leftmost base class' keys take precedence - for base in reversed(typeddict_bases): - self.add_keys_and_types_from_base( - base, field_types, required_keys, readonly_keys, defn - ) - new_field_types, new_statements, new_required_keys, new_readonly_keys = ( - self.analyze_typeddict_classdef_fields(defn, oldfields=field_types) - ) - if new_field_types is None: + bases_info: list[tuple[TypeInfo, dict[str, Type]]] = [] + for base in typeddict_bases: + base_info = self.fetch_keys_and_types_from_base(base, defn) + if base_info is not None: + bases_info.append(base_info) + new_field_sources, new_statements = self.analyze_typeddict_classdef_fields(defn) + if new_field_sources is None: return True, None # Defer - field_types.update(new_field_types) - required_keys.update(new_required_keys) - readonly_keys.update(new_readonly_keys) + field_types, required_keys, readonly_keys, is_closed, field_sources = ( + self.resolve_field_inheritance(bases_info, new_field_sources, is_closed, defn) + ) + typeddict_data = TypedDictData(True, bases_info, field_sources) info = self.build_typeddict_typeinfo( - defn.name, field_types, required_keys, readonly_keys, defn.line, existing_info + defn.name, + field_types, + required_keys, + readonly_keys, + is_closed, + defn.line, + existing_info, + typeddict_data=typeddict_data, ) defn.analyzed = TypedDictExpr(info) defn.analyzed.line = defn.line @@ -178,20 +202,15 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N defn.defs.body = new_statements return True, info - def add_keys_and_types_from_base( - self, - base: Expression, - field_types: dict[str, Type], - required_keys: set[str], - readonly_keys: set[str], - ctx: Context, - ) -> None: + def fetch_keys_and_types_from_base( + self, base: Expression, ctx: Context + ) -> tuple[TypeInfo, dict[str, Type]] | None: info = self._parse_typeddict_base(base, ctx) base_args: list[Type] = [] if isinstance(base, IndexExpr): args = self.analyze_base_args(base, ctx) if args is None: - return + return None base_args = args assert info.typeddict_type is not None @@ -210,13 +229,167 @@ def add_keys_and_types_from_base( with state.strict_optional_set(self.options.strict_optional): valid_items = self.map_items_to_base(valid_items, tvars, base_args) - for key in base_items: - if key in field_types: - self.fail(TYPEDDICT_OVERRIDE_MERGE.format(key), ctx) - field_types.update(valid_items) - required_keys.update(base_typed_dict.required_keys) - readonly_keys.update(base_typed_dict.readonly_keys) + return info, valid_items + + def field_sources_in_reverse_order( + self, + bases: list[tuple[TypeInfo, dict[str, Type]]], + child_field_sources: dict[str, FieldSource], + ctx: Context, + ) -> dict[str, list[FieldSource]]: + """Find all keys in bases and child, mapping them to a list of sources. + + Iterate bases in reverse order to preserve key ordering for display. + """ + result: dict[str, list[FieldSource]] = {} + for base_info, base_fields in reversed(bases): + assert base_info.typeddict_type is not None + for field_name, field_type in base_fields.items(): + source = FieldSource( + field_type=field_type, + is_readonly=field_name in base_info.typeddict_type.readonly_keys, + is_required=field_name in base_info.typeddict_type.required_keys, + base=base_info, + ctx=ctx, + ) + result.setdefault(field_name, []).append(source) + for field_name, source in child_field_sources.items(): + result.setdefault(field_name, []).append(source) + return result + + def primary_source(self, sources: list[FieldSource]) -> FieldSource: + """Select a primary source from a reverse-ordered list of sources. + + The primary source will be the last in the list, skipping readonly + base class sources unless they are the only available option. + """ + if not sources[-1].base: + return sources[-1] + mutable_sources = (s for s in reversed(sources) if not s.is_readonly) + return next(mutable_sources, sources[-1]) + + def verify_requiredness_compatibility( + self, + field_name: str, + source: FieldSource, + is_required: bool, + primary_source_base: TypeInfo | None, + ctx: Context, + ) -> None: + """Verify requiredness compatibility of the final child type field with a base class source.""" + assert source.base + if source.is_required and not is_required: + if primary_source_base is None: + self.fail( + f'Field "{field_name}" is required in base class "{source.base.name}"', ctx + ) + else: + self.fail( + f'Field "{field_name}" is required in base class "{source.base.name}" but can ' + f'be deleted in base class "{primary_source_base.name}"', + ctx, + ) + elif not source.is_required and not source.is_readonly and is_required: + if primary_source_base is None: + self.fail( + f'Field "{field_name}" can be deleted in base class "{source.base.name}"', ctx + ) + else: + self.fail( + f'Field "{field_name}" is required in base class "{primary_source_base.name}" ' + f'but can be deleted in base class "{source.base.name}"', + ctx, + ) + + def verify_field_against_closed_bases( + self, + field_name: str, + closed_bases: Collection[tuple[TypeInfo, Collection[str]]], + primary_source_base: TypeInfo | None, + ctx: Context, + ) -> None: + for closed_base_type, closed_base_fields in closed_bases: + if field_name in closed_base_fields: + continue + + if primary_source_base: + self.fail( + f'Cannot extend closed base class "{closed_base_type.name}" with field ' + f'"{field_name}" from base class "{primary_source_base.name}"', + ctx, + ) + else: + self.fail( + f'Cannot extend closed base class "{closed_base_type.name}" with new field ' + f'"{field_name}"', + ctx, + ) + + def resolve_field_inheritance( + self, + bases: list[tuple[TypeInfo, dict[str, Type]]], + child_field_sources: dict[str, FieldSource], + child_is_closed: bool | None, + ctx: Context, + ) -> tuple[dict[str, Type], set[str], set[str], bool, dict[str, TypedDictFieldSource]]: + """Determine field types, requiredness, readonlyness, and closedness.""" + field_sources = self.field_sources_in_reverse_order(bases, child_field_sources, ctx) + field_types: dict[str, Type] = {} + chosen_sources: dict[str, TypedDictFieldSource] = {} + required_keys: set[str] = set() + readonly_keys: set[str] = set() + closed_bases = [ + (base_info, base_fields.keys()) + for (base_info, base_fields) in bases + if base_info.typeddict_type and base_info.typeddict_type.is_closed + ] + + if child_is_closed is False and closed_bases: + for base_info, _ in closed_bases: + self.fail( + f"Open TypedDict class cannot subclass closed TypedDict class " + f'"{base_info.name}"', + ctx, + ) + + for field_name, sources in field_sources.items(): + primary_source = self.primary_source(sources) + # If a read-only field is only defined in base classes, joining the types + # is unlikely to produce a tight enough result. We could check all the + # candidates from the base classes, but it would be O(n^2) complexity + # to find out which is a supertype of all the others. Instead, use the + # first definition we encounter, and let the user provide the correct + # definition in the subclass if this fails. + field_types[field_name] = primary_source.field_type + chosen_sources[field_name] = TypedDictFieldSource( + base=primary_source.base, ctx=primary_source.ctx + ) + + if primary_source.is_readonly: + # If the primary source is readonly, all sources are readonly + is_readonly = True + is_required = any(source.is_required for source in sources) + else: + is_readonly = False + is_required = primary_source.is_required + + if is_required: + required_keys.add(field_name) + if is_readonly: + readonly_keys.add(field_name) + + for source in sources: + if source is not primary_source: + self.verify_requiredness_compatibility( + field_name, source, is_required, primary_source.base, primary_source.ctx + ) + self.verify_field_against_closed_bases( + field_name, closed_bases, primary_source.base, primary_source.ctx + ) + + is_closed = bool(closed_bases) if child_is_closed is None else child_is_closed + return field_types, required_keys, readonly_keys, is_closed, chosen_sources def _parse_typeddict_base(self, base: Expression, ctx: Context) -> TypeInfo: if isinstance(base, RefExpr): @@ -287,22 +460,19 @@ def map_items_to_base( return mapped_items def analyze_typeddict_classdef_fields( - self, defn: ClassDef, oldfields: Collection[str] | None = None - ) -> tuple[dict[str, Type] | None, list[Statement], set[str], set[str]]: + self, defn: ClassDef + ) -> tuple[dict[str, FieldSource] | None, list[Statement]]: """Analyze fields defined in a TypedDict class definition. This doesn't consider inherited fields (if any). Also consider totality, if given. Return tuple with these items: - * Dict of key -> type (or None if found an incomplete reference -> deferral) + * Dict of key -> field source (or None if found an incomplete reference -> deferral) * List of statements from defn.defs.body that are legally allowed to be a part of a TypedDict definition - * Set of required keys """ - fields: dict[str, Type] = {} - readonly_keys = set[str]() - required_keys = set[str]() + fields: dict[str, FieldSource] = {} statements: list[Statement] = [] total: bool | None = True @@ -312,6 +482,8 @@ def analyze_typeddict_classdef_fields( self.api, defn.keywords["total"], "total", True ) continue + elif key == "closed": + continue for_function = ' for "__init_subclass__" of "TypedDict"' self.msg.unexpected_keyword_argument_for_function(for_function, key, defn) @@ -332,8 +504,6 @@ def analyze_typeddict_classdef_fields( self.fail(TPDICT_CLASS_ERROR, stmt) else: name = stmt.lvalues[0].name - if name in (oldfields or []): - self.fail(f'Overwriting TypedDict field "{name}" while extending', stmt) if name in fields: self.fail(f'Duplicate TypedDict key "{name}"', stmt) continue @@ -352,18 +522,19 @@ def analyze_typeddict_classdef_fields( prohibit_special_class_field_types="TypedDict", ) if analyzed is None: - return None, [], set(), set() # Need to defer + return None, [] # Need to defer field_type = analyzed if not has_placeholder(analyzed): stmt.type = self.extract_meta_info(analyzed, stmt)[0] field_type, required, readonly = self.extract_meta_info(field_type) - fields[name] = field_type - - if (total or required is True) and required is not False: - required_keys.add(name) - if readonly: - readonly_keys.add(name) + fields[name] = FieldSource( + field_type=field_type, + is_required=(total or required is True) and required is not False, + is_readonly=readonly, + base=None, + ctx=stmt, + ) # ...despite possible minor failures that allow further analysis. if stmt.type is None or hasattr(stmt, "new_syntax") and not stmt.new_syntax: @@ -372,7 +543,7 @@ def analyze_typeddict_classdef_fields( # x: int assigns rvalue to TempNode(AnyType()) self.fail("Right hand side values are not supported in TypedDict", stmt) - return fields, statements, required_keys, readonly_keys + return fields, statements def extract_meta_info( self, typ: Type, context: Context | None = None @@ -433,10 +604,10 @@ def check_typeddict( # This is a valid typed dict, but some type is not ready. # The caller should defer this until next iteration. return True, None, [] - typename, items, types, total, tvar_defs, ok = res + typename, items, wrapped_types, total, closed, tvar_defs, ok = res if not ok: # Error. Construct dummy return value. - info = self.build_typeddict_typeinfo(name, {}, set(), set(), call.line, None) + info = self.build_typeddict_typeinfo(name, {}, set(), set(), False, call.line, None) else: if "@" not in name and name != typename: self.fail( @@ -446,18 +617,17 @@ def check_typeddict( node, code=codes.NAME_MATCH, ) - required_keys = { - field - for (field, t) in zip(items, types) - if (total or (isinstance(t, RequiredType) and t.required)) - and not (isinstance(t, RequiredType) and not t.required) - } - readonly_keys = { - field for (field, t) in zip(items, types) if isinstance(t, ReadOnlyType) - } - types = [ # unwrap Required[T] or ReadOnly[T] to just T - t.item if isinstance(t, (RequiredType, ReadOnlyType)) else t for t in types - ] + # Unwrap special forms (Required/NotRequired/ReadOnly) + types: list[Type] = [] + required_keys: set[str] = set() + readonly_keys: set[str] = set() + for field, t in zip(items, wrapped_types): + unwrapped_type, is_required, is_readonly = self.extract_meta_info(t, node) + types.append(unwrapped_type) + if is_required is True or (is_required is None and total): + required_keys.add(field) + if is_readonly: + readonly_keys.add(field) # Perform various validations after unwrapping. for t in types: @@ -478,6 +648,7 @@ def check_typeddict( dict(zip(items, types)), required_keys, readonly_keys, + closed, call.line, existing_info, ) @@ -492,25 +663,33 @@ def check_typeddict( def parse_typeddict_args( self, call: CallExpr - ) -> tuple[str, list[str], list[Type], bool, list[TypeVarLikeType], bool] | None: + ) -> tuple[str, list[str], list[Type], bool, bool, list[TypeVarLikeType], bool] | None: """Parse typed dict call expression. - Return names, types, totality, was there an error during parsing. + Return names, types, totality, open/closed, was there an error during parsing. If some type is not ready, return None. """ # TODO: Share code with check_argument_count in checkexpr.py? args = call.args if len(args) < 2: return self.fail_typeddict_arg("Too few arguments for TypedDict()", call) - if len(args) > 3: + if len(args) > 4: return self.fail_typeddict_arg("Too many arguments for TypedDict()", call) - # TODO: Support keyword arguments - if call.arg_kinds not in ([ARG_POS, ARG_POS], [ARG_POS, ARG_POS, ARG_NAMED]): + if call.arg_kinds[:2] != [ARG_POS, ARG_POS] or any( + arg_kind != ARG_NAMED for arg_kind in call.arg_kinds[2:] + ): return self.fail_typeddict_arg("Unexpected arguments to TypedDict()", call) - if len(args) == 3 and call.arg_names[2] != "total": - return self.fail_typeddict_arg( - f'Unexpected keyword argument "{call.arg_names[2]}" for "TypedDict"', call - ) + seen_arg_names = set() + for arg_name in call.arg_names[2:]: + if arg_name not in ("total", "closed"): + return self.fail_typeddict_arg( + f'Unexpected keyword argument "{arg_name}" for "TypedDict"', call + ) + if arg_name in seen_arg_names: + return self.fail_typeddict_arg( + f'Repeated keyword argument "{arg_name}" for "TypedDict"', call + ) + seen_arg_names.add(arg_name) if not isinstance(args[0], StrExpr): return self.fail_typeddict_arg( "TypedDict() expects a string literal as the first argument", call @@ -520,10 +699,16 @@ def parse_typeddict_args( "TypedDict() expects a dictionary literal as the second argument", call ) total: bool | None = True - if len(args) == 3: - total = require_bool_literal_argument(self.api, call.args[2], "total") - if total is None: - return "", [], [], True, [], False + closed: bool = False + for arg_name, arg in zip(call.arg_names[2:], call.args[2:]): + assert arg_name + value = require_bool_literal_argument(self.api, arg, arg_name) + if value is None: + return "", [], [], True, False, [], False + if arg_name == "closed": + closed = value + else: + total = value dictexpr = args[1] tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items]) res = self.parse_typeddict_fields_with_types(dictexpr.items) @@ -532,7 +717,7 @@ def parse_typeddict_args( return None items, types, ok = res assert total is not None - return args[0].value, items, types, total, tvar_defs, ok + return args[0].value, items, types, total, closed, tvar_defs, ok def parse_typeddict_fields_with_types( self, dict_items: list[tuple[Expression | None, Expression]] @@ -576,9 +761,9 @@ def parse_typeddict_fields_with_types( def fail_typeddict_arg( self, message: str, context: Context - ) -> tuple[str, list[str], list[Type], bool, list[TypeVarLikeType], bool]: + ) -> tuple[str, list[str], list[Type], bool, bool, list[TypeVarLikeType], bool]: self.fail(message, context) - return "", [], [], True, [], False + return "", [], [], True, False, [], False def build_typeddict_typeinfo( self, @@ -586,8 +771,10 @@ def build_typeddict_typeinfo( item_types: dict[str, Type], required_keys: set[str], readonly_keys: set[str], + is_closed: bool, line: int, existing_info: TypeInfo | None, + typeddict_data: TypedDictData | None = None, ) -> TypeInfo: # Prefer typing then typing_extensions if available. fallback = ( @@ -597,12 +784,29 @@ def build_typeddict_typeinfo( ) assert fallback is not None info = existing_info or self.api.basic_new_typeinfo(name, fallback, line) - typeddict_type = TypedDictType(item_types, required_keys, readonly_keys, fallback) - if has_placeholder(typeddict_type): + typeddict_type = TypedDictType( + item_types, required_keys, readonly_keys, is_closed, fallback + ) + any_placeholder = has_placeholder(typeddict_type) + if typeddict_data: + for _, base_fields in typeddict_data.bases: + for field_type in base_fields.values(): + if has_placeholder(field_type): + any_placeholder = True + else: + typeddict_data = TypedDictData(True, [], {}) + if any_placeholder: + typeddict_data.ready = False + force_progress = ( + typeddict_type != info.typeddict_type + or info.typeddict_data is None + or typeddict_data.bases != info.typeddict_data.bases + ) self.api.process_placeholder( - None, "TypedDict item", info, force_progress=typeddict_type != info.typeddict_type + None, "TypedDict item", info, force_progress=force_progress ) info.update_typeddict_type(typeddict_type) + info.typeddict_data = typeddict_data return info # Helpers diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index ecff546049f92..7b7a3d6c84564 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -500,7 +500,7 @@ def visit_typeddict_type(self, typ: TypedDictType) -> SnapshotItem: items = tuple((key, snapshot_type(item_type)) for key, item_type in typ.items.items()) required = tuple(sorted(typ.required_keys)) readonly = tuple(sorted(typ.readonly_keys)) - return ("TypedDictType", items, required, readonly) + return ("TypedDictType", items, required, readonly, typ.is_closed) def visit_literal_type(self, typ: LiteralType) -> SnapshotItem: return ("LiteralType", snapshot_type(typ.fallback), typ.value) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 9b0089b6aec0f..fe90c3d256b7a 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1094,9 +1094,13 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: if not isinstance(rvalue.args[0], StrExpr): self.annotate_as_incomplete(lvalue) return + if len(rvalue.args) > 2 and rvalue.arg_kinds[2] != ARG_NAMED: + self.annotate_as_incomplete(lvalue) + return items: list[tuple[str, Expression]] = [] total: Expression | None = None + closed: Expression | None = None if len(rvalue.args) > 1 and rvalue.arg_kinds[1] == ARG_POS: if not isinstance(rvalue.args[1], DictExpr): self.annotate_as_incomplete(lvalue) @@ -1106,11 +1110,14 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: self.annotate_as_incomplete(lvalue) return items.append((attr_name.value, attr_type)) - if len(rvalue.args) > 2: - if rvalue.arg_kinds[2] != ARG_NAMED or rvalue.arg_names[2] != "total": + for arg_name, arg in zip(rvalue.arg_names[2:], rvalue.args[2:]): + if arg_name == "total": + total = arg + elif arg_name == "closed": + closed = arg + else: self.annotate_as_incomplete(lvalue) return - total = rvalue.args[2] else: for arg_name, arg in zip(rvalue.arg_names[1:], rvalue.args[1:]): if not isinstance(arg_name, str): @@ -1130,6 +1137,8 @@ def process_typeddict(self, lvalue: NameExpr, rvalue: CallExpr) -> None: # TODO: Add support for generic TypedDicts. Requires `Generic` as base class. if total is not None: bases += f", total={total.accept(p)}" + if closed is not None: + bases += f", closed={closed.accept(p)}" class_def = f"{self._indent}class {lvalue.name}({bases}):" if len(items) == 0: self.add(f"{class_def} ...\n") diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 305cfa9de5f22..259bb3791deaf 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -938,46 +938,53 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool: elif isinstance(right, TypedDictType): if left == right: return True # Fast path - if not left.names_are_wider_than(right): + + # A closed type must remain closed + if right.is_closed and not left.is_closed: return False - for name, l, r in left.zip(right): - # TODO: should we pass on the full subtype_context here and below? - right_readonly = name in right.readonly_keys - if not right_readonly: + + # Perform fast key-based checks before recursing into value types + for name, l, r in left.zipall(right): + # Required keys must remain required + if r.required and not l.required: + return False + # Mutable keys must remain mutable + if r.mutable and not l.mutable: + return False + # Mutable optional keys must also remain optional, + # to retain the ability to delete them + if r.mutable and not r.required and l.required: + return False + + for name, l, r in left.zipall(right): + if r.mutable: + # None will only be used for missing ReadOnly[object] keys + assert r.typ is not None + # Guaranteed by "mutable keys must remain mutable" check + assert l.typ is not None + + # TODO: should we pass on the full subtype_context here and below? if self.proper_subtype: - check = is_same_type(l, r) + check = is_same_type(l.typ, r.typ) else: check = is_equivalent( - l, - r, + l.typ, + r.typ, ignore_type_params=self.subtype_context.ignore_type_params, options=self.options, ) else: # Read-only items behave covariantly - check = self._is_subtype(l, r) + if r.typ is None: + check = True + elif l.typ is None: + # Keys cannot be dropped in an open subtype + # as this would implicitly widen the type constraint + check = False + else: + check = self._is_subtype(l.typ, r.typ) if not check: return False - # Non-required key is not compatible with a required key since - # indexing may fail unexpectedly if a required key is missing. - # Required key is not compatible with a non-read-only non-required - # key since the prior doesn't support 'del' but the latter should - # support it. - # Required key is compatible with a read-only non-required key. - required_differ = (name in left.required_keys) != (name in right.required_keys) - if not right_readonly and required_differ: - return False - # Readonly fields check: - # - # A = TypedDict('A', {'x': ReadOnly[int]}) - # B = TypedDict('B', {'x': int}) - # def reset_x(b: B) -> None: - # b['x'] = 0 - # - # So, `A` cannot be a subtype of `B`, while `B` can be a subtype of `A`, - # because you can use `B` everywhere you use `A`, but not the other way around. - if name in left.readonly_keys and name not in right.readonly_keys: - return False # (NOTE: Fallbacks don't matter.) return True else: diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 8381c9a1f7508..7a486d6603a00 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -284,6 +284,7 @@ def visit_typeddict_type(self, t: TypedDictType, /) -> Type: items, t.required_keys, t.readonly_keys, + t.is_closed, # TODO: This appears to be unsafe. cast(Any, t.fallback.accept(self)), t.line, diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 51d26afd55e46..a303621d75ef2 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1388,6 +1388,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: " must be enabled with --enable-incomplete-feature=InlineTypedDict", t, ) + is_closed = False required_keys = req_keys fallback = self.named_type("typing._TypedDict") for typ in t.extra_items_from: @@ -1407,9 +1408,13 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: if sub_item_name in p_analyzed.readonly_keys: readonly_keys.add(sub_item_name) else: + readonly_keys = t.readonly_keys required_keys = t.required_keys fallback = t.fallback - return TypedDictType(items, required_keys, readonly_keys, fallback, t.line, t.column) + is_closed = t.is_closed + return TypedDictType( + items, required_keys, readonly_keys, is_closed, fallback, t.line, t.column + ) def visit_raw_expression_type(self, t: RawExpressionType) -> Type: # We should never see a bare Literal. We synthesize these raw literals diff --git a/mypy/types.py b/mypy/types.py index bc06e36d7a47e..129d98894770f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -9,6 +9,7 @@ Any, ClassVar, Final, + NamedTuple, NewType, TypeAlias as _TypeAlias, TypeGuard, @@ -2984,6 +2985,26 @@ def slice( return TupleType(slice_items, fallback, self.line, self.column, self.implicit) +class TypedDictItem(NamedTuple): + """Type, mutability and requiredness of an item in a TypedDict. + + If typ is `None`, the item comes from a missing item in an open TypedDict, and + the type should be treated as if it were a `builtins.object`. (Missing items in + closed TypedDicts will have an uninhabited type.) + + TODO: pass a `builtins.object` instead of None when TypedDictType gains a + proper extra_items field. + """ + + typ: Type | None + required: bool + readonly: bool + + @property + def mutable(self) -> bool: + return not self.readonly + + class TypedDictType(ProperType): """Type of TypedDict object {'k1': v1, ..., 'kn': vn}. @@ -3008,6 +3029,7 @@ class TypedDictType(ProperType): "items", "required_keys", "readonly_keys", + "is_closed", "fallback", "extra_items_from", "to_be_mutated", @@ -3026,6 +3048,7 @@ def __init__( items: dict[str, Type], required_keys: set[str], readonly_keys: set[str], + is_closed: bool, fallback: Instance, line: int = -1, column: int = -1, @@ -3034,6 +3057,7 @@ def __init__( self.items = items self.required_keys = required_keys self.readonly_keys = readonly_keys + self.is_closed = is_closed self.fallback = fallback self.can_be_true = len(self.items) > 0 self.can_be_false = len(self.required_keys) == 0 @@ -3050,6 +3074,7 @@ def __hash__(self) -> int: self.fallback, frozenset(self.required_keys), frozenset(self.readonly_keys), + self.is_closed, ) ) @@ -3067,6 +3092,7 @@ def __eq__(self, other: object) -> bool: and self.fallback == other.fallback and self.required_keys == other.required_keys and self.readonly_keys == other.readonly_keys + and self.is_closed == other.is_closed ) def serialize(self) -> JsonDict: @@ -3076,6 +3102,7 @@ def serialize(self) -> JsonDict: "required_keys": sorted(self.required_keys), "readonly_keys": sorted(self.readonly_keys), "fallback": self.fallback.serialize(), + "is_closed": self.is_closed, } @classmethod @@ -3085,6 +3112,7 @@ def deserialize(cls, data: JsonDict) -> TypedDictType: {n: deserialize_type(t) for (n, t) in data["items"]}, set(data["required_keys"]), set(data["readonly_keys"]), + bool(data["is_closed"]), Instance.deserialize(data["fallback"]), ) @@ -3094,6 +3122,7 @@ def write(self, data: WriteBuffer) -> None: write_type_map(data, self.items) write_str_list(data, sorted(self.required_keys)) write_str_list(data, sorted(self.readonly_keys)) + write_bool(data, self.is_closed) write_tag(data, END_TAG) @classmethod @@ -3101,7 +3130,11 @@ def read(cls, data: ReadBuffer) -> TypedDictType: assert read_tag(data) == INSTANCE fallback = Instance.read(data) ret = TypedDictType( - read_type_map(data), set(read_str_list(data)), set(read_str_list(data)), fallback + read_type_map(data), + set(read_str_list(data)), + set(read_str_list(data)), + read_bool(data), + fallback, ) assert read_tag(data) == END_TAG return ret @@ -3127,6 +3160,7 @@ def copy_modified( item_names: list[str] | None = None, required_keys: set[str] | None = None, readonly_keys: set[str] | None = None, + is_closed: bool | None = None, ) -> TypedDictType: if fallback is None: fallback = self.fallback @@ -3138,13 +3172,14 @@ def copy_modified( required_keys = self.required_keys if readonly_keys is None: readonly_keys = self.readonly_keys + if is_closed is None: + is_closed = self.is_closed if item_names is not None: items = {k: v for (k, v) in items.items() if k in item_names} required_keys &= set(item_names) - return TypedDictType(items, required_keys, readonly_keys, fallback, self.line, self.column) - - def names_are_wider_than(self, other: TypedDictType) -> bool: - return len(other.items.keys() - self.items.keys()) == 0 + return TypedDictType( + items, required_keys, readonly_keys, is_closed, fallback, self.line, self.column + ) def zip(self, right: TypedDictType) -> Iterable[tuple[str, Type, Type]]: left = self @@ -3153,15 +3188,28 @@ def zip(self, right: TypedDictType) -> Iterable[tuple[str, Type, Type]]: if right_item_type is not None: yield (item_name, left_item_type, right_item_type) - def zipall(self, right: TypedDictType) -> Iterable[tuple[str, Type | None, Type | None]]: + def item(self, item_name: str) -> TypedDictItem: + item_type = self.items.get(item_name) + if item_type is not None: + is_required = item_name in self.required_keys + is_readonly = item_name in self.readonly_keys + elif self.is_closed: + item_type = UninhabitedType() + is_required = False + is_readonly = False + else: + is_required = False + is_readonly = True + return TypedDictItem(item_type, is_required, is_readonly) + + def zipall(self, right: TypedDictType) -> Iterable[tuple[str, TypedDictItem, TypedDictItem]]: left = self - for item_name, left_item_type in left.items.items(): - right_item_type = right.items.get(item_name) - yield (item_name, left_item_type, right_item_type) - for item_name, right_item_type in right.items.items(): + for item_name in left.items: + yield (item_name, left.item(item_name), right.item(item_name)) + for item_name in right.items: if item_name in left.items: continue - yield (item_name, None, right_item_type) + yield (item_name, left.item(item_name), right.item(item_name)) class RawExpressionType(ProperType): @@ -4001,6 +4049,8 @@ def item_str(name: str, typ: str) -> str: + ", ".join(item_str(name, typ.accept(self)) for name, typ in t.items.items()) + "}" ) + if t.is_closed: + s += ", closed=True" prefix = "" if t.fallback and t.fallback.type: if t.fallback.type.fullname not in TPDICT_FB_NAMES: diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 970ba45d0e8e2..d1e928441a9ec 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2742,16 +2742,16 @@ def f8(x: int, /, **kwargs: str) -> int: def f9(x: int, **kwargs: Unpack[Opt]) -> int: return 0 -reveal_type(Sneaky(f1).kwargs) # N: Revealed type is "builtins.dict[builtins.str, Never]" -reveal_type(Sneaky(f2, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int})" -reveal_type(Sneaky(f3, 1).kwargs) # N: Revealed type is "builtins.dict[builtins.str, Never]" -reveal_type(Sneaky(f4, x=1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x': builtins.int})" -reveal_type(Sneaky(f5, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y'?: builtins.int})" -reveal_type(Sneaky(f5, 1, 2).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y'?: builtins.int})" +reveal_type(Sneaky(f1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {}, closed=True)" +reveal_type(Sneaky(f2, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int}, closed=True)" +reveal_type(Sneaky(f3, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {}, closed=True)" +reveal_type(Sneaky(f4, x=1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x': builtins.int}, closed=True)" +reveal_type(Sneaky(f5, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y'?: builtins.int}, closed=True)" +reveal_type(Sneaky(f5, 1, 2).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y'?: builtins.int}, closed=True)" reveal_type(Sneaky(f6, x=1).kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" reveal_type(Sneaky(f6, x=1, y=2).kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" reveal_type(Sneaky(f7, 1, y='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int})" reveal_type(Sneaky(f8, 1, y='').kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" -reveal_type(Sneaky(f9, 1, y=0).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})" -reveal_type(Sneaky(f9, 1, y=0, z='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})" +reveal_type(Sneaky(f9, 1, y=0).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str}, closed=True)" +reveal_type(Sneaky(f9, 1, y=0, z='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str}, closed=True)" [builtins fixtures/paramspec.pyi] diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 10333d113a689..9eff3d98fbe09 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -748,8 +748,8 @@ tdb: TDB T = TypeVar("T") def f(x: T, y: T) -> T: ... # Join for recursive types is very basic, but just add tests that we don't crash. -reveal_type(f(tda1, tda2)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': TypedDict('__main__.TDA1', {'x': builtins.int, 'y': ...})})" -reveal_type(f(tda1, tdb)) # N: Revealed type is "TypedDict({})" +reveal_type(f(tda1, tda2)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': TypedDict('__main__.TDA2', {'x': builtins.int, 'y': ...})})" +reveal_type(f(tda1, tdb)) # N: Revealed type is "TypedDict({'x'=: builtins.object, 'y'=: builtins.object})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/check-serialize.test b/test-data/unit/check-serialize.test index 7bc7f78bd961e..f504e00688cff 100644 --- a/test-data/unit/check-serialize.test +++ b/test-data/unit/check-serialize.test @@ -1088,6 +1088,20 @@ main:2: note: Revealed type is "TypedDict('m.D', {'x'?: builtins.int, 'y'?: buil [out2] main:2: note: Revealed type is "TypedDict('m.D', {'x'?: builtins.int, 'y'?: builtins.str})" +[case testSerializeClosedTotalTypedDict] +from m import d +reveal_type(d) +[file m.py] +from typing import TypedDict +D = TypedDict('D', {'x': int, 'y': str}, closed=True) +d: D +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[out1] +main:2: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" +[out2] +main:2: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" + -- -- Modules -- diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index a2fdb5054829f..01414ee2f457a 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -313,7 +313,7 @@ class Point1(TypedDict): x: int class Point2(TypedDict): x: float -class Bad(Point1, Point2): # E: Overwriting TypedDict field "x" while merging +class Bad(Point1, Point2): # E: Incompatible definitions of field "x" in base classes "Point2" and "Point1" pass b: Bad @@ -327,13 +327,82 @@ from typing import TypedDict class Point1(TypedDict): x: int class Point2(Point1): - x: float # E: Overwriting TypedDict field "x" while extending + x: float # E: Definition of field "x" incompatible with base class "Point1" p2: Point2 reveal_type(p2) # N: Revealed type is "TypedDict('__main__.Point2', {'x': builtins.float})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testCanCreateTypedDictWithClassOverwriting3] +from typing import TypedDict + +class Point1(TypedDict): + x: float +class Point2(Point1): + x: int # E: Definition of field "x" incompatible with base class "Point1" + +p2: Point2 +reveal_type(p2) # N: Revealed type is "TypedDict('__main__.Point2', {'x': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testCanCreateTypedDictWithClassOverwriting4] +from typing import TypedDict + +class Point1(TypedDict): + x: int +class Point2(TypedDict, total=False): + x: int +class Bad(Point1, Point2): # E: Field "x" is required in base class "Point1" but can be deleted in base class "Point2" + pass + +b: Bad +reveal_type(b) # N: Revealed type is "TypedDict('__main__.Bad', {'x': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testCanCreateTypedDictWithClassOverwriting5] +from typing import TypedDict + +class Point1(TypedDict): + x: int +class Point2(Point1, total=False): + x: int # E: Field "x" is required in base class "Point1" + +p2: Point2 +reveal_type(p2) # N: Revealed type is "TypedDict('__main__.Point2', {'x'?: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testCanCreateTypedDictWithClassOverwriting6] +from typing import TypedDict + +class Point1(TypedDict, total=False): + x: int +class Point2(TypedDict): + x: int +class Bad(Point1, Point2): # E: Field "x" is required in base class "Point2" but can be deleted in base class "Point1" + pass + +b: Bad +reveal_type(b) # N: Revealed type is "TypedDict('__main__.Bad', {'x'?: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testCanCreateTypedDictWithClassOverwriting7] +from typing import TypedDict + +class Point1(TypedDict, total=False): + x: int +class Point2(Point1): + x: int # E: Field "x" can be deleted in base class "Point1" + +p2: Point2 +reveal_type(p2) # N: Revealed type is "TypedDict('__main__.Point2', {'x': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + -- Subtyping @@ -486,7 +555,7 @@ def f(a: A) -> None: pass l = [a, b] # Join generates an anonymous TypedDict f(l) # E: Argument 1 to "f" has incompatible type "list[TypedDict({'x': int})]"; expected "A" ll = [b, c] -f(ll) # E: Argument 1 to "f" has incompatible type "list[TypedDict({'x': int, 'z': str})]"; expected "A" +f(ll) # E: Argument 1 to "f" has incompatible type "list[TypedDict({'x': int, 'z': str, 'a'=: object})]"; expected "A" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -554,16 +623,28 @@ reveal_type(joined_points) # N: Revealed type is "TypedDict({'x': builtins.int, [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testJoinOfTypedDictRemovesNonequivalentKeys] -from typing import TypedDict +[case testJoinOfTypedDictJoinsNonequivalentKeys] +from typing import TypedDict, NotRequired CellWithInt = TypedDict('CellWithInt', {'value': object, 'meta': int}) -CellWithObject = TypedDict('CellWithObject', {'value': object, 'meta': object}) +CellWithString = TypedDict('CellWithString', {'value': object, 'meta': str}) +CellWithNonRequiredInt = TypedDict('CellWithNonRequiredInt', {'value': object, 'meta': NotRequired[int]}) +CellWithNonRequiredString = TypedDict('CellWithNonRequiredString', {'value': object, 'meta': NotRequired[str]}) c1 = CellWithInt(value=1, meta=42) -c2 = CellWithObject(value=2, meta='turtle doves') -joined_cells = [c1, c2] -reveal_type(c1) # N: Revealed type is "TypedDict('__main__.CellWithInt', {'value': builtins.object, 'meta': builtins.int})" -reveal_type(c2) # N: Revealed type is "TypedDict('__main__.CellWithObject', {'value': builtins.object, 'meta': builtins.object})" -reveal_type(joined_cells) # N: Revealed type is "builtins.list[TypedDict({'value': builtins.object})]" +c2 = CellWithString(value=2, meta='turtle doves') +c3 = CellWithNonRequiredInt(value=1) +c4 = CellWithNonRequiredString(value=2) +j12 = [c1, c2] +j14 = [c1, c4] +j32 = [c3, c2] +j34 = [c3, c4] +reveal_type(c1) # N: Revealed type is "TypedDict('__main__.CellWithInt', {'value': builtins.object, 'meta': builtins.int})" +reveal_type(c2) # N: Revealed type is "TypedDict('__main__.CellWithString', {'value': builtins.object, 'meta': builtins.str})" +reveal_type(c3) # N: Revealed type is "TypedDict('__main__.CellWithNonRequiredInt', {'value': builtins.object, 'meta'?: builtins.int})" +reveal_type(c4) # N: Revealed type is "TypedDict('__main__.CellWithNonRequiredString', {'value': builtins.object, 'meta'?: builtins.str})" +reveal_type(j12) # N: Revealed type is "builtins.list[TypedDict({'value': builtins.object, 'meta'=: builtins.object})]" +reveal_type(j14) # N: Revealed type is "builtins.list[TypedDict({'value': builtins.object, 'meta'?=: builtins.object})]" +reveal_type(j32) # N: Revealed type is "builtins.list[TypedDict({'value': builtins.object, 'meta'?=: builtins.object})]" +reveal_type(j34) # N: Revealed type is "builtins.list[TypedDict({'value': builtins.object, 'meta'?=: builtins.object})]" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -630,6 +711,53 @@ reveal_type(f(g)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': bui [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testMeetOfTypedDictsWithAny] +from typing import Any, TypedDict, TypeVar, Callable +A = TypedDict('A', {'x': Any}) +B = TypedDict('B', {'x': int}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def gAB(x: A, y: B) -> None: pass +def gBA(x: B, y: A) -> None: pass +reveal_type(f(gAB)) # N: Revealed type is "TypedDict({'x': builtins.int})" +reveal_type(f(gBA)) # N: Revealed type is "TypedDict({'x': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictMissingAndAny] +from typing import Any, TypedDict, TypeVar, Callable, ReadOnly +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +A = TypedDict('A', {'x': Any}) +B = TypedDict('B', {'y': Any}) +C = TypedDict('C', {'y': Any}, total=False) +def fAB(x: A, y: B) -> None: pass +def fBA(x: B, y: A) -> None: pass +def fAC(x: A, y: C) -> None: pass +def fCA(x: C, y: A) -> None: pass +if int(): + reveal_type(meet(fAB)) # N: Revealed type is "TypedDict({'x': Any, 'y': Any})" +if int(): + reveal_type(meet(fBA)) # N: Revealed type is "TypedDict({'y': Any, 'x': Any})" +if int(): + reveal_type(meet(fAC)) # N: Revealed type is "TypedDict({'x': Any, 'y'?: Any})" +if int(): + reveal_type(meet(fCA)) # N: Revealed type is "TypedDict({'y'?: Any, 'x': Any})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsNotRequiredNeverAndAny] +from typing import Any, NotRequired, TypedDict, TypeVar, Callable +from typing_extensions import Never +A = TypedDict('A', {'x': NotRequired[Any], 'y': NotRequired[Never]}) +B = TypedDict('B', {'x': NotRequired[Never], 'y': NotRequired[Any]}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: A, y: B) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'?: Never, 'y'?: Never})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testMeetOfTypedDictsWithIncompatibleCommonKeysIsUninhabited] from typing import TypedDict, TypeVar, Callable XYa = TypedDict('XYa', {'x': int, 'y': int}) @@ -765,8 +893,8 @@ T = TypeVar('T') def join(x: T, y: T) -> T: return x ab = join(A(x='', y=1, z=''), B(x='', z=1)) ac = join(A(x='', y=1, z=''), C(x='', y=0, z=1)) -ab['y'] # E: "y" is not a valid TypedDict key; expected one of ("x") -ac['a'] # E: "a" is not a valid TypedDict key; expected one of ("x", "y") +ab['y'] # E: "y" is not a valid TypedDict key; expected one of ("x", "z") +ac['a'] # E: "a" is not a valid TypedDict key; expected one of ("x", "y", "z") [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -1010,10 +1138,10 @@ p4: Point = {'x': 1, 'y': 2} [case testCannotCreateAnonymousTypedDictInstanceUsingDictLiteralWithExtraItems] from typing import TypedDict, TypeVar A = TypedDict('A', {'x': int, 'y': int}) -B = TypedDict('B', {'x': int, 'y': str}) +B = TypedDict('B', {'x': int}) T = TypeVar('T') def join(x: T, y: T) -> T: return x -ab = join(A(x=1, y=1), B(x=1, y='')) +ab = join(A(x=1, y=1), B(x=1)) if int(): ab = {'x': 1, 'z': 1} # E: Expected TypedDict key "x" but found keys ("x", "z") [builtins fixtures/dict.pyi] @@ -1027,7 +1155,7 @@ T = TypeVar('T') def join(x: T, y: T) -> T: return x ab = join(A(x=1, y=1, z=1), B(x=1, y=1, z='')) if int(): - ab = {} # E: Expected TypedDict keys ("x", "y") but found no keys + ab = {} # E: Expected TypedDict keys ("x", "y", "z") but found no keys [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -1376,6 +1504,7 @@ A = TypedDict('A', {'x': int}, total=0) # E: "total" argument must be a True or B = TypedDict('B', {'x': int}, total=bool) # E: "total" argument must be a True or False literal C = TypedDict('C', {'x': int}, x=False) # E: Unexpected keyword argument "x" for "TypedDict" D = TypedDict('D', {'x': int}, False) # E: Unexpected arguments to TypedDict() +E = TypedDict('E', {'x': int}, total=False, x=False) # E: Unexpected keyword argument "x" for "TypedDict" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -1448,7 +1577,7 @@ a: A b: B c: C reveal_type(j(a, b)) \ - # N: Revealed type is "TypedDict({})" + # N: Revealed type is "TypedDict({'x'?=: builtins.int})" reveal_type(j(b, b)) \ # N: Revealed type is "TypedDict({'x'?: builtins.int})" reveal_type(j(c, c)) \ @@ -1510,7 +1639,7 @@ def f(a: A) -> None: pass l = [a, b] # Join generates an anonymous TypedDict f(l) # E: Argument 1 to "f" has incompatible type "list[TypedDict({'x'?: int})]"; expected "A" ll = [b, c] -f(ll) # E: Argument 1 to "f" has incompatible type "list[TypedDict({'x'?: int, 'z'?: str})]"; expected "A" +f(ll) # E: Argument 1 to "f" has incompatible type "list[TypedDict({'x'?: int, 'z'?: str, 'a'?=: object})]"; expected "A" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -1764,6 +1893,28 @@ reveal_type(x['a']['b']) # N: Revealed type is "builtins.int" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testForwardReferenceInOverwrittenKey] +from typing import ReadOnly, TypedDict + +class X("XB"): pass +class Y("YB"): pass +class Z("ZB"): pass + +class A(TypedDict): + key: "X" +class B(A): + key: "Y" # E: Definition of field "key" incompatible with base class "A" +class C(A): + key: "Z" # E: Definition of field "key" incompatible with base class "A" +class D(A): + key: int # E: Definition of field "key" incompatible with base class "A" + +class XB: pass +class YB(X): pass +class ZB: pass +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testSelfRecursiveTypedDictInheriting] from typing import TypedDict @@ -2343,6 +2494,26 @@ v = {bad2: 2} # E: Missing key "num" for TypedDict "Value" \ [case testOperatorContainsNarrowsTypedDicts_unionWithList] from __future__ import annotations +from typing import assert_type, TypedDict, Union + +class D(TypedDict): + foo: int + + +d_or_list: D | list[str] + +if 'foo' in d_or_list: + assert_type(d_or_list, Union[D, list[str]]) +elif 'bar' in d_or_list: + assert_type(d_or_list, list[str]) +else: + assert_type(d_or_list, list[str]) + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_unionWithList_final] +from __future__ import annotations from typing import assert_type, final, TypedDict, Union @final @@ -2362,20 +2533,67 @@ else: [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] -[case testOperatorContainsNarrowsTypedDicts_total] +[case testOperatorContainsNarrowsTypedDicts_total_never] from __future__ import annotations from typing import assert_type, final, Literal, TypedDict, TypeVar, Union +from typing_extensions import Never, NotRequired -@final class D1(TypedDict): foo: int + bar: NotRequired[Never] + invalid: NotRequired[Never] + +class D2(TypedDict): + foo: NotRequired[Never] + bar: int + invalid: NotRequired[Never] + +d: D1 | D2 + +if 'foo' in d: + assert_type(d, D1) +else: + assert_type(d, D2) + +foo_or_bar: Literal['foo', 'bar'] +if foo_or_bar in d: + assert_type(d, Union[D1, D2]) +else: + assert_type(d, Union[D1, D2]) + +foo_or_invalid: Literal['foo', 'invalid'] +if foo_or_invalid in d: + assert_type(d, D1) + # won't narrow 'foo_or_invalid' + assert_type(foo_or_invalid, Literal['foo', 'invalid']) +else: + assert_type(d, Union[D1, D2]) + # won't narrow 'foo_or_invalid' + assert_type(foo_or_invalid, Literal['foo', 'invalid']) +TD = TypeVar('TD', D1, D2) + +def f(arg: TD) -> None: + value: int + if 'foo' in arg: + assert_type(arg['foo'], int) + else: + assert_type(arg['bar'], int) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_total_final] +from __future__ import annotations +from typing import assert_type, final, Literal, TypedDict, TypeVar, Union + +@final +class D1(TypedDict): + foo: int @final class D2(TypedDict): bar: int - d: D1 | D2 if 'foo' in d: @@ -2407,7 +2625,49 @@ def f(arg: TD) -> None: assert_type(arg['foo'], int) else: assert_type(arg['bar'], int) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_never] +# flags: --warn-unreachable +from __future__ import annotations +from typing import assert_type, TypedDict, Union +from typing_extensions import Never, NotRequired + +class DFooNotBar(TypedDict): + foo: int + bar: NotRequired[Never] + + +class DBar(TypedDict): + bar: int + + +d_bar: DBar + +if 'bar' in d_bar: + assert_type(d_bar, DBar) +else: + spam = 'ham' # E: Statement is unreachable + +if 'spam' in d_bar: + assert_type(d_bar, DBar) +else: + assert_type(d_bar, DBar) + +d_foo_not_bar: DFooNotBar +if 'spam' in d_foo_not_bar: + spam = 'ham' +else: + assert_type(d_foo_not_bar, DFooNotBar) + +d_union: DFooNotBar | DBar + +if 'foo' in d_union: + assert_type(d_union, Union[DFooNotBar, DBar]) +else: + assert_type(d_union, DBar) [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] @@ -2455,7 +2715,41 @@ else: [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] -[case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse] +[case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse_never] +from __future__ import annotations +from typing import assert_type, Literal, TypedDict, Union +from typing_extensions import Never, NotRequired + +class DTotal(TypedDict): + required_key: int + optional_key: NotRequired[Never] + +class DNotTotal(TypedDict, total=False): + required_key: Never + optional_key: int + +d: DTotal | DNotTotal + +if 'required_key' in d: + assert_type(d, DTotal) +else: + assert_type(d, DNotTotal) + +if 'optional_key' in d: + assert_type(d, DNotTotal) +else: + assert_type(d, Union[DTotal, DNotTotal]) + +key: Literal['optional_key', 'required_key'] +if key in d: + assert_type(d, Union[DTotal, DNotTotal]) +else: + assert_type(d, Union[DTotal, DNotTotal]) + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse_final] from __future__ import annotations from typing import assert_type, final, Literal, TypedDict, Union @@ -2463,12 +2757,10 @@ from typing import assert_type, final, Literal, TypedDict, Union class DTotal(TypedDict): required_key: int - @final class DNotTotal(TypedDict, total=False): optional_key: int - d: DTotal | DNotTotal if 'required_key' in d: @@ -2490,7 +2782,36 @@ else: [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] -[case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired] +[case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired_never] +from __future__ import annotations +from typing import assert_type, final, TypedDict, Union +from typing_extensions import Never, Required, NotRequired + +class D1(TypedDict): + required_key: Required[int] + optional_key: NotRequired[int] + +class D2(TypedDict): + abc: int + xyz: int + required_key: NotRequired[Never] + optional_key: NotRequired[Never] + +d: D1 | D2 + +if 'required_key' in d: + assert_type(d, D1) +else: + assert_type(d, D2) + +if 'optional_key' in d: + assert_type(d, D1) +else: + assert_type(d, Union[D1, D2]) + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] +[case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired_final] from __future__ import annotations from typing import assert_type, final, TypedDict, Union from typing_extensions import Required, NotRequired @@ -2500,13 +2821,11 @@ class D1(TypedDict): required_key: Required[int] optional_key: NotRequired[int] - @final class D2(TypedDict): abc: int xyz: int - d: D1 | D2 if 'required_key' in d: @@ -4048,6 +4367,43 @@ x["other"] = "a" # E: ReadOnly TypedDict key "other" TypedDict is mutated [typ [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testTypedDictFunctionalSyntaxWithMultipleSpecialForms] +from typing import ReadOnly, Required, NotRequired, TypedDict +from typing_extensions import Annotated +D1 = TypedDict("D1", {"x": int, "y": ReadOnly[Required[int]]}, total=False) +D2 = TypedDict("D2", {"x": int, "y": Required[ReadOnly[int]]}, total=False) +D3 = TypedDict("D3", {"x": int, "y": ReadOnly[NotRequired[int]]}) +D4 = TypedDict("D4", {"x": int, "y": NotRequired[ReadOnly[int]]}) +D5 = TypedDict("D5", {"x": int, "y": Annotated[ReadOnly[int], "some annotation text"]}) +D6 = TypedDict("D6", {"x": int, "y": ReadOnly[Annotated[int, "some annotation text"]]}) +D7 = TypedDict("D7", {"x": int, "y": Annotated[ReadOnly[NotRequired[int]], "some annotation text"]}) +D8 = TypedDict("D8", {"x": int, "y": NotRequired[ReadOnly[Annotated[int, "some annotation text"]]]}) +d1: D1 +d2: D2 +d3: D3 +d4: D4 +d5: D5 +d6: D6 +d7: D7 +d8: D8 +reveal_type(d1) # N: Revealed type is "TypedDict('__main__.D1', {'x'?: builtins.int, 'y'=: builtins.int})" +reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'x'?: builtins.int, 'y'=: builtins.int})" +reveal_type(d3) # N: Revealed type is "TypedDict('__main__.D3', {'x': builtins.int, 'y'?=: builtins.int})" +reveal_type(d4) # N: Revealed type is "TypedDict('__main__.D4', {'x': builtins.int, 'y'?=: builtins.int})" +reveal_type(d5) # N: Revealed type is "TypedDict('__main__.D5', {'x': builtins.int, 'y'=: builtins.int})" +reveal_type(d6) # N: Revealed type is "TypedDict('__main__.D6', {'x': builtins.int, 'y'=: builtins.int})" +reveal_type(d7) # N: Revealed type is "TypedDict('__main__.D7', {'x': builtins.int, 'y'?=: builtins.int})" +reveal_type(d8) # N: Revealed type is "TypedDict('__main__.D8', {'x': builtins.int, 'y'?=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictFunctionalSyntaxWithContradictorySpecialForms] +from typing import Required, NotRequired, TypedDict +D1 = TypedDict("D1", {"x": NotRequired[Required[int]]}) # E: "Required[]" type cannot be nested +D2 = TypedDict("D2", {"x": Required[NotRequired[int]]}) # E: "NotRequired[]" type cannot be nested +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testTypedDictReadOnlyCreation] from typing import ReadOnly, TypedDict @@ -4264,27 +4620,267 @@ accepts_B(b) [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testTypedDictRequiredConsistentWithNotRequiredReadOnly] -from typing import NotRequired, ReadOnly, Required, TypedDict - -class A(TypedDict): - x: NotRequired[ReadOnly[str]] - -class B(TypedDict): - x: Required[str] - -def f(b: B): - a: A = b # ok +[case testTypedDictReadOnlyTypeVarBoundSubtyping] +from typing import ReadOnly, TypedDict, TypeVar +A = TypedDict('A', {'x': ReadOnly[int]}) +B = TypedDict('B', {'x': int}) +TA = TypeVar('TA', bound=A) +TB = TypeVar('TB', bound=B) +def fA(t: TA) -> TA: return t +def fB(t: TB) -> TB: return t +a: A +b: B +fA(a) +fA(b) +fB(a) # E: Value of type variable "TB" of "fB" cannot be "A" +fB(b) [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testTypedDictReadOnlyCall] +[case testTypedDictReadOnlySubclassing] from typing import ReadOnly, TypedDict -TP = TypedDict("TP", {"one": int, "other": ReadOnly[str]}) +class A(TypedDict): + key: ReadOnly[str] -x: TP -reveal_type(x["one"]) # N: Revealed type is "builtins.int" +class B(A): + key: str + +a: A +b: B + +def accepts_A(d: A): ... +def accepts_B(d: B): ... + +accepts_A(a) +accepts_A(b) +accepts_B(a) # E: Argument 1 to "accepts_B" has incompatible type "A"; expected "B" +accepts_B(b) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlySubclassingFieldSubtype] +from typing import ReadOnly, TypedDict + +class A(TypedDict): + key: ReadOnly[float] +class B(A): + key: int +class C(A): + key: ReadOnly[int] + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'key'=: builtins.float})" +b: B +reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'key': builtins.int})" +c: C +reveal_type(c) # N: Revealed type is "TypedDict('__main__.C', {'key'=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlySubclassingFieldSubtypeWithForwardReferenceBases] +from typing import ReadOnly, TypedDict + +class X("XB"): pass +class Y("YB"): pass +class Z("ZB"): pass + +class A(TypedDict): + key: ReadOnly["X"] +class B(A): + key: "Y" # ok: Y subclasses X via YB +class C(A): + key: "Z" # E: Definition of field "key" incompatible with base class "A" + +x: A +y: B +x = y # ok: B is a valid subtype of A + +class XB: pass +class YB(X): pass +class ZB: pass +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlyIllegalSubclassingFieldSupertype] +from typing import ReadOnly, TypedDict + +class A(TypedDict): + key: ReadOnly[int] +class B(A): + key: float # E: Definition of field "key" incompatible with base class "A" +class C(A): + key: ReadOnly[float] # E: Definition of field "key" incompatible with base class "A" + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'key'=: builtins.int})" +b: B +reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'key': builtins.float})" +c: C +reveal_type(c) # N: Revealed type is "TypedDict('__main__.C', {'key'=: builtins.float})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlyMixinFieldSubtype] +from typing import ReadOnly, TypedDict + +class A(TypedDict): + key: ReadOnly[float] + +class B(TypedDict): + key: ReadOnly[int] + +class C(B, A): # Left-most base "wins" + pass + +class D(A, B): # E: Incompatible definitions of field "key" in base classes "B" and "A" \ + # N: This can be resolved by redeclaring the field "key" with a mutually compatible type + pass + +class E(A, B): + key: ReadOnly[int] # ok: explicit redefinition + +c: C +reveal_type(c) # N: Revealed type is "TypedDict('__main__.C', {'key'=: builtins.int})" +d: D +reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'key'=: builtins.float})" +e: E +reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'key'=: builtins.int})" + +# Verify that subclasses are valid subtypes +a: A +b: B +a = c +b = c +a = e +b = e +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlySubclassingTotality] +from typing import ReadOnly, TypedDict + +class A(TypedDict, total=False): + key: ReadOnly[int] + +class B(A): + key: int + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'key'?=: builtins.int})" +b: B +reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'key': builtins.int})" + +a = b # ok: B is a valid subtype of A +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlyIllegalSubclassingTotality] +from typing import ReadOnly, TypedDict + +class A(TypedDict): + key: ReadOnly[int] + +class B(A, total=False): + key: int # E: Field "key" is required in base class "A" + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'key'=: builtins.int})" +b: B +reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'key'?: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlyMixinTotality] +from typing import ReadOnly, TypedDict + +class A(TypedDict, total=True): + key: ReadOnly[int] + +class B(TypedDict, total=False): + key: ReadOnly[int] + +class C(B, A): + pass +class D(A, B): + pass + +c: C +reveal_type(c) # N: Revealed type is "TypedDict('__main__.C', {'key'=: builtins.int})" +d: D +reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'key'=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlyMixinWithMutableField] +from typing import ReadOnly, TypedDict + +class A(TypedDict): + key: ReadOnly[float] + +class B(TypedDict): + key: float + +class C(TypedDict): + key: int + +class D1(A, B): pass +d1: D1 +reveal_type(d1) # N: Revealed type is "TypedDict('__main__.D1', {'key': builtins.float})" +class D2(B, A): pass +d2: D2 +reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'key': builtins.float})" +class D3(A, C): pass +d3: D3 +reveal_type(d3) # N: Revealed type is "TypedDict('__main__.D3', {'key': builtins.int})" +class D4(C, A): pass +d4: D4 +reveal_type(d4) # N: Revealed type is "TypedDict('__main__.D4', {'key': builtins.int})" +class D5(A, B, C): pass # E: Incompatible definitions of field "key" in base classes "C" and "B" +d5: D5 +reveal_type(d5) # N: Revealed type is "TypedDict('__main__.D5', {'key': builtins.float})" +class D6(C, B, A): pass # E: Incompatible definitions of field "key" in base classes "B" and "C" +d6: D6 +reveal_type(d6) # N: Revealed type is "TypedDict('__main__.D6', {'key': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + + +[case testTypedDictRequiredConsistentWithNotRequiredReadOnly] +from typing import NotRequired, ReadOnly, Required, TypedDict + +class A(TypedDict): + x: NotRequired[ReadOnly[str]] + +class B(TypedDict): + x: Required[str] + +def f(b: B): + a: A = b # ok +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictNotRequiredInconsistentWithRequiredReadOnly] +from typing import NotRequired, ReadOnly, Required, TypedDict + +class A(TypedDict): + x: Required[ReadOnly[str]] + +class B(TypedDict): + x: NotRequired[str] + +def f(b: B): + a: A = b # E: Incompatible types in assignment (expression has type "B", variable has type "A") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictReadOnlyCall] +from typing import ReadOnly, TypedDict + +TP = TypedDict("TP", {"one": int, "other": ReadOnly[str]}) + +x: TP +reveal_type(x["one"]) # N: Revealed type is "builtins.int" reveal_type(x["other"]) # N: Revealed type is "builtins.str" x["one"] = 1 # ok x["other"] = "a" # E: ReadOnly TypedDict key "other" TypedDict is mutated @@ -4325,7 +4921,7 @@ x["two"] = "a" # E: ReadOnly TypedDict key "two" TypedDict is mutated [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testMeetOfTypedDictsWithReadOnly] +[case testMeetOfTypedDictsWithNonOverlappingReadOnlyKeys] from typing import TypeVar, Callable, TypedDict, ReadOnly XY = TypedDict('XY', {'x': ReadOnly[int], 'y': int}) YZ = TypedDict('YZ', {'y': int, 'z': ReadOnly[int]}) @@ -4336,6 +4932,160 @@ reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'=: builtins.int, 'y': bu [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testMeetOfTypedDictsWithOverlappingReadOnlyAndMutableKeys] +from typing import TypedDict, TypeVar, Callable, ReadOnly +XY = TypedDict('XY', {'x': int, 'y': int}) +YZ = TypedDict('YZ', {'y': ReadOnly[int], 'z': int}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: XY, y: YZ) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': builtins.int, 'z': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsWithDifferingMutabilityAndAny] +from typing import Any, TypedDict, TypeVar, Callable, ReadOnly +A = TypedDict('A', {'x': Any}) +B = TypedDict('B', {'x': ReadOnly[int]}) +C = TypedDict('C', {'x': ReadOnly[Any]}) +D = TypedDict('D', {'x': int}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def gAB(x: A, y: B) -> None: pass +def gBA(x: B, y: A) -> None: pass +def gCD(x: C, y: D) -> None: pass +def gDC(x: D, y: C) -> None: pass +reveal_type(f(gAB)) # N: Revealed type is "TypedDict({'x': builtins.int})" +reveal_type(f(gBA)) # N: Revealed type is "TypedDict({'x': builtins.int})" +reveal_type(f(gCD)) # N: Revealed type is "TypedDict({'x': builtins.int})" +reveal_type(f(gDC)) # N: Revealed type is "TypedDict({'x': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsWithIncompatibleReadOnlyCommonKeysIsUninhabitedUnlessNotRequired] +from typing import TypedDict, TypeVar, Callable, ReadOnly +A = TypedDict('A', {'x': ReadOnly[int]}) +B = TypedDict('B', {'x': ReadOnly[str]}) +C = TypedDict('C', {'x': ReadOnly[int]}, total=False) +D = TypedDict('D', {'x': ReadOnly[str]}, total=False) +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +def fAB(x: A, y: B) -> None: pass +def fAD(x: A, y: D) -> None: pass +def fCB(x: C, y: B) -> None: pass +def fCD(x: C, y: D) -> None: pass +if int(): + reveal_type(meet(fAB)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fAD)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fCB)) # N: Revealed type is "Never" +reveal_type(meet(fCD)) # N: Revealed type is "TypedDict({'x'?=: Never})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsWithCompatibleReadOnlyAndMutableCommonKeys] +from typing import TypedDict, TypeVar, Callable, NotRequired, ReadOnly, Union +A = TypedDict('A', {'x': int, 'y': ReadOnly[Union[int, str]]}) +B = TypedDict('B', {'x': ReadOnly[float], 'y': str}) +C = TypedDict('C', {'x': ReadOnly[NotRequired[float]], 'y': str}) +D = TypedDict('D', {'x': NotRequired[int], 'y': ReadOnly[Union[int, str]]}) +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +def fAB(x: A, y: B) -> None: pass +def fBA(x: B, y: A) -> None: pass +def fAC(x: A, y: C) -> None: pass +def fCA(x: C, y: A) -> None: pass +def fCD(x: C, y: D) -> None: pass +def fDC(x: D, y: C) -> None: pass +reveal_type(meet(fAB)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': builtins.str})" +reveal_type(meet(fBA)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': builtins.str})" +reveal_type(meet(fAC)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': builtins.str})" +reveal_type(meet(fCA)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y': builtins.str})" +reveal_type(meet(fCD)) # N: Revealed type is "TypedDict({'x'?: builtins.int, 'y': builtins.str})" +reveal_type(meet(fDC)) # N: Revealed type is "TypedDict({'x'?: builtins.int, 'y': builtins.str})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsWithIncompatibleReadOnlyAndMutableCommonKeysIsUninhabited] +from typing import TypedDict, TypeVar, Callable, ReadOnly +XYa = TypedDict('XYa', {'x': int, 'y': ReadOnly[int]}) +YbZ = TypedDict('YbZ', {'y': str, 'z': int}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: XYa, y: YbZ) -> None: pass +reveal_type(f(g)) # N: Revealed type is "Never" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsWithCompatibleReadOnlyCommonKeys] +from typing import TypedDict, TypeVar, Callable, ReadOnly +A = TypedDict('A', {'a': int}) +B = TypedDict('B', {'b': int}) +Xa = TypedDict('Xa', {'x': ReadOnly[A]}) +Xb = TypedDict('Xb', {'x': ReadOnly[B]}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: Xa, y: Xb) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'=: TypedDict({'a': builtins.int, 'b': builtins.int})})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsWithReadOnlyKeysAndAny] +from typing import Any, TypedDict, TypeVar, Callable, ReadOnly +A = TypedDict('A', {'x': ReadOnly[Any]}) +B = TypedDict('B', {'x': ReadOnly[int]}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: A, y: B) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfReadOnlyTypedDictsWithNoCommonKeysHasAllKeysAndNewFallback] +from typing import TypedDict, TypeVar, Callable, ReadOnly +X = TypedDict('X', {'x': ReadOnly[int]}) +Z = TypedDict('Z', {'z': ReadOnly[int]}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: X, y: Z) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'=: builtins.int, 'z'=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfReadOnlyTypedDictsWithNonTotal] +from typing import TypedDict, TypeVar, Callable, ReadOnly +XY = TypedDict('XY', {'x': ReadOnly[int], 'y': ReadOnly[int]}, total=False) +YZ = TypedDict('YZ', {'y': ReadOnly[int], 'z': ReadOnly[int]}, total=False) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: XY, y: YZ) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'?=: builtins.int, 'y'?=: builtins.int, 'z'?=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfReadOnlyTypedDictsWithNonOverlappingNonTotalAndTotal] +from typing import TypedDict, TypeVar, Callable, ReadOnly +XY = TypedDict('XY', {'x': ReadOnly[int]}, total=False) +YZ = TypedDict('YZ', {'y': ReadOnly[int], 'z': ReadOnly[int]}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: XY, y: YZ) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'?=: builtins.int, 'y'=: builtins.int, 'z'=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfReadOnlyTypedDictsWithOverlappingNonTotalAndTotal] +from typing import TypedDict, TypeVar, Callable, ReadOnly +XY = TypedDict('XY', {'x': ReadOnly[int], 'y': ReadOnly[int]}, total=False) +YZ = TypedDict('YZ', {'y': ReadOnly[int], 'z': ReadOnly[int]}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: XY, y: YZ) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({'x'?=: builtins.int, 'y'=: builtins.int, 'z'=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testTypedDictReadOnlyUnpack] from typing import TypedDict from typing_extensions import Unpack, ReadOnly @@ -4425,6 +5175,725 @@ c: C = d # E: Incompatible types in assignment (expression has type "D", variab [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testTypedDictJoinWithReadOnly] +from typing import ReadOnly, TypedDict, TypeVar +A = TypedDict('A', {'x': ReadOnly[int]}) +B = TypedDict('B', {'x': ReadOnly[int]}, total=False) +C = TypedDict('C', {'x': int}) +D = TypedDict('D', {'x': int}, total=False) +T = TypeVar('T') +def j(x: T, y: T) -> T: return x +a: A +b: B +c: C +d: D +reveal_type(j(a, b)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" +reveal_type(j(b, a)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" +reveal_type(j(a, c)) # N: Revealed type is "TypedDict({'x'=: builtins.int})" +reveal_type(j(c, a)) # N: Revealed type is "TypedDict({'x'=: builtins.int})" +reveal_type(j(a, d)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" +reveal_type(j(d, a)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" +reveal_type(j(b, c)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" +reveal_type(j(c, b)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + + +# Closed +# See https://peps.python.org/pep-0728/ + +[case testTypedDictWithClosedFalse] +from typing import TypedDict +D = TypedDict('D', {'x': int, 'y': str}, closed=False) +d: D +reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int, 'y': builtins.str})" +class E(TypedDict, closed=False): + x: int + y: str +e: E +reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'x': builtins.int, 'y': builtins.str})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictWithClosedTrue] +from typing import TypedDict +D = TypedDict('D', {'x': int, 'y': str}, closed=True) +d: D +reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" +class E(TypedDict, closed=True): + x: int + y: str +e: E +reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'x': builtins.int, 'y': builtins.str}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictWithInvalidClosedArgument] +from typing import TypedDict +A = TypedDict('A', {'x': int}, closed=0) # E: "closed" argument must be a True or False literal +B = TypedDict('B', {'x': int}, closed=bool) # E: "closed" argument must be a True or False literal +class C(TypedDict, closed=0): # E: "closed" argument must be a True or False literal + x: int +class D(TypedDict, closed=bool): # E: "closed" argument must be a True or False literal + x: int +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictWithClosedAndTotal] +from typing import TypedDict +A = TypedDict('A', {'x': int, 'y': str}, total=False, closed=True) +B = TypedDict('B', {'x': int, 'y': str}, closed=True, total=False) +C = TypedDict('C', {'x': int, 'y': str}, total=True, closed=False) +a: A +b: B +c: C +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'x'?: builtins.int, 'y'?: builtins.str}, closed=True)" +reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'x'?: builtins.int, 'y'?: builtins.str}, closed=True)" +reveal_type(c) # N: Revealed type is "TypedDict('__main__.C', {'x': builtins.int, 'y': builtins.str})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictWithDuplicateKeywordArguments_no_parallel] +from typing import TypedDict +A = TypedDict('A', {'x': int}, closed=True, closed=False) # E: Repeated keyword argument "closed" for "TypedDict" +B = TypedDict('B', {'x': int}, total=True, total=False) # E: Repeated keyword argument "total" for "TypedDict" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictSubclassingClosed] +from typing import TypedDict +class D(TypedDict, closed=True): + x: int + y: float +d: D +reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int, 'y': builtins.float}, closed=True)" + +class D1(D): pass +d1: D1 +reveal_type(d1) # N: Revealed type is "TypedDict('__main__.D1', {'x': builtins.int, 'y': builtins.float}, closed=True)" + +class D2(D, closed=True): pass +d2: D2 +reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'x': builtins.int, 'y': builtins.float}, closed=True)" + +class D3(D, closed=False): pass # E: Open TypedDict class cannot subclass closed TypedDict class "D" +d3: D3 +reveal_type(d3) # N: Revealed type is "TypedDict('__main__.D3', {'x': builtins.int, 'y': builtins.float})" + +class D4(D): + z: int # E: Cannot extend closed base class "D" with new field "z" +d4: D4 +reveal_type(d4) # N: Revealed type is "TypedDict('__main__.D4', {'x': builtins.int, 'y': builtins.float, 'z': builtins.int}, closed=True)" + +class D5(D): + x: int +d5: D5 +reveal_type(d5) # N: Revealed type is "TypedDict('__main__.D5', {'x': builtins.int, 'y': builtins.float}, closed=True)" + +class D6(D): + x: float # E: Definition of field "x" incompatible with base class "D" + y: int # E: Definition of field "y" incompatible with base class "D" +d6: D6 +reveal_type(d6) # N: Revealed type is "TypedDict('__main__.D6', {'x': builtins.float, 'y': builtins.int}, closed=True)" + +class D7(TypedDict, closed=True): + x: int + z: float +class D8(D7, D): # E: Cannot extend closed base class "D7" with field "y" from base class "D" \ + # E: Cannot extend closed base class "D" with field "z" from base class "D7" + pass +d8: D8 +reveal_type(d8) # N: Revealed type is "TypedDict('__main__.D8', {'x': builtins.int, 'y': builtins.float, 'z': builtins.float}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictSubclassingOpenAndClosed] +from typing import TypedDict +class B1(TypedDict, closed=True): + x: int + y: float +class B2(TypedDict): + x: int +class D1(B1, B2): pass +class D2(B2, B1): pass +d1: D1 +d2: D2 +reveal_type(d1) # N: Revealed type is "TypedDict('__main__.D1', {'x': builtins.int, 'y': builtins.float}, closed=True)" +reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'x': builtins.int, 'y': builtins.float}, closed=True)" +class B3(TypedDict): + z: int +class D3(B1, B3): pass # E: Cannot extend closed base class "B1" with field "z" from base class "B3" +class D4(B3, B1): pass # E: Cannot extend closed base class "B1" with field "z" from base class "B3" +d3: D3 +d4: D4 +reveal_type(d3) # N: Revealed type is "TypedDict('__main__.D3', {'z': builtins.int, 'x': builtins.int, 'y': builtins.float}, closed=True)" +reveal_type(d4) # N: Revealed type is "TypedDict('__main__.D4', {'x': builtins.int, 'y': builtins.float, 'z': builtins.int}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testCanSubclassClosedTypedDictWithForwardDeclarations] +from typing import TypedDict, final + +class D1(TypedDict, closed=True): + forward_declared: "ForwardDeclared" + +class D2(D1): + pass + +class ForwardDeclared: pass + +d2: D2 +reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'forward_declared': __main__.ForwardDeclared}, closed=True)" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypedDictSubtypingClosedMustRemainClosed] +from typing import TypedDict +A = TypedDict('A', {'x': int}, closed=True) +B = TypedDict('B', {'x': int}, closed=True) +C = TypedDict('C', {'x': int}, closed=False) +D = TypedDict('D', {'x': int}, closed=False) +def f(x: A) -> None: pass +def g(x: C) -> None: pass +a: A +b: B +c: C +d: D +f(b) +f(d) # E: Argument 1 to "f" has incompatible type "D"; expected "A" +g(d) +g(b) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictSubtypingCannotAddKeysToAClosedSupertype] +from typing import TypedDict +A = TypedDict('A', {'x': int}, closed=True) +B = TypedDict('B', {'x': int, 'y': int}, closed=True) +C = TypedDict('C', {'x': int}) +D = TypedDict('D', {'x': int, 'y': int}) +def f(x: A) -> None: pass +def g(x: C) -> None: pass +b: B +d: D +f(b) # E: Argument 1 to "f" has incompatible type "B"; expected "A" +g(d) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictSubtypingCanDropOptionalReadonlyKeysInAClosedSubtype] +from typing import ReadOnly, TypedDict +# Optional, readonly, closed: permitted +A = TypedDict('A', {'x': int, 'y': ReadOnly[int]}, total=False, closed=True) +B = TypedDict('B', {'x': int}, total=False, closed=True) +b: B +def f_a(x: A) -> None: pass +f_a(b) + +# Optional, readonly, open: not permitted +C = TypedDict('C', {'x': int, 'y': ReadOnly[int]}, total=False) +D = TypedDict('D', {'x': int}, total=False) +d: D +def f_c(x: C) -> None: pass +f_c(d) # E: Argument 1 to "f_c" has incompatible type "D"; expected "C" + +# Optional, mutable, closed: not permitted +E = TypedDict('E', {'x': int, 'y': int}, total=False, closed=True) +F = TypedDict('F', {'x': int}, total=False, closed=True) +f: F +def f_e(x: E) -> None: pass +f_e(f) # E: Argument 1 to "f_e" has incompatible type "F"; expected "E" + +# Required, readonly, closed: not permitted +G = TypedDict('G', {'x': int, 'y': ReadOnly[int]}, closed=True) +H = TypedDict('H', {'x': int}, closed=True) +h: H +def f_g(x: G) -> None: pass +f_g(h) # E: Argument 1 to "f_g" has incompatible type "H"; expected "G" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictSubtypingClosedTypeVarBound] +from typing import TypedDict, TypeVar +A = TypedDict('A', {'x': int}, closed=True) +B = TypedDict('B', {'x': int}) +TA = TypeVar('TA', bound=A) +TB = TypeVar('TB', bound=B) +def fA(t: TA) -> TA: return t +def fB(t: TB) -> TB: return t +a: A +b: B +fA(a) +fA(b) # E: Value of type variable "TA" of "fA" cannot be "B" +fB(a) +fB(b) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictSubtypingClosedExplicitNever] +from typing import NotRequired, ReadOnly, TypedDict +from typing_extensions import Never +A = TypedDict('A', {}, closed=True) +B = TypedDict('B', {'x': NotRequired[Never]}, closed=True) +C = TypedDict('C', {'x': ReadOnly[NotRequired[Never]]}, closed=True) +def f_a(x: A) -> None: pass +def f_b(x: B) -> None: pass +def f_c(x: C) -> None: pass +a: A +b: B +c: C +# Logically identical types: legal +f_a(b) +f_b(a) +# Subtype allows key removal: legal +f_c(a) +f_c(b) +# Subtype prevents key removal: illegal +f_a(c) # E: Argument 1 to "f_a" has incompatible type "C"; expected "A" +f_b(c) # E: Argument 1 to "f_b" has incompatible type "C"; expected "B" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testJoinOfClosedTypedDict] +from typing import Any, TypedDict, TypeVar +A = TypedDict('A', {'x': int}, closed=True) +B = TypedDict('B', {'y': int}) +C = TypedDict('C', {'y': int}, closed=True) +D = TypedDict('D', {'x': int, 'y': int}, closed=True) +E = TypedDict('E', {'x': Any, 'y': Any}) +T = TypeVar('T') +def j(x: T, y: T) -> T: return x +a: A +b: B +c: C +d: D +e: E +reveal_type(j(a, b)) # N: Revealed type is "TypedDict({'y'?=: builtins.int})" +reveal_type(j(b, a)) # N: Revealed type is "TypedDict({'y'?=: builtins.int})" +reveal_type(j(a, c)) # N: Revealed type is "TypedDict({'x'?=: builtins.int, 'y'?=: builtins.int}, closed=True)" +reveal_type(j(c, a)) # N: Revealed type is "TypedDict({'y'?=: builtins.int, 'x'?=: builtins.int}, closed=True)" +reveal_type(j(a, d)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y'?=: builtins.int}, closed=True)" +reveal_type(j(d, a)) # N: Revealed type is "TypedDict({'x': builtins.int, 'y'?=: builtins.int}, closed=True)" +reveal_type(j(a, e)) # N: Revealed type is "TypedDict({'x': Any, 'y'?=: Any})" +reveal_type(j(e, a)) # N: Revealed type is "TypedDict({'x': Any, 'y'?=: Any})" +reveal_type(j(b, e)) # N: Revealed type is "TypedDict({'y': Any})" +reveal_type(j(e, b)) # N: Revealed type is "TypedDict({'y': Any})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfClosedTypedDictsWithMatchingRequiredKeysIsNotAnonymous] +from typing import TypedDict, TypeVar, Callable +X = TypedDict('X', {'x': int, 'y': int}, closed=True) +Y = TypedDict('Y', {'x': int, 'y': int}, closed=True) +Z = TypedDict('Z', {'x': int, 'y': int}) +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +def fXY(x: X, y: Y) -> None: pass +def fYX(x: Y, y: X) -> None: pass +def fXZ(x: X, y: Z) -> None: pass +def fZX(x: Z, y: X) -> None: pass +reveal_type(meet(fXY)) # N: Revealed type is "TypedDict('__main__.X', {'x': builtins.int, 'y': builtins.int}, closed=True)" +reveal_type(meet(fYX)) # N: Revealed type is "TypedDict('__main__.Y', {'x': builtins.int, 'y': builtins.int}, closed=True)" +reveal_type(meet(fXZ)) # N: Revealed type is "TypedDict('__main__.X', {'x': builtins.int, 'y': builtins.int}, closed=True)" +reveal_type(meet(fZX)) # N: Revealed type is "TypedDict('__main__.X', {'x': builtins.int, 'y': builtins.int}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfClosedTypedDictsWithDifferentKeys] +from typing import TypedDict, TypeVar, Callable, ReadOnly +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +A = TypedDict('A', {'a': int}, closed=True) +B = TypedDict('B', {'b': int}, closed=True) +def fAB(x: A, y: B) -> None: pass +if int(): + reveal_type(meet(fAB)) # N: Revealed type is "Never" +C = TypedDict('C', {'c': int}, closed=True, total=False) +D = TypedDict('D', {'d': int}, closed=True, total=False) +def fCD(x: C, y: D) -> None: pass +if int(): + reveal_type(meet(fCD)) # N: Revealed type is "Never" +E = TypedDict('E', {'e': ReadOnly[int]}, closed=True) +F = TypedDict('F', {'f': ReadOnly[int]}, closed=True) +def fEF(x: E, y: F) -> None: pass +if int(): + reveal_type(meet(fEF)) # N: Revealed type is "Never" +G = TypedDict('G', {'g': ReadOnly[int]}, closed=True, total=False) +H = TypedDict('H', {'h': ReadOnly[int]}, closed=True, total=False) +def fAH(x: A, y: H) -> None: pass +if int(): + reveal_type(meet(fAH)) # N: Revealed type is "Never" +def fCH(x: C, y: H) -> None: pass +if int(): + reveal_type(meet(fCH)) # N: Revealed type is "Never" +def fEH(x: E, y: H) -> None: pass +if int(): + reveal_type(meet(fEH)) # N: Revealed type is "Never" +def fGH(x: G, y: H) -> None: pass +if int(): + reveal_type(meet(fGH)) # N: Revealed type is "TypedDict({}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsExplicitAndImplicitClosedMissing] +from typing import Any, NotRequired, ReadOnly, TypedDict, TypeVar, Callable +from typing_extensions import Never +A = TypedDict('A', {'x': ReadOnly[NotRequired[Never]]}, closed=True) +B = TypedDict('B', {'y': ReadOnly[NotRequired[Never]]}, closed=True) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: A, y: B) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictClosedMissingAndAny] +from typing import Any, TypedDict, TypeVar, Callable, ReadOnly +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +A = TypedDict('A', {}, closed=True) +B = TypedDict('B', {'x': Any}) +C = TypedDict('C', {'x': Any}, total=False) +def fAB(x: A, y: B) -> None: pass +def fBA(x: B, y: A) -> None: pass +def fAC(x: A, y: C) -> None: pass +def fCA(x: C, y: A) -> None: pass +if int(): + reveal_type(meet(fAB)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fBA)) # N: Revealed type is "Never" +reveal_type(meet(fAC)) # N: Revealed type is "TypedDict({}, closed=True)" +reveal_type(meet(fCA)) # N: Revealed type is "TypedDict({}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfTypedDictsClosedNotRequiredNeverAndAny] +from typing import Any, NotRequired, TypedDict, TypeVar, Callable +from typing_extensions import Never +A = TypedDict('A', {'x': NotRequired[Any], 'y': NotRequired[Never]}, closed=True) +B = TypedDict('B', {'x': NotRequired[Never], 'y': NotRequired[Any]}, closed=True) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: A, y: B) -> None: pass +reveal_type(f(g)) # N: Revealed type is "TypedDict({}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfOpenAndClosedTypedDictsExtraKeyInClosed] +from typing import TypedDict, TypeVar, Callable, ReadOnly +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +Ax = TypedDict('Ax', {'a': ReadOnly[float]}) +AyB = TypedDict('AyB', {'a': ReadOnly[int], 'b': int}, closed=True) +def fAxAyB(x: Ax, y: AyB) -> None: pass +def fAyBAx(x: AyB, y: Ax) -> None: pass +reveal_type(meet(fAxAyB)) # N: Revealed type is "TypedDict({'a'=: builtins.int, 'b': builtins.int}, closed=True)" +reveal_type(meet(fAyBAx)) # N: Revealed type is "TypedDict({'a'=: builtins.int, 'b': builtins.int}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testMeetOfOpenAndClosedTypedDictsExtraKeyInOpen] +from typing import TypedDict, TypeVar, Callable, ReadOnly +T = TypeVar('T') +def meet(x: Callable[[T, T], None]) -> T: pass +C = TypedDict('C', {'a': ReadOnly[float]}, closed=True) +D1 = TypedDict('D1', {'a': ReadOnly[int], 'b': int}) +D2 = TypedDict('D2', {'a': ReadOnly[int], 'b': ReadOnly[int]}) +D3 = TypedDict('D3', {'a': ReadOnly[int], 'b': int}, total=False) +D4 = TypedDict('D4', {'a': ReadOnly[int], 'b': ReadOnly[int]}, total=False) +def fCD1(x: C, y: D1) -> None: pass +def fD1C(x: D1, y: C) -> None: pass +def fCD2(x: C, y: D2) -> None: pass +def fD2C(x: D2, y: C) -> None: pass +def fCD3(x: C, y: D3) -> None: pass +def fD3C(x: D3, y: C) -> None: pass +def fCD4(x: C, y: D4) -> None: pass +def fD4C(x: D4, y: C) -> None: pass +if int(): + reveal_type(meet(fCD1)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fD1C)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fCD2)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fD2C)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fCD3)) # N: Revealed type is "Never" +if int(): + reveal_type(meet(fD3C)) # N: Revealed type is "Never" +reveal_type(meet(fCD4)) # N: Revealed type is "TypedDict({'a'=: builtins.int}, closed=True)" +reveal_type(meet(fD4C)) # N: Revealed type is "TypedDict({'a'=: builtins.int}, closed=True)" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictGetMethodClosed] +from typing import TypedDict, Literal +class Unrelated: pass +D = TypedDict('D', {'x': int, 'y': str}, closed=True) +d: D +u: Unrelated +x: Literal['x'] +y: Literal['y'] +z: Literal['z'] +x_or_y: Literal['x', 'y'] +x_or_z: Literal['x', 'z'] +x_or_y_or_z: Literal['x', 'y', 'z'] + +# test with literal expression +reveal_type(d.get('x')) # N: Revealed type is "builtins.int" +reveal_type(d.get('y')) # N: Revealed type is "builtins.str" +reveal_type(d.get('z')) # N: Revealed type is "None" +reveal_type(d.get('z', u)) # N: Revealed type is "__main__.Unrelated" + +# test with literal type / union of literal types with implicit default +reveal_type(d.get(x)) # N: Revealed type is "builtins.int" +reveal_type(d.get(y)) # N: Revealed type is "builtins.str" +reveal_type(d.get(z)) # N: Revealed type is "None" +reveal_type(d.get(x_or_y)) # N: Revealed type is "builtins.int | builtins.str" +reveal_type(d.get(x_or_z)) # N: Revealed type is "builtins.int | None" +reveal_type(d.get(x_or_y_or_z)) # N: Revealed type is "builtins.int | builtins.str | None" + +# test with literal type / union of literal types with explicit default +reveal_type(d.get(x, u)) # N: Revealed type is "builtins.int" +reveal_type(d.get(y, u)) # N: Revealed type is "builtins.str" +reveal_type(d.get(z, u)) # N: Revealed type is "__main__.Unrelated" +reveal_type(d.get(x_or_y, u)) # N: Revealed type is "builtins.int | builtins.str" +reveal_type(d.get(x_or_z, u)) # N: Revealed type is "builtins.int | __main__.Unrelated" +reveal_type(d.get(x_or_y_or_z, u)) # N: Revealed type is "builtins.int | builtins.str | __main__.Unrelated" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testOperatorContainsNarrowsTypedDicts_unionWithList_closed] +from __future__ import annotations +from typing import assert_type, TypedDict, Union + +class D(TypedDict, closed=True): + foo: int + +d_or_list: D | list[str] + +if 'foo' in d_or_list: + assert_type(d_or_list, Union[D, list[str]]) +elif 'bar' in d_or_list: + assert_type(d_or_list, list[str]) +else: + assert_type(d_or_list, list[str]) + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_total_closed] +from __future__ import annotations +from typing import assert_type, Literal, TypedDict, TypeVar, Union + +class D1(TypedDict, closed=True): + foo: int + +class D2(TypedDict, closed=True): + bar: int + +d: D1 | D2 + +if 'foo' in d: + assert_type(d, D1) +else: + assert_type(d, D2) + +foo_or_bar: Literal['foo', 'bar'] +if foo_or_bar in d: + assert_type(d, Union[D1, D2]) +else: + assert_type(d, Union[D1, D2]) + +foo_or_invalid: Literal['foo', 'invalid'] +if foo_or_invalid in d: + assert_type(d, D1) + # won't narrow 'foo_or_invalid' + assert_type(foo_or_invalid, Literal['foo', 'invalid']) +else: + assert_type(d, Union[D1, D2]) + # won't narrow 'foo_or_invalid' + assert_type(foo_or_invalid, Literal['foo', 'invalid']) + +TD = TypeVar('TD', D1, D2) + +def f(arg: TD) -> None: + value: int + if 'foo' in arg: + assert_type(arg['foo'], int) + else: + assert_type(arg['bar'], int) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_closed] +# flags: --warn-unreachable +from __future__ import annotations +from typing import assert_type, TypedDict, Union + +class DClosed(TypedDict, closed=True): + foo: int + +class DNotClosed(TypedDict): + bar: int + +d_not_closed: DNotClosed + +if 'bar' in d_not_closed: + assert_type(d_not_closed, DNotClosed) +else: + spam = 'ham' # E: Statement is unreachable + +if 'spam' in d_not_closed: + assert_type(d_not_closed, DNotClosed) +else: + assert_type(d_not_closed, DNotClosed) + +d_closed: DClosed + +if 'spam' in d_closed: + spam = 'ham' # E: Statement is unreachable +else: + assert_type(d_closed, DClosed) + +d_union: DClosed | DNotClosed + +if 'foo' in d_union: + assert_type(d_union, Union[DClosed, DNotClosed]) +else: + assert_type(d_union, DNotClosed) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse_closed] +from __future__ import annotations +from typing import assert_type, Literal, TypedDict, Union + +class DTotal(TypedDict, closed=True): + required_key: int + +class DNotTotal(TypedDict, total=False, closed=True): + optional_key: int + +d: DTotal | DNotTotal + +if 'required_key' in d: + assert_type(d, DTotal) +else: + assert_type(d, DNotTotal) + +if 'optional_key' in d: + assert_type(d, DNotTotal) +else: + assert_type(d, Union[DTotal, DNotTotal]) + +key: Literal['optional_key', 'required_key'] +if key in d: + assert_type(d, Union[DTotal, DNotTotal]) +else: + assert_type(d, Union[DTotal, DNotTotal]) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired_closed] +from __future__ import annotations +from typing import assert_type, TypedDict, Union +from typing_extensions import Required, NotRequired + +class D1(TypedDict, closed=True): + required_key: Required[int] + optional_key: NotRequired[int] + +class D2(TypedDict, closed=True): + abc: int + xyz: int + +d: D1 | D2 + +if 'required_key' in d: + assert_type(d, D1) +else: + assert_type(d, D2) + +if 'optional_key' in d: + assert_type(d, D1) +else: + assert_type(d, Union[D1, D2]) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testOperatorContainsNarrowsTypeVarWithClosedTypedDictBound] +from typing import TypedDict, TypeVar +from typing_extensions import Never, assert_type + +T = TypeVar('T', bound='D') + +class D(TypedDict, closed=True): + a: int + +def func(t: T): + if "b" in t: + assert_type(t, Never) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictUnpackFromClosedMissingKey] +# flags: --extra-checks +from typing import TypedDict +from typing_extensions import Never, NotRequired +D1 = TypedDict("D1", {"a": int, "b": str}, closed=True) +D2 = TypedDict("D2", {"a": int, "b": str, "c": int}) +D3 = TypedDict("D3", {"a": int, "b": str, "c": NotRequired[int]}) +d1: D1 +d2: D2 = {**d1} # E: Missing key "c" for TypedDict "D2" +d3: D3 = {**d1} +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictUnpackIntoClosed] +# flags: --extra-checks +from typing import Any, Mapping, TypedDict, Union +from typing_extensions import Never, NotRequired +D1 = TypedDict("D1", {"a": int, "b": str}, closed=True) +D2 = TypedDict("D2", {"a": int, "b": str}) +D3 = TypedDict("D3", {"c": int, "d": str}, closed=True) +D4 = TypedDict("D4", {"c": int, "d": str}) +D5 = TypedDict("D5", {"a": int, "b": str, "c": int, "d": str}, closed=True) +d1: D1 +d2: D2 +d3: D3 +d4: D4 +d5: D5 +m: Mapping[Any, Any] +u34: Union[D3, D4] +u35: Union[D3, D5] +u5m: Union[D5, Mapping[Any, Any]] +d5 = {**d1, **d3} +d5 = { + **d1, + **d4, # E: Cannot unpack item that may contain extra keys into a closed TypedDict +} +d5 = { + **d2, # E: Cannot unpack item that may contain extra keys into a closed TypedDict + **d3, +} +d5 = { + **d1, + **u34, # E: Cannot unpack item that may contain extra keys into a closed TypedDict +} +d5 = {**d1, **u35} +d5 = { + **m, # E: Cannot unpack item that may contain extra keys into a closed TypedDict + **d5, +} +d5 = { + **u5m, # E: Cannot unpack item that may contain extra keys into a closed TypedDict +} +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testTypedDictFinalAndClassVar] from typing import TypedDict, Final, ClassVar diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index 1f1987183fe46..c4bbf7d3f2283 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -676,6 +676,24 @@ p = Point(dict(x=42, y=1337)) __main__.Point __main__.p +[case testTypedDict5] +from typing import TypedDict +class Point(TypedDict): + x: int + y: int +p = Point(dict(x=42, y=1337)) +[file next.py] +from typing import TypedDict +class Point(TypedDict, closed=True): + x: int + y: int +p = Point(dict(x=42, y=1337)) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[out] +__main__.Point +__main__.p + [case testTypeAliasSimple] A = int B = int diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 3e21a77db3431..bc02f49e5d835 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -3745,6 +3745,83 @@ b.py:4: error: ReadOnly TypedDict key "y" TypedDict is mutated == b.py:3: error: ReadOnly TypedDict key "x" TypedDict is mutated +[case testTypedDictUpdateClosed] +import b +[file a.py] +from typing import TypedDict, Union +class A(TypedDict): + a: int +class B(TypedDict, closed=True): + b: int +C = Union[A, B] +[file a.py.2] +from typing import TypedDict, Union +class A(TypedDict, closed=True): + a: int +class B(TypedDict): + b: int +C = Union[A, B] +[file a.py.3] +from typing import TypedDict, Union +class A(TypedDict, closed=True): + a: int +class B(TypedDict, closed=True): + b: int +C = Union[A, B] +[file b.py] +from a import C +def foo(x: C) -> int: + if "b" in x: + return x["b"] + else: + return x["a"] +def bar(x: C) -> int: + if "a" in x: + return x["a"] + else: + return x["b"] +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[out] +b.py:4: error: TypedDict "A" has no key "b" +== +b.py:9: error: TypedDict "B" has no key "a" +== + +[case testTypedDictUpdateClosedPassingToFailing] +import b +[file a.py] +from typing import TypedDict, Union +class A(TypedDict, closed=True): + a: int +class B(TypedDict, closed=True): + b: int +C = Union[A, B] +[file a.py.2] +from typing import TypedDict, Union +class A(TypedDict): + a: int +class B(TypedDict, closed=True): + b: int +C = Union[A, B] +[file b.py] +from a import C +def foo(x: C) -> int: + if "b" in x: + return x["b"] + else: + return x["a"] +def bar(x: C) -> int: + if "a" in x: + return x["a"] + else: + return x["b"] +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[out] +== +b.py:4: error: TypedDict "A" has no key "b" + [case testBasicAliasUpdate] import b [file a.py] diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 161f14e8aea77..0c8b74ecf29a1 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -3822,16 +3822,26 @@ def f(x: str | None) -> None: ... [case testTypeddict] import typing, x -X = typing.TypedDict('X', {'a': int, 'b': str}) -Y = typing.TypedDict('X', {'a': int, 'b': str}, total=False) +D1 = typing.TypedDict('D1', {'a': int, 'b': str}) +D2 = typing.TypedDict('D2', {'a': int, 'b': str}, total=False) +D3 = typing.TypedDict('D3', {'a': int, 'b': str}, closed=True) +D4 = typing.TypedDict('D4', {'a': int, 'b': str}, closed=True, total=False) [out] from typing_extensions import TypedDict -class X(TypedDict): +class D1(TypedDict): a: int b: str -class Y(TypedDict, total=False): +class D2(TypedDict, total=False): + a: int + b: str + +class D3(TypedDict, closed=True): + a: int + b: str + +class D4(TypedDict, total=False, closed=True): a: int b: str From cbd8c82b7426322f51654bc54c6dad963f84999a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 2 Jun 2026 17:57:06 +0100 Subject: [PATCH 060/127] Fix docs build (#21580) It looks like https://github.com/python/cpython/commit/7d661587a2d76fd1eaa0349612d37a809560a88d broke intersphinx. This should fix it. --- docs/source/kinds_of_types.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/kinds_of_types.rst b/docs/source/kinds_of_types.rst index 26c2f002231e6..b736078a796cf 100644 --- a/docs/source/kinds_of_types.rst +++ b/docs/source/kinds_of_types.rst @@ -443,7 +443,7 @@ case you should add an explicit ``... | None`` annotation. The Python interpreter internally uses the name ``NoneType`` for the type of ``None``, but ``None`` is always used in type annotations. The latter is shorter and reads better. (``NoneType`` - is available as :py:data:`types.NoneType` on Python 3.10+, but is + is available as :py:class:`types.NoneType` on Python 3.10+, but is not exposed at all on earlier versions of Python.) .. note:: From 363be141eb198ab4526ae9d6dedb02b7aa462475 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 3 Jun 2026 11:22:13 +0100 Subject: [PATCH 061/127] Fix crash on invalid recursive variadic alias (#21572) Fixes https://github.com/python/mypy/issues/21125 This one was tricky. This is because the above issue actually exposed _two_ different crash scenarios: * A crash on invalid constructs like `*tuple[Ts]` (must be `*tuple[*Ts]`). * An infinite recursion when trying to detect pathological and divergent aliases. And while working on this I discovered two more cases: * A crash where an invalid type (like a union) appears in unpack in a recursive alias definition. * A crash on non-normalizeable recursive tuple. I fix the first by tightening logic in `typeanal.py` w.r.t. where exactly a `TypeVarTuple` is allowed. I fix the second and third by avoiding `get_proper_type()` calls in `expand_type()` for recursive tuples. The fourth is the most problematic, and is kind of a fundamental thing. This PR only avoids an immediate crash for such aliases. We will still need to update various call sites where we special-case tuples to expect non-normal ones. Couple more related things: * I fix couple issues with `is_recursive` cache invalidation. * I added a fast path to `detect_diverging_alias()` to avoid creating sets unless really needed. --- mypy/expandtype.py | 10 ++++---- mypy/semanal.py | 8 +++---- mypy/semanal_typeargs.py | 5 +++- mypy/server/astmerge.py | 2 ++ mypy/typeanal.py | 24 ++++++++++++++----- mypy/types.py | 11 ++++++--- test-data/unit/check-typevar-tuple.test | 32 +++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 18 deletions(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index b576d9f97d8e5..186429abd36a9 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -228,11 +228,11 @@ def visit_instance(self, t: Instance) -> Type: if t.type.fullname == "builtins.tuple": # Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...] arg = args[0] - if isinstance(arg, UnpackType): + if isinstance(arg, UnpackType) and not ( + isinstance(arg.type, TypeAliasType) and arg.type.is_recursive + ): unpacked = get_proper_type(arg.type) if isinstance(unpacked, Instance): - # TODO: this and similar asserts below may be unsafe because get_proper_type() - # may be called during semantic analysis before all invalid types are removed. assert unpacked.type.fullname == "builtins.tuple" args = list(unpacked.args) return t.copy_modified(args=args) @@ -536,7 +536,9 @@ def visit_tuple_type(self, t: TupleType) -> Type: if len(items) == 1: # Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...] item = items[0] - if isinstance(item, UnpackType): + if isinstance(item, UnpackType) and not ( + isinstance(item.type, TypeAliasType) and item.type.is_recursive + ): unpacked = get_proper_type(item.type) if isinstance(unpacked, Instance): # expand_type() may be called during semantic analysis, before invalid unpacks are fixed. diff --git a/mypy/semanal.py b/mypy/semanal.py index 58c152fa066e7..e010273b0781f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4263,6 +4263,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # An alias gets updated. updated = False if isinstance(existing.node, TypeAlias): + # Invalidate recursive status cache in case it was previously set. + existing.node._is_recursive = None if existing.node.target != res: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. @@ -4271,8 +4273,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: existing.node.alias_tvars = alias_tvars existing.node.no_args = no_args updated = True - # Invalidate recursive status cache in case it was previously set. - existing.node._is_recursive = None else: # Otherwise just replace existing placeholder with type alias *in place*. existing._node = alias_node @@ -5830,6 +5830,8 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: ): updated = False if isinstance(existing.node, TypeAlias): + # Invalidate recursive status cache in case it was previously set. + existing.node._is_recursive = None if ( existing.node.target != res or existing.node.alias_tvars != alias_node.alias_tvars @@ -5840,8 +5842,6 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: existing.node.default_depends = default_depends existing.node.alias_tvars = alias_tvars updated = True - # Invalidate recursive status cache in case it was previously set. - existing.node._is_recursive = None else: # Otherwise just replace existing placeholder with type alias *in place*. existing._node = alias_node diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 0f62a4aa8b1a2..5b6d9b16273ef 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -108,7 +108,10 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None: self.seen_aliases.discard(t) def visit_tuple_type(self, t: TupleType) -> None: - t.items = flatten_nested_tuples(t.items) + # Unfortunately, universal normalization of tuples is not possible in presence of + # recursive aliases, see testNoCrashOnNonNormalRecursiveTuple for an example. + # TODO: update the places where we handle tuples to always expect non-normal ones. + t.items = flatten_nested_tuples(t.items, handle_recursive=False) for i, it in enumerate(t.items): if self.check_non_paramspec(it, "tuple", t): t.items[i] = AnyType(TypeOfAny.from_error) diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 075bf7cb540bf..5b723711405a1 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -340,6 +340,8 @@ def visit_var(self, node: Var) -> None: super().visit_var(node) def visit_type_alias(self, node: TypeAlias) -> None: + # Updating alias target can invalidate its recursive status. + node._is_recursive = None self.fixup_type(node.target) for v in node.alias_tvars: self.fixup_type(v) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a303621d75ef2..aa5d14bddc65a 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -75,6 +75,7 @@ BoolTypeQuery, CallableArgument, CallableType, + CollectAliasesVisitor, DeletedType, EllipsisType, ErasedType, @@ -275,7 +276,9 @@ def __init__( self.prohibit_special_class_field_types = prohibit_special_class_field_types # Allow variables typed as Type[Any] and type (useful for base classes). self.allow_type_any = allow_type_any - self.allow_type_var_tuple = False + # Level of nesting at which a TypeVarTuple is allowed. Note we specify exact level + # to prohibit things like Unpack[list[Ts]], which are not supported. + self.allow_type_var_tuple = -1 self.allow_unpack = allow_unpack # Set when we are analyzing a default of a type variable. self.analyzing_tvar_def = analyzing_tvar_def @@ -453,7 +456,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) self.fail(msg, t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) assert isinstance(tvar_def, TypeVarTupleType) - if not self.allow_type_var_tuple: + if self.allow_type_var_tuple != self.nesting_level: self.fail( f'TypeVarTuple "{t.name}" is only valid with an unpack', t, @@ -808,9 +811,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ if not self.allow_unpack: self.fail(message_registry.INVALID_UNPACK_POSITION, t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) - self.allow_type_var_tuple = True + self.allow_type_var_tuple = self.nesting_level + 1 result = UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column) - self.allow_type_var_tuple = False + self.allow_type_var_tuple = -1 return result elif fullname in SELF_TYPE_NAMES: if t.args: @@ -1161,9 +1164,9 @@ def visit_unpack_type(self, t: UnpackType) -> Type: if not self.allow_unpack: self.fail(message_registry.INVALID_UNPACK_POSITION, t.type, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) - self.allow_type_var_tuple = True + self.allow_type_var_tuple = self.nesting_level + 1 result = UnpackType(self.anal_type(t.type), from_star_syntax=t.from_star_syntax) - self.allow_type_var_tuple = False + self.allow_type_var_tuple = -1 return result def visit_parameters(self, t: Parameters) -> Type: @@ -2523,6 +2526,15 @@ def detect_diverging_alias(node: TypeAlias, target: Type) -> bool: They may be handy in rare cases, e.g. to express a union of non-mixed nested lists: Nested = Union[T, Nested[List[T]]] ~> Union[T, List[T], List[List[T]], ...] """ + is_recursive = node._is_recursive + if is_recursive is None: + is_recursive = node in node.target.accept(CollectAliasesVisitor()) + if not is_recursive: + # Fast path: this is not a recursive alias at all. + return False + # Note we only cache positive case, caching negative case is risky, as this type alias + # (or more importantly any other alias it uses) may be not ready yet. + node._is_recursive = True visitor = DivergingAliasDetector({node}) _ = target.accept(visitor) return visitor.diverging diff --git a/mypy/types.py b/mypy/types.py index 129d98894770f..324135df014d3 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -4301,12 +4301,12 @@ def find_unpack_in_list(items: Sequence[Type]) -> int | None: # Funky code here avoids mypyc narrowing the type of unpack_index. old_index = unpack_index assert old_index is None - # Don't return so that we can also sanity check there is only one. + # Don't return so that we can also sanity-check there is only one. unpack_index = i return unpack_index -def flatten_nested_tuples(types: Iterable[Type]) -> list[Type]: +def flatten_nested_tuples(types: Iterable[Type], handle_recursive: bool = True) -> list[Type]: """Recursively flatten TupleTypes nested with Unpack. For example this will transform @@ -4320,7 +4320,12 @@ def flatten_nested_tuples(types: Iterable[Type]) -> list[Type]: res.append(typ) continue p_type = get_proper_type(typ.type) - if not isinstance(p_type, TupleType): + if ( + not isinstance(p_type, TupleType) + or not handle_recursive + and isinstance(typ.type, TypeAliasType) + and typ.type.is_recursive + ): res.append(typ) continue if isinstance(typ.type, TypeAliasType): diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 3f0765ba5c770..55f125d1fc036 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -882,7 +882,39 @@ z: C reveal_type(x) # N: Revealed type is "Any" reveal_type(y) # N: Revealed type is "Any" reveal_type(z) # N: Revealed type is "tuple[builtins.int, Unpack[builtins.tuple[Any, ...]]]" +[builtins fixtures/tuple.pyi] + +[case testBanPathologicalRecursiveTuplesGeneric] +from typing import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +A = tuple[Unpack[B[Unpack[Ts]]]] # E: Invalid recursive alias: a tuple item of itself \ + # E: Name "B" is used before definition +B = tuple[Unpack[A[Unpack[Ts]]]] +[builtins fixtures/tuple.pyi] + +[case testNoCrashOnInvalidRecursiveUnpackOfUnion] +from typing import Unpack + +A = tuple[int, str] | list[tuple[Unpack[A]]] # E: "tuple[int, str] | list[tuple[Unpack[A]]]" cannot be unpacked (must be tuple or TypeVarTuple) +[builtins fixtures/tuple.pyi] + +[case testNoCrashOnNonNormalRecursiveTuple] +from typing import Unpack + +A = tuple[int, list[tuple[str, Unpack[A]]]] +a: A +x, y = a +y[0] = 1 # E: Incompatible types in assignment (expression has type "int", target has type "tuple[str, Unpack[A]]") +[builtins fixtures/list.pyi] +[case testBanTypeVarTupleNotImmediatelyInsideUnpack] +from typing import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +A = tuple[Unpack[tuple[Ts]]] # E: TypeVarTuple "Ts" is only valid with an unpack +x: A[int, str] +reveal_type(x) # N: Revealed type is "tuple[Any]" [builtins fixtures/tuple.pyi] [case testInferenceAgainstGenericVariadicWithBadType] From bfa3b88ea1c80a4f1f860fad4ab344d8fc28c868 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 16:04:16 +0100 Subject: [PATCH 062/127] [mypyc] Report file and line number on uncaught exceptions (#21584) For example, this will help debug uncaught exceptions in the reference counting transform. --- mypyc/codegen/emitmodule.py | 47 ++++++++++++++++++++----------------- mypyc/crash.py | 4 ++++ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index fa0a4385f4fb5..e2cd0a829dd77 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -56,6 +56,7 @@ shared_lib_name, short_id_from_name, ) +from mypyc.crash import catch_errors from mypyc.errors import Errors from mypyc.ir.deps import ( LIBRT_BASE64, @@ -259,29 +260,31 @@ def compile_scc_to_ir( env_user_functions[cls.env_user_function] = cls for module in modules.values(): + module_path = result.graph[module.fullname].xpath for fn in module.functions: - # Insert checks for uninitialized values. - insert_uninit_checks(fn, compiler_options.strict_traceback_checks) - # Insert exception handling. - insert_exception_handling(fn, compiler_options.strict_traceback_checks) - # Insert reference count handling. - insert_ref_count_opcodes(fn) - - if fn in env_user_functions: - insert_spills(fn, env_user_functions[fn]) - - if compiler_options.log_trace: - insert_event_trace_logging(fn, compiler_options) - - # Switch to lower abstraction level IR. - lower_ir(fn, compiler_options) - # Calculate implicit module dependencies (needed for librt) - deps = find_implicit_op_dependencies(fn) - if deps is not None: - module.dependencies.update(deps) - # Perform optimizations. - do_copy_propagation(fn, compiler_options) - do_flag_elimination(fn, compiler_options) + with catch_errors(module_path, fn.line): + # Insert checks for uninitialized values. + insert_uninit_checks(fn, compiler_options.strict_traceback_checks) + # Insert exception handling. + insert_exception_handling(fn, compiler_options.strict_traceback_checks) + # Insert reference count handling. + insert_ref_count_opcodes(fn) + + if fn in env_user_functions: + insert_spills(fn, env_user_functions[fn]) + + if compiler_options.log_trace: + insert_event_trace_logging(fn, compiler_options) + + # Switch to lower abstraction level IR. + lower_ir(fn, compiler_options) + # Calculate implicit module dependencies (needed for librt) + deps = find_implicit_op_dependencies(fn) + if deps is not None: + module.dependencies.update(deps) + # Perform optimizations. + do_copy_propagation(fn, compiler_options) + do_flag_elimination(fn, compiler_options) # Calculate implicit dependencies from class attribute types for cl in module.classes: diff --git a/mypyc/crash.py b/mypyc/crash.py index 1227aa8978af2..498651a23b1e7 100644 --- a/mypyc/crash.py +++ b/mypyc/crash.py @@ -29,4 +29,8 @@ def crash_report(module_path: str, line: int) -> NoReturn: for s in traceback.format_list(tb + tb2): print(s.rstrip("\n")) print(f"{module_path}:{line}: {type(err).__name__}: {err}") + print( + f"{module_path}:{line}: note: this is an internal mypyc error; " + "please report a bug at https://github.com/mypyc/mypyc/issues" + ) raise SystemExit(2) From 52de0c739d2f066ea06fa9a886bc8fcc9fa079c6 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:32:08 +0300 Subject: [PATCH 063/127] [mypyc] Preserve inherited attribute defaults under separate=True (#21547) Fixes #21542 Under `separate=True`, when a subclass is recompiled while its parent is loaded from mypy's incremental cache, parent default-attribute assignments are silently dropped from the subclass's `__mypyc_defaults_setup`. The first read of an inherited default-attr then raises: ``` AttributeError: attribute '' of '' undefined ``` `find_attr_initializers` walks `cdef.info.mro` and reads `info.defn.defs.body` for `AssignmentStmt`s. `ClassDef.serialize` (mypy/nodes.py) does not serialize `defs`, so a cache-loaded parent has `defs = Block([])`; the MRO walk collects no parent assignments and the subclass's emitted setup leaves inherited slots in the undefined-sentinel state. This PR implements the fix discussed in the linked issue. --- mypyc/codegen/emitclass.py | 3 +- mypyc/common.py | 1 + mypyc/irbuild/classdef.py | 183 ++++++++++++++------------- mypyc/irbuild/prepare.py | 92 +++++++++++++- mypyc/irbuild/util.py | 49 +++++++ mypyc/test-data/irbuild-classes.test | 3 +- mypyc/test-data/run-multimodule.test | 83 ++++++++++++ 7 files changed, 320 insertions(+), 94 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index ea3e2ddd74fea..db94f1de9406e 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -29,6 +29,7 @@ BITMAP_BITS, BITMAP_TYPE, CPYFUNCTION_NAME, + MYPYC_DEFAULTS_SETUP, NATIVE_PREFIX, PREFIX, REG_PREFIX, @@ -285,7 +286,7 @@ def emit_line() -> None: # If the class has a method to initialize default attribute # values, we need to call it during initialization. - defaults_fn = cl.get_method("__mypyc_defaults_setup") + defaults_fn = cl.get_method(MYPYC_DEFAULTS_SETUP) # If there is a __init__ method, we'll use it in the native constructor. init_fn = cl.get_method("__init__") diff --git a/mypyc/common.py b/mypyc/common.py index 64fe8126087b8..382d640a84083 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -24,6 +24,7 @@ LAMBDA_NAME: Final = "__mypyc_lambda__" PROPSET_PREFIX: Final = "__mypyc_setter__" SELF_NAME: Final = "__mypyc_self__" +MYPYC_DEFAULTS_SETUP: Final = "__mypyc_defaults_setup" GENERATOR_ATTRIBUTE_PREFIX: Final = "__mypyc_generator_attribute__" CPYFUNCTION_NAME = "__cpyfunction__" diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 3f95cf82c89c8..5bc19c961010b 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -7,6 +7,7 @@ from typing import Final from mypy.nodes import ( + ARG_POS, EXCLUDED_ENUM_ATTRIBUTES, TYPE_VAR_TUPLE_KIND, AssignmentStmt, @@ -21,7 +22,6 @@ NameExpr, OverloadedFuncDef, PassStmt, - RefExpr, StrExpr, TempNode, TypeInfo, @@ -29,7 +29,7 @@ is_class_var, ) from mypy.types import Instance, UnboundType, get_proper_type -from mypyc.common import PROPSET_PREFIX +from mypyc.common import MYPYC_DEFAULTS_SETUP, PROPSET_PREFIX from mypyc.ir.class_ir import ClassIR, NonExtClassInfo from mypyc.ir.func_ir import FuncDecl, FuncSignature from mypyc.ir.ops import ( @@ -48,15 +48,7 @@ TupleSet, Value, ) -from mypyc.ir.rtypes import ( - RType, - bool_rprimitive, - dict_rprimitive, - is_none_rprimitive, - is_object_rprimitive, - is_optional_type, - object_rprimitive, -) +from mypyc.ir.rtypes import RType, bool_rprimitive, dict_rprimitive, object_rprimitive from mypyc.irbuild.builder import IRBuilder, create_type_params from mypyc.irbuild.function import ( gen_property_getter_ir, @@ -66,7 +58,13 @@ load_type, ) from mypyc.irbuild.prepare import GENERATOR_HELPER_NAME -from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator +from mypyc.irbuild.util import ( + dataclass_type, + default_attr_name, + get_func_def, + is_constant, + is_dataclass_decorator, +) from mypyc.primitives.dict_ops import dict_new_op, exact_dict_set_item_op from mypyc.primitives.generic_ops import ( iter_op, @@ -322,10 +320,6 @@ def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None: def class_body_obj(self) -> Value | None: return self.type_obj - def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool: - """Controls whether to skip generating a default for an attribute.""" - return False - def add_method(self, fdef: FuncDef) -> None: handle_ext_method(self.builder, self.cdef, fdef) @@ -348,11 +342,18 @@ def finalize(self, ir: ClassIR) -> None: # Call __init_subclass__ after class attributes have been set self.builder.call_c(py_init_subclass_op, [self.type_obj], self.cdef.line) - attrs_with_defaults, default_assignments = find_attr_initializers( - self.builder, self.cdef, self.skip_attr_default - ) - ir.attrs_with_defaults.update(attrs_with_defaults) - generate_attr_defaults_init(self.builder, self.cdef, default_assignments) + # Under separate compilation, prepare.py pre-registers the decl iff + # the class has its own default attribute assignments to emit, so we + # can skip the body walk entirely when it isn't present. Without + # separate compilation, find_attr_initializers walks the MRO so that + # inherited defaults are reflected in ir.attrs_with_defaults (relied + # on by the attribute-definedness analysis), so we always run it. + if not self.builder.options.separate or MYPYC_DEFAULTS_SETUP in ir.method_decls: + attrs_with_defaults, default_assignments = find_attr_initializers( + self.builder, self.cdef + ) + ir.attrs_with_defaults.update(attrs_with_defaults) + generate_attr_defaults_init(self.builder, self.cdef, default_assignments) create_ne_from_eq(self.builder, self.cdef) @@ -380,9 +381,6 @@ def create_non_ext_info(self) -> NonExtClassInfo: self.builder.add(LoadAddress(type_object_op.type, type_object_op.src, self.cdef.line)), ) - def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool: - return stmt.type is not None - def get_type_annotation(self, stmt: AssignmentStmt) -> TypeInfo | None: # We populate __annotations__ because dataclasses uses it to determine # which attributes to compute on. @@ -445,9 +443,6 @@ class AttrsClassBuilder(DataClassBuilder): add_annotations_to_dict = False - def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool: - return True - def get_type_annotation(self, stmt: AssignmentStmt) -> TypeInfo | None: if isinstance(stmt.rvalue, CallExpr): # find the type arg in `attr.ib(type=str)` @@ -741,58 +736,50 @@ def add_non_ext_class_attr( def find_attr_initializers( - builder: IRBuilder, cdef: ClassDef, skip: Callable[[str, AssignmentStmt], bool] | None = None + builder: IRBuilder, cdef: ClassDef ) -> tuple[set[str], list[tuple[AssignmentStmt, str]]]: """Find initializers of attributes in a class body. - If provided, the skip arg should be a callable which will return whether - to skip generating a default for an attribute. It will be passed the name of - the attribute and the corresponding AssignmentStmt. + Under separate compilation, only this class's own body is walked, and + generate_attr_defaults_init emits a runtime call to the parent's + __mypyc_defaults_setup so inherited defaults are produced by chaining, + not by inlining. Walking the MRO here would break under separate=True + with mypy's incremental cache: a base class loaded from the cache has + an empty ClassDef.defs.body (mypy/nodes.py::ClassDef.serialize doesn't + serialize the class body), so inherited assignments would be silently + dropped and the subclass's __mypyc_defaults_setup would leave inherited + slots in the "undefined" state at runtime. + + Without separate compilation, all modules are parsed in the same pass + and the MRO walk is safe; we keep the original inline-all behavior + there as an optimization (no chain call needed for instance creation). """ cls = builder.mapper.type_to_ir[cdef.info] if cls.builtin_base: return set(), [] - attrs_with_defaults = set() + cls_type = dataclass_type(cdef) + attrs_with_defaults: set[str] = set() + default_assignments: list[tuple[AssignmentStmt, str]] = [] - # Pull out all assignments in classes in the mro so we can initialize them # TODO: Support nested statements - default_assignments: list[tuple[AssignmentStmt, str]] = [] - for info in reversed(cdef.info.mro): - if info not in builder.mapper.type_to_ir: + if builder.options.separate: + infos: list[TypeInfo] = [cdef.info] + else: + infos = list(reversed(cdef.info.mro)) + + for info in infos: + info_ir = builder.mapper.type_to_ir.get(info) + if info_ir is None: continue for stmt in info.defn.defs.body: - if ( - isinstance(stmt, AssignmentStmt) - and isinstance(stmt.lvalues[0], NameExpr) - and not is_class_var(stmt.lvalues[0]) - and not isinstance(stmt.rvalue, TempNode) - ): - name = stmt.lvalues[0].name - if name == "__slots__": - continue - - if name == "__deletable__": - check_deletable_declaration(builder, cls, stmt.line) - continue - - if skip is not None and skip(name, stmt): - continue - - attr_type = cls.attr_type(name) - - # If the attribute is initialized to None and type isn't optional, - # doesn't initialize it to anything (special case for "# type:" comments). - if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == "builtins.None": - if ( - not is_optional_type(attr_type) - and not is_object_rprimitive(attr_type) - and not is_none_rprimitive(attr_type) - ): - continue - - attrs_with_defaults.add(name) - default_assignments.append((stmt, info.module_name)) + if not isinstance(stmt, AssignmentStmt): + continue + name = default_attr_name(stmt, info_ir, cls_type) + if name is None: + continue + attrs_with_defaults.add(name) + default_assignments.append((stmt, info.module_name)) return attrs_with_defaults, default_assignments @@ -800,15 +787,49 @@ def find_attr_initializers( def generate_attr_defaults_init( builder: IRBuilder, cdef: ClassDef, default_assignments: list[tuple[AssignmentStmt, str]] ) -> None: - """Generate an initialization method for default attr values (from class vars).""" - if not default_assignments: - return + """Generate an initialization method for default attr values (from class vars). + + Under separate compilation, the emitted __mypyc_defaults_setup chains to + the nearest ancestor that has the method (Python __init__ style), then + sets only this class's own defaults; inherited defaults are produced by + the chain at runtime. The ancestor lookup uses cls.mro[1:] and relies on + prepare.py having registered the FuncDecl on every class that needs one + before any IR build runs. IR build within a compilation group proceeds + in filename order, so this class may be IR-built before its base, and a + method_decls lookup that depended on the base having been IR-built first + would miss. Without separate compilation, find_attr_initializers has + already collected the full MRO's defaults into default_assignments, so + we inline them all as before. + """ cls = builder.mapper.type_to_ir[cdef.info] if cls.builtin_base: return - with builder.enter_method(cls, "__mypyc_defaults_setup", bool_rprimitive): + parent_with_defaults: ClassIR | None = None + if builder.options.separate: + for ancestor in cls.mro[1:]: + if MYPYC_DEFAULTS_SETUP in ancestor.method_decls: + parent_with_defaults = ancestor + break + + if not default_assignments and parent_with_defaults is None: + return + + with builder.enter_method(cls, MYPYC_DEFAULTS_SETUP, bool_rprimitive): self_var = builder.self() + + # Chain to parent's setup so inherited defaults run first; propagate + # its False return so a parent default that raised still aborts + # instance creation rather than being silently swallowed here. + if parent_with_defaults is not None: + decl = parent_with_defaults.method_decl(MYPYC_DEFAULTS_SETUP) + parent_ok = builder.builder.call(decl, [self_var], [ARG_POS], [None], cdef.line) + fail_block, continue_block = BasicBlock(), BasicBlock() + builder.add(Branch(parent_ok, continue_block, fail_block, Branch.BOOL)) + builder.activate_block(fail_block) + builder.add(Return(builder.false())) + builder.activate_block(continue_block) + for stmt, origin_module in default_assignments: lvalue = stmt.lvalues[0] assert isinstance(lvalue, NameExpr), lvalue @@ -833,26 +854,6 @@ def generate_attr_defaults_init( builder.add(Return(builder.true())) -def check_deletable_declaration(builder: IRBuilder, cl: ClassIR, line: int) -> None: - for attr in cl.deletable: - if attr not in cl.attributes: - if not cl.has_attr(attr): - builder.error(f'Attribute "{attr}" not defined', line) - continue - for base in cl.mro: - if attr in base.property_types: - builder.error(f'Cannot make property "{attr}" deletable', line) - break - else: - _, base = cl.attr_details(attr) - builder.error( - ('Attribute "{}" not defined in "{}" ' + '(defined in "{}")').format( - attr, cl.name, base.name - ), - line, - ) - - def create_ne_from_eq(builder: IRBuilder, cdef: ClassDef) -> None: """Create a "__ne__" method from a "__eq__" method (if only latter exists).""" cls = builder.mapper.type_to_ir[cdef.info] diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index f143ce1b44025..8b73b10bf8064 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -21,6 +21,7 @@ from mypy.nodes import ( ARG_STAR, ARG_STAR2, + AssignmentStmt, CallExpr, ClassDef, Decorator, @@ -39,7 +40,13 @@ from mypy.semanal import refers_to_fullname from mypy.traverser import TraverserVisitor from mypy.types import Instance, Type, get_proper_type -from mypyc.common import FAST_PREFIX, PROPSET_PREFIX, SELF_NAME, get_id_from_name +from mypyc.common import ( + FAST_PREFIX, + MYPYC_DEFAULTS_SETUP, + PROPSET_PREFIX, + SELF_NAME, + get_id_from_name, +) from mypyc.crash import catch_errors from mypyc.errors import Errors from mypyc.ir.class_ir import ClassIR @@ -55,6 +62,7 @@ from mypyc.ir.rtypes import ( RInstance, RType, + bool_rprimitive, dict_rprimitive, none_rprimitive, object_pointer_rprimitive, @@ -63,6 +71,8 @@ ) from mypyc.irbuild.mapper import Mapper from mypyc.irbuild.util import ( + dataclass_type, + default_attr_name, get_func_def, get_mypyc_attrs, is_dataclass, @@ -131,6 +141,24 @@ def build_type_map( if class_ir.is_ext_class: prepare_implicit_property_accessors(cdef.info, class_ir, module.fullname, mapper) + # Register __mypyc_defaults_setup FuncDecls on classes that have their own + # class-level default attribute assignments. Done here, before any IR build + # runs, so that the cross-class lookup in generate_attr_defaults_init is + # order-independent: IR build within a compilation group proceeds in + # filename order, so a subclass may be IR-built before its base. + for module, cdef in classes: + class_ir = mapper.type_to_ir[cdef.info] + if class_ir.is_ext_class and _has_own_default_attrs(cdef, class_ir): + _register_defaults_setup_decl(class_ir, module.fullname) + + # Validate __deletable__ declarations. Done here so the compiler exits + # early on invalid input before any IR is built. + for module, cdef in classes: + class_ir = mapper.type_to_ir[cdef.info] + if class_ir.is_ext_class: + with catch_errors(module.path, cdef.line): + _check_deletable_declarations(module.path, cdef, class_ir, errors) + # Collect all the functions also. We collect from the symbol table # so that we can easily pick out the right copy of a function that # is conditionally defined. This doesn't include nested functions! @@ -408,6 +436,68 @@ def validate_acyclic_class_bases( ) +def _has_own_default_attrs(cdef: ClassDef, ir: ClassIR) -> bool: + """Whether this class's own body has any default attribute assignment + that would be emitted into __mypyc_defaults_setup. + + Used during prepare to decide whether to register a + __mypyc_defaults_setup FuncDecl ahead of IR build. + """ + if ir.builtin_base or ir.is_trait: + return False + cls_type = dataclass_type(cdef) + return any( + default_attr_name(stmt, ir, cls_type) is not None + for stmt in cdef.info.defn.defs.body + if isinstance(stmt, AssignmentStmt) + ) + + +def _register_defaults_setup_decl(ir: ClassIR, module_name: str) -> None: + sig = FuncSignature([RuntimeArg(SELF_NAME, RInstance(ir))], bool_rprimitive) + ir.method_decls[MYPYC_DEFAULTS_SETUP] = FuncDecl( + MYPYC_DEFAULTS_SETUP, ir.name, module_name, sig + ) + + +def _check_deletable_declarations(path: str, cdef: ClassDef, ir: ClassIR, errors: Errors) -> None: + """Validate that attributes listed in __deletable__ refer to definable + attributes on the class. + + Runs in the prepare phase so we exit early on invalid programs before + any IR is built. + """ + if not ir.deletable: + return + line = next( + ( + stmt.line + for stmt in cdef.info.defn.defs.body + if isinstance(stmt, AssignmentStmt) + and isinstance(stmt.lvalues[0], NameExpr) + and stmt.lvalues[0].name == "__deletable__" + ), + cdef.line, + ) + for attr in ir.deletable: + if attr not in ir.attributes: + if not ir.has_attr(attr): + errors.error(f'Attribute "{attr}" not defined', path, line) + continue + for base in ir.mro: + if attr in base.property_types: + errors.error(f'Cannot make property "{attr}" deletable', path, line) + break + else: + _, base = ir.attr_details(attr) + errors.error( + f'Attribute "{attr}" not defined in "{ir.name}" ' + f'(defined in "{base.name}")', + path, + line, + ) + + def prepare_class_def( path: str, module_name: str, diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index 5eda51a1a5dea..a6f793ccdc1a1 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -12,6 +12,7 @@ ARG_POS, GDEF, ArgKind, + AssignmentStmt, BytesExpr, CallExpr, ClassDef, @@ -24,13 +25,17 @@ OverloadedFuncDef, RefExpr, StrExpr, + TempNode, TupleExpr, UnaryExpr, Var, + is_class_var, ) from mypy.semanal import refers_to_fullname from mypy.types import FINAL_DECORATOR_NAMES from mypyc.errors import Errors +from mypyc.ir.class_ir import ClassIR +from mypyc.ir.rtypes import is_none_rprimitive, is_object_rprimitive, is_optional_type MYPYC_ATTRS: Final[frozenset[MypycAttr]] = frozenset( ["native_class", "allow_interpreted_subclasses", "serializable", "free_list_len", "acyclic"] @@ -102,6 +107,50 @@ def dataclass_type(cdef: ClassDef) -> str | None: return None +def _defaults_skip(stmt: AssignmentStmt, cls_type: str | None) -> bool: + """Whether a class-level default assignment is skipped when emitting + __mypyc_defaults_setup, based on class type. + + - attr (auto_attribs=False): skip all (handled by attr.ib machinery). + - dataclasses / attr-auto: skip annotated assignments. + - regular extension class: skip nothing. + """ + if cls_type == "attr": + return True + if cls_type in ("dataclasses", "attr-auto"): + return stmt.type is not None + return False + + +def default_attr_name(stmt: AssignmentStmt, ir: ClassIR, cls_type: str | None) -> str | None: + """Return the attribute name if `stmt` is a class-level default assignment + that __mypyc_defaults_setup should emit; otherwise None. + + Single source of truth for the predicate used by both + mypyc.irbuild.classdef.find_attr_initializers (IR build) and + mypyc.irbuild.prepare._has_own_default_attrs (prepare-phase decl registration). + """ + lvalue = stmt.lvalues[0] + if not isinstance(lvalue, NameExpr) or is_class_var(lvalue): + return None + if isinstance(stmt.rvalue, TempNode): + return None + name = lvalue.name + if name in ("__slots__", "__deletable__") or name not in ir.attributes: + return None + if _defaults_skip(stmt, cls_type): + return None + if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == "builtins.None": + attr_type = ir.attributes[name] + if ( + not is_optional_type(attr_type) + and not is_object_rprimitive(attr_type) + and not is_none_rprimitive(attr_type) + ): + return None + return name + + def get_mypyc_attr_literal(e: Expression) -> Any: """Convert an expression from a mypyc_attr decorator to a value. diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index d13bd956e1259..a7fdc09399009 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1135,7 +1135,7 @@ class Ok2: __deletable__ = ['x'] x: int -[case testInvalidDeletableAttribute] +[case testDeleteNonDeletableAttribute] class NotDeletable: __deletable__ = ['x'] x: int @@ -1146,6 +1146,7 @@ def g(o: NotDeletable) -> None: del o.y # E: "y" cannot be deleted \ # N: Using "__deletable__ = ['']" in the class body enables "del obj." +[case testInvalidDeletableAttribute] class Base: x: int diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index ace1ab9fb1a12..e586a3cba22e9 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1813,3 +1813,86 @@ hello [out2] empty hello + +[case testIncrementalCrossModuleInheritedAttrDefaultsWithOverride] +# Regression: same shape as testIncrementalCrossModuleInheritedAttrDefaults, +# but the subclass adds an attribute of its own, so generate_attr_defaults_init +# emits a __mypyc_defaults_setup for it. Before the fix, the recompiled +# subclass walked the parent's ClassDef.defs.body to collect inherited +# defaults; when the parent was loaded from mypy's incremental cache that +# body was empty, so the inherited initialization was dropped and any +# access to an inherited attribute through compiled code raised +# "AttributeError: attribute '' of '' undefined". +import other_a + +def test() -> None: + c = other_a.Child() + # Inherited attributes must still be initialized after the subclass + # has been recompiled against a cache-loaded parent. + assert c.x == 1 + assert c.y == "hello" + # Own override is set by the subclass's own __mypyc_defaults_setup. + assert c.z is True + # Method defined on the parent reads an inherited attribute through + # the compiled path; this is what crashes pre-fix. + assert c.use() == 1 + +[file other_b.py] +class Parent: + x: int = 1 + y: str = "hello" + z: bool = False + + def use(self) -> int: + if self.x: + return 1 + return 0 + +[file other_a.py] +from other_b import Parent + +class Child(Parent): + z: bool = True + +[file other_a.py.2] +from other_b import Parent + +class Child(Parent): + z: bool = True + +def _force_recompile() -> int: + return 1 + +[file driver.py] +from native import test +test() + +[case testCrossModuleInheritedAttrDefaultsSameGroup] +# separate: [(["native.py"], "grp1"), (["other_a.py", "other_b.py"], "grp2")] +# Regression: with the subclass (other_a) and base (other_b) in the same +# compilation group, IR build runs alphabetically within the group, so +# the subclass is IR-built before the base. The decision to emit +# __mypyc_defaults_setup (and a chained call to the ancestor's) must be +# set up in the prepare phase, before any IR build runs; otherwise the +# subclass's lookup of the parent's setup decl misses and inherited +# defaults are lost on a fresh build. +import other_a + +def test() -> None: + c = other_a.Child() + assert c.x == 1 + assert c.z is True + +[file other_b.py] +class Parent: + x: int = 1 + +[file other_a.py] +from other_b import Parent + +class Child(Parent): + z: bool = True + +[file driver.py] +from native import test +test() From 84a20bdbf993d6eb1bdfa9d4e5d26d7096a241db Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 4 Jun 2026 14:50:49 +0300 Subject: [PATCH 064/127] [mypyc] Specialize `s[i] == 'x'` to a codepoint int compare (#21579) 7th PR of #21418 Lowers `s[i] == 'x'` (and the symmetric `==` / `!=` forms) down to a bounds-checked codepoint read + int compare, instead of `CPyStr_GetItem` + `CPyStr_EqualLiteral` which (may) allocate a 1-character `PyUnicode` per iteration. No annotations are required for this optimization. On microbenchmarks (1-compare-per-iter hot loop, ~2.5M-codepoint SQL-like string) the comparison is ~3.6x times faster.
Some follow up optimizations that might be worth it I can work on: - In operator e.g `s[i] in ('a', 'b', 'c')` --> Fuse to one check with N int comparisons - Comparison operators e.g `s[i] < 'x'` --> Need to expand the op set - `s[i] == s[j]` --> Need drop the literal-only guard --- mypyc/irbuild/expression.py | 65 +++++++++++++- mypyc/test-data/irbuild-str.test | 150 +++++++++++++++++++++++++++++++ mypyc/test-data/run-strings.test | 61 +++++++++++++ 3 files changed, 275 insertions(+), 1 deletion(-) diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index e8d22a051cc4d..f953dd3825ef2 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -93,9 +93,12 @@ is_list_rprimitive, is_none_rprimitive, is_object_rprimitive, + is_str_rprimitive, + is_tagged, is_tuple_rprimitive, object_rprimitive, set_rprimitive, + short_int_rprimitive, vec_api_by_item_type, ) from mypyc.irbuild.ast_helpers import is_borrow_friendly_expr, process_conditional @@ -119,6 +122,7 @@ apply_dunder_specialization, apply_function_specialization, apply_method_specialization, + translate_getitem_with_bounds_check, translate_object_new, translate_object_setattr, ) @@ -137,7 +141,12 @@ from mypyc.primitives.list_ops import list_append_op, list_extend_op, list_slice_op from mypyc.primitives.misc_ops import ellipsis_op, get_module_dict_op, new_slice_op, type_op from mypyc.primitives.set_ops import set_add_op, set_in_op, set_update_op -from mypyc.primitives.str_ops import str_slice_op +from mypyc.primitives.str_ops import ( + str_adjust_index_op, + str_get_item_unsafe_as_int_op, + str_range_check_op, + str_slice_op, +) from mypyc.primitives.tuple_ops import list_tuple_op, tuple_slice_op # Name and attribute references @@ -918,6 +927,16 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value: return result if len(e.operators) == 1: + # s[i] == 'x' / s[i] != 'x' (and the symmetric RHS) -> int compare of + # codepoints. Skips the per-iteration 1-char str allocation/lookup and + # generic str equality call. + if first_op in ("==", "!="): + result = try_specialize_str_index_compare( + builder, first_op, e.operands[0], e.operands[1], e.line + ) + if result is not None: + return result + # Special some common simple cases if first_op in ("is", "is not"): right_expr = e.operands[1] @@ -960,6 +979,50 @@ def go(i: int, prev: Value) -> Value: return go(0, builder.accept(e.operands[0])) +def try_specialize_str_index_compare( + builder: IRBuilder, op: str, lhs: Expression, rhs: Expression, line: int +) -> Value | None: + """Specialize `s[i] == 'x'` / `s[i] != 'x'` (and the symmetric form with + operands swapped) into an int compare of codepoints. + + Returns None if the pattern doesn't match: the indexed base must be str, + the index must be an integer, and the literal must be a 1-character str. + Multi-character or empty literals fall through to the generic str compare + (which still returns False for them, matching today's behavior). + """ + # Normalize so the IndexExpr is on the left. + if isinstance(rhs, IndexExpr) and not isinstance(lhs, IndexExpr): + tmp = lhs + lhs, rhs = rhs, tmp + # Shape: s[i] {==, !=} "x" where "x" is exactly one codepoint. + if ( + not isinstance(lhs, IndexExpr) + or not isinstance(rhs, StrExpr) + or len(rhs.value) != 1 + or not is_str_rprimitive(builder.node_type(lhs.base)) + ): + return None + index_type = builder.node_type(lhs.index) + if not (is_tagged(index_type) or is_fixed_width_rtype(index_type)): + return None + + # ord(s[i]) with bounds check; raises IndexError for out-of-range indices, + # matching the behavior of the generic s[i] path. + codepoint = translate_getitem_with_bounds_check( + builder, + lhs.base, + [lhs.index], + lhs, + str_adjust_index_op, + str_range_check_op, + str_get_item_unsafe_as_int_op, + ) + if codepoint is None: + return None + literal_cp = Integer(ord(rhs.value), short_int_rprimitive, line) + return builder.binary_op(codepoint, literal_cp, op, line) + + def try_specialize_in_expr( builder: IRBuilder, op: str, lhs: Expression, rhs: Expression, line: int ) -> Value | None: diff --git a/mypyc/test-data/irbuild-str.test b/mypyc/test-data/irbuild-str.test index 81cd5bd34c046..b16057f645ba7 100644 --- a/mypyc/test-data/irbuild-str.test +++ b/mypyc/test-data/irbuild-str.test @@ -1025,3 +1025,153 @@ def is_digit(x): L0: r0 = CPyStr_IsDigit(x) return r0 + +[case testStrIndexEqLiteral_64bit] +def is_comma(s: str, i: int) -> bool: + return s[i] == "," +def is_comma_swapped(s: str, i: int) -> bool: + return "," == s[i] +def is_comma_ne(s: str, i: int) -> bool: + return s[i] != "," +[out] +def is_comma(s, i): + s :: str + i :: int + r0 :: native_int + r1 :: bit + r2, r3 :: i64 + r4 :: ptr + r5 :: c_ptr + r6, r7 :: i64 + r8, r9 :: bool + r10 :: short_int + r11 :: bit +L0: + r0 = i & 1 + r1 = r0 == 0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = i >> 1 + r3 = r2 + goto L3 +L2: + r4 = i ^ 1 + r5 = r4 + r6 = CPyLong_AsInt64(r5) + r3 = r6 + keep_alive i +L3: + r7 = CPyStr_AdjustIndex(s, r3) + r8 = CPyStr_RangeCheck(s, r7) + if r8 goto L5 else goto L4 :: bool +L4: + r9 = raise IndexError('index out of range') + unreachable +L5: + r10 = CPyStr_GetItemUnsafeAsInt(s, r7) + r11 = int_eq r10, 88 + return r11 +def is_comma_swapped(s, i): + s :: str + i :: int + r0 :: native_int + r1 :: bit + r2, r3 :: i64 + r4 :: ptr + r5 :: c_ptr + r6, r7 :: i64 + r8, r9 :: bool + r10 :: short_int + r11 :: bit +L0: + r0 = i & 1 + r1 = r0 == 0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = i >> 1 + r3 = r2 + goto L3 +L2: + r4 = i ^ 1 + r5 = r4 + r6 = CPyLong_AsInt64(r5) + r3 = r6 + keep_alive i +L3: + r7 = CPyStr_AdjustIndex(s, r3) + r8 = CPyStr_RangeCheck(s, r7) + if r8 goto L5 else goto L4 :: bool +L4: + r9 = raise IndexError('index out of range') + unreachable +L5: + r10 = CPyStr_GetItemUnsafeAsInt(s, r7) + r11 = int_eq r10, 88 + return r11 +def is_comma_ne(s, i): + s :: str + i :: int + r0 :: native_int + r1 :: bit + r2, r3 :: i64 + r4 :: ptr + r5 :: c_ptr + r6, r7 :: i64 + r8, r9 :: bool + r10 :: short_int + r11 :: bit +L0: + r0 = i & 1 + r1 = r0 == 0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = i >> 1 + r3 = r2 + goto L3 +L2: + r4 = i ^ 1 + r5 = r4 + r6 = CPyLong_AsInt64(r5) + r3 = r6 + keep_alive i +L3: + r7 = CPyStr_AdjustIndex(s, r3) + r8 = CPyStr_RangeCheck(s, r7) + if r8 goto L5 else goto L4 :: bool +L4: + r9 = raise IndexError('index out of range') + unreachable +L5: + r10 = CPyStr_GetItemUnsafeAsInt(s, r7) + r11 = int_ne r10, 88 + return r11 + +[case testStrIndexEqLiteralNoSpecialize] +def two_char_literal(s: str, i: int) -> bool: + # Multi-char literals don't match the specialization; falls through to + # the generic str equality path. + return s[i] == "ab" +def empty_literal(s: str, i: int) -> bool: + # Empty string literals also fall through; the generic path returns False. + return s[i] == "" +[out] +def two_char_literal(s, i): + s :: str + i :: int + r0, r1 :: str + r2 :: bool +L0: + r0 = CPyStr_GetItem(s, i) + r1 = 'ab' + r2 = CPyStr_EqualLiteral(r0, r1, 2) + return r2 +def empty_literal(s, i): + s :: str + i :: int + r0, r1 :: str + r2 :: bool +L0: + r0 = CPyStr_GetItem(s, i) + r1 = '' + r2 = CPyStr_EqualLiteral(r0, r1, 0) + return r2 diff --git a/mypyc/test-data/run-strings.test b/mypyc/test-data/run-strings.test index ec662da969865..81b85580d7a7b 100644 --- a/mypyc/test-data/run-strings.test +++ b/mypyc/test-data/run-strings.test @@ -1412,3 +1412,64 @@ def test_isdigit_strings() -> None: assert not "\u00e9\u00e8".isdigit() assert not "123\u00e9".isdigit() assert not "\U0001d7ce!".isdigit() + +[case testStrIndexEqLiteralSpecialize] +from testutil import assertRaises + +# The specializer fires on the AST shape `IndexExpr == StrLiteral` (or the +# symmetric swap, and `!=`). The literal has to be a real source-level +# string literal (can't be passed in as a parameter), so each test +# function pins one distinct shape. + +def eq_comma(s: str, i: int) -> bool: + # Specialized: s[i] == "x". + return s[i] == "," + +def ne_comma(s: str, i: int) -> bool: + # Specialized: s[i] != "x". + return s[i] != "," + +def comma_eq(s: str, i: int) -> bool: + # Specialized: "x" == s[i]. Operand-swap is normalized. + return "," == s[i] + +def eq_two_chars(s: str, i: int) -> bool: + # Not specialized: literal isn't 1 char. Falls through to the generic + # str compare, which returns False since s[i] is always 1 codepoint. + return s[i] == "ab" + +def eq_empty(s: str, i: int) -> bool: + # Not specialized: empty literal. Same fall-through. + return s[i] == "" + +def test_specialized_path() -> None: + s = "a,b" # comma at index 1 + assert eq_comma(s, 1) + assert not eq_comma(s, 0) + assert not eq_comma(s, 2) + # != inverts. + assert ne_comma(s, 0) + assert not ne_comma(s, 1) + # Literal on the LHS is normalized to the same shape. + assert comma_eq(s, 1) + assert not comma_eq(s, 0) + +def test_negative_index_is_adjusted() -> None: + s = "a,b" + assert eq_comma(s, -2) # -2 -> 1 (',') + assert not eq_comma(s, -1) # -1 -> 2 ('b') + +def test_non_1char_literal_falls_through() -> None: + s = "a,b" + # Generic str compare answers False because s[i] has length 1. + assert not eq_two_chars(s, 0) + assert not eq_two_chars(s, 1) + assert not eq_empty(s, 0) + +def test_out_of_range_raises_indexerror() -> None: + # Bounds-check semantics match the unspecialized s[i] path. + s = "a,b" + with assertRaises(IndexError): + eq_comma(s, 3) + with assertRaises(IndexError): + eq_comma(s, -4) From fad733ff78852f1aa435485fead63ca7506849f8 Mon Sep 17 00:00:00 2001 From: Kevin Kannammalil Date: Thu, 4 Jun 2026 08:49:38 -0400 Subject: [PATCH 065/127] Add Python version checks to native parser (#21539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native parser (rust based) previously accepted newer Python syntax without checking the configured target Python version, whereas the legacy parser (fastparse) would report when a feature isn't available on the target (although those would be blocking errors). This PR adds those compatibility checks to the native parser so that it produces a non blocking error. The feature gates added are: - improved type parameter syntax for Python 3.12 - type parameter defaults for Python 3.13 - type alias statements for Python 3.12 - exception groups (except*) for Python 3.11 - star unpack for Python 3.11 - t-strings for Python 3.14 I've also moved the tests that would now introduce a python version check error to it's own test file so that it gets the correct Python versoin In a later PR, I will be adding support for checking this feature since it requires modifying the rust parser a bit: - Parentheses optional for multiple exception types in except --- mypy/nativeparse.py | 54 +++++- mypy/parse.py | 2 +- mypy/test/test_nativeparse.py | 8 +- test-data/unit/check-typevar-tuple.test | 4 + test-data/unit/check-varargs.test | 1 + test-data/unit/native-parser-python311.test | 108 +++++++++++ test-data/unit/native-parser-python312.test | 90 +++++++++ test-data/unit/native-parser.test | 201 -------------------- test-data/unit/pythoneval.test | 30 +++ 9 files changed, 294 insertions(+), 204 deletions(-) create mode 100644 test-data/unit/native-parser-python311.test create mode 100644 test-data/unit/native-parser-python312.test diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index f371746cab8b8..c7a97862b461e 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -167,8 +167,9 @@ class State: - def __init__(self, options: Options) -> None: + def __init__(self, options: Options, is_stub: bool = False) -> None: self.options = options + self.is_stub = is_stub self.errors: list[ParseError] = [] self.num_funcs = 0 @@ -180,6 +181,29 @@ def add_error( {"line": line, "column": column, "message": message, "blocker": blocker, "code": code} ) + def check_min_version( + self, + feature: str, + min_version: tuple[int, int], + line: int, + column: int, + *, + enforce_in_stubs: bool = False, + ) -> None: + """Report a non blocker syntax error if the target Python feature is older than min_version.""" + if self.is_stub and not enforce_in_stubs: + return + if self.options.python_version < min_version: + curr = self.options.python_version + self.add_error( + f"{feature}: requires Python {min_version[0]}.{min_version[1]} or newer " + f"(current target: Python {curr[0]}.{curr[1]})", + line, + column, + blocker=False, + code="syntax", + ) + def native_parse( filename: str, @@ -607,6 +631,13 @@ def read_parameters(state: State, data: ReadBuffer) -> tuple[list[Argument], boo return arguments, has_ann +def check_type_param_defaults( + state: State, type_params: list[TypeParam], line: int, column: int +) -> None: + if any(p.default is not None for p in type_params): + state.check_min_version("Type parameter defaults", (3, 13), line, column) + + def read_type_params(state: State, data: ReadBuffer) -> list[TypeParam]: """Read type parameters (PEP 695 generics).""" type_params: list[TypeParam] = [] @@ -680,6 +711,11 @@ def read_func_def(state: State, data: ReadBuffer) -> FuncDef: if is_async: func_def.is_coroutine = True read_loc(data, func_def) + if type_params: + state.check_min_version( + "Improved type parameter syntax", (3, 12), func_def.line, func_def.column + ) + check_type_param_defaults(state, type_params, func_def.line, func_def.column) if typ: typ.line = func_def.line typ.column = func_def.column @@ -727,6 +763,11 @@ def read_class_def(state: State, data: ReadBuffer) -> ClassDef: ) class_def.decorators = decorators read_loc(data, class_def) + if type_params: + state.check_min_version( + "Improved type parameter syntax", (3, 12), class_def.line, class_def.column + ) + check_type_param_defaults(state, type_params, class_def.line, class_def.column) expect_end_tag(data) return class_def @@ -781,6 +822,8 @@ def read_type_alias_stmt(state: State, data: ReadBuffer) -> TypeAliasStmt: stmt = TypeAliasStmt(name, type_params, lambda_expr) read_loc(data, stmt) + state.check_min_version('"type" statements', (3, 12), stmt.line, stmt.column) + check_type_param_defaults(state, type_params, stmt.line, stmt.column) expect_end_tag(data) return stmt @@ -832,6 +875,10 @@ def read_try_stmt(state: State, data: ReadBuffer) -> TryStmt: stmt = TryStmt(body, vars_list, types_list, handlers, else_body, finally_body) stmt.is_star = is_star read_loc(data, stmt) + if is_star: + state.check_min_version("Exception groups", (3, 11), stmt.line, stmt.column) + if state.options.python_version < (3, 11): + stmt.is_star = False expect_end_tag(data) return stmt @@ -967,6 +1014,8 @@ def read_type(state: State, data: ReadBuffer) -> Type: from_star_syntax = read_bool(data) unpack = UnpackType(inner_type, from_star_syntax=from_star_syntax) read_loc(data, unpack) + if from_star_syntax: + state.check_min_version("Star unpack syntax", (3, 11), unpack.line, unpack.column) expect_end_tag(data) return unpack elif tag == types.CALL_TYPE: @@ -1474,6 +1523,9 @@ def read_expression(state: State, data: ReadBuffer) -> Expression: titems.append(s) expr = TemplateStrExpr(titems) read_loc(data, expr) + state.check_min_version( + "t-strings", (3, 14), expr.line, expr.column, enforce_in_stubs=True + ) expect_end_tag(data) return expr elif tag == nodes.LAMBDA_EXPR: diff --git a/mypy/parse.py b/mypy/parse.py index a8fb5542a7049..47e2f95f0f307 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -71,7 +71,7 @@ def load_from_raw( """ from mypy.nativeparse import State, deserialize_imports, read_statements - state = State(options) + state = State(options, is_stub=fnam.endswith(".pyi")) if imports_only: defs = [] else: diff --git a/mypy/test/test_nativeparse.py b/mypy/test/test_nativeparse.py index e0a0da29166b9..1e63f4abc2991 100644 --- a/mypy/test/test_nativeparse.py +++ b/mypy/test/test_nativeparse.py @@ -52,7 +52,11 @@ class NativeParserSuite(DataSuite): required_out_section = True base_path = "." - files = ["native-parser.test"] if has_nativeparse else [] + files = ( + ["native-parser.test", "native-parser-python311.test", "native-parser-python312.test"] + if has_nativeparse + else [] + ) def run_case(self, testcase: DataDrivenTestCase) -> None: test_parser(testcase) @@ -77,6 +81,8 @@ def test_parser(testcase: DataDrivenTestCase) -> None: if testcase.file.endswith("python310.test"): options.python_version = (3, 10) + elif testcase.file.endswith("python311.test"): + options.python_version = (3, 11) elif testcase.file.endswith("python312.test"): options.python_version = (3, 12) elif testcase.file.endswith("python313.test"): diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 55f125d1fc036..7db0fa6f4388c 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -578,6 +578,7 @@ reveal_type(b) # N: Revealed type is "def (*Any) -> builtins.int" [builtins fixtures/tuple.pyi] [case testTypeVarTuplePep646CallableNewSyntax] +# flags: --python-version 3.11 from typing import Callable, Generic, Tuple from typing_extensions import ParamSpec @@ -2187,6 +2188,7 @@ def f(x: int | None): [builtins fixtures/tuple.pyi] [case testJoinOfVariadicTupleCallablesNoCrash] +# flags: --python-version 3.11 from typing import Callable, Tuple f: Callable[[int, *Tuple[str, ...], int], None] @@ -2640,6 +2642,7 @@ def test(xs: tuple[Unpack[Ts]], xsi: tuple[int, Unpack[Ts]]) -> None: [builtins fixtures/tuple.pyi] [case testTypeVarTupleInferAgainstAnyCallableSuffix] +# flags: --python-version 3.11 from typing import Any, Callable, TypeVar, TypeVarTuple Ts = TypeVarTuple("Ts") @@ -2652,6 +2655,7 @@ reveal_type(deco(untyped)) # N: Revealed type is "def (*Any) -> Any" [builtins fixtures/tuple.pyi] [case testNoCrashOnNonNormalUnpackInCallable] +# flags: --python-version 3.11 from typing import Callable, Unpack, TypeVar T = TypeVar("T") diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 172e57cf1a4b3..4de3d649c0686 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -1116,6 +1116,7 @@ class D: [builtins fixtures/dict.pyi] [case testUnpackInCallableType] +# flags: --python-version 3.11 from typing import Callable, TypedDict from typing_extensions import Unpack diff --git a/test-data/unit/native-parser-python311.test b/test-data/unit/native-parser-python311.test new file mode 100644 index 0000000000000..9a314715133a0 --- /dev/null +++ b/test-data/unit/native-parser-python311.test @@ -0,0 +1,108 @@ +[case testTryExceptStar] +try: + x +except* ValueError: + y +[out] +MypyFile:1( + TryStmt:1( + Block:2( + ExpressionStmt:2( + NameExpr(x))) + * + NameExpr(ValueError) + Block:4( + ExpressionStmt:4( + NameExpr(y))))) + +[case testTryExceptStarWithVar] +try: + x +except* Exception as e: + y +[out] +MypyFile:1( + TryStmt:1( + Block:2( + ExpressionStmt:2( + NameExpr(x))) + * + NameExpr(Exception) + NameExpr(e) + Block:4( + ExpressionStmt:4( + NameExpr(y))))) + +[case testTryMultipleExceptStar] +try: + x +except* ValueError: + y +except* KeyError as e: + z +[out] +MypyFile:1( + TryStmt:1( + Block:2( + ExpressionStmt:2( + NameExpr(x))) + * + NameExpr(ValueError) + Block:4( + ExpressionStmt:4( + NameExpr(y))) + NameExpr(KeyError) + NameExpr(e) + Block:6( + ExpressionStmt:6( + NameExpr(z))))) + +[case testTryExceptStarElseFinally] +try: + x +except* ValueError: + y +else: + z +finally: + w +[out] +MypyFile:1( + TryStmt:1( + Block:2( + ExpressionStmt:2( + NameExpr(x))) + * + NameExpr(ValueError) + Block:4( + ExpressionStmt:4( + NameExpr(y))) + Else( + ExpressionStmt:6( + NameExpr(z))) + Finally( + ExpressionStmt:8( + NameExpr(w))))) + +[case testUnpackTypeInSignature] +def f(*args: *Ts) -> None: + pass +[out] +MypyFile:1( + FuncDef:1( + f + def (*args: *Ts?) -> None? + VarArg( + Var(args)) + Block:2( + PassStmt:2()))) + +[case testUnpackTypeInTuple] +x: tuple[int, *Ts, str] +[out] +MypyFile:1( + AssignmentStmt:1( + NameExpr(x) + TempNode:1( + Any) + tuple?[int?, *Ts?, str?])) diff --git a/test-data/unit/native-parser-python312.test b/test-data/unit/native-parser-python312.test new file mode 100644 index 0000000000000..2b1f9b42e0f78 --- /dev/null +++ b/test-data/unit/native-parser-python312.test @@ -0,0 +1,90 @@ +[case testPEP695TypeAlias] +# comment +type A[T] = C[T] +[out] +MypyFile:1( + TypeAliasStmt:2( + NameExpr(A) + TypeParam( + T) + LambdaExpr:2( + Block:-1( + ReturnStmt:2( + IndexExpr:2( + NameExpr(C) + NameExpr(T))))))) + +[case testPEP695GenericFunction] +# comment + +def f[T](): pass +def g[T: str](): pass +def h[T: (int, str)](): pass +[out] +MypyFile:1( + FuncDef:3( + f + TypeParam( + T) + Block:3( + PassStmt:3())) + FuncDef:4( + g + TypeParam( + T + str?) + Block:4( + PassStmt:4())) + FuncDef:5( + h + TypeParam( + T + Values( + int? + str?)) + Block:5( + PassStmt:5()))) + +[case testPEP695ParamSpec] +# comment + +def f[**P](): pass +class C[T: int, **P]: pass +[out] +MypyFile:1( + FuncDef:3( + f + TypeParam( + **P) + Block:3( + PassStmt:3())) + ClassDef:4( + C + TypeParam( + T + int?) + TypeParam( + **P) + PassStmt:4())) + +[case testPEP695TypeVarTuple] +# comment + +def f[*Ts](): pass +class C[T: int, *Ts]: pass +[out] +MypyFile:1( + FuncDef:3( + f + TypeParam( + *Ts) + Block:3( + PassStmt:3())) + ClassDef:4( + C + TypeParam( + T + int?) + TypeParam( + *Ts) + PassStmt:4())) diff --git a/test-data/unit/native-parser.test b/test-data/unit/native-parser.test index 8962875a9cf27..abf653fc213c1 100644 --- a/test-data/unit/native-parser.test +++ b/test-data/unit/native-parser.test @@ -1294,92 +1294,6 @@ MypyFile:1( ExpressionStmt:6( NameExpr(z))))) -[case testTryExceptStar] -try: - x -except* ValueError: - y -[out] -MypyFile:1( - TryStmt:1( - Block:2( - ExpressionStmt:2( - NameExpr(x))) - * - NameExpr(ValueError) - Block:4( - ExpressionStmt:4( - NameExpr(y))))) - -[case testTryExceptStarWithVar] -try: - x -except* Exception as e: - y -[out] -MypyFile:1( - TryStmt:1( - Block:2( - ExpressionStmt:2( - NameExpr(x))) - * - NameExpr(Exception) - NameExpr(e) - Block:4( - ExpressionStmt:4( - NameExpr(y))))) - -[case testTryMultipleExceptStar] -try: - x -except* ValueError: - y -except* KeyError as e: - z -[out] -MypyFile:1( - TryStmt:1( - Block:2( - ExpressionStmt:2( - NameExpr(x))) - * - NameExpr(ValueError) - Block:4( - ExpressionStmt:4( - NameExpr(y))) - NameExpr(KeyError) - NameExpr(e) - Block:6( - ExpressionStmt:6( - NameExpr(z))))) - -[case testTryExceptStarElseFinally] -try: - x -except* ValueError: - y -else: - z -finally: - w -[out] -MypyFile:1( - TryStmt:1( - Block:2( - ExpressionStmt:2( - NameExpr(x))) - * - NameExpr(ValueError) - Block:4( - ExpressionStmt:4( - NameExpr(y))) - Else( - ExpressionStmt:6( - NameExpr(z))) - Finally( - ExpressionStmt:8( - NameExpr(w))))) - [case testConditionalExprSimple] x if y else z [out] @@ -2018,30 +1932,6 @@ MypyFile:1( y)) PassStmt:5())) -[case testUnpackTypeInSignature] -def f(*args: *Ts) -> None: - pass -[out] -MypyFile:1( - FuncDef:1( - f - def (*args: *Ts?) -> None? - VarArg( - Var(args)) - Block:2( - PassStmt:2()))) - -[case testUnpackTypeInTuple] -x: tuple[int, *Ts, str] -[out] -MypyFile:1( - AssignmentStmt:1( - NameExpr(x) - TempNode:1( - Any) - tuple?[int?, *Ts?, str?])) - - [case testLiteralStringType] from typing import Literal x: Literal["hello"] @@ -3186,97 +3076,6 @@ MypyFile:1( Body( PassStmt:3()))) -[case testPEP695TypeAlias] -# comment -type A[T] = C[T] -[out] -MypyFile:1( - TypeAliasStmt:2( - NameExpr(A) - TypeParam( - T) - LambdaExpr:2( - Block:-1( - ReturnStmt:2( - IndexExpr:2( - NameExpr(C) - NameExpr(T))))))) - -[case testPEP695GenericFunction] -# comment - -def f[T](): pass -def g[T: str](): pass -def h[T: (int, str)](): pass -[out] -MypyFile:1( - FuncDef:3( - f - TypeParam( - T) - Block:3( - PassStmt:3())) - FuncDef:4( - g - TypeParam( - T - str?) - Block:4( - PassStmt:4())) - FuncDef:5( - h - TypeParam( - T - Values( - int? - str?)) - Block:5( - PassStmt:5()))) - -[case testPEP695ParamSpec] -# comment - -def f[**P](): pass -class C[T: int, **P]: pass -[out] -MypyFile:1( - FuncDef:3( - f - TypeParam( - **P) - Block:3( - PassStmt:3())) - ClassDef:4( - C - TypeParam( - T - int?) - TypeParam( - **P) - PassStmt:4())) - -[case testPEP695TypeVarTuple] -# comment - -def f[*Ts](): pass -class C[T: int, *Ts]: pass -[out] -MypyFile:1( - FuncDef:3( - f - TypeParam( - *Ts) - Block:3( - PassStmt:3())) - ClassDef:4( - C - TypeParam( - T - int?) - TypeParam( - *Ts) - PassStmt:4())) - [case testTypeCommentLastLine] d = [ # Placeholder for items diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 659a8515455d3..399003f02c7fb 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -2268,3 +2268,33 @@ _testNarrowingMappingAndAbstractSet.py:33: note: Revealed type is "dict[str, int _testNarrowingMappingAndAbstractSet.py:34: note: Revealed type is "set[str]" _testNarrowingMappingAndAbstractSet.py:38: note: Revealed type is "_collections_abc.dict_keys[str, int]" _testNarrowingMappingAndAbstractSet.py:39: note: Revealed type is "set[str]" + +[case testNativeParserTStringRequiresPython314] +# flags: --python-version=3.13 --native-parser --ignore-missing-imports +x = t"hello {1}" +reveal_type(x) +[out] +_testNativeParserTStringRequiresPython314.py:2: error: T-strings: requires Python 3.14 or newer (current target: Python 3.13) +_testNativeParserTStringRequiresPython314.py:3: note: Revealed type is "Any" + +[case testNativeParserTStringRequiresPython314InStub] +# flags: --python-version=3.13 --native-parser --ignore-missing-imports +import m +[file m.pyi] +x: object +def f() -> object: ... +y = t"hello {1}" +[out] +m.pyi:3: error: T-strings: requires Python 3.14 or newer (current target: Python 3.13) + +[case testNativeParserStarUnpackRequiresPython311] +# flags: --python-version=3.10 --native-parser +from typing_extensions import TypeVarTuple, Unpack +Ts = TypeVarTuple("Ts") +def f(x: tuple[Unpack[Ts]]) -> None: ... +def g(x: tuple[*Ts]) -> None: ... +reveal_type(g) +g((1, 2)) +[out] +_testNativeParserStarUnpackRequiresPython311.py:5: error: Star unpack syntax: requires Python 3.11 or newer (current target: Python 3.10) +_testNativeParserStarUnpackRequiresPython311.py:6: note: Revealed type is "def [Ts] (x: tuple[*Ts])" From 7c9097b971071b2fc80daa6f151c1a6a5f82fc1a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 4 Jun 2026 15:39:38 +0100 Subject: [PATCH 066/127] Update TypedDictType.__init__ signature to preserve backward compat (#21590) The new signature added a new parameter in the middle of the signature, which may break plugins. Add the new parameter to the end of the parameter list with a default value so that existing calls will continue to work. This looks a bit ugly, but backward compatibility is the priority here. --- mypy/copytype.py | 5 ++++- mypy/expandtype.py | 2 +- mypy/exprtotype.py | 2 +- mypy/fastparse.py | 2 +- mypy/join.py | 4 +++- mypy/meet.py | 4 +++- mypy/nativeparse.py | 2 +- mypy/semanal_typeddict.py | 2 +- mypy/test/testtypes.py | 17 +++++++++++++++++ mypy/type_visitor.py | 2 +- mypy/typeanal.py | 2 +- mypy/types.py | 15 +++++++++++---- 12 files changed, 45 insertions(+), 14 deletions(-) diff --git a/mypy/copytype.py b/mypy/copytype.py index 3ec512193bece..12dda9d263975 100644 --- a/mypy/copytype.py +++ b/mypy/copytype.py @@ -107,7 +107,10 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: def visit_typeddict_type(self, t: TypedDictType) -> ProperType: return self.copy_common( - t, TypedDictType(t.items, t.required_keys, t.readonly_keys, t.is_closed, t.fallback) + t, + TypedDictType( + t.items, t.required_keys, t.readonly_keys, t.fallback, is_closed=t.is_closed + ), ) def visit_literal_type(self, t: LiteralType) -> ProperType: diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 186429abd36a9..fd507216a6be9 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -350,7 +350,7 @@ def _possible_callable_kwargs(cls, repl: Parameters, dict_type: Instance) -> Pro return Instance(dict_type.type, [dict_type.args[0], extra_items]) # TODO: when PEP 728 `extra_items` is implemented, pass extra_items below. is_closed = extra_items is None - return TypedDictType(kwargs, required_names, set(), is_closed, fallback=dict_type) + return TypedDictType(kwargs, required_names, set(), dict_type, is_closed=is_closed) def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: # Sometimes solver may need to expand a type variable with (a copy of) itself diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 5fecf7b6fba9f..1c9323be056dd 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -277,7 +277,7 @@ def expr_to_unanalyzed_type( value, options, allow_new_syntax, expr, lookup_qualified=lookup_qualified ) result = TypedDictType( - items, set(), set(), False, Instance(MISSING_FALLBACK, ()), expr.line, expr.column + items, set(), set(), Instance(MISSING_FALLBACK, ()), expr.line, expr.column ) result.extra_items_from = extra_items_from return result diff --git a/mypy/fastparse.py b/mypy/fastparse.py index c2d2243d7555e..d9e2d5df8f4c1 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2143,7 +2143,7 @@ def visit_Dict(self, n: ast3.Dict) -> Type: continue return self.invalid_type(n) items[item_name.value] = self.visit(value) - result = TypedDictType(items, set(), set(), False, _dummy_fallback, n.lineno, n.col_offset) + result = TypedDictType(items, set(), set(), _dummy_fallback, n.lineno, n.col_offset) result.extra_items_from = extra_items_from return result diff --git a/mypy/join.py b/mypy/join.py index b99ed190cc9da..c609b303bb087 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -663,7 +663,9 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType: fallback = self.s.create_anonymous_fallback() is_closed = self.s.is_closed and t.is_closed - return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) + return TypedDictType( + items, required_keys, readonly_keys, fallback, is_closed=is_closed + ) elif isinstance(self.s, Instance): return join_types(self.s, t.fallback) else: diff --git a/mypy/meet.py b/mypy/meet.py index 57b79c51f8e11..bfc6d88a1e209 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1186,7 +1186,9 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType: fallback = self.s.create_anonymous_fallback() required_keys = self.s.required_keys | t.required_keys - return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) + return TypedDictType( + items, required_keys, readonly_keys, fallback, is_closed=is_closed + ) elif isinstance(self.s, Instance) and is_subtype(t, self.s): return t else: diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index c7a97862b461e..d5af1c8c6ec8d 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -973,7 +973,7 @@ def read_type(state: State, data: ReadBuffer) -> Type: extra_items_from.append(val) else: td_items[key] = val - typeddict_type = TypedDictType(td_items, set(), set(), False, _dummy_fallback) + typeddict_type = TypedDictType(td_items, set(), set(), _dummy_fallback) typeddict_type.extra_items_from = extra_items_from read_loc(data, typeddict_type) expect_end_tag(data) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 5f152ba0e8988..b1ba9f6c3abec 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -785,7 +785,7 @@ def build_typeddict_typeinfo( assert fallback is not None info = existing_info or self.api.basic_new_typeinfo(name, fallback, line) typeddict_type = TypedDictType( - item_types, required_keys, readonly_keys, is_closed, fallback + item_types, required_keys, readonly_keys, fallback, is_closed=is_closed ) any_placeholder = has_placeholder(typeddict_type) if typeddict_data: diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index ec9af3e669344..ac6d24b1ef4c0 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -39,6 +39,7 @@ ProperType, TupleType, Type, + TypedDictType, TypeOfAny, TypeType, TypeVarId, @@ -223,6 +224,22 @@ def test_indirection_no_infinite_recursion(self) -> None: modules = visitor.modules assert modules == {"__main__", "builtins"} + def test_typeddict_type_constructor_signature(self) -> None: + typ = TypedDictType({"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20) + + assert typ.fallback is self.fx.a + assert_equal(typ.line, 10) + assert_equal(typ.column, 20) + assert not typ.is_closed + + closed = TypedDictType({"x": self.fx.o}, {"x"}, set(), self.fx.a, is_closed=True) + assert closed.is_closed + + with self.assertRaises(TypeError): + TypedDictType( # type: ignore[misc] + {"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20, True + ) + class TypeOpsSuite(Suite): def setUp(self) -> None: diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 7a486d6603a00..7052c80118710 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -284,11 +284,11 @@ def visit_typeddict_type(self, t: TypedDictType, /) -> Type: items, t.required_keys, t.readonly_keys, - t.is_closed, # TODO: This appears to be unsafe. cast(Any, t.fallback.accept(self)), t.line, t.column, + is_closed=t.is_closed, ) self.set_cached(t, result) return result diff --git a/mypy/typeanal.py b/mypy/typeanal.py index aa5d14bddc65a..ff3c8bd2816e1 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1416,7 +1416,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: fallback = t.fallback is_closed = t.is_closed return TypedDictType( - items, required_keys, readonly_keys, is_closed, fallback, t.line, t.column + items, required_keys, readonly_keys, fallback, t.line, t.column, is_closed=is_closed ) def visit_raw_expression_type(self, t: RawExpressionType) -> Type: diff --git a/mypy/types.py b/mypy/types.py index 324135df014d3..6e934f64315c0 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3048,10 +3048,11 @@ def __init__( items: dict[str, Type], required_keys: set[str], readonly_keys: set[str], - is_closed: bool, fallback: Instance, line: int = -1, column: int = -1, + *, + is_closed: bool = False, ) -> None: super().__init__(line, column) self.items = items @@ -3112,8 +3113,8 @@ def deserialize(cls, data: JsonDict) -> TypedDictType: {n: deserialize_type(t) for (n, t) in data["items"]}, set(data["required_keys"]), set(data["readonly_keys"]), - bool(data["is_closed"]), Instance.deserialize(data["fallback"]), + is_closed=bool(data["is_closed"]), ) def write(self, data: WriteBuffer) -> None: @@ -3133,8 +3134,8 @@ def read(cls, data: ReadBuffer) -> TypedDictType: read_type_map(data), set(read_str_list(data)), set(read_str_list(data)), - read_bool(data), fallback, + is_closed=read_bool(data), ) assert read_tag(data) == END_TAG return ret @@ -3178,7 +3179,13 @@ def copy_modified( items = {k: v for (k, v) in items.items() if k in item_names} required_keys &= set(item_names) return TypedDictType( - items, required_keys, readonly_keys, is_closed, fallback, self.line, self.column + items, + required_keys, + readonly_keys, + fallback, + self.line, + self.column, + is_closed=is_closed, ) def zip(self, right: TypedDictType) -> Iterable[tuple[str, Type, Type]]: From f779f77c063c6ce4cdd9d4e9f4a34e36693e193e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 4 Jun 2026 16:11:00 +0100 Subject: [PATCH 067/127] Add test for disabling syntax version checks in .pyi files (#21589) Follow-up to #21539. Stub files should accept the most recent syntax, even if targeting an older Python version, since they aren't executed. --- test-data/unit/pythoneval.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 399003f02c7fb..8c37e48d7c332 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -2298,3 +2298,15 @@ g((1, 2)) [out] _testNativeParserStarUnpackRequiresPython311.py:5: error: Star unpack syntax: requires Python 3.11 or newer (current target: Python 3.10) _testNativeParserStarUnpackRequiresPython311.py:6: note: Revealed type is "def [Ts] (x: tuple[*Ts])" + +[case testNativeParserPEP695InStubNoVersionError] +# flags: --python-version=3.11 --native-parser --ignore-missing-imports +import m +reveal_type(m.f) +[file m.pyi] +class C[T]: ... +def f[T](x: T) -> T: ... +type Alias[T] = list[T] +def g[T = int](x: T) -> T: ... +[out] +_testNativeParserPEP695InStubNoVersionError.py:3: note: Revealed type is "def [T] (x: T) -> T" From d69ab34a2cd1ea3440fd2690b482d0bc0035c62d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 5 Jun 2026 00:36:43 +0100 Subject: [PATCH 068/127] Add support for TypeForm in incremental mode (#21591) Fixes https://github.com/python/mypy/issues/21587 Fix is trivial. It looks like we didn't have any incremental tests for `TypeForm`. --- mypy/types.py | 3 ++- test-data/unit/check-incremental.test | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index 6e934f64315c0..da420d1c012d2 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3688,11 +3688,12 @@ def deserialize(cls, data: JsonDict) -> Type: def write(self, data: WriteBuffer) -> None: write_tag(data, TYPE_TYPE) self.item.write(data) + write_bool(data, self.is_type_form) write_tag(data, END_TAG) @classmethod def read(cls, data: ReadBuffer) -> Type: - ret = TypeType.make_normalized(read_type(data)) + ret = TypeType.make_normalized(read_type(data), is_type_form=read_bool(data)) assert read_tag(data) == END_TAG return ret diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 22f0c805799bb..9647393cdac66 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -8116,3 +8116,26 @@ tmp/m.py:5: note: Revealed type is "builtins.str" tmp/m.py:4: note: Revealed type is "def () -> builtins.int" tmp/m.py:5: error: Incompatible types in assignment (expression has type "type[A]", variable has type "int") tmp/m.py:6: note: Revealed type is "builtins.str" + +[case testTypeFormWorksInIncrementalMode] +import b +[file a.py] +from typing import TypeVar +from typing_extensions import TypeForm + +T = TypeVar("T") +def test(arg: TypeForm[T]) -> T: ... + +[file b.py] +from a import test +test("int") + +[file b.py.2] +from a import test +reveal_type(test("int")) + +[builtins fixtures/primitives.pyi] +[typing fixtures/typing-full.pyi] +[out] +[out2] +tmp/b.py:2: note: Revealed type is "builtins.int" From 5e7e91bc7aafccf370b2a3f3111377cbab0057e3 Mon Sep 17 00:00:00 2001 From: Kevin Kannammalil Date: Thu, 4 Jun 2026 20:14:44 -0400 Subject: [PATCH 069/127] Speed up transitive_dep_hash for singleton SCCs (#21390) I noticed on a large codebase ~99.84% of SCCs are singletons. So the two main changes are adding a singleton fast path and extracting `graph[id]` to be a local variable so that it does the lookup once per module rather than once per dependency, The time in the `transitive_dep_hash` function drops 22%, and improved the overall warm run by 1% --- mypy/build.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 09e739f7fb991..a7116a78a6aa5 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -5075,18 +5075,33 @@ def deps_filtered(graph: Graph, vertices: AbstractSet[str], id: str, pri_max: in def transitive_dep_hash(scc: SCC, graph: Graph) -> bytes: """Compute stable snapshot of transitive import structure for given SCC.""" - all_direct_deps = sorted( - { - dep - for id in scc.mod_ids - for dep in graph[id].dependencies - if graph[id].priorities.get(dep) != PRI_INDIRECT - } - ) + mod_ids = scc.mod_ids + if len(mod_ids) == 1: + # Fast path: State.dependencies is already deduped and never contains + # self.id, so we can skip the dedupe set and the self-membership check. + (only_id,) = mod_ids + st = graph[only_id] + priorities = st.priorities + all_direct_deps = sorted( + dep for dep in st.dependencies if priorities.get(dep) != PRI_INDIRECT + ) + buf = WriteBuffer() + for dep_id in all_direct_deps: + write_str_bare(buf, dep_id) + write_bytes_bare(buf, graph[dep_id].trans_dep_hash) + return hash_digest_bytes(buf.getvalue()) + deps_set: set[str] = set() + for id in mod_ids: + state = graph[id] + priorities = state.priorities + for dep in state.dependencies: + if priorities.get(dep) != PRI_INDIRECT: + deps_set.add(dep) + all_direct_deps = sorted(deps_set) buf = WriteBuffer() for dep_id in all_direct_deps: write_str_bare(buf, dep_id) - if dep_id not in scc.mod_ids: + if dep_id not in mod_ids: write_bytes_bare(buf, graph[dep_id].trans_dep_hash) return hash_digest_bytes(buf.getvalue()) From 28751e9c68752a2e3b5a087974f7ac3ae370a4b0 Mon Sep 17 00:00:00 2001 From: Kevin Kannammalil Date: Thu, 4 Jun 2026 20:38:41 -0400 Subject: [PATCH 070/127] Optimize SCC scheduler `not_ready_deps` tracking (#21389) The `not_ready_deps` was populated with each direct dependency ID to do the two operations below: - mark a dependency as completed by discarding it - check if all dependences are done based on if it's empty All of these could be done with a counter. The ids are never actually being used/inspected, just the size. So with a counter, we can just populate a counter with `len(dep_sccs)`, and rather than discarding an id we can just decrement the counter, and we can check if the counter is zero for when all dependencies have been completed. This is roughly a ~2% speed up on a warm run. --------- Co-authored-by: Ivan Levkivskyi --- mypy/build.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index a7116a78a6aa5..3d0dc8df8b1c2 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -230,10 +230,10 @@ def __init__( self.mod_ids = ids # Direct dependencies, should be populated by the caller. self.deps: set[int] = set(deps) if deps is not None else set() - # Direct dependencies that have not been processed yet. - # Should be populated by the caller. This set may change during graph - # processing, while the above stays constant. - self.not_ready_deps: set[int] = set() + # Count of direct dependencies that have not been processed yet. + # Populated by the caller from len(deps); decremented during graph + # processing as each dep completes. self.deps above stays constant. + self.not_ready_count: int = 0 # SCCs that (directly) depend on this SCC. Note this is a list to # make processing order more predictable. Dependents will be notified # that they may be ready in the order in this list. @@ -4619,10 +4619,11 @@ def process_graph(graph: Graph, manager: BuildManager) -> None: ready = [] for done_scc in done: for dependent in done_scc.direct_dependents: - scc_by_id[dependent].not_ready_deps.discard(done_scc.id) - if not scc_by_id[dependent].not_ready_deps: - not_ready.remove(scc_by_id[dependent]) - ready.append(scc_by_id[dependent]) + dep_scc = scc_by_id[dependent] + dep_scc.not_ready_count -= 1 + if not dep_scc.not_ready_count: + not_ready.remove(dep_scc) + ready.append(dep_scc) manager.trace(f"Transitive deps cache size: {sys.getsizeof(manager.transitive_deps_cache)}") @@ -5003,9 +5004,10 @@ def prepare_sccs_full( for scc in sccs: # Remove trivial dependency on itself. scc_deps_map[scc].discard(scc) - for dep_scc in scc_deps_map[scc]: + dep_sccs = scc_deps_map[scc] + for dep_scc in dep_sccs: scc.deps.add(dep_scc.id) - scc.not_ready_deps.add(dep_scc.id) + scc.not_ready_count = len(dep_sccs) return scc_deps_map From e15a6d58da79c9703812b9102e6c18e09351b830 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 5 Jun 2026 13:01:36 +0100 Subject: [PATCH 071/127] [mypyc] Fix name lookup when class var and module var have the same name (#21594) Disambiguate using the Var node, since the short name itself is not enough to disambiguate between global variable and class variable with the same name. Fixes https://github.com/mypyc/mypyc/issues/1201. --- mypyc/irbuild/builder.py | 4 ++-- mypyc/irbuild/classdef.py | 4 +++- mypyc/irbuild/expression.py | 6 +++++- mypyc/test-data/run-classes.test | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 066954e920165..587be873a46d9 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -251,11 +251,11 @@ def __init__( self.visitor = visitor - # Class body context: tracks ClassVar names defined so far when processing + # Class body context: tracks ClassVars defined so far when processing # a class body, so that intra-class references (e.g. C = A | B where A is # a ClassVar defined earlier in the same class) can be resolved correctly. # Without this, mypyc looks up such names in module globals, which fails. - self.class_body_classvars: dict[str, None] = {} + self.class_body_classvars: dict[Var, None] = {} self.class_body_obj: Value | None = None self.class_body_ir: ClassIR | None = None diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 5bc19c961010b..4fd9a2e2cc748 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -26,6 +26,7 @@ TempNode, TypeInfo, TypeParam, + Var, is_class_var, ) from mypy.types import Instance, UnboundType, get_proper_type @@ -186,7 +187,8 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None: cls_builder.add_attr(lvalue, stmt) # Track this ClassVar so subsequent class body statements can reference it. if is_class_var(lvalue) or stmt.is_final_def: - builder.class_body_classvars[lvalue.name] = None + assert isinstance(lvalue.node, Var), lvalue.node + builder.class_body_classvars[lvalue.node] = None elif isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr): # Docstring. Ignore diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index f953dd3825ef2..cd7295ef709ad 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -232,7 +232,11 @@ def transform_name_expr(builder: IRBuilder, expr: NameExpr) -> Value: # If we're evaluating a class body and this name is a ClassVar defined earlier # in the same class, load it from the class being built (type object for ext classes, # class dict for non-ext classes) instead of module globals. - if builder.class_body_obj is not None and expr.name in builder.class_body_classvars: + if ( + builder.class_body_obj is not None + and isinstance(expr.node, Var) + and expr.node in builder.class_body_classvars + ): if builder.class_body_ir is not None and builder.class_body_ir.is_ext_class: return builder.py_get_attr(builder.class_body_obj, expr.name, expr.line) else: diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 39172a6385696..7722cf26ca910 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1113,6 +1113,24 @@ assert f() == 10 A.x = 200 assert f() == 200 +[case testClassVarDoesNotShadowMethodGlobal] +from typing import ClassVar +from testutil import assertRaises + +class E(Exception): + pass + +class C: + E: ClassVar[type[E]] = E + + def m(self, b: bytes) -> None: + if not b: + raise E() + +def test_class_var_does_not_shadow_method_global() -> None: + with assertRaises(E): + C().m(b"") + [case testInitSubclassWithClassVar] from typing import ClassVar From 5004eae4add3a8fd0be58597b98acdf18963a1e4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:13:16 +0200 Subject: [PATCH 072/127] Sync typeshed (#21507) Source commit: https://github.com/python/typeshed/commit/616424285beccaa76f90e87e1e922b1dc68710ca --------- Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Co-authored-by: Ivan Levkivskyi Co-authored-by: hauntsaninja Co-authored-by: AlexWaygood --- ...fix-mypy-lookup-error-due-to-circula.patch | 83 +- ...ially-revert-Clean-up-argparse-hacks.patch | 12 +- ...e-of-LiteralString-in-builtins-13743.patch | 81 +- ...redundant-inheritances-from-Iterator.patch | 101 +-- ...ravariant-type-variable-in-Container.patch | 10 +- ...1-Revert-dict.__or__-typeshed-change.patch | 65 +- .../0001-Revert-operator-changes.patch | 23 +- ...ert-sum-literal-integer-change-13961.patch | 8 +- .../0001-Revert-typeshed-ctypes-change.patch | 15 +- ...rarily-revert-contextlib-deprecation.patch | 26 +- mypy/test/teststubtest.py | 1 - mypy/typeshed/stdlib/VERSIONS | 18 +- mypy/typeshed/stdlib/_ast.pyi | 26 +- mypy/typeshed/stdlib/_asyncio.pyi | 8 +- mypy/typeshed/stdlib/_bisect.pyi | 229 +++-- mypy/typeshed/stdlib/_bootlocale.pyi | 1 - mypy/typeshed/stdlib/_codecs.pyi | 10 +- mypy/typeshed/stdlib/_collections_abc.pyi | 22 +- mypy/typeshed/stdlib/_contextvars.pyi | 9 +- mypy/typeshed/stdlib/_csv.pyi | 65 +- mypy/typeshed/stdlib/_ctypes.pyi | 31 +- mypy/typeshed/stdlib/_curses.pyi | 56 +- mypy/typeshed/stdlib/_dbm.pyi | 6 +- mypy/typeshed/stdlib/_decimal.pyi | 5 +- mypy/typeshed/stdlib/_frozen_importlib.pyi | 23 +- .../stdlib/_frozen_importlib_external.pyi | 64 +- mypy/typeshed/stdlib/_gdbm.pyi | 6 +- mypy/typeshed/stdlib/_hashlib.pyi | 12 +- mypy/typeshed/stdlib/_heapq.pyi | 2 +- mypy/typeshed/stdlib/_interpqueues.pyi | 20 +- mypy/typeshed/stdlib/_interpreters.pyi | 5 +- mypy/typeshed/stdlib/_io.pyi | 53 +- mypy/typeshed/stdlib/_json.pyi | 10 +- mypy/typeshed/stdlib/_lsprof.pyi | 7 +- mypy/typeshed/stdlib/_lzma.pyi | 4 +- mypy/typeshed/stdlib/_markupbase.pyi | 6 - mypy/typeshed/stdlib/_msi.pyi | 2 +- mypy/typeshed/stdlib/_operator.pyi | 9 +- mypy/typeshed/stdlib/_pickle.pyi | 8 +- mypy/typeshed/stdlib/_pydecimal.pyi | 3 + mypy/typeshed/stdlib/_random.pyi | 10 +- mypy/typeshed/stdlib/_remote_debugging.pyi | 183 ++++ mypy/typeshed/stdlib/_socket.pyi | 49 +- mypy/typeshed/stdlib/_sqlite3.pyi | 20 +- mypy/typeshed/stdlib/_ssl.pyi | 61 +- mypy/typeshed/stdlib/_struct.pyi | 1 + mypy/typeshed/stdlib/_thread.pyi | 14 +- mypy/typeshed/stdlib/_threading_local.pyi | 4 +- mypy/typeshed/stdlib/_tkinter.pyi | 6 +- mypy/typeshed/stdlib/_typeshed/__init__.pyi | 22 +- .../_typeshed/_type_checker_internals.pyi | 4 + mypy/typeshed/stdlib/_typeshed/dbapi.pyi | 3 +- mypy/typeshed/stdlib/_typeshed/wsgi.pyi | 3 +- mypy/typeshed/stdlib/_warnings.pyi | 1 - mypy/typeshed/stdlib/_weakrefset.pyi | 1 + mypy/typeshed/stdlib/_winapi.pyi | 57 +- mypy/typeshed/stdlib/_zstd.pyi | 6 +- mypy/typeshed/stdlib/abc.pyi | 9 +- mypy/typeshed/stdlib/aifc.pyi | 4 +- mypy/typeshed/stdlib/annotationlib.pyi | 10 + mypy/typeshed/stdlib/argparse.pyi | 201 ++--- mypy/typeshed/stdlib/array.pyi | 25 +- mypy/typeshed/stdlib/ast.pyi | 679 ++++++++------- mypy/typeshed/stdlib/asyncio/__init__.pyi | 21 +- mypy/typeshed/stdlib/asyncio/base_events.pyi | 7 +- .../stdlib/asyncio/base_subprocess.pyi | 3 +- mypy/typeshed/stdlib/asyncio/coroutines.pyi | 6 +- mypy/typeshed/stdlib/asyncio/events.pyi | 52 +- .../stdlib/asyncio/format_helpers.pyi | 3 +- mypy/typeshed/stdlib/asyncio/graph.pyi | 1 + mypy/typeshed/stdlib/asyncio/locks.pyi | 31 +- .../stdlib/asyncio/proactor_events.pyi | 32 +- mypy/typeshed/stdlib/asyncio/queues.pyi | 16 +- mypy/typeshed/stdlib/asyncio/runners.pyi | 7 +- mypy/typeshed/stdlib/asyncio/sslproto.pyi | 3 +- mypy/typeshed/stdlib/asyncio/streams.pyi | 85 +- mypy/typeshed/stdlib/asyncio/subprocess.pyi | 66 +- mypy/typeshed/stdlib/asyncio/taskgroups.pyi | 2 + mypy/typeshed/stdlib/asyncio/tasks.pyi | 400 +++------ mypy/typeshed/stdlib/asyncio/threads.pyi | 3 +- mypy/typeshed/stdlib/asyncio/tools.pyi | 9 +- mypy/typeshed/stdlib/asyncio/trsock.pyi | 12 +- mypy/typeshed/stdlib/asyncio/unix_events.pyi | 1 + mypy/typeshed/stdlib/asyncore.pyi | 5 +- mypy/typeshed/stdlib/atexit.pyi | 3 +- mypy/typeshed/stdlib/audioop.pyi | 3 +- mypy/typeshed/stdlib/base64.pyi | 102 ++- mypy/typeshed/stdlib/bdb.pyi | 23 +- mypy/typeshed/stdlib/binascii.pyi | 72 +- mypy/typeshed/stdlib/binhex.pyi | 3 +- mypy/typeshed/stdlib/builtins.pyi | 709 +++++++++------ mypy/typeshed/stdlib/bz2.pyi | 6 +- mypy/typeshed/stdlib/cProfile.pyi | 8 +- mypy/typeshed/stdlib/calendar.pyi | 63 +- mypy/typeshed/stdlib/cgi.pyi | 1 + mypy/typeshed/stdlib/cmath.pyi | 3 +- mypy/typeshed/stdlib/codecs.pyi | 6 +- mypy/typeshed/stdlib/collections/__init__.pyi | 123 ++- mypy/typeshed/stdlib/colorsys.pyi | 2 +- mypy/typeshed/stdlib/compileall.pyi | 101 +-- .../stdlib/compression/zstd/__init__.pyi | 1 + .../stdlib/compression/zstd/_zstdfile.pyi | 4 +- .../stdlib/concurrent/futures/_base.pyi | 4 +- .../stdlib/concurrent/futures/interpreter.pyi | 7 +- .../stdlib/concurrent/futures/thread.pyi | 7 +- .../concurrent/interpreters/__init__.pyi | 4 +- .../concurrent/interpreters/_crossinterp.pyi | 4 +- mypy/typeshed/stdlib/configparser.pyi | 18 +- mypy/typeshed/stdlib/contextlib.pyi | 94 +- mypy/typeshed/stdlib/copyreg.pyi | 3 +- mypy/typeshed/stdlib/csv.pyi | 8 +- mypy/typeshed/stdlib/ctypes/__init__.pyi | 80 +- mypy/typeshed/stdlib/ctypes/wintypes.pyi | 4 +- mypy/typeshed/stdlib/curses/__init__.pyi | 7 +- mypy/typeshed/stdlib/dataclasses.pyi | 113 +-- mypy/typeshed/stdlib/datetime.pyi | 59 +- mypy/typeshed/stdlib/dbm/__init__.pyi | 4 +- mypy/typeshed/stdlib/dbm/dumb.pyi | 6 +- mypy/typeshed/stdlib/dbm/sqlite3.pyi | 9 +- mypy/typeshed/stdlib/decimal.pyi | 8 +- mypy/typeshed/stdlib/difflib.pyi | 38 +- mypy/typeshed/stdlib/dis.pyi | 4 +- .../stdlib/distutils/archive_util.pyi | 1 + mypy/typeshed/stdlib/distutils/ccompiler.pyi | 10 +- mypy/typeshed/stdlib/distutils/cmd.pyi | 9 + .../stdlib/distutils/command/__init__.pyi | 7 - .../distutils/command/bdist_wininst.pyi | 16 - .../stdlib/distutils/command/check.pyi | 3 +- .../stdlib/distutils/command/install.pyi | 4 - mypy/typeshed/stdlib/distutils/dist.pyi | 7 +- .../stdlib/distutils/fancy_getopt.pyi | 5 +- mypy/typeshed/stdlib/distutils/file_util.pyi | 2 + mypy/typeshed/stdlib/distutils/filelist.pyi | 3 + mypy/typeshed/stdlib/distutils/sysconfig.pyi | 7 +- mypy/typeshed/stdlib/doctest.pyi | 4 +- mypy/typeshed/stdlib/email/__init__.pyi | 6 +- .../stdlib/email/_header_value_parser.pyi | 11 +- mypy/typeshed/stdlib/email/charset.pyi | 2 + mypy/typeshed/stdlib/email/errors.pyi | 6 +- mypy/typeshed/stdlib/email/feedparser.pyi | 2 + mypy/typeshed/stdlib/email/generator.pyi | 2 + mypy/typeshed/stdlib/email/iterators.pyi | 5 +- mypy/typeshed/stdlib/email/message.pyi | 19 +- mypy/typeshed/stdlib/email/parser.pyi | 3 + mypy/typeshed/stdlib/email/policy.pyi | 2 + mypy/typeshed/stdlib/email/utils.pyi | 7 +- mypy/typeshed/stdlib/enum.pyi | 11 +- mypy/typeshed/stdlib/faulthandler.pyi | 48 +- mypy/typeshed/stdlib/fcntl.pyi | 9 +- mypy/typeshed/stdlib/fileinput.pyi | 196 ++--- mypy/typeshed/stdlib/formatter.pyi | 88 -- mypy/typeshed/stdlib/fractions.pyi | 24 +- mypy/typeshed/stdlib/ftplib.pyi | 1 + mypy/typeshed/stdlib/functools.pyi | 25 +- mypy/typeshed/stdlib/gc.pyi | 3 +- mypy/typeshed/stdlib/genericpath.pyi | 68 +- mypy/typeshed/stdlib/gettext.pyi | 3 +- mypy/typeshed/stdlib/glob.pyi | 12 +- mypy/typeshed/stdlib/graphlib.pyi | 1 + mypy/typeshed/stdlib/grp.pyi | 3 +- mypy/typeshed/stdlib/gzip.pyi | 11 +- mypy/typeshed/stdlib/hashlib.pyi | 25 +- mypy/typeshed/stdlib/heapq.pyi | 26 +- mypy/typeshed/stdlib/hmac.pyi | 3 +- mypy/typeshed/stdlib/html/__init__.pyi | 6 +- mypy/typeshed/stdlib/http/client.pyi | 50 +- mypy/typeshed/stdlib/http/cookiejar.pyi | 7 +- mypy/typeshed/stdlib/http/cookies.pyi | 26 +- mypy/typeshed/stdlib/http/server.pyi | 57 +- mypy/typeshed/stdlib/imaplib.pyi | 5 +- mypy/typeshed/stdlib/importlib/_abc.pyi | 25 +- mypy/typeshed/stdlib/importlib/abc.pyi | 85 +- .../stdlib/importlib/metadata/__init__.pyi | 97 +-- .../stdlib/importlib/metadata/_meta.pyi | 2 + mypy/typeshed/stdlib/importlib/readers.pyi | 87 +- .../stdlib/importlib/resources/__init__.pyi | 17 +- .../stdlib/importlib/resources/_common.pyi | 5 +- .../importlib/resources/_functional.pyi | 4 + .../stdlib/importlib/resources/abc.pyi | 10 +- .../stdlib/importlib/resources/simple.pyi | 2 + mypy/typeshed/stdlib/importlib/util.pyi | 4 +- mypy/typeshed/stdlib/inspect.pyi | 57 +- mypy/typeshed/stdlib/ipaddress.pyi | 6 +- mypy/typeshed/stdlib/itertools.pyi | 28 +- mypy/typeshed/stdlib/json/__init__.pyi | 74 +- mypy/typeshed/stdlib/json/decoder.pyi | 38 +- .../stdlib/lib2to3/pgen2/__init__.pyi | 3 +- .../typeshed/stdlib/lib2to3/pgen2/grammar.pyi | 3 +- mypy/typeshed/stdlib/lib2to3/pgen2/parse.pyi | 2 +- mypy/typeshed/stdlib/lib2to3/pgen2/pgen.pyi | 1 + .../stdlib/lib2to3/pgen2/tokenize.pyi | 2 +- mypy/typeshed/stdlib/lib2to3/pytree.pyi | 4 +- mypy/typeshed/stdlib/lib2to3/refactor.pyi | 3 + mypy/typeshed/stdlib/linecache.pyi | 3 +- mypy/typeshed/stdlib/locale.pyi | 12 +- mypy/typeshed/stdlib/logging/__init__.pyi | 48 +- mypy/typeshed/stdlib/logging/config.pyi | 27 +- mypy/typeshed/stdlib/lzma.pyi | 5 +- mypy/typeshed/stdlib/mailbox.pyi | 141 +-- mypy/typeshed/stdlib/mailcap.pyi | 2 +- mypy/typeshed/stdlib/marshal.pyi | 9 +- .../stdlib/{math.pyi => math/__init__.pyi} | 21 +- mypy/typeshed/stdlib/math/integer.pyi | 8 + mypy/typeshed/stdlib/mmap.pyi | 53 +- mypy/typeshed/stdlib/msilib/__init__.pyi | 20 +- mypy/typeshed/stdlib/msilib/sequence.pyi | 3 +- mypy/typeshed/stdlib/msvcrt.pyi | 3 +- .../stdlib/multiprocessing/connection.pyi | 29 +- .../stdlib/multiprocessing/context.pyi | 50 +- .../stdlib/multiprocessing/forkserver.pyi | 25 +- mypy/typeshed/stdlib/multiprocessing/heap.pyi | 12 +- .../stdlib/multiprocessing/managers.pyi | 27 +- .../stdlib/multiprocessing/reduction.pyi | 4 +- .../stdlib/multiprocessing/shared_memory.pyi | 2 + .../stdlib/multiprocessing/sharedctypes.pyi | 11 + .../stdlib/multiprocessing/synchronize.pyi | 2 +- mypy/typeshed/stdlib/multiprocessing/util.pyi | 1 + mypy/typeshed/stdlib/netrc.pyi | 2 +- mypy/typeshed/stdlib/nntplib.pyi | 4 +- mypy/typeshed/stdlib/ntpath.pyi | 19 +- mypy/typeshed/stdlib/nturl2path.pyi | 14 +- mypy/typeshed/stdlib/numbers.pyi | 3 + mypy/typeshed/stdlib/opcode.pyi | 8 +- mypy/typeshed/stdlib/operator.pyi | 2 + mypy/typeshed/stdlib/optparse.pyi | 4 + mypy/typeshed/stdlib/os/__init__.pyi | 289 ++++--- mypy/typeshed/stdlib/parser.pyi | 25 - mypy/typeshed/stdlib/pathlib/__init__.pyi | 128 ++- mypy/typeshed/stdlib/pdb.pyi | 28 +- mypy/typeshed/stdlib/pickletools.pyi | 3 +- mypy/typeshed/stdlib/pkgutil.pyi | 19 +- mypy/typeshed/stdlib/platform.pyi | 18 +- mypy/typeshed/stdlib/poplib.pyi | 7 +- mypy/typeshed/stdlib/posix.pyi | 66 +- mypy/typeshed/stdlib/posixpath.pyi | 138 ++- mypy/typeshed/stdlib/pprint.pyi | 23 +- mypy/typeshed/stdlib/profile.pyi | 4 +- mypy/typeshed/stdlib/profiling/__init__.pyi | 3 + .../stdlib/profiling/sampling/__init__.pyi | 17 + .../stdlib/profiling/sampling/collector.pyi | 25 + .../profiling/sampling/gecko_collector.pyi | 13 + .../profiling/sampling/heatmap_collector.pyi | 24 + .../profiling/sampling/jsonl_collector.pyi | 15 + .../profiling/sampling/pstats_collector.pyi | 17 + .../profiling/sampling/stack_collector.pyi | 39 + .../profiling/sampling/string_table.pyi | 5 + mypy/typeshed/stdlib/profiling/tracing.pyi | 9 + mypy/typeshed/stdlib/pstats.pyi | 9 +- mypy/typeshed/stdlib/pty.pyi | 16 +- mypy/typeshed/stdlib/pwd.pyi | 3 +- mypy/typeshed/stdlib/py_compile.pyi | 8 +- mypy/typeshed/stdlib/pyclbr.pyi | 71 +- mypy/typeshed/stdlib/pydoc.pyi | 24 +- mypy/typeshed/stdlib/pyexpat/__init__.pyi | 14 +- mypy/typeshed/stdlib/random.pyi | 6 +- mypy/typeshed/stdlib/re.pyi | 72 +- mypy/typeshed/stdlib/readline.pyi | 3 +- mypy/typeshed/stdlib/reprlib.pyi | 3 +- mypy/typeshed/stdlib/resource.pyi | 45 +- mypy/typeshed/stdlib/sched.pyi | 31 +- mypy/typeshed/stdlib/select.pyi | 9 +- mypy/typeshed/stdlib/selectors.pyi | 24 +- mypy/typeshed/stdlib/shelve.pyi | 63 +- mypy/typeshed/stdlib/shutil.pyi | 13 +- mypy/typeshed/stdlib/signal.pyi | 28 +- mypy/typeshed/stdlib/site.pyi | 10 + mypy/typeshed/stdlib/smtpd.pyi | 4 +- mypy/typeshed/stdlib/smtplib.pyi | 8 +- mypy/typeshed/stdlib/socket.pyi | 86 +- mypy/typeshed/stdlib/socketserver.pyi | 4 +- mypy/typeshed/stdlib/spwd.pyi | 23 +- mypy/typeshed/stdlib/sqlite3/__init__.pyi | 48 +- mypy/typeshed/stdlib/sqlite3/dbapi2.pyi | 21 +- mypy/typeshed/stdlib/sre_parse.pyi | 4 +- mypy/typeshed/stdlib/ssl.pyi | 150 ++-- mypy/typeshed/stdlib/stat.pyi | 12 + mypy/typeshed/stdlib/statistics.pyi | 30 +- mypy/typeshed/stdlib/string/__init__.pyi | 2 + mypy/typeshed/stdlib/string/templatelib.pyi | 16 +- mypy/typeshed/stdlib/subprocess.pyi | 814 +++--------------- mypy/typeshed/stdlib/sunau.pyi | 4 +- mypy/typeshed/stdlib/symbol.pyi | 95 -- mypy/typeshed/stdlib/symtable.pyi | 18 +- mypy/typeshed/stdlib/sys/__init__.pyi | 89 +- mypy/typeshed/stdlib/sys/__jit.pyi | 11 + mypy/typeshed/stdlib/sys/_monitoring.pyi | 3 +- mypy/typeshed/stdlib/sysconfig.pyi | 24 +- mypy/typeshed/stdlib/syslog.pyi | 1 + mypy/typeshed/stdlib/tarfile.pyi | 47 +- mypy/typeshed/stdlib/tempfile.pyi | 23 +- mypy/typeshed/stdlib/termios.pyi | 3 +- mypy/typeshed/stdlib/threading.pyi | 33 +- mypy/typeshed/stdlib/time.pyi | 6 +- mypy/typeshed/stdlib/timeit.pyi | 11 +- mypy/typeshed/stdlib/tkinter/__init__.pyi | 290 +++++-- mypy/typeshed/stdlib/tkinter/font.pyi | 18 +- mypy/typeshed/stdlib/tkinter/simpledialog.pyi | 4 + mypy/typeshed/stdlib/tkinter/ttk.pyi | 103 ++- mypy/typeshed/stdlib/token.pyi | 7 +- mypy/typeshed/stdlib/tokenize.pyi | 8 +- mypy/typeshed/stdlib/tomllib.pyi | 1 + mypy/typeshed/stdlib/trace.pyi | 3 +- mypy/typeshed/stdlib/traceback.pyi | 106 +-- mypy/typeshed/stdlib/tracemalloc.pyi | 5 +- mypy/typeshed/stdlib/tty.pyi | 3 +- mypy/typeshed/stdlib/turtle.pyi | 69 +- mypy/typeshed/stdlib/types.pyi | 188 ++-- mypy/typeshed/stdlib/typing.pyi | 322 ++++--- mypy/typeshed/stdlib/typing_extensions.pyi | 82 +- mypy/typeshed/stdlib/unicodedata.pyi | 31 +- mypy/typeshed/stdlib/unittest/_log.pyi | 10 +- mypy/typeshed/stdlib/unittest/async_case.pyi | 3 +- mypy/typeshed/stdlib/unittest/case.pyi | 55 +- mypy/typeshed/stdlib/unittest/loader.pyi | 57 +- mypy/typeshed/stdlib/unittest/main.pyi | 7 +- mypy/typeshed/stdlib/unittest/mock.pyi | 92 +- mypy/typeshed/stdlib/unittest/result.pyi | 3 +- mypy/typeshed/stdlib/unittest/runner.pyi | 4 +- mypy/typeshed/stdlib/unittest/signals.pyi | 4 +- mypy/typeshed/stdlib/unittest/suite.pyi | 3 +- mypy/typeshed/stdlib/unittest/util.pyi | 3 +- mypy/typeshed/stdlib/urllib/error.pyi | 1 + mypy/typeshed/stdlib/urllib/parse.pyi | 239 ++++- mypy/typeshed/stdlib/urllib/request.pyi | 7 +- mypy/typeshed/stdlib/uu.pyi | 3 +- mypy/typeshed/stdlib/uuid.pyi | 4 +- mypy/typeshed/stdlib/warnings.pyi | 54 +- mypy/typeshed/stdlib/wave.pyi | 43 +- mypy/typeshed/stdlib/weakref.pyi | 19 +- mypy/typeshed/stdlib/webbrowser.pyi | 14 +- mypy/typeshed/stdlib/winreg.pyi | 9 +- mypy/typeshed/stdlib/winsound.pyi | 2 + mypy/typeshed/stdlib/wsgiref/headers.pyi | 5 +- mypy/typeshed/stdlib/wsgiref/types.pyi | 3 +- mypy/typeshed/stdlib/wsgiref/validate.pyi | 3 +- mypy/typeshed/stdlib/xml/__init__.pyi | 6 + mypy/typeshed/stdlib/xml/dom/expatbuilder.pyi | 3 +- mypy/typeshed/stdlib/xml/dom/minidom.pyi | 19 +- mypy/typeshed/stdlib/xml/dom/pulldom.pyi | 4 +- .../stdlib/xml/etree/ElementInclude.pyi | 1 + .../typeshed/stdlib/xml/etree/ElementPath.pyi | 5 +- .../typeshed/stdlib/xml/etree/ElementTree.pyi | 53 +- mypy/typeshed/stdlib/xml/sax/__init__.pyi | 3 +- mypy/typeshed/stdlib/xml/sax/expatreader.pyi | 20 +- mypy/typeshed/stdlib/xml/sax/handler.pyi | 14 +- mypy/typeshed/stdlib/xml/sax/xmlreader.pyi | 8 +- mypy/typeshed/stdlib/xml/utils.pyi | 2 + mypy/typeshed/stdlib/xmlrpc/client.pyi | 6 +- mypy/typeshed/stdlib/xmlrpc/server.pyi | 3 +- mypy/typeshed/stdlib/xxlimited.pyi | 13 +- mypy/typeshed/stdlib/zipapp.pyi | 3 +- mypy/typeshed/stdlib/zipfile/__init__.pyi | 24 +- .../stdlib/zipfile/_path/__init__.pyi | 4 + mypy/typeshed/stdlib/zipimport.pyi | 41 +- mypy/typeshed/stdlib/zlib.pyi | 7 + mypy/typeshed/stdlib/zoneinfo/__init__.pyi | 1 + 356 files changed, 6668 insertions(+), 5397 deletions(-) delete mode 100644 mypy/typeshed/stdlib/_bootlocale.pyi create mode 100644 mypy/typeshed/stdlib/_remote_debugging.pyi delete mode 100644 mypy/typeshed/stdlib/distutils/command/bdist_wininst.pyi delete mode 100644 mypy/typeshed/stdlib/formatter.pyi rename mypy/typeshed/stdlib/{math.pyi => math/__init__.pyi} (92%) create mode 100644 mypy/typeshed/stdlib/math/integer.pyi delete mode 100644 mypy/typeshed/stdlib/parser.pyi create mode 100644 mypy/typeshed/stdlib/profiling/__init__.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/__init__.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/collector.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/heatmap_collector.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/jsonl_collector.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/pstats_collector.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/stack_collector.pyi create mode 100644 mypy/typeshed/stdlib/profiling/sampling/string_table.pyi create mode 100644 mypy/typeshed/stdlib/profiling/tracing.pyi delete mode 100644 mypy/typeshed/stdlib/symbol.pyi create mode 100644 mypy/typeshed/stdlib/sys/__jit.pyi create mode 100644 mypy/typeshed/stdlib/xml/utils.pyi diff --git a/misc/typeshed_patches/0001-Adjust-stubs-to-fix-mypy-lookup-error-due-to-circula.patch b/misc/typeshed_patches/0001-Adjust-stubs-to-fix-mypy-lookup-error-due-to-circula.patch index d8c4e24af917b..87aed9de2bf78 100644 --- a/misc/typeshed_patches/0001-Adjust-stubs-to-fix-mypy-lookup-error-due-to-circula.patch +++ b/misc/typeshed_patches/0001-Adjust-stubs-to-fix-mypy-lookup-error-due-to-circula.patch @@ -1,23 +1,86 @@ -From 5c922a2484f2e18c7f901e62bb499b6414cf1090 Mon Sep 17 00:00:00 2001 +From 8985351bf6f917db1a7b15ffed95ec2357d9819d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> -Date: Wed, 18 Feb 2026 13:11:02 +0100 +Date: Mon, 18 May 2026 17:33:09 +0200 Subject: [PATCH] Adjust stubs to fix mypy lookup error due to circular dependencies in stubs --- - mypy/typeshed/stdlib/time.pyi | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) + mypy/typeshed/stdlib/__future__.pyi | 2 +- + mypy/typeshed/stdlib/dis.pyi | 4 ++-- + mypy/typeshed/stdlib/inspect.pyi | 4 ++-- + mypy/typeshed/stdlib/itertools.pyi | 2 +- + mypy/typeshed/stdlib/time.pyi | 6 +++--- + 5 files changed, 9 insertions(+), 9 deletions(-) +diff --git a/mypy/typeshed/stdlib/__future__.pyi b/mypy/typeshed/stdlib/__future__.pyi +index aa445d22b..a90cf1edd 100644 +--- a/mypy/typeshed/stdlib/__future__.pyi ++++ b/mypy/typeshed/stdlib/__future__.pyi +@@ -1,4 +1,4 @@ +-from typing import TypeAlias ++from typing_extensions import TypeAlias + + _VersionInfo: TypeAlias = tuple[int, int, int, str, int] + +diff --git a/mypy/typeshed/stdlib/dis.pyi b/mypy/typeshed/stdlib/dis.pyi +index 984f932e3..0ad928934 100644 +--- a/mypy/typeshed/stdlib/dis.pyi ++++ b/mypy/typeshed/stdlib/dis.pyi +@@ -2,7 +2,7 @@ import sys + import types + from collections.abc import Callable, Iterator + from opcode import * # `dis` re-exports it as a part of public API +-from typing import IO, Any, Final, NamedTuple, TypeAlias, overload ++from typing import IO, Any, Final, NamedTuple, overload + from typing_extensions import Self, deprecated, disjoint_base + + __all__ = [ +@@ -41,7 +41,7 @@ else: + + # Strictly this should not have to include Callable, but mypy doesn't use FunctionType + # for functions (python/mypy#3171) +-_HaveCodeType: TypeAlias = types.MethodType | types.FunctionType | types.CodeType | type | Callable[..., Any] ++_HaveCodeType = types.MethodType | types.FunctionType | types.CodeType | type | Callable[..., Any] + + if sys.version_info >= (3, 11): + class Positions(NamedTuple): +diff --git a/mypy/typeshed/stdlib/inspect.pyi b/mypy/typeshed/stdlib/inspect.pyi +index cc39a7c9f..0ca4cff75 100644 +--- a/mypy/typeshed/stdlib/inspect.pyi ++++ b/mypy/typeshed/stdlib/inspect.pyi +@@ -196,8 +196,8 @@ if sys.version_info >= (3, 14): + + modulesbyfile: dict[str, Any] + +-_GetMembersPredicateTypeGuard: TypeAlias = Callable[[Any], TypeGuard[_T]] +-_GetMembersPredicateTypeIs: TypeAlias = Callable[[Any], TypeIs[_T]] ++_GetMembersPredicateTypeGuard = Callable[[Any], TypeGuard[_T]] ++_GetMembersPredicateTypeIs = Callable[[Any], TypeIs[_T]] + _GetMembersPredicate: TypeAlias = Callable[[Any], bool] + _GetMembersReturn: TypeAlias = list[tuple[str, _T]] + +diff --git a/mypy/typeshed/stdlib/itertools.pyi b/mypy/typeshed/stdlib/itertools.pyi +index d26a4e1da..60e79bc2e 100644 +--- a/mypy/typeshed/stdlib/itertools.pyi ++++ b/mypy/typeshed/stdlib/itertools.pyi +@@ -23,7 +23,7 @@ _T10 = TypeVar("_T10") + + _Step: TypeAlias = SupportsFloat | SupportsInt | SupportsIndex | SupportsComplex + +-_Predicate: TypeAlias = Callable[[_T], object] ++_Predicate = Callable[[_T], object] + + # Technically count can take anything that implements a number protocol and has an add method + # but we can't enforce the add method diff --git a/mypy/typeshed/stdlib/time.pyi b/mypy/typeshed/stdlib/time.pyi -index 64a009318..d0853792b 100644 +index ac53089b8..9b5344b32 100644 --- a/mypy/typeshed/stdlib/time.pyi +++ b/mypy/typeshed/stdlib/time.pyi -@@ -1,16 +1,16 @@ +@@ -1,15 +1,15 @@ import sys from _typeshed import structseq --from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, final, type_check_only -+from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, Union, final, type_check_only - from typing_extensions import TypeAlias +-from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypeAlias, final, type_check_only ++from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypeAlias, Union, final, type_check_only _TimeTuple: TypeAlias = tuple[int, int, int, int, int, int, int, int, int] @@ -33,5 +96,5 @@ index 64a009318..d0853792b 100644 altzone: int daylight: int -- -2.53.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Partially-revert-Clean-up-argparse-hacks.patch b/misc/typeshed_patches/0001-Partially-revert-Clean-up-argparse-hacks.patch index a1b1966fea1e0..bd7cbae517543 100644 --- a/misc/typeshed_patches/0001-Partially-revert-Clean-up-argparse-hacks.patch +++ b/misc/typeshed_patches/0001-Partially-revert-Clean-up-argparse-hacks.patch @@ -1,4 +1,4 @@ -From b6d495c10e79fb56ff64f8fb90c87894090f9cbe Mon Sep 17 00:00:00 2001 +From 3fa6658041d3ccd7f2a4bd725e21fa9768bf6ebc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:11:06 +0100 Subject: [PATCH] Partially revert Clean up argparse hacks @@ -8,16 +8,16 @@ Subject: [PATCH] Partially revert Clean up argparse hacks 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/typeshed/stdlib/argparse.pyi b/mypy/typeshed/stdlib/argparse.pyi -index ae99eb036..c87b8f4fc 100644 +index 22d330a08..fa22f842d 100644 --- a/mypy/typeshed/stdlib/argparse.pyi +++ b/mypy/typeshed/stdlib/argparse.pyi @@ -2,7 +2,7 @@ import sys from _typeshed import SupportsWrite, sentinel from collections.abc import Callable, Generator, Iterable, Sequence from re import Pattern --from typing import IO, Any, ClassVar, Final, Generic, NoReturn, Protocol, TypeVar, overload, type_check_only -+from typing import IO, Any, ClassVar, Final, Generic, NewType, NoReturn, Protocol, TypeVar, overload, type_check_only - from typing_extensions import Self, TypeAlias, deprecated +-from typing import IO, Any, ClassVar, Final, Generic, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only ++from typing import IO, Any, ClassVar, Final, Generic, NewType, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only + from typing_extensions import Self, deprecated __all__ = [ @@ -36,7 +36,9 @@ ONE_OR_MORE: Final = "+" @@ -41,5 +41,5 @@ index ae99eb036..c87b8f4fc 100644 default: Any = ..., type: _ActionType = ..., -- -2.52.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Remove-use-of-LiteralString-in-builtins-13743.patch b/misc/typeshed_patches/0001-Remove-use-of-LiteralString-in-builtins-13743.patch index 3ca8f4deec922..6794731274d14 100644 --- a/misc/typeshed_patches/0001-Remove-use-of-LiteralString-in-builtins-13743.patch +++ b/misc/typeshed_patches/0001-Remove-use-of-LiteralString-in-builtins-13743.patch @@ -1,80 +1,94 @@ -From b3021cdc4b4c2cf547c5bc2d1d21a5a00ffea001 Mon Sep 17 00:00:00 2001 +From 83cf9b2dcc569149510c3a9118b9a7dc44144232 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 26 Sep 2022 12:55:07 -0700 Subject: [PATCH] Remove use of LiteralString in builtins (#13743) --- - mypy/typeshed/stdlib/builtins.pyi | 98 ------------------------------- - 1 file changed, 98 deletions(-) + mypy/typeshed/stdlib/builtins.pyi | 127 +----------------------------- + 1 file changed, 1 insertion(+), 126 deletions(-) diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi -index efc51fe25..8600a372b 100644 +index 0fe6d9a69..1808e28e2 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi -@@ -64,7 +64,6 @@ from typing import ( # noqa: Y022,UP035 - from typing_extensions import ( # noqa: Y023 - Concatenate, - Literal, -- LiteralString, - ParamSpec, - Self, - TypeAlias, -@@ -482,31 +481,16 @@ class str(Sequence[str]): +@@ -65,7 +65,7 @@ from typing import ( # noqa: Y022,UP035 + ) + + # we can't import `Literal` from typing or mypy crashes: see #11247 +-from typing_extensions import Literal, LiteralString, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 ++from typing_extensions import Literal, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 + + if sys.version_info >= (3, 14): + from _typeshed import AnnotateFunc +@@ -489,20 +489,8 @@ class str(Sequence[str]): def __new__(cls, object: object = "") -> Self: ... @overload def __new__(cls, object: ReadableBuffer, encoding: str = "utf-8", errors: str = "strict") -> Self: ... +- - @overload - def capitalize(self: LiteralString) -> LiteralString: ... - @overload def capitalize(self) -> str: ... # type: ignore[misc] +- - @overload - def casefold(self: LiteralString) -> LiteralString: ... - @overload def casefold(self) -> str: ... # type: ignore[misc] +- - @overload - def center(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = " ", /) -> LiteralString: ... - @overload def center(self, width: SupportsIndex, fillchar: str = " ", /) -> str: ... # type: ignore[misc] + def count(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... - def encode(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: ... +@@ -510,17 +498,9 @@ class str(Sequence[str]): def endswith( self, suffix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, / ) -> bool: ... +- - @overload - def expandtabs(self: LiteralString, tabsize: SupportsIndex = 8) -> LiteralString: ... - @overload def expandtabs(self, tabsize: SupportsIndex = 8) -> str: ... # type: ignore[misc] + def find(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... +- - @overload - def format(self: LiteralString, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ... - @overload def format(self, *args: object, **kwargs: object) -> str: ... + def format_map(self, mapping: _FormatMapMapping, /) -> str: ... - def index(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... -@@ -522,98 +506,34 @@ class str(Sequence[str]): +@@ -537,119 +517,38 @@ class str(Sequence[str]): def isspace(self) -> bool: ... def istitle(self) -> bool: ... def isupper(self) -> bool: ... +- - @overload - def join(self: LiteralString, iterable: Iterable[LiteralString], /) -> LiteralString: ... - @overload def join(self, iterable: Iterable[str], /) -> str: ... # type: ignore[misc] +- - @overload - def ljust(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = " ", /) -> LiteralString: ... - @overload def ljust(self, width: SupportsIndex, fillchar: str = " ", /) -> str: ... # type: ignore[misc] +- - @overload - def lower(self: LiteralString) -> LiteralString: ... - @overload def lower(self) -> str: ... # type: ignore[misc] +- - @overload - def lstrip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: ... - @overload def lstrip(self, chars: str | None = None, /) -> str: ... # type: ignore[misc] +- - @overload - def partition(self: LiteralString, sep: LiteralString, /) -> tuple[LiteralString, LiteralString, LiteralString]: ... - @overload def partition(self, sep: str, /) -> tuple[str, str, str]: ... # type: ignore[misc] + if sys.version_info >= (3, 13): - @overload - def replace( @@ -94,102 +108,127 @@ index efc51fe25..8600a372b 100644 - def removeprefix(self: LiteralString, prefix: LiteralString, /) -> LiteralString: ... - @overload def removeprefix(self, prefix: str, /) -> str: ... # type: ignore[misc] +- - @overload - def removesuffix(self: LiteralString, suffix: LiteralString, /) -> LiteralString: ... - @overload def removesuffix(self, suffix: str, /) -> str: ... # type: ignore[misc] + def rfind(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... def rindex(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... +- - @overload - def rjust(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = " ", /) -> LiteralString: ... - @overload def rjust(self, width: SupportsIndex, fillchar: str = " ", /) -> str: ... # type: ignore[misc] +- - @overload - def rpartition(self: LiteralString, sep: LiteralString, /) -> tuple[LiteralString, LiteralString, LiteralString]: ... - @overload def rpartition(self, sep: str, /) -> tuple[str, str, str]: ... # type: ignore[misc] +- - @overload - def rsplit(self: LiteralString, sep: LiteralString | None = None, maxsplit: SupportsIndex = -1) -> list[LiteralString]: ... - @overload def rsplit(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: ... # type: ignore[misc] +- - @overload - def rstrip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: ... - @overload def rstrip(self, chars: str | None = None, /) -> str: ... # type: ignore[misc] +- - @overload - def split(self: LiteralString, sep: LiteralString | None = None, maxsplit: SupportsIndex = -1) -> list[LiteralString]: ... - @overload def split(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: ... # type: ignore[misc] +- - @overload - def splitlines(self: LiteralString, keepends: bool = False) -> list[LiteralString]: ... - @overload def splitlines(self, keepends: bool = False) -> list[str]: ... # type: ignore[misc] + def startswith( self, prefix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, / ) -> bool: ... +- - @overload - def strip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: ... - @overload def strip(self, chars: str | None = None, /) -> str: ... # type: ignore[misc] +- - @overload - def swapcase(self: LiteralString) -> LiteralString: ... - @overload def swapcase(self) -> str: ... # type: ignore[misc] +- - @overload - def title(self: LiteralString) -> LiteralString: ... - @overload def title(self) -> str: ... # type: ignore[misc] + def translate(self, table: _TranslateTable, /) -> str: ... +- - @overload - def upper(self: LiteralString) -> LiteralString: ... - @overload def upper(self) -> str: ... # type: ignore[misc] +- - @overload - def zfill(self: LiteralString, width: SupportsIndex, /) -> LiteralString: ... - @overload def zfill(self, width: SupportsIndex, /) -> str: ... # type: ignore[misc] - @staticmethod - @overload -@@ -624,39 +544,21 @@ class str(Sequence[str]): + + if sys.version_info >= (3, 15): +@@ -677,49 +576,25 @@ class str(Sequence[str]): @staticmethod @overload def maketrans(x: str, y: str, z: str, /) -> dict[int, int | None]: ... +- - @overload - def __add__(self: LiteralString, value: LiteralString, /) -> LiteralString: ... - @overload def __add__(self, value: str, /) -> str: ... # type: ignore[misc] + # Incompatible with Sequence.__contains__ def __contains__(self, key: str, /) -> bool: ... # type: ignore[override] def __eq__(self, value: object, /) -> bool: ... def __ge__(self, value: str, /) -> bool: ... +- - @overload - def __getitem__(self: LiteralString, key: SupportsIndex | slice[SupportsIndex | None], /) -> LiteralString: ... - @overload def __getitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> str: ... # type: ignore[misc] + def __gt__(self, value: str, /) -> bool: ... def __hash__(self) -> int: ... +- - @overload - def __iter__(self: LiteralString) -> Iterator[LiteralString]: ... - @overload def __iter__(self) -> Iterator[str]: ... # type: ignore[misc] + def __le__(self, value: str, /) -> bool: ... def __len__(self) -> int: ... def __lt__(self, value: str, /) -> bool: ... +- - @overload - def __mod__(self: LiteralString, value: LiteralString | tuple[LiteralString, ...], /) -> LiteralString: ... - @overload def __mod__(self, value: Any, /) -> str: ... +- - @overload - def __mul__(self: LiteralString, value: SupportsIndex, /) -> LiteralString: ... - @overload def __mul__(self, value: SupportsIndex, /) -> str: ... # type: ignore[misc] + def __ne__(self, value: object, /) -> bool: ... +- - @overload - def __rmul__(self: LiteralString, value: SupportsIndex, /) -> LiteralString: ... - @overload def __rmul__(self, value: SupportsIndex, /) -> str: ... # type: ignore[misc] + def __getnewargs__(self) -> tuple[str]: ... - def __format__(self, format_spec: str, /) -> str: ... -- -2.52.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Revert-Remove-redundant-inheritances-from-Iterator.patch b/misc/typeshed_patches/0001-Revert-Remove-redundant-inheritances-from-Iterator.patch index 6194dc62cd36e..96779922b52f6 100644 --- a/misc/typeshed_patches/0001-Revert-Remove-redundant-inheritances-from-Iterator.patch +++ b/misc/typeshed_patches/0001-Revert-Remove-redundant-inheritances-from-Iterator.patch @@ -1,4 +1,4 @@ -From 69791281c2c5e919cea9a77c4a771f79d9e70384 Mon Sep 17 00:00:00 2001 +From d3dabcf7b7aaf0997b59f0a28bb41d17b6098f2c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:36:38 +0100 Subject: [PATCH] Revert Remove redundant inheritances from Iterator in @@ -15,7 +15,7 @@ Subject: [PATCH] Revert Remove redundant inheritances from Iterator in 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/mypy/typeshed/stdlib/_asyncio.pyi b/mypy/typeshed/stdlib/_asyncio.pyi -index d663f5d93..f43178e4d 100644 +index 23b690a9c..0ce93a435 100644 --- a/mypy/typeshed/stdlib/_asyncio.pyi +++ b/mypy/typeshed/stdlib/_asyncio.pyi @@ -1,6 +1,6 @@ @@ -25,7 +25,7 @@ index d663f5d93..f43178e4d 100644 +from collections.abc import Awaitable, Callable, Coroutine, Generator, Iterable from contextvars import Context from types import FrameType, GenericAlias - from typing import Any, Literal, TextIO, TypeVar + from typing import Any, Literal, TextIO, TypeAlias, TypeVar @@ -11,7 +11,7 @@ _T_co = TypeVar("_T_co", covariant=True) _TaskYieldType: TypeAlias = Future[object] | None @@ -36,10 +36,10 @@ index d663f5d93..f43178e4d 100644 @property def _exception(self) -> BaseException | None: ... diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi -index 8acdeadff..5a3bb5908 100644 +index 1808e28e2..9db6f4fc7 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi -@@ -1226,7 +1226,7 @@ class frozenset(AbstractSet[_T_co]): +@@ -1369,7 +1369,7 @@ class frozenset(AbstractSet[_T_co]): def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @disjoint_base @@ -48,7 +48,7 @@ index 8acdeadff..5a3bb5908 100644 def __new__(cls, iterable: Iterable[_T], start: int = 0) -> Self: ... def __iter__(self) -> Self: ... def __next__(self) -> tuple[int, _T]: ... -@@ -1413,7 +1413,7 @@ else: +@@ -1635,7 +1635,7 @@ else: exit: _sitebuiltins.Quitter @disjoint_base @@ -57,16 +57,16 @@ index 8acdeadff..5a3bb5908 100644 @overload def __new__(cls, function: None, iterable: Iterable[_T | None], /) -> Self: ... @overload -@@ -1477,7 +1477,7 @@ license: _sitebuiltins._Printer - +@@ -1706,7 +1706,7 @@ license: _sitebuiltins._Printer def locals() -> dict[str, Any]: ... + @disjoint_base -class map(Generic[_S]): +class map(Iterator[_S]): # 3.14 adds `strict` argument. if sys.version_info >= (3, 14): @overload -@@ -1784,7 +1784,7 @@ def pow(base: _SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex +@@ -2022,7 +2022,7 @@ def pow(base: _SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex quit: _sitebuiltins.Quitter @disjoint_base @@ -75,29 +75,29 @@ index 8acdeadff..5a3bb5908 100644 @overload def __new__(cls, sequence: Reversible[_T], /) -> Iterator[_T]: ... # type: ignore[misc] @overload -@@ -1848,7 +1848,7 @@ def vars(object: type, /) -> types.MappingProxyType[str, Any]: ... - @overload +@@ -2100,7 +2100,7 @@ def vars(object: type, /) -> types.MappingProxyType[str, Any]: ... def vars(object: Any = ..., /) -> dict[str, Any]: ... + @disjoint_base -class zip(Generic[_T_co]): +class zip(Iterator[_T_co]): - if sys.version_info >= (3, 10): - @overload - def __new__(cls, *, strict: bool = False) -> zip[Any]: ... + @overload + def __new__(cls, *, strict: bool = False) -> zip[Any]: ... + @overload diff --git a/mypy/typeshed/stdlib/csv.pyi b/mypy/typeshed/stdlib/csv.pyi -index 2c8e7109c..4ed0ab1d8 100644 +index f8ab5f000..f3b4286a6 100644 --- a/mypy/typeshed/stdlib/csv.pyi +++ b/mypy/typeshed/stdlib/csv.pyi -@@ -25,7 +25,7 @@ else: - from _csv import _reader as Reader, _writer as Writer - +@@ -21,7 +21,7 @@ if sys.version_info >= (3, 12): + from _csv import QUOTE_NOTNULL as QUOTE_NOTNULL, QUOTE_STRINGS as QUOTE_STRINGS + from _csv import Reader, Writer from _typeshed import SupportsWrite -from collections.abc import Collection, Iterable, Mapping, Sequence +from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence from types import GenericAlias from typing import Any, Generic, Literal, TypeVar, overload from typing_extensions import Self -@@ -73,7 +73,7 @@ class excel(Dialect): ... +@@ -69,7 +69,7 @@ class excel(Dialect): ... class excel_tab(excel): ... class unix_dialect(Dialect): ... @@ -107,7 +107,7 @@ index 2c8e7109c..4ed0ab1d8 100644 restkey: _T | None restval: str | Any | None diff --git a/mypy/typeshed/stdlib/fileinput.pyi b/mypy/typeshed/stdlib/fileinput.pyi -index 6778b7648..95164de2f 100644 +index 37783254c..db9c228f5 100644 --- a/mypy/typeshed/stdlib/fileinput.pyi +++ b/mypy/typeshed/stdlib/fileinput.pyi @@ -1,8 +1,8 @@ @@ -116,22 +116,22 @@ index 6778b7648..95164de2f 100644 -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Iterator from types import GenericAlias, TracebackType --from typing import IO, Any, AnyStr, Generic, Literal, Protocol, overload, type_check_only -+from typing import IO, Any, AnyStr, Literal, Protocol, overload, type_check_only - from typing_extensions import Self, TypeAlias, deprecated +-from typing import IO, Any, AnyStr, Generic, Literal, Protocol, TypeAlias, overload, type_check_only ++from typing import IO, Any, AnyStr, Literal, Protocol, TypeAlias, overload, type_check_only + from typing_extensions import Self, deprecated __all__ = [ -@@ -105,7 +105,7 @@ def fileno() -> int: ... +@@ -74,7 +74,7 @@ def fileno() -> int: ... def isfirstline() -> bool: ... def isstdin() -> bool: ... -class FileInput(Generic[AnyStr]): +class FileInput(Iterator[AnyStr]): - if sys.version_info >= (3, 10): - # encoding and errors are added - @overload + # encoding and errors are added + @overload + def __init__( diff --git a/mypy/typeshed/stdlib/itertools.pyi b/mypy/typeshed/stdlib/itertools.pyi -index 8a924ad8b..5c2bf7f83 100644 +index d26a4e1da..9d1cd84da 100644 --- a/mypy/typeshed/stdlib/itertools.pyi +++ b/mypy/typeshed/stdlib/itertools.pyi @@ -28,7 +28,7 @@ _Predicate: TypeAlias = Callable[[_T], object] @@ -143,7 +143,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls) -> count[int]: ... @overload -@@ -39,13 +39,13 @@ class count(Generic[_N]): +@@ -40,13 +40,13 @@ class count(Generic[_N]): def __iter__(self) -> Self: ... @disjoint_base @@ -159,7 +159,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, object: _T) -> Self: ... @overload -@@ -55,7 +55,7 @@ class repeat(Generic[_T]): +@@ -57,7 +57,7 @@ class repeat(Generic[_T]): def __length_hint__(self) -> int: ... @disjoint_base @@ -168,7 +168,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iterable: Iterable[_T], func: None = None, *, initial: _T | None = None) -> Self: ... @overload -@@ -64,7 +64,7 @@ class accumulate(Generic[_T]): +@@ -67,7 +67,7 @@ class accumulate(Generic[_T]): def __next__(self) -> _T: ... @disjoint_base @@ -177,7 +177,7 @@ index 8a924ad8b..5c2bf7f83 100644 def __new__(cls, *iterables: Iterable[_T]) -> Self: ... def __next__(self) -> _T: ... def __iter__(self) -> Self: ... -@@ -74,25 +74,25 @@ class chain(Generic[_T]): +@@ -77,25 +77,25 @@ class chain(Generic[_T]): def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @disjoint_base @@ -207,7 +207,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iterable: Iterable[_T1], key: None = None) -> groupby[_T1, _T1]: ... @overload -@@ -101,7 +101,7 @@ class groupby(Generic[_T_co, _S_co]): +@@ -105,7 +105,7 @@ class groupby(Generic[_T_co, _S_co]): def __next__(self) -> tuple[_T_co, Iterator[_S_co]]: ... @disjoint_base @@ -216,7 +216,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iterable: Iterable[_T], stop: int | None, /) -> Self: ... @overload -@@ -110,20 +110,20 @@ class islice(Generic[_T]): +@@ -115,13 +115,13 @@ class islice(Generic[_T]): def __next__(self) -> _T: ... @disjoint_base @@ -232,15 +232,16 @@ index 8a924ad8b..5c2bf7f83 100644 def __new__(cls, predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Self: ... def __iter__(self) -> Self: ... def __next__(self) -> _T: ... - +@@ -129,7 +129,7 @@ class takewhile(Generic[_T]): def tee(iterable: Iterable[_T], n: int = 2, /) -> tuple[Iterator[_T], ...]: ... + @disjoint_base -class zip_longest(Generic[_T_co]): +class zip_longest(Iterator[_T_co]): # one iterable (fillvalue doesn't matter) @overload def __new__(cls, iter1: Iterable[_T1], /, *, fillvalue: object = None) -> zip_longest[tuple[_T1]]: ... -@@ -202,7 +202,7 @@ class zip_longest(Generic[_T_co]): +@@ -209,7 +209,7 @@ class zip_longest(Generic[_T_co]): def __next__(self) -> _T_co: ... @disjoint_base @@ -249,7 +250,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iter1: Iterable[_T1], /) -> product[tuple[_T1]]: ... @overload -@@ -288,7 +288,7 @@ class product(Generic[_T_co]): +@@ -296,7 +296,7 @@ class product(Generic[_T_co]): def __next__(self) -> _T_co: ... @disjoint_base @@ -258,7 +259,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iterable: Iterable[_T], r: Literal[2]) -> permutations[tuple[_T, _T]]: ... @overload -@@ -303,7 +303,7 @@ class permutations(Generic[_T_co]): +@@ -312,7 +312,7 @@ class permutations(Generic[_T_co]): def __next__(self) -> _T_co: ... @disjoint_base @@ -267,7 +268,7 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iterable: Iterable[_T], r: Literal[2]) -> combinations[tuple[_T, _T]]: ... @overload -@@ -318,7 +318,7 @@ class combinations(Generic[_T_co]): +@@ -328,7 +328,7 @@ class combinations(Generic[_T_co]): def __next__(self) -> _T_co: ... @disjoint_base @@ -276,15 +277,15 @@ index 8a924ad8b..5c2bf7f83 100644 @overload def __new__(cls, iterable: Iterable[_T], r: Literal[2]) -> combinations_with_replacement[tuple[_T, _T]]: ... @overload -@@ -334,14 +334,14 @@ class combinations_with_replacement(Generic[_T_co]): +@@ -344,14 +344,14 @@ class combinations_with_replacement(Generic[_T_co]): + def __next__(self) -> _T_co: ... - if sys.version_info >= (3, 10): - @disjoint_base -- class pairwise(Generic[_T_co]): -+ class pairwise(Iterator[_T_co]): - def __new__(cls, iterable: Iterable[_T], /) -> pairwise[tuple[_T, _T]]: ... - def __iter__(self) -> Self: ... - def __next__(self) -> _T_co: ... + @disjoint_base +-class pairwise(Generic[_T_co]): ++class pairwise(Iterator[_T_co]): + def __new__(cls, iterable: Iterable[_T], /) -> pairwise[tuple[_T, _T]]: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... if sys.version_info >= (3, 12): @disjoint_base @@ -313,10 +314,10 @@ index b79f9e773..f276372d0 100644 def __iter__(self) -> Self: ... def next(self, timeout: float | None = None) -> _T: ... diff --git a/mypy/typeshed/stdlib/sqlite3/__init__.pyi b/mypy/typeshed/stdlib/sqlite3/__init__.pyi -index 04b978b1b..e4604144f 100644 +index 7c033bdf4..7bf020558 100644 --- a/mypy/typeshed/stdlib/sqlite3/__init__.pyi +++ b/mypy/typeshed/stdlib/sqlite3/__init__.pyi -@@ -408,7 +408,7 @@ class Connection: +@@ -426,7 +426,7 @@ class Connection: ) -> Literal[False]: ... @disjoint_base @@ -326,5 +327,5 @@ index 04b978b1b..e4604144f 100644 @property def connection(self) -> Connection: ... -- -2.52.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Revert-Use-contravariant-type-variable-in-Container.patch b/misc/typeshed_patches/0001-Revert-Use-contravariant-type-variable-in-Container.patch index 814645b7f09d1..e31001589faf9 100644 --- a/misc/typeshed_patches/0001-Revert-Use-contravariant-type-variable-in-Container.patch +++ b/misc/typeshed_patches/0001-Revert-Use-contravariant-type-variable-in-Container.patch @@ -1,4 +1,4 @@ -From 274a77fe74d961e9885036ed31e5ed46f8d91daa Mon Sep 17 00:00:00 2001 +From 1b131f8f39532078597f51143e74e07e6cc95dec Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:00:18 +0100 Subject: [PATCH] Revert Use contravariant type variable in Container @@ -8,11 +8,11 @@ Subject: [PATCH] Revert Use contravariant type variable in Container 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mypy/typeshed/stdlib/typing.pyi b/mypy/typeshed/stdlib/typing.pyi -index 83e114aac..e9f67877c 100644 +index 2018a835c..2379bec34 100644 --- a/mypy/typeshed/stdlib/typing.pyi +++ b/mypy/typeshed/stdlib/typing.pyi -@@ -640,17 +640,14 @@ class AsyncGenerator(AsyncIterator[_YieldT_co], Protocol[_YieldT_co, _SendT_cont - ) -> Coroutine[Any, Any, _YieldT_co]: ... +@@ -663,17 +663,14 @@ class AsyncGenerator(AsyncIterator[_YieldT_co], Protocol[_YieldT_co, _SendT_cont + def aclose(self) -> Coroutine[Any, Any, None]: ... -_ContainerT_contra = TypeVar("_ContainerT_contra", contravariant=True, default=Any) @@ -33,5 +33,5 @@ index 83e114aac..e9f67877c 100644 @abstractmethod def __len__(self) -> int: ... -- -2.53.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Revert-dict.__or__-typeshed-change.patch b/misc/typeshed_patches/0001-Revert-dict.__or__-typeshed-change.patch index 890b2782abedd..f144e989fb683 100644 --- a/misc/typeshed_patches/0001-Revert-dict.__or__-typeshed-change.patch +++ b/misc/typeshed_patches/0001-Revert-dict.__or__-typeshed-change.patch @@ -1,30 +1,63 @@ -From d88a4b774fc59ecd0f2d0fdda8e0e61092adafd3 Mon Sep 17 00:00:00 2001 +From 92a0169fa37f3ff1c41beb98ae0f61e3b3ff19dc Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 8 Apr 2026 00:01:44 +0100 Subject: [PATCH] Revert dict.__or__ typeshed change --- - mypy/typeshed/stdlib/builtins.pyi | 6 ++++++ - 1 file changed, 6 insertions(+) + mypy/typeshed/stdlib/builtins.pyi | 20 ++++++++++++++++++++ + 1 file changed, 20 insertions(+) diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi -index 674142d70..25b21ba97 100644 +index 9db6f4fc7..7ec0e2d7c 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi -@@ -1142,7 +1142,13 @@ class dict(MutableMapping[_KT, _VT]): - def __reversed__(self) -> Iterator[_KT]: ... +@@ -1228,14 +1228,27 @@ class dict(MutableMapping[_KT, _VT]): __hash__: ClassVar[None] # type: ignore[assignment] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... -+ @overload -+ def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... -+ @overload - def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... -+ @overload -+ def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... -+ @overload - def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + if sys.version_info >= (3, 15): ++ @overload ++ def __or__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> dict[_KT, _VT]: ... ++ @overload + def __or__(self, value: dict[_T1, _T2] | frozendict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + ++ @overload ++ def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload ++ def __ror__(self, value: frozendict[_KT, _VT], /) -> frozendict[_KT, _VT]: ... ++ @overload + def __ror__(self, value: frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + else: ++ @overload ++ def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... ++ @overload + def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... ++ @overload ++ def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... ++ @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + # dict.__ior__ should be kept roughly in line with MutableMapping.update() - @overload # type: ignore[misc] +@@ -1290,11 +1303,18 @@ if sys.version_info >= (3, 15): + def __iter__(self) -> Iterator[_KT]: ... + def __hash__(self) -> int: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... ++ @overload ++ def __or__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> frozendict[_KT, _VT]: ... ++ @overload + def __or__(self, value: dict[_T1, _T2] | frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + ++ @overload ++ def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload ++ def __ror__(self, value: frozendict[_KT, _VT], /) -> frozendict[_KT, _VT]: ... ++ @overload + def __ror__(self, value: frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + + @disjoint_base -- -2.25.1 +2.54.0 diff --git a/misc/typeshed_patches/0001-Revert-operator-changes.patch b/misc/typeshed_patches/0001-Revert-operator-changes.patch index 71b0ae9b47abd..77cdeebb619bf 100644 --- a/misc/typeshed_patches/0001-Revert-operator-changes.patch +++ b/misc/typeshed_patches/0001-Revert-operator-changes.patch @@ -1,14 +1,14 @@ -From 7f38b86464d59188a87ff8c9913c257e788a2f0b Mon Sep 17 00:00:00 2001 +From 5ab958030bd7d4ed9cb3e2afa656b68ea5a07901 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 6 May 2026 19:49:41 -0700 Subject: [PATCH] Revert operator changes --- - mypy/typeshed/stdlib/_operator.pyi | 51 ++++++++---------------------- - 1 file changed, 14 insertions(+), 37 deletions(-) + mypy/typeshed/stdlib/_operator.pyi | 58 ++++++++---------------------- + 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/mypy/typeshed/stdlib/_operator.pyi b/mypy/typeshed/stdlib/_operator.pyi -index e7d85f811..8c705065b 100644 +index 04dae79dc..29a6d7057 100644 --- a/mypy/typeshed/stdlib/_operator.pyi +++ b/mypy/typeshed/stdlib/_operator.pyi @@ -1,15 +1,5 @@ @@ -27,8 +27,8 @@ index e7d85f811..8c705065b 100644 +from _typeshed import SupportsGetItem from collections.abc import Callable, Container, Iterable, MutableMapping, MutableSequence, Sequence from operator import attrgetter as attrgetter, itemgetter as itemgetter, methodcaller as methodcaller - from typing import Any, AnyStr, Protocol, SupportsAbs, SupportsIndex, TypeVar, overload, type_check_only -@@ -18,7 +8,6 @@ from typing_extensions import ParamSpec, TypeAlias, TypeIs + from typing import Any, AnyStr, ParamSpec, Protocol, SupportsAbs, SupportsIndex, TypeAlias, TypeVar, overload, type_check_only +@@ -18,7 +8,6 @@ from typing_extensions import TypeIs _R = TypeVar("_R") _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) @@ -36,14 +36,16 @@ index e7d85f811..8c705065b 100644 _K = TypeVar("_K") _V = TypeVar("_V") _P = ParamSpec("_P") -@@ -69,36 +58,24 @@ def truth(a: object, /) -> bool: ... +@@ -69,43 +58,24 @@ def truth(a: object, /) -> bool: ... def is_(a: object, b: object, /) -> bool: ... def is_not(a: object, b: object, /) -> bool: ... def abs(a: SupportsAbs[_T], /) -> _T: ... +- -@overload -def add(a: SupportsAdd[_T_contra, _T_co], b: _T_contra, /) -> _T_co: ... -@overload -def add(a: _T_contra, b: SupportsRAdd[_T_contra, _T_co], /) -> _T_co: ... +- -def and_(a, b, /): ... -def floordiv(a, b, /): ... +def add(a: Any, b: Any, /) -> Any: ... @@ -53,14 +55,17 @@ index e7d85f811..8c705065b 100644 def inv(a: _SupportsInversion[_T_co], /) -> _T_co: ... def invert(a: _SupportsInversion[_T_co], /) -> _T_co: ... -def lshift(a, b, /): ... +- -@overload -def mod(a: SupportsMod[_T_contra, _T_co], b: _T_contra, /) -> _T_co: ... -@overload -def mod(a: _T_contra, b: SupportsRMod[_T_contra, _T_co], /) -> _T_co: ... +- -@overload -def mul(a: SupportsMul[_T_contra, _T_co], b: _T_contra, /) -> _T_co: ... -@overload -def mul(a: _T_contra, b: SupportsRMul[_T_contra, _T_co], /) -> _T_co: ... +- -def matmul(a, b, /): ... +def lshift(a: Any, b: Any, /) -> Any: ... +def mod(a: Any, b: Any, /) -> Any: ... @@ -72,10 +77,12 @@ index e7d85f811..8c705065b 100644 def pos(a: _SupportsPos[_T_co], /) -> _T_co: ... -def pow(a, b, /): ... -def rshift(a, b, /): ... +- -@overload -def sub(a: SupportsSub[_T_contra, _T_co], b: _T_contra, /) -> _T_co: ... -@overload -def sub(a: _T_contra, b: SupportsRSub[_T_contra, _T_co], /) -> _T_co: ... +- -def truediv(a, b, /): ... -def xor(a, b, /): ... +def pow(a: Any, b: Any, /) -> Any: ... @@ -87,5 +94,5 @@ index e7d85f811..8c705065b 100644 def contains(a: Container[object], b: object, /) -> bool: ... def countOf(a: Iterable[object], b: object, /) -> int: ... -- -2.53.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Revert-sum-literal-integer-change-13961.patch b/misc/typeshed_patches/0001-Revert-sum-literal-integer-change-13961.patch index d05f66ff26108..99e4cb0e8d187 100644 --- a/misc/typeshed_patches/0001-Revert-sum-literal-integer-change-13961.patch +++ b/misc/typeshed_patches/0001-Revert-sum-literal-integer-change-13961.patch @@ -1,4 +1,4 @@ -From 0e30b762e8335f02e19977c055ac7b98e707991c Mon Sep 17 00:00:00 2001 +From a97066922305395b64cbbc1a6f68f7cae60a3aa6 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:47:21 -0700 Subject: [PATCH] Revert sum literal integer change (#13961) @@ -19,10 +19,10 @@ within mypy, I might pursue upstreaming this in typeshed. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi -index 5a3bb5908..0f2196070 100644 +index dc24bc540..6f21960cb 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi -@@ -1835,7 +1835,7 @@ _SupportsSumNoDefaultT = TypeVar("_SupportsSumNoDefaultT", bound=_SupportsSumWit +@@ -2099,7 +2099,7 @@ _SupportsSumNoDefaultT = TypeVar("_SupportsSumNoDefaultT", bound=_SupportsSumWit # without creating many false-positive errors (see #7578). # Instead, we special-case the most common examples of this: bool and literal integers. @overload @@ -32,5 +32,5 @@ index 5a3bb5908..0f2196070 100644 def sum(iterable: Iterable[_SupportsSumNoDefaultT], /) -> _SupportsSumNoDefaultT | Literal[0]: ... @overload -- -2.52.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Revert-typeshed-ctypes-change.patch b/misc/typeshed_patches/0001-Revert-typeshed-ctypes-change.patch index e795be53bce71..a19d87ba8c3f8 100644 --- a/misc/typeshed_patches/0001-Revert-typeshed-ctypes-change.patch +++ b/misc/typeshed_patches/0001-Revert-typeshed-ctypes-change.patch @@ -1,4 +1,4 @@ -From 80a710e0e2c09612c6fc30c644f2052450e69ccd Mon Sep 17 00:00:00 2001 +From 22a35cde63e8e89b91b4b62a20f4ef0fc3f5d3fe Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 1 May 2023 20:34:55 +0100 Subject: [PATCH] Revert typeshed ctypes change @@ -7,26 +7,27 @@ The plugin provides superior type checking: https://github.com/python/mypy/pull/13987#issuecomment-1310863427 A manual cherry-pick of e437cdf. --- - mypy/typeshed/stdlib/_ctypes.pyi | 6 +----- - 1 file changed, 1 insertion(+), 5 deletions(-) + mypy/typeshed/stdlib/_ctypes.pyi | 7 +------ + 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mypy/typeshed/stdlib/_ctypes.pyi b/mypy/typeshed/stdlib/_ctypes.pyi -index be7792818..b7a3fb104 100644 +index d800ac5c2..1efb11926 100644 --- a/mypy/typeshed/stdlib/_ctypes.pyi +++ b/mypy/typeshed/stdlib/_ctypes.pyi -@@ -320,11 +320,7 @@ class Array(_CData, Generic[_CT], metaclass=_PyCArrayType): - def _type_(self) -> type[_CT]: ... +@@ -324,12 +324,7 @@ class Array(_CData, Generic[_CT], metaclass=_PyCArrayType): @_type_.setter def _type_(self, value: type[_CT]) -> None: ... + - # Note: only available if _CT == c_char - @property - def raw(self) -> bytes: ... - @raw.setter - def raw(self, value: ReadableBuffer) -> None: ... +- + raw: bytes # Note: only available if _CT == c_char value: Any # Note: bytes if _CT == c_char, str if _CT == c_wchar, unavailable otherwise # TODO: These methods cannot be annotated correctly at the moment. # All of these "Any"s stand for the array's element type, but it's not possible to use _CT -- -2.52.0 +2.54.0 diff --git a/misc/typeshed_patches/0001-Temporarily-revert-contextlib-deprecation.patch b/misc/typeshed_patches/0001-Temporarily-revert-contextlib-deprecation.patch index 4540e891f9954..fab037c38045e 100644 --- a/misc/typeshed_patches/0001-Temporarily-revert-contextlib-deprecation.patch +++ b/misc/typeshed_patches/0001-Temporarily-revert-contextlib-deprecation.patch @@ -1,4 +1,4 @@ -From ae45edc46f2ca9c939e317648b44ed3372487798 Mon Sep 17 00:00:00 2001 +From 12fb8e77eb5efc5c025d961e04ba7ccef23333bf Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sun, 22 Feb 2026 20:37:48 -0800 Subject: [PATCH] Temporarily revert contextlib deprecation @@ -8,19 +8,19 @@ Subject: [PATCH] Temporarily revert contextlib deprecation 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/mypy/typeshed/stdlib/contextlib.pyi b/mypy/typeshed/stdlib/contextlib.pyi -index 8d5902c46..221102ee2 100644 +index b95c23502..73cdda3b8 100644 --- a/mypy/typeshed/stdlib/contextlib.pyi +++ b/mypy/typeshed/stdlib/contextlib.pyi @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator from types import TracebackType - from typing import Any, Generic, Protocol, TypeVar, overload, runtime_checkable, type_check_only --from typing_extensions import ParamSpec, Self, TypeAlias, deprecated -+from typing_extensions import ParamSpec, Self, TypeAlias + from typing import Any, Generic, ParamSpec, Protocol, TypeAlias, TypeVar, overload, runtime_checkable, type_check_only +-from typing_extensions import Self, deprecated ++from typing_extensions import Self __all__ = [ - "contextmanager", -@@ -86,12 +86,6 @@ class _GeneratorContextManager( + "aclosing", +@@ -84,12 +84,6 @@ class _GeneratorContextManager( self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None ) -> bool | None: ... @@ -32,10 +32,10 @@ index 8d5902c46..221102ee2 100644 -) def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... - if sys.version_info >= (3, 10): -@@ -118,13 +112,6 @@ else: - self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool | None: ... + _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) +@@ -107,13 +101,6 @@ class _AsyncGeneratorContextManager( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... -@overload -def asynccontextmanager(func: Callable[_P, AsyncGenerator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... @@ -45,8 +45,8 @@ index 8d5902c46..221102ee2 100644 - "Use `-> AsyncGenerator[Foo]` instead." -) def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... + @type_check_only - class _SupportsClose(Protocol): -- -2.53.0 +2.54.0 diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 71555c03b9ad4..858fd860fd01d 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -3135,7 +3135,6 @@ def test_get_typeshed_stdlib_modules(self) -> None: assert "os.path" in stdlib assert "asyncio" in stdlib assert "graphlib" not in stdlib - assert "formatter" in stdlib assert "contextvars" in stdlib # 3.7+ assert "importlib.metadata" not in stdlib diff --git a/mypy/typeshed/stdlib/VERSIONS b/mypy/typeshed/stdlib/VERSIONS index e26bb8da2574e..e9c8d91fdbd71 100644 --- a/mypy/typeshed/stdlib/VERSIONS +++ b/mypy/typeshed/stdlib/VERSIONS @@ -23,7 +23,6 @@ _ast: 3.0- _asyncio: 3.0- _bisect: 3.0- _blake2: 3.6- -_bootlocale: 3.4-3.9 _bz2: 3.3- _codecs: 3.0- _collections_abc: 3.3- @@ -61,6 +60,7 @@ _py_abc: 3.7- _pydecimal: 3.5- _queue: 3.7- _random: 3.0- +_remote_debugging: 3.15- _sitebuiltins: 3.4- _socket: 3.0- # present in 3.0 at runtime, but not in typeshed _sqlite3: 3.0- @@ -143,7 +143,6 @@ difflib: 3.0- dis: 3.0- distutils: 3.0-3.11 distutils.command.bdist_msi: 3.0-3.10 -distutils.command.bdist_wininst: 3.0-3.9 doctest: 3.0- email: 3.0- encodings: 3.0- @@ -160,7 +159,6 @@ fcntl: 3.0- filecmp: 3.0- fileinput: 3.0- fnmatch: 3.0- -formatter: 3.0-3.9 fractions: 3.0- ftplib: 3.0- functools: 3.0- @@ -211,6 +209,7 @@ mailbox: 3.0- mailcap: 3.0-3.12 marshal: 3.0- math: 3.0- +math.integer: 3.15- mimetypes: 3.0- mmap: 3.0- modulefinder: 3.0- @@ -231,7 +230,6 @@ operator: 3.0- optparse: 3.0- os: 3.0- ossaudiodev: 3.0-3.12 -parser: 3.0-3.9 pathlib: 3.4- pathlib.types: 3.14- pdb: 3.0- @@ -246,6 +244,9 @@ posix: 3.0- posixpath: 3.0- pprint: 3.0- profile: 3.0- +profiling: 3.15- +profiling.sampling: 3.15- +profiling.tracing: 3.15- pstats: 3.0- pty: 3.0- pwd: 3.0- @@ -280,9 +281,9 @@ socket: 3.0- socketserver: 3.0- spwd: 3.0-3.12 sqlite3: 3.0- -sre_compile: 3.0- -sre_constants: 3.0- -sre_parse: 3.0- +sre_compile: 3.0-3.14 +sre_constants: 3.0-3.14 +sre_parse: 3.0-3.14 ssl: 3.0- stat: 3.0- statistics: 3.4- @@ -292,9 +293,9 @@ stringprep: 3.0- struct: 3.0- subprocess: 3.0- sunau: 3.0-3.12 -symbol: 3.0-3.9 symtable: 3.0- sys: 3.0- +sys.__jit: 3.14- # Similar to sys._monitoring sys._monitoring: 3.12- # Doesn't actually exist. See comments in the stub. sysconfig: 3.0- syslog: 3.0- @@ -339,6 +340,7 @@ wsgiref: 3.0- wsgiref.types: 3.11- xdrlib: 3.0-3.12 xml: 3.0- +xml.utils: 3.15- xmlrpc: 3.0- xxlimited: 3.2- zipapp: 3.5- diff --git a/mypy/typeshed/stdlib/_ast.pyi b/mypy/typeshed/stdlib/_ast.pyi index d8d5a1829991e..fd89973aefe65 100644 --- a/mypy/typeshed/stdlib/_ast.pyi +++ b/mypy/typeshed/stdlib/_ast.pyi @@ -58,6 +58,15 @@ from ast import ( LShift as LShift, Lt as Lt, LtE as LtE, + Match as Match, + MatchAs as MatchAs, + MatchClass as MatchClass, + MatchMapping as MatchMapping, + MatchOr as MatchOr, + MatchSequence as MatchSequence, + MatchSingleton as MatchSingleton, + MatchStar as MatchStar, + MatchValue as MatchValue, MatMult as MatMult, Mod as Mod, Module as Module, @@ -101,8 +110,10 @@ from ast import ( expr as expr, expr_context as expr_context, keyword as keyword, + match_case as match_case, mod as mod, operator as operator, + pattern as pattern, stmt as stmt, type_ignore as type_ignore, unaryop as unaryop, @@ -122,21 +133,6 @@ if sys.version_info >= (3, 12): if sys.version_info >= (3, 11): from ast import TryStar as TryStar -if sys.version_info >= (3, 10): - from ast import ( - Match as Match, - MatchAs as MatchAs, - MatchClass as MatchClass, - MatchMapping as MatchMapping, - MatchOr as MatchOr, - MatchSequence as MatchSequence, - MatchSingleton as MatchSingleton, - MatchStar as MatchStar, - MatchValue as MatchValue, - match_case as match_case, - pattern as pattern, - ) - PyCF_ALLOW_TOP_LEVEL_AWAIT: Final = 8192 PyCF_ONLY_AST: Final = 1024 PyCF_TYPE_COMMENTS: Final = 4096 diff --git a/mypy/typeshed/stdlib/_asyncio.pyi b/mypy/typeshed/stdlib/_asyncio.pyi index f43178e4d7258..1e36b515d408a 100644 --- a/mypy/typeshed/stdlib/_asyncio.pyi +++ b/mypy/typeshed/stdlib/_asyncio.pyi @@ -3,8 +3,8 @@ from asyncio.events import AbstractEventLoop from collections.abc import Awaitable, Callable, Coroutine, Generator, Iterable from contextvars import Context from types import FrameType, GenericAlias -from typing import Any, Literal, TextIO, TypeVar -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import Any, Literal, TextIO, TypeAlias, TypeVar +from typing_extensions import Self, disjoint_base _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) @@ -16,10 +16,12 @@ class Future(Awaitable[_T], Iterable[_T]): @property def _exception(self) -> BaseException | None: ... _blocking: bool + @property def _log_traceback(self) -> bool: ... @_log_traceback.setter def _log_traceback(self, val: Literal[False]) -> None: ... + _asyncio_future_blocking: bool # is a part of duck-typing contract for `Future` def __init__(self, *, loop: AbstractEventLoop | None = None) -> None: ... def __del__(self) -> None: ... @@ -97,7 +99,7 @@ class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportIn def get_event_loop() -> AbstractEventLoop: ... def get_running_loop() -> AbstractEventLoop: ... def _set_running_loop(loop: AbstractEventLoop | None, /) -> None: ... -def _get_running_loop() -> AbstractEventLoop: ... +def _get_running_loop() -> AbstractEventLoop | None: ... def _register_task(task: Task[Any]) -> None: ... def _unregister_task(task: Task[Any]) -> None: ... def _enter_task(loop: AbstractEventLoop, task: Task[Any]) -> None: ... diff --git a/mypy/typeshed/stdlib/_bisect.pyi b/mypy/typeshed/stdlib/_bisect.pyi index 5ac43b3e06414..b87dd8f3fb871 100644 --- a/mypy/typeshed/stdlib/_bisect.pyi +++ b/mypy/typeshed/stdlib/_bisect.pyi @@ -1,140 +1,103 @@ -import sys from _typeshed import SupportsGetItem, SupportsLenAndGetItem, SupportsRichComparisonT from collections.abc import Callable, MutableSequence from typing import TypeVar, overload _T = TypeVar("_T") -if sys.version_info >= (3, 10): - @overload - def bisect_left( - a: SupportsLenAndGetItem[SupportsRichComparisonT], - x: SupportsRichComparisonT, - lo: int = 0, - hi: int | None = None, - *, - key: None = None, - ) -> int: ... - @overload - def bisect_left( - a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int, hi: int, *, key: None = None - ) -> int: ... - @overload - def bisect_left( - a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, *, hi: int, key: None = None - ) -> int: ... - @overload - def bisect_left( - a: SupportsLenAndGetItem[_T], - x: SupportsRichComparisonT, - lo: int = 0, - hi: int | None = None, - *, - key: Callable[[_T], SupportsRichComparisonT], - ) -> int: ... - @overload - def bisect_left( - a: SupportsGetItem[int, _T], x: SupportsRichComparisonT, lo: int, hi: int, *, key: Callable[[_T], SupportsRichComparisonT] - ) -> int: ... - @overload - def bisect_left( - a: SupportsGetItem[int, _T], - x: SupportsRichComparisonT, - lo: int = 0, - *, - hi: int, - key: Callable[[_T], SupportsRichComparisonT], - ) -> int: ... - @overload - def bisect_right( - a: SupportsLenAndGetItem[SupportsRichComparisonT], - x: SupportsRichComparisonT, - lo: int = 0, - hi: int | None = None, - *, - key: None = None, - ) -> int: ... - @overload - def bisect_right( - a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int, hi: int, *, key: None = None - ) -> int: ... - @overload - def bisect_right( - a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, *, hi: int, key: None = None - ) -> int: ... - @overload - def bisect_right( - a: SupportsLenAndGetItem[_T], - x: SupportsRichComparisonT, - lo: int = 0, - hi: int | None = None, - *, - key: Callable[[_T], SupportsRichComparisonT], - ) -> int: ... - @overload - def bisect_right( - a: SupportsGetItem[int, _T], x: SupportsRichComparisonT, lo: int, hi: int, *, key: Callable[[_T], SupportsRichComparisonT] - ) -> int: ... - @overload - def bisect_right( - a: SupportsGetItem[int, _T], - x: SupportsRichComparisonT, - lo: int = 0, - *, - hi: int, - key: Callable[[_T], SupportsRichComparisonT], - ) -> int: ... - @overload - def insort_left( - a: MutableSequence[SupportsRichComparisonT], - x: SupportsRichComparisonT, - lo: int = 0, - hi: int | None = None, - *, - key: None = None, - ) -> None: ... - @overload - def insort_left( - a: MutableSequence[_T], x: _T, lo: int = 0, hi: int | None = None, *, key: Callable[[_T], SupportsRichComparisonT] - ) -> None: ... - @overload - def insort_right( - a: MutableSequence[SupportsRichComparisonT], - x: SupportsRichComparisonT, - lo: int = 0, - hi: int | None = None, - *, - key: None = None, - ) -> None: ... - @overload - def insort_right( - a: MutableSequence[_T], x: _T, lo: int = 0, hi: int | None = None, *, key: Callable[[_T], SupportsRichComparisonT] - ) -> None: ... +@overload +def bisect_left( + a: SupportsLenAndGetItem[SupportsRichComparisonT], + x: SupportsRichComparisonT, + lo: int = 0, + hi: int | None = None, + *, + key: None = None, +) -> int: ... +@overload +def bisect_left( + a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int, hi: int, *, key: None = None +) -> int: ... +@overload +def bisect_left( + a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, *, hi: int, key: None = None +) -> int: ... +@overload +def bisect_left( + a: SupportsLenAndGetItem[_T], + x: SupportsRichComparisonT, + lo: int = 0, + hi: int | None = None, + *, + key: Callable[[_T], SupportsRichComparisonT], +) -> int: ... +@overload +def bisect_left( + a: SupportsGetItem[int, _T], x: SupportsRichComparisonT, lo: int, hi: int, *, key: Callable[[_T], SupportsRichComparisonT] +) -> int: ... +@overload +def bisect_left( + a: SupportsGetItem[int, _T], x: SupportsRichComparisonT, lo: int = 0, *, hi: int, key: Callable[[_T], SupportsRichComparisonT] +) -> int: ... -else: - @overload - def bisect_left( - a: SupportsLenAndGetItem[SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, hi: int | None = None - ) -> int: ... - @overload - def bisect_left(a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int, hi: int) -> int: ... - @overload - def bisect_left( - a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, *, hi: int - ) -> int: ... - @overload - def bisect_right( - a: SupportsLenAndGetItem[SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, hi: int | None = None - ) -> int: ... - @overload - def bisect_right(a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int, hi: int) -> int: ... - @overload - def bisect_right( - a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, *, hi: int - ) -> int: ... - def insort_left( - a: MutableSequence[SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, hi: int | None = None - ) -> None: ... - def insort_right( - a: MutableSequence[SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, hi: int | None = None - ) -> None: ... +@overload +def bisect_right( + a: SupportsLenAndGetItem[SupportsRichComparisonT], + x: SupportsRichComparisonT, + lo: int = 0, + hi: int | None = None, + *, + key: None = None, +) -> int: ... +@overload +def bisect_right( + a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int, hi: int, *, key: None = None +) -> int: ... +@overload +def bisect_right( + a: SupportsGetItem[int, SupportsRichComparisonT], x: SupportsRichComparisonT, lo: int = 0, *, hi: int, key: None = None +) -> int: ... +@overload +def bisect_right( + a: SupportsLenAndGetItem[_T], + x: SupportsRichComparisonT, + lo: int = 0, + hi: int | None = None, + *, + key: Callable[[_T], SupportsRichComparisonT], +) -> int: ... +@overload +def bisect_right( + a: SupportsGetItem[int, _T], x: SupportsRichComparisonT, lo: int, hi: int, *, key: Callable[[_T], SupportsRichComparisonT] +) -> int: ... +@overload +def bisect_right( + a: SupportsGetItem[int, _T], x: SupportsRichComparisonT, lo: int = 0, *, hi: int, key: Callable[[_T], SupportsRichComparisonT] +) -> int: ... + +@overload +def insort_left( + a: MutableSequence[SupportsRichComparisonT], + x: SupportsRichComparisonT, + lo: int = 0, + hi: int | None = None, + *, + key: None = None, +) -> None: ... +@overload +def insort_left( + a: MutableSequence[_T], x: _T, lo: int = 0, hi: int | None = None, *, key: Callable[[_T], SupportsRichComparisonT] +) -> None: ... + +@overload +def insort_right( + a: MutableSequence[SupportsRichComparisonT], + x: SupportsRichComparisonT, + lo: int = 0, + hi: int | None = None, + *, + key: None = None, +) -> None: ... +@overload +def insort_right( + a: MutableSequence[_T], x: _T, lo: int = 0, hi: int | None = None, *, key: Callable[[_T], SupportsRichComparisonT] +) -> None: ... diff --git a/mypy/typeshed/stdlib/_bootlocale.pyi b/mypy/typeshed/stdlib/_bootlocale.pyi deleted file mode 100644 index 233d4934f3c6d..0000000000000 --- a/mypy/typeshed/stdlib/_bootlocale.pyi +++ /dev/null @@ -1 +0,0 @@ -def getpreferredencoding(do_setlocale: bool = True) -> str: ... diff --git a/mypy/typeshed/stdlib/_codecs.pyi b/mypy/typeshed/stdlib/_codecs.pyi index 89cb78c33571d..38cf857d05691 100644 --- a/mypy/typeshed/stdlib/_codecs.pyi +++ b/mypy/typeshed/stdlib/_codecs.pyi @@ -2,8 +2,7 @@ import codecs import sys from _typeshed import ReadableBuffer from collections.abc import Callable -from typing import Literal, final, overload, type_check_only -from typing_extensions import TypeAlias +from typing import Literal, TypeAlias, final, overload, type_check_only # This type is not exposed; it is defined in unicodeobject.c # At runtime it calls itself builtins.EncodingMap @@ -17,10 +16,7 @@ _Handler: TypeAlias = Callable[[UnicodeError], tuple[str | bytes, int]] _SearchFunction: TypeAlias = Callable[[str], codecs.CodecInfo | None] def register(search_function: _SearchFunction, /) -> None: ... - -if sys.version_info >= (3, 10): - def unregister(search_function: _SearchFunction, /) -> None: ... - +def unregister(search_function: _SearchFunction, /) -> None: ... def register_error(errors: str, handler: _Handler, /) -> None: ... def lookup_error(name: str, /) -> _Handler: ... @@ -53,6 +49,7 @@ def encode(obj: ReadableBuffer, encoding: _BytesToBytesEncoding, errors: str = " def encode(obj: str, encoding: _StrToStrEncoding, errors: str = "strict") -> str: ... # type: ignore[overload-overlap] @overload def encode(obj: str, encoding: str = "utf-8", errors: str = "strict") -> bytes: ... + @overload def decode(obj: ReadableBuffer, encoding: _BytesToBytesEncoding, errors: str = "strict") -> bytes: ... # type: ignore[overload-overlap] @overload @@ -71,6 +68,7 @@ def decode( def decode(obj: str, encoding: Literal["hex", "hex_codec"], errors: str = "strict") -> bytes: ... @overload def decode(obj: ReadableBuffer, encoding: str = "utf-8", errors: str = "strict") -> str: ... + def lookup(encoding: str, /) -> codecs.CodecInfo: ... def charmap_build(map: str, /) -> _CharMap: ... def ascii_decode(data: ReadableBuffer, errors: str | None = None, /) -> tuple[str, int]: ... diff --git a/mypy/typeshed/stdlib/_collections_abc.pyi b/mypy/typeshed/stdlib/_collections_abc.pyi index 0fa81662dfbc1..6fc32078532e0 100644 --- a/mypy/typeshed/stdlib/_collections_abc.pyi +++ b/mypy/typeshed/stdlib/_collections_abc.pyi @@ -1,7 +1,7 @@ import sys from abc import abstractmethod from types import MappingProxyType -from typing import ( # noqa: Y022,Y038,UP035,Y057,RUF100 +from typing import ( # noqa: Y022,Y038,UP035,Y057 AbstractSet as Set, AsyncGenerator as AsyncGenerator, AsyncIterable as AsyncIterable, @@ -60,8 +60,9 @@ __all__ = [ "ValuesView", "Sequence", "MutableSequence", - "ByteString", ] +if sys.version_info < (3, 15): + __all__ += ["ByteString"] if sys.version_info >= (3, 12): __all__ += ["Buffer"] @@ -75,16 +76,15 @@ class dict_keys(KeysView[_KT_co], Generic[_KT_co, _VT_co]): # undocumented __hash__: ClassVar[None] # type: ignore[assignment] if sys.version_info >= (3, 13): def isdisjoint(self, other: Iterable[_KT_co], /) -> bool: ... - if sys.version_info >= (3, 10): - @property - def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ... + + @property + def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ... @final class dict_values(ValuesView[_VT_co], Generic[_KT_co, _VT_co]): # undocumented def __reversed__(self) -> Iterator[_VT_co]: ... - if sys.version_info >= (3, 10): - @property - def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ... + @property + def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ... @final class dict_items(ItemsView[_KT_co, _VT_co]): # undocumented @@ -93,9 +93,9 @@ class dict_items(ItemsView[_KT_co, _VT_co]): # undocumented __hash__: ClassVar[None] # type: ignore[assignment] if sys.version_info >= (3, 13): def isdisjoint(self, other: Iterable[tuple[_KT_co, _VT_co]], /) -> bool: ... - if sys.version_info >= (3, 10): - @property - def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ... + + @property + def mapping(self) -> MappingProxyType[_KT_co, _VT_co]: ... if sys.version_info >= (3, 12): @runtime_checkable diff --git a/mypy/typeshed/stdlib/_contextvars.pyi b/mypy/typeshed/stdlib/_contextvars.pyi index 0ddeca7882cd1..56440884a1508 100644 --- a/mypy/typeshed/stdlib/_contextvars.pyi +++ b/mypy/typeshed/stdlib/_contextvars.pyi @@ -1,8 +1,8 @@ import sys from collections.abc import Callable, Iterator, Mapping from types import GenericAlias, TracebackType -from typing import Any, ClassVar, Generic, TypeVar, final, overload -from typing_extensions import ParamSpec, Self +from typing import Any, ClassVar, Generic, ParamSpec, TypeVar, final, overload +from typing_extensions import Self _T = TypeVar("_T") _D = TypeVar("_D") @@ -14,15 +14,18 @@ class ContextVar(Generic[_T]): def __new__(cls, name: str) -> Self: ... @overload def __new__(cls, name: str, *, default: _T) -> Self: ... + def __hash__(self) -> int: ... @property def name(self) -> str: ... + @overload def get(self) -> _T: ... @overload def get(self, default: _T, /) -> _T: ... @overload def get(self, default: _D, /) -> _D | _T: ... + def set(self, value: _T, /) -> Token[_T]: ... def reset(self, token: Token[_T], /) -> None: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @@ -49,12 +52,14 @@ def copy_context() -> Context: ... @final class Context(Mapping[ContextVar[Any], Any]): def __init__(self) -> None: ... + @overload def get(self, key: ContextVar[_T], default: None = None, /) -> _T | None: ... @overload def get(self, key: ContextVar[_T], default: _T, /) -> _T: ... @overload def get(self, key: ContextVar[_T], default: _D, /) -> _T | _D: ... + def run(self, callable: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: ... def copy(self) -> Context: ... __hash__: ClassVar[None] # type: ignore[assignment] diff --git a/mypy/typeshed/stdlib/_csv.pyi b/mypy/typeshed/stdlib/_csv.pyi index ea90766afee66..d3702be78c3d5 100644 --- a/mypy/typeshed/stdlib/_csv.pyi +++ b/mypy/typeshed/stdlib/_csv.pyi @@ -2,8 +2,8 @@ import csv import sys from _typeshed import SupportsWrite from collections.abc import Iterable -from typing import Any, Final, Literal, type_check_only -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import Any, Final, Literal, TypeAlias +from typing_extensions import Self, disjoint_base __version__: Final[str] @@ -47,47 +47,24 @@ class Dialect: strict: bool = False, ) -> Self: ... -if sys.version_info >= (3, 10): - # This class calls itself _csv.reader. - @disjoint_base - class Reader: - @property - def dialect(self) -> Dialect: ... - line_num: int - def __iter__(self) -> Self: ... - def __next__(self) -> list[str]: ... - - # This class calls itself _csv.writer. - @disjoint_base - class Writer: - @property - def dialect(self) -> Dialect: ... - if sys.version_info >= (3, 13): - def writerow(self, row: Iterable[Any], /) -> Any: ... - def writerows(self, rows: Iterable[Iterable[Any]], /) -> None: ... - else: - def writerow(self, row: Iterable[Any]) -> Any: ... - def writerows(self, rows: Iterable[Iterable[Any]]) -> None: ... - - # For the return types below. - # These aliases can be removed when typeshed drops support for 3.9. - _reader = Reader - _writer = Writer -else: - # This class is not exposed. It calls itself _csv.reader. - @type_check_only - class _reader: - @property - def dialect(self) -> Dialect: ... - line_num: int - def __iter__(self) -> Self: ... - def __next__(self) -> list[str]: ... +# This class calls itself _csv.reader. +@disjoint_base +class Reader: + @property + def dialect(self) -> Dialect: ... + line_num: int + def __iter__(self) -> Self: ... + def __next__(self) -> list[str]: ... - # This class is not exposed. It calls itself _csv.writer. - @type_check_only - class _writer: - @property - def dialect(self) -> Dialect: ... +# This class calls itself _csv.writer. +@disjoint_base +class Writer: + @property + def dialect(self) -> Dialect: ... + if sys.version_info >= (3, 13): + def writerow(self, row: Iterable[Any], /) -> Any: ... + def writerows(self, rows: Iterable[Iterable[Any]], /) -> None: ... + else: def writerow(self, row: Iterable[Any]) -> Any: ... def writerows(self, rows: Iterable[Iterable[Any]]) -> None: ... @@ -104,7 +81,7 @@ def writer( lineterminator: str = "\r\n", quoting: _QuotingType = 0, strict: bool = False, -) -> _writer: ... +) -> Writer: ... def reader( iterable: Iterable[str], /, @@ -118,7 +95,7 @@ def reader( lineterminator: str = "\r\n", quoting: _QuotingType = 0, strict: bool = False, -) -> _reader: ... +) -> Reader: ... def register_dialect( name: str, /, diff --git a/mypy/typeshed/stdlib/_ctypes.pyi b/mypy/typeshed/stdlib/_ctypes.pyi index 0e02092a361c9..1efb119263ef1 100644 --- a/mypy/typeshed/stdlib/_ctypes.pyi +++ b/mypy/typeshed/stdlib/_ctypes.pyi @@ -6,8 +6,8 @@ from abc import abstractmethod from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from ctypes import CDLL, ArgumentError as ArgumentError, c_void_p from types import GenericAlias -from typing import Any, ClassVar, Final, Generic, Literal, SupportsIndex, TypeVar, final, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, ClassVar, Final, Generic, Literal, SupportsIndex, TypeAlias, TypeVar, final, overload, type_check_only +from typing_extensions import Self _T = TypeVar("_T") _CT = TypeVar("_CT", bound=_CData) @@ -127,14 +127,17 @@ class _PyCPointerType(_CTypeBaseType): class _Pointer(_PointerLike, _CData, Generic[_CT], metaclass=_PyCPointerType): _type_: type[_CT] contents: _CT + @overload def __init__(self) -> None: ... @overload def __init__(self, arg: _CT) -> None: ... + @overload def __getitem__(self, key: int, /) -> Any: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> list[Any]: ... + def __setitem__(self, key: int, value: Any, /) -> None: ... if sys.version_info < (3, 14): @@ -142,6 +145,7 @@ if sys.version_info < (3, 14): def POINTER(type: None, /) -> type[c_void_p]: ... @overload def POINTER(type: type[_CT], /) -> type[_Pointer[_CT]]: ... + def pointer(obj: _CT, /) -> _Pointer[_CT]: ... # This class is not exposed. It calls itself _ctypes.CArgObject. @@ -177,6 +181,7 @@ class CFuncPtr(_PointerLike, _CData, metaclass=_PyCFuncPtrType): errcheck: _ECT # Abstract attribute that must be defined on subclasses _flags_: ClassVar[int] + @overload def __new__(cls) -> Self: ... @overload @@ -209,10 +214,12 @@ if sys.version_info >= (3, 14): bit_offset: int bit_size: int is_anonymous: bool + @overload def __get__(self, instance: None, owner: builtins.type[Any] | None = None, /) -> Self: ... @overload def __get__(self, instance: Any, owner: builtins.type[Any] | None = None, /) -> _GetT: ... + def __set__(self, instance: Any, value: _SetT, /) -> None: ... _CField = CField @@ -223,16 +230,11 @@ else: class _CField(Generic[_CT, _GetT, _SetT]): offset: int size: int - if sys.version_info >= (3, 10): - @overload - def __get__(self, instance: None, owner: type[Any] | None = None, /) -> Self: ... - @overload - def __get__(self, instance: Any, owner: type[Any] | None = None, /) -> _GetT: ... - else: - @overload - def __get__(self, instance: None, owner: type[Any] | None, /) -> Self: ... - @overload - def __get__(self, instance: Any, owner: type[Any] | None, /) -> _GetT: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None, /) -> Self: ... + @overload + def __get__(self, instance: Any, owner: type[Any] | None = None, /) -> _GetT: ... def __set__(self, instance: Any, value: _SetT, /) -> None: ... @@ -315,11 +317,13 @@ class Array(_CData, Generic[_CT], metaclass=_PyCArrayType): def _length_(self) -> int: ... @_length_.setter def _length_(self, value: int) -> None: ... + @property @abstractmethod def _type_(self) -> type[_CT]: ... @_type_.setter def _type_(self, value: type[_CT]) -> None: ... + raw: bytes # Note: only available if _CT == c_char value: Any # Note: bytes if _CT == c_char, str if _CT == c_wchar, unavailable otherwise # TODO: These methods cannot be annotated correctly at the moment. @@ -335,14 +339,17 @@ class Array(_CData, Generic[_CT], metaclass=_PyCArrayType): # This special behavior is not easy to model in a stub, so for now all places where # the array element type would belong are annotated with Any instead. def __init__(self, *args: Any) -> None: ... + @overload def __getitem__(self, key: int, /) -> Any: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> list[Any]: ... + @overload def __setitem__(self, key: int, value: Any, /) -> None: ... @overload def __setitem__(self, key: slice[SupportsIndex | None], value: Iterable[Any], /) -> None: ... + def __iter__(self) -> Iterator[Any]: ... # Can't inherit from Sized because the metaclass conflict between # Sized and _CData prevents using _CDataMeta. diff --git a/mypy/typeshed/stdlib/_curses.pyi b/mypy/typeshed/stdlib/_curses.pyi index d4e4d48f4e20f..449cf75dad422 100644 --- a/mypy/typeshed/stdlib/_curses.pyi +++ b/mypy/typeshed/stdlib/_curses.pyi @@ -1,8 +1,7 @@ import sys from _typeshed import ReadOnlyBuffer, SupportsRead, SupportsWrite from curses import _ncurses_version -from typing import Any, Final, final, overload -from typing_extensions import TypeAlias +from typing import Any, Final, TypeAlias, final, overload # NOTE: This module is ordinarily only available on Unix, but the windows-curses # package makes it available on Windows as well with the same contents. @@ -96,13 +95,12 @@ BUTTON4_PRESSED: Final[int] BUTTON4_RELEASED: Final[int] BUTTON4_TRIPLE_CLICKED: Final[int] # Darwin ncurses doesn't provide BUTTON5_* constants prior to 3.12.10 and 3.13.3 -if sys.version_info >= (3, 10): - if sys.version_info >= (3, 12) or sys.platform != "darwin": - BUTTON5_PRESSED: Final[int] - BUTTON5_RELEASED: Final[int] - BUTTON5_CLICKED: Final[int] - BUTTON5_DOUBLE_CLICKED: Final[int] - BUTTON5_TRIPLE_CLICKED: Final[int] +if sys.version_info >= (3, 12) or sys.platform != "darwin": + BUTTON5_PRESSED: Final[int] + BUTTON5_RELEASED: Final[int] + BUTTON5_CLICKED: Final[int] + BUTTON5_DOUBLE_CLICKED: Final[int] + BUTTON5_TRIPLE_CLICKED: Final[int] BUTTON_ALT: Final[int] BUTTON_CTRL: Final[int] BUTTON_SHIFT: Final[int] @@ -300,9 +298,7 @@ def getsyx() -> tuple[int, int]: ... def getwin(file: SupportsRead[bytes], /) -> window: ... def halfdelay(tenths: int, /) -> None: ... def has_colors() -> bool: ... - -if sys.version_info >= (3, 10): - def has_extended_color_support() -> bool: ... +def has_extended_color_support() -> bool: ... if sys.version_info >= (3, 14): def assume_default_colors(fg: int, bg: int, /) -> None: ... @@ -379,18 +375,22 @@ class error(Exception): ... @final class window: # undocumented encoding: str + @overload def addch(self, ch: _ChType, attr: int = ...) -> None: ... @overload def addch(self, y: int, x: int, ch: _ChType, attr: int = ...) -> None: ... + @overload def addnstr(self, str: str, n: int, attr: int = ...) -> None: ... @overload def addnstr(self, y: int, x: int, str: str, n: int, attr: int = ...) -> None: ... + @overload def addstr(self, str: str, attr: int = ...) -> None: ... @overload def addstr(self, y: int, x: int, str: str, attr: int = ...) -> None: ... + def attroff(self, attr: int, /) -> None: ... def attron(self, attr: int, /) -> None: ... def attrset(self, attr: int, /) -> None: ... @@ -407,10 +407,12 @@ class window: # undocumented bl: _ChType = ..., br: _ChType = ..., ) -> None: ... + @overload def box(self) -> None: ... @overload def box(self, vertch: _ChType = 0, horch: _ChType = 0) -> None: ... + @overload def chgat(self, attr: int) -> None: ... @overload @@ -419,39 +421,49 @@ class window: # undocumented def chgat(self, y: int, x: int, attr: int) -> None: ... @overload def chgat(self, y: int, x: int, num: int, attr: int) -> None: ... + def clear(self) -> None: ... def clearok(self, yes: int) -> None: ... def clrtobot(self) -> None: ... def clrtoeol(self) -> None: ... def cursyncup(self) -> None: ... + @overload def delch(self) -> None: ... @overload def delch(self, y: int, x: int) -> None: ... + def deleteln(self) -> None: ... + @overload def derwin(self, begin_y: int, begin_x: int) -> window: ... @overload def derwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ... + def echochar(self, ch: _ChType, attr: int = 0, /) -> None: ... def enclose(self, y: int, x: int, /) -> bool: ... def erase(self) -> None: ... def getbegyx(self) -> tuple[int, int]: ... def getbkgd(self) -> tuple[int, int]: ... + @overload def getch(self) -> int: ... @overload def getch(self, y: int, x: int) -> int: ... + @overload def get_wch(self) -> int | str: ... @overload def get_wch(self, y: int, x: int) -> int | str: ... + @overload def getkey(self) -> str: ... @overload def getkey(self, y: int, x: int) -> str: ... + def getmaxyx(self) -> tuple[int, int]: ... def getparyx(self) -> tuple[int, int]: ... + @overload def getstr(self) -> bytes: ... @overload @@ -460,36 +472,46 @@ class window: # undocumented def getstr(self, y: int, x: int) -> bytes: ... @overload def getstr(self, y: int, x: int, n: int) -> bytes: ... + def getyx(self) -> tuple[int, int]: ... + @overload def hline(self, ch: _ChType, n: int) -> None: ... @overload def hline(self, y: int, x: int, ch: _ChType, n: int) -> None: ... + def idcok(self, flag: bool) -> None: ... def idlok(self, yes: bool) -> None: ... def immedok(self, flag: bool) -> None: ... + @overload def inch(self) -> int: ... @overload def inch(self, y: int, x: int) -> int: ... + @overload def insch(self, ch: _ChType, attr: int = ...) -> None: ... @overload def insch(self, y: int, x: int, ch: _ChType, attr: int = ...) -> None: ... + def insdelln(self, nlines: int) -> None: ... def insertln(self) -> None: ... + @overload def insnstr(self, str: str, n: int, attr: int = ...) -> None: ... @overload def insnstr(self, y: int, x: int, str: str, n: int, attr: int = ...) -> None: ... + @overload def insstr(self, str: str, attr: int = ...) -> None: ... @overload def insstr(self, y: int, x: int, str: str, attr: int = ...) -> None: ... + @overload def instr(self, n: int = 2047) -> bytes: ... @overload def instr(self, y: int, x: int, n: int = 2047) -> bytes: ... + def is_linetouched(self, line: int, /) -> bool: ... def is_wintouched(self) -> bool: ... def keypad(self, yes: bool, /) -> None: ... @@ -499,43 +521,52 @@ class window: # undocumented def mvwin(self, new_y: int, new_x: int) -> None: ... def nodelay(self, yes: bool) -> None: ... def notimeout(self, yes: bool) -> None: ... + @overload def noutrefresh(self) -> None: ... @overload def noutrefresh(self, pminrow: int, pmincol: int, sminrow: int, smincol: int, smaxrow: int, smaxcol: int) -> None: ... + @overload def overlay(self, destwin: window) -> None: ... @overload def overlay( self, destwin: window, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int ) -> None: ... + @overload def overwrite(self, destwin: window) -> None: ... @overload def overwrite( self, destwin: window, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int ) -> None: ... + def putwin(self, file: SupportsWrite[bytes], /) -> None: ... def redrawln(self, beg: int, num: int, /) -> None: ... def redrawwin(self) -> None: ... + @overload def refresh(self) -> None: ... @overload def refresh(self, pminrow: int, pmincol: int, sminrow: int, smincol: int, smaxrow: int, smaxcol: int) -> None: ... + def resize(self, nlines: int, ncols: int) -> None: ... def scroll(self, lines: int = 1) -> None: ... def scrollok(self, flag: bool) -> None: ... def setscrreg(self, top: int, bottom: int, /) -> None: ... def standend(self) -> None: ... def standout(self) -> None: ... + @overload def subpad(self, begin_y: int, begin_x: int) -> window: ... @overload def subpad(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ... + @overload def subwin(self, begin_y: int, begin_x: int) -> window: ... @overload def subwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ... + def syncdown(self) -> None: ... def syncok(self, flag: bool) -> None: ... def syncup(self) -> None: ... @@ -543,6 +574,7 @@ class window: # undocumented def touchline(self, start: int, count: int, changed: bool = True) -> None: ... def touchwin(self) -> None: ... def untouchwin(self) -> None: ... + @overload def vline(self, ch: _ChType, n: int) -> None: ... @overload diff --git a/mypy/typeshed/stdlib/_dbm.pyi b/mypy/typeshed/stdlib/_dbm.pyi index 222c3ffcb246b..29d9b4c2fadca 100644 --- a/mypy/typeshed/stdlib/_dbm.pyi +++ b/mypy/typeshed/stdlib/_dbm.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import ReadOnlyBuffer, StrOrBytesPath from types import TracebackType -from typing import Final, TypeVar, final, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Final, TypeAlias, TypeVar, final, overload, type_check_only +from typing_extensions import Self if sys.platform != "win32": _T = TypeVar("_T") @@ -28,10 +28,12 @@ if sys.platform != "win32": def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None ) -> None: ... + @overload def get(self, k: _KeyType, /) -> bytes | None: ... @overload def get(self, k: _KeyType, default: _T, /) -> bytes | _T: ... + def keys(self) -> list[bytes]: ... def setdefault(self, k: _KeyType, default: _ValueType = b"", /) -> bytes: ... # This isn't true, but the class can't be instantiated. See #13024 diff --git a/mypy/typeshed/stdlib/_decimal.pyi b/mypy/typeshed/stdlib/_decimal.pyi index 3cfe8944dfaf4..75d5be277f959 100644 --- a/mypy/typeshed/stdlib/_decimal.pyi +++ b/mypy/typeshed/stdlib/_decimal.pyi @@ -19,13 +19,14 @@ from decimal import ( Underflow as Underflow, _ContextManager, ) -from typing import Final -from typing_extensions import TypeAlias +from typing import Final, TypeAlias _TrapType: TypeAlias = type[DecimalException] __version__: Final[str] __libmpdec_version__: Final[str] +if sys.version_info >= (3, 15): + SPEC_VERSION: Final[str] ROUND_DOWN: Final = "ROUND_DOWN" ROUND_HALF_UP: Final = "ROUND_HALF_UP" diff --git a/mypy/typeshed/stdlib/_frozen_importlib.pyi b/mypy/typeshed/stdlib/_frozen_importlib.pyi index 58db64a016f34..172da4522d8ff 100644 --- a/mypy/typeshed/stdlib/_frozen_importlib.pyi +++ b/mypy/typeshed/stdlib/_frozen_importlib.pyi @@ -74,16 +74,11 @@ class BuiltinImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader) "The module spec is now used by the import machinery to generate a module repr." ) def module_repr(module: types.ModuleType) -> str: ... - if sys.version_info >= (3, 10): - @staticmethod - def create_module(spec: ModuleSpec) -> types.ModuleType | None: ... - @staticmethod - def exec_module(module: types.ModuleType) -> None: ... - else: - @classmethod - def create_module(cls, spec: ModuleSpec) -> types.ModuleType | None: ... - @classmethod - def exec_module(cls, module: types.ModuleType) -> None: ... + + @staticmethod + def create_module(spec: ModuleSpec) -> types.ModuleType | None: ... + @staticmethod + def exec_module(module: types.ModuleType) -> None: ... class FrozenImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader): # MetaPathFinder @@ -113,12 +108,8 @@ class FrozenImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader): "The module spec is now used by the import machinery to generate a module repr." ) def module_repr(m: types.ModuleType) -> str: ... - if sys.version_info >= (3, 10): - @staticmethod - def create_module(spec: ModuleSpec) -> types.ModuleType | None: ... - else: - @classmethod - def create_module(cls, spec: ModuleSpec) -> types.ModuleType | None: ... + @staticmethod + def create_module(spec: ModuleSpec) -> types.ModuleType | None: ... @staticmethod def exec_module(module: types.ModuleType) -> None: ... diff --git a/mypy/typeshed/stdlib/_frozen_importlib_external.pyi b/mypy/typeshed/stdlib/_frozen_importlib_external.pyi index 660cee6e84ec8..907e8def93806 100644 --- a/mypy/typeshed/stdlib/_frozen_importlib_external.pyi +++ b/mypy/typeshed/stdlib/_frozen_importlib_external.pyi @@ -1,19 +1,16 @@ import _ast -import _io import importlib.abc import importlib.machinery +import importlib.readers import sys import types from _typeshed import ReadableBuffer, StrOrBytesPath, StrPath from _typeshed.importlib import LoaderProtocol -from collections.abc import Callable, Iterable, Iterator, Mapping, MutableSequence, Sequence +from collections.abc import Callable, Iterable, Mapping, MutableSequence, Sequence from importlib.machinery import ModuleSpec from importlib.metadata import DistributionFinder, PathDistribution from typing import Any, Final, Literal, overload -from typing_extensions import Self, deprecated - -if sys.version_info >= (3, 10): - import importlib.readers +from typing_extensions import deprecated if sys.platform == "win32": path_separators: Literal["\\/"] @@ -33,6 +30,7 @@ MAGIC_NUMBER: Final[bytes] def cache_from_source(path: StrPath, debug_override: bool, *, optimization: None = None) -> str: ... @overload def cache_from_source(path: StrPath, debug_override: None = None, *, optimization: Any | None = None) -> str: ... + def source_from_cache(path: StrPath) -> str: ... def decode_source(source_bytes: ReadableBuffer) -> str: ... def spec_from_file_location( @@ -42,6 +40,7 @@ def spec_from_file_location( loader: LoaderProtocol | None = None, submodule_search_locations: list[str] | None = ..., ) -> importlib.machinery.ModuleSpec | None: ... + @deprecated( "Deprecated since Python 3.6. Use site configuration instead. " "Future versions of Python may not enable this finder by default." @@ -58,19 +57,10 @@ class WindowsRegistryFinder(importlib.abc.MetaPathFinder): ) -> ModuleSpec | None: ... class PathFinder(importlib.abc.MetaPathFinder): - if sys.version_info >= (3, 10): - @staticmethod - def invalidate_caches() -> None: ... - else: - @classmethod - def invalidate_caches(cls) -> None: ... - if sys.version_info >= (3, 10): - @staticmethod - def find_distributions(context: DistributionFinder.Context = ...) -> Iterable[PathDistribution]: ... - else: - @classmethod - def find_distributions(cls, context: DistributionFinder.Context = ...) -> Iterable[PathDistribution]: ... - + @staticmethod + def invalidate_caches() -> None: ... + @staticmethod + def find_distributions(context: DistributionFinder.Context = ...) -> Iterable[PathDistribution]: ... @classmethod def find_spec( cls, fullname: str, path: Sequence[str] | None = None, target: types.ModuleType | None = None @@ -117,14 +107,7 @@ class FileLoader: def get_data(self, path: str) -> bytes: ... def get_filename(self, fullname: str | None = None) -> str: ... def load_module(self, fullname: str | None = None) -> types.ModuleType: ... - if sys.version_info >= (3, 10): - def get_resource_reader(self, name: str | None = None) -> importlib.readers.FileReader: ... - else: - def get_resource_reader(self, name: str | None = None) -> Self | None: ... - def open_resource(self, resource: str) -> _io.FileIO: ... - def resource_path(self, resource: str) -> str: ... - def is_resource(self, name: str) -> bool: ... - def contents(self) -> Iterator[str]: ... + def get_resource_reader(self, name: str | None = None) -> importlib.readers.FileReader: ... class SourceFileLoader(importlib.abc.FileLoader, FileLoader, importlib.abc.SourceLoader, SourceLoader): # type: ignore[misc] # incompatible method arguments in base classes def set_data(self, path: str, data: ReadableBuffer, *, _mode: int = 0o666) -> None: ... @@ -183,24 +166,15 @@ else: def get_code(self, fullname: str) -> types.CodeType: ... def create_module(self, spec: ModuleSpec) -> None: ... def exec_module(self, module: types.ModuleType) -> None: ... - if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10; will be removed in Python 3.15. Use `exec_module()` instead.") - def load_module(self, fullname: str) -> types.ModuleType: ... - @staticmethod - @deprecated( - "Deprecated since Python 3.4; removed in Python 3.12. " - "The module spec is now used by the import machinery to generate a module repr." - ) - def module_repr(module: types.ModuleType) -> str: ... - def get_resource_reader(self, module: types.ModuleType) -> importlib.readers.NamespaceReader: ... - else: - def load_module(self, fullname: str) -> types.ModuleType: ... - @classmethod - @deprecated( - "Deprecated since Python 3.4; removed in Python 3.12. " - "The module spec is now used by the import machinery to generate a module repr." - ) - def module_repr(cls, module: types.ModuleType) -> str: ... + @deprecated("Deprecated since Python 3.10; will be removed in Python 3.15. Use `exec_module()` instead.") + def load_module(self, fullname: str) -> types.ModuleType: ... + @staticmethod + @deprecated( + "Deprecated since Python 3.4; removed in Python 3.12. " + "The module spec is now used by the import machinery to generate a module repr." + ) + def module_repr(module: types.ModuleType) -> str: ... + def get_resource_reader(self, module: types.ModuleType) -> importlib.readers.NamespaceReader: ... if sys.version_info >= (3, 13): class AppleFrameworkLoader(ExtensionFileLoader, importlib.abc.ExecutionLoader): ... diff --git a/mypy/typeshed/stdlib/_gdbm.pyi b/mypy/typeshed/stdlib/_gdbm.pyi index 2cb5fba29dfa1..b7a01a4531205 100644 --- a/mypy/typeshed/stdlib/_gdbm.pyi +++ b/mypy/typeshed/stdlib/_gdbm.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import ReadOnlyBuffer, StrOrBytesPath from types import TracebackType -from typing import TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self if sys.platform != "win32": _T = TypeVar("_T") @@ -32,10 +32,12 @@ if sys.platform != "win32": def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None ) -> None: ... + @overload def get(self, k: _KeyType) -> bytes | None: ... @overload def get(self, k: _KeyType, default: _T) -> bytes | _T: ... + def keys(self) -> list[bytes]: ... def setdefault(self, k: _KeyType, default: _ValueType = ...) -> bytes: ... # Don't exist at runtime diff --git a/mypy/typeshed/stdlib/_hashlib.pyi b/mypy/typeshed/stdlib/_hashlib.pyi index 03c1eef3be3ff..b98edc5757c39 100644 --- a/mypy/typeshed/stdlib/_hashlib.pyi +++ b/mypy/typeshed/stdlib/_hashlib.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import ReadableBuffer from collections.abc import Callable from types import ModuleType -from typing import AnyStr, Protocol, final, overload, type_check_only -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import AnyStr, Protocol, TypeAlias, final, overload, type_check_only +from typing_extensions import Self, disjoint_base _DigestMod: TypeAlias = str | Callable[[], _HashObject] | ModuleType | None @@ -35,8 +35,7 @@ class HASH: def hexdigest(self) -> str: ... def update(self, obj: ReadableBuffer, /) -> None: ... -if sys.version_info >= (3, 10): - class UnsupportedDigestmodError(ValueError): ... +class UnsupportedDigestmodError(ValueError): ... class HASHXOF(HASH): def digest(self, length: int) -> bytes: ... # type: ignore[override] @@ -59,8 +58,9 @@ class HMAC: def compare_digest(a: ReadableBuffer, b: ReadableBuffer, /) -> bool: ... @overload def compare_digest(a: AnyStr, b: AnyStr, /) -> bool: ... + def get_fips_mode() -> int: ... -def hmac_new(key: bytes | bytearray, msg: ReadableBuffer = b"", digestmod: _DigestMod = None) -> HMAC: ... +def hmac_new(key: ReadableBuffer, msg: ReadableBuffer = b"", digestmod: _DigestMod = None) -> HMAC: ... if sys.version_info >= (3, 13): def new( @@ -118,7 +118,7 @@ else: def openssl_shake_128(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ... def openssl_shake_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ... -def hmac_digest(key: bytes | bytearray, msg: ReadableBuffer, digest: str) -> bytes: ... +def hmac_digest(key: ReadableBuffer, msg: ReadableBuffer, digest: str) -> bytes: ... def pbkdf2_hmac( hash_name: str, password: ReadableBuffer, salt: ReadableBuffer, iterations: int, dklen: int | None = None ) -> bytes: ... diff --git a/mypy/typeshed/stdlib/_heapq.pyi b/mypy/typeshed/stdlib/_heapq.pyi index 4d7d6aba32418..7664b826ae0b5 100644 --- a/mypy/typeshed/stdlib/_heapq.pyi +++ b/mypy/typeshed/stdlib/_heapq.pyi @@ -4,7 +4,7 @@ from typing import Final __about__: Final[str] -def heapify(heap: list[_T], /) -> None: ... +def heapify(heap: list[_T], /) -> None: ... # To work around the fact that list is invariant def heappop(heap: list[_T], /) -> _T: ... def heappush(heap: list[_T], item: _T, /) -> None: ... def heappushpop(heap: list[_T], item: _T, /) -> _T: ... diff --git a/mypy/typeshed/stdlib/_interpqueues.pyi b/mypy/typeshed/stdlib/_interpqueues.pyi index c9323b106f3dc..94605e4c0dda5 100644 --- a/mypy/typeshed/stdlib/_interpqueues.pyi +++ b/mypy/typeshed/stdlib/_interpqueues.pyi @@ -1,5 +1,5 @@ -from typing import Any, Literal, SupportsIndex -from typing_extensions import TypeAlias +import sys +from typing import Any, Literal, SupportsIndex, TypeAlias _UnboundOp: TypeAlias = Literal[1, 2, 3] @@ -7,7 +7,13 @@ class QueueError(RuntimeError): ... class QueueNotFoundError(QueueError): ... def bind(qid: SupportsIndex) -> None: ... -def create(maxsize: SupportsIndex, fmt: SupportsIndex, unboundop: _UnboundOp) -> int: ... + +if sys.version_info >= (3, 15): + def create(maxsize: SupportsIndex, unboundop: SupportsIndex = -1, fallback: SupportsIndex = -1) -> int: ... + +else: + def create(maxsize: SupportsIndex, fmt: SupportsIndex, unboundop: _UnboundOp) -> int: ... + def destroy(qid: SupportsIndex) -> None: ... def get(qid: SupportsIndex) -> tuple[Any, int, _UnboundOp | None]: ... def get_count(qid: SupportsIndex) -> int: ... @@ -15,5 +21,11 @@ def get_maxsize(qid: SupportsIndex) -> int: ... def get_queue_defaults(qid: SupportsIndex) -> tuple[int, _UnboundOp]: ... def is_full(qid: SupportsIndex) -> bool: ... def list_all() -> list[tuple[int, int, _UnboundOp]]: ... -def put(qid: SupportsIndex, obj: Any, fmt: SupportsIndex, unboundop: _UnboundOp) -> None: ... + +if sys.version_info >= (3, 15): + def put(qid: SupportsIndex, obj: Any, unboundop: SupportsIndex = -1, fallback: SupportsIndex = -1) -> None: ... + +else: + def put(qid: SupportsIndex, obj: Any, fmt: SupportsIndex, unboundop: _UnboundOp) -> None: ... + def release(qid: SupportsIndex) -> None: ... diff --git a/mypy/typeshed/stdlib/_interpreters.pyi b/mypy/typeshed/stdlib/_interpreters.pyi index 8e097efad618a..3885669278f51 100644 --- a/mypy/typeshed/stdlib/_interpreters.pyi +++ b/mypy/typeshed/stdlib/_interpreters.pyi @@ -1,7 +1,7 @@ import types from collections.abc import Callable -from typing import Any, Final, Literal, SupportsIndex, TypeVar, overload -from typing_extensions import TypeAlias, disjoint_base +from typing import Any, Final, Literal, SupportsIndex, TypeAlias, TypeVar, overload +from typing_extensions import disjoint_base _R = TypeVar("_R") @@ -47,6 +47,7 @@ def set___main___attrs(id: SupportsIndex, updates: _SharedDict, *, restrict: boo def incref(id: SupportsIndex, *, implieslink: bool = False, restrict: bool = False) -> None: ... def decref(id: SupportsIndex, *, restrict: bool = False) -> None: ... def is_shareable(obj: object) -> bool: ... + @overload def capture_exception(exc: BaseException) -> types.SimpleNamespace: ... @overload diff --git a/mypy/typeshed/stdlib/_io.pyi b/mypy/typeshed/stdlib/_io.pyi index ed8eff2759a98..5e216123be984 100644 --- a/mypy/typeshed/stdlib/_io.pyi +++ b/mypy/typeshed/stdlib/_io.pyi @@ -163,13 +163,39 @@ class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_Buffere def seek(self, target: int, whence: int = 0, /) -> int: ... def truncate(self, pos: int | None = None, /) -> int: ... +@type_check_only +class _BufferedWriterStream(Protocol): + def write(self, b: WriteableBuffer, /) -> int | None: ... + def seek(self, pos: int, whence: int, /) -> int: ... + def tell(self) -> int: ... + def truncate(self, size: int, /) -> int: ... + def flush(self) -> object: ... + def close(self) -> object: ... + @property + def closed(self) -> bool: ... + def writable(self) -> bool: ... + def seekable(self) -> bool: ... + + # The following methods just pass through to the underlying stream. Since + # not all streams support them, they are marked as optional here, and will + # raise an AttributeError if called on a stream that does not support them. + + # @property + # def name(self) -> Any: ... # Type is inconsistent between the various I/O types. + # @property + # def mode(self) -> str: ... + # def fileno(self) -> int: ... + # def isatty(self) -> bool: ... + +_BufferedWriterStreamT = TypeVar("_BufferedWriterStreamT", bound=_BufferedWriterStream, default=_BufferedWriterStream) + @disjoint_base -class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of writelines in the base classes - raw: RawIOBase +class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_BufferedWriterStreamT]): # type: ignore[misc] # incompatible definitions of writelines in the base classes + raw: _BufferedWriterStreamT if sys.version_info >= (3, 14): - def __init__(self, raw: RawIOBase, buffer_size: int = 131072) -> None: ... + def __init__(self, raw: _BufferedWriterStreamT, buffer_size: int = 131072) -> None: ... else: - def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ... + def __init__(self, raw: _BufferedWriterStreamT, buffer_size: int = 8192) -> None: ... def write(self, buffer: ReadableBuffer, /) -> int: ... def seek(self, target: int, whence: int = 0, /) -> int: ... @@ -190,11 +216,15 @@ class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore def truncate(self, pos: int | None = None, /) -> int: ... @disjoint_base -class BufferedRWPair(BufferedIOBase, _BufferedIOBase, Generic[_BufferedReaderStreamT]): +class BufferedRWPair(BufferedIOBase, _BufferedIOBase, Generic[_BufferedReaderStreamT, _BufferedWriterStreamT]): if sys.version_info >= (3, 14): - def __init__(self, reader: _BufferedReaderStreamT, writer: RawIOBase, buffer_size: int = 131072, /) -> None: ... + def __init__( + self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 131072, / + ) -> None: ... else: - def __init__(self, reader: _BufferedReaderStreamT, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ... + def __init__( + self, reader: _BufferedReaderStreamT, writer: _BufferedWriterStreamT, buffer_size: int = 8192, / + ) -> None: ... def peek(self, size: int = 0, /) -> bytes: ... @@ -294,8 +324,7 @@ class IncrementalNewlineDecoder: def reset(self) -> None: ... def setstate(self, state: tuple[bytes, int], /) -> None: ... -if sys.version_info >= (3, 10): - @overload - def text_encoding(encoding: None, stacklevel: int = 2, /) -> Literal["locale", "utf-8"]: ... - @overload - def text_encoding(encoding: _S, stacklevel: int = 2, /) -> _S: ... +@overload +def text_encoding(encoding: None, stacklevel: int = 2, /) -> Literal["locale", "utf-8"]: ... +@overload +def text_encoding(encoding: _S, stacklevel: int = 2, /) -> _S: ... diff --git a/mypy/typeshed/stdlib/_json.pyi b/mypy/typeshed/stdlib/_json.pyi index 4a77e5be594ab..c6c2c97e83aa9 100644 --- a/mypy/typeshed/stdlib/_json.pyi +++ b/mypy/typeshed/stdlib/_json.pyi @@ -1,3 +1,4 @@ +import sys from collections.abc import Callable from typing import Any, final from typing_extensions import Self @@ -36,6 +37,8 @@ class make_encoder: @final class make_scanner: + if sys.version_info >= (3, 15): + array_hook: Any object_hook: Any object_pairs_hook: Any parse_int: Any @@ -48,4 +51,9 @@ class make_scanner: def encode_basestring(s: str, /) -> str: ... def encode_basestring_ascii(s: str, /) -> str: ... -def scanstring(string: str, end: int, strict: bool = True) -> tuple[str, int]: ... + +if sys.version_info >= (3, 15): + def scanstring(pystr: str, end: int, strict: bool = True, /) -> tuple[str, int]: ... + +else: + def scanstring(string: str, end: int, strict: bool = True) -> tuple[str, int]: ... diff --git a/mypy/typeshed/stdlib/_lsprof.pyi b/mypy/typeshed/stdlib/_lsprof.pyi index 4f6d98b8ffb61..d04c2a74ee07b 100644 --- a/mypy/typeshed/stdlib/_lsprof.pyi +++ b/mypy/typeshed/stdlib/_lsprof.pyi @@ -1,4 +1,3 @@ -import sys from _typeshed import structseq from collections.abc import Callable from types import CodeType @@ -17,8 +16,7 @@ class Profiler: @final class profiler_entry(structseq[Any], tuple[CodeType | str, int, int, float, float, list[profiler_subentry]]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("code", "callcount", "reccallcount", "totaltime", "inlinetime", "calls") + __match_args__: Final = ("code", "callcount", "reccallcount", "totaltime", "inlinetime", "calls") code: CodeType | str callcount: int reccallcount: int @@ -28,8 +26,7 @@ class profiler_entry(structseq[Any], tuple[CodeType | str, int, int, float, floa @final class profiler_subentry(structseq[Any], tuple[CodeType | str, int, int, float, float]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("code", "callcount", "reccallcount", "totaltime", "inlinetime") + __match_args__: Final = ("code", "callcount", "reccallcount", "totaltime", "inlinetime") code: CodeType | str callcount: int reccallcount: int diff --git a/mypy/typeshed/stdlib/_lzma.pyi b/mypy/typeshed/stdlib/_lzma.pyi index b38dce9fadedf..83cd8fd756b9a 100644 --- a/mypy/typeshed/stdlib/_lzma.pyi +++ b/mypy/typeshed/stdlib/_lzma.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import ReadableBuffer from collections.abc import Mapping, Sequence -from typing import Any, Final, final -from typing_extensions import Self, TypeAlias +from typing import Any, Final, TypeAlias, final +from typing_extensions import Self _FilterChain: TypeAlias = Sequence[Mapping[str, Any]] diff --git a/mypy/typeshed/stdlib/_markupbase.pyi b/mypy/typeshed/stdlib/_markupbase.pyi index 597bd09b700b0..acc3ccac7188c 100644 --- a/mypy/typeshed/stdlib/_markupbase.pyi +++ b/mypy/typeshed/stdlib/_markupbase.pyi @@ -1,6 +1,3 @@ -import sys -from typing import Any - class ParserBase: def reset(self) -> None: ... def getpos(self) -> tuple[int, int]: ... @@ -9,8 +6,5 @@ class ParserBase: def parse_declaration(self, i: int) -> int: ... # undocumented def parse_marked_section(self, i: int, report: bool = True) -> int: ... # undocumented def updatepos(self, i: int, j: int) -> int: ... # undocumented - if sys.version_info < (3, 10): - # Removed from ParserBase: https://bugs.python.org/issue31844 - def error(self, message: str) -> Any: ... # undocumented lineno: int # undocumented offset: int # undocumented diff --git a/mypy/typeshed/stdlib/_msi.pyi b/mypy/typeshed/stdlib/_msi.pyi index edceed51bf9db..e5b408811ee7a 100644 --- a/mypy/typeshed/stdlib/_msi.pyi +++ b/mypy/typeshed/stdlib/_msi.pyi @@ -52,7 +52,7 @@ if sys.platform == "win32": __init__: None # type: ignore[assignment] def UuidCreate() -> str: ... - def FCICreate(cabname: str, files: list[str], /) -> None: ... + def FCICreate(cabname: str, files: list[tuple[str, str]], /) -> None: ... def OpenDatabase(path: str, persist: int, /) -> _Database: ... def CreateRecord(count: int, /) -> _Record: ... diff --git a/mypy/typeshed/stdlib/_operator.pyi b/mypy/typeshed/stdlib/_operator.pyi index 8c705065bde7d..29a6d70572028 100644 --- a/mypy/typeshed/stdlib/_operator.pyi +++ b/mypy/typeshed/stdlib/_operator.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import SupportsGetItem from collections.abc import Callable, Container, Iterable, MutableMapping, MutableSequence, Sequence from operator import attrgetter as attrgetter, itemgetter as itemgetter, methodcaller as methodcaller -from typing import Any, AnyStr, Protocol, SupportsAbs, SupportsIndex, TypeVar, overload, type_check_only -from typing_extensions import ParamSpec, TypeAlias, TypeIs +from typing import Any, AnyStr, ParamSpec, Protocol, SupportsAbs, SupportsIndex, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import TypeIs _R = TypeVar("_R") _T = TypeVar("_T") @@ -79,23 +79,28 @@ def xor(a: Any, b: Any, /) -> Any: ... def concat(a: Sequence[_T], b: Sequence[_T], /) -> Sequence[_T]: ... def contains(a: Container[object], b: object, /) -> bool: ... def countOf(a: Iterable[object], b: object, /) -> int: ... + @overload def delitem(a: MutableSequence[Any], b: int, /) -> None: ... @overload def delitem(a: MutableSequence[Any], b: slice[int | None], /) -> None: ... @overload def delitem(a: MutableMapping[_K, Any], b: _K, /) -> None: ... + @overload def getitem(a: Sequence[_T], b: slice[int | None], /) -> Sequence[_T]: ... @overload def getitem(a: SupportsGetItem[_K, _V], b: _K, /) -> _V: ... + def indexOf(a: Iterable[_T], b: _T, /) -> int: ... + @overload def setitem(a: MutableSequence[_T], b: int, c: _T, /) -> None: ... @overload def setitem(a: MutableSequence[_T], b: slice[int | None], c: Sequence[_T], /) -> None: ... @overload def setitem(a: MutableMapping[_K, _V], b: _K, c: _V, /) -> None: ... + def length_hint(obj: object, default: int = 0, /) -> int: ... def iadd(a, b, /): ... def iand(a, b, /): ... diff --git a/mypy/typeshed/stdlib/_pickle.pyi b/mypy/typeshed/stdlib/_pickle.pyi index 4294de4c2c7fe..f8411c6abc4b8 100644 --- a/mypy/typeshed/stdlib/_pickle.pyi +++ b/mypy/typeshed/stdlib/_pickle.pyi @@ -1,8 +1,8 @@ from _typeshed import ReadableBuffer, SupportsWrite from collections.abc import Callable, Iterable, Iterator, Mapping from pickle import PickleBuffer as PickleBuffer -from typing import Any, Protocol, type_check_only -from typing_extensions import TypeAlias, disjoint_base +from typing import Any, Protocol, TypeAlias, type_check_only +from typing_extensions import disjoint_base @type_check_only class _ReadableFileobj(Protocol): @@ -69,10 +69,12 @@ class Pickler: fix_imports: bool = True, buffer_callback: _BufferCallback = None, ) -> None: ... + @property def memo(self) -> PicklerMemoProxy: ... @memo.setter def memo(self, value: PicklerMemoProxy | dict[int, tuple[int, Any]]) -> None: ... + def dump(self, obj: Any, /) -> None: ... def clear_memo(self) -> None: ... @@ -99,10 +101,12 @@ class Unpickler: errors: str = "strict", buffers: Iterable[Any] | None = (), ) -> None: ... + @property def memo(self) -> UnpicklerMemoProxy: ... @memo.setter def memo(self, value: UnpicklerMemoProxy | dict[int, tuple[int, Any]]) -> None: ... + def load(self) -> Any: ... def find_class(self, module_name: str, global_name: str, /) -> Any: ... diff --git a/mypy/typeshed/stdlib/_pydecimal.pyi b/mypy/typeshed/stdlib/_pydecimal.pyi index a6723f749da6d..9fabcc6400969 100644 --- a/mypy/typeshed/stdlib/_pydecimal.pyi +++ b/mypy/typeshed/stdlib/_pydecimal.pyi @@ -45,3 +45,6 @@ __all__ = [ if sys.version_info >= (3, 14): __all__ += ["IEEEContext", "IEEE_CONTEXT_MAX_BITS"] + +if sys.version_info >= (3, 15): + __all__ += ["SPEC_VERSION"] diff --git a/mypy/typeshed/stdlib/_random.pyi b/mypy/typeshed/stdlib/_random.pyi index ac00fdfb7272b..e333b00d1084e 100644 --- a/mypy/typeshed/stdlib/_random.pyi +++ b/mypy/typeshed/stdlib/_random.pyi @@ -1,16 +1,12 @@ -import sys -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import TypeAlias +from typing_extensions import disjoint_base # Actually Tuple[(int,) * 625] _State: TypeAlias = tuple[int, ...] @disjoint_base class Random: - if sys.version_info >= (3, 10): - def __init__(self, seed: object = ..., /) -> None: ... - else: - def __new__(self, seed: object = ..., /) -> Self: ... - + def __init__(self, seed: object = ..., /) -> None: ... def seed(self, n: object = None, /) -> None: ... def getstate(self) -> _State: ... def setstate(self, state: _State, /) -> None: ... diff --git a/mypy/typeshed/stdlib/_remote_debugging.pyi b/mypy/typeshed/stdlib/_remote_debugging.pyi new file mode 100644 index 0000000000000..b001962aad7bf --- /dev/null +++ b/mypy/typeshed/stdlib/_remote_debugging.pyi @@ -0,0 +1,183 @@ +from _typeshed import StrOrBytesPath, structseq +from collections.abc import Callable +from typing import Final, TypeAlias, final +from typing_extensions import Self + +_Location: TypeAlias = tuple[int, int, int, int] | LocationInfo | None +_Frame: TypeAlias = tuple[str, _Location, str, int | None] | FrameInfo +_Stats: TypeAlias = dict[str, int | float] + +PROCESS_VM_READV_SUPPORTED: Final[int] +THREAD_STATUS_GIL_REQUESTED: Final[int] +THREAD_STATUS_HAS_EXCEPTION: Final[int] +THREAD_STATUS_HAS_GIL: Final[int] +THREAD_STATUS_MAIN_THREAD: Final[int] +THREAD_STATUS_ON_CPU: Final[int] +THREAD_STATUS_UNKNOWN: Final[int] + +@final +class LocationInfo(structseq[int], tuple[int, int, int, int]): + __match_args__: Final = ("lineno", "end_lineno", "col_offset", "end_col_offset") + @property + def lineno(self) -> int: ... + @property + def end_lineno(self) -> int: ... + @property + def col_offset(self) -> int: ... + @property + def end_col_offset(self) -> int: ... + +@final +class FrameInfo(structseq[object], tuple[str, _Location, str, int | None]): + __match_args__: Final = ("filename", "location", "funcname", "opcode") + @property + def filename(self) -> str: ... + @property + def location(self) -> _Location: ... + @property + def funcname(self) -> str: ... + @property + def opcode(self) -> int | None: ... + +@final +class CoroInfo(structseq[object], tuple[list[_Frame], int | str]): + __match_args__: Final = ("call_stack", "task_name") + @property + def call_stack(self) -> list[_Frame]: ... + @property + def task_name(self) -> int | str: ... + +@final +class TaskInfo(structseq[object], tuple[int, str, list[CoroInfo], list[CoroInfo]]): + __match_args__: Final = ("task_id", "task_name", "coroutine_stack", "awaited_by") + @property + def task_id(self) -> int: ... + @property + def task_name(self) -> str: ... + @property + def coroutine_stack(self) -> list[CoroInfo]: ... + @property + def awaited_by(self) -> list[CoroInfo]: ... + +@final +class ThreadInfo(structseq[object], tuple[int, int, list[_Frame]]): + __match_args__: Final = ("thread_id", "status", "frame_info") + @property + def thread_id(self) -> int: ... + @property + def status(self) -> int: ... + @property + def frame_info(self) -> list[_Frame]: ... + +@final +class InterpreterInfo(structseq[object], tuple[int, list[ThreadInfo]]): + __match_args__: Final = ("interpreter_id", "threads") + @property + def interpreter_id(self) -> int: ... + @property + def threads(self) -> list[ThreadInfo]: ... + +@final +class AwaitedInfo(structseq[object], tuple[int, list[TaskInfo]]): + __match_args__: Final = ("thread_id", "awaited_by") + @property + def thread_id(self) -> int: ... + @property + def awaited_by(self) -> list[TaskInfo]: ... + +@final +class GCStatsInfo(structseq[object], tuple[int, int, int, int, int, int, int, int, int, float]): + __match_args__: Final = ( + "gen", + "iid", + "ts_start", + "ts_stop", + "collections", + "collected", + "uncollectable", + "candidates", + "heap_size", + "duration", + ) + @property + def gen(self) -> int: ... + @property + def iid(self) -> int: ... + @property + def ts_start(self) -> int: ... + @property + def ts_stop(self) -> int: ... + @property + def collections(self) -> int: ... + @property + def collected(self) -> int: ... + @property + def uncollectable(self) -> int: ... + @property + def candidates(self) -> int: ... + @property + def heap_size(self) -> int: ... + @property + def duration(self) -> float: ... + +@final +class RemoteUnwinder: + def __init__( + self, + pid: int, + *, + all_threads: bool = False, + only_active_thread: bool = False, + mode: int = 0, + debug: bool = False, + skip_non_matching_threads: bool = True, + native: bool = False, + gc: bool = False, + opcodes: bool = False, + cache_frames: bool = False, + stats: bool = False, + ) -> None: ... + def get_stack_trace(self) -> list[InterpreterInfo]: ... + def get_all_awaited_by(self) -> list[AwaitedInfo]: ... + def get_async_stack_trace(self) -> list[AwaitedInfo]: ... + def get_stats(self) -> _Stats: ... + def pause_threads(self) -> bool: ... + def resume_threads(self) -> bool: ... + +@final +class GCMonitor: + def __init__(self, pid: int, *, debug: bool = False) -> None: ... + def get_gc_stats(self, all_interpreters: bool = False) -> list[GCStatsInfo]: ... + +@final +class BinaryWriter: + def __init__( + self, filename: StrOrBytesPath, sample_interval_us: int, start_time_us: int, *, compression: int = 0 + ) -> None: ... + @property + def total_samples(self) -> int: ... + def write_sample(self, stack_frames: list[InterpreterInfo], timestamp_us: int) -> None: ... + def finalize(self) -> None: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type: object = None, exc_val: object = None, exc_tb: object = None) -> bool: ... + def get_stats(self) -> _Stats: ... + +@final +class BinaryReader: + def __init__(self, filename: StrOrBytesPath) -> None: ... + @property + def sample_count(self) -> int: ... + @property + def sample_interval_us(self) -> int: ... + def replay(self, collector: object, progress_callback: Callable[[int, int], object] | None = None) -> int: ... + def get_info(self) -> dict[str, object]: ... + def get_stats(self) -> _Stats: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type: object = None, exc_val: object = None, exc_tb: object = None) -> bool: ... + +def zstd_available() -> bool: ... +def get_child_pids(pid: int, *, recursive: bool = True) -> list[int]: ... +def is_python_process(pid: int) -> bool: ... +def get_gc_stats(pid: int, *, all_interpreters: bool = False) -> list[GCStatsInfo]: ... diff --git a/mypy/typeshed/stdlib/_socket.pyi b/mypy/typeshed/stdlib/_socket.pyi index 918bffc7f9085..bb9b08e2f79ad 100644 --- a/mypy/typeshed/stdlib/_socket.pyi +++ b/mypy/typeshed/stdlib/_socket.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import ReadableBuffer, WriteableBuffer from collections.abc import Iterable from socket import error as error, gaierror as gaierror, herror as herror, timeout as timeout -from typing import Any, Final, SupportsIndex, overload -from typing_extensions import CapsuleType, TypeAlias, disjoint_base +from typing import Any, Final, SupportsIndex, TypeAlias, overload +from typing_extensions import CapsuleType, disjoint_base _CMSG: TypeAlias = tuple[int, int, bytes] _CMSGArg: TypeAlias = tuple[int, int, ReadableBuffer] @@ -195,7 +195,7 @@ if sys.platform != "win32" and sys.platform != "darwin" and sys.platform != "lin if sys.platform == "linux": # Availability: Linux >= 2.6.20, FreeBSD >= 10.1 IPPROTO_UDPLITE: Final[int] -if sys.version_info >= (3, 10) and sys.platform == "linux": +if sys.platform == "linux": IPPROTO_MPTCP: Final[int] IPPORT_RESERVED: Final[int] @@ -218,8 +218,7 @@ IP_MULTICAST_TTL: Final[int] IP_OPTIONS: Final[int] if sys.platform != "linux": IP_RECVDSTADDR: Final[int] -if sys.version_info >= (3, 10): - IP_RECVTOS: Final[int] +IP_RECVTOS: Final[int] IP_TOS: Final[int] IP_TTL: Final[int] if sys.platform != "win32": @@ -231,6 +230,9 @@ if sys.platform != "win32": IP_RETOPTS: Final[int] if sys.version_info >= (3, 13) and sys.platform == "linux": CAN_RAW_ERR_FILTER: Final[int] +if sys.version_info >= (3, 15): + if sys.platform == "win32" or sys.platform == "linux": + IPV6_HDRINCL: Final[int] if sys.version_info >= (3, 14): IP_RECVTTL: Final[int] @@ -343,7 +345,7 @@ if sys.platform != "win32": TCP_NOTSENT_LOWAT: Final[int] if sys.platform != "darwin": TCP_KEEPIDLE: Final[int] -if sys.version_info >= (3, 10) and sys.platform == "darwin": +if sys.platform == "darwin": TCP_KEEPALIVE: Final[int] if sys.version_info >= (3, 11) and sys.platform == "darwin": TCP_CONNECTION_INFO: Final[int] @@ -438,6 +440,35 @@ if sys.platform == "linux": CAN_RAW_JOIN_FILTERS: Final[int] # Availability: Linux >= 2.6.25 CAN_ISOTP: Final[int] + if sys.version_info >= (3, 15): + CAN_ISOTP_CHK_PAD_DATA: Final[int] + CAN_ISOTP_CHK_PAD_LEN: Final[int] + CAN_ISOTP_DEFAULT_EXT_ADDRESS: Final[int] + CAN_ISOTP_DEFAULT_FLAGS: Final[int] + CAN_ISOTP_DEFAULT_FRAME_TXTIME: Final[int] + CAN_ISOTP_DEFAULT_LL_MTU: Final[int] + CAN_ISOTP_DEFAULT_LL_TX_DL: Final[int] + CAN_ISOTP_DEFAULT_LL_TX_FLAGS: Final[int] + CAN_ISOTP_DEFAULT_PAD_CONTENT: Final[int] + CAN_ISOTP_DEFAULT_RECV_BS: Final[int] + CAN_ISOTP_DEFAULT_RECV_STMIN: Final[int] + CAN_ISOTP_DEFAULT_RECV_WFTMAX: Final[int] + CAN_ISOTP_EXTEND_ADDR: Final[int] + CAN_ISOTP_FORCE_RXSTMIN: Final[int] + CAN_ISOTP_FORCE_TXSTMIN: Final[int] + CAN_ISOTP_HALF_DUPLEX: Final[int] + CAN_ISOTP_LL_OPTS: Final[int] + CAN_ISOTP_LISTEN_MODE: Final[int] + CAN_ISOTP_OPTS: Final[int] + CAN_ISOTP_RECV_FC: Final[int] + CAN_ISOTP_RX_EXT_ADDR: Final[int] + CAN_ISOTP_RX_PADDING: Final[int] + CAN_ISOTP_RX_STMIN: Final[int] + CAN_ISOTP_SF_BROADCAST: Final[int] + CAN_ISOTP_TX_PADDING: Final[int] + CAN_ISOTP_TX_STMIN: Final[int] + CAN_ISOTP_WAIT_TX_DONE: Final[int] + SOL_CAN_ISOTP: Final[int] # Availability: Linux >= 5.4 CAN_J1939: Final[int] @@ -757,10 +788,12 @@ class socket: def fileno(self) -> int: ... def getpeername(self) -> _RetAddress: ... def getsockname(self) -> _RetAddress: ... + @overload def getsockopt(self, level: int, optname: int, /) -> int: ... @overload def getsockopt(self, level: int, optname: int, buflen: int, /) -> bytes: ... + def getblocking(self) -> bool: ... def gettimeout(self) -> float | None: ... if sys.platform == "win32": @@ -779,10 +812,12 @@ class socket: def recv_into(self, buffer: WriteableBuffer, nbytes: int = 0, flags: int = 0) -> int: ... def send(self, data: ReadableBuffer, flags: int = 0, /) -> int: ... def sendall(self, data: ReadableBuffer, flags: int = 0, /) -> None: ... + @overload def sendto(self, data: ReadableBuffer, address: _Address, /) -> int: ... @overload def sendto(self, data: ReadableBuffer, flags: int, address: _Address, /) -> int: ... + if sys.platform != "win32": def sendmsg( self, @@ -799,10 +834,12 @@ class socket: def setblocking(self, flag: bool, /) -> None: ... def settimeout(self, value: float | None, /) -> None: ... + @overload def setsockopt(self, level: int, optname: int, value: int | ReadableBuffer, /) -> None: ... @overload def setsockopt(self, level: int, optname: int, value: None, optlen: int, /) -> None: ... + if sys.platform == "win32": def share(self, process_id: int, /) -> bytes: ... diff --git a/mypy/typeshed/stdlib/_sqlite3.pyi b/mypy/typeshed/stdlib/_sqlite3.pyi index 5361584d6b184..b6d96121afa98 100644 --- a/mypy/typeshed/stdlib/_sqlite3.pyi +++ b/mypy/typeshed/stdlib/_sqlite3.pyi @@ -18,8 +18,8 @@ from sqlite3 import ( Warning as Warning, _IsolationLevel, ) -from typing import Any, Final, Literal, TypeVar, overload -from typing_extensions import TypeAlias, deprecated +from typing import Any, Final, Literal, TypeAlias, TypeVar, overload +from typing_extensions import deprecated if sys.version_info >= (3, 11): from sqlite3 import Blob as Blob @@ -69,6 +69,8 @@ SQLITE_SAVEPOINT: Final = 32 SQLITE_SELECT: Final = 21 SQLITE_TRANSACTION: Final = 22 SQLITE_UPDATE: Final = 23 +if sys.version_info >= (3, 15): + SQLITE_KEYWORDS: tuple[str, ...] adapters: dict[tuple[type[Any], type[Any]], _Adapter[Any]] converters: dict[str, _Converter] sqlite_version: str @@ -218,6 +220,7 @@ if sys.version_info >= (3, 11): def adapt(obj: Any, proto: Any, /) -> Any: ... @overload def adapt(obj: Any, proto: Any, alt: _T, /) -> Any | _T: ... + def complete_statement(statement: str) -> bool: ... if sys.version_info >= (3, 12): @@ -259,7 +262,6 @@ if sys.version_info >= (3, 12): uri: bool = False, autocommit: bool = ..., ) -> _ConnectionT: ... - else: @overload def connect( @@ -305,13 +307,5 @@ if sys.version_info < (3, 12): ) def enable_shared_cache(do_enable: int) -> None: ... # undocumented -if sys.version_info >= (3, 10): - def register_adapter(type: type[_T], adapter: _Adapter[_T], /) -> None: ... - def register_converter(typename: str, converter: _Converter, /) -> None: ... - -else: - def register_adapter(type: type[_T], caster: _Adapter[_T], /) -> None: ... - def register_converter(name: str, converter: _Converter, /) -> None: ... - -if sys.version_info < (3, 10): - OptimizedUnicode = str # undocumented +def register_adapter(type: type[_T], adapter: _Adapter[_T], /) -> None: ... +def register_converter(typename: str, converter: _Converter, /) -> None: ... diff --git a/mypy/typeshed/stdlib/_ssl.pyi b/mypy/typeshed/stdlib/_ssl.pyi index e84b24e8f4db7..87023d90e13bb 100644 --- a/mypy/typeshed/stdlib/_ssl.pyi +++ b/mypy/typeshed/stdlib/_ssl.pyi @@ -12,8 +12,8 @@ from ssl import ( SSLWantWriteError as SSLWantWriteError, SSLZeroReturnError as SSLZeroReturnError, ) -from typing import Any, ClassVar, Final, Literal, TypedDict, final, overload, type_check_only -from typing_extensions import NotRequired, Self, TypeAlias, deprecated, disjoint_base +from typing import Any, ClassVar, Final, Literal, TypeAlias, TypedDict, final, overload, type_check_only +from typing_extensions import NotRequired, Self, deprecated, disjoint_base _PasswordType: TypeAlias = Callable[[], str | bytes | bytearray] | str | bytes | bytearray _PCTRTT: TypeAlias = tuple[tuple[str, str], ...] @@ -54,13 +54,12 @@ if sys.version_info < (3, 12): @deprecated("Deprecated since Python 3.6; removed in Python 3.12. Use `ssl.RAND_bytes()` instead.") def RAND_pseudo_bytes(n: int, /) -> tuple[bytes, bool]: ... -if sys.version_info < (3, 10): - @deprecated("Unsupported by OpenSSL since 1.1.1; removed in Python 3.10.") - def RAND_egd(path: str) -> None: ... - def RAND_status() -> bool: ... def get_default_verify_paths() -> tuple[str, str, str, str]: ... +if sys.version_info >= (3, 15): + def get_sigalgs() -> list[str]: ... + if sys.platform == "win32": _EnumRetType: TypeAlias = list[tuple[bytes, str, set[str] | bool]] def enum_certificates(store_name: str) -> _EnumRetType: ... @@ -68,6 +67,7 @@ if sys.platform == "win32": def txt2obj(txt: str, name: bool = False) -> tuple[int, str, str, str]: ... def nid2obj(nid: int, /) -> tuple[int, str, str, str]: ... + @disjoint_base class _SSLContext: check_hostname: bool @@ -78,19 +78,20 @@ class _SSLContext: options: int post_handshake_auth: bool protocol: int - if sys.version_info >= (3, 10): - security_level: int + security_level: int sni_callback: Callable[[SSLObject, str, SSLContext], None | int] | None verify_flags: int verify_mode: int def __new__(cls, protocol: int, /) -> Self: ... def cert_store_stats(self) -> dict[str, int]: ... + @overload def get_ca_certs(self, binary_form: Literal[False] = False) -> list[_PeerCertRetDictType]: ... @overload def get_ca_certs(self, binary_form: Literal[True]) -> list[bytes]: ... @overload def get_ca_certs(self, binary_form: bool = False) -> Any: ... + def get_ciphers(self) -> list[_Cipher]: ... def load_cert_chain( self, certfile: StrOrBytesPath, keyfile: StrOrBytesPath | None = None, password: _PasswordType | None = None @@ -106,6 +107,12 @@ class _SSLContext: def set_ciphers(self, cipherlist: str, /) -> None: ... def set_default_verify_paths(self) -> None: ... def set_ecdh_curve(self, name: str, /) -> None: ... + if sys.version_info >= (3, 15): + def get_groups(self, *, include_aliases: bool = False) -> list[str]: ... + def set_ciphersuites(self, ciphersuites: str, /) -> None: ... + def set_client_sigalgs(self, sigalgslist: str, /) -> None: ... + def set_groups(self, grouplist: str, /) -> None: ... + def set_server_sigalgs(self, sigalgslist: str, /) -> None: ... if sys.version_info >= (3, 13): def set_psk_client_callback(self, callback: Callable[[str | None], tuple[str | None, bytes]] | None) -> None: ... def set_psk_server_callback( @@ -145,18 +152,18 @@ class SSLSession: # # You can find a _ssl._SSLSocket object as the _sslobj attribute of a ssl.SSLSocket object -if sys.version_info >= (3, 10): - @final - class Certificate: - def get_info(self) -> _CertInfo: ... - @overload - def public_bytes(self) -> str: ... - @overload - def public_bytes(self, format: Literal[1] = 1, /) -> str: ... # ENCODING_PEM - @overload - def public_bytes(self, format: Literal[2], /) -> bytes: ... # ENCODING_DER - @overload - def public_bytes(self, format: int, /) -> str | bytes: ... +@final +class Certificate: + def get_info(self) -> _CertInfo: ... + + @overload + def public_bytes(self) -> str: ... + @overload + def public_bytes(self, format: Literal[1] = 1, /) -> str: ... # ENCODING_PEM + @overload + def public_bytes(self, format: Literal[2], /) -> bytes: ... # ENCODING_DER + @overload + def public_bytes(self, format: int, /) -> str | bytes: ... if sys.version_info < (3, 12): err_codes_to_names: dict[tuple[int, int], str] @@ -187,9 +194,8 @@ VERIFY_CRL_CHECK_LEAF: Final = 0x04 VERIFY_CRL_CHECK_CHAIN: Final = 0x0C VERIFY_X509_STRICT: Final = 0x20 VERIFY_X509_TRUSTED_FIRST: Final = 0x8000 -if sys.version_info >= (3, 10): - VERIFY_ALLOW_PROXY_CERTS: Final = 0x40 - VERIFY_X509_PARTIAL_CHAIN: Final = 0x80000 +VERIFY_ALLOW_PROXY_CERTS: Final = 0x40 +VERIFY_X509_PARTIAL_CHAIN: Final = 0x80000 # alert descriptions ALERT_DESCRIPTION_CLOSE_NOTIFY: Final = 0 @@ -258,10 +264,9 @@ HOSTFLAG_NO_PARTIAL_WILDCARDS: Final = 0x4 HOSTFLAG_MULTI_LABEL_WILDCARDS: Final = 0x8 HOSTFLAG_SINGLE_LABEL_SUBDOMAINS: Final = 0x10 -if sys.version_info >= (3, 10): - # certificate file types - ENCODING_PEM: Final = 1 - ENCODING_DER: Final = 2 +# certificate file types +ENCODING_PEM: Final = 1 +ENCODING_DER: Final = 2 # protocol versions PROTO_MINIMUM_SUPPORTED: Final = -2 @@ -279,6 +284,8 @@ HAS_ECDH: Final[bool] HAS_NPN: Final[bool] if sys.version_info >= (3, 13): HAS_PSK: Final[bool] +if sys.version_info >= (3, 15): + HAS_PSK_TLS13: Final[bool] HAS_ALPN: Final[bool] HAS_SSLv2: Final[bool] HAS_SSLv3: Final[bool] diff --git a/mypy/typeshed/stdlib/_struct.pyi b/mypy/typeshed/stdlib/_struct.pyi index a8fac2aea1b00..c13e440e10aa9 100644 --- a/mypy/typeshed/stdlib/_struct.pyi +++ b/mypy/typeshed/stdlib/_struct.pyi @@ -9,6 +9,7 @@ def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> tuple[Any, ...]: . def unpack_from(format: str | bytes, /, buffer: ReadableBuffer, offset: int = 0) -> tuple[Any, ...]: ... def iter_unpack(format: str | bytes, buffer: ReadableBuffer, /) -> Iterator[tuple[Any, ...]]: ... def calcsize(format: str | bytes, /) -> int: ... + @disjoint_base class Struct: @property diff --git a/mypy/typeshed/stdlib/_thread.pyi b/mypy/typeshed/stdlib/_thread.pyi index 1323a55e9aad8..e2d257c9ec0ba 100644 --- a/mypy/typeshed/stdlib/_thread.pyi +++ b/mypy/typeshed/stdlib/_thread.pyi @@ -12,6 +12,7 @@ _Ts = TypeVarTuple("_Ts") error = RuntimeError def _count() -> int: ... + @final class RLock: def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: ... @@ -33,6 +34,7 @@ if sys.version_info >= (3, 13): def start_joinable_thread( function: Callable[[], object], handle: _ThreadHandle | None = None, daemon: bool = True ) -> _ThreadHandle: ... + @final class lock: def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: ... @@ -71,6 +73,7 @@ else: def start_new_thread(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts]], /) -> int: ... @overload def start_new_thread(function: Callable[..., object], args: tuple[Any, ...], kwargs: dict[str, Any], /) -> int: ... + @overload @deprecated("Obsolete synonym. Use `start_new_thread()` instead.") def start_new(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts]], /) -> int: ... # undocumented @@ -78,12 +81,7 @@ def start_new(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts] @deprecated("Obsolete synonym. Use `start_new_thread()` instead.") def start_new(function: Callable[..., object], args: tuple[Any, ...], kwargs: dict[str, Any], /) -> int: ... # undocumented -if sys.version_info >= (3, 10): - def interrupt_main(signum: signal.Signals = signal.SIGINT, /) -> None: ... - -else: - def interrupt_main() -> None: ... - +def interrupt_main(signum: signal.Signals = signal.SIGINT, /) -> None: ... def exit() -> NoReturn: ... @deprecated("Obsolete synonym. Use `exit()` instead.") def exit_thread() -> NoReturn: ... # undocumented @@ -96,10 +94,10 @@ def stack_size(size: int = 0, /) -> int: ... TIMEOUT_MAX: Final[float] def get_native_id() -> int: ... # only available on some platforms + @final class _ExceptHookArgs(structseq[Any], tuple[type[BaseException], BaseException | None, TracebackType | None, Thread | None]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("exc_type", "exc_value", "exc_traceback", "thread") + __match_args__: Final = ("exc_type", "exc_value", "exc_traceback", "thread") @property def exc_type(self) -> type[BaseException]: ... diff --git a/mypy/typeshed/stdlib/_threading_local.pyi b/mypy/typeshed/stdlib/_threading_local.pyi index 5f6acaf840aa1..7d4e61c6f2ec8 100644 --- a/mypy/typeshed/stdlib/_threading_local.pyi +++ b/mypy/typeshed/stdlib/_threading_local.pyi @@ -1,6 +1,6 @@ from threading import RLock -from typing import Any -from typing_extensions import Self, TypeAlias +from typing import Any, TypeAlias +from typing_extensions import Self from weakref import ReferenceType __all__ = ["local"] diff --git a/mypy/typeshed/stdlib/_tkinter.pyi b/mypy/typeshed/stdlib/_tkinter.pyi index 5e46668e08b1c..500345f91e063 100644 --- a/mypy/typeshed/stdlib/_tkinter.pyi +++ b/mypy/typeshed/stdlib/_tkinter.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import FileDescriptorLike, Incomplete from collections.abc import Callable -from typing import Any, ClassVar, Final, Literal, final, overload -from typing_extensions import TypeAlias, deprecated +from typing import Any, ClassVar, Final, Literal, TypeAlias, final, overload +from typing_extensions import deprecated # _tkinter is meant to be only used internally by tkinter, but some tkinter # functions e.g. return _tkinter.Tcl_Obj objects. Tcl_Obj represents a Tcl @@ -91,6 +91,7 @@ class TkappType: def splitlist(self, arg, /) -> tuple[Incomplete, ...]: ... def unsetvar(self, *args, **kwargs): ... + if sys.version_info >= (3, 14): @overload def wantobjects(self) -> Literal[0, 1]: ... @@ -100,6 +101,7 @@ class TkappType: @overload def wantobjects(self, wantobjects: Literal[0, 1] | bool, /) -> None: ... + def willdispatch(self) -> None: ... if sys.version_info >= (3, 12): def gettrace(self, /) -> _TkinterTraceFunc | None: ... diff --git a/mypy/typeshed/stdlib/_typeshed/__init__.pyi b/mypy/typeshed/stdlib/_typeshed/__init__.pyi index c006322b81451..2f65d8aab9c51 100644 --- a/mypy/typeshed/stdlib/_typeshed/__init__.pyi +++ b/mypy/typeshed/stdlib/_typeshed/__init__.pyi @@ -6,7 +6,7 @@ import sys from collections.abc import Awaitable, Callable, Iterable, Iterator, Sequence, Set as AbstractSet, Sized from dataclasses import Field from os import PathLike -from types import FrameType, TracebackType +from types import FrameType, NoneType as NoneType, TracebackType from typing import ( Any, AnyStr, @@ -18,11 +18,11 @@ from typing import ( SupportsFloat, SupportsIndex, SupportsInt, + TypeAlias, TypeVar, - final, overload, ) -from typing_extensions import Buffer, LiteralString, Self as _Self, TypeAlias +from typing_extensions import Buffer, LiteralString, Self as _Self _KT = TypeVar("_KT") _KT_co = TypeVar("_KT_co", covariant=True) @@ -313,6 +313,7 @@ class IndexableBuffer(Buffer, Protocol): class SupportsGetItemBuffer(SliceableBuffer, IndexableBuffer, Protocol): def __contains__(self, x: Any, /) -> bool: ... + @overload def __getitem__(self, slice: slice[SupportsIndex | None], /) -> Sequence[int]: ... @overload @@ -323,15 +324,6 @@ class SizedBuffer(Sized, Buffer, Protocol): ... ExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType] OptExcInfo: TypeAlias = ExcInfo | tuple[None, None, None] -# stable -if sys.version_info >= (3, 10): - from types import NoneType as NoneType -else: - # Used by type checkers for checks involving None (does not exist at runtime) - @final - class NoneType: - def __bool__(self) -> Literal[False]: ... - # This is an internal CPython type that is like, but subtly different from, a NamedTuple # Subclasses of this type are found in multiple modules. # In typeshed, `structseq` is only ever used as a mixin in combination with a fixed-length `Tuple` @@ -359,10 +351,12 @@ AnyOrLiteralStr = TypeVar("AnyOrLiteralStr", str, bytes, LiteralString) # noqa: StrOrLiteralStr = TypeVar("StrOrLiteralStr", LiteralString, str) # noqa: Y001 # Objects suitable to be passed to sys.setprofile, threading.setprofile, and similar -ProfileFunction: TypeAlias = Callable[[FrameType, str, Any], object] +ProfileFunction: TypeAlias = Callable[[FrameType, Literal["call", "return", "c_call", "c_return", "c_exception"], Any], object] # Objects suitable to be passed to sys.settrace, threading.settrace, and similar -TraceFunction: TypeAlias = Callable[[FrameType, str, Any], TraceFunction | None] +TraceFunction: TypeAlias = Callable[ + [FrameType, Literal["call", "line", "return", "exception", "opcode"], Any], TraceFunction | None +] # experimental # Might not work as expected for pyright, see diff --git a/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi b/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi index feb22aae00732..375e997e2c932 100644 --- a/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi +++ b/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi @@ -40,14 +40,17 @@ class TypedDictFallback(Mapping[str, object], metaclass=ABCMeta): def items(self) -> dict_items[str, object]: ... def keys(self) -> dict_keys[str, object]: ... def values(self) -> dict_values[str, object]: ... + @overload def __or__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... @overload def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ... + @overload def __ror__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... @overload def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ... + # supposedly incompatible definitions of __or__ and __ior__ def __ior__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... # type: ignore[misc] @@ -67,6 +70,7 @@ class NamedTupleFallback(tuple[Any, ...]): "Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15" ) def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ... + @classmethod def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... def _asdict(self) -> dict[str, Any]: ... diff --git a/mypy/typeshed/stdlib/_typeshed/dbapi.pyi b/mypy/typeshed/stdlib/_typeshed/dbapi.pyi index d54fbee57042a..e08a84553dfc0 100644 --- a/mypy/typeshed/stdlib/_typeshed/dbapi.pyi +++ b/mypy/typeshed/stdlib/_typeshed/dbapi.pyi @@ -2,8 +2,7 @@ # https://www.python.org/dev/peps/pep-0249/ from collections.abc import Mapping, Sequence -from typing import Any, Protocol -from typing_extensions import TypeAlias +from typing import Any, Protocol, TypeAlias DBAPITypeCode: TypeAlias = Any | None # Strictly speaking, this should be a Sequence, but the type system does diff --git a/mypy/typeshed/stdlib/_typeshed/wsgi.pyi b/mypy/typeshed/stdlib/_typeshed/wsgi.pyi index 63f204eb889b6..980a24122252e 100644 --- a/mypy/typeshed/stdlib/_typeshed/wsgi.pyi +++ b/mypy/typeshed/stdlib/_typeshed/wsgi.pyi @@ -7,8 +7,7 @@ import sys from _typeshed import OptExcInfo from collections.abc import Callable, Iterable, Iterator -from typing import Any, Protocol -from typing_extensions import TypeAlias +from typing import Any, Protocol, TypeAlias class _Readable(Protocol): def read(self, size: int = ..., /) -> bytes: ... diff --git a/mypy/typeshed/stdlib/_warnings.pyi b/mypy/typeshed/stdlib/_warnings.pyi index 2dbc7b8552813..5f4648259025d 100644 --- a/mypy/typeshed/stdlib/_warnings.pyi +++ b/mypy/typeshed/stdlib/_warnings.pyi @@ -24,7 +24,6 @@ if sys.version_info >= (3, 12): *, skip_file_prefixes: tuple[str, ...] = (), ) -> None: ... - else: @overload def warn(message: str, category: type[Warning] | None = None, stacklevel: int = 1, source: Any | None = None) -> None: ... diff --git a/mypy/typeshed/stdlib/_weakrefset.pyi b/mypy/typeshed/stdlib/_weakrefset.pyi index dad1ed7a4fb5c..82ffa4463f488 100644 --- a/mypy/typeshed/stdlib/_weakrefset.pyi +++ b/mypy/typeshed/stdlib/_weakrefset.pyi @@ -13,6 +13,7 @@ class WeakSet(MutableSet[_T]): def __init__(self, data: None = None) -> None: ... @overload def __init__(self, data: Iterable[_T]) -> None: ... + def add(self, item: _T) -> None: ... def discard(self, item: _T) -> None: ... def copy(self) -> Self: ... diff --git a/mypy/typeshed/stdlib/_winapi.pyi b/mypy/typeshed/stdlib/_winapi.pyi index 42efce9bed705..7fd918d9b1e99 100644 --- a/mypy/typeshed/stdlib/_winapi.pyi +++ b/mypy/typeshed/stdlib/_winapi.pyi @@ -29,6 +29,14 @@ if sys.platform == "win32": ERROR_PIPE_CONNECTED: Final = 535 ERROR_SEM_TIMEOUT: Final = 121 + if sys.version_info >= (3, 15): + EVENTLOG_AUDIT_FAILURE: Final = 16 + EVENTLOG_AUDIT_SUCCESS: Final = 8 + EVENTLOG_ERROR_TYPE: Final = 1 + EVENTLOG_INFORMATION_TYPE: Final = 4 + EVENTLOG_SUCCESS: Final = 0 + EVENTLOG_WARNING_TYPE: Final = 2 + FILE_FLAG_FIRST_PIPE_INSTANCE: Final = 0x80000 FILE_FLAG_OVERLAPPED: Final = 0x40000000 @@ -127,22 +135,21 @@ if sys.platform == "win32": WAIT_OBJECT_0: Final = 0 WAIT_TIMEOUT: Final = 258 - if sys.version_info >= (3, 10): - LOCALE_NAME_INVARIANT: Final[str] - LOCALE_NAME_MAX_LENGTH: Final[int] - LOCALE_NAME_SYSTEM_DEFAULT: Final[str] - LOCALE_NAME_USER_DEFAULT: Final[str | None] - - LCMAP_FULLWIDTH: Final[int] - LCMAP_HALFWIDTH: Final[int] - LCMAP_HIRAGANA: Final[int] - LCMAP_KATAKANA: Final[int] - LCMAP_LINGUISTIC_CASING: Final[int] - LCMAP_LOWERCASE: Final[int] - LCMAP_SIMPLIFIED_CHINESE: Final[int] - LCMAP_TITLECASE: Final[int] - LCMAP_TRADITIONAL_CHINESE: Final[int] - LCMAP_UPPERCASE: Final[int] + LOCALE_NAME_INVARIANT: Final[str] + LOCALE_NAME_MAX_LENGTH: Final[int] + LOCALE_NAME_SYSTEM_DEFAULT: Final[str] + LOCALE_NAME_USER_DEFAULT: Final[str | None] + + LCMAP_FULLWIDTH: Final[int] + LCMAP_HALFWIDTH: Final[int] + LCMAP_HIRAGANA: Final[int] + LCMAP_KATAKANA: Final[int] + LCMAP_LINGUISTIC_CASING: Final[int] + LCMAP_LOWERCASE: Final[int] + LCMAP_SIMPLIFIED_CHINESE: Final[int] + LCMAP_TITLECASE: Final[int] + LCMAP_TRADITIONAL_CHINESE: Final[int] + LCMAP_UPPERCASE: Final[int] if sys.version_info >= (3, 12): COPYFILE2_CALLBACK_CHUNK_STARTED: Final = 1 @@ -176,12 +183,14 @@ if sys.platform == "win32": COPY_FILE_DIRECTORY: Final = 0x00000080 def CloseHandle(handle: int, /) -> None: ... + @overload def ConnectNamedPipe(handle: int, overlapped: Literal[True]) -> Overlapped: ... @overload def ConnectNamedPipe(handle: int, overlapped: Literal[False] = False) -> None: ... @overload def ConnectNamedPipe(handle: int, overlapped: bool) -> Overlapped | None: ... + def CreateFile( file_name: str, desired_access: int, @@ -231,6 +240,10 @@ if sys.platform == "win32": ) -> int: ... def ExitProcess(ExitCode: int, /) -> NoReturn: ... def GetACP() -> int: ... + if sys.version_info >= (3, 15): + def DeregisterEventSource(handle: int, /) -> None: ... + def GetOEMCP() -> int: ... + def GetFileType(handle: int) -> int: ... def GetCurrentProcess() -> int: ... def GetExitCodeProcess(process: int, /) -> int: ... @@ -243,9 +256,12 @@ if sys.platform == "win32": ) -> int: ... def OpenProcess(desired_access: int, inherit_handle: bool, process_id: int, /) -> int: ... def PeekNamedPipe(handle: int, size: int = 0, /) -> tuple[int, int] | tuple[bytes, int, int]: ... - if sys.version_info >= (3, 10): - def LCMapStringEx(locale: str, flags: int, src: str) -> str: ... - def UnmapViewOfFile(address: int, /) -> None: ... + def LCMapStringEx(locale: str, flags: int, src: str) -> str: ... + if sys.version_info >= (3, 15): + def RegisterEventSource(unc_server_name: str | None, source_name: str, /) -> int: ... + def ReportEvent(handle: int, type: int, category: int, event_id: int, string: str, /) -> None: ... + + def UnmapViewOfFile(address: int, /) -> None: ... @overload def ReadFile(handle: int, size: int, overlapped: Literal[True]) -> tuple[Overlapped, int]: ... @@ -253,6 +269,7 @@ if sys.platform == "win32": def ReadFile(handle: int, size: int, overlapped: Literal[False] = False) -> tuple[bytes, int]: ... @overload def ReadFile(handle: int, size: int, overlapped: int | bool) -> tuple[Any, int]: ... + def SetNamedPipeHandleState( named_pipe: int, mode: int | None, max_collection_count: int | None, collect_data_timeout: int | None, / ) -> None: ... @@ -261,12 +278,14 @@ if sys.platform == "win32": def WaitForMultipleObjects(handle_seq: Sequence[int], wait_flag: bool, milliseconds: int = 0xFFFFFFFF, /) -> int: ... def WaitForSingleObject(handle: int, milliseconds: int, /) -> int: ... def WaitNamedPipe(name: str, timeout: int, /) -> None: ... + @overload def WriteFile(handle: int, buffer: ReadableBuffer, overlapped: Literal[True]) -> tuple[Overlapped, int]: ... @overload def WriteFile(handle: int, buffer: ReadableBuffer, overlapped: Literal[False] = False) -> tuple[int, int]: ... @overload def WriteFile(handle: int, buffer: ReadableBuffer, overlapped: int | bool) -> tuple[Any, int]: ... + @final class Overlapped: event: int diff --git a/mypy/typeshed/stdlib/_zstd.pyi b/mypy/typeshed/stdlib/_zstd.pyi index e40c7d12b6e77..34b619f9b0b62 100644 --- a/mypy/typeshed/stdlib/_zstd.pyi +++ b/mypy/typeshed/stdlib/_zstd.pyi @@ -1,8 +1,8 @@ from _typeshed import ReadableBuffer from collections.abc import Mapping from compression.zstd import CompressionParameter, DecompressionParameter -from typing import Final, Literal, final -from typing_extensions import Self, TypeAlias +from typing import Final, Literal, TypeAlias, final +from typing_extensions import Self ZSTD_CLEVEL_DEFAULT: Final = 3 ZSTD_DStreamOutSize: Final = 131072 @@ -74,7 +74,7 @@ class ZstdDecompressor: @final class ZstdDict: - def __new__(cls, dict_content: bytes, /, *, is_raw: bool = False) -> Self: ... + def __new__(cls, dict_content: ReadableBuffer, /, *, is_raw: bool = False) -> Self: ... def __len__(self, /) -> int: ... @property def as_digested_dict(self) -> tuple[Self, int]: ... diff --git a/mypy/typeshed/stdlib/abc.pyi b/mypy/typeshed/stdlib/abc.pyi index 7e76abace214e..a43a641489a43 100644 --- a/mypy/typeshed/stdlib/abc.pyi +++ b/mypy/typeshed/stdlib/abc.pyi @@ -2,8 +2,8 @@ import _typeshed import sys from _typeshed import SupportsWrite from collections.abc import Callable -from typing import Any, Literal, TypeVar -from typing_extensions import Concatenate, ParamSpec, deprecated +from typing import Any, Concatenate, Literal, ParamSpec, TypeVar +from typing_extensions import deprecated _T = TypeVar("_T") _R_co = TypeVar("_R_co", covariant=True) @@ -28,6 +28,7 @@ class ABCMeta(type): def register(cls: ABCMeta, subclass: type[_T]) -> type[_T]: ... def abstractmethod(funcobj: _FuncT) -> _FuncT: ... + @deprecated("Deprecated since Python 3.3. Use `@classmethod` stacked on top of `@abstractmethod` instead.") class abstractclassmethod(classmethod[_T, _P, _R_co]): __isabstractmethod__: Literal[True] @@ -46,6 +47,4 @@ class ABC(metaclass=ABCMeta): __slots__ = () def get_cache_token() -> object: ... - -if sys.version_info >= (3, 10): - def update_abstractmethods(cls: type[_T]) -> type[_T]: ... +def update_abstractmethods(cls: type[_T]) -> type[_T]: ... diff --git a/mypy/typeshed/stdlib/aifc.pyi b/mypy/typeshed/stdlib/aifc.pyi index bfe12c6af2b0b..afb9029f710ea 100644 --- a/mypy/typeshed/stdlib/aifc.pyi +++ b/mypy/typeshed/stdlib/aifc.pyi @@ -1,6 +1,6 @@ from types import TracebackType -from typing import IO, Any, Literal, NamedTuple, overload -from typing_extensions import Self, TypeAlias +from typing import IO, Any, Literal, NamedTuple, TypeAlias, overload +from typing_extensions import Self __all__ = ["Error", "open"] diff --git a/mypy/typeshed/stdlib/annotationlib.pyi b/mypy/typeshed/stdlib/annotationlib.pyi index 3679dc29daaa0..c3e843d95d9a8 100644 --- a/mypy/typeshed/stdlib/annotationlib.pyi +++ b/mypy/typeshed/stdlib/annotationlib.pyi @@ -41,13 +41,16 @@ if sys.version_info >= (3, 14): "__cell__", "__owner__", "__stringifier_dict__", + "__resolved_str_cache__", ) __forward_is_argument__: bool __forward_is_class__: bool __forward_module__: str | None + __resolved_str_cache__: str | None def __init__( self, arg: str, *, module: str | None = None, owner: object = None, is_argument: bool = True, is_class: bool = False ) -> None: ... + @overload def evaluate( self, @@ -78,6 +81,7 @@ if sys.version_info >= (3, 14): owner: object = None, format: Format = Format.VALUE, # noqa: Y011 ) -> AnnotationForm: ... + @deprecated("Use `ForwardRef.evaluate()` or `typing.evaluate_forward_ref()` instead.") def _evaluate( self, @@ -91,6 +95,8 @@ if sys.version_info >= (3, 14): def __forward_arg__(self) -> str: ... @property def __forward_code__(self) -> types.CodeType: ... + @property + def __resolved_str__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __hash__(self) -> int: ... def __or__(self, other: Any) -> types.UnionType: ... @@ -104,6 +110,7 @@ if sys.version_info >= (3, 14): ) -> AnnotationForm | ForwardRef: ... @overload def call_evaluate_function(evaluate: EvaluateFunc, format: Format, *, owner: object = None) -> AnnotationForm: ... + @overload def call_annotate_function( annotate: AnnotateFunc, format: Literal[Format.STRING], *, owner: object = None @@ -114,7 +121,9 @@ if sys.version_info >= (3, 14): ) -> dict[str, AnnotationForm | ForwardRef]: ... @overload def call_annotate_function(annotate: AnnotateFunc, format: Format, *, owner: object = None) -> dict[str, AnnotationForm]: ... + def get_annotate_from_class_namespace(obj: Mapping[str, object]) -> AnnotateFunc | None: ... + @overload def get_annotations( obj: Any, # any object with __annotations__ or __annotate__ @@ -142,5 +151,6 @@ if sys.version_info >= (3, 14): eval_str: bool = False, format: Format = Format.VALUE, # noqa: Y011 ) -> dict[str, AnnotationForm]: ... + def type_repr(value: object) -> str: ... def annotations_to_string(annotations: SupportsItems[str, object]) -> dict[str, str]: ... diff --git a/mypy/typeshed/stdlib/argparse.pyi b/mypy/typeshed/stdlib/argparse.pyi index 7d4bd1a3a8418..fa22f842de028 100644 --- a/mypy/typeshed/stdlib/argparse.pyi +++ b/mypy/typeshed/stdlib/argparse.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import SupportsWrite, sentinel from collections.abc import Callable, Generator, Iterable, Sequence from re import Pattern -from typing import IO, Any, ClassVar, Final, Generic, NewType, NoReturn, Protocol, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated +from typing import IO, Any, ClassVar, Final, Generic, NewType, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, deprecated __all__ = [ "ArgumentParser", @@ -93,39 +93,28 @@ class _ActionsContainer: version: str = ..., **kwargs: Any, ) -> Action: ... - if sys.version_info >= (3, 14): - @overload - def add_argument_group( - self, - title: str | None = None, - description: str | None = None, - *, - # argument_default's type must be valid for the arguments in the group - argument_default: Any = ..., - conflict_handler: str = ..., - ) -> _ArgumentGroup: ... - @overload - @deprecated("The `prefix_chars` parameter deprecated since Python 3.14.") - def add_argument_group( - self, - title: str | None = None, - description: str | None = None, - *, - prefix_chars: str, - argument_default: Any = ..., - conflict_handler: str = ..., - ) -> _ArgumentGroup: ... - else: - def add_argument_group( - self, - title: str | None = None, - description: str | None = None, - *, - prefix_chars: str = ..., - # argument_default's type must be valid for the arguments in the group - argument_default: Any = ..., - conflict_handler: str = ..., - ) -> _ArgumentGroup: ... + + @overload + def add_argument_group( + self, + title: str | None = None, + description: str | None = None, + *, + # argument_default's type must be valid for the arguments in the group + argument_default: Any = ..., + conflict_handler: str = ..., + ) -> _ArgumentGroup: ... + @overload + @deprecated("The `prefix_chars` parameter deprecated since Python 3.14.") + def add_argument_group( + self, + title: str | None = None, + description: str | None = None, + *, + prefix_chars: str, + argument_default: Any = ..., + conflict_handler: str = ..., + ) -> _ArgumentGroup: ... def add_mutually_exclusive_group(self, *, required: bool = False) -> _MutuallyExclusiveGroup: ... def _add_action(self, action: _ActionT) -> _ActionT: ... @@ -163,7 +152,28 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): _subparsers: _ArgumentGroup | None # Note: the constructor arguments are also used in _SubParsersAction.add_parser. - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): + def __init__( + self, + prog: str | None = None, + usage: str | None = None, + description: str | None = None, + epilog: str | None = None, + parents: Iterable[ArgumentParser] = [], + formatter_class: _FormatterClass = ..., + prefix_chars: str = "-", + fromfile_prefix_chars: str | None = None, + argument_default: Any = None, + conflict_handler: str = "error", + add_help: bool = True, + allow_abbrev: bool = True, + exit_on_error: bool = True, + *, + suggest_on_error: bool = True, + color: bool = True, + ) -> None: ... + + elif sys.version_info >= (3, 14): def __init__( self, prog: str | None = None, @@ -207,6 +217,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): def parse_args(self, args: Iterable[str] | None, namespace: _N) -> _N: ... @overload def parse_args(self, *, namespace: _N) -> _N: ... + @overload def add_subparsers( self: _ArgumentParserT, @@ -236,25 +247,35 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): help: str | None = None, metavar: str | None = None, ) -> _SubParsersAction[_ArgumentParserT]: ... + def print_usage(self, file: SupportsWrite[str] | None = None) -> None: ... def print_help(self, file: SupportsWrite[str] | None = None) -> None: ... - def format_usage(self) -> str: ... - def format_help(self) -> str: ... + if sys.version_info >= (3, 15): + def format_usage(self, formatter: HelpFormatter | None = None) -> str: ... + def format_help(self, formatter: HelpFormatter | None = None) -> str: ... + + else: + def format_usage(self) -> str: ... + def format_help(self) -> str: ... + @overload def parse_known_args(self, args: Iterable[str] | None = None, namespace: None = None) -> tuple[Namespace, list[str]]: ... @overload def parse_known_args(self, args: Iterable[str] | None, namespace: _N) -> tuple[_N, list[str]]: ... @overload def parse_known_args(self, *, namespace: _N) -> tuple[_N, list[str]]: ... + def convert_arg_line_to_args(self, arg_line: str) -> list[str]: ... def exit(self, status: int = 0, message: str | None = None) -> NoReturn: ... def error(self, message: str) -> NoReturn: ... + @overload def parse_intermixed_args(self, args: Iterable[str] | None = None, namespace: None = None) -> Namespace: ... @overload def parse_intermixed_args(self, args: Iterable[str] | None, namespace: _N) -> _N: ... @overload def parse_intermixed_args(self, *, namespace: _N) -> _N: ... + @overload def parse_known_intermixed_args( self, args: Iterable[str] | None = None, namespace: None = None @@ -263,6 +284,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): def parse_known_intermixed_args(self, args: Iterable[str] | None, namespace: _N) -> tuple[_N, list[str]]: ... @overload def parse_known_intermixed_args(self, *, namespace: _N) -> tuple[_N, list[str]]: ... + # undocumented def _get_optional_actions(self) -> list[Action]: ... def _get_positional_actions(self) -> list[Action]: ... @@ -286,7 +308,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): def _get_values(self, action: Action, arg_strings: list[str]) -> Any: ... def _get_value(self, action: Action, arg_string: str) -> Any: ... def _check_value(self, action: Action, value: Any) -> None: ... - def _get_formatter(self) -> HelpFormatter: ... + if sys.version_info >= (3, 15): + def _get_formatter(self, file: SupportsWrite[str] | None = None) -> HelpFormatter: ... + else: + def _get_formatter(self) -> HelpFormatter: ... + def _print_message(self, message: str, file: SupportsWrite[str] | None = None) -> None: ... class HelpFormatter: @@ -311,7 +337,12 @@ class HelpFormatter: def __init__(self, formatter: HelpFormatter, parent: Self | None, heading: str | None = None) -> None: ... def format_help(self) -> str: ... - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): + def __init__( + self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None + ) -> None: ... + + elif sys.version_info >= (3, 14): def __init__( self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None, color: bool = True ) -> None: ... @@ -501,69 +532,43 @@ class Namespace(_AttributeHolder): def __eq__(self, other: object) -> bool: ... __hash__: ClassVar[None] # type: ignore[assignment] -if sys.version_info >= (3, 14): - @deprecated("Deprecated since Python 3.14. Open files after parsing arguments instead.") - class FileType: - # undocumented - _mode: str - _bufsize: int - _encoding: str | None - _errors: str | None - def __init__( - self, mode: str = "r", bufsize: int = -1, encoding: str | None = None, errors: str | None = None - ) -> None: ... - def __call__(self, string: str) -> IO[Any]: ... - -else: - class FileType: - # undocumented - _mode: str - _bufsize: int - _encoding: str | None - _errors: str | None - def __init__( - self, mode: str = "r", bufsize: int = -1, encoding: str | None = None, errors: str | None = None - ) -> None: ... - def __call__(self, string: str) -> IO[Any]: ... +@deprecated("Deprecated since Python 3.14. Open files after parsing arguments instead.") +class FileType: + # undocumented + _mode: str + _bufsize: int + _encoding: str | None + _errors: str | None + def __init__(self, mode: str = "r", bufsize: int = -1, encoding: str | None = None, errors: str | None = None) -> None: ... + def __call__(self, string: str) -> IO[Any]: ... # undocumented class _ArgumentGroup(_ActionsContainer): title: str | None _group_actions: list[Action] - if sys.version_info >= (3, 14): - @overload - def __init__( - self, - container: _ActionsContainer, - title: str | None = None, - description: str | None = None, - *, - argument_default: Any = ..., - conflict_handler: str = ..., - ) -> None: ... - @overload - @deprecated("Undocumented `prefix_chars` parameter is deprecated since Python 3.14.") - def __init__( - self, - container: _ActionsContainer, - title: str | None = None, - description: str | None = None, - *, - prefix_chars: str, - argument_default: Any = ..., - conflict_handler: str = ..., - ) -> None: ... - else: - def __init__( - self, - container: _ActionsContainer, - title: str | None = None, - description: str | None = None, - *, - prefix_chars: str = ..., - argument_default: Any = ..., - conflict_handler: str = ..., - ) -> None: ... + + @overload + def __init__( + self, + container: _ActionsContainer, + title: str | None = None, + description: str | None = None, + *, + argument_default: Any = ..., + conflict_handler: str = ..., + ) -> None: ... + @overload + @deprecated("Undocumented `prefix_chars` parameter is deprecated since Python 3.14.") + def __init__( + self, + container: _ActionsContainer, + title: str | None = None, + description: str | None = None, + *, + prefix_chars: str, + argument_default: Any = ..., + conflict_handler: str = ..., + ) -> None: ... # undocumented class _MutuallyExclusiveGroup(_ArgumentGroup): diff --git a/mypy/typeshed/stdlib/array.pyi b/mypy/typeshed/stdlib/array.pyi index eb679dd50f722..2c83146edbf00 100644 --- a/mypy/typeshed/stdlib/array.pyi +++ b/mypy/typeshed/stdlib/array.pyi @@ -2,11 +2,14 @@ import sys from _typeshed import ReadableBuffer, SupportsRead, SupportsWrite from collections.abc import Iterable, MutableSequence from types import GenericAlias -from typing import Any, ClassVar, Literal, SupportsIndex, TypeVar, overload -from typing_extensions import Self, TypeAlias, deprecated, disjoint_base +from typing import Any, ClassVar, Literal, SupportsIndex, TypeAlias, TypeVar, overload +from typing_extensions import Self, deprecated, disjoint_base _IntTypeCode: TypeAlias = Literal["b", "B", "h", "H", "i", "I", "l", "L", "q", "Q"] -_FloatTypeCode: TypeAlias = Literal["f", "d"] +if sys.version_info >= (3, 15): + _FloatTypeCode: TypeAlias = Literal["f", "d", "e", "Zf", "Zd"] +else: + _FloatTypeCode: TypeAlias = Literal["f", "d"] if sys.version_info >= (3, 13): _UnicodeTypeCode: TypeAlias = Literal["u", "w"] else: @@ -15,7 +18,10 @@ _TypeCode: TypeAlias = _IntTypeCode | _FloatTypeCode | _UnicodeTypeCode _T = TypeVar("_T", int, float, str) -typecodes: str +if sys.version_info >= (3, 15): + typecodes: tuple[str, ...] +else: + typecodes: str @disjoint_base class array(MutableSequence[_T]): @@ -23,6 +29,7 @@ class array(MutableSequence[_T]): def typecode(self) -> _TypeCode: ... @property def itemsize(self) -> int: ... + @overload def __new__( cls: type[array[int]], typecode: _IntTypeCode, initializer: bytes | bytearray | Iterable[int] = ..., / @@ -52,6 +59,7 @@ class array(MutableSequence[_T]): def __new__(cls, typecode: str, initializer: Iterable[_T], /) -> Self: ... @overload def __new__(cls, typecode: str, initializer: bytes | bytearray = ..., /) -> Self: ... + def append(self, v: _T, /) -> None: ... def buffer_info(self) -> tuple[int, int]: ... def byteswap(self) -> None: ... @@ -61,11 +69,7 @@ class array(MutableSequence[_T]): def fromfile(self, f: SupportsRead[bytes], n: int, /) -> None: ... def fromlist(self, list: list[_T], /) -> None: ... def fromunicode(self, ustr: str, /) -> None: ... - if sys.version_info >= (3, 10): - def index(self, v: _T, start: int = 0, stop: int = sys.maxsize, /) -> int: ... - else: - def index(self, v: _T, /) -> int: ... # type: ignore[override] - + def index(self, v: _T, start: int = 0, stop: int = sys.maxsize, /) -> int: ... def insert(self, i: int, v: _T, /) -> None: ... def pop(self, i: int = -1, /) -> _T: ... def remove(self, v: _T, /) -> None: ... @@ -77,14 +81,17 @@ class array(MutableSequence[_T]): __hash__: ClassVar[None] # type: ignore[assignment] def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... + @overload def __getitem__(self, key: SupportsIndex, /) -> _T: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> array[_T]: ... + @overload # type: ignore[override] def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ... @overload def __setitem__(self, key: slice[SupportsIndex | None], value: array[_T], /) -> None: ... + def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: ... def __add__(self, value: array[_T], /) -> array[_T]: ... def __eq__(self, value: object, /) -> bool: ... diff --git a/mypy/typeshed/stdlib/ast.pyi b/mypy/typeshed/stdlib/ast.pyi index e66e609ee6645..14a98b9a5fcaf 100644 --- a/mypy/typeshed/stdlib/ast.pyi +++ b/mypy/typeshed/stdlib/ast.pyi @@ -10,6 +10,7 @@ from _ast import ( ) from _typeshed import ReadableBuffer, Unused from collections.abc import Iterable, Iterator, Sequence +from types import EllipsisType from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar as _TypeVar, overload, type_check_only from typing_extensions import Self, Unpack, deprecated, disjoint_base @@ -44,16 +45,14 @@ if sys.version_info >= (3, 12): else: class AST: - if sys.version_info >= (3, 10): - __match_args__ = () + __match_args__ = () _attributes: ClassVar[tuple[str, ...]] _fields: ClassVar[tuple[str, ...]] class mod(AST): ... class Module(mod): - if sys.version_info >= (3, 10): - __match_args__ = ("body", "type_ignores") + __match_args__ = ("body", "type_ignores") body: list[stmt] type_ignores: list[TypeIgnore] if sys.version_info >= (3, 13): @@ -65,8 +64,7 @@ class Module(mod): def __replace__(self, *, body: list[stmt] = ..., type_ignores: list[TypeIgnore] = ...) -> Self: ... class Interactive(mod): - if sys.version_info >= (3, 10): - __match_args__ = ("body",) + __match_args__ = ("body",) body: list[stmt] if sys.version_info >= (3, 13): def __init__(self, body: list[stmt] = ...) -> None: ... @@ -77,8 +75,7 @@ class Interactive(mod): def __replace__(self, *, body: list[stmt] = ...) -> Self: ... class Expression(mod): - if sys.version_info >= (3, 10): - __match_args__ = ("body",) + __match_args__ = ("body",) body: expr def __init__(self, body: expr) -> None: ... @@ -86,8 +83,7 @@ class Expression(mod): def __replace__(self, *, body: expr = ...) -> Self: ... class FunctionType(mod): - if sys.version_info >= (3, 10): - __match_args__ = ("argtypes", "returns") + __match_args__ = ("argtypes", "returns") argtypes: list[expr] returns: expr if sys.version_info >= (3, 13): @@ -114,7 +110,7 @@ class stmt(AST): class FunctionDef(stmt): if sys.version_info >= (3, 12): __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment", "type_params") - elif sys.version_info >= (3, 10): + else: __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment") name: str args: arguments @@ -191,7 +187,7 @@ class FunctionDef(stmt): class AsyncFunctionDef(stmt): if sys.version_info >= (3, 12): __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment", "type_params") - elif sys.version_info >= (3, 10): + else: __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment") name: str args: arguments @@ -268,7 +264,7 @@ class AsyncFunctionDef(stmt): class ClassDef(stmt): if sys.version_info >= (3, 12): __match_args__ = ("name", "bases", "keywords", "body", "decorator_list", "type_params") - elif sys.version_info >= (3, 10): + else: __match_args__ = ("name", "bases", "keywords", "body", "decorator_list") name: str bases: list[expr] @@ -324,8 +320,7 @@ class ClassDef(stmt): ) -> Self: ... class Return(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("value",) + __match_args__ = ("value",) value: expr | None def __init__(self, value: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ... @@ -333,8 +328,7 @@ class Return(stmt): def __replace__(self, *, value: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Delete(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("targets",) + __match_args__ = ("targets",) targets: list[expr] if sys.version_info >= (3, 13): def __init__(self, targets: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -345,8 +339,7 @@ class Delete(stmt): def __replace__(self, *, targets: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Assign(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("targets", "value", "type_comment") + __match_args__ = ("targets", "value", "type_comment") targets: list[expr] value: expr type_comment: str | None @@ -400,8 +393,7 @@ if sys.version_info >= (3, 12): ) -> Self: ... class AugAssign(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("target", "op", "value") + __match_args__ = ("target", "op", "value") target: Name | Attribute | Subscript op: operator value: expr @@ -420,12 +412,12 @@ class AugAssign(stmt): ) -> Self: ... class AnnAssign(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("target", "annotation", "value", "simple") + __match_args__ = ("target", "annotation", "value", "simple") target: Name | Attribute | Subscript annotation: expr value: expr | None simple: int + @overload def __init__( self, @@ -458,8 +450,7 @@ class AnnAssign(stmt): ) -> Self: ... class For(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("target", "iter", "body", "orelse", "type_comment") + __match_args__ = ("target", "iter", "body", "orelse", "type_comment") target: expr iter: expr body: list[stmt] @@ -499,8 +490,7 @@ class For(stmt): ) -> Self: ... class AsyncFor(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("target", "iter", "body", "orelse", "type_comment") + __match_args__ = ("target", "iter", "body", "orelse", "type_comment") target: expr iter: expr body: list[stmt] @@ -540,8 +530,7 @@ class AsyncFor(stmt): ) -> Self: ... class While(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("test", "body", "orelse") + __match_args__ = ("test", "body", "orelse") test: expr body: list[stmt] orelse: list[stmt] @@ -558,8 +547,7 @@ class While(stmt): ) -> Self: ... class If(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("test", "body", "orelse") + __match_args__ = ("test", "body", "orelse") test: expr body: list[stmt] orelse: list[stmt] @@ -576,8 +564,7 @@ class If(stmt): ) -> Self: ... class With(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("items", "body", "type_comment") + __match_args__ = ("items", "body", "type_comment") items: list[withitem] body: list[stmt] type_comment: str | None @@ -605,8 +592,7 @@ class With(stmt): ) -> Self: ... class AsyncWith(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("items", "body", "type_comment") + __match_args__ = ("items", "body", "type_comment") items: list[withitem] body: list[stmt] type_comment: str | None @@ -634,8 +620,7 @@ class AsyncWith(stmt): ) -> Self: ... class Raise(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("exc", "cause") + __match_args__ = ("exc", "cause") exc: expr | None cause: expr | None def __init__(self, exc: expr | None = None, cause: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ... @@ -644,8 +629,7 @@ class Raise(stmt): def __replace__(self, *, exc: expr | None = ..., cause: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Try(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("body", "handlers", "orelse", "finalbody") + __match_args__ = ("body", "handlers", "orelse", "finalbody") body: list[stmt] handlers: list[ExceptHandler] orelse: list[stmt] @@ -718,8 +702,7 @@ if sys.version_info >= (3, 11): ) -> Self: ... class Assert(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("test", "msg") + __match_args__ = ("test", "msg") test: expr msg: expr | None def __init__(self, test: expr, msg: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ... @@ -728,24 +711,53 @@ class Assert(stmt): def __replace__(self, *, test: expr = ..., msg: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Import(stmt): - if sys.version_info >= (3, 10): + if sys.version_info >= (3, 15): + __match_args__ = ("names", "is_lazy") + else: __match_args__ = ("names",) names: list[alias] - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 15): + is_lazy: bool | None + if sys.version_info >= (3, 15): + def __init__(self, names: list[alias] = ..., is_lazy: bool | None = None, **kwargs: Unpack[_Attributes]) -> None: ... + + elif sys.version_info >= (3, 13): def __init__(self, names: list[alias] = ..., **kwargs: Unpack[_Attributes]) -> None: ... else: def __init__(self, names: list[alias], **kwargs: Unpack[_Attributes]) -> None: ... - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): + def __replace__(self, *, names: list[alias] = ..., is_lazy: bool | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + + elif sys.version_info >= (3, 14): def __replace__(self, *, names: list[alias] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class ImportFrom(stmt): - if sys.version_info >= (3, 10): + if sys.version_info >= (3, 15): + __match_args__ = ("module", "names", "level", "is_lazy") + else: __match_args__ = ("module", "names", "level") module: str | None names: list[alias] level: int - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 15): + is_lazy: bool | None + if sys.version_info >= (3, 15): + @overload + def __init__( + self, module: str | None, names: list[alias], level: int, is_lazy: bool | None = None, **kwargs: Unpack[_Attributes] + ) -> None: ... + @overload + def __init__( + self, + module: str | None = None, + names: list[alias] = ..., + *, + level: int, + is_lazy: bool | None = None, + **kwargs: Unpack[_Attributes], + ) -> None: ... + elif sys.version_info >= (3, 13): @overload def __init__(self, module: str | None, names: list[alias], level: int, **kwargs: Unpack[_Attributes]) -> None: ... @overload @@ -760,14 +772,24 @@ class ImportFrom(stmt): self, module: str | None = None, *, names: list[alias], level: int, **kwargs: Unpack[_Attributes] ) -> None: ... - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): + def __replace__( + self, + *, + module: str | None = ..., + names: list[alias] = ..., + level: int = ..., + is_lazy: bool | None = ..., + **kwargs: Unpack[_Attributes], + ) -> Self: ... + + elif sys.version_info >= (3, 14): def __replace__( self, *, module: str | None = ..., names: list[alias] = ..., level: int = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... class Global(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("names",) + __match_args__ = ("names",) names: list[str] if sys.version_info >= (3, 13): def __init__(self, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -778,8 +800,7 @@ class Global(stmt): def __replace__(self, *, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Nonlocal(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("names",) + __match_args__ = ("names",) names: list[str] if sys.version_info >= (3, 13): def __init__(self, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -790,8 +811,7 @@ class Nonlocal(stmt): def __replace__(self, *, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Expr(stmt): - if sys.version_info >= (3, 10): - __match_args__ = ("value",) + __match_args__ = ("value",) value: expr def __init__(self, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... @@ -813,8 +833,7 @@ class expr(AST): def __replace__(self, **kwargs: Unpack[_Attributes]) -> Self: ... class BoolOp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("op", "values") + __match_args__ = ("op", "values") op: boolop values: list[expr] if sys.version_info >= (3, 13): @@ -826,8 +845,7 @@ class BoolOp(expr): def __replace__(self, *, op: boolop = ..., values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class NamedExpr(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("target", "value") + __match_args__ = ("target", "value") target: Name value: expr def __init__(self, target: Name, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... @@ -836,8 +854,7 @@ class NamedExpr(expr): def __replace__(self, *, target: Name = ..., value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class BinOp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("left", "op", "right") + __match_args__ = ("left", "op", "right") left: expr op: operator right: expr @@ -849,8 +866,7 @@ class BinOp(expr): ) -> Self: ... class UnaryOp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("op", "operand") + __match_args__ = ("op", "operand") op: unaryop operand: expr def __init__(self, op: unaryop, operand: expr, **kwargs: Unpack[_Attributes]) -> None: ... @@ -859,8 +875,7 @@ class UnaryOp(expr): def __replace__(self, *, op: unaryop = ..., operand: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Lambda(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("args", "body") + __match_args__ = ("args", "body") args: arguments body: expr def __init__(self, args: arguments, body: expr, **kwargs: Unpack[_Attributes]) -> None: ... @@ -869,8 +884,7 @@ class Lambda(expr): def __replace__(self, *, args: arguments = ..., body: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class IfExp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("test", "body", "orelse") + __match_args__ = ("test", "body", "orelse") test: expr body: expr orelse: expr @@ -882,8 +896,7 @@ class IfExp(expr): ) -> Self: ... class Dict(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("keys", "values") + __match_args__ = ("keys", "values") keys: list[expr | None] values: list[expr] if sys.version_info >= (3, 13): @@ -897,8 +910,7 @@ class Dict(expr): ) -> Self: ... class Set(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("elts",) + __match_args__ = ("elts",) elts: list[expr] if sys.version_info >= (3, 13): def __init__(self, elts: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -909,8 +921,7 @@ class Set(expr): def __replace__(self, *, elts: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class ListComp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("elt", "generators") + __match_args__ = ("elt", "generators") elt: expr generators: list[comprehension] if sys.version_info >= (3, 13): @@ -924,8 +935,7 @@ class ListComp(expr): ) -> Self: ... class SetComp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("elt", "generators") + __match_args__ = ("elt", "generators") elt: expr generators: list[comprehension] if sys.version_info >= (3, 13): @@ -939,10 +949,12 @@ class SetComp(expr): ) -> Self: ... class DictComp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("key", "value", "generators") + __match_args__ = ("key", "value", "generators") key: expr - value: expr + if sys.version_info >= (3, 15): + value: expr | None + else: + value: expr generators: list[comprehension] if sys.version_info >= (3, 13): def __init__( @@ -957,8 +969,7 @@ class DictComp(expr): ) -> Self: ... class GeneratorExp(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("elt", "generators") + __match_args__ = ("elt", "generators") elt: expr generators: list[comprehension] if sys.version_info >= (3, 13): @@ -972,8 +983,7 @@ class GeneratorExp(expr): ) -> Self: ... class Await(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value",) + __match_args__ = ("value",) value: expr def __init__(self, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... @@ -981,8 +991,7 @@ class Await(expr): def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Yield(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value",) + __match_args__ = ("value",) value: expr | None def __init__(self, value: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ... @@ -990,8 +999,7 @@ class Yield(expr): def __replace__(self, *, value: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class YieldFrom(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value",) + __match_args__ = ("value",) value: expr def __init__(self, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... @@ -999,8 +1007,7 @@ class YieldFrom(expr): def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Compare(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("left", "ops", "comparators") + __match_args__ = ("left", "ops", "comparators") left: expr ops: list[cmpop] comparators: list[expr] @@ -1017,8 +1024,7 @@ class Compare(expr): ) -> Self: ... class Call(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("func", "args", "keywords") + __match_args__ = ("func", "args", "keywords") func: expr args: list[expr] keywords: list[keyword] @@ -1035,8 +1041,7 @@ class Call(expr): ) -> Self: ... class FormattedValue(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value", "conversion", "format_spec") + __match_args__ = ("value", "conversion", "format_spec") value: expr conversion: int format_spec: expr | None @@ -1048,8 +1053,7 @@ class FormattedValue(expr): ) -> Self: ... class JoinedStr(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("values",) + __match_args__ = ("values",) values: list[expr] if sys.version_info >= (3, 13): def __init__(self, values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -1090,17 +1094,10 @@ if sys.version_info >= (3, 14): **kwargs: Unpack[_Attributes], ) -> Self: ... -if sys.version_info >= (3, 10): - from types import EllipsisType - - _ConstantValue: typing_extensions.TypeAlias = str | bytes | bool | int | float | complex | None | EllipsisType -else: - # Rely on builtins.ellipsis - _ConstantValue: typing_extensions.TypeAlias = str | bytes | bool | int | float | complex | None | ellipsis # noqa: F821 +_ConstantValue: typing_extensions.TypeAlias = str | bytes | bool | int | float | complex | None | EllipsisType class Constant(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value", "kind") + __match_args__ = ("value", "kind") value: _ConstantValue kind: str | None if sys.version_info < (3, 14): @@ -1111,6 +1108,7 @@ class Constant(expr): @n.setter @deprecated("Removed in Python 3.14. Use `value` instead.") def n(self, value: _ConstantValue) -> None: ... + @property @deprecated("Removed in Python 3.14. Use `value` instead.") def s(self) -> _ConstantValue: ... @@ -1124,8 +1122,7 @@ class Constant(expr): def __replace__(self, *, value: _ConstantValue = ..., kind: str | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Attribute(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value", "attr", "ctx") + __match_args__ = ("value", "attr", "ctx") value: expr attr: str ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` @@ -1137,8 +1134,7 @@ class Attribute(expr): ) -> Self: ... class Subscript(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value", "slice", "ctx") + __match_args__ = ("value", "slice", "ctx") value: expr slice: expr ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` @@ -1150,8 +1146,7 @@ class Subscript(expr): ) -> Self: ... class Starred(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("value", "ctx") + __match_args__ = ("value", "ctx") value: expr ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` def __init__(self, value: expr, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -1160,8 +1155,7 @@ class Starred(expr): def __replace__(self, *, value: expr = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Name(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("id", "ctx") + __match_args__ = ("id", "ctx") id: str ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` def __init__(self, id: str, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... @@ -1170,8 +1164,7 @@ class Name(expr): def __replace__(self, *, id: str = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class List(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("elts", "ctx") + __match_args__ = ("elts", "ctx") elts: list[expr] ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` if sys.version_info >= (3, 13): @@ -1183,8 +1176,7 @@ class List(expr): def __replace__(self, *, elts: list[expr] = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Tuple(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("elts", "ctx") + __match_args__ = ("elts", "ctx") elts: list[expr] ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` dims: list[expr] @@ -1200,8 +1192,7 @@ class Tuple(expr): class slice(AST): ... class Slice(expr): - if sys.version_info >= (3, 10): - __match_args__ = ("lower", "upper", "step") + __match_args__ = ("lower", "upper", "step") lower: expr | None upper: expr | None step: expr | None @@ -1274,8 +1265,7 @@ class In(cmpop): ... class NotIn(cmpop): ... class comprehension(AST): - if sys.version_info >= (3, 10): - __match_args__ = ("target", "iter", "ifs", "is_async") + __match_args__ = ("target", "iter", "ifs", "is_async") target: expr iter: expr ifs: list[expr] @@ -1304,8 +1294,7 @@ class excepthandler(AST): ) -> Self: ... class ExceptHandler(excepthandler): - if sys.version_info >= (3, 10): - __match_args__ = ("type", "name", "body") + __match_args__ = ("type", "name", "body") type: expr | None name: str | None body: list[stmt] @@ -1327,8 +1316,7 @@ class ExceptHandler(excepthandler): ) -> Self: ... class arguments(AST): - if sys.version_info >= (3, 10): - __match_args__ = ("posonlyargs", "args", "vararg", "kwonlyargs", "kw_defaults", "kwarg", "defaults") + __match_args__ = ("posonlyargs", "args", "vararg", "kwonlyargs", "kw_defaults", "kwarg", "defaults") posonlyargs: list[arg] args: list[arg] vararg: arg | None @@ -1398,12 +1386,11 @@ class arguments(AST): ) -> Self: ... class arg(AST): + __match_args__ = ("arg", "annotation", "type_comment") lineno: int col_offset: int end_lineno: int | None end_col_offset: int | None - if sys.version_info >= (3, 10): - __match_args__ = ("arg", "annotation", "type_comment") arg: str annotation: expr | None type_comment: str | None @@ -1417,14 +1404,14 @@ class arg(AST): ) -> Self: ... class keyword(AST): + __match_args__ = ("arg", "value") lineno: int col_offset: int end_lineno: int | None end_col_offset: int | None - if sys.version_info >= (3, 10): - __match_args__ = ("arg", "value") arg: str | None value: expr + @overload def __init__(self, arg: str | None, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... @overload @@ -1434,26 +1421,20 @@ class keyword(AST): def __replace__(self, *, arg: str | None = ..., value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class alias(AST): + __match_args__ = ("name", "asname") name: str asname: str | None - if sys.version_info >= (3, 10): - lineno: int - col_offset: int - end_lineno: int | None - end_col_offset: int | None - if sys.version_info >= (3, 10): - __match_args__ = ("name", "asname") - if sys.version_info >= (3, 10): - def __init__(self, name: str, asname: str | None = None, **kwargs: Unpack[_Attributes]) -> None: ... - else: - def __init__(self, name: str, asname: str | None = None) -> None: ... + lineno: int + col_offset: int + end_lineno: int | None + end_col_offset: int | None + def __init__(self, name: str, asname: str | None = None, **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): def __replace__(self, *, name: str = ..., asname: str | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class withitem(AST): - if sys.version_info >= (3, 10): - __match_args__ = ("context_expr", "optional_vars") + __match_args__ = ("context_expr", "optional_vars") context_expr: expr optional_vars: expr | None def __init__(self, context_expr: expr, optional_vars: expr | None = None) -> None: ... @@ -1461,177 +1442,173 @@ class withitem(AST): if sys.version_info >= (3, 14): def __replace__(self, *, context_expr: expr = ..., optional_vars: expr | None = ...) -> Self: ... -if sys.version_info >= (3, 10): - class pattern(AST): - lineno: int - col_offset: int - end_lineno: int - end_col_offset: int - def __init__(self, **kwargs: Unpack[_Attributes[int]]) -> None: ... +class pattern(AST): + lineno: int + col_offset: int + end_lineno: int + end_col_offset: int + def __init__(self, **kwargs: Unpack[_Attributes[int]]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__( - self, *, lineno: int = ..., col_offset: int = ..., end_lineno: int = ..., end_col_offset: int = ... - ) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__( + self, *, lineno: int = ..., col_offset: int = ..., end_lineno: int = ..., end_col_offset: int = ... + ) -> Self: ... - class match_case(AST): - __match_args__ = ("pattern", "guard", "body") - pattern: ast.pattern - guard: expr | None - body: list[stmt] - if sys.version_info >= (3, 13): - def __init__(self, pattern: ast.pattern, guard: expr | None = None, body: list[stmt] = ...) -> None: ... - elif sys.version_info >= (3, 10): - @overload - def __init__(self, pattern: ast.pattern, guard: expr | None, body: list[stmt]) -> None: ... - @overload - def __init__(self, pattern: ast.pattern, guard: expr | None = None, *, body: list[stmt]) -> None: ... +class match_case(AST): + __match_args__ = ("pattern", "guard", "body") + pattern: ast.pattern + guard: expr | None + body: list[stmt] + if sys.version_info >= (3, 13): + def __init__(self, pattern: ast.pattern, guard: expr | None = None, body: list[stmt] = ...) -> None: ... + else: + @overload + def __init__(self, pattern: ast.pattern, guard: expr | None, body: list[stmt]) -> None: ... + @overload + def __init__(self, pattern: ast.pattern, guard: expr | None = None, *, body: list[stmt]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__(self, *, pattern: ast.pattern = ..., guard: expr | None = ..., body: list[stmt] = ...) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, pattern: ast.pattern = ..., guard: expr | None = ..., body: list[stmt] = ...) -> Self: ... - class Match(stmt): - __match_args__ = ("subject", "cases") - subject: expr - cases: list[match_case] - if sys.version_info >= (3, 13): - def __init__(self, subject: expr, cases: list[match_case] = ..., **kwargs: Unpack[_Attributes]) -> None: ... - else: - def __init__(self, subject: expr, cases: list[match_case], **kwargs: Unpack[_Attributes]) -> None: ... +class Match(stmt): + __match_args__ = ("subject", "cases") + subject: expr + cases: list[match_case] + if sys.version_info >= (3, 13): + def __init__(self, subject: expr, cases: list[match_case] = ..., **kwargs: Unpack[_Attributes]) -> None: ... + else: + def __init__(self, subject: expr, cases: list[match_case], **kwargs: Unpack[_Attributes]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__( - self, *, subject: expr = ..., cases: list[match_case] = ..., **kwargs: Unpack[_Attributes] - ) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, subject: expr = ..., cases: list[match_case] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... - class MatchValue(pattern): - __match_args__ = ("value",) - value: expr - def __init__(self, value: expr, **kwargs: Unpack[_Attributes[int]]) -> None: ... +class MatchValue(pattern): + __match_args__ = ("value",) + value: expr + def __init__(self, value: expr, **kwargs: Unpack[_Attributes[int]]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... - class MatchSingleton(pattern): - __match_args__ = ("value",) - value: bool | None - def __init__(self, value: bool | None, **kwargs: Unpack[_Attributes[int]]) -> None: ... +class MatchSingleton(pattern): + __match_args__ = ("value",) + value: bool | None + def __init__(self, value: bool | None, **kwargs: Unpack[_Attributes[int]]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__(self, *, value: bool | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, value: bool | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... - class MatchSequence(pattern): - __match_args__ = ("patterns",) - patterns: list[pattern] - if sys.version_info >= (3, 13): - def __init__(self, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> None: ... - else: - def __init__(self, patterns: list[pattern], **kwargs: Unpack[_Attributes[int]]) -> None: ... +class MatchSequence(pattern): + __match_args__ = ("patterns",) + patterns: list[pattern] + if sys.version_info >= (3, 13): + def __init__(self, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> None: ... + else: + def __init__(self, patterns: list[pattern], **kwargs: Unpack[_Attributes[int]]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__(self, *, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... - class MatchMapping(pattern): - __match_args__ = ("keys", "patterns", "rest") - keys: list[expr] - patterns: list[pattern] - rest: str | None - if sys.version_info >= (3, 13): - def __init__( - self, - keys: list[expr] = ..., - patterns: list[pattern] = ..., - rest: str | None = None, - **kwargs: Unpack[_Attributes[int]], - ) -> None: ... - else: - def __init__( - self, keys: list[expr], patterns: list[pattern], rest: str | None = None, **kwargs: Unpack[_Attributes[int]] - ) -> None: ... +class MatchMapping(pattern): + __match_args__ = ("keys", "patterns", "rest") + keys: list[expr] + patterns: list[pattern] + rest: str | None + if sys.version_info >= (3, 13): + def __init__( + self, + keys: list[expr] = ..., + patterns: list[pattern] = ..., + rest: str | None = None, + **kwargs: Unpack[_Attributes[int]], + ) -> None: ... + else: + def __init__( + self, keys: list[expr], patterns: list[pattern], rest: str | None = None, **kwargs: Unpack[_Attributes[int]] + ) -> None: ... - if sys.version_info >= (3, 14): - def __replace__( - self, - *, - keys: list[expr] = ..., - patterns: list[pattern] = ..., - rest: str | None = ..., - **kwargs: Unpack[_Attributes[int]], - ) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__( + self, + *, + keys: list[expr] = ..., + patterns: list[pattern] = ..., + rest: str | None = ..., + **kwargs: Unpack[_Attributes[int]], + ) -> Self: ... - class MatchClass(pattern): - __match_args__ = ("cls", "patterns", "kwd_attrs", "kwd_patterns") - cls: expr - patterns: list[pattern] - kwd_attrs: list[str] - kwd_patterns: list[pattern] - if sys.version_info >= (3, 13): - def __init__( - self, - cls: expr, - patterns: list[pattern] = ..., - kwd_attrs: list[str] = ..., - kwd_patterns: list[pattern] = ..., - **kwargs: Unpack[_Attributes[int]], - ) -> None: ... - else: - def __init__( - self, - cls: expr, - patterns: list[pattern], - kwd_attrs: list[str], - kwd_patterns: list[pattern], - **kwargs: Unpack[_Attributes[int]], - ) -> None: ... +class MatchClass(pattern): + __match_args__ = ("cls", "patterns", "kwd_attrs", "kwd_patterns") + cls: expr + patterns: list[pattern] + kwd_attrs: list[str] + kwd_patterns: list[pattern] + if sys.version_info >= (3, 13): + def __init__( + self, + cls: expr, + patterns: list[pattern] = ..., + kwd_attrs: list[str] = ..., + kwd_patterns: list[pattern] = ..., + **kwargs: Unpack[_Attributes[int]], + ) -> None: ... + else: + def __init__( + self, + cls: expr, + patterns: list[pattern], + kwd_attrs: list[str], + kwd_patterns: list[pattern], + **kwargs: Unpack[_Attributes[int]], + ) -> None: ... - if sys.version_info >= (3, 14): - def __replace__( - self, - *, - cls: expr = ..., - patterns: list[pattern] = ..., - kwd_attrs: list[str] = ..., - kwd_patterns: list[pattern] = ..., - **kwargs: Unpack[_Attributes[int]], - ) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__( + self, + *, + cls: expr = ..., + patterns: list[pattern] = ..., + kwd_attrs: list[str] = ..., + kwd_patterns: list[pattern] = ..., + **kwargs: Unpack[_Attributes[int]], + ) -> Self: ... - class MatchStar(pattern): - __match_args__ = ("name",) - name: str | None - def __init__(self, name: str | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... +class MatchStar(pattern): + __match_args__ = ("name",) + name: str | None + def __init__(self, name: str | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__(self, *, name: str | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, name: str | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... - class MatchAs(pattern): - __match_args__ = ("pattern", "name") - pattern: ast.pattern | None - name: str | None - def __init__( - self, pattern: ast.pattern | None = None, name: str | None = None, **kwargs: Unpack[_Attributes[int]] - ) -> None: ... +class MatchAs(pattern): + __match_args__ = ("pattern", "name") + pattern: ast.pattern | None + name: str | None + def __init__( + self, pattern: ast.pattern | None = None, name: str | None = None, **kwargs: Unpack[_Attributes[int]] + ) -> None: ... - if sys.version_info >= (3, 14): - def __replace__( - self, *, pattern: ast.pattern | None = ..., name: str | None = ..., **kwargs: Unpack[_Attributes[int]] - ) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__( + self, *, pattern: ast.pattern | None = ..., name: str | None = ..., **kwargs: Unpack[_Attributes[int]] + ) -> Self: ... - class MatchOr(pattern): - __match_args__ = ("patterns",) - patterns: list[pattern] - if sys.version_info >= (3, 13): - def __init__(self, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> None: ... - else: - def __init__(self, patterns: list[pattern], **kwargs: Unpack[_Attributes[int]]) -> None: ... +class MatchOr(pattern): + __match_args__ = ("patterns",) + patterns: list[pattern] + if sys.version_info >= (3, 13): + def __init__(self, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> None: ... + else: + def __init__(self, patterns: list[pattern], **kwargs: Unpack[_Attributes[int]]) -> None: ... - if sys.version_info >= (3, 14): - def __replace__(self, *, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... + if sys.version_info >= (3, 14): + def __replace__(self, *, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... class type_ignore(AST): ... class TypeIgnore(type_ignore): - if sys.version_info >= (3, 10): - __match_args__ = ("lineno", "tag") + __match_args__ = ("lineno", "tag") lineno: int tag: str def __init__(self, lineno: int, tag: str) -> None: ... @@ -1743,7 +1720,104 @@ if sys.version_info < (3, 14): _T = _TypeVar("_T", bound=AST) -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 15): + @overload + def parse( + source: _T, + filename: str | bytes | os.PathLike[Any] = "", + mode: Literal["exec", "eval", "func_type", "single"] = "exec", + *, + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> _T: ... + @overload + def parse( + source: str | ReadableBuffer, + filename: str | bytes | os.PathLike[Any] = "", + mode: Literal["exec"] = "exec", + *, + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> Module: ... + @overload + def parse( + source: str | ReadableBuffer, + filename: str | bytes | os.PathLike[Any], + mode: Literal["eval"], + *, + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> Expression: ... + @overload + def parse( + source: str | ReadableBuffer, + filename: str | bytes | os.PathLike[Any], + mode: Literal["func_type"], + *, + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> FunctionType: ... + @overload + def parse( + source: str | ReadableBuffer, + filename: str | bytes | os.PathLike[Any], + mode: Literal["single"], + *, + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> Interactive: ... + @overload + def parse( + source: str | ReadableBuffer, + *, + mode: Literal["eval"], + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> Expression: ... + @overload + def parse( + source: str | ReadableBuffer, + *, + mode: Literal["func_type"], + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> FunctionType: ... + @overload + def parse( + source: str | ReadableBuffer, + *, + mode: Literal["single"], + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> Interactive: ... + @overload + def parse( + source: str | ReadableBuffer, + filename: str | bytes | os.PathLike[Any] = "", + mode: str = "exec", + *, + type_comments: bool = False, + feature_version: None | int | tuple[int, int] = None, + optimize: Literal[-1, 0, 1, 2] = -1, + module: str | None = None, + ) -> mod: ... +elif sys.version_info >= (3, 13): @overload def parse( source: _T, @@ -1831,7 +1905,6 @@ if sys.version_info >= (3, 13): feature_version: None | int | tuple[int, int] = None, optimize: Literal[-1, 0, 1, 2] = -1, ) -> mod: ... - else: @overload def parse( @@ -1914,7 +1987,18 @@ else: def literal_eval(node_or_string: str | AST) -> Any: ... -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 15): + def dump( + node: AST, + annotate_fields: bool = True, + include_attributes: bool = False, + *, + indent: int | str | None = None, + show_empty: bool = False, + color: bool = False, + ) -> str: ... + +elif sys.version_info >= (3, 13): def dump( node: AST, annotate_fields: bool = True, @@ -2043,17 +2127,16 @@ class NodeVisitor: def visit_keyword(self, node: keyword) -> Any: ... def visit_alias(self, node: alias) -> Any: ... def visit_withitem(self, node: withitem) -> Any: ... - if sys.version_info >= (3, 10): - def visit_Match(self, node: Match) -> Any: ... - def visit_match_case(self, node: match_case) -> Any: ... - def visit_MatchValue(self, node: MatchValue) -> Any: ... - def visit_MatchSequence(self, node: MatchSequence) -> Any: ... - def visit_MatchSingleton(self, node: MatchSingleton) -> Any: ... - def visit_MatchStar(self, node: MatchStar) -> Any: ... - def visit_MatchMapping(self, node: MatchMapping) -> Any: ... - def visit_MatchClass(self, node: MatchClass) -> Any: ... - def visit_MatchAs(self, node: MatchAs) -> Any: ... - def visit_MatchOr(self, node: MatchOr) -> Any: ... + def visit_Match(self, node: Match) -> Any: ... + def visit_match_case(self, node: match_case) -> Any: ... + def visit_MatchValue(self, node: MatchValue) -> Any: ... + def visit_MatchSequence(self, node: MatchSequence) -> Any: ... + def visit_MatchSingleton(self, node: MatchSingleton) -> Any: ... + def visit_MatchStar(self, node: MatchStar) -> Any: ... + def visit_MatchMapping(self, node: MatchMapping) -> Any: ... + def visit_MatchClass(self, node: MatchClass) -> Any: ... + def visit_MatchAs(self, node: MatchAs) -> Any: ... + def visit_MatchOr(self, node: MatchOr) -> Any: ... if sys.version_info >= (3, 11): def visit_TryStar(self, node: TryStar) -> Any: ... diff --git a/mypy/typeshed/stdlib/asyncio/__init__.pyi b/mypy/typeshed/stdlib/asyncio/__init__.pyi index 23cf57aaac335..5748c85af4c6c 100644 --- a/mypy/typeshed/stdlib/asyncio/__init__.pyi +++ b/mypy/typeshed/stdlib/asyncio/__init__.pyi @@ -2,8 +2,7 @@ # Can't NOQA on a specific line: https://github.com/plinss/flake8-noqa/issues/22 import sys from collections.abc import Awaitable, Coroutine, Generator -from typing import Any, TypeVar -from typing_extensions import TypeAlias +from typing import Any, TypeAlias, TypeVar # As at runtime, this depends on all submodules defining __all__ accurately. from .base_events import * @@ -33,6 +32,24 @@ if sys.platform == "win32": else: from .unix_events import * +if sys.version_info >= (3, 14): + from .events import _AbstractEventLoopPolicy + + AbstractEventLoopPolicy = _AbstractEventLoopPolicy + +if sys.platform == "win32": + if sys.version_info >= (3, 14): + from .windows_events import _DefaultEventLoopPolicy, _WindowsProactorEventLoopPolicy, _WindowsSelectorEventLoopPolicy + + DefaultEventLoopPolicy = _DefaultEventLoopPolicy + WindowsProactorEventLoopPolicy = _WindowsProactorEventLoopPolicy + WindowsSelectorEventLoopPolicy = _WindowsSelectorEventLoopPolicy +else: + if sys.version_info >= (3, 14): + from .unix_events import _DefaultEventLoopPolicy + + DefaultEventLoopPolicy = _DefaultEventLoopPolicy + if sys.platform == "win32": if sys.version_info >= (3, 14): diff --git a/mypy/typeshed/stdlib/asyncio/base_events.pyi b/mypy/typeshed/stdlib/asyncio/base_events.pyi index 0d8ac2d474914..9a3367b6aa3b2 100644 --- a/mypy/typeshed/stdlib/asyncio/base_events.pyi +++ b/mypy/typeshed/stdlib/asyncio/base_events.pyi @@ -11,8 +11,8 @@ from collections.abc import Callable, Iterable, Sequence from concurrent.futures import Executor, ThreadPoolExecutor from contextvars import Context from socket import AddressFamily, AddressInfo, SocketKind, _Address, _RetAddress, socket -from typing import IO, Any, Literal, TypeVar, overload -from typing_extensions import TypeAlias, TypeVarTuple, Unpack +from typing import IO, Any, Literal, TypeAlias, TypeVar, overload +from typing_extensions import TypeVarTuple, Unpack # Keep asyncio.__all__ updated with any changes to __all__ here __all__ = ("BaseEventLoop", "Server") @@ -115,8 +115,9 @@ class BaseEventLoop(AbstractEventLoop): type: int = 0, proto: int = 0, flags: int = 0, - ) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int]]]: ... + ) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes]]]: ... async def getnameinfo(self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int = 0) -> tuple[str, str]: ... + if sys.version_info >= (3, 12): @overload async def create_connection( diff --git a/mypy/typeshed/stdlib/asyncio/base_subprocess.pyi b/mypy/typeshed/stdlib/asyncio/base_subprocess.pyi index a5fe24e8768b7..36f0f6099cbc6 100644 --- a/mypy/typeshed/stdlib/asyncio/base_subprocess.pyi +++ b/mypy/typeshed/stdlib/asyncio/base_subprocess.pyi @@ -1,8 +1,7 @@ import subprocess from collections import deque from collections.abc import Callable, Sequence -from typing import IO, Any -from typing_extensions import TypeAlias +from typing import IO, Any, TypeAlias from . import events, futures, protocols, transports diff --git a/mypy/typeshed/stdlib/asyncio/coroutines.pyi b/mypy/typeshed/stdlib/asyncio/coroutines.pyi index 777961d804412..7599c692949bd 100644 --- a/mypy/typeshed/stdlib/asyncio/coroutines.pyi +++ b/mypy/typeshed/stdlib/asyncio/coroutines.pyi @@ -1,7 +1,7 @@ import sys from collections.abc import Awaitable, Callable, Coroutine -from typing import Any, TypeVar, overload -from typing_extensions import ParamSpec, TypeGuard, TypeIs, deprecated +from typing import Any, ParamSpec, TypeGuard, TypeVar, overload +from typing_extensions import TypeIs, deprecated # Keep asyncio.__all__ updated with any changes to __all__ here if sys.version_info >= (3, 11): @@ -32,11 +32,9 @@ if sys.version_info >= (3, 11): @overload @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: object) -> TypeGuard[Callable[..., Coroutine[Any, Any, Any]]]: ... - else: # Sometimes needed in Python < 3.11 due to the fact that it supports @coroutine # which was removed in 3.11 which the inspect version doesn't support. - @overload def iscoroutinefunction(func: Callable[..., Coroutine[Any, Any, Any]]) -> bool: ... @overload diff --git a/mypy/typeshed/stdlib/asyncio/events.pyi b/mypy/typeshed/stdlib/asyncio/events.pyi index 4f2f45355f5ba..5e561b09cd460 100644 --- a/mypy/typeshed/stdlib/asyncio/events.pyi +++ b/mypy/typeshed/stdlib/asyncio/events.pyi @@ -12,8 +12,8 @@ from collections.abc import Callable, Sequence from concurrent.futures import Executor from contextvars import Context from socket import AddressFamily, AddressInfo, SocketKind, _Address, _RetAddress, socket -from typing import IO, Any, Literal, Protocol, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack, deprecated +from typing import IO, Any, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, TypeVarTuple, Unpack, deprecated from . import _AwaitableLike, _CoroutineLike from .base_events import Server @@ -205,9 +205,10 @@ class AbstractEventLoop: type: int = 0, proto: int = 0, flags: int = 0, - ) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int]]]: ... + ) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes]]]: ... @abstractmethod async def getnameinfo(self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int = 0) -> tuple[str, str]: ... + if sys.version_info >= (3, 11): @overload @abstractmethod @@ -467,7 +468,7 @@ class AbstractEventLoop: ssl_handshake_timeout: float | None = None, ssl_shutdown_timeout: float | None = None, ) -> tuple[Transport, _ProtocolT]: ... - elif sys.version_info >= (3, 10): + else: async def connect_accepted_socket( self, protocol_factory: Callable[[], _ProtocolT], @@ -631,18 +632,12 @@ else: @abstractmethod def new_event_loop(self) -> AbstractEventLoop: ... # Child processes handling (Unix only). - if sys.version_info >= (3, 12): - @abstractmethod - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") - def get_child_watcher(self) -> AbstractChildWatcher: ... - @abstractmethod - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") - def set_child_watcher(self, watcher: AbstractChildWatcher) -> None: ... - else: - @abstractmethod - def get_child_watcher(self) -> AbstractChildWatcher: ... - @abstractmethod - def set_child_watcher(self, watcher: AbstractChildWatcher) -> None: ... + @abstractmethod + @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + def get_child_watcher(self) -> AbstractChildWatcher: ... + @abstractmethod + @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + def set_child_watcher(self, watcher: AbstractChildWatcher) -> None: ... AbstractEventLoopPolicy = _AbstractEventLoopPolicy @@ -661,25 +656,16 @@ else: if sys.version_info >= (3, 14): def _get_event_loop_policy() -> _AbstractEventLoopPolicy: ... def _set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") - def get_event_loop_policy() -> _AbstractEventLoopPolicy: ... - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") - def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... - -else: - def get_event_loop_policy() -> _AbstractEventLoopPolicy: ... - def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... +@deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") +def get_event_loop_policy() -> _AbstractEventLoopPolicy: ... +@deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") +def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... def set_event_loop(loop: AbstractEventLoop | None) -> None: ... def new_event_loop() -> AbstractEventLoop: ... if sys.version_info < (3, 14): - if sys.version_info >= (3, 12): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") - def get_child_watcher() -> AbstractChildWatcher: ... - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") - def set_child_watcher(watcher: AbstractChildWatcher) -> None: ... - - else: - def get_child_watcher() -> AbstractChildWatcher: ... - def set_child_watcher(watcher: AbstractChildWatcher) -> None: ... + @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + def get_child_watcher() -> AbstractChildWatcher: ... + @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + def set_child_watcher(watcher: AbstractChildWatcher) -> None: ... diff --git a/mypy/typeshed/stdlib/asyncio/format_helpers.pyi b/mypy/typeshed/stdlib/asyncio/format_helpers.pyi index 597eb9e56e1a1..ff830a3b73d45 100644 --- a/mypy/typeshed/stdlib/asyncio/format_helpers.pyi +++ b/mypy/typeshed/stdlib/asyncio/format_helpers.pyi @@ -3,8 +3,7 @@ import sys import traceback from collections.abc import Iterable from types import FrameType, FunctionType -from typing import Any, overload, type_check_only -from typing_extensions import TypeAlias +from typing import Any, TypeAlias, overload, type_check_only @type_check_only class _HasWrapper: diff --git a/mypy/typeshed/stdlib/asyncio/graph.pyi b/mypy/typeshed/stdlib/asyncio/graph.pyi index 18a8a6457d757..2f89de71b16b6 100644 --- a/mypy/typeshed/stdlib/asyncio/graph.pyi +++ b/mypy/typeshed/stdlib/asyncio/graph.pyi @@ -22,6 +22,7 @@ if sys.version_info >= (3, 14): def capture_call_graph(future: None = None, /, *, depth: int = 1, limit: int | None = None) -> FutureCallGraph | None: ... @overload def capture_call_graph(future: Future[Any], /, *, depth: int = 1, limit: int | None = None) -> FutureCallGraph | None: ... + def format_call_graph(future: Future[Any] | None = None, /, *, depth: int = 1, limit: int | None = None) -> str: ... def print_call_graph( future: Future[Any] | None = None, /, *, file: SupportsWrite[str] | None = None, depth: int = 1, limit: int | None = None diff --git a/mypy/typeshed/stdlib/asyncio/locks.pyi b/mypy/typeshed/stdlib/asyncio/locks.pyi index 17390b0c5a0ee..4420d02fcd269 100644 --- a/mypy/typeshed/stdlib/asyncio/locks.pyi +++ b/mypy/typeshed/stdlib/asyncio/locks.pyi @@ -7,13 +7,8 @@ from types import TracebackType from typing import Any, Literal, TypeVar from typing_extensions import Self -from .events import AbstractEventLoop from .futures import Future - -if sys.version_info >= (3, 10): - from .mixins import _LoopBoundMixin -else: - _LoopBoundMixin = object +from .mixins import _LoopBoundMixin # Keep asyncio.__all__ updated with any changes to __all__ here if sys.version_info >= (3, 11): @@ -31,22 +26,14 @@ class _ContextManagerMixin: class Lock(_ContextManagerMixin, _LoopBoundMixin): _waiters: deque[Future[Any]] | None - if sys.version_info >= (3, 10): - def __init__(self) -> None: ... - else: - def __init__(self, *, loop: AbstractEventLoop | None = None) -> None: ... - + def __init__(self) -> None: ... def locked(self) -> bool: ... async def acquire(self) -> Literal[True]: ... def release(self) -> None: ... class Event(_LoopBoundMixin): _waiters: deque[Future[Any]] - if sys.version_info >= (3, 10): - def __init__(self) -> None: ... - else: - def __init__(self, *, loop: AbstractEventLoop | None = None) -> None: ... - + def __init__(self) -> None: ... def is_set(self) -> bool: ... def set(self) -> None: ... def clear(self) -> None: ... @@ -54,11 +41,7 @@ class Event(_LoopBoundMixin): class Condition(_ContextManagerMixin, _LoopBoundMixin): _waiters: deque[Future[Any]] - if sys.version_info >= (3, 10): - def __init__(self, lock: Lock | None = None) -> None: ... - else: - def __init__(self, lock: Lock | None = None, *, loop: AbstractEventLoop | None = None) -> None: ... - + def __init__(self, lock: Lock | None = None) -> None: ... def locked(self) -> bool: ... async def acquire(self) -> Literal[True]: ... def release(self) -> None: ... @@ -70,11 +53,7 @@ class Condition(_ContextManagerMixin, _LoopBoundMixin): class Semaphore(_ContextManagerMixin, _LoopBoundMixin): _value: int _waiters: deque[Future[Any]] | None - if sys.version_info >= (3, 10): - def __init__(self, value: int = 1) -> None: ... - else: - def __init__(self, value: int = 1, *, loop: AbstractEventLoop | None = None) -> None: ... - + def __init__(self, value: int = 1) -> None: ... def locked(self) -> bool: ... async def acquire(self) -> Literal[True]: ... def release(self) -> None: ... diff --git a/mypy/typeshed/stdlib/asyncio/proactor_events.pyi b/mypy/typeshed/stdlib/asyncio/proactor_events.pyi index 909d671df289d..09c096d40f04b 100644 --- a/mypy/typeshed/stdlib/asyncio/proactor_events.pyi +++ b/mypy/typeshed/stdlib/asyncio/proactor_events.pyi @@ -1,4 +1,3 @@ -import sys from collections.abc import Mapping from socket import socket from typing import Any, ClassVar, Literal @@ -20,27 +19,16 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin, transports.BaseTr def __del__(self) -> None: ... class _ProactorReadPipeTransport(_ProactorBasePipeTransport, transports.ReadTransport): - if sys.version_info >= (3, 10): - def __init__( - self, - loop: events.AbstractEventLoop, - sock: socket, - protocol: streams.StreamReaderProtocol, - waiter: futures.Future[Any] | None = None, - extra: Mapping[Any, Any] | None = None, - server: events.AbstractServer | None = None, - buffer_size: int = 65536, - ) -> None: ... - else: - def __init__( - self, - loop: events.AbstractEventLoop, - sock: socket, - protocol: streams.StreamReaderProtocol, - waiter: futures.Future[Any] | None = None, - extra: Mapping[Any, Any] | None = None, - server: events.AbstractServer | None = None, - ) -> None: ... + def __init__( + self, + loop: events.AbstractEventLoop, + sock: socket, + protocol: streams.StreamReaderProtocol, + waiter: futures.Future[Any] | None = None, + extra: Mapping[Any, Any] | None = None, + server: events.AbstractServer | None = None, + buffer_size: int = 65536, + ) -> None: ... class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport, transports.WriteTransport): ... class _ProactorWritePipeTransport(_ProactorBaseWritePipeTransport): ... diff --git a/mypy/typeshed/stdlib/asyncio/queues.pyi b/mypy/typeshed/stdlib/asyncio/queues.pyi index 2fa2226d0e6ae..de7c4879d348e 100644 --- a/mypy/typeshed/stdlib/asyncio/queues.pyi +++ b/mypy/typeshed/stdlib/asyncio/queues.pyi @@ -1,13 +1,9 @@ import sys from _typeshed import SupportsRichComparisonT -from asyncio.events import AbstractEventLoop from types import GenericAlias from typing import Any, Generic, TypeVar -if sys.version_info >= (3, 10): - from .mixins import _LoopBoundMixin -else: - _LoopBoundMixin = object +from .mixins import _LoopBoundMixin class QueueEmpty(Exception): ... class QueueFull(Exception): ... @@ -24,14 +20,8 @@ _T = TypeVar("_T") if sys.version_info >= (3, 13): class QueueShutDown(Exception): ... -# If Generic[_T] is last and _LoopBoundMixin is object, pyright is unhappy. -# We can remove the noqa pragma when dropping 3.9 support. -class Queue(Generic[_T], _LoopBoundMixin): # noqa: Y059 - if sys.version_info >= (3, 10): - def __init__(self, maxsize: int = 0) -> None: ... - else: - def __init__(self, maxsize: int = 0, *, loop: AbstractEventLoop | None = None) -> None: ... - +class Queue(_LoopBoundMixin, Generic[_T]): + def __init__(self, maxsize: int = 0) -> None: ... def _init(self, maxsize: int) -> None: ... def _get(self) -> _T: ... def _put(self, item: _T) -> None: ... diff --git a/mypy/typeshed/stdlib/asyncio/runners.pyi b/mypy/typeshed/stdlib/asyncio/runners.pyi index a100c9bcec6d6..3a1e33aac689e 100644 --- a/mypy/typeshed/stdlib/asyncio/runners.pyi +++ b/mypy/typeshed/stdlib/asyncio/runners.pyi @@ -27,7 +27,12 @@ if sys.version_info >= (3, 11): else: def run(self, coro: Coroutine[Any, Any, _T], *, context: Context | None = None) -> _T: ... -if sys.version_info >= (3, 12): +if sys.version_info >= (3, 14): + def run( + main: Awaitable[_T], *, debug: bool | None = None, loop_factory: Callable[[], AbstractEventLoop] | None = None + ) -> _T: ... + +elif sys.version_info >= (3, 12): def run( main: Coroutine[Any, Any, _T], *, debug: bool | None = None, loop_factory: Callable[[], AbstractEventLoop] | None = None ) -> _T: ... diff --git a/mypy/typeshed/stdlib/asyncio/sslproto.pyi b/mypy/typeshed/stdlib/asyncio/sslproto.pyi index ab102f124c2e8..72b4ea449e867 100644 --- a/mypy/typeshed/stdlib/asyncio/sslproto.pyi +++ b/mypy/typeshed/stdlib/asyncio/sslproto.pyi @@ -3,8 +3,7 @@ import sys from collections import deque from collections.abc import Callable from enum import Enum -from typing import Any, ClassVar, Final, Literal -from typing_extensions import TypeAlias +from typing import Any, ClassVar, Final, Literal, TypeAlias from . import constants, events, futures, protocols, transports diff --git a/mypy/typeshed/stdlib/asyncio/streams.pyi b/mypy/typeshed/stdlib/asyncio/streams.pyi index 33cffb11ed780..9e76c69d8732e 100644 --- a/mypy/typeshed/stdlib/asyncio/streams.pyi +++ b/mypy/typeshed/stdlib/asyncio/streams.pyi @@ -3,8 +3,8 @@ import sys from _typeshed import ReadableBuffer, StrPath from collections.abc import Awaitable, Callable, Iterable, Sequence, Sized from types import ModuleType -from typing import Any, Protocol, SupportsIndex, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, Protocol, SupportsIndex, TypeAlias, type_check_only +from typing_extensions import Self from . import events, protocols, transports from .base_events import Server @@ -28,67 +28,32 @@ _ClientConnectedCallback: TypeAlias = Callable[[StreamReader, StreamWriter], Awa @type_check_only class _ReaduntilBuffer(ReadableBuffer, Sized, Protocol): ... -if sys.version_info >= (3, 10): - async def open_connection( - host: str | None = None, - port: int | str | None = None, - *, - limit: int = 65536, - ssl_handshake_timeout: float | None = None, - **kwds: Any, - ) -> tuple[StreamReader, StreamWriter]: ... - async def start_server( - client_connected_cb: _ClientConnectedCallback, - host: str | Sequence[str] | None = None, - port: int | str | None = None, - *, - limit: int = 65536, - ssl_handshake_timeout: float | None = None, - **kwds: Any, - ) -> Server: ... +async def open_connection( + host: str | None = None, + port: int | str | None = None, + *, + limit: int = 65536, + ssl_handshake_timeout: float | None = None, + **kwds: Any, +) -> tuple[StreamReader, StreamWriter]: ... +async def start_server( + client_connected_cb: _ClientConnectedCallback, + host: str | Sequence[str] | None = None, + port: int | str | None = None, + *, + limit: int = 65536, + ssl_handshake_timeout: float | None = None, + **kwds: Any, +) -> Server: ... -else: - async def open_connection( - host: str | None = None, - port: int | str | None = None, - *, - loop: events.AbstractEventLoop | None = None, - limit: int = 65536, - ssl_handshake_timeout: float | None = None, - **kwds: Any, +if sys.platform != "win32": + async def open_unix_connection( + path: StrPath | None = None, *, limit: int = 65536, **kwds: Any ) -> tuple[StreamReader, StreamWriter]: ... - async def start_server( - client_connected_cb: _ClientConnectedCallback, - host: str | None = None, - port: int | str | None = None, - *, - loop: events.AbstractEventLoop | None = None, - limit: int = 65536, - ssl_handshake_timeout: float | None = None, - **kwds: Any, + async def start_unix_server( + client_connected_cb: _ClientConnectedCallback, path: StrPath | None = None, *, limit: int = 65536, **kwds: Any ) -> Server: ... -if sys.platform != "win32": - if sys.version_info >= (3, 10): - async def open_unix_connection( - path: StrPath | None = None, *, limit: int = 65536, **kwds: Any - ) -> tuple[StreamReader, StreamWriter]: ... - async def start_unix_server( - client_connected_cb: _ClientConnectedCallback, path: StrPath | None = None, *, limit: int = 65536, **kwds: Any - ) -> Server: ... - else: - async def open_unix_connection( - path: StrPath | None = None, *, loop: events.AbstractEventLoop | None = None, limit: int = 65536, **kwds: Any - ) -> tuple[StreamReader, StreamWriter]: ... - async def start_unix_server( - client_connected_cb: _ClientConnectedCallback, - path: StrPath | None = None, - *, - loop: events.AbstractEventLoop | None = None, - limit: int = 65536, - **kwds: Any, - ) -> Server: ... - class FlowControlMixin(protocols.Protocol): def __init__(self, loop: events.AbstractEventLoop | None = None) -> None: ... @@ -141,7 +106,7 @@ class StreamWriter: class StreamReader: def __init__(self, limit: int = 65536, loop: events.AbstractEventLoop | None = None) -> None: ... - def exception(self) -> Exception: ... + def exception(self) -> Exception | None: ... def set_exception(self, exc: Exception) -> None: ... def set_transport(self, transport: transports.BaseTransport) -> None: ... def feed_eof(self) -> None: ... diff --git a/mypy/typeshed/stdlib/asyncio/subprocess.pyi b/mypy/typeshed/stdlib/asyncio/subprocess.pyi index ceee2b5b90a09..6405e5ae14741 100644 --- a/mypy/typeshed/stdlib/asyncio/subprocess.pyi +++ b/mypy/typeshed/stdlib/asyncio/subprocess.pyi @@ -101,7 +101,7 @@ if sys.version_info >= (3, 11): pipesize: int = -1, ) -> Process: ... -elif sys.version_info >= (3, 10): +else: async def create_subprocess_shell( cmd: str | bytes, stdin: int | IO[Any] | None = None, @@ -164,67 +164,3 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> Process: ... - -else: # >= 3.9 - async def create_subprocess_shell( - cmd: str | bytes, - stdin: int | IO[Any] | None = None, - stdout: int | IO[Any] | None = None, - stderr: int | IO[Any] | None = None, - loop: events.AbstractEventLoop | None = None, - limit: int = 65536, - *, - # These parameters are forced to these values by BaseEventLoop.subprocess_shell - universal_newlines: Literal[False] = False, - shell: Literal[True] = True, - bufsize: Literal[0] = 0, - encoding: None = None, - errors: None = None, - text: Literal[False] | None = None, - # These parameters are taken by subprocess.Popen, which this ultimately delegates to - executable: StrOrBytesPath | None = None, - preexec_fn: Callable[[], Any] | None = None, - close_fds: bool = True, - cwd: StrOrBytesPath | None = None, - env: subprocess._ENV | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - group: None | str | int = None, - extra_groups: None | Collection[str | int] = None, - user: None | str | int = None, - umask: int = -1, - ) -> Process: ... - async def create_subprocess_exec( - program: StrOrBytesPath, - *args: StrOrBytesPath, - stdin: int | IO[Any] | None = None, - stdout: int | IO[Any] | None = None, - stderr: int | IO[Any] | None = None, - loop: events.AbstractEventLoop | None = None, - limit: int = 65536, - # These parameters are forced to these values by BaseEventLoop.subprocess_exec - universal_newlines: Literal[False] = False, - shell: Literal[False] = False, - bufsize: Literal[0] = 0, - encoding: None = None, - errors: None = None, - text: Literal[False] | None = None, - # These parameters are taken by subprocess.Popen, which this ultimately delegates to - executable: StrOrBytesPath | None = None, - preexec_fn: Callable[[], Any] | None = None, - close_fds: bool = True, - cwd: StrOrBytesPath | None = None, - env: subprocess._ENV | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - group: None | str | int = None, - extra_groups: None | Collection[str | int] = None, - user: None | str | int = None, - umask: int = -1, - ) -> Process: ... diff --git a/mypy/typeshed/stdlib/asyncio/taskgroups.pyi b/mypy/typeshed/stdlib/asyncio/taskgroups.pyi index 2968b07197614..886a79c4beb5a 100644 --- a/mypy/typeshed/stdlib/asyncio/taskgroups.pyi +++ b/mypy/typeshed/stdlib/asyncio/taskgroups.pyi @@ -37,3 +37,5 @@ class TaskGroup: ) -> Task[_T]: ... def _on_task_done(self, task: Task[object]) -> None: ... + if sys.version_info >= (3, 15): + def cancel(self) -> None: ... diff --git a/mypy/typeshed/stdlib/asyncio/tasks.pyi b/mypy/typeshed/stdlib/asyncio/tasks.pyi index 06e9f381a89ff..66c31f15e6fb1 100644 --- a/mypy/typeshed/stdlib/asyncio/tasks.pyi +++ b/mypy/typeshed/stdlib/asyncio/tasks.pyi @@ -8,8 +8,7 @@ from _asyncio import ( _unregister_task as _unregister_task, ) from collections.abc import AsyncIterator, Awaitable, Coroutine, Generator, Iterable, Iterator -from typing import Any, Final, Literal, Protocol, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only from . import _CoroutineLike from .events import AbstractEventLoop @@ -88,17 +87,12 @@ ALL_COMPLETED: Final = concurrent.futures.ALL_COMPLETED if sys.version_info >= (3, 13): @type_check_only - class _SyncAndAsyncIterator(Iterator[_T_co], AsyncIterator[_T_co], Protocol[_T_co]): ... + class _SyncAndAsyncIterator(Iterator[Coroutine[Any, Any, _T]], AsyncIterator[Future[_T]], Protocol[_T]): ... - def as_completed(fs: Iterable[_FutureLike[_T]], *, timeout: float | None = None) -> _SyncAndAsyncIterator[Future[_T]]: ... - -elif sys.version_info >= (3, 10): - def as_completed(fs: Iterable[_FutureLike[_T]], *, timeout: float | None = None) -> Iterator[Future[_T]]: ... + def as_completed(fs: Iterable[_FutureLike[_T]], *, timeout: float | None = None) -> _SyncAndAsyncIterator[_T]: ... else: - def as_completed( - fs: Iterable[_FutureLike[_T]], *, loop: AbstractEventLoop | None = None, timeout: float | None = None - ) -> Iterator[Future[_T]]: ... + def as_completed(fs: Iterable[_FutureLike[_T]], *, timeout: float | None = None) -> Iterator[Future[_T]]: ... @overload def ensure_future(coro_or_future: _FT, *, loop: AbstractEventLoop | None = None) -> _FT: ... # type: ignore[overload-overlap] @@ -111,269 +105,133 @@ def ensure_future(coro_or_future: Awaitable[_T], *, loop: AbstractEventLoop | No # typing PR #1550 for discussion. # # N.B. Having overlapping overloads is the only way to get acceptable type inference in all edge cases. -if sys.version_info >= (3, 10): - @overload - def gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: Literal[False] = False) -> Future[tuple[_T1]]: ... # type: ignore[overload-overlap] - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: Literal[False] = False - ) -> Future[tuple[_T1, _T2]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - /, - *, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - /, - *, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3, _T4]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - /, - *, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3, _T4, _T5]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - coro_or_future6: _FutureLike[_T6], - /, - *, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3, _T4, _T5, _T6]]: ... - @overload - def gather(*coros_or_futures: _FutureLike[_T], return_exceptions: Literal[False] = False) -> Future[list[_T]]: ... # type: ignore[overload-overlap] - @overload - def gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: bool) -> Future[tuple[_T1 | BaseException]]: ... - @overload - def gather( - coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: bool - ) -> Future[tuple[_T1 | BaseException, _T2 | BaseException]]: ... - @overload - def gather( - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - /, - *, - return_exceptions: bool, - ) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException]]: ... - @overload - def gather( - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - /, - *, - return_exceptions: bool, - ) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException]]: ... - @overload - def gather( - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - /, - *, - return_exceptions: bool, - ) -> Future[ - tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException, _T5 | BaseException] - ]: ... - @overload - def gather( - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - coro_or_future6: _FutureLike[_T6], - /, - *, - return_exceptions: bool, - ) -> Future[ - tuple[ - _T1 | BaseException, - _T2 | BaseException, - _T3 | BaseException, - _T4 | BaseException, - _T5 | BaseException, - _T6 | BaseException, - ] - ]: ... - @overload - def gather(*coros_or_futures: _FutureLike[_T], return_exceptions: bool) -> Future[list[_T | BaseException]]: ... - -else: - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], /, *, loop: AbstractEventLoop | None = None, return_exceptions: Literal[False] = False - ) -> Future[tuple[_T1]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3, _T4]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3, _T4, _T5]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - coro_or_future6: _FutureLike[_T6], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: Literal[False] = False, - ) -> Future[tuple[_T1, _T2, _T3, _T4, _T5, _T6]]: ... - @overload - def gather( # type: ignore[overload-overlap] - *coros_or_futures: _FutureLike[_T], loop: AbstractEventLoop | None = None, return_exceptions: Literal[False] = False - ) -> Future[list[_T]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], /, *, loop: AbstractEventLoop | None = None, return_exceptions: bool - ) -> Future[tuple[_T1 | BaseException]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: bool, - ) -> Future[tuple[_T1 | BaseException, _T2 | BaseException]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: bool, - ) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: bool, - ) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException]]: ... - @overload - def gather( # type: ignore[overload-overlap] - coro_or_future1: _FutureLike[_T1], - coro_or_future2: _FutureLike[_T2], - coro_or_future3: _FutureLike[_T3], - coro_or_future4: _FutureLike[_T4], - coro_or_future5: _FutureLike[_T5], - coro_or_future6: _FutureLike[_T6], - /, - *, - loop: AbstractEventLoop | None = None, - return_exceptions: bool, - ) -> Future[ - tuple[ - _T1 | BaseException, - _T2 | BaseException, - _T3 | BaseException, - _T4 | BaseException, - _T5 | BaseException, - _T6 | BaseException, - ] - ]: ... - @overload - def gather( - *coros_or_futures: _FutureLike[_T], loop: AbstractEventLoop | None = None, return_exceptions: bool - ) -> Future[list[_T | BaseException]]: ... +@overload +def gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: Literal[False] = False) -> Future[tuple[_T1]]: ... # type: ignore[overload-overlap] +@overload +def gather( # type: ignore[overload-overlap] + coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: Literal[False] = False +) -> Future[tuple[_T1, _T2]]: ... +@overload +def gather( # type: ignore[overload-overlap] + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + /, + *, + return_exceptions: Literal[False] = False, +) -> Future[tuple[_T1, _T2, _T3]]: ... +@overload +def gather( # type: ignore[overload-overlap] + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + coro_or_future4: _FutureLike[_T4], + /, + *, + return_exceptions: Literal[False] = False, +) -> Future[tuple[_T1, _T2, _T3, _T4]]: ... +@overload +def gather( # type: ignore[overload-overlap] + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + coro_or_future4: _FutureLike[_T4], + coro_or_future5: _FutureLike[_T5], + /, + *, + return_exceptions: Literal[False] = False, +) -> Future[tuple[_T1, _T2, _T3, _T4, _T5]]: ... +@overload +def gather( # type: ignore[overload-overlap] + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + coro_or_future4: _FutureLike[_T4], + coro_or_future5: _FutureLike[_T5], + coro_or_future6: _FutureLike[_T6], + /, + *, + return_exceptions: Literal[False] = False, +) -> Future[tuple[_T1, _T2, _T3, _T4, _T5, _T6]]: ... +@overload +def gather(*coros_or_futures: _FutureLike[_T], return_exceptions: Literal[False] = False) -> Future[list[_T]]: ... # type: ignore[overload-overlap] +@overload +def gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: bool) -> Future[tuple[_T1 | BaseException]]: ... +@overload +def gather( + coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: bool +) -> Future[tuple[_T1 | BaseException, _T2 | BaseException]]: ... +@overload +def gather( + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + /, + *, + return_exceptions: bool, +) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException]]: ... +@overload +def gather( + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + coro_or_future4: _FutureLike[_T4], + /, + *, + return_exceptions: bool, +) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException]]: ... +@overload +def gather( + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + coro_or_future4: _FutureLike[_T4], + coro_or_future5: _FutureLike[_T5], + /, + *, + return_exceptions: bool, +) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException, _T5 | BaseException]]: ... +@overload +def gather( + coro_or_future1: _FutureLike[_T1], + coro_or_future2: _FutureLike[_T2], + coro_or_future3: _FutureLike[_T3], + coro_or_future4: _FutureLike[_T4], + coro_or_future5: _FutureLike[_T5], + coro_or_future6: _FutureLike[_T6], + /, + *, + return_exceptions: bool, +) -> Future[ + tuple[ + _T1 | BaseException, + _T2 | BaseException, + _T3 | BaseException, + _T4 | BaseException, + _T5 | BaseException, + _T6 | BaseException, + ] +]: ... +@overload +def gather(*coros_or_futures: _FutureLike[_T], return_exceptions: bool) -> Future[list[_T | BaseException]]: ... # unlike some asyncio apis, This does strict runtime checking of actually being a coroutine, not of any future-like. def run_coroutine_threadsafe(coro: Coroutine[Any, Any, _T], loop: AbstractEventLoop) -> concurrent.futures.Future[_T]: ... +def shield(arg: _FutureLike[_T]) -> Future[_T]: ... -if sys.version_info >= (3, 10): - def shield(arg: _FutureLike[_T]) -> Future[_T]: ... - @overload - async def sleep(delay: float) -> None: ... - @overload - async def sleep(delay: float, result: _T) -> _T: ... - async def wait_for(fut: _FutureLike[_T], timeout: float | None) -> _T: ... +@overload +async def sleep(delay: float) -> None: ... +@overload +async def sleep(delay: float, result: _T) -> _T: ... -else: - def shield(arg: _FutureLike[_T], *, loop: AbstractEventLoop | None = None) -> Future[_T]: ... - @overload - async def sleep(delay: float, *, loop: AbstractEventLoop | None = None) -> None: ... - @overload - async def sleep(delay: float, result: _T, *, loop: AbstractEventLoop | None = None) -> _T: ... - async def wait_for(fut: _FutureLike[_T], timeout: float | None, *, loop: AbstractEventLoop | None = None) -> _T: ... +async def wait_for(fut: _FutureLike[_T], timeout: float | None) -> _T: ... if sys.version_info >= (3, 11): async def wait( fs: Iterable[_FT], *, timeout: float | None = None, return_when: str = "ALL_COMPLETED" ) -> tuple[set[_FT], set[_FT]]: ... -elif sys.version_info >= (3, 10): +else: @overload async def wait( # type: ignore[overload-overlap] fs: Iterable[_FT], *, timeout: float | None = None, return_when: str = "ALL_COMPLETED" @@ -383,24 +241,6 @@ elif sys.version_info >= (3, 10): fs: Iterable[Awaitable[_T]], *, timeout: float | None = None, return_when: str = "ALL_COMPLETED" ) -> tuple[set[Task[_T]], set[Task[_T]]]: ... -else: - @overload - async def wait( # type: ignore[overload-overlap] - fs: Iterable[_FT], - *, - loop: AbstractEventLoop | None = None, - timeout: float | None = None, - return_when: str = "ALL_COMPLETED", - ) -> tuple[set[_FT], set[_FT]]: ... - @overload - async def wait( - fs: Iterable[Awaitable[_T]], - *, - loop: AbstractEventLoop | None = None, - timeout: float | None = None, - return_when: str = "ALL_COMPLETED", - ) -> tuple[set[Task[_T]], set[Task[_T]]]: ... - if sys.version_info >= (3, 12): _TaskCompatibleCoro: TypeAlias = Coroutine[Any, Any, _T_co] else: diff --git a/mypy/typeshed/stdlib/asyncio/threads.pyi b/mypy/typeshed/stdlib/asyncio/threads.pyi index 00aae2ea814cb..f1d8829180982 100644 --- a/mypy/typeshed/stdlib/asyncio/threads.pyi +++ b/mypy/typeshed/stdlib/asyncio/threads.pyi @@ -1,6 +1,5 @@ from collections.abc import Callable -from typing import TypeVar -from typing_extensions import ParamSpec +from typing import ParamSpec, TypeVar # Keep asyncio.__all__ updated with any changes to __all__ here __all__ = ("to_thread",) diff --git a/mypy/typeshed/stdlib/asyncio/tools.pyi b/mypy/typeshed/stdlib/asyncio/tools.pyi index bc8b809b9c055..36c65541c9cd0 100644 --- a/mypy/typeshed/stdlib/asyncio/tools.pyi +++ b/mypy/typeshed/stdlib/asyncio/tools.pyi @@ -42,5 +42,10 @@ def build_task_table(result: Iterable[_AwaitedInfo]) -> list[list[int | str]]: . if sys.version_info >= (3, 14): def exit_with_permission_help_text() -> None: ... -def display_awaited_by_tasks_table(pid: SupportsIndex) -> None: ... -def display_awaited_by_tasks_tree(pid: SupportsIndex) -> None: ... +if sys.version_info >= (3, 15): + def display_awaited_by_tasks_table(pid: SupportsIndex, retries: SupportsIndex = 3) -> None: ... + def display_awaited_by_tasks_tree(pid: SupportsIndex, retries: SupportsIndex = 3) -> None: ... + +else: + def display_awaited_by_tasks_table(pid: SupportsIndex) -> None: ... + def display_awaited_by_tasks_tree(pid: SupportsIndex) -> None: ... diff --git a/mypy/typeshed/stdlib/asyncio/trsock.pyi b/mypy/typeshed/stdlib/asyncio/trsock.pyi index d3e95559ebefa..f621240d668a2 100644 --- a/mypy/typeshed/stdlib/asyncio/trsock.pyi +++ b/mypy/typeshed/stdlib/asyncio/trsock.pyi @@ -4,8 +4,8 @@ from _typeshed import ReadableBuffer from builtins import type as Type # alias to avoid name clashes with property named "type" from collections.abc import Iterable from types import TracebackType -from typing import Any, BinaryIO, NoReturn, overload -from typing_extensions import TypeAlias, deprecated +from typing import Any, BinaryIO, NoReturn, TypeAlias, overload +from typing_extensions import deprecated # These are based in socket, maybe move them out into _typeshed.pyi or such _Address: TypeAlias = socket._Address @@ -27,14 +27,17 @@ class TransportSocket: def dup(self) -> socket.socket: ... def get_inheritable(self) -> bool: ... def shutdown(self, how: int) -> None: ... + @overload def getsockopt(self, level: int, optname: int) -> int: ... @overload def getsockopt(self, level: int, optname: int, buflen: int) -> bytes: ... + @overload def setsockopt(self, level: int, optname: int, value: int | ReadableBuffer) -> None: ... @overload def setsockopt(self, level: int, optname: int, value: None, optlen: int) -> None: ... + def getpeername(self) -> _RetAddress: ... def getsockname(self) -> _RetAddress: ... def getsockbyname(self) -> NoReturn: ... # This method doesn't exist on socket, yet is passed through? @@ -51,6 +54,7 @@ class TransportSocket: def connect_ex(self, address: _Address) -> int: ... @deprecated("Removed in Python 3.11") def bind(self, address: _Address) -> None: ... + if sys.platform == "win32": @deprecated("Removed in Python 3.11") def ioctl(self, control: int, option: int | tuple[int, int, int] | bool) -> None: ... @@ -68,6 +72,7 @@ class TransportSocket: def close(self) -> None: ... @deprecated("Removed in Python 3.11") def detach(self) -> int: ... + if sys.platform == "linux": @deprecated("Removed in Python 3.11") def sendmsg_afalg( @@ -88,18 +93,21 @@ class TransportSocket: address: _Address | None = None, /, ) -> int: ... + @overload @deprecated("Removed in Python 3.11.") def sendto(self, data: ReadableBuffer, address: _Address) -> int: ... @overload @deprecated("Removed in Python 3.11.") def sendto(self, data: ReadableBuffer, flags: int, address: _Address) -> int: ... + @deprecated("Removed in Python 3.11.") def send(self, data: ReadableBuffer, flags: int = 0) -> int: ... @deprecated("Removed in Python 3.11.") def sendall(self, data: ReadableBuffer, flags: int = 0) -> None: ... @deprecated("Removed in Python 3.11.") def set_inheritable(self, inheritable: bool) -> None: ... + if sys.platform == "win32": @deprecated("Removed in Python 3.11.") def share(self, process_id: int) -> bytes: ... diff --git a/mypy/typeshed/stdlib/asyncio/unix_events.pyi b/mypy/typeshed/stdlib/asyncio/unix_events.pyi index 54a2a039749e3..368cac38302f6 100644 --- a/mypy/typeshed/stdlib/asyncio/unix_events.pyi +++ b/mypy/typeshed/stdlib/asyncio/unix_events.pyi @@ -95,6 +95,7 @@ if sys.platform != "win32": if sys.version_info >= (3, 12): # Doesn't actually have ABCMeta metaclass at runtime, but mypy complains if we don't have it in the stub. # See discussion in #7412 + @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") class BaseChildWatcher(AbstractChildWatcher, metaclass=ABCMeta): def close(self) -> None: ... def is_active(self) -> bool: ... diff --git a/mypy/typeshed/stdlib/asyncore.pyi b/mypy/typeshed/stdlib/asyncore.pyi index 36d1862fdda78..96f9f637c7333 100644 --- a/mypy/typeshed/stdlib/asyncore.pyi +++ b/mypy/typeshed/stdlib/asyncore.pyi @@ -1,8 +1,7 @@ import sys from _typeshed import FileDescriptorLike, ReadableBuffer from socket import socket -from typing import Any, overload -from typing_extensions import TypeAlias +from typing import Any, TypeAlias, overload # cyclic dependence with asynchat _MapType: TypeAlias = dict[int, Any] @@ -75,10 +74,12 @@ if sys.platform != "win32": def __init__(self, fd: int) -> None: ... def recv(self, bufsize: int, flags: int = ...) -> bytes: ... def send(self, data: bytes, flags: int = ...) -> int: ... + @overload def getsockopt(self, level: int, optname: int, buflen: None = None) -> int: ... @overload def getsockopt(self, level: int, optname: int, buflen: int) -> bytes: ... + def read(self, bufsize: int, flags: int = ...) -> bytes: ... def write(self, data: bytes, flags: int = ...) -> int: ... def close(self) -> None: ... diff --git a/mypy/typeshed/stdlib/atexit.pyi b/mypy/typeshed/stdlib/atexit.pyi index 7f7b05ccc0a39..9177d80169be2 100644 --- a/mypy/typeshed/stdlib/atexit.pyi +++ b/mypy/typeshed/stdlib/atexit.pyi @@ -1,6 +1,5 @@ from collections.abc import Callable -from typing import TypeVar -from typing_extensions import ParamSpec +from typing import ParamSpec, TypeVar _T = TypeVar("_T") _P = ParamSpec("_P") diff --git a/mypy/typeshed/stdlib/audioop.pyi b/mypy/typeshed/stdlib/audioop.pyi index f3ce78ccb7fae..a7d5e8adb7a52 100644 --- a/mypy/typeshed/stdlib/audioop.pyi +++ b/mypy/typeshed/stdlib/audioop.pyi @@ -1,4 +1,5 @@ -from typing_extensions import Buffer, TypeAlias +from typing import TypeAlias +from typing_extensions import Buffer _AdpcmState: TypeAlias = tuple[int, int] _RatecvState: TypeAlias = tuple[int, tuple[tuple[int, int], ...]] diff --git a/mypy/typeshed/stdlib/base64.pyi b/mypy/typeshed/stdlib/base64.pyi index 279d74a94ebe2..67bc37309a976 100644 --- a/mypy/typeshed/stdlib/base64.pyi +++ b/mypy/typeshed/stdlib/base64.pyi @@ -13,6 +13,8 @@ __all__ = [ "b32decode", "b16encode", "b16decode", + "b32hexencode", + "b32hexdecode", "b85encode", "b85decode", "a85encode", @@ -23,39 +25,101 @@ __all__ = [ "urlsafe_b64decode", ] -if sys.version_info >= (3, 10): - __all__ += ["b32hexencode", "b32hexdecode"] if sys.version_info >= (3, 13): __all__ += ["z85decode", "z85encode"] -def b64encode(s: ReadableBuffer, altchars: ReadableBuffer | None = None) -> bytes: ... -def b64decode(s: str | ReadableBuffer, altchars: str | ReadableBuffer | None = None, validate: bool = False) -> bytes: ... +if sys.version_info >= (3, 15): + def b64encode( + s: ReadableBuffer, altchars: ReadableBuffer | None = None, *, padded: bool = True, wrapcol: int = 0 + ) -> bytes: ... + def b64decode( + s: str | ReadableBuffer, + altchars: str | ReadableBuffer | None = None, + validate: bool = ..., + *, + padded: bool = True, + ignorechars: ReadableBuffer = ..., + canonical: bool = False, + ) -> bytes: ... + +else: + def b64encode(s: ReadableBuffer, altchars: ReadableBuffer | None = None) -> bytes: ... + def b64decode(s: str | ReadableBuffer, altchars: str | ReadableBuffer | None = None, validate: bool = False) -> bytes: ... + def standard_b64encode(s: ReadableBuffer) -> bytes: ... def standard_b64decode(s: str | ReadableBuffer) -> bytes: ... -def urlsafe_b64encode(s: ReadableBuffer) -> bytes: ... -def urlsafe_b64decode(s: str | ReadableBuffer) -> bytes: ... -def b32encode(s: ReadableBuffer) -> bytes: ... -def b32decode(s: str | ReadableBuffer, casefold: bool = False, map01: str | ReadableBuffer | None = None) -> bytes: ... -def b16encode(s: ReadableBuffer) -> bytes: ... -def b16decode(s: str | ReadableBuffer, casefold: bool = False) -> bytes: ... - -if sys.version_info >= (3, 10): + +if sys.version_info >= (3, 15): + def urlsafe_b64encode(s: ReadableBuffer, *, padded: bool = True) -> bytes: ... + def urlsafe_b64decode(s: str | ReadableBuffer, *, padded: bool = False) -> bytes: ... + def b32encode(s: ReadableBuffer, *, padded: bool = True, wrapcol: int = 0) -> bytes: ... + def b32decode( + s: str | ReadableBuffer, + casefold: bool = False, + map01: str | ReadableBuffer | None = None, + *, + padded: bool = True, + ignorechars: ReadableBuffer = b"", + canonical: bool = False, + ) -> bytes: ... + def b16encode(s: ReadableBuffer, *, wrapcol: int = 0) -> bytes: ... + def b16decode(s: str | ReadableBuffer, casefold: bool = False, *, ignorechars: ReadableBuffer = b"") -> bytes: ... + +else: + def urlsafe_b64encode(s: ReadableBuffer) -> bytes: ... + def urlsafe_b64decode(s: str | ReadableBuffer) -> bytes: ... + def b32encode(s: ReadableBuffer) -> bytes: ... + def b32decode(s: str | ReadableBuffer, casefold: bool = False, map01: str | ReadableBuffer | None = None) -> bytes: ... + def b16encode(s: ReadableBuffer) -> bytes: ... + def b16decode(s: str | ReadableBuffer, casefold: bool = False) -> bytes: ... + +if sys.version_info >= (3, 15): + def b32hexencode(s: ReadableBuffer, *, padded: bool = True, wrapcol: int = 0) -> bytes: ... + def b32hexdecode( + s: str | ReadableBuffer, + casefold: bool = False, + *, + padded: bool = True, + ignorechars: ReadableBuffer = b"", + canonical: bool = False, + ) -> bytes: ... + +else: def b32hexencode(s: ReadableBuffer) -> bytes: ... def b32hexdecode(s: str | ReadableBuffer, casefold: bool = False) -> bytes: ... def a85encode( b: ReadableBuffer, *, foldspaces: bool = False, wrapcol: int = 0, pad: bool = False, adobe: bool = False ) -> bytes: ... -def a85decode( - b: str | ReadableBuffer, *, foldspaces: bool = False, adobe: bool = False, ignorechars: bytearray | bytes = b" \t\n\r\x0b" -) -> bytes: ... -def b85encode(b: ReadableBuffer, pad: bool = False) -> bytes: ... -def b85decode(b: str | ReadableBuffer) -> bytes: ... + +if sys.version_info >= (3, 15): + def a85decode( + b: str | ReadableBuffer, + *, + foldspaces: bool = False, + adobe: bool = False, + ignorechars: bytearray | bytes = b" \t\n\r\x0b", + canonical: bool = False, + ) -> bytes: ... + def b85encode(b: ReadableBuffer, pad: bool = False, *, wrapcol: int = 0) -> bytes: ... + def b85decode(b: str | ReadableBuffer, *, ignorechars: ReadableBuffer = b"", canonical: bool = False) -> bytes: ... + +else: + def a85decode( + b: str | ReadableBuffer, *, foldspaces: bool = False, adobe: bool = False, ignorechars: bytearray | bytes = b" \t\n\r\x0b" + ) -> bytes: ... + def b85encode(b: ReadableBuffer, pad: bool = False) -> bytes: ... + def b85decode(b: str | ReadableBuffer) -> bytes: ... + def decode(input: IO[bytes], output: IO[bytes]) -> None: ... def encode(input: IO[bytes], output: IO[bytes]) -> None: ... def encodebytes(s: ReadableBuffer) -> bytes: ... def decodebytes(s: ReadableBuffer) -> bytes: ... if sys.version_info >= (3, 13): - def z85encode(s: ReadableBuffer) -> bytes: ... - def z85decode(s: str | ReadableBuffer) -> bytes: ... + if sys.version_info >= (3, 15): + def z85encode(s: ReadableBuffer, pad: bool = False, *, wrapcol: int = 0) -> bytes: ... + def z85decode(s: str | ReadableBuffer, *, ignorechars: ReadableBuffer = b"", canonical: bool = False) -> bytes: ... + else: + def z85encode(s: ReadableBuffer) -> bytes: ... + def z85decode(s: str | ReadableBuffer) -> bytes: ... diff --git a/mypy/typeshed/stdlib/bdb.pyi b/mypy/typeshed/stdlib/bdb.pyi index b6be2210ffe2e..c2c45e2684b4b 100644 --- a/mypy/typeshed/stdlib/bdb.pyi +++ b/mypy/typeshed/stdlib/bdb.pyi @@ -1,10 +1,9 @@ import sys -from _typeshed import ExcInfo, TraceFunction, Unused +from _typeshed import ExcInfo, ReadableBuffer, TraceFunction, Unused from collections.abc import Callable, Iterable, Iterator, Mapping from contextlib import contextmanager from types import CodeType, FrameType, TracebackType -from typing import IO, Any, Final, Literal, SupportsInt, TypeVar -from typing_extensions import ParamSpec, TypeAlias +from typing import IO, Any, Final, Literal, ParamSpec, SupportsInt, TypeAlias, TypeVar __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -85,11 +84,21 @@ class Bdb: def get_all_breaks(self) -> dict[str, list[int]]: ... def get_stack(self, f: FrameType | None, t: TracebackType | None) -> tuple[list[tuple[FrameType, int]], int]: ... def format_stack_entry(self, frame_lineno: tuple[FrameType, int], lprefix: str = ": ") -> str: ... - def run( - self, cmd: str | CodeType, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None + def run( # matches `builtins.exec` + self, + cmd: str | ReadableBuffer | CodeType, + globals: dict[str, Any] | None = None, + locals: Mapping[str, object] | None = None, ) -> None: ... - def runeval(self, expr: str, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None) -> None: ... - def runctx(self, cmd: str | CodeType, globals: dict[str, Any] | None, locals: Mapping[str, Any] | None) -> None: ... + def runctx( # matches `builtins.exec` + self, cmd: str | ReadableBuffer | CodeType, globals: dict[str, Any] | None, locals: Mapping[str, object] | None + ) -> None: ... + def runeval( # matches `builtins.eval` + self, + expr: str | ReadableBuffer | CodeType, + globals: dict[str, Any] | None = None, + locals: Mapping[str, object] | None = None, + ) -> Any: ... def runcall(self, func: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> _T | None: ... if sys.version_info >= (3, 14): def start_trace(self) -> None: ... diff --git a/mypy/typeshed/stdlib/binascii.pyi b/mypy/typeshed/stdlib/binascii.pyi index 5606d5cdf74d9..6840c68838119 100644 --- a/mypy/typeshed/stdlib/binascii.pyi +++ b/mypy/typeshed/stdlib/binascii.pyi @@ -1,6 +1,7 @@ import sys from _typeshed import ReadableBuffer -from typing_extensions import TypeAlias, deprecated +from typing import TypeAlias +from typing_extensions import deprecated # Many functions in binascii accept buffer objects # or ASCII-only strings. @@ -9,13 +10,68 @@ _AsciiBuffer: TypeAlias = str | ReadableBuffer def a2b_uu(data: _AsciiBuffer, /) -> bytes: ... def b2a_uu(data: ReadableBuffer, /, *, backtick: bool = False) -> bytes: ... -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 15): + ASCII85_ALPHABET: bytes + BINHEX_ALPHABET: bytes + CRYPT_ALPHABET: bytes + UU_ALPHABET: bytes + BASE64_ALPHABET: bytes + URLSAFE_BASE64_ALPHABET: bytes + BASE32_ALPHABET: bytes + BASE32HEX_ALPHABET: bytes + BASE85_ALPHABET: bytes + Z85_ALPHABET: bytes + def a2b_base64( + data: _AsciiBuffer, + /, + *, + strict_mode: bool = False, + alphabet: bytes = ..., + padded: bool = True, + ignorechars: ReadableBuffer = ..., + canonical: bool = False, + ) -> bytes: ... + def b2a_base64( + data: ReadableBuffer, /, *, newline: bool = True, alphabet: ReadableBuffer = ..., padded: bool = True, wrapcol: int = 0 + ) -> bytes: ... + def b2a_base32( + data: ReadableBuffer, /, *, alphabet: ReadableBuffer = ..., padded: bool = True, wrapcol: int = 0 + ) -> bytes: ... + def a2b_base32( + data: _AsciiBuffer, + /, + *, + alphabet: bytes = ..., + padded: bool = True, + ignorechars: ReadableBuffer = b"", + canonical: bool = False, + ) -> bytes: ... + def b2a_ascii85( + data: ReadableBuffer, /, *, foldspaces: bool = False, wrapcol: int = 0, pad: bool = False, adobe: bool = False + ) -> bytes: ... + def a2b_ascii85( + data: _AsciiBuffer, + /, + *, + foldspaces: bool = False, + adobe: bool = False, + ignorechars: ReadableBuffer = b"", + canonical: bool = False, + ) -> bytes: ... + def b2a_base85(data: ReadableBuffer, /, *, alphabet: ReadableBuffer = ..., pad: bool = False, wrapcol: int = 0) -> bytes: ... + def a2b_base85( + data: _AsciiBuffer, /, *, alphabet: bytes = ..., ignorechars: ReadableBuffer = b"", canonical: bool = False + ) -> bytes: ... + +elif sys.version_info >= (3, 11): def a2b_base64(data: _AsciiBuffer, /, *, strict_mode: bool = False) -> bytes: ... else: def a2b_base64(data: _AsciiBuffer, /) -> bytes: ... -def b2a_base64(data: ReadableBuffer, /, *, newline: bool = True) -> bytes: ... +if sys.version_info < (3, 15): + def b2a_base64(data: ReadableBuffer, /, *, newline: bool = True) -> bytes: ... + def a2b_qp(data: _AsciiBuffer, header: bool = False) -> bytes: ... def b2a_qp(data: ReadableBuffer, quotetabs: bool = False, istext: bool = True, header: bool = False) -> bytes: ... @@ -33,8 +89,14 @@ def crc_hqx(data: ReadableBuffer, crc: int, /) -> int: ... def crc32(data: ReadableBuffer, crc: int = 0, /) -> int: ... def b2a_hex(data: ReadableBuffer, sep: str | bytes = ..., bytes_per_sep: int = 1) -> bytes: ... def hexlify(data: ReadableBuffer, sep: str | bytes = ..., bytes_per_sep: int = 1) -> bytes: ... -def a2b_hex(hexstr: _AsciiBuffer, /) -> bytes: ... -def unhexlify(hexstr: _AsciiBuffer, /) -> bytes: ... + +if sys.version_info >= (3, 15): + def a2b_hex(hexstr: _AsciiBuffer, /, *, ignorechars: ReadableBuffer = b"") -> bytes: ... + def unhexlify(hexstr: _AsciiBuffer, /, *, ignorechars: ReadableBuffer = b"") -> bytes: ... + +else: + def a2b_hex(hexstr: _AsciiBuffer, /) -> bytes: ... + def unhexlify(hexstr: _AsciiBuffer, /) -> bytes: ... class Error(ValueError): ... class Incomplete(Exception): ... diff --git a/mypy/typeshed/stdlib/binhex.pyi b/mypy/typeshed/stdlib/binhex.pyi index bdead928468f4..f309f4e026a59 100644 --- a/mypy/typeshed/stdlib/binhex.pyi +++ b/mypy/typeshed/stdlib/binhex.pyi @@ -1,6 +1,5 @@ from _typeshed import SizedBuffer -from typing import IO, Any, Final -from typing_extensions import TypeAlias +from typing import IO, Any, Final, TypeAlias __all__ = ["binhex", "hexbin", "Error"] diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi index 25b21ba971540..d773f98e90b6b 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi @@ -33,20 +33,22 @@ from _typeshed import ( from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from os import PathLike -from types import CellType, CodeType, GenericAlias, TracebackType +from types import CellType, CodeType, EllipsisType, GenericAlias, NotImplementedType, TracebackType # mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} # are imported from collections.abc in builtins.pyi -from typing import ( # noqa: Y022,UP035,RUF100 +from typing import ( # noqa: Y022,UP035 IO, Any, BinaryIO, ClassVar, + Concatenate, Final, Generic, Mapping, MutableMapping, MutableSequence, + ParamSpec, Protocol, Sequence, SupportsAbs, @@ -54,6 +56,8 @@ from typing import ( # noqa: Y022,UP035,RUF100 SupportsComplex, SupportsFloat, SupportsIndex, + TypeAlias, + TypeGuard, TypeVar, final, overload, @@ -61,18 +65,7 @@ from typing import ( # noqa: Y022,UP035,RUF100 ) # we can't import `Literal` from typing or mypy crashes: see #11247 -from typing_extensions import ( # noqa: Y023 - Concatenate, - Literal, - ParamSpec, - Self, - TypeAlias, - TypeGuard, - TypeIs, - TypeVarTuple, - deprecated, - disjoint_base, -) +from typing_extensions import Literal, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 if sys.version_info >= (3, 14): from _typeshed import AnnotateFunc @@ -110,10 +103,12 @@ class object: __dict__: dict[str, Any] __module__: str __annotations__: dict[str, Any] + @property def __class__(self) -> type[Self]: ... @__class__.setter def __class__(self, type: type[Self], /) -> None: ... + def __init__(self) -> None: ... def __new__(cls) -> Self: ... # N.B. `object.__setattr__` and `object.__delattr__` are heavily special-cased by type checkers. @@ -142,41 +137,43 @@ class object: @disjoint_base class staticmethod(Generic[_P, _R_co]): + __name__: str + __qualname__: str @property def __func__(self) -> Callable[_P, _R_co]: ... @property def __isabstractmethod__(self) -> bool: ... def __init__(self, f: Callable[_P, _R_co], /) -> None: ... + @overload def __get__(self, instance: None, owner: type, /) -> Callable[_P, _R_co]: ... @overload def __get__(self, instance: _T, owner: type[_T] | None = None, /) -> Callable[_P, _R_co]: ... - if sys.version_info >= (3, 10): - __name__: str - __qualname__: str - @property - def __wrapped__(self) -> Callable[_P, _R_co]: ... - def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... + + @property + def __wrapped__(self) -> Callable[_P, _R_co]: ... + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... if sys.version_info >= (3, 14): def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... __annotate__: AnnotateFunc | None @disjoint_base class classmethod(Generic[_T, _P, _R_co]): + __name__: str + __qualname__: str @property def __func__(self) -> Callable[Concatenate[type[_T], _P], _R_co]: ... @property def __isabstractmethod__(self) -> bool: ... def __init__(self, f: Callable[Concatenate[type[_T], _P], _R_co], /) -> None: ... + @overload def __get__(self, instance: _T, owner: type[_T] | None = None, /) -> Callable[_P, _R_co]: ... @overload def __get__(self, instance: None, owner: type[_T], /) -> Callable[_P, _R_co]: ... - if sys.version_info >= (3, 10): - __name__: str - __qualname__: str - @property - def __wrapped__(self) -> Callable[Concatenate[type[_T], _P], _R_co]: ... + + @property + def __wrapped__(self) -> Callable[Concatenate[type[_T], _P], _R_co]: ... if sys.version_info >= (3, 14): def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... __annotate__: AnnotateFunc | None @@ -207,16 +204,19 @@ class type: def __text_signature__(self) -> str | None: ... @property def __weakrefoffset__(self) -> int: ... + @overload def __init__(self, o: object, /) -> None: ... @overload def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... + @overload def __new__(cls, o: object, /) -> type: ... @overload def __new__( cls: type[_typeshed.Self], name: str, bases: tuple[type, ...], namespace: dict[str, Any], /, **kwds: Any ) -> _typeshed.Self: ... + def __call__(self, *args: Any, **kwds: Any) -> Any: ... def __subclasses__(self: _typeshed.Self) -> list[_typeshed.Self]: ... # Note: the documentation doesn't specify what the return type is, the standard @@ -226,11 +226,10 @@ class type: def __subclasscheck__(self, subclass: type, /) -> bool: ... @classmethod def __prepare__(metacls, name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]: ... - if sys.version_info >= (3, 10): - # `int | str` produces an instance of `UnionType`, but `int | int` produces an instance of `type`, - # and `abc.ABC | abc.ABC` produces an instance of `abc.ABCMeta`. - def __or__(self: _typeshed.Self, value: Any, /) -> types.UnionType | _typeshed.Self: ... - def __ror__(self: _typeshed.Self, value: Any, /) -> types.UnionType | _typeshed.Self: ... + # `int | str` produces an instance of `UnionType`, but `int | int` produces an instance of `type`, + # and `abc.ABC | abc.ABC` produces an instance of `abc.ABCMeta`. + def __or__(self: _typeshed.Self, value: Any, /) -> types.UnionType | _typeshed.Self: ... + def __ror__(self: _typeshed.Self, value: Any, /) -> types.UnionType | _typeshed.Self: ... if sys.version_info >= (3, 12): __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] __annotations__: dict[str, AnnotationForm] @@ -256,6 +255,7 @@ class int: def __new__(cls, x: ConvertibleToInt = 0, /) -> Self: ... @overload def __new__(cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self: ... + def as_integer_ratio(self) -> tuple[int, Literal[1]]: ... @property def real(self) -> int: ... @@ -267,8 +267,7 @@ class int: def denominator(self) -> Literal[1]: ... def conjugate(self) -> int: ... def bit_length(self) -> int: ... - if sys.version_info >= (3, 10): - def bit_count(self) -> int: ... + def bit_count(self) -> int: ... if sys.version_info >= (3, 11): def to_bytes( @@ -310,6 +309,7 @@ class int: def __rtruediv__(self, value: int, /) -> float: ... def __rmod__(self, value: int, /) -> int: ... def __rdivmod__(self, value: int, /) -> tuple[int, int]: ... + @overload def __pow__(self, x: Literal[0], /) -> Literal[1]: ... @overload @@ -324,6 +324,7 @@ class int: def __pow__(self, value: int, mod: None = None, /) -> Any: ... @overload def __pow__(self, value: int, mod: int, /) -> int: ... + def __rpow__(self, value: int, mod: int | None = None, /) -> Any: ... def __and__(self, value: int, /) -> int: ... def __or__(self, value: int, /) -> int: ... @@ -381,12 +382,14 @@ class float: def __truediv__(self, value: float, /) -> float: ... def __mod__(self, value: float, /) -> float: ... def __divmod__(self, value: float, /) -> tuple[float, float]: ... + @overload def __pow__(self, value: int, mod: None = None, /) -> float: ... # positive __value -> float; negative __value -> complex # return type must be Any as `float | complex` causes too many false-positive errors @overload def __pow__(self, value: float, mod: None = None, /) -> Any: ... + def __radd__(self, value: float, /) -> float: ... def __rsub__(self, value: float, /) -> float: ... def __rmul__(self, value: float, /) -> float: ... @@ -394,6 +397,7 @@ class float: def __rtruediv__(self, value: float, /) -> float: ... def __rmod__(self, value: float, /) -> float: ... def __rdivmod__(self, value: float, /) -> tuple[float, float]: ... + @overload def __rpow__(self, value: _PositiveInteger, mod: None = None, /) -> float: ... @overload @@ -401,14 +405,17 @@ class float: # Returning `complex` for the general case gives too many false-positive errors. @overload def __rpow__(self, value: float, mod: None = None, /) -> Any: ... + def __getnewargs__(self) -> tuple[float]: ... def __trunc__(self) -> int: ... def __ceil__(self) -> int: ... def __floor__(self) -> int: ... + @overload def __round__(self, ndigits: None = None, /) -> int: ... @overload def __round__(self, ndigits: SupportsIndex, /) -> float: ... + def __eq__(self, value: object, /) -> bool: ... def __ne__(self, value: object, /) -> bool: ... def __lt__(self, value: float, /) -> bool: ... @@ -438,6 +445,7 @@ class complex: ) -> Self: ... @overload def __new__(cls, real: str | SupportsComplex | SupportsFloat | SupportsIndex | complex) -> Self: ... + @property def real(self) -> float: ... @property @@ -484,14 +492,17 @@ class str(Sequence[str]): def capitalize(self) -> str: ... # type: ignore[misc] def casefold(self) -> str: ... # type: ignore[misc] def center(self, width: SupportsIndex, fillchar: str = " ", /) -> str: ... # type: ignore[misc] + def count(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... def encode(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: ... def endswith( self, suffix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, / ) -> bool: ... def expandtabs(self, tabsize: SupportsIndex = 8) -> str: ... # type: ignore[misc] + def find(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... def format(self, *args: object, **kwargs: object) -> str: ... + def format_map(self, mapping: _FormatMapMapping, /) -> str: ... def index(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... def isalnum(self) -> bool: ... @@ -511,6 +522,7 @@ class str(Sequence[str]): def lower(self) -> str: ... # type: ignore[misc] def lstrip(self, chars: str | None = None, /) -> str: ... # type: ignore[misc] def partition(self, sep: str, /) -> tuple[str, str, str]: ... # type: ignore[misc] + if sys.version_info >= (3, 13): def replace(self, old: str, new: str, /, count: SupportsIndex = -1) -> str: ... # type: ignore[misc] else: @@ -518,6 +530,7 @@ class str(Sequence[str]): def removeprefix(self, prefix: str, /) -> str: ... # type: ignore[misc] def removesuffix(self, suffix: str, /) -> str: ... # type: ignore[misc] + def rfind(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... def rindex(self, sub: str, start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> int: ... def rjust(self, width: SupportsIndex, fillchar: str = " ", /) -> str: ... # type: ignore[misc] @@ -526,18 +539,37 @@ class str(Sequence[str]): def rstrip(self, chars: str | None = None, /) -> str: ... # type: ignore[misc] def split(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: ... # type: ignore[misc] def splitlines(self, keepends: bool = False) -> list[str]: ... # type: ignore[misc] + def startswith( self, prefix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, / ) -> bool: ... def strip(self, chars: str | None = None, /) -> str: ... # type: ignore[misc] def swapcase(self) -> str: ... # type: ignore[misc] def title(self) -> str: ... # type: ignore[misc] + def translate(self, table: _TranslateTable, /) -> str: ... def upper(self) -> str: ... # type: ignore[misc] def zfill(self, width: SupportsIndex, /) -> str: ... # type: ignore[misc] - @staticmethod - @overload - def maketrans(x: dict[int, _T] | dict[str, _T] | dict[str | int, _T], /) -> dict[int, _T]: ... + + if sys.version_info >= (3, 15): + @staticmethod + @overload + def maketrans( + x: ( + dict[int, _T] + | dict[str, _T] + | dict[str | int, _T] + | frozendict[int, _T] + | frozendict[str, _T] + | frozendict[str | int, _T] + ), + /, + ) -> dict[int, _T]: ... + else: + @staticmethod + @overload + def maketrans(x: dict[int, _T] | dict[str, _T] | dict[str | int, _T], /) -> dict[int, _T]: ... + @staticmethod @overload def maketrans(x: str, y: str, /) -> dict[int, int]: ... @@ -545,21 +577,26 @@ class str(Sequence[str]): @overload def maketrans(x: str, y: str, z: str, /) -> dict[int, int | None]: ... def __add__(self, value: str, /) -> str: ... # type: ignore[misc] + # Incompatible with Sequence.__contains__ def __contains__(self, key: str, /) -> bool: ... # type: ignore[override] def __eq__(self, value: object, /) -> bool: ... def __ge__(self, value: str, /) -> bool: ... def __getitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> str: ... # type: ignore[misc] + def __gt__(self, value: str, /) -> bool: ... def __hash__(self) -> int: ... def __iter__(self) -> Iterator[str]: ... # type: ignore[misc] + def __le__(self, value: str, /) -> bool: ... def __len__(self) -> int: ... def __lt__(self, value: str, /) -> bool: ... def __mod__(self, value: Any, /) -> str: ... def __mul__(self, value: SupportsIndex, /) -> str: ... # type: ignore[misc] + def __ne__(self, value: object, /) -> bool: ... def __rmul__(self, value: SupportsIndex, /) -> str: ... # type: ignore[misc] + def __getnewargs__(self) -> tuple[str]: ... def __format__(self, format_spec: str, /) -> str: ... @@ -571,6 +608,7 @@ class bytes(Sequence[int]): def __new__(cls, string: str, /, encoding: str, errors: str = "strict") -> Self: ... @overload def __new__(cls) -> Self: ... + def capitalize(self) -> bytes: ... def center(self, width: SupportsIndex, fillchar: bytes = b" ", /) -> bytes: ... def count( @@ -605,7 +643,11 @@ class bytes(Sequence[int]): def lower(self) -> bytes: ... def lstrip(self, bytes: ReadableBuffer | None = None, /) -> bytes: ... def partition(self, sep: ReadableBuffer, /) -> tuple[bytes, bytes, bytes]: ... - def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytes: ... + if sys.version_info >= (3, 15): + def replace(self, old: ReadableBuffer, new: ReadableBuffer, /, count: SupportsIndex = -1) -> bytes: ... + else: + def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytes: ... + def removeprefix(self, prefix: ReadableBuffer, /) -> bytes: ... def removesuffix(self, suffix: ReadableBuffer, /) -> bytes: ... def rfind( @@ -633,6 +675,7 @@ class bytes(Sequence[int]): def translate(self, table: ReadableBuffer | None, /, delete: ReadableBuffer = b"") -> bytes: ... def upper(self) -> bytes: ... def zfill(self, width: SupportsIndex, /) -> bytes: ... + if sys.version_info >= (3, 14): @classmethod def fromhex(cls, string: str | ReadableBuffer, /) -> Self: ... @@ -645,10 +688,12 @@ class bytes(Sequence[int]): def __len__(self) -> int: ... def __iter__(self) -> Iterator[int]: ... def __hash__(self) -> int: ... + @overload def __getitem__(self, key: SupportsIndex, /) -> int: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytes: ... + def __add__(self, value: ReadableBuffer, /) -> bytes: ... def __mul__(self, value: SupportsIndex, /) -> bytes: ... def __rmul__(self, value: SupportsIndex, /) -> bytes: ... @@ -675,6 +720,7 @@ class bytearray(MutableSequence[int]): def __init__(self, ints: Iterable[SupportsIndex] | SupportsIndex | ReadableBuffer, /) -> None: ... @overload def __init__(self, string: str, /, encoding: str, errors: str = "strict") -> None: ... + def append(self, item: SupportsIndex, /) -> None: ... def capitalize(self) -> bytearray: ... def center(self, width: SupportsIndex, fillchar: bytes = b" ", /) -> bytearray: ... @@ -717,7 +763,11 @@ class bytearray(MutableSequence[int]): def remove(self, value: int, /) -> None: ... def removeprefix(self, prefix: ReadableBuffer, /) -> bytearray: ... def removesuffix(self, suffix: ReadableBuffer, /) -> bytearray: ... - def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytearray: ... + if sys.version_info >= (3, 15): + def replace(self, old: ReadableBuffer, new: ReadableBuffer, /, count: SupportsIndex = -1) -> bytearray: ... + else: + def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytearray: ... + def rfind( self, sub: ReadableBuffer | SupportsIndex, start: SupportsIndex | None = None, end: SupportsIndex | None = None, / ) -> int: ... @@ -741,8 +791,12 @@ class bytearray(MutableSequence[int]): def swapcase(self) -> bytearray: ... def title(self) -> bytearray: ... def translate(self, table: ReadableBuffer | None, /, delete: bytes = b"") -> bytearray: ... + if sys.version_info >= (3, 15): + def take_bytes(self, n: int | None = None, /) -> bytes: ... + def upper(self) -> bytearray: ... def zfill(self, width: SupportsIndex, /) -> bytearray: ... + if sys.version_info >= (3, 14): @classmethod def fromhex(cls, string: str | ReadableBuffer, /) -> Self: ... @@ -755,14 +809,17 @@ class bytearray(MutableSequence[int]): def __len__(self) -> int: ... def __iter__(self) -> Iterator[int]: ... __hash__: ClassVar[None] # type: ignore[assignment] + @overload def __getitem__(self, key: SupportsIndex, /) -> int: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytearray: ... + @overload def __setitem__(self, key: SupportsIndex, value: SupportsIndex, /) -> None: ... @overload def __setitem__(self, key: slice[SupportsIndex | None], value: Iterable[SupportsIndex] | bytes, /) -> None: ... + def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: ... def __add__(self, value: ReadableBuffer, /) -> bytearray: ... # The superclass wants us to accept Iterable[int], but that fails at runtime. @@ -824,6 +881,7 @@ class memoryview(Sequence[_I]): exc_tb: TracebackType | None, /, ) -> None: ... + @overload def cast(self, format: Literal["c", "@c"], shape: list[int] | tuple[int, ...] = ...) -> memoryview[bytes]: ... @overload @@ -832,24 +890,24 @@ class memoryview(Sequence[_I]): def cast(self, format: Literal["?"], shape: list[int] | tuple[int, ...] = ...) -> memoryview[bool]: ... @overload def cast(self, format: _IntegerFormats, shape: list[int] | tuple[int, ...] = ...) -> memoryview: ... + @overload def __getitem__(self, key: SupportsIndex | tuple[SupportsIndex, ...], /) -> _I: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> memoryview[_I]: ... + def __contains__(self, x: object, /) -> bool: ... def __iter__(self) -> Iterator[_I]: ... def __len__(self) -> int: ... def __eq__(self, value: object, /) -> bool: ... def __hash__(self) -> int: ... + @overload def __setitem__(self, key: slice[SupportsIndex | None], value: ReadableBuffer, /) -> None: ... @overload def __setitem__(self, key: SupportsIndex | tuple[SupportsIndex, ...], value: _I, /) -> None: ... - if sys.version_info >= (3, 10): - def tobytes(self, order: Literal["C", "F", "A"] | None = "C") -> bytes: ... - else: - def tobytes(self, order: Literal["C", "F", "A"] | None = None) -> bytes: ... + def tobytes(self, order: Literal["C", "F", "A"] | None = "C") -> bytes: ... def tolist(self) -> list[int]: ... def toreadonly(self) -> memoryview: ... def release(self) -> None: ... @@ -871,32 +929,39 @@ class memoryview(Sequence[_I]): @final class bool(int): def __new__(cls, o: object = False, /) -> Self: ... + # The following overloads could be represented more elegantly with a TypeVar("_B", bool, int), # however mypy has a bug regarding TypeVar constraints (https://github.com/python/mypy/issues/11880). @overload def __and__(self, value: bool, /) -> bool: ... @overload def __and__(self, value: int, /) -> int: ... + @overload def __or__(self, value: bool, /) -> bool: ... @overload def __or__(self, value: int, /) -> int: ... + @overload def __xor__(self, value: bool, /) -> bool: ... @overload def __xor__(self, value: int, /) -> int: ... + @overload def __rand__(self, value: bool, /) -> bool: ... @overload def __rand__(self, value: int, /) -> int: ... + @overload def __ror__(self, value: bool, /) -> bool: ... @overload def __ror__(self, value: int, /) -> int: ... + @overload def __rxor__(self, value: bool, /) -> bool: ... @overload def __rxor__(self, value: int, /) -> int: ... + def __getnewargs__(self) -> tuple[int]: ... @deprecated("Will throw an error in Python 3.16. Use `not` for logical negation of bools instead.") def __invert__(self) -> int: ... @@ -909,6 +974,7 @@ class slice(Generic[_StartT_co, _StopT_co, _StepT_co]): def step(self) -> _StepT_co: ... @property def stop(self) -> _StopT_co: ... + # Note: __new__ overloads map `None` to `Any`, since users expect slice(x, None) # to be compatible with slice(None, x). # generic slice -------------------------------------------------------------------- @@ -933,6 +999,7 @@ class slice(Generic[_StartT_co, _StopT_co, _StepT_co]): def __new__(cls, start: None, stop: _T2, step: _T3, /) -> slice[Any, _T2, _T3]: ... @overload def __new__(cls, start: _T1, stop: _T2, step: _T3, /) -> slice[_T1, _T2, _T3]: ... + def __eq__(self, value: object, /) -> bool: ... if sys.version_info >= (3, 12): def __hash__(self) -> int: ... @@ -940,16 +1007,20 @@ class slice(Generic[_StartT_co, _StopT_co, _StepT_co]): __hash__: ClassVar[None] # type: ignore[assignment] def indices(self, len: SupportsIndex, /) -> tuple[int, int, int]: ... + if sys.version_info >= (3, 15): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @disjoint_base class tuple(Sequence[_T_co]): def __new__(cls, iterable: Iterable[_T_co] = (), /) -> Self: ... def __len__(self) -> int: ... def __contains__(self, key: object, /) -> bool: ... + @overload def __getitem__(self, key: SupportsIndex, /) -> _T_co: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> tuple[_T_co, ...]: ... + def __iter__(self) -> Iterator[_T_co]: ... def __lt__(self, value: tuple[_T_co, ...], /) -> bool: ... def __le__(self, value: tuple[_T_co, ...], /) -> bool: ... @@ -957,10 +1028,12 @@ class tuple(Sequence[_T_co]): def __ge__(self, value: tuple[_T_co, ...], /) -> bool: ... def __eq__(self, value: object, /) -> bool: ... def __hash__(self) -> int: ... + @overload def __add__(self, value: tuple[_T_co, ...], /) -> tuple[_T_co, ...]: ... @overload def __add__(self, value: tuple[_T, ...], /) -> tuple[_T_co | _T, ...]: ... + def __mul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ... def __rmul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ... def count(self, value: Any, /) -> int: ... @@ -988,9 +1061,8 @@ class function: if sys.version_info >= (3, 14): __annotate__: AnnotateFunc | None __kwdefaults__: dict[str, Any] | None - if sys.version_info >= (3, 10): - @property - def __builtins__(self) -> dict[str, Any]: ... + @property + def __builtins__(self) -> dict[str, Any]: ... if sys.version_info >= (3, 12): __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] @@ -1024,6 +1096,7 @@ class list(MutableSequence[_T]): def __init__(self) -> None: ... @overload def __init__(self, iterable: Iterable[_T], /) -> None: ... + def copy(self) -> list[_T]: ... def append(self, object: _T, /) -> None: ... def extend(self, iterable: Iterable[_T], /) -> None: ... @@ -1034,6 +1107,7 @@ class list(MutableSequence[_T]): def count(self, value: _T, /) -> int: ... def insert(self, index: SupportsIndex, object: _T, /) -> None: ... def remove(self, value: _T, /) -> None: ... + # Signature of `list.sort` should be kept inline with `collections.UserList.sort()` # and multiprocessing.managers.ListProxy.sort() # @@ -1043,23 +1117,29 @@ class list(MutableSequence[_T]): def sort(self: list[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ... @overload def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ... + def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T]: ... __hash__: ClassVar[None] # type: ignore[assignment] + @overload def __getitem__(self, i: SupportsIndex, /) -> _T: ... @overload def __getitem__(self, s: slice[SupportsIndex | None], /) -> list[_T]: ... + @overload def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ... @overload def __setitem__(self, key: slice[SupportsIndex | None], value: Iterable[_T], /) -> None: ... + def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: ... + # Overloading looks unnecessary, but is needed to work around complex mypy problems @overload def __add__(self, value: list[_T], /) -> list[_T]: ... @overload def __add__(self, value: list[_S], /) -> list[_S | _T]: ... + def __iadd__(self, value: Iterable[_T], /) -> Self: ... # type: ignore[misc] def __mul__(self, value: SupportsIndex, /) -> list[_T]: ... def __rmul__(self, value: SupportsIndex, /) -> list[_T]: ... @@ -1105,11 +1185,13 @@ class dict(MutableMapping[_KT, _VT]): def __init__(self: dict[str, str], iterable: Iterable[list[str]], /) -> None: ... @overload def __init__(self: dict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ... + def __new__(cls, /, *args: Any, **kwargs: Any) -> Self: ... def copy(self) -> dict[_KT, _VT]: ... def keys(self) -> dict_keys[_KT, _VT]: ... def values(self) -> dict_values[_KT, _VT]: ... def items(self) -> dict_items[_KT, _VT]: ... + # Signature of `dict.fromkeys` should be kept identical to # `fromkeys` methods of `OrderedDict`/`ChainMap`/`UserDict` in `collections` # TODO: the true signature of `dict.fromkeys` is not expressible in the current type system. @@ -1120,6 +1202,7 @@ class dict(MutableMapping[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ... + # Positional-only in dict, but not in MutableMapping @overload # type: ignore[override] def get(self, key: _KT, default: None = None, /) -> _VT | None: ... @@ -1127,12 +1210,14 @@ class dict(MutableMapping[_KT, _VT]): def get(self, key: _KT, default: _VT, /) -> _VT: ... @overload def get(self, key: _KT, default: _T, /) -> _VT | _T: ... + @overload def pop(self, key: _KT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _VT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... + def __len__(self) -> int: ... def __getitem__(self, key: _KT, /) -> _VT: ... def __setitem__(self, key: _KT, value: _VT, /) -> None: ... @@ -1142,26 +1227,103 @@ class dict(MutableMapping[_KT, _VT]): def __reversed__(self) -> Iterator[_KT]: ... __hash__: ClassVar[None] # type: ignore[assignment] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - @overload - def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... - @overload - def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... - @overload - def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + if sys.version_info >= (3, 15): + @overload + def __or__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __or__(self, value: dict[_T1, _T2] | frozendict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: frozendict[_KT, _VT], /) -> frozendict[_KT, _VT]: ... + @overload + def __ror__(self, value: frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + else: + @overload + def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + # dict.__ior__ should be kept roughly in line with MutableMapping.update() @overload # type: ignore[misc] def __ior__(self, value: SupportsKeysAndGetItem[_KT, _VT], /) -> Self: ... @overload def __ior__(self, value: Iterable[tuple[_KT, _VT]], /) -> Self: ... +if sys.version_info >= (3, 15): + @disjoint_base + class frozendict(Mapping[_KT, _VT]): + @overload + def __new__(cls, /) -> frozendict[Any, Any]: ... + @overload + def __new__(cls: type[frozendict[str, _VT]], /, **kwargs: _VT) -> frozendict[str, _VT]: ... + @overload + def __new__(cls, map: SupportsKeysAndGetItem[_KT, _VT], /) -> frozendict[_KT, _VT]: ... + @overload + def __new__( + cls: type[frozendict[str, _VT]], map: SupportsKeysAndGetItem[str, _VT], /, **kwargs: _VT + ) -> frozendict[str, _VT]: ... + @overload + def __new__(cls, iterable: Iterable[tuple[_KT, _VT]], /) -> frozendict[_KT, _VT]: ... + @overload + def __new__( + cls: type[frozendict[str, _VT]], iterable: Iterable[tuple[str, _VT]], /, **kwargs: _VT + ) -> frozendict[str, _VT]: ... + + def __init__(self) -> None: ... + def copy(self) -> frozendict[_KT, _VT]: ... + + @overload + @classmethod + def fromkeys(cls, iterable: Iterable[_T], value: None = None, /) -> frozendict[_T, Any | None]: ... + @overload + @classmethod + def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> frozendict[_T, _S]: ... + + @overload # type: ignore[override] + def get(self, key: _KT, default: None = None, /) -> _VT | None: ... + @overload + def get(self, key: _KT, default: _VT, /) -> _VT: ... + @overload + def get(self, key: _KT, default: _T, /) -> _VT | _T: ... + + def keys(self) -> dict_keys[_KT, _VT]: ... + def values(self) -> dict_values[_KT, _VT]: ... + def items(self) -> dict_items[_KT, _VT]: ... + def __len__(self) -> int: ... + def __getitem__(self, key: _KT, /) -> _VT: ... + def __reversed__(self) -> Iterator[_KT]: ... + def __iter__(self) -> Iterator[_KT]: ... + def __hash__(self) -> int: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + @overload + def __or__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> frozendict[_KT, _VT]: ... + @overload + def __or__(self, value: dict[_T1, _T2] | frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: frozendict[_KT, _VT], /) -> frozendict[_KT, _VT]: ... + @overload + def __ror__(self, value: frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + @disjoint_base class set(MutableSet[_T]): @overload def __init__(self) -> None: ... @overload def __init__(self, iterable: Iterable[_T], /) -> None: ... + def add(self, element: _T, /) -> None: ... def copy(self) -> set[_T]: ... def difference(self, *s: Iterable[object]) -> set[_T]: ... @@ -1202,6 +1364,7 @@ class frozenset(AbstractSet[_T_co]): def __new__(cls) -> Self: ... @overload def __new__(cls, iterable: Iterable[_T_co], /) -> Self: ... + def copy(self) -> frozenset[_T_co]: ... def difference(self, *s: Iterable[object]) -> frozenset[_T_co]: ... def intersection(self, *s: Iterable[object]) -> frozenset[_T_co]: ... @@ -1240,10 +1403,12 @@ class range(Sequence[int]): def stop(self) -> int: ... @property def step(self) -> int: ... + @overload def __new__(cls, stop: SupportsIndex, /) -> Self: ... @overload def __new__(cls, start: SupportsIndex, stop: SupportsIndex, step: SupportsIndex = 1, /) -> Self: ... + def count(self, value: int, /) -> int: ... def index(self, value: int, /) -> int: ... # type: ignore[override] def __len__(self) -> int: ... @@ -1251,10 +1416,12 @@ class range(Sequence[int]): def __hash__(self) -> int: ... def __contains__(self, key: object, /) -> bool: ... def __iter__(self) -> Iterator[int]: ... + @overload def __getitem__(self, key: SupportsIndex, /) -> int: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> range: ... + def __reversed__(self) -> Iterator[int]: ... @disjoint_base @@ -1276,10 +1443,12 @@ class property: def getter(self, fget: Callable[[Any], Any], /) -> property: ... def setter(self, fset: Callable[[Any, Any], None], /) -> property: ... def deleter(self, fdel: Callable[[Any], None], /) -> property: ... + @overload def __get__(self, instance: None, owner: type, /) -> Self: ... @overload def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... + def __set__(self, instance: Any, value: Any, /) -> None: ... def __delete__(self, instance: Any, /) -> None: ... @@ -1287,77 +1456,132 @@ def abs(x: SupportsAbs[_T], /) -> _T: ... def all(iterable: Iterable[object], /) -> bool: ... def any(iterable: Iterable[object], /) -> bool: ... def ascii(obj: object, /) -> str: ... -def bin(number: SupportsIndex, /) -> str: ... + +if sys.version_info >= (3, 15): + def bin(integer: SupportsIndex, /) -> str: ... + +else: + def bin(number: SupportsIndex, /) -> str: ... + def breakpoint(*args: Any, **kws: Any) -> None: ... def callable(obj: object, /) -> TypeIs[Callable[..., object]]: ... def chr(i: SupportsIndex, /) -> str: ... +def aiter(async_iterable: SupportsAiter[_SupportsAnextT_co], /) -> _SupportsAnextT_co: ... -if sys.version_info >= (3, 10): - def aiter(async_iterable: SupportsAiter[_SupportsAnextT_co], /) -> _SupportsAnextT_co: ... - @type_check_only - class _SupportsSynchronousAnext(Protocol[_AwaitableT_co]): - def __anext__(self) -> _AwaitableT_co: ... +@type_check_only +class _SupportsSynchronousAnext(Protocol[_AwaitableT_co]): + def __anext__(self) -> _AwaitableT_co: ... - @overload - # `anext` is not, in fact, an async function. When default is not provided - # `anext` is just a passthrough for `obj.__anext__` - # See discussion in #7491 and pure-Python implementation of `anext` at https://github.com/python/cpython/blob/ea786a882b9ed4261eafabad6011bc7ef3b5bf94/Lib/test/test_asyncgen.py#L52-L80 - def anext(i: _SupportsSynchronousAnext[_AwaitableT], /) -> _AwaitableT: ... - @overload - async def anext(i: SupportsAnext[_T], default: _VT, /) -> _T | _VT: ... +@overload +# `anext` is not, in fact, an async function. When default is not provided +# `anext` is just a passthrough for `obj.__anext__` +# See discussion in #7491 and pure-Python implementation of `anext` at https://github.com/python/cpython/blob/ea786a882b9ed4261eafabad6011bc7ef3b5bf94/Lib/test/test_asyncgen.py#L52-L80 +def anext(i: _SupportsSynchronousAnext[_AwaitableT], /) -> _AwaitableT: ... +@overload +async def anext(i: SupportsAnext[_T], default: _VT, /) -> _T | _VT: ... # compile() returns a CodeType, unless the flags argument includes PyCF_ONLY_AST (=1024), # in which case it returns ast.AST. We have overloads for flag 0 (the default) and for # explicitly passing PyCF_ONLY_AST. We fall back to Any for other values of flags. -@overload -def compile( - source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | bytes | PathLike[Any], - mode: str, - flags: Literal[0], - dont_inherit: bool = False, - optimize: int = -1, - *, - _feature_version: int = -1, -) -> CodeType: ... -@overload -def compile( - source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | bytes | PathLike[Any], - mode: str, - *, - dont_inherit: bool = False, - optimize: int = -1, - _feature_version: int = -1, -) -> CodeType: ... -@overload -def compile( - source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | bytes | PathLike[Any], - mode: str, - flags: Literal[1024], - dont_inherit: bool = False, - optimize: int = -1, - *, - _feature_version: int = -1, -) -> _ast.AST: ... -@overload -def compile( - source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | bytes | PathLike[Any], - mode: str, - flags: int, - dont_inherit: bool = False, - optimize: int = -1, - *, - _feature_version: int = -1, -) -> Any: ... +if sys.version_info >= (3, 15): + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + flags: Literal[0], + dont_inherit: bool = False, + optimize: int = -1, + *, + module: str | None = None, + _feature_version: int = -1, + ) -> CodeType: ... + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + *, + dont_inherit: bool = False, + optimize: int = -1, + module: str | None = None, + _feature_version: int = -1, + ) -> CodeType: ... + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + flags: Literal[1024], + dont_inherit: bool = False, + optimize: int = -1, + *, + module: str | None = None, + _feature_version: int = -1, + ) -> _ast.AST: ... + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + flags: int, + dont_inherit: bool = False, + optimize: int = -1, + *, + module: str | None = None, + _feature_version: int = -1, + ) -> Any: ... +else: + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + flags: Literal[0], + dont_inherit: bool = False, + optimize: int = -1, + *, + _feature_version: int = -1, + ) -> CodeType: ... + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + *, + dont_inherit: bool = False, + optimize: int = -1, + _feature_version: int = -1, + ) -> CodeType: ... + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + flags: Literal[1024], + dont_inherit: bool = False, + optimize: int = -1, + *, + _feature_version: int = -1, + ) -> _ast.AST: ... + @overload + def compile( + source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, + filename: str | bytes | PathLike[Any], + mode: str, + flags: int, + dont_inherit: bool = False, + optimize: int = -1, + *, + _feature_version: int = -1, + ) -> Any: ... copyright: _sitebuiltins._Printer credits: _sitebuiltins._Printer def delattr(obj: object, name: str, /) -> None: ... def dir(o: object = ..., /) -> list[str]: ... + @overload def divmod(x: SupportsDivMod[_T_contra, _T_co], y: _T_contra, /) -> _T_co: ... @overload @@ -1365,7 +1589,15 @@ def divmod(x: _T_contra, y: SupportsRDivMod[_T_contra, _T_co], /) -> _T_co: ... # The `globals` argument to `eval` has to be `dict[str, Any]` rather than `dict[str, object]` due to invariance. # (The `globals` argument has to be a "real dict", rather than any old mapping, unlike the `locals` argument.) -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 15): + def eval( + source: str | ReadableBuffer | CodeType, + /, + globals: dict[str, Any] | frozendict[str, Any] | None = None, + locals: Mapping[str, object] | None = None, + ) -> Any: ... + +elif sys.version_info >= (3, 13): def eval( source: str | ReadableBuffer | CodeType, /, @@ -1382,7 +1614,17 @@ else: ) -> Any: ... # Comment above regarding `eval` applies to `exec` as well -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 15): + def exec( + source: str | ReadableBuffer | CodeType, + /, + globals: dict[str, Any] | frozendict[str, Any] | None = None, + locals: Mapping[str, object] | None = None, + *, + closure: tuple[CellType, ...] | None = None, + ) -> None: ... + +elif sys.version_info >= (3, 13): def exec( source: str | ReadableBuffer | CodeType, /, @@ -1422,10 +1664,12 @@ class filter(Iterator[_T]): def __new__(cls, function: Callable[[_S], TypeIs[_T]], iterable: Iterable[_S], /) -> Self: ... @overload def __new__(cls, function: Callable[[_T], Any], iterable: Iterable[_T], /) -> Self: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T: ... def format(value: object, format_spec: str = "", /) -> str: ... + @overload def getattr(o: object, name: str, /) -> Any: ... @@ -1442,15 +1686,22 @@ def getattr(o: object, name: str, default: list[Any], /) -> Any | list[Any]: ... def getattr(o: object, name: str, default: dict[Any, Any], /) -> Any | dict[Any, Any]: ... @overload def getattr(o: object, name: str, default: _T, /) -> Any | _T: ... + def globals() -> dict[str, Any]: ... def hasattr(obj: object, name: str, /) -> bool: ... def hash(obj: object, /) -> int: ... help: _sitebuiltins._Helper -def hex(number: SupportsIndex, /) -> str: ... +if sys.version_info >= (3, 15): + def hex(integer: SupportsIndex, /) -> str: ... + +else: + def hex(number: SupportsIndex, /) -> str: ... + def id(obj: object, /) -> int: ... def input(prompt: object = "", /) -> str: ... + @type_check_only class _GetItemIterable(Protocol[_T_co]): def __getitem__(self, i: int, /) -> _T_co: ... @@ -1464,10 +1715,7 @@ def iter(object: Callable[[], _T | None], sentinel: None, /) -> Iterator[_T]: .. @overload def iter(object: Callable[[], _T], sentinel: object, /) -> Iterator[_T]: ... -if sys.version_info >= (3, 10): - _ClassInfo: TypeAlias = type | types.UnionType | tuple[_ClassInfo, ...] -else: - _ClassInfo: TypeAlias = type | tuple[_ClassInfo, ...] +_ClassInfo: TypeAlias = type | types.UnionType | tuple[_ClassInfo, ...] def isinstance(obj: object, class_or_tuple: _ClassInfo, /) -> bool: ... def issubclass(cls: type, class_or_tuple: _ClassInfo, /) -> bool: ... @@ -1476,6 +1724,7 @@ def len(obj: Sized, /) -> int: ... license: _sitebuiltins._Printer def locals() -> dict[str, Any]: ... + @disjoint_base class map(Iterator[_S]): # 3.14 adds `strict` argument. @@ -1597,6 +1846,7 @@ def max(iterable: Iterable[_T], /, *, key: Callable[[_T], SupportsRichComparison def max(iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T) -> SupportsRichComparisonT | _T: ... @overload def max(iterable: Iterable[_T1], /, *, key: Callable[[_T1], SupportsRichComparison], default: _T2) -> _T1 | _T2: ... + @overload def min( arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, key: None = None @@ -1611,11 +1861,17 @@ def min(iterable: Iterable[_T], /, *, key: Callable[[_T], SupportsRichComparison def min(iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T) -> SupportsRichComparisonT | _T: ... @overload def min(iterable: Iterable[_T1], /, *, key: Callable[[_T1], SupportsRichComparison], default: _T2) -> _T1 | _T2: ... + @overload def next(i: SupportsNext[_T], /) -> _T: ... @overload def next(i: SupportsNext[_T], default: _VT, /) -> _T | _VT: ... -def oct(number: SupportsIndex, /) -> str: ... + +if sys.version_info >= (3, 15): + def oct(integer: SupportsIndex, /) -> str: ... + +else: + def oct(number: SupportsIndex, /) -> str: ... _Opener: TypeAlias = Callable[[str, int], int] @@ -1705,7 +1961,9 @@ def open( closefd: bool = True, opener: _Opener | None = None, ) -> IO[Any]: ... + def ord(c: str | bytes | bytearray, /) -> int: ... + @type_check_only class _SupportsWriteAndFlush(SupportsWrite[_T_contra], SupportsFlush, Protocol[_T_contra]): ... @@ -1789,6 +2047,7 @@ class reversed(Iterator[_T]): def __new__(cls, sequence: Reversible[_T], /) -> Iterator[_T]: ... # type: ignore[misc] @overload def __new__(cls, sequence: SupportsLenAndGetItem[_T], /) -> Iterator[_T]: ... # type: ignore[misc] + def __iter__(self) -> Self: ... def __next__(self) -> _T: ... def __length_hint__(self) -> int: ... @@ -1815,6 +2074,18 @@ def round(number: _SupportsRound2[_T], ndigits: SupportsIndex) -> _T: ... # See https://github.com/python/typeshed/pull/6292#discussion_r748875189 # for why arg 3 of `setattr` should be annotated with `Any` and not `object` def setattr(obj: object, name: str, value: Any, /) -> None: ... + +if sys.version_info >= (3, 15): + @final + class sentinel: + __name__: str + __module__: str + def __new__(cls, name: str, /, *, repr: str | None = None) -> Self: ... + def __copy__(self, /) -> Self: ... + def __deepcopy__(self, memo: Any, /) -> Self: ... + def __or__(self, other: Any, /) -> Any: ... + def __ror__(self, other: Any, /) -> Any: ... + @overload def sorted( iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, reverse: bool = False @@ -1847,84 +2118,48 @@ def sum(iterable: Iterable[_AddableT1], /, start: _AddableT2) -> _AddableT1 | _A def vars(object: type, /) -> types.MappingProxyType[str, Any]: ... @overload def vars(object: Any = ..., /) -> dict[str, Any]: ... + @disjoint_base class zip(Iterator[_T_co]): - if sys.version_info >= (3, 10): - @overload - def __new__(cls, *, strict: bool = False) -> zip[Any]: ... - @overload - def __new__(cls, iter1: Iterable[_T1], /, *, strict: bool = False) -> zip[tuple[_T1]]: ... - @overload - def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /, *, strict: bool = False) -> zip[tuple[_T1, _T2]]: ... - @overload - def __new__( - cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /, *, strict: bool = False - ) -> zip[tuple[_T1, _T2, _T3]]: ... - @overload - def __new__( - cls, - iter1: Iterable[_T1], - iter2: Iterable[_T2], - iter3: Iterable[_T3], - iter4: Iterable[_T4], - /, - *, - strict: bool = False, - ) -> zip[tuple[_T1, _T2, _T3, _T4]]: ... - @overload - def __new__( - cls, - iter1: Iterable[_T1], - iter2: Iterable[_T2], - iter3: Iterable[_T3], - iter4: Iterable[_T4], - iter5: Iterable[_T5], - /, - *, - strict: bool = False, - ) -> zip[tuple[_T1, _T2, _T3, _T4, _T5]]: ... - @overload - def __new__( - cls, - iter1: Iterable[Any], - iter2: Iterable[Any], - iter3: Iterable[Any], - iter4: Iterable[Any], - iter5: Iterable[Any], - iter6: Iterable[Any], - /, - *iterables: Iterable[Any], - strict: bool = False, - ) -> zip[tuple[Any, ...]]: ... - else: - @overload - def __new__(cls) -> zip[Any]: ... - @overload - def __new__(cls, iter1: Iterable[_T1], /) -> zip[tuple[_T1]]: ... - @overload - def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /) -> zip[tuple[_T1, _T2]]: ... - @overload - def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /) -> zip[tuple[_T1, _T2, _T3]]: ... - @overload - def __new__( - cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], / - ) -> zip[tuple[_T1, _T2, _T3, _T4]]: ... - @overload - def __new__( - cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], / - ) -> zip[tuple[_T1, _T2, _T3, _T4, _T5]]: ... - @overload - def __new__( - cls, - iter1: Iterable[Any], - iter2: Iterable[Any], - iter3: Iterable[Any], - iter4: Iterable[Any], - iter5: Iterable[Any], - iter6: Iterable[Any], - /, - *iterables: Iterable[Any], - ) -> zip[tuple[Any, ...]]: ... + @overload + def __new__(cls, *, strict: bool = False) -> zip[Any]: ... + @overload + def __new__(cls, iter1: Iterable[_T1], /, *, strict: bool = False) -> zip[tuple[_T1]]: ... + @overload + def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /, *, strict: bool = False) -> zip[tuple[_T1, _T2]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /, *, strict: bool = False + ) -> zip[tuple[_T1, _T2, _T3]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /, *, strict: bool = False + ) -> zip[tuple[_T1, _T2, _T3, _T4]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + /, + *, + strict: bool = False, + ) -> zip[tuple[_T1, _T2, _T3, _T4, _T5]]: ... + @overload + def __new__( + cls, + iter1: Iterable[Any], + iter2: Iterable[Any], + iter3: Iterable[Any], + iter4: Iterable[Any], + iter5: Iterable[Any], + iter6: Iterable[Any], + /, + *iterables: Iterable[Any], + strict: bool = False, + ) -> zip[tuple[Any, ...]]: ... def __iter__(self) -> Self: ... def __next__(self) -> _T_co: ... @@ -1938,31 +2173,24 @@ def __import__( fromlist: Sequence[str] | None = (), level: int = 0, ) -> types.ModuleType: ... -def __build_class__(func: Callable[[], CellType | Any], name: str, /, *bases: Any, metaclass: Any = ..., **kwds: Any) -> Any: ... -if sys.version_info >= (3, 10): - from types import EllipsisType, NotImplementedType - - # Backwards compatibility hack for folks who relied on the ellipsis type - # existing in typeshed in Python 3.9 and earlier. - ellipsis = EllipsisType - - Ellipsis: EllipsisType - NotImplemented: NotImplementedType -else: - # Actually the type of Ellipsis is , but since it's - # not exposed anywhere under that name, we make it private here. - @final - @type_check_only - class ellipsis: ... +if sys.version_info >= (3, 15): + def __lazy_import__( + name: str, + globals: Mapping[str, object] | None = None, + locals: Mapping[str, object] | None = None, + fromlist: Sequence[str] | None = (), + level: int = 0, + ) -> Any: ... - Ellipsis: ellipsis +def __build_class__(func: Callable[[], CellType | Any], name: str, /, *bases: Any, metaclass: Any = ..., **kwds: Any) -> Any: ... - @final - @type_check_only - class _NotImplementedType(Any): ... +# Backwards compatibility hack for folks who relied on the ellipsis type +# existing in typeshed in Python 3.9 and earlier. +ellipsis = EllipsisType - NotImplemented: _NotImplementedType +Ellipsis: EllipsisType +NotImplemented: NotImplementedType @disjoint_base class BaseException: @@ -2015,15 +2243,11 @@ if sys.platform == "win32": class ArithmeticError(Exception): ... class AssertionError(Exception): ... -if sys.version_info >= (3, 10): - @disjoint_base - class AttributeError(Exception): - def __init__(self, *args: object, name: str | None = None, obj: object = None) -> None: ... - name: str | None - obj: object - -else: - class AttributeError(Exception): ... +@disjoint_base +class AttributeError(Exception): + def __init__(self, *args: object, name: str | None = None, obj: object = None) -> None: ... + name: str | None + obj: object class BufferError(Exception): ... class EOFError(Exception): ... @@ -2037,17 +2261,16 @@ class ImportError(Exception): if sys.version_info >= (3, 12): name_from: str | None # undocumented +if sys.version_info >= (3, 15): + class ImportCycleError(ImportError): ... + class LookupError(Exception): ... class MemoryError(Exception): ... -if sys.version_info >= (3, 10): - @disjoint_base - class NameError(Exception): - def __init__(self, *args: object, name: str | None = None) -> None: ... - name: str | None - -else: - class NameError(Exception): ... +@disjoint_base +class NameError(Exception): + def __init__(self, *args: object, name: str | None = None) -> None: ... + name: str | None class ReferenceError(Exception): ... class RuntimeError(Exception): ... @@ -2063,9 +2286,8 @@ class SyntaxError(Exception): # Errors are displayed differently if this attribute exists on the exception. # The value is always None. print_file_and_line: None - if sys.version_info >= (3, 10): - end_lineno: int | None - end_offset: int | None + end_lineno: int | None + end_offset: int | None @overload def __init__(self) -> None: ... @@ -2074,12 +2296,11 @@ class SyntaxError(Exception): # Second argument is the tuple (filename, lineno, offset, text) @overload def __init__(self, msg: str, info: tuple[str | None, int | None, int | None, str | None], /) -> None: ... - if sys.version_info >= (3, 10): - # end_lineno and end_offset must both be provided if one is. - @overload - def __init__( - self, msg: str, info: tuple[str | None, int | None, int | None, str | None, int | None, int | None], / - ) -> None: ... + # end_lineno and end_offset must both be provided if one is. + @overload + def __init__( + self, msg: str, info: tuple[str | None, int | None, int | None, str | None, int | None, int | None], / + ) -> None: ... # If you provide more than two arguments, it still creates the SyntaxError, but # the arguments from the info tuple are not parsed. This form is omitted. @@ -2155,9 +2376,7 @@ class ImportWarning(Warning): ... class UnicodeWarning(Warning): ... class BytesWarning(Warning): ... class ResourceWarning(Warning): ... - -if sys.version_info >= (3, 10): - class EncodingWarning(Warning): ... +class EncodingWarning(Warning): ... if sys.version_info >= (3, 11): _BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True, default=BaseException) @@ -2174,6 +2393,7 @@ if sys.version_info >= (3, 11): def message(self) -> str: ... @property def exceptions(self) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: ... + @overload def subgroup( self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / @@ -2186,6 +2406,7 @@ if sys.version_info >= (3, 11): def subgroup( self, matcher_value: Callable[[_BaseExceptionT_co | Self], bool], / ) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ... + @overload def split( self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / @@ -2198,11 +2419,13 @@ if sys.version_info >= (3, 11): def split( self, matcher_value: Callable[[_BaseExceptionT_co | Self], bool], / ) -> tuple[BaseExceptionGroup[_BaseExceptionT_co] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ... + # In reality it is `NonEmptySequence`: @overload def derive(self, excs: Sequence[_ExceptionT], /) -> ExceptionGroup[_ExceptionT]: ... @overload def derive(self, excs: Sequence[_BaseExceptionT], /) -> BaseExceptionGroup[_BaseExceptionT]: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception): @@ -2210,6 +2433,7 @@ if sys.version_info >= (3, 11): def __init__(self, message: str, exceptions: Sequence[_ExceptionT_co], /) -> None: ... @property def exceptions(self) -> tuple[_ExceptionT_co | ExceptionGroup[_ExceptionT_co], ...]: ... + # We accept a narrower type, but that's OK. @overload # type: ignore[override] def subgroup( @@ -2219,6 +2443,7 @@ if sys.version_info >= (3, 11): def subgroup( self, matcher_value: Callable[[_ExceptionT_co | Self], bool], / ) -> ExceptionGroup[_ExceptionT_co] | None: ... + @overload # type: ignore[override] def split( self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / diff --git a/mypy/typeshed/stdlib/bz2.pyi b/mypy/typeshed/stdlib/bz2.pyi index 7bd829d040cb8..fec6b30af2f5a 100644 --- a/mypy/typeshed/stdlib/bz2.pyi +++ b/mypy/typeshed/stdlib/bz2.pyi @@ -3,8 +3,8 @@ from _bz2 import BZ2Compressor as BZ2Compressor, BZ2Decompressor as BZ2Decompres from _typeshed import ReadableBuffer, StrOrBytesPath, WriteableBuffer from collections.abc import Iterable from io import TextIOWrapper -from typing import IO, Literal, Protocol, SupportsIndex, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import IO, Literal, Protocol, SupportsIndex, TypeAlias, overload, type_check_only +from typing_extensions import Self if sys.version_info >= (3, 14): from compression._common._streams import BaseStream, _Reader @@ -100,6 +100,7 @@ def open( class BZ2File(BaseStream, IO[bytes]): def __enter__(self) -> Self: ... + @overload def __init__(self, filename: _WritableFileobj, mode: _WriteBinaryMode, *, compresslevel: int = 9) -> None: ... @overload @@ -108,6 +109,7 @@ class BZ2File(BaseStream, IO[bytes]): def __init__( self, filename: StrOrBytesPath, mode: _ReadBinaryMode | _WriteBinaryMode = "r", *, compresslevel: int = 9 ) -> None: ... + def read(self, size: int | None = -1) -> bytes: ... def read1(self, size: int = -1) -> bytes: ... def readline(self, size: SupportsIndex = -1) -> bytes: ... # type: ignore[override] diff --git a/mypy/typeshed/stdlib/cProfile.pyi b/mypy/typeshed/stdlib/cProfile.pyi index e921584d43905..0e414206f5156 100644 --- a/mypy/typeshed/stdlib/cProfile.pyi +++ b/mypy/typeshed/stdlib/cProfile.pyi @@ -1,9 +1,10 @@ import _lsprof +import sys from _typeshed import StrOrBytesPath, Unused from collections.abc import Callable, Mapping from types import CodeType -from typing import Any, TypeVar -from typing_extensions import ParamSpec, Self, TypeAlias +from typing import Any, ParamSpec, TypeAlias, TypeVar +from typing_extensions import Self __all__ = ["run", "runctx", "Profile"] @@ -28,4 +29,5 @@ class Profile(_lsprof.Profiler): def __enter__(self) -> Self: ... def __exit__(self, *exc_info: Unused) -> None: ... -def label(code: str | CodeType) -> _Label: ... # undocumented +if sys.version_info < (3, 15): + def label(code: str | CodeType) -> _Label: ... # undocumented diff --git a/mypy/typeshed/stdlib/calendar.pyi b/mypy/typeshed/stdlib/calendar.pyi index 0d3a0a7490a33..63ec715fb51be 100644 --- a/mypy/typeshed/stdlib/calendar.pyi +++ b/mypy/typeshed/stdlib/calendar.pyi @@ -2,12 +2,18 @@ import datetime import enum import sys from _typeshed import Unused -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Iterator from time import struct_time -from typing import ClassVar, Final -from typing_extensions import TypeAlias +from typing import ClassVar, Final, TypeAlias, overload __all__ = [ + "FRIDAY", + "MONDAY", + "SATURDAY", + "SUNDAY", + "THURSDAY", + "TUESDAY", + "WEDNESDAY", "IllegalMonthError", "IllegalWeekdayError", "setfirstweekday", @@ -34,8 +40,6 @@ __all__ = [ "weekheader", ] -if sys.version_info >= (3, 10): - __all__ += ["FRIDAY", "MONDAY", "SATURDAY", "SUNDAY", "THURSDAY", "TUESDAY", "WEDNESDAY"] if sys.version_info >= (3, 12): __all__ += [ "Day", @@ -53,6 +57,8 @@ if sys.version_info >= (3, 12): "NOVEMBER", "DECEMBER", ] +if sys.version_info >= (3, 15): + __all__ += ["standalone_month_name", "standalone_month_abbr"] _LocaleType: TypeAlias = tuple[str | None, str | None] @@ -88,9 +94,9 @@ class Calendar: def itermonthdays4(self, year: int, month: int) -> Iterable[tuple[int, int, int, int]]: ... class TextCalendar(Calendar): - def prweek(self, theweek: int, width: int) -> None: ... + def prweek(self, theweek: Iterable[tuple[int, int]], width: int) -> None: ... def formatday(self, day: int, weekday: int, width: int) -> str: ... - def formatweek(self, theweek: int, width: int) -> str: ... + def formatweek(self, theweek: Iterable[tuple[int, int]], width: int) -> str: ... def formatweekday(self, day: int, width: int) -> str: ... def formatweekheader(self, width: int) -> str: ... def formatmonthname(self, theyear: int, themonth: int, width: int, withyear: bool = True) -> str: ... @@ -123,6 +129,11 @@ class HTMLCalendar(Calendar): def formatweekheader(self) -> str: ... def formatmonthname(self, theyear: int, themonth: int, withyear: bool = True) -> str: ... def formatmonth(self, theyear: int, themonth: int, withyear: bool = True) -> str: ... + if sys.version_info >= (3, 15): + def formatmonthpage( + self, theyear: int, themonth: int, width: int = 3, css: str | None = "calendar.css", encoding: str | None = None + ) -> bytes: ... + def formatyear(self, theyear: int, width: int = 3) -> str: ... def formatyearpage( self, theyear: int, width: int = 3, css: str | None = "calendar.css", encoding: str | None = None @@ -145,14 +156,38 @@ c: TextCalendar def setfirstweekday(firstweekday: int) -> None: ... def format(cols: int, colwidth: int = 20, spacing: int = 6) -> str: ... -def formatstring(cols: int, colwidth: int = 20, spacing: int = 6) -> str: ... +def formatstring(cols: Iterable[str], colwidth: int = 20, spacing: int = 6) -> str: ... def timegm(tuple: tuple[int, ...] | struct_time) -> int: ... # Data attributes -day_name: Sequence[str] -day_abbr: Sequence[str] -month_name: Sequence[str] -month_abbr: Sequence[str] +class _localized_month: + format: str + def __init__(self, format: str) -> None: ... + + @overload + def __getitem__(self, i: int) -> str: ... + @overload + def __getitem__(self, i: slice) -> list[str]: ... + + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... + +class _localized_day: + format: str + def __init__(self, format: str) -> None: ... + + @overload + def __getitem__(self, i: int) -> str: ... + @overload + def __getitem__(self, i: slice) -> list[str]: ... + + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... + +day_name: _localized_day +day_abbr: _localized_day +month_name: _localized_month +month_abbr: _localized_month if sys.version_info >= (3, 12): class Month(enum.IntEnum): @@ -208,3 +243,7 @@ else: SUNDAY: Final = 6 EPOCH: Final = 1970 + +if sys.version_info >= (3, 15): + standalone_month_name: _localized_month + standalone_month_abbr: _localized_month diff --git a/mypy/typeshed/stdlib/cgi.pyi b/mypy/typeshed/stdlib/cgi.pyi index 0f9d4343b6307..b7f88ded315f4 100644 --- a/mypy/typeshed/stdlib/cgi.pyi +++ b/mypy/typeshed/stdlib/cgi.pyi @@ -32,6 +32,7 @@ def parse( def parse_multipart( fp: IO[Any], pdict: SupportsGetItem[str, bytes], encoding: str = "utf-8", errors: str = "replace", separator: str = "&" ) -> dict[str, list[Any]]: ... + @type_check_only class _Environ(Protocol): def __getitem__(self, k: str, /) -> str: ... diff --git a/mypy/typeshed/stdlib/cmath.pyi b/mypy/typeshed/stdlib/cmath.pyi index fdf8ae7bfed8d..554eb54e2e4cc 100644 --- a/mypy/typeshed/stdlib/cmath.pyi +++ b/mypy/typeshed/stdlib/cmath.pyi @@ -1,5 +1,4 @@ -from typing import Final, SupportsComplex, SupportsFloat, SupportsIndex -from typing_extensions import TypeAlias +from typing import Final, SupportsComplex, SupportsFloat, SupportsIndex, TypeAlias e: Final[float] pi: Final[float] diff --git a/mypy/typeshed/stdlib/codecs.pyi b/mypy/typeshed/stdlib/codecs.pyi index 9164a4a626d4f..b341d0f813c41 100644 --- a/mypy/typeshed/stdlib/codecs.pyi +++ b/mypy/typeshed/stdlib/codecs.pyi @@ -4,8 +4,8 @@ from _codecs import * from _typeshed import ReadableBuffer from abc import abstractmethod from collections.abc import Callable, Generator, Iterable -from typing import Any, BinaryIO, ClassVar, Final, Literal, Protocol, TextIO, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated, disjoint_base +from typing import Any, BinaryIO, ClassVar, Final, Literal, Protocol, TextIO, TypeAlias, overload, type_check_only +from typing_extensions import Self, deprecated, disjoint_base __all__ = [ "register", @@ -185,10 +185,12 @@ else: def getencoder(encoding: str) -> _Encoder: ... def getdecoder(encoding: str) -> _Decoder: ... def getincrementalencoder(encoding: str) -> _IncrementalEncoder: ... + @overload def getincrementaldecoder(encoding: _BufferedEncoding) -> _BufferedIncrementalDecoder: ... @overload def getincrementaldecoder(encoding: str) -> _IncrementalDecoder: ... + def getreader(encoding: str) -> _StreamReader: ... def getwriter(encoding: str) -> _StreamWriter: ... @deprecated("Deprecated since Python 3.14. Use `open()` instead.") diff --git a/mypy/typeshed/stdlib/collections/__init__.pyi b/mypy/typeshed/stdlib/collections/__init__.pyi index 95f13b0c8dd2b..d9c2e342c5871 100644 --- a/mypy/typeshed/stdlib/collections/__init__.pyi +++ b/mypy/typeshed/stdlib/collections/__init__.pyi @@ -1,25 +1,24 @@ import sys from _collections_abc import dict_items, dict_keys, dict_values from _typeshed import SupportsItems, SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT +from collections.abc import ( + Callable, + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + MutableMapping, + MutableSequence, + Sequence, + ValuesView, +) from types import GenericAlias from typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only from typing_extensions import Self, disjoint_base -if sys.version_info >= (3, 10): - from collections.abc import ( - Callable, - ItemsView, - Iterable, - Iterator, - KeysView, - Mapping, - MutableMapping, - MutableSequence, - Sequence, - ValuesView, - ) -else: - from _collections_abc import * +if sys.version_info >= (3, 15): + from builtins import frozendict __all__ = ["ChainMap", "Counter", "OrderedDict", "UserDict", "UserList", "UserString", "defaultdict", "deque", "namedtuple"] @@ -44,6 +43,7 @@ def namedtuple( class UserDict(MutableMapping[_KT, _VT]): data: dict[_KT, _VT] + # __init__ should be kept roughly in line with `dict.__init__`, which has the same semantics @overload def __init__(self, dict: None = None, /) -> None: ... @@ -73,6 +73,7 @@ class UserDict(MutableMapping[_KT, _VT]): def __init__(self: UserDict[str, str], iterable: Iterable[list[str]], /) -> None: ... @overload def __init__(self: UserDict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ... + def __len__(self) -> int: ... def __getitem__(self, key: _KT) -> _VT: ... def __setitem__(self, key: _KT, item: _VT) -> None: ... @@ -91,19 +92,23 @@ class UserDict(MutableMapping[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S) -> UserDict[_T, _S]: ... + @overload def __or__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... @overload def __or__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... + @overload def __ror__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... @overload def __ror__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... + # UserDict.__ior__ should be kept roughly in line with MutableMapping.update() @overload # type: ignore[misc] def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... @overload def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... + if sys.version_info >= (3, 12): @overload def get(self, key: _KT, default: None = None) -> _VT | None: ... @@ -114,10 +119,12 @@ class UserDict(MutableMapping[_KT, _VT]): class UserList(MutableSequence[_T]): data: list[_T] + @overload def __init__(self, initlist: None = None) -> None: ... @overload def __init__(self, initlist: Iterable[_T]) -> None: ... + __hash__: ClassVar[None] # type: ignore[assignment] def __lt__(self, other: list[_T] | UserList[_T]) -> bool: ... def __le__(self, other: list[_T] | UserList[_T]) -> bool: ... @@ -126,14 +133,17 @@ class UserList(MutableSequence[_T]): def __eq__(self, other: object) -> bool: ... def __contains__(self, item: object) -> bool: ... def __len__(self) -> int: ... + @overload def __getitem__(self, i: SupportsIndex) -> _T: ... @overload def __getitem__(self, i: slice[SupportsIndex | None]) -> Self: ... + @overload def __setitem__(self, i: SupportsIndex, item: _T) -> None: ... @overload def __setitem__(self, i: slice[SupportsIndex | None], item: Iterable[_T]) -> None: ... + def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None]) -> None: ... def __add__(self, other: Iterable[_T]) -> Self: ... def __radd__(self, other: Iterable[_T]) -> Self: ... @@ -152,11 +162,13 @@ class UserList(MutableSequence[_T]): # to `list.index`. In order to give more precise types, we pretend that the # `item` argument is positional-only. def index(self, item: _T, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ... + # All arguments are passed to `list.sort` at runtime, so the signature should be kept in line with `list.sort`. @overload def sort(self: UserList[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ... @overload def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ... + def extend(self, other: Iterable[_T]) -> None: ... class UserString(Sequence[UserString]): @@ -235,10 +247,12 @@ class UserString(Sequence[UserString]): class deque(MutableSequence[_T]): @property def maxlen(self) -> int | None: ... + @overload def __init__(self, *, maxlen: int | None = None) -> None: ... @overload def __init__(self, iterable: Iterable[_T], maxlen: int | None = None) -> None: ... + def append(self, x: _T, /) -> None: ... def appendleft(self, x: _T, /) -> None: ... def copy(self) -> Self: ... @@ -280,17 +294,20 @@ class Counter(dict[_T, int], Generic[_T]): def __init__(self, mapping: SupportsKeysAndGetItem[_T, int], /) -> None: ... @overload def __init__(self, iterable: Iterable[_T], /) -> None: ... + def copy(self) -> Self: ... def elements(self) -> Iterator[_T]: ... def most_common(self, n: int | None = None) -> list[tuple[_T, int]]: ... @classmethod def fromkeys(cls, iterable: Any, v: int | None = None) -> NoReturn: ... # type: ignore[override] + @overload def subtract(self, iterable: None = None, /) -> None: ... @overload def subtract(self, mapping: Mapping[_T, int], /) -> None: ... @overload def subtract(self, iterable: Iterable[_T], /) -> None: ... + # Unlike dict.update(), use Mapping instead of SupportsKeysAndGetItem for the first overload # (source code does an `isinstance(other, Mapping)` check) # @@ -303,16 +320,23 @@ class Counter(dict[_T, int], Generic[_T]): def update(self, iterable: Iterable[_T], /, **kwargs: int) -> None: ... @overload def update(self, iterable: None = None, /, **kwargs: int) -> None: ... + + def total(self) -> int: ... def __missing__(self, key: _T) -> int: ... def __delitem__(self, elem: object) -> None: ... - if sys.version_info >= (3, 10): - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __le__(self, other: Counter[Any]) -> bool: ... + def __lt__(self, other: Counter[Any]) -> bool: ... + def __ge__(self, other: Counter[Any]) -> bool: ... + def __gt__(self, other: Counter[Any]) -> bool: ... def __add__(self, other: Counter[_S]) -> Counter[_T | _S]: ... def __sub__(self, other: Counter[_T]) -> Counter[_T]: ... def __and__(self, other: Counter[_T]) -> Counter[_T]: ... def __or__(self, other: Counter[_S]) -> Counter[_T | _S]: ... # type: ignore[override] + if sys.version_info >= (3, 15): + def __xor__(self, other: Counter[_S]) -> Counter[_T | _S]: ... # type: ignore[override] + def __pos__(self) -> Counter[_T]: ... def __neg__(self) -> Counter[_T]: ... # several type: ignores because __iadd__ is supposedly incompatible with __add__, etc. @@ -320,12 +344,8 @@ class Counter(dict[_T, int], Generic[_T]): def __isub__(self, other: SupportsItems[_T, int]) -> Self: ... def __iand__(self, other: SupportsItems[_T, int]) -> Self: ... def __ior__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[override,misc] - if sys.version_info >= (3, 10): - def total(self) -> int: ... - def __le__(self, other: Counter[Any]) -> bool: ... - def __lt__(self, other: Counter[Any]) -> bool: ... - def __ge__(self, other: Counter[Any]) -> bool: ... - def __gt__(self, other: Counter[Any]) -> bool: ... + if sys.version_info >= (3, 15): + def __ixor__(self, other: Counter[_T]) -> Self: ... # type: ignore[misc] # The pure-Python implementations of the "views" classes # These are exposed at runtime in `collections/__init__.py` @@ -366,6 +386,7 @@ class OrderedDict(dict[_KT, _VT]): def keys(self) -> _odict_keys[_KT, _VT]: ... def items(self) -> _odict_items[_KT, _VT]: ... def values(self) -> _odict_values[_KT, _VT]: ... + # The signature of OrderedDict.fromkeys should be kept in line with `dict.fromkeys`, modulo positional-only differences. # Like dict.fromkeys, its true signature is not expressible in the current type system. # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. @@ -375,11 +396,13 @@ class OrderedDict(dict[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S) -> OrderedDict[_T, _S]: ... + # Keep OrderedDict.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. @overload def setdefault(self: OrderedDict[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... @overload def setdefault(self, key: _KT, default: _VT) -> _VT: ... + # Same as dict.pop, but accepts keyword arguments @overload def pop(self, key: _KT) -> _VT: ... @@ -387,19 +410,36 @@ class OrderedDict(dict[_KT, _VT]): def pop(self, key: _KT, default: _VT) -> _VT: ... @overload def pop(self, key: _KT, default: _T) -> _VT | _T: ... + def __eq__(self, value: object, /) -> bool: ... - @overload - def __or__(self, value: dict[_KT, _VT], /) -> Self: ... - @overload - def __or__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... - @overload - def __ror__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] + + if sys.version_info >= (3, 15): + @overload + def __or__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2] | frozendict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + + @overload # type: ignore[override] + def __ror__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> Self: ... # type: ignore[override,misc] + @overload + def __ror__( # type: ignore[misc] + self, value: dict[_T1, _T2] | frozendict[_T1, _T2], / + ) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + else: + @overload + def __or__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] @disjoint_base class defaultdict(dict[_KT, _VT]): default_factory: Callable[[], _VT] | None + @overload def __init__(self) -> None: ... @overload @@ -433,14 +473,19 @@ class defaultdict(dict[_KT, _VT]): /, **kwargs: _VT, ) -> None: ... + def __missing__(self, key: _KT, /) -> _VT: ... def __copy__(self) -> Self: ... def copy(self) -> Self: ... - @overload + + # defaultdict rejects frozendict in its direct __or__/__ror__ methods, even though dict accepts it. + # See https://github.com/python/cpython/issues/149534. + @overload # type: ignore[override] def __or__(self, value: dict[_KT, _VT], /) -> Self: ... @overload def __or__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... - @overload + + @overload # type: ignore[override] def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... @overload def __ror__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] @@ -457,25 +502,30 @@ class ChainMap(MutableMapping[_KT, _VT]): def __iter__(self) -> Iterator[_KT]: ... def __len__(self) -> int: ... def __contains__(self, key: object) -> bool: ... + @overload def get(self, key: _KT, default: None = None) -> _VT | None: ... @overload def get(self, key: _KT, default: _VT) -> _VT: ... @overload def get(self, key: _KT, default: _T) -> _VT | _T: ... + def __missing__(self, key: _KT) -> _VT: ... # undocumented def __bool__(self) -> bool: ... + # Keep ChainMap.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. @overload def setdefault(self: ChainMap[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... @overload def setdefault(self, key: _KT, default: _VT) -> _VT: ... + @overload def pop(self, key: _KT) -> _VT: ... @overload def pop(self, key: _KT, default: _VT) -> _VT: ... @overload def pop(self, key: _KT, default: _T) -> _VT | _T: ... + def copy(self) -> Self: ... __copy__ = copy # All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime, @@ -496,14 +546,17 @@ class ChainMap(MutableMapping[_KT, _VT]): @classmethod @overload def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> ChainMap[_T, _S]: ... + @overload def __or__(self, other: Mapping[_KT, _VT]) -> Self: ... @overload def __or__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... + @overload def __ror__(self, other: Mapping[_KT, _VT]) -> Self: ... @overload def __ror__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... + # ChainMap.__ior__ should be kept roughly in line with MutableMapping.update() @overload # type: ignore[misc] def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... diff --git a/mypy/typeshed/stdlib/colorsys.pyi b/mypy/typeshed/stdlib/colorsys.pyi index 4afcb5392b58e..d4edab12ecc70 100644 --- a/mypy/typeshed/stdlib/colorsys.pyi +++ b/mypy/typeshed/stdlib/colorsys.pyi @@ -9,7 +9,7 @@ def hls_to_rgb(h: float, l: float, s: float) -> tuple[float, float, float]: ... def rgb_to_hsv(r: float, g: float, b: float) -> tuple[float, float, float]: ... def hsv_to_rgb(h: float, s: float, v: float) -> tuple[float, float, float]: ... -# TODO: undocumented +# undocumented ONE_SIXTH: Final[float] ONE_THIRD: Final[float] TWO_THIRD: Final[float] diff --git a/mypy/typeshed/stdlib/compileall.pyi b/mypy/typeshed/stdlib/compileall.pyi index 8972d50a4a634..49a4c69dd3fb0 100644 --- a/mypy/typeshed/stdlib/compileall.pyi +++ b/mypy/typeshed/stdlib/compileall.pyi @@ -1,4 +1,3 @@ -import sys from _typeshed import StrPath from py_compile import PycInvalidationMode from typing import Any, Protocol, type_check_only @@ -9,74 +8,38 @@ __all__ = ["compile_dir", "compile_file", "compile_path"] class _SupportsSearch(Protocol): def search(self, string: str, /) -> Any: ... -if sys.version_info >= (3, 10): - def compile_dir( - dir: StrPath, - maxlevels: int | None = None, - ddir: StrPath | None = None, - force: bool = False, - rx: _SupportsSearch | None = None, - quiet: int = 0, - legacy: bool = False, - optimize: int = -1, - workers: int = 1, - invalidation_mode: PycInvalidationMode | None = None, - *, - stripdir: StrPath | None = None, - prependdir: StrPath | None = None, - limit_sl_dest: StrPath | None = None, - hardlink_dupes: bool = False, - ) -> bool: ... - def compile_file( - fullname: StrPath, - ddir: StrPath | None = None, - force: bool = False, - rx: _SupportsSearch | None = None, - quiet: int = 0, - legacy: bool = False, - optimize: int = -1, - invalidation_mode: PycInvalidationMode | None = None, - *, - stripdir: StrPath | None = None, - prependdir: StrPath | None = None, - limit_sl_dest: StrPath | None = None, - hardlink_dupes: bool = False, - ) -> bool: ... - -else: - def compile_dir( - dir: StrPath, - maxlevels: int | None = None, - ddir: StrPath | None = None, - force: bool = False, - rx: _SupportsSearch | None = None, - quiet: int = 0, - legacy: bool = False, - optimize: int = -1, - workers: int = 1, - invalidation_mode: PycInvalidationMode | None = None, - *, - stripdir: str | None = None, # https://bugs.python.org/issue40447 - prependdir: StrPath | None = None, - limit_sl_dest: StrPath | None = None, - hardlink_dupes: bool = False, - ) -> bool: ... - def compile_file( - fullname: StrPath, - ddir: StrPath | None = None, - force: bool = False, - rx: _SupportsSearch | None = None, - quiet: int = 0, - legacy: bool = False, - optimize: int = -1, - invalidation_mode: PycInvalidationMode | None = None, - *, - stripdir: str | None = None, # https://bugs.python.org/issue40447 - prependdir: StrPath | None = None, - limit_sl_dest: StrPath | None = None, - hardlink_dupes: bool = False, - ) -> bool: ... - +def compile_dir( + dir: StrPath, + maxlevels: int | None = None, + ddir: StrPath | None = None, + force: bool = False, + rx: _SupportsSearch | None = None, + quiet: int = 0, + legacy: bool = False, + optimize: int = -1, + workers: int = 1, + invalidation_mode: PycInvalidationMode | None = None, + *, + stripdir: StrPath | None = None, + prependdir: StrPath | None = None, + limit_sl_dest: StrPath | None = None, + hardlink_dupes: bool = False, +) -> bool: ... +def compile_file( + fullname: StrPath, + ddir: StrPath | None = None, + force: bool = False, + rx: _SupportsSearch | None = None, + quiet: int = 0, + legacy: bool = False, + optimize: int = -1, + invalidation_mode: PycInvalidationMode | None = None, + *, + stripdir: StrPath | None = None, + prependdir: StrPath | None = None, + limit_sl_dest: StrPath | None = None, + hardlink_dupes: bool = False, +) -> bool: ... def compile_path( skip_curdir: bool = ..., maxlevels: int = 0, diff --git a/mypy/typeshed/stdlib/compression/zstd/__init__.pyi b/mypy/typeshed/stdlib/compression/zstd/__init__.pyi index acfbe4913b5da..8673c59a41c22 100644 --- a/mypy/typeshed/stdlib/compression/zstd/__init__.pyi +++ b/mypy/typeshed/stdlib/compression/zstd/__init__.pyi @@ -52,6 +52,7 @@ def compress( def decompress( data: ReadableBuffer, zstd_dict: ZstdDict | tuple[ZstdDict, int] | None = None, options: Mapping[int, int] | None = None ) -> bytes: ... + @final class CompressionParameter(enum.IntEnum): compression_level = _zstd.ZSTD_c_compressionLevel diff --git a/mypy/typeshed/stdlib/compression/zstd/_zstdfile.pyi b/mypy/typeshed/stdlib/compression/zstd/_zstdfile.pyi index d37e6b1741664..b16b43c1da0ae 100644 --- a/mypy/typeshed/stdlib/compression/zstd/_zstdfile.pyi +++ b/mypy/typeshed/stdlib/compression/zstd/_zstdfile.pyi @@ -3,8 +3,7 @@ from collections.abc import Mapping from compression._common import _streams from compression.zstd import ZstdDict from io import TextIOWrapper, _WrappedBuffer -from typing import Literal, Protocol, overload, type_check_only -from typing_extensions import TypeAlias +from typing import Literal, Protocol, TypeAlias, overload, type_check_only from _zstd import ZstdCompressor, _ZstdCompressorFlushBlock, _ZstdCompressorFlushFrame @@ -49,6 +48,7 @@ class ZstdFile(_streams.BaseStream): options: Mapping[int, int] | None = None, zstd_dict: ZstdDict | tuple[ZstdDict, int] | None = None, ) -> None: ... + def write(self, data: ReadableBuffer, /) -> int: ... def flush(self, mode: _ZstdCompressorFlushBlock | _ZstdCompressorFlushFrame = 1) -> bytes: ... # type: ignore[override] def read(self, size: int | None = -1) -> bytes: ... diff --git a/mypy/typeshed/stdlib/concurrent/futures/_base.pyi b/mypy/typeshed/stdlib/concurrent/futures/_base.pyi index be48a6e4289c8..05680b5de4619 100644 --- a/mypy/typeshed/stdlib/concurrent/futures/_base.pyi +++ b/mypy/typeshed/stdlib/concurrent/futures/_base.pyi @@ -4,8 +4,8 @@ from _typeshed import Unused from collections.abc import Callable, Iterable, Iterator from logging import Logger from types import GenericAlias, TracebackType -from typing import Any, Final, Generic, NamedTuple, Protocol, TypeVar, type_check_only -from typing_extensions import ParamSpec, Self +from typing import Any, Final, Generic, NamedTuple, ParamSpec, Protocol, TypeVar, type_check_only +from typing_extensions import Self FIRST_COMPLETED: Final = "FIRST_COMPLETED" FIRST_EXCEPTION: Final = "FIRST_EXCEPTION" diff --git a/mypy/typeshed/stdlib/concurrent/futures/interpreter.pyi b/mypy/typeshed/stdlib/concurrent/futures/interpreter.pyi index e101022babcb6..f6925806a5eb9 100644 --- a/mypy/typeshed/stdlib/concurrent/futures/interpreter.pyi +++ b/mypy/typeshed/stdlib/concurrent/futures/interpreter.pyi @@ -1,8 +1,8 @@ import sys from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor -from typing import Any, Literal, Protocol, overload, type_check_only -from typing_extensions import ParamSpec, Self, TypeAlias, TypeVar, TypeVarTuple, Unpack +from typing import Any, Literal, ParamSpec, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import Self, TypeVar, TypeVarTuple, Unpack _Task: TypeAlias = tuple[bytes, Literal["function", "script"]] _Ts = TypeVarTuple("_Ts") @@ -25,6 +25,7 @@ if sys.version_info >= (3, 14): class WorkerContext(ThreadWorkerContext): interp: Interpreter | None results: Queue | None + @overload # type: ignore[override] @classmethod def prepare( @@ -33,6 +34,7 @@ if sys.version_info >= (3, 14): @overload @classmethod def prepare(cls, initializer: Callable[[], object], initargs: tuple[()]) -> tuple[Callable[[], Self], _TaskFunc]: ... + def __init__(self, initdata: _Task) -> None: ... def __del__(self) -> None: ... def run(self, task: _Task) -> None: ... # type: ignore[override] @@ -52,6 +54,7 @@ if sys.version_info >= (3, 14): def prepare_context( cls, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]] ) -> tuple[Callable[[], WorkerContext], _TaskFunc]: ... + @overload def __init__( self, diff --git a/mypy/typeshed/stdlib/concurrent/futures/thread.pyi b/mypy/typeshed/stdlib/concurrent/futures/thread.pyi index 50a6a9c6f43ea..685bf1cfc104a 100644 --- a/mypy/typeshed/stdlib/concurrent/futures/thread.pyi +++ b/mypy/typeshed/stdlib/concurrent/futures/thread.pyi @@ -3,8 +3,8 @@ import sys from collections.abc import Callable, Iterable, Mapping, Set as AbstractSet from threading import Lock, Semaphore, Thread from types import GenericAlias -from typing import Any, Generic, Protocol, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack +from typing import Any, Generic, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, TypeVarTuple, Unpack from weakref import ref from ._base import BrokenExecutor, Executor, Future @@ -43,10 +43,12 @@ if sys.version_info >= (3, 14): def prepare( cls, initializer: Callable[[], object], initargs: tuple[()] ) -> tuple[Callable[[], Self], _ResolveTaskFunc]: ... + @overload def __init__(self, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]]) -> None: ... @overload def __init__(self, initializer: Callable[[], object], initargs: tuple[()]) -> None: ... + def initialize(self) -> None: ... def finalize(self) -> None: ... def run(self, task: _Task) -> None: ... @@ -136,5 +138,6 @@ class ThreadPoolExecutor(Executor): initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]], ) -> None: ... + def _adjust_thread_count(self) -> None: ... def _initializer_failed(self) -> None: ... diff --git a/mypy/typeshed/stdlib/concurrent/interpreters/__init__.pyi b/mypy/typeshed/stdlib/concurrent/interpreters/__init__.pyi index 171fadb2202be..d19db09642602 100644 --- a/mypy/typeshed/stdlib/concurrent/interpreters/__init__.pyi +++ b/mypy/typeshed/stdlib/concurrent/interpreters/__init__.pyi @@ -2,8 +2,8 @@ import sys import threading import types from collections.abc import Callable -from typing import Any, Literal, TypeVar -from typing_extensions import ParamSpec, Self +from typing import Any, Literal, ParamSpec, TypeVar +from typing_extensions import Self if sys.version_info >= (3, 14): # needed to satisfy pyright checks for Python <= 3.13 from _interpreters import ( diff --git a/mypy/typeshed/stdlib/concurrent/interpreters/_crossinterp.pyi b/mypy/typeshed/stdlib/concurrent/interpreters/_crossinterp.pyi index 50fe7cf0b4ba4..c8e29aaafa18a 100644 --- a/mypy/typeshed/stdlib/concurrent/interpreters/_crossinterp.pyi +++ b/mypy/typeshed/stdlib/concurrent/interpreters/_crossinterp.pyi @@ -1,7 +1,7 @@ import sys from collections.abc import Callable -from typing import Final, NewType -from typing_extensions import Never, Self, TypeAlias +from typing import Final, NewType, TypeAlias +from typing_extensions import Never, Self if sys.version_info >= (3, 14): # needed to satisfy pyright checks for Python <= 3.13 from _interpqueues import _UnboundOp diff --git a/mypy/typeshed/stdlib/configparser.pyi b/mypy/typeshed/stdlib/configparser.pyi index 9b3f02324b7fd..385336ec154ab 100644 --- a/mypy/typeshed/stdlib/configparser.pyi +++ b/mypy/typeshed/stdlib/configparser.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import BytesPath, GenericPath, MaybeNone, StrOrBytesPath, StrPath, SupportsWrite from collections.abc import Callable, ItemsView, Iterable, Iterator, Mapping, MutableMapping, Sequence from re import Pattern -from typing import Any, AnyStr, ClassVar, Final, Literal, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias, deprecated +from typing import Any, AnyStr, ClassVar, Final, Literal, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import deprecated if sys.version_info >= (3, 14): __all__ = ( @@ -269,6 +269,7 @@ class RawConfigParser(_Parser): def has_section(self, section: _SectionName) -> bool: ... def options(self, section: _SectionName) -> list[str]: ... def has_option(self, section: _SectionName, option: str) -> bool: ... + @overload def read(self, filenames: GenericPath[AnyStr], encoding: str | None = None) -> list[AnyStr]: ... @overload @@ -277,12 +278,14 @@ class RawConfigParser(_Parser): def read(self, filenames: Iterable[BytesPath], encoding: str | None = None) -> list[bytes]: ... @overload def read(self, filenames: Iterable[StrOrBytesPath], encoding: str | None = None) -> list[str | bytes]: ... + def read_file(self, f: Iterable[str], source: str | None = None) -> None: ... def read_string(self, string: str, source: str = "") -> None: ... def read_dict(self, dictionary: Mapping[str, Mapping[str, Any]], source: str = "") -> None: ... if sys.version_info < (3, 12): @deprecated("Deprecated since Python 3.2; removed in Python 3.12. Use `parser.read_file()` instead.") def readfp(self, fp: Iterable[str], filename: str | None = None) -> None: ... + # These get* methods are partially applied (with the same names) in # SectionProxy; the stubs should be kept updated together @overload @@ -291,18 +294,21 @@ class RawConfigParser(_Parser): def getint( self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None, fallback: _T ) -> int | _T: ... + @overload def getfloat(self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None) -> float: ... @overload def getfloat( self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None, fallback: _T ) -> float | _T: ... + @overload def getboolean(self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None) -> bool: ... @overload def getboolean( self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None, fallback: _T ) -> bool | _T: ... + def _get_conv( self, section: _SectionName, @@ -313,6 +319,7 @@ class RawConfigParser(_Parser): vars: _Section | None = None, fallback: _T = ..., ) -> _T: ... + # This is incompatible with MutableMapping so we ignore the type @overload # type: ignore[override] def get(self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None) -> str | MaybeNone: ... @@ -320,10 +327,12 @@ class RawConfigParser(_Parser): def get( self, section: _SectionName, option: str, *, raw: bool = False, vars: _Section | None = None, fallback: _T ) -> str | _T | MaybeNone: ... + @overload def items(self, *, raw: bool = False, vars: _Section | None = None) -> ItemsView[str, SectionProxy]: ... @overload def items(self, section: _SectionName, raw: bool = False, vars: _Section | None = None) -> list[tuple[str, str]]: ... + def set(self, section: _SectionName, option: str, value: str | None = None) -> None: ... def write(self, fp: SupportsWrite[str], space_around_delimiters: bool = True) -> None: ... def remove_option(self, section: _SectionName, option: str) -> bool: ... @@ -357,6 +366,7 @@ class SectionProxy(MutableMapping[str, str]): def parser(self) -> RawConfigParser: ... @property def name(self) -> str: ... + # This is incompatible with MutableMapping so we ignore the type @overload # type: ignore[override] def get( @@ -380,20 +390,24 @@ class SectionProxy(MutableMapping[str, str]): _impl: Any | None = None, **kwargs: Any, # passed to the underlying parser's get() method ) -> str | _T: ... + # These are partially-applied version of the methods with the same names in # RawConfigParser; the stubs should be kept updated together @overload def getint(self, option: str, *, raw: bool = False, vars: _Section | None = None) -> int | None: ... @overload def getint(self, option: str, fallback: _T = ..., *, raw: bool = False, vars: _Section | None = None) -> int | _T: ... + @overload def getfloat(self, option: str, *, raw: bool = False, vars: _Section | None = None) -> float | None: ... @overload def getfloat(self, option: str, fallback: _T = ..., *, raw: bool = False, vars: _Section | None = None) -> float | _T: ... + @overload def getboolean(self, option: str, *, raw: bool = False, vars: _Section | None = None) -> bool | None: ... @overload def getboolean(self, option: str, fallback: _T = ..., *, raw: bool = False, vars: _Section | None = None) -> bool | _T: ... + # SectionProxy can have arbitrary attributes when custom converters are used def __getattr__(self, key: str) -> Callable[..., Any]: ... diff --git a/mypy/typeshed/stdlib/contextlib.pyi b/mypy/typeshed/stdlib/contextlib.pyi index 0670787a5db1b..73cdda3b8f342 100644 --- a/mypy/typeshed/stdlib/contextlib.pyi +++ b/mypy/typeshed/stdlib/contextlib.pyi @@ -4,10 +4,11 @@ from _typeshed import FileDescriptorOrPath, Unused from abc import ABC, abstractmethod from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator from types import TracebackType -from typing import Any, Generic, Protocol, TypeVar, overload, runtime_checkable, type_check_only -from typing_extensions import ParamSpec, Self, TypeAlias +from typing import Any, Generic, ParamSpec, Protocol, TypeAlias, TypeVar, overload, runtime_checkable, type_check_only +from typing_extensions import Self __all__ = [ + "aclosing", "contextmanager", "closing", "AbstractContextManager", @@ -22,9 +23,6 @@ __all__ = [ "nullcontext", ] -if sys.version_info >= (3, 10): - __all__ += ["aclosing"] - if sys.version_info >= (3, 11): __all__ += ["chdir"] @@ -88,31 +86,23 @@ class _GeneratorContextManager( def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... -if sys.version_info >= (3, 10): - _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) - - class AsyncContextDecorator: - def _recreate_cm(self) -> Self: ... - def __call__(self, func: _AF) -> _AF: ... - - class _AsyncGeneratorContextManager( - _GeneratorContextManagerBase[AsyncGenerator[_T_co, _SendT_contra]], - AbstractAsyncContextManager[_T_co, bool | None], - AsyncContextDecorator, - ): - async def __aexit__( - self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool | None: ... - -else: - class _AsyncGeneratorContextManager( - _GeneratorContextManagerBase[AsyncGenerator[_T_co, _SendT_contra]], AbstractAsyncContextManager[_T_co, bool | None] - ): - async def __aexit__( - self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool | None: ... +_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) + +class AsyncContextDecorator: + def _recreate_cm(self) -> Self: ... + def __call__(self, func: _AF) -> _AF: ... + +class _AsyncGeneratorContextManager( + _GeneratorContextManagerBase[AsyncGenerator[_T_co, _SendT_contra]], + AbstractAsyncContextManager[_T_co, bool | None], + AsyncContextDecorator, +): + async def __aexit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... + @type_check_only class _SupportsClose(Protocol): def close(self) -> object: ... @@ -123,16 +113,15 @@ class closing(AbstractContextManager[_SupportsCloseT, None]): def __init__(self, thing: _SupportsCloseT) -> None: ... def __exit__(self, *exc_info: Unused) -> None: ... -if sys.version_info >= (3, 10): - @type_check_only - class _SupportsAclose(Protocol): - def aclose(self) -> Awaitable[object]: ... +@type_check_only +class _SupportsAclose(Protocol): + def aclose(self) -> Awaitable[object]: ... - _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) +_SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) - class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): - def __init__(self, thing: _SupportsAcloseT) -> None: ... - async def __aexit__(self, *exc_info: Unused) -> None: ... +class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): + def __init__(self, thing: _SupportsAcloseT) -> None: ... + async def __aexit__(self, *exc_info: Unused) -> None: ... class suppress(AbstractContextManager[None, bool]): def __init__(self, *exceptions: type[BaseException]) -> None: ... @@ -199,27 +188,18 @@ class AsyncExitStack(_BaseExitStackAbstract[_ExitT_co]): self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / ) -> _ExitT_co: ... -if sys.version_info >= (3, 10): - class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): - enter_result: _T - @overload - def __init__(self: nullcontext[None]) -> None: ... - @overload - def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 - def __enter__(self) -> _T: ... - def __exit__(self, *exctype: Unused) -> None: ... - async def __aenter__(self) -> _T: ... - async def __aexit__(self, *exctype: Unused) -> None: ... - -else: - class nullcontext(AbstractContextManager[_T, None]): - enter_result: _T - @overload - def __init__(self: nullcontext[None]) -> None: ... - @overload - def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 - def __enter__(self) -> _T: ... - def __exit__(self, *exctype: Unused) -> None: ... +class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): + enter_result: _T + + @overload + def __init__(self: nullcontext[None]) -> None: ... + @overload + def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + + def __enter__(self) -> _T: ... + def __exit__(self, *exctype: Unused) -> None: ... + async def __aenter__(self) -> _T: ... + async def __aexit__(self, *exctype: Unused) -> None: ... if sys.version_info >= (3, 11): _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) diff --git a/mypy/typeshed/stdlib/copyreg.pyi b/mypy/typeshed/stdlib/copyreg.pyi index 8f7fd957fc526..3bfc0de8158f1 100644 --- a/mypy/typeshed/stdlib/copyreg.pyi +++ b/mypy/typeshed/stdlib/copyreg.pyi @@ -1,6 +1,5 @@ from collections.abc import Callable, Hashable -from typing import Any, SupportsInt, TypeVar -from typing_extensions import TypeAlias +from typing import Any, SupportsInt, TypeAlias, TypeVar _T = TypeVar("_T") _Reduce: TypeAlias = tuple[Callable[..., _T], tuple[Any, ...]] | tuple[Callable[..., _T], tuple[Any, ...], Any | None] diff --git a/mypy/typeshed/stdlib/csv.pyi b/mypy/typeshed/stdlib/csv.pyi index 4ed0ab1d83b82..f3b4286a6b495 100644 --- a/mypy/typeshed/stdlib/csv.pyi +++ b/mypy/typeshed/stdlib/csv.pyi @@ -19,11 +19,7 @@ from _csv import ( if sys.version_info >= (3, 12): from _csv import QUOTE_NOTNULL as QUOTE_NOTNULL, QUOTE_STRINGS as QUOTE_STRINGS -if sys.version_info >= (3, 10): - from _csv import Reader, Writer -else: - from _csv import _reader as Reader, _writer as Writer - +from _csv import Reader, Writer from _typeshed import SupportsWrite from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence from types import GenericAlias @@ -80,6 +76,7 @@ class DictReader(Iterator[dict[_T | Any, str | Any]], Generic[_T]): reader: Reader dialect: _DialectLike line_num: int + @overload def __init__( self, @@ -116,6 +113,7 @@ class DictReader(Iterator[dict[_T | Any, str | Any]], Generic[_T]): quoting: _QuotingType = 0, strict: bool = False, ) -> None: ... + def __iter__(self) -> Self: ... def __next__(self) -> dict[_T | Any, str | Any]: ... if sys.version_info >= (3, 12): diff --git a/mypy/typeshed/stdlib/ctypes/__init__.pyi b/mypy/typeshed/stdlib/ctypes/__init__.pyi index 8d048aa97e7d5..7eee1b760063f 100644 --- a/mypy/typeshed/stdlib/ctypes/__init__.pyi +++ b/mypy/typeshed/stdlib/ctypes/__init__.pyi @@ -26,8 +26,8 @@ from _ctypes import ( from _typeshed import StrPath, SupportsBool, SupportsLen from ctypes._endian import BigEndianStructure as BigEndianStructure, LittleEndianStructure as LittleEndianStructure from types import GenericAlias -from typing import Any, ClassVar, Final, Generic, Literal, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated +from typing import Any, ClassVar, Final, Generic, Literal, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, deprecated if sys.platform == "win32": from _ctypes import FormatError as FormatError, get_last_error as get_last_error, set_last_error as set_last_error @@ -50,6 +50,7 @@ if sys.version_info >= (3, 14): def POINTER(cls: None) -> type[c_void_p]: ... @overload def POINTER(cls: type[_CT]) -> type[_Pointer[_CT]]: ... + def pointer(obj: _CT) -> _Pointer[_CT]: ... else: @@ -166,15 +167,12 @@ c_buffer = create_string_buffer def create_unicode_buffer(init: int | str, size: int | None = None) -> Array[c_wchar]: ... -if sys.version_info >= (3, 13): +if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def SetPointerType(pointer: type[_Pointer[Any]], cls: _CTypeBaseType) -> None: ... - @deprecated("Soft deprecated since Python 3.13. Use multiplication instead.") - def ARRAY(typ: _CT, len: int) -> Array[_CT]: ... -else: - def SetPointerType(pointer: type[_Pointer[Any]], cls: _CTypeBaseType) -> None: ... - def ARRAY(typ: _CT, len: int) -> Array[_CT]: ... +@deprecated("Soft deprecated since Python 3.13. Use multiplication instead.") +def ARRAY(typ: _CT, len: int) -> Array[_CT]: ... if sys.platform == "win32": def DllCanUnloadNow() -> int: ... @@ -221,86 +219,146 @@ class py_object(_CanCastTo, _SimpleCData[_T]): class c_bool(_SimpleCData[bool]): _type_: ClassVar[Literal["?"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] def __init__(self, value: SupportsBool | SupportsLen | None = ...) -> None: ... class c_byte(_SimpleCData[int]): _type_: ClassVar[Literal["b"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_ubyte(_SimpleCData[int]): _type_: ClassVar[Literal["B"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_short(_SimpleCData[int]): _type_: ClassVar[Literal["h"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_ushort(_SimpleCData[int]): _type_: ClassVar[Literal["H"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_long(_SimpleCData[int]): _type_: ClassVar[Literal["l"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_ulong(_SimpleCData[int]): _type_: ClassVar[Literal["L"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_int(_SimpleCData[int]): # can be an alias for c_long _type_: ClassVar[Literal["i", "l"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_uint(_SimpleCData[int]): # can be an alias for c_ulong _type_: ClassVar[Literal["I", "L"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_longlong(_SimpleCData[int]): # can be an alias for c_long _type_: ClassVar[Literal["q", "l"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_ulonglong(_SimpleCData[int]): # can be an alias for c_ulong _type_: ClassVar[Literal["Q", "L"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] c_int8 = c_byte c_uint8 = c_ubyte class c_int16(_SimpleCData[int]): # can be an alias for c_short or c_int _type_: ClassVar[Literal["h", "i"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_uint16(_SimpleCData[int]): # can be an alias for c_ushort or c_uint _type_: ClassVar[Literal["H", "I"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_int32(_SimpleCData[int]): # can be an alias for c_int or c_long _type_: ClassVar[Literal["i", "l"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_uint32(_SimpleCData[int]): # can be an alias for c_uint or c_ulong _type_: ClassVar[Literal["I", "L"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_int64(_SimpleCData[int]): # can be an alias for c_long or c_longlong _type_: ClassVar[Literal["l", "q"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_uint64(_SimpleCData[int]): # can be an alias for c_ulong or c_ulonglong _type_: ClassVar[Literal["L", "Q"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_ssize_t(_SimpleCData[int]): # alias for c_int, c_long, or c_longlong _type_: ClassVar[Literal["i", "l", "q"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_size_t(_SimpleCData[int]): # alias for c_uint, c_ulong, or c_ulonglong _type_: ClassVar[Literal["I", "L", "Q"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_float(_SimpleCData[float]): _type_: ClassVar[Literal["f"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_double(_SimpleCData[float]): _type_: ClassVar[Literal["d"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_longdouble(_SimpleCData[float]): # can be an alias for c_double _type_: ClassVar[Literal["d", "g"]] if sys.version_info >= (3, 14) and sys.platform != "win32": + # NOTE: currently (3.14.4) the `__ctype_{be,le}__` attributes of these complex types are missing at runtime: + # https://github.com/python/cpython/issues/148464 + class c_double_complex(_SimpleCData[complex]): - _type_: ClassVar[Literal["D"]] + if sys.version_info >= (3, 15): + _type_: ClassVar[Literal["Zd"]] + else: + _type_: ClassVar[Literal["D"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_float_complex(_SimpleCData[complex]): - _type_: ClassVar[Literal["F"]] + if sys.version_info >= (3, 15): + _type_: ClassVar[Literal["Zf"]] + else: + _type_: ClassVar[Literal["F"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] class c_longdouble_complex(_SimpleCData[complex]): - _type_: ClassVar[Literal["G"]] + if sys.version_info >= (3, 15): + _type_: ClassVar[Literal["Zg"]] + else: + _type_: ClassVar[Literal["G"]] class c_char(_SimpleCData[bytes]): _type_: ClassVar[Literal["c"]] + __ctype_be__: ClassVar[type[Self]] + __ctype_le__: ClassVar[type[Self]] def __init__(self, value: int | bytes | bytearray = ...) -> None: ... class c_char_p(_PointerLike, _SimpleCData[bytes | None]): diff --git a/mypy/typeshed/stdlib/ctypes/wintypes.pyi b/mypy/typeshed/stdlib/ctypes/wintypes.pyi index 0f0d61a396d5f..b94c5e74148a2 100644 --- a/mypy/typeshed/stdlib/ctypes/wintypes.pyi +++ b/mypy/typeshed/stdlib/ctypes/wintypes.pyi @@ -21,8 +21,8 @@ from ctypes import ( c_wchar, c_wchar_p, ) -from typing import Any, Final, TypeVar -from typing_extensions import Self, TypeAlias +from typing import Any, Final, TypeAlias, TypeVar +from typing_extensions import Self if sys.version_info >= (3, 12): from ctypes import c_ubyte diff --git a/mypy/typeshed/stdlib/curses/__init__.pyi b/mypy/typeshed/stdlib/curses/__init__.pyi index 3e32487ad99f2..cf50481108786 100644 --- a/mypy/typeshed/stdlib/curses/__init__.pyi +++ b/mypy/typeshed/stdlib/curses/__init__.pyi @@ -1,10 +1,8 @@ -import sys from _curses import * from _curses import window as window from _typeshed import structseq from collections.abc import Callable -from typing import Final, TypeVar, final, type_check_only -from typing_extensions import Concatenate, ParamSpec +from typing import Concatenate, Final, ParamSpec, TypeVar, final, type_check_only # NOTE: The _curses module is ordinarily only available on Unix, but the # windows-curses package makes it available on Windows as well with the same @@ -30,8 +28,7 @@ def wrapper(func: Callable[Concatenate[window, _P], _T], /, *arg: _P.args, **kwd @final @type_check_only class _ncurses_version(structseq[int], tuple[int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("major", "minor", "patch") + __match_args__: Final = ("major", "minor", "patch") @property def major(self) -> int: ... diff --git a/mypy/typeshed/stdlib/dataclasses.pyi b/mypy/typeshed/stdlib/dataclasses.pyi index 3a1c8cb5d62dd..1a7b8fd645589 100644 --- a/mypy/typeshed/stdlib/dataclasses.pyi +++ b/mypy/typeshed/stdlib/dataclasses.pyi @@ -17,6 +17,7 @@ __all__ = [ "Field", "FrozenInstanceError", "InitVar", + "KW_ONLY", "MISSING", "fields", "asdict", @@ -26,9 +27,6 @@ __all__ = [ "is_dataclass", ] -if sys.version_info >= (3, 10): - __all__ += ["KW_ONLY"] - _DataclassT = TypeVar("_DataclassT", bound=DataclassInstance) @type_check_only @@ -60,13 +58,13 @@ class _MISSING_TYPE(enum.Enum): MISSING: Final = _MISSING_TYPE.MISSING -if sys.version_info >= (3, 10): - class KW_ONLY: ... +class KW_ONLY: ... @overload def asdict(obj: DataclassInstance) -> dict[str, Any]: ... @overload def asdict(obj: DataclassInstance, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T: ... + @overload def astuple(obj: DataclassInstance) -> tuple[Any, ...]: ... @overload @@ -105,8 +103,7 @@ if sys.version_info >= (3, 11): slots: bool = False, weakref_slot: bool = False, ) -> Callable[[type[_T]], type[_T]]: ... - -elif sys.version_info >= (3, 10): +else: @overload def dataclass( cls: type[_T], @@ -138,32 +135,6 @@ elif sys.version_info >= (3, 10): slots: bool = False, ) -> Callable[[type[_T]], type[_T]]: ... -else: - @overload - def dataclass( - cls: type[_T], - /, - *, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - ) -> type[_T]: ... - @overload - def dataclass( - cls: None = None, - /, - *, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - ) -> Callable[[type[_T]], type[_T]]: ... - # See https://github.com/python/mypy/issues/10750 @type_check_only class _DefaultFactory(Protocol[_T_co]): @@ -185,7 +156,7 @@ class Field(Generic[_T]): "doc", "_field_type", ) - elif sys.version_info >= (3, 10): + else: __slots__ = ( "name", "type", @@ -199,8 +170,6 @@ class Field(Generic[_T]): "kw_only", "_field_type", ) - else: - __slots__ = ("name", "type", "default", "default_factory", "repr", "hash", "init", "compare", "metadata", "_field_type") name: str type: Type[_T] | str | Any default: _T | Literal[_MISSING_TYPE.MISSING] @@ -214,8 +183,7 @@ class Field(Generic[_T]): if sys.version_info >= (3, 14): doc: str | None - if sys.version_info >= (3, 10): - kw_only: bool | Literal[_MISSING_TYPE.MISSING] + kw_only: bool | Literal[_MISSING_TYPE.MISSING] if sys.version_info >= (3, 14): def __init__( @@ -230,18 +198,6 @@ class Field(Generic[_T]): kw_only: bool, doc: str | None, ) -> None: ... - elif sys.version_info >= (3, 10): - def __init__( - self, - default: _T, - default_factory: Callable[[], _T], - init: bool, - repr: bool, - hash: bool | None, - compare: bool, - metadata: Mapping[Any, Any], - kw_only: bool, - ) -> None: ... else: def __init__( self, @@ -252,6 +208,7 @@ class Field(Generic[_T]): hash: bool | None, compare: bool, metadata: Mapping[Any, Any], + kw_only: bool, ) -> None: ... def __set_name__(self, owner: Type[Any], name: str) -> None: ... @@ -299,8 +256,7 @@ if sys.version_info >= (3, 14): kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., doc: str | None = None, ) -> Any: ... - -elif sys.version_info >= (3, 10): +else: @overload # `default` and `default_factory` are optional and mutually exclusive. def field( *, @@ -338,41 +294,6 @@ elif sys.version_info >= (3, 10): kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., ) -> Any: ... -else: - @overload # `default` and `default_factory` are optional and mutually exclusive. - def field( - *, - default: _T, - default_factory: Literal[_MISSING_TYPE.MISSING] = ..., - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - ) -> _T: ... - @overload - def field( - *, - default: Literal[_MISSING_TYPE.MISSING] = ..., - default_factory: Callable[[], _T], - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - ) -> _T: ... - @overload - def field( - *, - default: Literal[_MISSING_TYPE.MISSING] = ..., - default_factory: Literal[_MISSING_TYPE.MISSING] = ..., - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - ) -> Any: ... - def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tuple[Field[Any], ...]: ... # HACK: `obj: Never` typing matches if object argument is using `Any` type. @@ -389,6 +310,7 @@ class InitVar(Generic[_T]): __slots__ = ("type",) type: Type[_T] def __init__(self, type: Type[_T]) -> None: ... + @overload def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm] @overload @@ -454,7 +376,7 @@ elif sys.version_info >= (3, 11): weakref_slot: bool = False, ) -> type: ... -elif sys.version_info >= (3, 10): +else: def make_dataclass( cls_name: str, fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], @@ -472,19 +394,4 @@ elif sys.version_info >= (3, 10): slots: bool = False, ) -> type: ... -else: - def make_dataclass( - cls_name: str, - fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], - *, - bases: tuple[type, ...] = (), - namespace: dict[str, Any] | None = None, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - ) -> type: ... - def replace(obj: _DataclassT, /, **changes: Any) -> _DataclassT: ... diff --git a/mypy/typeshed/stdlib/datetime.pyi b/mypy/typeshed/stdlib/datetime.pyi index 8a0536c006d57..95c62a9c1b712 100644 --- a/mypy/typeshed/stdlib/datetime.pyi +++ b/mypy/typeshed/stdlib/datetime.pyi @@ -1,8 +1,8 @@ import sys from abc import abstractmethod from time import struct_time -from typing import ClassVar, Final, NoReturn, SupportsIndex, final, overload, type_check_only -from typing_extensions import CapsuleType, Self, TypeAlias, deprecated, disjoint_base +from typing import ClassVar, Final, NoReturn, SupportsIndex, TypeAlias, final, overload, type_check_only +from typing_extensions import CapsuleType, Self, deprecated, disjoint_base if sys.version_info >= (3, 11): __all__ = ("date", "datetime", "time", "timedelta", "timezone", "tzinfo", "MINYEAR", "MAXYEAR", "UTC") @@ -63,8 +63,14 @@ class date: def today(cls) -> Self: ... @classmethod def fromordinal(cls, n: int, /) -> Self: ... - @classmethod - def fromisoformat(cls, date_string: str, /) -> Self: ... + + if sys.version_info >= (3, 15): + @classmethod + def fromisoformat(cls, string: str, /) -> Self: ... + else: + @classmethod + def fromisoformat(cls, date_string: str, /) -> Self: ... + @classmethod def fromisocalendar(cls, year: int, week: int, day: int) -> Self: ... @property @@ -76,8 +82,12 @@ class date: def ctime(self) -> str: ... if sys.version_info >= (3, 14): - @classmethod - def strptime(cls, date_string: str, format: str, /) -> Self: ... + if sys.version_info >= (3, 15): + @classmethod + def strptime(cls, string: str, format: str, /) -> Self: ... + else: + @classmethod + def strptime(cls, date_string: str, format: str, /) -> Self: ... # On <3.12, the name of the parameter in the pure-Python implementation # didn't match the name in the C implementation, @@ -102,12 +112,14 @@ class date: def __eq__(self, value: object, /) -> bool: ... def __add__(self, value: timedelta, /) -> Self: ... def __radd__(self, value: timedelta, /) -> Self: ... + @overload def __sub__(self, value: datetime, /) -> NoReturn: ... @overload def __sub__(self, value: Self, /) -> timedelta: ... @overload def __sub__(self, value: timedelta, /) -> Self: ... + def __hash__(self) -> int: ... def weekday(self) -> int: ... def isoweekday(self) -> int: ... @@ -147,12 +159,21 @@ class time: def __eq__(self, value: object, /) -> bool: ... def __hash__(self) -> int: ... def isoformat(self, timespec: str = "auto") -> str: ... - @classmethod - def fromisoformat(cls, time_string: str, /) -> Self: ... - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): @classmethod - def strptime(cls, date_string: str, format: str, /) -> Self: ... + def fromisoformat(cls, string: str, /) -> Self: ... + else: + @classmethod + def fromisoformat(cls, time_string: str, /) -> Self: ... + + if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): + @classmethod + def strptime(cls, string: str, format: str, /) -> Self: ... + else: + @classmethod + def strptime(cls, date_string: str, format: str, /) -> Self: ... # On <3.12, the name of the parameter in the pure-Python implementation # didn't match the name in the C implementation, @@ -224,14 +245,17 @@ class timedelta: def __abs__(self) -> timedelta: ... def __mul__(self, value: float, /) -> timedelta: ... def __rmul__(self, value: float, /) -> timedelta: ... + @overload def __floordiv__(self, value: timedelta, /) -> int: ... @overload def __floordiv__(self, value: int, /) -> timedelta: ... + @overload def __truediv__(self, value: timedelta, /) -> float: ... @overload def __truediv__(self, value: float, /) -> timedelta: ... + def __mod__(self, value: timedelta, /) -> timedelta: ... def __divmod__(self, value: timedelta, /) -> tuple[int, timedelta]: ... def __le__(self, value: timedelta, /) -> bool: ... @@ -291,6 +315,10 @@ class datetime(date): def utcnow(cls) -> Self: ... @classmethod def combine(cls, date: _Date, time: _Time, tzinfo: _TzInfo | None = ...) -> Self: ... + if sys.version_info >= (3, 15): + @classmethod + def fromisoformat(cls, string: str, /) -> Self: ... + def timestamp(self) -> float: ... def utctimetuple(self) -> struct_time: ... def date(self) -> _Date: ... @@ -327,8 +355,14 @@ class datetime(date): ) -> Self: ... def astimezone(self, tz: _TzInfo | None = None) -> Self: ... def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: ... - @classmethod - def strptime(cls, date_string: str, format: str, /) -> Self: ... + + if sys.version_info >= (3, 15): + @classmethod + def strptime(cls, string: str, format: str, /) -> Self: ... + else: + @classmethod + def strptime(cls, date_string: str, format: str, /) -> Self: ... + def utcoffset(self) -> timedelta | None: ... def tzname(self) -> str | None: ... def dst(self) -> timedelta | None: ... @@ -338,6 +372,7 @@ class datetime(date): def __gt__(self, value: datetime, /) -> bool: ... # type: ignore[override] def __eq__(self, value: object, /) -> bool: ... def __hash__(self) -> int: ... + @overload # type: ignore[override] def __sub__(self, value: Self, /) -> timedelta: ... @overload diff --git a/mypy/typeshed/stdlib/dbm/__init__.pyi b/mypy/typeshed/stdlib/dbm/__init__.pyi index 7cbb63cf2f06e..0871381e8ec0f 100644 --- a/mypy/typeshed/stdlib/dbm/__init__.pyi +++ b/mypy/typeshed/stdlib/dbm/__init__.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import StrOrBytesPath from collections.abc import Iterator, MutableMapping from types import TracebackType -from typing import Literal, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Literal, TypeAlias, type_check_only +from typing_extensions import Self __all__ = ["open", "whichdb", "error"] diff --git a/mypy/typeshed/stdlib/dbm/dumb.pyi b/mypy/typeshed/stdlib/dbm/dumb.pyi index 1c0b7756f2925..d5a769e6a1c25 100644 --- a/mypy/typeshed/stdlib/dbm/dumb.pyi +++ b/mypy/typeshed/stdlib/dbm/dumb.pyi @@ -2,7 +2,8 @@ import sys from _typeshed import StrOrBytesPath from collections.abc import Iterator, MutableMapping from types import TracebackType -from typing_extensions import Self, TypeAlias +from typing import TypeAlias +from typing_extensions import Self __all__ = ["error", "open"] @@ -17,6 +18,9 @@ error = OSError class _Database(MutableMapping[_KeyType, bytes]): def __init__(self, filebasename: str, mode: str, flag: str = "c") -> None: ... def sync(self) -> None: ... + if sys.version_info >= (3, 15): + def reorganize(self) -> None: ... + def iterkeys(self) -> Iterator[bytes]: ... # undocumented def close(self) -> None: ... def __getitem__(self, key: _KeyType) -> bytes: ... diff --git a/mypy/typeshed/stdlib/dbm/sqlite3.pyi b/mypy/typeshed/stdlib/dbm/sqlite3.pyi index e2fba93b20017..e7034cfde50d1 100644 --- a/mypy/typeshed/stdlib/dbm/sqlite3.pyi +++ b/mypy/typeshed/stdlib/dbm/sqlite3.pyi @@ -1,7 +1,8 @@ +import sys from _typeshed import ReadableBuffer, StrOrBytesPath, Unused from collections.abc import Generator, MutableMapping -from typing import Final, Literal -from typing_extensions import LiteralString, Self, TypeAlias +from typing import Final, Literal, TypeAlias +from typing_extensions import LiteralString, Self BUILD_TABLE: Final[LiteralString] GET_SIZE: Final[LiteralString] @@ -9,6 +10,8 @@ LOOKUP_KEY: Final[LiteralString] STORE_KV: Final[LiteralString] DELETE_KEY: Final[LiteralString] ITER_KEYS: Final[LiteralString] +if sys.version_info >= (3, 15): + REORGANIZE: Final[LiteralString] _SqliteData: TypeAlias = str | ReadableBuffer | int | float @@ -25,5 +28,7 @@ class _Database(MutableMapping[bytes, bytes]): def keys(self) -> list[bytes]: ... # type: ignore[override] def __enter__(self) -> Self: ... def __exit__(self, *args: Unused) -> None: ... + if sys.version_info >= (3, 15): + def reorganize(self) -> None: ... def open(filename: StrOrBytesPath, /, flag: Literal["r", "w", "c", "n"] = "r", mode: int = 0o666) -> _Database: ... diff --git a/mypy/typeshed/stdlib/decimal.pyi b/mypy/typeshed/stdlib/decimal.pyi index 2e06c2d1b724a..f16fa8aaae31f 100644 --- a/mypy/typeshed/stdlib/decimal.pyi +++ b/mypy/typeshed/stdlib/decimal.pyi @@ -26,11 +26,13 @@ from _decimal import ( ) from collections.abc import Container, Sequence from types import TracebackType -from typing import Any, ClassVar, Literal, NamedTuple, final, overload, type_check_only -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import Any, ClassVar, Literal, NamedTuple, TypeAlias, final, overload, type_check_only +from typing_extensions import Self, disjoint_base if sys.version_info >= (3, 14): from _decimal import IEEE_CONTEXT_MAX_BITS as IEEE_CONTEXT_MAX_BITS, IEEEContext as IEEEContext +if sys.version_info >= (3, 15): + from _decimal import SPEC_VERSION as SPEC_VERSION _Decimal: TypeAlias = Decimal | int _DecimalNew: TypeAlias = Decimal | float | str | tuple[int, Sequence[int], int] @@ -116,10 +118,12 @@ class Decimal: def imag(self) -> Decimal: ... def conjugate(self) -> Decimal: ... def __complex__(self) -> complex: ... + @overload def __round__(self) -> int: ... @overload def __round__(self, ndigits: int, /) -> Decimal: ... + def __floor__(self) -> int: ... def __ceil__(self) -> int: ... def fma(self, other: _Decimal, third: _Decimal, context: Context | None = None) -> Decimal: ... diff --git a/mypy/typeshed/stdlib/difflib.pyi b/mypy/typeshed/stdlib/difflib.pyi index 6efe68322bb65..a3bda7f9b2d0c 100644 --- a/mypy/typeshed/stdlib/difflib.pyi +++ b/mypy/typeshed/stdlib/difflib.pyi @@ -39,6 +39,7 @@ class SequenceMatcher(Generic[_T]): b: Sequence[str] = "", autojunk: bool = True, ) -> None: ... + def set_seqs(self, a: Sequence[_T], b: Sequence[_T]) -> None: ... def set_seq1(self, a: Sequence[_T]) -> None: ... def set_seq2(self, b: Sequence[_T]) -> None: ... @@ -69,16 +70,33 @@ else: def IS_LINE_JUNK(line: str, pat: Callable[[str], re.Match[str] | None] = ...) -> bool: ... def IS_CHARACTER_JUNK(ch: str, ws: str = " \t") -> bool: ... # ws is undocumented -def unified_diff( - a: Sequence[str], - b: Sequence[str], - fromfile: str = "", - tofile: str = "", - fromfiledate: str = "", - tofiledate: str = "", - n: int = 3, - lineterm: str = "\n", -) -> Iterator[str]: ... + +if sys.version_info >= (3, 15): + def unified_diff( + a: Sequence[str], + b: Sequence[str], + fromfile: str = "", + tofile: str = "", + fromfiledate: str = "", + tofiledate: str = "", + n: int = 3, + lineterm: str = "\n", + *, + color: bool = False, + ) -> Iterator[str]: ... + +else: + def unified_diff( + a: Sequence[str], + b: Sequence[str], + fromfile: str = "", + tofile: str = "", + fromfiledate: str = "", + tofiledate: str = "", + n: int = 3, + lineterm: str = "\n", + ) -> Iterator[str]: ... + def context_diff( a: Sequence[str], b: Sequence[str], diff --git a/mypy/typeshed/stdlib/dis.pyi b/mypy/typeshed/stdlib/dis.pyi index 52794a588ca83..0ad928934671b 100644 --- a/mypy/typeshed/stdlib/dis.pyi +++ b/mypy/typeshed/stdlib/dis.pyi @@ -3,7 +3,7 @@ import types from collections.abc import Callable, Iterator from opcode import * # `dis` re-exports it as a part of public API from typing import IO, Any, Final, NamedTuple, overload -from typing_extensions import Self, TypeAlias, deprecated, disjoint_base +from typing_extensions import Self, deprecated, disjoint_base __all__ = [ "code_info", @@ -41,7 +41,7 @@ else: # Strictly this should not have to include Callable, but mypy doesn't use FunctionType # for functions (python/mypy#3171) -_HaveCodeType: TypeAlias = types.MethodType | types.FunctionType | types.CodeType | type | Callable[..., Any] +_HaveCodeType = types.MethodType | types.FunctionType | types.CodeType | type | Callable[..., Any] if sys.version_info >= (3, 11): class Positions(NamedTuple): diff --git a/mypy/typeshed/stdlib/distutils/archive_util.pyi b/mypy/typeshed/stdlib/distutils/archive_util.pyi index 16684ff069568..5de23bad6bdb3 100644 --- a/mypy/typeshed/stdlib/distutils/archive_util.pyi +++ b/mypy/typeshed/stdlib/distutils/archive_util.pyi @@ -23,6 +23,7 @@ def make_archive( owner: str | None = None, group: str | None = None, ) -> str: ... + def make_tarball( base_name: str, base_dir: StrPath, diff --git a/mypy/typeshed/stdlib/distutils/ccompiler.pyi b/mypy/typeshed/stdlib/distutils/ccompiler.pyi index 5bff209807eef..83bbada6c4c48 100644 --- a/mypy/typeshed/stdlib/distutils/ccompiler.pyi +++ b/mypy/typeshed/stdlib/distutils/ccompiler.pyi @@ -1,8 +1,8 @@ from _typeshed import BytesPath, StrPath, Unused from collections.abc import Callable, Iterable, Sequence from distutils.file_util import _BytesPathT, _StrPathT -from typing import Literal, overload -from typing_extensions import TypeAlias, TypeVarTuple, Unpack +from typing import Literal, TypeAlias, overload +from typing_extensions import TypeVarTuple, Unpack _Macro: TypeAlias = tuple[str] | tuple[str, str | None] _Ts = TypeVarTuple("_Ts") @@ -148,29 +148,35 @@ class CCompiler: extra_preargs: list[str] | None = None, extra_postargs: list[str] | None = None, ) -> None: ... + @overload def executable_filename(self, basename: str, strip_dir: Literal[0, False] = 0, output_dir: StrPath = "") -> str: ... @overload def executable_filename(self, basename: StrPath, strip_dir: Literal[1, True], output_dir: StrPath = "") -> str: ... + def library_filename( self, libname: str, lib_type: str = "static", strip_dir: bool | Literal[0, 1] = 0, output_dir: StrPath = "" ) -> str: ... def object_filenames( self, source_filenames: Iterable[StrPath], strip_dir: bool | Literal[0, 1] = 0, output_dir: StrPath | None = "" ) -> list[str]: ... + @overload def shared_object_filename(self, basename: str, strip_dir: Literal[0, False] = 0, output_dir: StrPath = "") -> str: ... @overload def shared_object_filename(self, basename: StrPath, strip_dir: Literal[1, True], output_dir: StrPath = "") -> str: ... + def execute( self, func: Callable[[Unpack[_Ts]], Unused], args: tuple[Unpack[_Ts]], msg: str | None = None, level: int = 1 ) -> None: ... def spawn(self, cmd: Iterable[str]) -> None: ... def mkpath(self, name: str, mode: int = 0o777) -> None: ... + @overload def move_file(self, src: StrPath, dst: _StrPathT) -> _StrPathT | str: ... @overload def move_file(self, src: BytesPath, dst: _BytesPathT) -> _BytesPathT | bytes: ... + def announce(self, msg: str, level: int = 1) -> None: ... def warn(self, msg: str) -> None: ... def debug_print(self, msg: str) -> None: ... diff --git a/mypy/typeshed/stdlib/distutils/cmd.pyi b/mypy/typeshed/stdlib/distutils/cmd.pyi index 7f97bc3a2c9e0..35b991aba0883 100644 --- a/mypy/typeshed/stdlib/distutils/cmd.pyi +++ b/mypy/typeshed/stdlib/distutils/cmd.pyi @@ -49,6 +49,7 @@ class Command: def ensure_dirname(self, option: str) -> None: ... def get_command_name(self) -> str: ... def set_undefined_options(self, src_cmd: str, *option_pairs: tuple[str, str]) -> None: ... + # NOTE: This list comes directly from the distutils/command folder. Minus bdist_msi and bdist_wininst. @overload def get_finalized_command(self, command: Literal["bdist"], create: bool | Literal[0, 1] = 1) -> bdist: ... @@ -94,6 +95,7 @@ class Command: def get_finalized_command(self, command: Literal["upload"], create: bool | Literal[0, 1] = 1) -> upload: ... @overload def get_finalized_command(self, command: str, create: bool | Literal[0, 1] = 1) -> Command: ... + @overload def reinitialize_command(self, command: Literal["bdist"], reinit_subcommands: bool | Literal[0, 1] = 0) -> bdist: ... @overload @@ -154,6 +156,7 @@ class Command: def reinitialize_command(self, command: str, reinit_subcommands: bool | Literal[0, 1] = 0) -> Command: ... @overload def reinitialize_command(self, command: _CommandT, reinit_subcommands: bool | Literal[0, 1] = 0) -> _CommandT: ... + def run_command(self, command: str) -> None: ... def get_sub_commands(self) -> list[str]: ... def warn(self, msg: str) -> None: ... @@ -161,6 +164,7 @@ class Command: self, func: Callable[[Unpack[_Ts]], Unused], args: tuple[Unpack[_Ts]], msg: str | None = None, level: int = 1 ) -> None: ... def mkpath(self, name: str, mode: int = 0o777) -> None: ... + @overload def copy_file( self, @@ -181,6 +185,7 @@ class Command: link: str | None = None, level: Unused = 1, ) -> tuple[_BytesPathT | bytes, bool]: ... + def copy_tree( self, infile: StrPath, @@ -190,11 +195,14 @@ class Command: preserve_symlinks: bool | Literal[0, 1] = 0, level: Unused = 1, ) -> list[str]: ... + @overload def move_file(self, src: StrPath, dst: _StrPathT, level: Unused = 1) -> _StrPathT | str: ... @overload def move_file(self, src: BytesPath, dst: _BytesPathT, level: Unused = 1) -> _BytesPathT | bytes: ... + def spawn(self, cmd: Iterable[str], search_path: bool | Literal[0, 1] = 1, level: Unused = 1) -> None: ... + @overload def make_archive( self, @@ -215,6 +223,7 @@ class Command: owner: str | None = None, group: str | None = None, ) -> str: ... + def make_file( self, infiles: str | list[str] | tuple[str, ...], diff --git a/mypy/typeshed/stdlib/distutils/command/__init__.pyi b/mypy/typeshed/stdlib/distutils/command/__init__.pyi index 4d7372858af34..856c5eb8b44ad 100644 --- a/mypy/typeshed/stdlib/distutils/command/__init__.pyi +++ b/mypy/typeshed/stdlib/distutils/command/__init__.pyi @@ -1,5 +1,3 @@ -import sys - from . import ( bdist, bdist_dumb, @@ -41,8 +39,3 @@ __all__ = [ "check", "upload", ] - -if sys.version_info < (3, 10): - from . import bdist_wininst - - __all__ += ["bdist_wininst"] diff --git a/mypy/typeshed/stdlib/distutils/command/bdist_wininst.pyi b/mypy/typeshed/stdlib/distutils/command/bdist_wininst.pyi deleted file mode 100644 index cf333bc5400dd..0000000000000 --- a/mypy/typeshed/stdlib/distutils/command/bdist_wininst.pyi +++ /dev/null @@ -1,16 +0,0 @@ -from _typeshed import StrOrBytesPath -from distutils.cmd import Command -from typing import ClassVar - -class bdist_wininst(Command): - description: ClassVar[str] - user_options: ClassVar[list[tuple[str, str | None, str]]] - boolean_options: ClassVar[list[str]] - - def initialize_options(self) -> None: ... - def finalize_options(self) -> None: ... - def run(self) -> None: ... - def get_inidata(self) -> str: ... - def create_exe(self, arcname: StrOrBytesPath, fullname: str, bitmap: StrOrBytesPath | None = None) -> None: ... - def get_installer_filename(self, fullname: str) -> str: ... - def get_exe_bytes(self) -> bytes: ... diff --git a/mypy/typeshed/stdlib/distutils/command/check.pyi b/mypy/typeshed/stdlib/distutils/command/check.pyi index 2c807fd2c4396..f2034e6555fcc 100644 --- a/mypy/typeshed/stdlib/distutils/command/check.pyi +++ b/mypy/typeshed/stdlib/distutils/command/check.pyi @@ -1,6 +1,5 @@ from _typeshed import Incomplete -from typing import Any, ClassVar, Final, Literal -from typing_extensions import TypeAlias +from typing import Any, ClassVar, Final, Literal, TypeAlias from ..cmd import Command diff --git a/mypy/typeshed/stdlib/distutils/command/install.pyi b/mypy/typeshed/stdlib/distutils/command/install.pyi index 1714e01a2c284..7e11cf257c2c3 100644 --- a/mypy/typeshed/stdlib/distutils/command/install.pyi +++ b/mypy/typeshed/stdlib/distutils/command/install.pyi @@ -1,4 +1,3 @@ -import sys from _typeshed import Incomplete from collections.abc import Callable from typing import Any, ClassVar, Final, Literal @@ -10,9 +9,6 @@ HAS_USER_SITE: Final[bool] SCHEME_KEYS: Final[tuple[Literal["purelib"], Literal["platlib"], Literal["headers"], Literal["scripts"], Literal["data"]]] INSTALL_SCHEMES: Final[dict[str, dict[str, str]]] -if sys.version_info < (3, 10): - WINDOWS_SCHEME: Final[dict[str, str]] - class install(Command): description: str user_options: ClassVar[list[tuple[str, str | None, str]]] diff --git a/mypy/typeshed/stdlib/distutils/dist.pyi b/mypy/typeshed/stdlib/distutils/dist.pyi index 412b94131b54e..58650e853cce6 100644 --- a/mypy/typeshed/stdlib/distutils/dist.pyi +++ b/mypy/typeshed/stdlib/distutils/dist.pyi @@ -22,8 +22,7 @@ from distutils.command.register import register from distutils.command.sdist import sdist from distutils.command.upload import upload from re import Pattern -from typing import IO, ClassVar, Literal, TypeVar, overload -from typing_extensions import TypeAlias +from typing import IO, ClassVar, Literal, TypeAlias, TypeVar, overload command_re: Pattern[str] @@ -122,6 +121,7 @@ class Distribution: def print_commands(self) -> None: ... def get_command_list(self): ... def get_command_packages(self): ... + # NOTE: This list comes directly from the distutils/command folder. Minus bdist_msi and bdist_wininst. @overload def get_command_obj(self, command: Literal["bdist"], create: Literal[1, True] = 1) -> bdist: ... @@ -168,6 +168,7 @@ class Distribution: # Not replicating the overloads for "Command | None", user may use "isinstance" @overload def get_command_obj(self, command: str, create: Literal[0, False]) -> Command | None: ... + @overload def get_command_class(self, command: Literal["bdist"]) -> type[bdist]: ... @overload @@ -210,6 +211,7 @@ class Distribution: def get_command_class(self, command: Literal["upload"]) -> type[upload]: ... @overload def get_command_class(self, command: str) -> type[Command]: ... + @overload def reinitialize_command(self, command: Literal["bdist"], reinit_subcommands: bool = False) -> bdist: ... @overload @@ -256,6 +258,7 @@ class Distribution: def reinitialize_command(self, command: str, reinit_subcommands: bool = False) -> Command: ... @overload def reinitialize_command(self, command: _CommandT, reinit_subcommands: bool = False) -> _CommandT: ... + def announce(self, msg, level: int = 2) -> None: ... def run_commands(self) -> None: ... def run_command(self, command: str) -> None: ... diff --git a/mypy/typeshed/stdlib/distutils/fancy_getopt.pyi b/mypy/typeshed/stdlib/distutils/fancy_getopt.pyi index f3fa2a1255a6d..676ce1bea313e 100644 --- a/mypy/typeshed/stdlib/distutils/fancy_getopt.pyi +++ b/mypy/typeshed/stdlib/distutils/fancy_getopt.pyi @@ -1,8 +1,7 @@ from collections.abc import Iterable, Mapping from getopt import _SliceableT, _StrSequenceT_co from re import Pattern -from typing import Any, Final, overload -from typing_extensions import TypeAlias +from typing import Any, Final, TypeAlias, overload _Option: TypeAlias = tuple[str, str | None, str] @@ -13,6 +12,7 @@ longopt_xlate: Final[dict[int, int]] class FancyGetopt: def __init__(self, option_table: list[_Option] | None = None) -> None: ... + # TODO: kinda wrong, `getopt(object=object())` is invalid @overload def getopt( @@ -22,6 +22,7 @@ class FancyGetopt: def getopt( self, args: _SliceableT[_StrSequenceT_co] | None, object: Any ) -> _StrSequenceT_co: ... # object is an arbitrary non-slotted object + def get_option_order(self) -> list[tuple[str, str]]: ... def generate_help(self, header: str | None = None) -> list[str]: ... diff --git a/mypy/typeshed/stdlib/distutils/file_util.pyi b/mypy/typeshed/stdlib/distutils/file_util.pyi index c763f91a958d7..9d5bf5080b058 100644 --- a/mypy/typeshed/stdlib/distutils/file_util.pyi +++ b/mypy/typeshed/stdlib/distutils/file_util.pyi @@ -27,6 +27,7 @@ def copy_file( verbose: bool | Literal[0, 1] = 1, dry_run: bool | Literal[0, 1] = 0, ) -> tuple[_BytesPathT | bytes, bool]: ... + @overload def move_file( src: StrPath, dst: _StrPathT, verbose: bool | Literal[0, 1] = 1, dry_run: bool | Literal[0, 1] = 0 @@ -35,4 +36,5 @@ def move_file( def move_file( src: BytesPath, dst: _BytesPathT, verbose: bool | Literal[0, 1] = 1, dry_run: bool | Literal[0, 1] = 0 ) -> _BytesPathT | bytes: ... + def write_file(filename: StrOrBytesPath, contents: Iterable[str]) -> None: ... diff --git a/mypy/typeshed/stdlib/distutils/filelist.pyi b/mypy/typeshed/stdlib/distutils/filelist.pyi index 607a78a1fbaca..c3347fe7d1d20 100644 --- a/mypy/typeshed/stdlib/distutils/filelist.pyi +++ b/mypy/typeshed/stdlib/distutils/filelist.pyi @@ -15,6 +15,7 @@ class FileList: def sort(self) -> None: ... def remove_duplicates(self) -> None: ... def process_template_line(self, line: str) -> None: ... + @overload def include_pattern( self, pattern: str, anchor: bool | Literal[0, 1] = 1, prefix: str | None = None, is_regex: Literal[0, False] = 0 @@ -29,6 +30,7 @@ class FileList: prefix: str | None = None, is_regex: bool | Literal[0, 1] = 0, ) -> bool: ... + @overload def exclude_pattern( self, pattern: str, anchor: bool | Literal[0, 1] = 1, prefix: str | None = None, is_regex: Literal[0, False] = 0 @@ -46,6 +48,7 @@ class FileList: def findall(dir: str = ".") -> list[str]: ... def glob_to_re(pattern: str) -> str: ... + @overload def translate_pattern( pattern: str, anchor: bool | Literal[0, 1] = 1, prefix: str | None = None, is_regex: Literal[False, 0] = 0 diff --git a/mypy/typeshed/stdlib/distutils/sysconfig.pyi b/mypy/typeshed/stdlib/distutils/sysconfig.pyi index 4a9c45eb562a4..7c8e0e7b149e0 100644 --- a/mypy/typeshed/stdlib/distutils/sysconfig.pyi +++ b/mypy/typeshed/stdlib/distutils/sysconfig.pyi @@ -1,4 +1,3 @@ -import sys from collections.abc import Mapping from distutils.ccompiler import CCompiler from typing import Final, Literal, overload @@ -12,15 +11,18 @@ project_base: Final[str] python_build: Final[bool] def expand_makefile_vars(s: str, vars: Mapping[str, str]) -> str: ... + @overload @deprecated("SO is deprecated, use EXT_SUFFIX. Support is removed in Python 3.11") def get_config_var(name: Literal["SO"]) -> int | str | None: ... @overload def get_config_var(name: str) -> int | str | None: ... + @overload def get_config_vars() -> dict[str, str | int]: ... @overload def get_config_vars(arg: str, /, *args: str) -> list[str | int]: ... + def get_config_h_filename() -> str: ... def get_makefile_filename() -> str: ... def get_python_inc(plat_specific: bool | Literal[0, 1] = 0, prefix: str | None = None) -> str: ... @@ -28,6 +30,3 @@ def get_python_lib( plat_specific: bool | Literal[0, 1] = 0, standard_lib: bool | Literal[0, 1] = 0, prefix: str | None = None ) -> str: ... def customize_compiler(compiler: CCompiler) -> None: ... - -if sys.version_info < (3, 10): - def get_python_version() -> str: ... diff --git a/mypy/typeshed/stdlib/doctest.pyi b/mypy/typeshed/stdlib/doctest.pyi index 1bb96e1a77868..32c68e3f2c7b3 100644 --- a/mypy/typeshed/stdlib/doctest.pyi +++ b/mypy/typeshed/stdlib/doctest.pyi @@ -3,8 +3,8 @@ import types import unittest from _typeshed import ExcInfo from collections.abc import Callable -from typing import Any, Final, NamedTuple, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, Final, NamedTuple, TypeAlias, type_check_only +from typing_extensions import Self __all__ = [ "register_optionflag", diff --git a/mypy/typeshed/stdlib/email/__init__.pyi b/mypy/typeshed/stdlib/email/__init__.pyi index 53f8c350b01e3..aabdca32d506d 100644 --- a/mypy/typeshed/stdlib/email/__init__.pyi +++ b/mypy/typeshed/stdlib/email/__init__.pyi @@ -2,8 +2,7 @@ from collections.abc import Callable from email._policybase import _MessageT from email.message import Message from email.policy import Policy -from typing import IO, overload -from typing_extensions import TypeAlias +from typing import IO, TypeAlias, overload # At runtime, listing submodules in __all__ without them being imported is # valid, and causes them to be included in a star import. See #6523 @@ -38,6 +37,7 @@ def message_from_string(s: str) -> Message: ... def message_from_string(s: str, _class: Callable[[], _MessageT]) -> _MessageT: ... @overload def message_from_string(s: str, _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... + @overload def message_from_bytes(s: bytes | bytearray) -> Message: ... @overload @@ -46,12 +46,14 @@ def message_from_bytes(s: bytes | bytearray, _class: Callable[[], _MessageT]) -> def message_from_bytes( s: bytes | bytearray, _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT] ) -> _MessageT: ... + @overload def message_from_file(fp: IO[str]) -> Message: ... @overload def message_from_file(fp: IO[str], _class: Callable[[], _MessageT]) -> _MessageT: ... @overload def message_from_file(fp: IO[str], _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... + @overload def message_from_binary_file(fp: IO[bytes]) -> Message: ... @overload diff --git a/mypy/typeshed/stdlib/email/_header_value_parser.pyi b/mypy/typeshed/stdlib/email/_header_value_parser.pyi index a6d7c48d69cd8..e75e7ba1cf06c 100644 --- a/mypy/typeshed/stdlib/email/_header_value_parser.pyi +++ b/mypy/typeshed/stdlib/email/_header_value_parser.pyi @@ -17,18 +17,17 @@ TOKEN_ENDS: Final[set[str]] ASPECIALS: Final[set[str]] ATTRIBUTE_ENDS: Final[set[str]] EXTENDED_ATTRIBUTE_ENDS: Final[set[str]] -# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.10.15, 3.11.10, 3.12.5 NLSET: Final[set[str]] -# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.10.15, 3.11.10, 3.12.5 SPECIALSNL: Final[set[str]] -# Added in Python 3.9.23, 3.10.17, 3.11.12, 3.12.9, 3.13.2 +# Added in Python 3.10.17, 3.11.12, 3.12.9, 3.13.2 def make_quoted_pairs(value: Any) -> str: ... def quote_string(value: Any) -> str: ... -if sys.version_info >= (3, 10): - # Added in Python 3.10.20, 3.11.15, 3.12.13, 3.13.12, 3.14.3 - def make_parenthesis_pairs(value: Any) -> str: ... +# Added in Python 3.10.20, 3.11.15, 3.12.13, 3.13.12, 3.14.3 +def make_parenthesis_pairs(value: Any) -> str: ... rfc2047_matcher: Final[Pattern[str]] diff --git a/mypy/typeshed/stdlib/email/charset.pyi b/mypy/typeshed/stdlib/email/charset.pyi index e1930835bbd11..353cdeb0b9ddd 100644 --- a/mypy/typeshed/stdlib/email/charset.pyi +++ b/mypy/typeshed/stdlib/email/charset.pyi @@ -27,10 +27,12 @@ class Charset: def get_output_charset(self) -> str | None: ... def header_encode(self, string: str) -> str: ... def header_encode_lines(self, string: str, maxlengths: Iterator[int]) -> list[str | None]: ... + @overload def body_encode(self, string: None) -> None: ... @overload def body_encode(self, string: str | bytes) -> str: ... + __hash__: ClassVar[None] # type: ignore[assignment] def __eq__(self, other: object) -> bool: ... def __ne__(self, value: object, /) -> bool: ... diff --git a/mypy/typeshed/stdlib/email/errors.pyi b/mypy/typeshed/stdlib/email/errors.pyi index b501a58665560..4da60250965e5 100644 --- a/mypy/typeshed/stdlib/email/errors.pyi +++ b/mypy/typeshed/stdlib/email/errors.pyi @@ -1,5 +1,3 @@ -import sys - class MessageError(Exception): ... class MessageParseError(MessageError): ... class HeaderParseError(MessageParseError): ... @@ -37,6 +35,4 @@ class NonPrintableDefect(HeaderDefect): class ObsoleteHeaderDefect(HeaderDefect): ... class NonASCIILocalPartDefect(HeaderDefect): ... - -if sys.version_info >= (3, 10): - class InvalidDateDefect(HeaderDefect): ... +class InvalidDateDefect(HeaderDefect): ... diff --git a/mypy/typeshed/stdlib/email/feedparser.pyi b/mypy/typeshed/stdlib/email/feedparser.pyi index d9279e9cd996d..ec92eef678861 100644 --- a/mypy/typeshed/stdlib/email/feedparser.pyi +++ b/mypy/typeshed/stdlib/email/feedparser.pyi @@ -11,6 +11,7 @@ class FeedParser(Generic[_MessageT]): def __init__(self: FeedParser[Message], _factory: None = None, *, policy: Policy[Message] = ...) -> None: ... @overload def __init__(self, _factory: Callable[[], _MessageT], *, policy: Policy[_MessageT] = ...) -> None: ... + def feed(self, data: str) -> None: ... def close(self) -> _MessageT: ... @@ -19,4 +20,5 @@ class BytesFeedParser(FeedParser[_MessageT]): def __init__(self: BytesFeedParser[Message], _factory: None = None, *, policy: Policy[Message] = ...) -> None: ... @overload def __init__(self, _factory: Callable[[], _MessageT], *, policy: Policy[_MessageT] = ...) -> None: ... + def feed(self, data: bytes | bytearray) -> None: ... # type: ignore[override] diff --git a/mypy/typeshed/stdlib/email/generator.pyi b/mypy/typeshed/stdlib/email/generator.pyi index d30e686299fab..c2a9bb0921d78 100644 --- a/mypy/typeshed/stdlib/email/generator.pyi +++ b/mypy/typeshed/stdlib/email/generator.pyi @@ -12,6 +12,7 @@ _MessageT = TypeVar("_MessageT", bound=Message[Any, Any], default=Any) class Generator(Generic[_MessageT]): maxheaderlen: int | None policy: Policy[_MessageT] | None + @overload def __init__( self: Generator[Any], # The Policy of the message is used. @@ -30,6 +31,7 @@ class Generator(Generic[_MessageT]): *, policy: Policy[_MessageT], ) -> None: ... + def write(self, s: str) -> None: ... def flatten(self, msg: _MessageT, unixfrom: bool = False, linesep: str | None = None) -> None: ... def clone(self, fp: SupportsWrite[str]) -> Self: ... diff --git a/mypy/typeshed/stdlib/email/iterators.pyi b/mypy/typeshed/stdlib/email/iterators.pyi index d964d68438336..b9a8de835401e 100644 --- a/mypy/typeshed/stdlib/email/iterators.pyi +++ b/mypy/typeshed/stdlib/email/iterators.pyi @@ -1,11 +1,14 @@ from _typeshed import SupportsWrite from collections.abc import Iterator from email.message import Message +from typing import TypeVar + +_T = TypeVar("_T", bound=Message) __all__ = ["body_line_iterator", "typed_subpart_iterator", "walk"] def body_line_iterator(msg: Message, decode: bool = False) -> Iterator[str]: ... -def typed_subpart_iterator(msg: Message, maintype: str = "text", subtype: str | None = None) -> Iterator[str]: ... +def typed_subpart_iterator(msg: _T, maintype: str = "text", subtype: str | None = None) -> Iterator[_T]: ... def walk(self: Message) -> Iterator[Message]: ... # We include the seemingly private function because it is documented in the stdlib documentation. diff --git a/mypy/typeshed/stdlib/email/message.pyi b/mypy/typeshed/stdlib/email/message.pyi index 08ba88b4ee6da..784c2cace4255 100644 --- a/mypy/typeshed/stdlib/email/message.pyi +++ b/mypy/typeshed/stdlib/email/message.pyi @@ -5,8 +5,8 @@ from email.charset import Charset from email.contentmanager import ContentManager from email.errors import MessageDefect from email.policy import Policy -from typing import Any, Generic, Literal, Protocol, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, Generic, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self __all__ = ["Message", "EmailMessage"] @@ -44,6 +44,7 @@ class Message(Generic[_HeaderT_co, _HeaderParamT_contra]): def set_unixfrom(self, unixfrom: str) -> None: ... def get_unixfrom(self) -> str | None: ... def attach(self, payload: _PayloadType) -> None: ... + # `i: int` without a multipart payload results in an error # `| MaybeNone` acts like `| Any`: can be None for cleared or unset payload, but annoying to check @overload # multipart @@ -56,6 +57,7 @@ class Message(Generic[_HeaderT_co, _HeaderParamT_contra]): def get_payload(self, i: None = None, *, decode: Literal[True]) -> _EncodedPayloadType | MaybeNone: ... @overload # not multipart, IDEM but w/o kwarg def get_payload(self, i: None, decode: Literal[True]) -> _EncodedPayloadType | MaybeNone: ... + # If `charset=None` and payload supports both `encode` AND `decode`, # then an invalid payload could be passed, but this is unlikely # Not[_SupportsEncodeToPayload] @@ -69,6 +71,7 @@ class Message(Generic[_HeaderT_co, _HeaderParamT_contra]): payload: _SupportsEncodeToPayload | _SupportsDecodeToPayload | _PayloadType | _MultipartPayloadType, charset: Charset | str, ) -> None: ... + def set_charset(self, charset: _CharsetType) -> None: ... def get_charset(self) -> _CharsetType: ... def __len__(self) -> int: ... @@ -84,14 +87,17 @@ class Message(Generic[_HeaderT_co, _HeaderParamT_contra]): def keys(self) -> list[str]: ... def values(self) -> list[_HeaderT_co]: ... def items(self) -> list[tuple[str, _HeaderT_co]]: ... + @overload def get(self, name: str, failobj: None = None) -> _HeaderT_co | None: ... @overload def get(self, name: str, failobj: _T) -> _HeaderT_co | _T: ... + @overload def get_all(self, name: str, failobj: None = None) -> list[_HeaderT_co] | None: ... @overload def get_all(self, name: str, failobj: _T) -> list[_HeaderT_co] | _T: ... + def add_header(self, _name: str, _value: str, **_params: _ParamsType) -> None: ... def replace_header(self, _name: str, _value: _HeaderParamT_contra) -> None: ... def get_content_type(self) -> str: ... @@ -99,37 +105,46 @@ class Message(Generic[_HeaderT_co, _HeaderParamT_contra]): def get_content_subtype(self) -> str: ... def get_default_type(self) -> str: ... def set_default_type(self, ctype: str) -> None: ... + @overload def get_params( self, failobj: None = None, header: str = "content-type", unquote: bool = True ) -> list[tuple[str, str]] | None: ... @overload def get_params(self, failobj: _T, header: str = "content-type", unquote: bool = True) -> list[tuple[str, str]] | _T: ... + @overload def get_param( self, param: str, failobj: None = None, header: str = "content-type", unquote: bool = True ) -> _ParamType | None: ... @overload def get_param(self, param: str, failobj: _T, header: str = "content-type", unquote: bool = True) -> _ParamType | _T: ... + def del_param(self, param: str, header: str = "content-type", requote: bool = True) -> None: ... def set_type(self, type: str, header: str = "Content-Type", requote: bool = True) -> None: ... + @overload def get_filename(self, failobj: None = None) -> str | None: ... @overload def get_filename(self, failobj: _T) -> str | _T: ... + @overload def get_boundary(self, failobj: None = None) -> str | None: ... @overload def get_boundary(self, failobj: _T) -> str | _T: ... + def set_boundary(self, boundary: str) -> None: ... + @overload def get_content_charset(self) -> str | None: ... @overload def get_content_charset(self, failobj: _T) -> str | _T: ... + @overload def get_charsets(self, failobj: None = None) -> list[str | None]: ... @overload def get_charsets(self, failobj: _T) -> list[str | _T]: ... + def walk(self) -> Generator[Self]: ... def get_content_disposition(self) -> str | None: ... def as_string(self, unixfrom: bool = False, maxheaderlen: int = 0, policy: Policy[Any] | None = None) -> str: ... diff --git a/mypy/typeshed/stdlib/email/parser.pyi b/mypy/typeshed/stdlib/email/parser.pyi index a4924a6cbd88f..f1b418ee30a02 100644 --- a/mypy/typeshed/stdlib/email/parser.pyi +++ b/mypy/typeshed/stdlib/email/parser.pyi @@ -16,6 +16,7 @@ class Parser(Generic[_MessageT]): def __init__(self, _class: None = None, *, policy: Policy[_MessageT]) -> None: ... @overload def __init__(self, _class: Callable[[], _MessageT] | None, *, policy: Policy[_MessageT] = ...) -> None: ... + def parse(self, fp: SupportsRead[str], headersonly: bool = False) -> _MessageT: ... def parsestr(self, text: str, headersonly: bool = False) -> _MessageT: ... @@ -25,12 +26,14 @@ class HeaderParser(Parser[_MessageT]): class BytesParser(Generic[_MessageT]): parser: Parser[_MessageT] + @overload def __init__(self: BytesParser[Message[str, str]], _class: None = None) -> None: ... @overload def __init__(self, _class: None = None, *, policy: Policy[_MessageT]) -> None: ... @overload def __init__(self, _class: Callable[[], _MessageT], *, policy: Policy[_MessageT] = ...) -> None: ... + def parse(self, fp: _WrappedBuffer, headersonly: bool = False) -> _MessageT: ... def parsebytes(self, text: bytes | bytearray, headersonly: bool = False) -> _MessageT: ... diff --git a/mypy/typeshed/stdlib/email/policy.pyi b/mypy/typeshed/stdlib/email/policy.pyi index 35c999919eede..6b719f3c93fa0 100644 --- a/mypy/typeshed/stdlib/email/policy.pyi +++ b/mypy/typeshed/stdlib/email/policy.pyi @@ -12,6 +12,7 @@ class EmailPolicy(Policy[_MessageT]): refold_source: str header_factory: Callable[[str, Any], Any] content_manager: ContentManager + @overload def __init__( self: EmailPolicy[EmailMessage], @@ -46,6 +47,7 @@ class EmailPolicy(Policy[_MessageT]): header_factory: Callable[[str, str], str] = ..., content_manager: ContentManager = ..., ) -> None: ... + def header_source_parse(self, sourcelines: list[str]) -> tuple[str, str]: ... def header_store_parse(self, name: str, value: Any) -> tuple[str, Any]: ... def header_fetch_parse(self, name: str, value: str) -> Any: ... diff --git a/mypy/typeshed/stdlib/email/utils.pyi b/mypy/typeshed/stdlib/email/utils.pyi index cece2f2a11190..6b47950a2ef8f 100644 --- a/mypy/typeshed/stdlib/email/utils.pyi +++ b/mypy/typeshed/stdlib/email/utils.pyi @@ -4,8 +4,8 @@ from _typeshed import Unused from collections.abc import Iterable from email import _ParamType from email.charset import Charset -from typing import overload -from typing_extensions import TypeAlias, deprecated +from typing import TypeAlias, overload +from typing_extensions import deprecated __all__ = [ "collapse_rfc2231_value", @@ -36,14 +36,17 @@ def formataddr(pair: tuple[str | None, str], charset: str | Charset = "utf-8") - # `strict` parameter added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 def getaddresses(fieldvalues: Iterable[str], *, strict: bool = True) -> list[tuple[str, str]]: ... + @overload def parsedate(data: None) -> None: ... @overload def parsedate(data: str) -> tuple[int, int, int, int, int, int, int, int, int] | None: ... + @overload def parsedate_tz(data: None) -> None: ... @overload def parsedate_tz(data: str) -> _PDTZ | None: ... + def parsedate_to_datetime(data: str) -> datetime.datetime: ... def mktime_tz(data: _PDTZ) -> int: ... def formatdate(timeval: float | None = None, localtime: bool = False, usegmt: bool = False) -> str: ... diff --git a/mypy/typeshed/stdlib/enum.pyi b/mypy/typeshed/stdlib/enum.pyi index f9b53e1d45f16..1d216b90b6b7b 100644 --- a/mypy/typeshed/stdlib/enum.pyi +++ b/mypy/typeshed/stdlib/enum.pyi @@ -4,8 +4,8 @@ import types from _typeshed import SupportsKeysAndGetItem, Unused from builtins import property as _builtins_property from collections.abc import Callable, Iterable, Iterator, Mapping -from typing import Any, Final, Generic, Literal, SupportsIndex, TypeVar, overload -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import Any, Final, Generic, Literal, SupportsIndex, TypeAlias, TypeVar, overload +from typing_extensions import Self, disjoint_base __all__ = ["EnumMeta", "Enum", "IntEnum", "Flag", "IntFlag", "auto", "unique"] @@ -37,6 +37,8 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 13): __all__ += ["EnumDict"] +if sys.version_info >= (3, 15): + __all__ += ["show_flag_values", "bin"] _EnumMemberT = TypeVar("_EnumMemberT") _EnumerationT = TypeVar("_EnumerationT", bound=type[Enum]) @@ -81,6 +83,7 @@ class _EnumDict(dict[str, Any]): def update(self, members: SupportsKeysAndGetItem[str, Any], **more_members: Any) -> None: ... @overload def update(self, members: Iterable[tuple[str, Any]], **more_members: Any) -> None: ... + if sys.version_info >= (3, 13): @property def member_names(self) -> list[str]: ... @@ -114,10 +117,8 @@ class EnumMeta(type): def __contains__(self: type[Any], value: object) -> bool: ... elif sys.version_info >= (3, 11): def __contains__(self: type[Any], member: object) -> bool: ... - elif sys.version_info >= (3, 10): - def __contains__(self: type[Any], obj: object) -> bool: ... else: - def __contains__(self: type[Any], member: object) -> bool: ... + def __contains__(self: type[Any], obj: object) -> bool: ... def __getitem__(self: type[_EnumMemberT], name: str) -> _EnumMemberT: ... @_builtins_property diff --git a/mypy/typeshed/stdlib/faulthandler.pyi b/mypy/typeshed/stdlib/faulthandler.pyi index 17d4eef69af76..6999933c43b98 100644 --- a/mypy/typeshed/stdlib/faulthandler.pyi +++ b/mypy/typeshed/stdlib/faulthandler.pyi @@ -3,16 +3,39 @@ from _typeshed import FileDescriptorLike def cancel_dump_traceback_later() -> None: ... def disable() -> None: ... -def dump_traceback(file: FileDescriptorLike = sys.stderr, all_threads: bool = True) -> None: ... + +if sys.version_info >= (3, 15): + def dump_traceback( + file: FileDescriptorLike = sys.stderr, all_threads: bool = True, *, max_threads: int | None = None + ) -> None: ... + +else: + def dump_traceback(file: FileDescriptorLike = sys.stderr, all_threads: bool = True) -> None: ... if sys.version_info >= (3, 14): def dump_c_stack(file: FileDescriptorLike = sys.stderr) -> None: ... -def dump_traceback_later( - timeout: float, repeat: bool = False, file: FileDescriptorLike = sys.stderr, exit: bool = False -) -> None: ... +if sys.version_info >= (3, 15): + def dump_traceback_later( + timeout: float, + repeat: bool = False, + file: FileDescriptorLike = sys.stderr, + exit: bool = False, + *, + max_threads: int | None = None, + ) -> None: ... -if sys.version_info >= (3, 14): +else: + def dump_traceback_later( + timeout: float, repeat: bool = False, file: FileDescriptorLike = sys.stderr, exit: bool = False + ) -> None: ... + +if sys.version_info >= (3, 15): + def enable( + file: FileDescriptorLike = sys.stderr, all_threads: bool = True, c_stack: bool = True, *, max_threads: int | None = None + ) -> None: ... + +elif sys.version_info >= (3, 14): def enable(file: FileDescriptorLike = sys.stderr, all_threads: bool = True, c_stack: bool = True) -> None: ... else: @@ -21,5 +44,18 @@ else: def is_enabled() -> bool: ... if sys.platform != "win32": - def register(signum: int, file: FileDescriptorLike = sys.stderr, all_threads: bool = True, chain: bool = False) -> None: ... + if sys.version_info >= (3, 15): + def register( + signum: int, + file: FileDescriptorLike = sys.stderr, + all_threads: bool = True, + chain: bool = False, + *, + max_threads: int | None = None, + ) -> None: ... + else: + def register( + signum: int, file: FileDescriptorLike = sys.stderr, all_threads: bool = True, chain: bool = False + ) -> None: ... + def unregister(signum: int, /) -> None: ... diff --git a/mypy/typeshed/stdlib/fcntl.pyi b/mypy/typeshed/stdlib/fcntl.pyi index 5a3e89b0c6766..c17f31c4bebe8 100644 --- a/mypy/typeshed/stdlib/fcntl.pyi +++ b/mypy/typeshed/stdlib/fcntl.pyi @@ -45,11 +45,8 @@ if sys.platform != "win32": F_OFD_GETLK: Final[int] F_OFD_SETLK: Final[int] F_OFD_SETLKW: Final[int] - - if sys.version_info >= (3, 10): - F_GETPIPE_SZ: Final[int] - F_SETPIPE_SZ: Final[int] - + F_GETPIPE_SZ: Final[int] + F_SETPIPE_SZ: Final[int] DN_ACCESS: Final[int] DN_ATTRIB: Final[int] DN_CREATE: Final[int] @@ -140,6 +137,7 @@ if sys.platform != "win32": def fcntl(fd: FileDescriptorLike, cmd: int, arg: int = 0, /) -> int: ... @overload def fcntl(fd: FileDescriptorLike, cmd: int, arg: str | ReadOnlyBuffer, /) -> bytes: ... + # If arg is an int, return int @overload def ioctl(fd: FileDescriptorLike, request: int, arg: int = 0, mutate_flag: bool = True, /) -> int: ... @@ -154,5 +152,6 @@ if sys.platform != "win32": def ioctl(fd: FileDescriptorLike, request: int, arg: WriteableBuffer, mutate_flag: Literal[False], /) -> bytes: ... @overload def ioctl(fd: FileDescriptorLike, request: int, arg: Buffer, mutate_flag: bool = True, /) -> Any: ... + def flock(fd: FileDescriptorLike, operation: int, /) -> None: ... def lockf(fd: FileDescriptorLike, cmd: int, len: int = 0, start: int = 0, whence: int = 0, /) -> Any: ... diff --git a/mypy/typeshed/stdlib/fileinput.pyi b/mypy/typeshed/stdlib/fileinput.pyi index 95164de2f0107..db9c228f5f28e 100644 --- a/mypy/typeshed/stdlib/fileinput.pyi +++ b/mypy/typeshed/stdlib/fileinput.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import AnyStr_co, StrOrBytesPath from collections.abc import Callable, Iterable, Iterator from types import GenericAlias, TracebackType -from typing import IO, Any, AnyStr, Literal, Protocol, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated +from typing import IO, Any, AnyStr, Literal, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import Self, deprecated __all__ = [ "input", @@ -30,10 +30,55 @@ class _HasReadlineAndFileno(Protocol[AnyStr_co]): def readline(self) -> AnyStr_co: ... def fileno(self) -> int: ... -if sys.version_info >= (3, 10): +# encoding and errors are added +@overload +def input( + files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, + inplace: bool = False, + backup: str = "", + *, + mode: _TextMode = "r", + openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[str]] | None = None, + encoding: str | None = None, + errors: str | None = None, +) -> FileInput[str]: ... +@overload +def input( + files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, + inplace: bool = False, + backup: str = "", + *, + mode: Literal["rb"], + openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[bytes]] | None = None, + encoding: None = None, + errors: None = None, +) -> FileInput[bytes]: ... +@overload +def input( + files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, + inplace: bool = False, + backup: str = "", + *, + mode: str, + openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[Any]] | None = None, + encoding: str | None = None, + errors: str | None = None, +) -> FileInput[Any]: ... + +def close() -> None: ... +def nextfile() -> None: ... +def filename() -> str: ... +def lineno() -> int: ... +def filelineno() -> int: ... +def fileno() -> int: ... +def isfirstline() -> bool: ... +def isstdin() -> bool: ... + +class FileInput(Iterator[AnyStr]): # encoding and errors are added @overload - def input( + def __init__( + self: FileInput[str], files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, inplace: bool = False, backup: str = "", @@ -42,9 +87,10 @@ if sys.version_info >= (3, 10): openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[str]] | None = None, encoding: str | None = None, errors: str | None = None, - ) -> FileInput[str]: ... + ) -> None: ... @overload - def input( + def __init__( + self: FileInput[bytes], files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, inplace: bool = False, backup: str = "", @@ -53,9 +99,10 @@ if sys.version_info >= (3, 10): openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[bytes]] | None = None, encoding: None = None, errors: None = None, - ) -> FileInput[bytes]: ... + ) -> None: ... @overload - def input( + def __init__( + self: FileInput[Any], files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, inplace: bool = False, backup: str = "", @@ -64,119 +111,7 @@ if sys.version_info >= (3, 10): openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[Any]] | None = None, encoding: str | None = None, errors: str | None = None, - ) -> FileInput[Any]: ... - -else: - # bufsize is dropped and mode and openhook become keyword-only - @overload - def input( - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: _TextMode = "r", - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[str]] | None = None, - ) -> FileInput[str]: ... - @overload - def input( - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: Literal["rb"], - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[bytes]] | None = None, - ) -> FileInput[bytes]: ... - @overload - def input( - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: str, - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[Any]] | None = None, - ) -> FileInput[Any]: ... - -def close() -> None: ... -def nextfile() -> None: ... -def filename() -> str: ... -def lineno() -> int: ... -def filelineno() -> int: ... -def fileno() -> int: ... -def isfirstline() -> bool: ... -def isstdin() -> bool: ... - -class FileInput(Iterator[AnyStr]): - if sys.version_info >= (3, 10): - # encoding and errors are added - @overload - def __init__( - self: FileInput[str], - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: _TextMode = "r", - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[str]] | None = None, - encoding: str | None = None, - errors: str | None = None, - ) -> None: ... - @overload - def __init__( - self: FileInput[bytes], - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: Literal["rb"], - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[bytes]] | None = None, - encoding: None = None, - errors: None = None, - ) -> None: ... - @overload - def __init__( - self: FileInput[Any], - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: str, - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[Any]] | None = None, - encoding: str | None = None, - errors: str | None = None, - ) -> None: ... - - else: - # bufsize is dropped and mode and openhook become keyword-only - @overload - def __init__( - self: FileInput[str], - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: _TextMode = "r", - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[str]] | None = None, - ) -> None: ... - @overload - def __init__( - self: FileInput[bytes], - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: Literal["rb"], - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[bytes]] | None = None, - ) -> None: ... - @overload - def __init__( - self: FileInput[Any], - files: StrOrBytesPath | Iterable[StrOrBytesPath] | None = None, - inplace: bool = False, - backup: str = "", - *, - mode: str, - openhook: Callable[[StrOrBytesPath, str], _HasReadlineAndFileno[Any]] | None = None, - ) -> None: ... + ) -> None: ... def __del__(self) -> None: ... def close(self) -> None: ... @@ -199,17 +134,8 @@ class FileInput(Iterator[AnyStr]): def isstdin(self) -> bool: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... -if sys.version_info >= (3, 10): - def hook_compressed( - filename: StrOrBytesPath, mode: str, *, encoding: str | None = None, errors: str | None = None - ) -> IO[Any]: ... - -else: - def hook_compressed(filename: StrOrBytesPath, mode: str) -> IO[Any]: ... - -if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10. Use `fileinput.input` or `fileinput.FileInput` instead.") - def hook_encoded(encoding: str, errors: str | None = None) -> Callable[[StrOrBytesPath, str], IO[Any]]: ... - -else: - def hook_encoded(encoding: str, errors: str | None = None) -> Callable[[StrOrBytesPath, str], IO[Any]]: ... +def hook_compressed( + filename: StrOrBytesPath, mode: str, *, encoding: str | None = None, errors: str | None = None +) -> IO[Any]: ... +@deprecated("Deprecated since Python 3.10. Use `fileinput.input` or `fileinput.FileInput` instead.") +def hook_encoded(encoding: str, errors: str | None = None) -> Callable[[StrOrBytesPath, str], IO[Any]]: ... diff --git a/mypy/typeshed/stdlib/formatter.pyi b/mypy/typeshed/stdlib/formatter.pyi deleted file mode 100644 index 05c3c8b3dd41c..0000000000000 --- a/mypy/typeshed/stdlib/formatter.pyi +++ /dev/null @@ -1,88 +0,0 @@ -from collections.abc import Iterable -from typing import IO, Any -from typing_extensions import TypeAlias - -AS_IS: None -_FontType: TypeAlias = tuple[str, bool, bool, bool] -_StylesType: TypeAlias = tuple[Any, ...] - -class NullFormatter: - writer: NullWriter | None - def __init__(self, writer: NullWriter | None = None) -> None: ... - def end_paragraph(self, blankline: int) -> None: ... - def add_line_break(self) -> None: ... - def add_hor_rule(self, *args: Any, **kw: Any) -> None: ... - def add_label_data(self, format: str, counter: int, blankline: int | None = None) -> None: ... - def add_flowing_data(self, data: str) -> None: ... - def add_literal_data(self, data: str) -> None: ... - def flush_softspace(self) -> None: ... - def push_alignment(self, align: str | None) -> None: ... - def pop_alignment(self) -> None: ... - def push_font(self, x: _FontType) -> None: ... - def pop_font(self) -> None: ... - def push_margin(self, margin: int) -> None: ... - def pop_margin(self) -> None: ... - def set_spacing(self, spacing: str | None) -> None: ... - def push_style(self, *styles: _StylesType) -> None: ... - def pop_style(self, n: int = 1) -> None: ... - def assert_line_data(self, flag: int = 1) -> None: ... - -class AbstractFormatter: - writer: NullWriter - align: str | None - align_stack: list[str | None] - font_stack: list[_FontType] - margin_stack: list[int] - spacing: str | None - style_stack: Any - nospace: int - softspace: int - para_end: int - parskip: int - hard_break: int - have_label: int - def __init__(self, writer: NullWriter) -> None: ... - def end_paragraph(self, blankline: int) -> None: ... - def add_line_break(self) -> None: ... - def add_hor_rule(self, *args: Any, **kw: Any) -> None: ... - def add_label_data(self, format: str, counter: int, blankline: int | None = None) -> None: ... - def format_counter(self, format: Iterable[str], counter: int) -> str: ... - def format_letter(self, case: str, counter: int) -> str: ... - def format_roman(self, case: str, counter: int) -> str: ... - def add_flowing_data(self, data: str) -> None: ... - def add_literal_data(self, data: str) -> None: ... - def flush_softspace(self) -> None: ... - def push_alignment(self, align: str | None) -> None: ... - def pop_alignment(self) -> None: ... - def push_font(self, font: _FontType) -> None: ... - def pop_font(self) -> None: ... - def push_margin(self, margin: int) -> None: ... - def pop_margin(self) -> None: ... - def set_spacing(self, spacing: str | None) -> None: ... - def push_style(self, *styles: _StylesType) -> None: ... - def pop_style(self, n: int = 1) -> None: ... - def assert_line_data(self, flag: int = 1) -> None: ... - -class NullWriter: - def flush(self) -> None: ... - def new_alignment(self, align: str | None) -> None: ... - def new_font(self, font: _FontType) -> None: ... - def new_margin(self, margin: int, level: int) -> None: ... - def new_spacing(self, spacing: str | None) -> None: ... - def new_styles(self, styles: tuple[Any, ...]) -> None: ... - def send_paragraph(self, blankline: int) -> None: ... - def send_line_break(self) -> None: ... - def send_hor_rule(self, *args: Any, **kw: Any) -> None: ... - def send_label_data(self, data: str) -> None: ... - def send_flowing_data(self, data: str) -> None: ... - def send_literal_data(self, data: str) -> None: ... - -class AbstractWriter(NullWriter): ... - -class DumbWriter(NullWriter): - file: IO[str] - maxcol: int - def __init__(self, file: IO[str] | None = None, maxcol: int = 72) -> None: ... - def reset(self) -> None: ... - -def test(file: str | None = None) -> None: ... diff --git a/mypy/typeshed/stdlib/fractions.pyi b/mypy/typeshed/stdlib/fractions.pyi index ef4066aa65b52..42947f2e2266a 100644 --- a/mypy/typeshed/stdlib/fractions.pyi +++ b/mypy/typeshed/stdlib/fractions.pyi @@ -2,8 +2,8 @@ import sys from collections.abc import Callable from decimal import Decimal from numbers import Rational, Real -from typing import Any, Literal, Protocol, SupportsIndex, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, Literal, Protocol, SupportsIndex, TypeAlias, overload, type_check_only +from typing_extensions import Self _ComparableNum: TypeAlias = int | float | Decimal | Real @@ -15,11 +15,11 @@ class _ConvertibleToIntegerRatio(Protocol): class Fraction(Rational): __slots__ = ("_numerator", "_denominator") + @overload def __new__(cls, numerator: int | Rational = 0, denominator: int | Rational | None = None) -> Self: ... @overload def __new__(cls, numerator: float | Decimal | str) -> Self: ... - if sys.version_info >= (3, 14): @overload def __new__(cls, numerator: _ConvertibleToIntegerRatio) -> Self: ... @@ -37,78 +37,93 @@ class Fraction(Rational): def numerator(a) -> int: ... @property def denominator(a) -> int: ... + @overload def __add__(a, b: int | Fraction) -> Fraction: ... @overload def __add__(a, b: float) -> float: ... @overload def __add__(a, b: complex) -> complex: ... + @overload def __radd__(b, a: int | Fraction) -> Fraction: ... @overload def __radd__(b, a: float) -> float: ... @overload def __radd__(b, a: complex) -> complex: ... + @overload def __sub__(a, b: int | Fraction) -> Fraction: ... @overload def __sub__(a, b: float) -> float: ... @overload def __sub__(a, b: complex) -> complex: ... + @overload def __rsub__(b, a: int | Fraction) -> Fraction: ... @overload def __rsub__(b, a: float) -> float: ... @overload def __rsub__(b, a: complex) -> complex: ... + @overload def __mul__(a, b: int | Fraction) -> Fraction: ... @overload def __mul__(a, b: float) -> float: ... @overload def __mul__(a, b: complex) -> complex: ... + @overload def __rmul__(b, a: int | Fraction) -> Fraction: ... @overload def __rmul__(b, a: float) -> float: ... @overload def __rmul__(b, a: complex) -> complex: ... + @overload def __truediv__(a, b: int | Fraction) -> Fraction: ... @overload def __truediv__(a, b: float) -> float: ... @overload def __truediv__(a, b: complex) -> complex: ... + @overload def __rtruediv__(b, a: int | Fraction) -> Fraction: ... @overload def __rtruediv__(b, a: float) -> float: ... @overload def __rtruediv__(b, a: complex) -> complex: ... + @overload def __floordiv__(a, b: int | Fraction) -> int: ... @overload def __floordiv__(a, b: float) -> float: ... + @overload def __rfloordiv__(b, a: int | Fraction) -> int: ... @overload def __rfloordiv__(b, a: float) -> float: ... + @overload def __mod__(a, b: int | Fraction) -> Fraction: ... @overload def __mod__(a, b: float) -> float: ... + @overload def __rmod__(b, a: int | Fraction) -> Fraction: ... @overload def __rmod__(b, a: float) -> float: ... + @overload def __divmod__(a, b: int | Fraction) -> tuple[int, Fraction]: ... @overload def __divmod__(a, b: float) -> tuple[float, Fraction]: ... + @overload def __rdivmod__(a, b: int | Fraction) -> tuple[int, Fraction]: ... @overload def __rdivmod__(a, b: float) -> tuple[float, Fraction]: ... + if sys.version_info >= (3, 14): @overload def __pow__(a, b: int, modulo: None = None) -> Fraction: ... @@ -123,6 +138,7 @@ class Fraction(Rational): def __pow__(a, b: float | Fraction) -> float: ... @overload def __pow__(a, b: complex) -> complex: ... + if sys.version_info >= (3, 14): @overload def __rpow__(b, a: float | Fraction, modulo: None = None) -> float: ... @@ -140,10 +156,12 @@ class Fraction(Rational): def __trunc__(a) -> int: ... def __floor__(a) -> int: ... def __ceil__(a) -> int: ... + @overload def __round__(self, ndigits: None = None) -> int: ... @overload def __round__(self, ndigits: int) -> Fraction: ... + def __hash__(self) -> int: ... # type: ignore[override] def __eq__(a, b: object) -> bool: ... def __lt__(a, b: _ComparableNum) -> bool: ... diff --git a/mypy/typeshed/stdlib/ftplib.pyi b/mypy/typeshed/stdlib/ftplib.pyi index 73eaa8a34e578..1b7222fc94f75 100644 --- a/mypy/typeshed/stdlib/ftplib.pyi +++ b/mypy/typeshed/stdlib/ftplib.pyi @@ -154,6 +154,7 @@ class FTP_TLS(FTP): *, encoding: str = "utf-8", ) -> None: ... + ssl_version: int keyfile: StrOrBytesPath | None certfile: StrOrBytesPath | None diff --git a/mypy/typeshed/stdlib/functools.pyi b/mypy/typeshed/stdlib/functools.pyi index 57bc3f179f7ac..5619a64f6ed20 100644 --- a/mypy/typeshed/stdlib/functools.pyi +++ b/mypy/typeshed/stdlib/functools.pyi @@ -3,8 +3,21 @@ import types from _typeshed import SupportsAllComparisons, SupportsItems from collections.abc import Callable, Hashable, Iterable, Sized from types import GenericAlias -from typing import Any, Final, Generic, Literal, NamedTuple, TypedDict, TypeVar, final, overload, type_check_only -from typing_extensions import ParamSpec, Self, TypeAlias, disjoint_base +from typing import ( + Any, + Final, + Generic, + Literal, + NamedTuple, + ParamSpec, + TypeAlias, + TypedDict, + TypeVar, + final, + overload, + type_check_only, +) +from typing_extensions import Self, disjoint_base __all__ = [ "update_wrapper", @@ -34,7 +47,6 @@ _RWrapper = TypeVar("_RWrapper") if sys.version_info >= (3, 14): @overload def reduce(function: Callable[[_T, _S], _T], iterable: Iterable[_S], /, initial: _T) -> _T: ... - else: @overload def reduce(function: Callable[[_T, _S], _T], iterable: Iterable[_S], initial: _T, /) -> _T: ... @@ -154,6 +166,7 @@ else: def total_ordering(cls: type[_T]) -> type[_T]: ... def cmp_to_key(mycmp: Callable[[_T, _T], int]) -> Callable[[_T], SupportsAllComparisons]: ... + @disjoint_base class partial(Generic[_T]): @property @@ -198,6 +211,7 @@ else: class _SingleDispatchCallable(Generic[_T]): registry: types.MappingProxyType[Any, Callable[..., _T]] def dispatch(self, cls: Any) -> Callable[..., _T]: ... + # @fun.register(complex) # def _(arg, verbose=False): ... @overload @@ -209,6 +223,7 @@ class _SingleDispatchCallable(Generic[_T]): # fun.register(int, lambda x: x) @overload def register(self, cls: _RegType, func: Callable[..., _T]) -> Callable[..., _T]: ... + def _clear_cache(self) -> None: ... def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... @@ -220,22 +235,26 @@ class singledispatchmethod(Generic[_T]): def __init__(self, func: Callable[..., _T]) -> None: ... @property def __isabstractmethod__(self) -> bool: ... + @overload def register(self, cls: _RegType, method: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... @overload def register(self, cls: Callable[..., _T], method: None = None) -> Callable[..., _T]: ... @overload def register(self, cls: _RegType, method: Callable[..., _T]) -> Callable[..., _T]: ... + def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]: ... class cached_property(Generic[_T_co]): func: Callable[[Any], _T_co] attrname: str | None def __init__(self, func: Callable[[Any], _T_co]) -> None: ... + @overload def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... @overload def __get__(self, instance: object, owner: type[Any] | None = None) -> _T_co: ... + def __set_name__(self, owner: type[Any], name: str) -> None: ... # __set__ is not defined at runtime, but @cached_property is designed to be settable def __set__(self, instance: object, value: _T_co) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] diff --git a/mypy/typeshed/stdlib/gc.pyi b/mypy/typeshed/stdlib/gc.pyi index ec1ed2681c5c8..dc20a570cc89b 100644 --- a/mypy/typeshed/stdlib/gc.pyi +++ b/mypy/typeshed/stdlib/gc.pyi @@ -1,6 +1,5 @@ from collections.abc import Callable -from typing import Any, Final, Literal -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, TypeAlias DEBUG_COLLECTABLE: Final = 2 DEBUG_LEAK: Final = 38 diff --git a/mypy/typeshed/stdlib/genericpath.pyi b/mypy/typeshed/stdlib/genericpath.pyi index 3caed77a661ac..07c58cc496b60 100644 --- a/mypy/typeshed/stdlib/genericpath.pyi +++ b/mypy/typeshed/stdlib/genericpath.pyi @@ -3,7 +3,7 @@ import sys from _typeshed import BytesPath, FileDescriptorOrPath, StrOrBytesPath, StrPath, SupportsRichComparisonT from collections.abc import Sequence from typing import Literal, NewType, overload -from typing_extensions import LiteralString +from typing_extensions import LiteralString, deprecated __all__ = [ "commonprefix", @@ -23,22 +23,41 @@ if sys.version_info >= (3, 12): __all__ += ["islink"] if sys.version_info >= (3, 13): __all__ += ["isjunction", "isdevdrive", "lexists"] +if sys.version_info >= (3, 15): + __all__ += ["ALL_BUT_LAST"] # All overloads can return empty string. Ideally, Literal[""] would be a valid # Iterable[T], so that list[T] | Literal[""] could be used as a return # type. But because this only works when T is str, we need Sequence[T] instead. -@overload -def commonprefix(m: Sequence[LiteralString]) -> LiteralString: ... -@overload -def commonprefix(m: Sequence[StrPath]) -> str: ... -@overload -def commonprefix(m: Sequence[BytesPath]) -> bytes | Literal[""]: ... -@overload -def commonprefix(m: Sequence[list[SupportsRichComparisonT]]) -> Sequence[SupportsRichComparisonT]: ... -@overload -def commonprefix(m: Sequence[tuple[SupportsRichComparisonT, ...]]) -> Sequence[SupportsRichComparisonT]: ... +if sys.version_info >= (3, 15): + @overload + @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") + def commonprefix(m: Sequence[LiteralString], /) -> LiteralString: ... + @overload + @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") + def commonprefix(m: Sequence[StrPath], /) -> str: ... + @overload + @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") + def commonprefix(m: Sequence[BytesPath], /) -> bytes | Literal[""]: ... + @overload + @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") + def commonprefix(m: Sequence[list[SupportsRichComparisonT]], /) -> Sequence[SupportsRichComparisonT]: ... + @overload + @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") + def commonprefix(m: Sequence[tuple[SupportsRichComparisonT, ...]], /) -> Sequence[SupportsRichComparisonT]: ... +else: + @overload + def commonprefix(m: Sequence[LiteralString]) -> LiteralString: ... + @overload + def commonprefix(m: Sequence[StrPath]) -> str: ... + @overload + def commonprefix(m: Sequence[BytesPath]) -> bytes | Literal[""]: ... + @overload + def commonprefix(m: Sequence[list[SupportsRichComparisonT]]) -> Sequence[SupportsRichComparisonT]: ... + @overload + def commonprefix(m: Sequence[tuple[SupportsRichComparisonT, ...]]) -> Sequence[SupportsRichComparisonT]: ... + def exists(path: FileDescriptorOrPath) -> bool: ... -def getsize(filename: FileDescriptorOrPath) -> int: ... def isfile(path: FileDescriptorOrPath) -> bool: ... def isdir(s: FileDescriptorOrPath) -> bool: ... @@ -47,12 +66,23 @@ if sys.version_info >= (3, 12): # These return float if os.stat_float_times() == True, # but int is a subclass of float. -def getatime(filename: FileDescriptorOrPath) -> float: ... -def getmtime(filename: FileDescriptorOrPath) -> float: ... -def getctime(filename: FileDescriptorOrPath) -> float: ... -def samefile(f1: FileDescriptorOrPath, f2: FileDescriptorOrPath) -> bool: ... def sameopenfile(fp1: int, fp2: int) -> bool: ... -def samestat(s1: os.stat_result, s2: os.stat_result) -> bool: ... + +if sys.version_info >= (3, 15): + def getsize(filename: FileDescriptorOrPath, /) -> int: ... + def getatime(filename: FileDescriptorOrPath, /) -> float: ... + def getmtime(filename: FileDescriptorOrPath, /) -> float: ... + def getctime(filename: FileDescriptorOrPath, /) -> float: ... + def samefile(f1: FileDescriptorOrPath, f2: FileDescriptorOrPath, /) -> bool: ... + def samestat(s1: os.stat_result, s2: os.stat_result, /) -> bool: ... + +else: + def getsize(filename: FileDescriptorOrPath) -> int: ... + def getatime(filename: FileDescriptorOrPath) -> float: ... + def getmtime(filename: FileDescriptorOrPath) -> float: ... + def getctime(filename: FileDescriptorOrPath) -> float: ... + def samefile(f1: FileDescriptorOrPath, f2: FileDescriptorOrPath) -> bool: ... + def samestat(s1: os.stat_result, s2: os.stat_result) -> bool: ... if sys.version_info >= (3, 13): def isjunction(path: StrOrBytesPath) -> bool: ... @@ -62,3 +92,7 @@ if sys.version_info >= (3, 13): # Added in Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 _AllowMissingType = NewType("_AllowMissingType", object) ALLOW_MISSING: _AllowMissingType + +if sys.version_info >= (3, 15): + _AllButLastType = NewType("_AllButLastType", object) + ALL_BUT_LAST: _AllButLastType diff --git a/mypy/typeshed/stdlib/gettext.pyi b/mypy/typeshed/stdlib/gettext.pyi index e9ffd7a4a4a42..aac2d3edf7d98 100644 --- a/mypy/typeshed/stdlib/gettext.pyi +++ b/mypy/typeshed/stdlib/gettext.pyi @@ -110,8 +110,8 @@ if sys.version_info >= (3, 11): class_: Callable[[io.BufferedReader], NullTranslations] | None = None, fallback: bool = False, ) -> NullTranslations: ... - def install(domain: str, localedir: StrPath | None = None, *, names: Container[str] | None = None) -> None: ... + def install(domain: str, localedir: StrPath | None = None, *, names: Container[str] | None = None) -> None: ... else: @overload def translation( @@ -150,6 +150,7 @@ else: fallback: bool = False, codeset: str | None = ..., ) -> NullTranslations: ... + @overload def install(domain: str, localedir: StrPath | None = None, names: Container[str] | None = None) -> None: ... @overload diff --git a/mypy/typeshed/stdlib/glob.pyi b/mypy/typeshed/stdlib/glob.pyi index 942fd73961963..bdfb2cfbcfedd 100644 --- a/mypy/typeshed/stdlib/glob.pyi +++ b/mypy/typeshed/stdlib/glob.pyi @@ -9,7 +9,7 @@ __all__ = ["escape", "glob", "iglob"] if sys.version_info >= (3, 13): __all__ += ["translate"] -if sys.version_info >= (3, 10): +if sys.version_info < (3, 15): @deprecated( "Deprecated since Python 3.10; will be removed in Python 3.15. Use `glob.glob()` with the *root_dir* argument instead." ) @@ -19,10 +19,6 @@ if sys.version_info >= (3, 10): ) def glob1(dirname: AnyStr, pattern: AnyStr) -> list[AnyStr]: ... -else: - def glob0(dirname: AnyStr, pattern: AnyStr) -> list[AnyStr]: ... - def glob1(dirname: AnyStr, pattern: AnyStr) -> list[AnyStr]: ... - if sys.version_info >= (3, 11): def glob( pathname: AnyStr, @@ -41,7 +37,7 @@ if sys.version_info >= (3, 11): include_hidden: bool = False, ) -> Iterator[AnyStr]: ... -elif sys.version_info >= (3, 10): +else: def glob( pathname: AnyStr, *, root_dir: StrOrBytesPath | None = None, dir_fd: int | None = None, recursive: bool = False ) -> list[AnyStr]: ... @@ -49,10 +45,6 @@ elif sys.version_info >= (3, 10): pathname: AnyStr, *, root_dir: StrOrBytesPath | None = None, dir_fd: int | None = None, recursive: bool = False ) -> Iterator[AnyStr]: ... -else: - def glob(pathname: AnyStr, *, recursive: bool = False) -> list[AnyStr]: ... - def iglob(pathname: AnyStr, *, recursive: bool = False) -> Iterator[AnyStr]: ... - def escape(pathname: AnyStr) -> AnyStr: ... def has_magic(s: str | bytes) -> bool: ... # undocumented diff --git a/mypy/typeshed/stdlib/graphlib.pyi b/mypy/typeshed/stdlib/graphlib.pyi index 1ca8cbe12b085..f0ac72b6135e8 100644 --- a/mypy/typeshed/stdlib/graphlib.pyi +++ b/mypy/typeshed/stdlib/graphlib.pyi @@ -15,6 +15,7 @@ class TopologicalSorter(Generic[_T]): def __init__(self, graph: None = None) -> None: ... @overload def __init__(self, graph: SupportsItems[_T, Iterable[_T]]) -> None: ... + def add(self, node: _T, *predecessors: _T) -> None: ... def prepare(self) -> None: ... def is_active(self) -> bool: ... diff --git a/mypy/typeshed/stdlib/grp.pyi b/mypy/typeshed/stdlib/grp.pyi index 965ecece2a56d..9f372b4d63dc8 100644 --- a/mypy/typeshed/stdlib/grp.pyi +++ b/mypy/typeshed/stdlib/grp.pyi @@ -5,8 +5,7 @@ from typing import Any, Final, final if sys.platform != "win32": @final class struct_group(structseq[Any], tuple[str, str | None, int, list[str]]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("gr_name", "gr_passwd", "gr_gid", "gr_mem") + __match_args__: Final = ("gr_name", "gr_passwd", "gr_gid", "gr_mem") @property def gr_name(self) -> str: ... diff --git a/mypy/typeshed/stdlib/gzip.pyi b/mypy/typeshed/stdlib/gzip.pyi index b18f76f06e3ee..4322e6c84c905 100644 --- a/mypy/typeshed/stdlib/gzip.pyi +++ b/mypy/typeshed/stdlib/gzip.pyi @@ -2,8 +2,8 @@ import sys import zlib from _typeshed import ReadableBuffer, SizedBuffer, StrOrBytesPath, WriteableBuffer from io import FileIO, TextIOWrapper -from typing import Final, Literal, Protocol, overload, type_check_only -from typing_extensions import TypeAlias, deprecated +from typing import Final, Literal, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import deprecated if sys.version_info >= (3, 14): from compression._common._streams import BaseStream, DecompressReader @@ -96,6 +96,7 @@ class GzipFile(BaseStream): name: str compress: zlib._Compress fileobj: _ReadableFileobj | _WritableFileobj + @overload def __init__( self, @@ -141,6 +142,7 @@ class GzipFile(BaseStream): fileobj: _ReadableFileobj | _WritableFileobj | None = None, mtime: float | None = None, ) -> None: ... + if sys.version_info < (3, 12): @property @deprecated("Deprecated since Python 2.6; removed in Python 3.12. Use `name` attribute instead.") @@ -167,7 +169,10 @@ class GzipFile(BaseStream): class _GzipReader(DecompressReader): def __init__(self, fp: _ReadableFileobj) -> None: ... -if sys.version_info >= (3, 14): +if sys.version_info >= (3, 15): + def compress(data: SizedBuffer, compresslevel: int = 6, *, mtime: float = 0) -> bytes: ... + +elif sys.version_info >= (3, 14): def compress(data: SizedBuffer, compresslevel: int = 9, *, mtime: float = 0) -> bytes: ... else: diff --git a/mypy/typeshed/stdlib/hashlib.pyi b/mypy/typeshed/stdlib/hashlib.pyi index 1763c23182736..50bc8e21f1d52 100644 --- a/mypy/typeshed/stdlib/hashlib.pyi +++ b/mypy/typeshed/stdlib/hashlib.pyi @@ -22,7 +22,7 @@ from _typeshed import ReadableBuffer from collections.abc import Callable, Set as AbstractSet from typing import Protocol, type_check_only -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 15): __all__ = ( "md5", "sha1", @@ -41,8 +41,31 @@ if sys.version_info >= (3, 11): "new", "algorithms_guaranteed", "algorithms_available", + "file_digest", "pbkdf2_hmac", + "scrypt", + ) +elif sys.version_info >= (3, 11): + __all__ = ( + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "blake2b", + "blake2s", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + "shake_128", + "shake_256", + "new", + "algorithms_guaranteed", + "algorithms_available", "file_digest", + "pbkdf2_hmac", ) else: __all__ = ( diff --git a/mypy/typeshed/stdlib/heapq.pyi b/mypy/typeshed/stdlib/heapq.pyi index ff8ba7ff1f826..7969aa4e00152 100644 --- a/mypy/typeshed/stdlib/heapq.pyi +++ b/mypy/typeshed/stdlib/heapq.pyi @@ -1,8 +1,8 @@ import sys from _heapq import * -from _typeshed import SupportsRichComparison +from _typeshed import SupportsRichComparison, SupportsRichComparisonT as _T from collections.abc import Callable, Generator, Iterable -from typing import Any, Final, TypeVar +from typing import Final, TypeVar, overload __all__ = ["heappush", "heappop", "heapify", "heapreplace", "merge", "nlargest", "nsmallest", "heappushpop"] @@ -14,9 +14,19 @@ _S = TypeVar("_S") __about__: Final[str] -def merge( - *iterables: Iterable[_S], key: Callable[[_S], SupportsRichComparison] | None = None, reverse: bool = False -) -> Generator[_S]: ... -def nlargest(n: int, iterable: Iterable[_S], key: Callable[[_S], SupportsRichComparison] | None = None) -> list[_S]: ... -def nsmallest(n: int, iterable: Iterable[_S], key: Callable[[_S], SupportsRichComparison] | None = None) -> list[_S]: ... -def _heapify_max(heap: list[Any], /) -> None: ... # undocumented +@overload +def merge(*iterables: Iterable[_S], key: Callable[[_S], SupportsRichComparison], reverse: bool = False) -> Generator[_S]: ... +@overload +def merge(*iterables: Iterable[_T], key: None = None, reverse: bool = False) -> Generator[_T]: ... + +@overload +def nlargest(n: int, iterable: Iterable[_S], key: Callable[[_S], SupportsRichComparison]) -> list[_S]: ... +@overload +def nlargest(n: int, iterable: Iterable[_T], key: None = None) -> list[_T]: ... + +@overload +def nsmallest(n: int, iterable: Iterable[_S], key: Callable[[_S], SupportsRichComparison]) -> list[_S]: ... +@overload +def nsmallest(n: int, iterable: Iterable[_T], key: None = None) -> list[_T]: ... + +def _heapify_max(heap: list[SupportsRichComparison], /) -> None: ... # undocumented diff --git a/mypy/typeshed/stdlib/hmac.pyi b/mypy/typeshed/stdlib/hmac.pyi index 070c59b1c166d..9beabcc4dd92e 100644 --- a/mypy/typeshed/stdlib/hmac.pyi +++ b/mypy/typeshed/stdlib/hmac.pyi @@ -2,8 +2,7 @@ from _hashlib import _HashObject, compare_digest as compare_digest from _typeshed import ReadableBuffer, SizedBuffer from collections.abc import Callable from types import ModuleType -from typing import overload -from typing_extensions import TypeAlias +from typing import TypeAlias, overload _DigestMod: TypeAlias = str | Callable[[], _HashObject] | ModuleType diff --git a/mypy/typeshed/stdlib/html/__init__.pyi b/mypy/typeshed/stdlib/html/__init__.pyi index afba90832535d..8ad72f1265882 100644 --- a/mypy/typeshed/stdlib/html/__init__.pyi +++ b/mypy/typeshed/stdlib/html/__init__.pyi @@ -1,6 +1,4 @@ -from typing import AnyStr - __all__ = ["escape", "unescape"] -def escape(s: AnyStr, quote: bool = True) -> AnyStr: ... -def unescape(s: AnyStr) -> AnyStr: ... +def escape(s: str, quote: bool = True) -> str: ... +def unescape(s: str) -> str: ... diff --git a/mypy/typeshed/stdlib/http/client.pyi b/mypy/typeshed/stdlib/http/client.pyi index 699ef0e4c6d65..d22335b56c547 100644 --- a/mypy/typeshed/stdlib/http/client.pyi +++ b/mypy/typeshed/stdlib/http/client.pyi @@ -7,8 +7,8 @@ from _typeshed import MaybeNone, ReadableBuffer, StrOrBytesPath, SupportsRead, S from collections.abc import Callable, Iterable, Iterator, Mapping from email._policybase import _MessageT from socket import socket -from typing import BinaryIO, Final, TypeVar, overload -from typing_extensions import Self, TypeAlias, deprecated +from typing import BinaryIO, Final, TypeAlias, TypeVar, overload +from typing_extensions import Self, deprecated __all__ = [ "HTTPResponse", @@ -149,10 +149,12 @@ class HTTPResponse(io.BufferedIOBase, BinaryIO): # type: ignore[misc] # incomp def read1(self, n: int = -1) -> bytes: ... def readinto(self, b: WriteableBuffer) -> int: ... def readline(self, limit: int = -1) -> bytes: ... # type: ignore[override] + @overload def getheader(self, name: str) -> str | None: ... @overload def getheader(self, name: str, default: _T) -> str | _T: ... + def getheaders(self) -> list[tuple[str, str]]: ... def isclosed(self) -> bool: ... def __iter__(self) -> Iterator[bytes]: ... @@ -178,14 +180,27 @@ class HTTPConnection: host: str port: int sock: socket | MaybeNone # can be `None` if `.connect()` was not called - def __init__( - self, - host: str, - port: int | None = None, - timeout: float | None = ..., - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - ) -> None: ... + if sys.version_info >= (3, 15): + def __init__( + self, + host: str, + port: int | None = None, + timeout: float | None = ..., + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + *, + max_response_headers: int | None = None, + ) -> None: ... + else: + def __init__( + self, + host: str, + port: int | None = None, + timeout: float | None = ..., + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + ) -> None: ... + def request( self, method: str, @@ -211,7 +226,19 @@ class HTTPConnection: class HTTPSConnection(HTTPConnection): # Can be `None` if `.connect()` was not called: sock: ssl.SSLSocket | MaybeNone - if sys.version_info >= (3, 12): + if sys.version_info >= (3, 15): + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: float | None = ..., + source_address: tuple[str, int] | None = None, + context: ssl.SSLContext | None = None, + blocksize: int = 8192, + max_response_headers: int | None = None, + ) -> None: ... + elif sys.version_info >= (3, 12): def __init__( self, host: str, @@ -255,6 +282,7 @@ class HTTPSConnection(HTTPConnection): check_hostname: bool | None = None, blocksize: int = 8192, ) -> None: ... + key_file: StrOrBytesPath | None cert_file: StrOrBytesPath | None diff --git a/mypy/typeshed/stdlib/http/cookiejar.pyi b/mypy/typeshed/stdlib/http/cookiejar.pyi index 31e1d3fc83785..2cbe432c55866 100644 --- a/mypy/typeshed/stdlib/http/cookiejar.pyi +++ b/mypy/typeshed/stdlib/http/cookiejar.pyi @@ -1,4 +1,3 @@ -import sys from _typeshed import StrPath from collections.abc import Iterator, Sequence from http.client import HTTPResponse @@ -49,9 +48,7 @@ class FileCookieJar(CookieJar): def load(self, filename: str | None = None, ignore_discard: bool = False, ignore_expires: bool = False) -> None: ... def revert(self, filename: str | None = None, ignore_discard: bool = False, ignore_expires: bool = False) -> None: ... -class MozillaCookieJar(FileCookieJar): - if sys.version_info < (3, 10): - header: ClassVar[str] # undocumented +class MozillaCookieJar(FileCookieJar): ... class LWPCookieJar(FileCookieJar): def as_lwp_str(self, ignore_discard: bool = True, ignore_expires: bool = True) -> str: ... # undocumented @@ -151,9 +148,11 @@ class Cookie: rfc2109: bool = False, ) -> None: ... def has_nonstandard_attr(self, name: str) -> bool: ... + @overload def get_nonstandard_attr(self, name: str) -> str | None: ... @overload def get_nonstandard_attr(self, name: str, default: _T) -> str | _T: ... + def set_nonstandard_attr(self, name: str, value: str) -> None: ... def is_expired(self, now: int | None = None) -> bool: ... diff --git a/mypy/typeshed/stdlib/http/cookies.pyi b/mypy/typeshed/stdlib/http/cookies.pyi index eadf054c3a5f4..bdec1068b5ea0 100644 --- a/mypy/typeshed/stdlib/http/cookies.pyi +++ b/mypy/typeshed/stdlib/http/cookies.pyi @@ -1,18 +1,17 @@ -from _typeshed import MaybeNone -from collections.abc import Iterable, Mapping +from _typeshed import MaybeNone, SupportsItems, SupportsKeysAndGetItem +from collections.abc import Container, Iterable from types import GenericAlias from typing import Any, Generic, TypeVar, overload -from typing_extensions import TypeAlias __all__ = ["CookieError", "BaseCookie", "SimpleCookie"] -_DataType: TypeAlias = str | Mapping[str, str | Morsel[Any]] _T = TypeVar("_T") @overload def _quote(str: None) -> None: ... @overload def _quote(str: str) -> str: ... + @overload def _unquote(str: None) -> None: ... @overload @@ -31,27 +30,24 @@ class Morsel(dict[str, Any], Generic[_T]): def set(self, key: str, val: str, coded_val: _T) -> None: ... def setdefault(self, key: str, val: str | None = None) -> str: ... # The dict update can also get a keywords argument so this is incompatible - @overload # type: ignore[override] - def update(self, values: Mapping[str, str]) -> None: ... - @overload - def update(self, values: Iterable[tuple[str, str]]) -> None: ... + def update(self, values: Iterable[tuple[str, str]] | SupportsKeysAndGetItem[str, str]) -> None: ... # type: ignore[override] def isReservedKey(self, K: str) -> bool: ... - def output(self, attrs: list[str] | None = None, header: str = "Set-Cookie:") -> str: ... + def output(self, attrs: Container[str] | None = None, header: str = "Set-Cookie:") -> str: ... __str__ = output - def js_output(self, attrs: list[str] | None = None) -> str: ... - def OutputString(self, attrs: list[str] | None = None) -> str: ... + def js_output(self, attrs: Container[str] | None = None) -> str: ... + def OutputString(self, attrs: Container[str] | None = None) -> str: ... def __eq__(self, morsel: object) -> bool: ... def __setitem__(self, K: str, V: Any) -> None: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... class BaseCookie(dict[str, Morsel[_T]], Generic[_T]): - def __init__(self, input: _DataType | None = None) -> None: ... + def __init__(self, input: str | SupportsItems[str, str | Morsel[Any]] | None = None) -> None: ... def value_decode(self, val: str) -> tuple[_T, str]: ... def value_encode(self, val: _T) -> tuple[str, str]: ... - def output(self, attrs: list[str] | None = None, header: str = "Set-Cookie:", sep: str = "\r\n") -> str: ... + def output(self, attrs: Container[str] | None = None, header: str = "Set-Cookie:", sep: str = "\r\n") -> str: ... __str__ = output - def js_output(self, attrs: list[str] | None = None) -> str: ... - def load(self, rawdata: _DataType) -> None: ... + def js_output(self, attrs: Container[str] | None = None) -> str: ... + def load(self, rawdata: str | SupportsItems[str, str | Morsel[Any]]) -> None: ... def __setitem__(self, key: str, value: str | Morsel[_T]) -> None: ... class SimpleCookie(BaseCookie[str]): ... diff --git a/mypy/typeshed/stdlib/http/server.pyi b/mypy/typeshed/stdlib/http/server.pyi index 2c1a374331bcc..88cd2469cf115 100644 --- a/mypy/typeshed/stdlib/http/server.pyi +++ b/mypy/typeshed/stdlib/http/server.pyi @@ -10,18 +10,11 @@ from ssl import Purpose, SSLContext from typing import Any, AnyStr, BinaryIO, ClassVar, Protocol, type_check_only from typing_extensions import Self, deprecated +__all__ = ["HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler"] +if sys.version_info < (3, 15): + __all__ += ["CGIHTTPRequestHandler"] if sys.version_info >= (3, 14): - __all__ = [ - "HTTPServer", - "ThreadingHTTPServer", - "HTTPSServer", - "ThreadingHTTPSServer", - "BaseHTTPRequestHandler", - "SimpleHTTPRequestHandler", - "CGIHTTPRequestHandler", - ] -else: - __all__ = ["HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler"] + __all__ = ["HTTPSServer", "ThreadingHTTPSServer"] class HTTPServer(socketserver.TCPServer): server_name: str @@ -77,6 +70,8 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): protocol_version: str MessageClass: type responses: Mapping[int, tuple[str, str]] + if sys.version_info >= (3, 15): + default_content_type: str default_request_version: str # undocumented weekdayname: ClassVar[Sequence[str]] # undocumented monthname: ClassVar[Sequence[str | None]] # undocumented @@ -102,14 +97,26 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): if sys.version_info >= (3, 12): index_pages: ClassVar[tuple[str, ...]] directory: str - def __init__( - self, - request: socketserver._RequestType, - client_address: _socket._RetAddress, - server: socketserver.BaseServer, - *, - directory: StrPath | None = None, - ) -> None: ... + if sys.version_info >= (3, 15): + def __init__( + self, + request: socketserver._RequestType, + client_address: _socket._RetAddress, + server: socketserver.BaseServer, + *, + directory: StrPath | None = None, + extra_response_headers: Mapping[str, str] | None = None, + ) -> None: ... + else: + def __init__( + self, + request: socketserver._RequestType, + client_address: _socket._RetAddress, + server: socketserver.BaseServer, + *, + directory: StrPath | None = None, + ) -> None: ... + def do_GET(self) -> None: ... def do_HEAD(self) -> None: ... def send_head(self) -> io.BytesIO | BinaryIO | None: ... # undocumented @@ -120,7 +127,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): def executable(path: StrPath) -> bool: ... # undocumented -if sys.version_info >= (3, 13): +if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): cgi_directories: list[str] @@ -130,13 +137,3 @@ if sys.version_info >= (3, 13): def is_executable(self, path: StrPath) -> bool: ... # undocumented def is_python(self, path: StrPath) -> bool: ... # undocumented def run_cgi(self) -> None: ... # undocumented - -else: - class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): - cgi_directories: list[str] - have_fork: bool # undocumented - def do_POST(self) -> None: ... - def is_cgi(self) -> bool: ... # undocumented - def is_executable(self, path: StrPath) -> bool: ... # undocumented - def is_python(self, path: StrPath) -> bool: ... # undocumented - def run_cgi(self) -> None: ... # undocumented diff --git a/mypy/typeshed/stdlib/imaplib.pyi b/mypy/typeshed/stdlib/imaplib.pyi index 94b96f0a1283c..fd14e45ed725f 100644 --- a/mypy/typeshed/stdlib/imaplib.pyi +++ b/mypy/typeshed/stdlib/imaplib.pyi @@ -9,8 +9,8 @@ from re import Pattern from socket import socket as _socket from ssl import SSLContext, SSLSocket from types import TracebackType -from typing import IO, Any, Literal, SupportsAbs, SupportsInt, overload -from typing_extensions import Self, TypeAlias, deprecated +from typing import IO, Any, Literal, SupportsAbs, SupportsInt, TypeAlias, overload +from typing_extensions import Self, deprecated __all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple", "Int2AP", "ParseFlags", "Time2Internaldate", "IMAP4_SSL"] @@ -151,6 +151,7 @@ class IMAP4_SSL(IMAP4): ssl_context: None = None, timeout: float | None = None, ) -> None: ... + keyfile: StrOrBytesPath | None certfile: StrOrBytesPath | None sslobj: SSLSocket diff --git a/mypy/typeshed/stdlib/importlib/_abc.pyi b/mypy/typeshed/stdlib/importlib/_abc.pyi index 90ab340219172..e8c80c0447aed 100644 --- a/mypy/typeshed/stdlib/importlib/_abc.pyi +++ b/mypy/typeshed/stdlib/importlib/_abc.pyi @@ -4,17 +4,16 @@ from abc import ABCMeta from importlib.machinery import ModuleSpec from typing_extensions import deprecated -if sys.version_info >= (3, 10): - class Loader(metaclass=ABCMeta): - def load_module(self, fullname: str) -> types.ModuleType: ... - if sys.version_info < (3, 12): - @deprecated( - "Deprecated since Python 3.4; removed in Python 3.12. " - "The module spec is now used by the import machinery to generate a module repr." - ) - def module_repr(self, module: types.ModuleType) -> str: ... +class Loader(metaclass=ABCMeta): + def load_module(self, fullname: str) -> types.ModuleType: ... + if sys.version_info < (3, 12): + @deprecated( + "Deprecated since Python 3.4; removed in Python 3.12. " + "The module spec is now used by the import machinery to generate a module repr." + ) + def module_repr(self, module: types.ModuleType) -> str: ... - def create_module(self, spec: ModuleSpec) -> types.ModuleType | None: ... - # Not defined on the actual class for backwards-compatibility reasons, - # but expected in new code. - def exec_module(self, module: types.ModuleType) -> None: ... + def create_module(self, spec: ModuleSpec) -> types.ModuleType | None: ... + # Not defined on the actual class for backwards-compatibility reasons, + # but expected in new code. + def exec_module(self, module: types.ModuleType) -> None: ... diff --git a/mypy/typeshed/stdlib/importlib/abc.pyi b/mypy/typeshed/stdlib/importlib/abc.pyi index ef7761f7119b9..945b8d2080d1f 100644 --- a/mypy/typeshed/stdlib/importlib/abc.pyi +++ b/mypy/typeshed/stdlib/importlib/abc.pyi @@ -5,6 +5,7 @@ from _typeshed import ReadableBuffer, StrPath from abc import ABCMeta, abstractmethod from collections.abc import Iterator, Mapping, Sequence from importlib import _bootstrap_external +from importlib._abc import Loader as Loader from importlib.machinery import ModuleSpec from io import BufferedReader from typing import IO, Any, Literal, Protocol, overload, runtime_checkable @@ -25,17 +26,6 @@ if sys.version_info >= (3, 11): if sys.version_info < (3, 12): __all__ += ["Finder", "ResourceReader", "Traversable", "TraversableResources"] -if sys.version_info >= (3, 10): - from importlib._abc import Loader as Loader -else: - class Loader(metaclass=ABCMeta): - def load_module(self, fullname: str) -> types.ModuleType: ... - def module_repr(self, module: types.ModuleType) -> str: ... - def create_module(self, spec: ModuleSpec) -> types.ModuleType | None: ... - # Not defined on the actual class for backwards-compatibility reasons, - # but expected in new code. - def exec_module(self, module: types.ModuleType) -> None: ... - if sys.version_info < (3, 12): @deprecated("Deprecated since Python 3.3; removed in Python 3.12. Use `MetaPathFinder` or `PathEntryFinder` instead.") class Finder(metaclass=ABCMeta): ... @@ -67,47 +57,28 @@ class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLo def get_source(self, fullname: str) -> str | None: ... def path_stats(self, path: str) -> Mapping[str, Any]: ... -# The base classes differ starting in 3.10: -if sys.version_info >= (3, 10): - # Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol - class MetaPathFinder(metaclass=ABCMeta): - if sys.version_info < (3, 12): - @deprecated("Deprecated since Python 3.4; removed in Python 3.12. Use `MetaPathFinder.find_spec()` instead.") - def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ... - - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec( - self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / - ) -> ModuleSpec | None: ... - - class PathEntryFinder(metaclass=ABCMeta): - if sys.version_info < (3, 12): - @deprecated("Deprecated since Python 3.4; removed in Python 3.12. Use `PathEntryFinder.find_spec()` instead.") - def find_module(self, fullname: str) -> Loader | None: ... - @deprecated("Deprecated since Python 3.4; removed in Python 3.12. Use `find_spec()` instead.") - def find_loader(self, fullname: str) -> tuple[Loader | None, Sequence[str]]: ... - - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... - -else: - # Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol - class MetaPathFinder(Finder): +# Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol +class MetaPathFinder(metaclass=ABCMeta): + if sys.version_info < (3, 12): + @deprecated("Deprecated since Python 3.4; removed in Python 3.12. Use `MetaPathFinder.find_spec()` instead.") def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ... - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec( - self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / - ) -> ModuleSpec | None: ... - class PathEntryFinder(Finder): + def invalidate_caches(self) -> None: ... + # Not defined on the actual class, but expected to exist. + def find_spec( + self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / + ) -> ModuleSpec | None: ... + +class PathEntryFinder(metaclass=ABCMeta): + if sys.version_info < (3, 12): + @deprecated("Deprecated since Python 3.4; removed in Python 3.12. Use `PathEntryFinder.find_spec()` instead.") def find_module(self, fullname: str) -> Loader | None: ... + @deprecated("Deprecated since Python 3.4; removed in Python 3.12. Use `find_spec()` instead.") def find_loader(self, fullname: str) -> tuple[Loader | None, Sequence[str]]: ... - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... + + def invalidate_caches(self) -> None: ... + # Not defined on the actual class, but expected to exist. + def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader, metaclass=ABCMeta): name: str @@ -123,13 +94,8 @@ if sys.version_info < (3, 11): def open_resource(self, resource: str) -> IO[bytes]: ... @abstractmethod def resource_path(self, resource: str) -> str: ... - if sys.version_info >= (3, 10): - @abstractmethod - def is_resource(self, path: str) -> bool: ... - else: - @abstractmethod - def is_resource(self, name: str) -> bool: ... - + @abstractmethod + def is_resource(self, path: str) -> bool: ... @abstractmethod def contents(self) -> Iterator[str]: ... @@ -141,6 +107,7 @@ if sys.version_info < (3, 11): def is_file(self) -> bool: ... @abstractmethod def iterdir(self) -> Iterator[Traversable]: ... + if sys.version_info >= (3, 11): @abstractmethod def joinpath(self, *descendants: str) -> Traversable: ... @@ -157,15 +124,11 @@ if sys.version_info < (3, 11): @overload @abstractmethod def open(self, mode: Literal["rb"]) -> IO[bytes]: ... + @property @abstractmethod def name(self) -> str: ... - if sys.version_info >= (3, 10): - def __truediv__(self, child: str, /) -> Traversable: ... - else: - @abstractmethod - def __truediv__(self, child: str, /) -> Traversable: ... - + def __truediv__(self, child: str, /) -> Traversable: ... @abstractmethod def read_bytes(self) -> bytes: ... @abstractmethod diff --git a/mypy/typeshed/stdlib/importlib/metadata/__init__.pyi b/mypy/typeshed/stdlib/importlib/metadata/__init__.pyi index bb1b22f11a624..866fd969e2fe5 100644 --- a/mypy/typeshed/stdlib/importlib/metadata/__init__.pyi +++ b/mypy/typeshed/stdlib/importlib/metadata/__init__.pyi @@ -5,13 +5,12 @@ import types from _collections_abc import dict_keys, dict_values from _typeshed import StrPath from collections.abc import Iterable, Iterator, Mapping -from email.message import Message from importlib.abc import MetaPathFinder +from importlib.metadata._meta import PackageMetadata as PackageMetadata, SimplePath from os import PathLike -from pathlib import Path from re import Pattern -from typing import Any, ClassVar, Generic, NamedTuple, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated, disjoint_base +from typing import Any, ClassVar, Generic, NamedTuple, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, deprecated, disjoint_base _T = TypeVar("_T") _KT = TypeVar("_KT") @@ -20,32 +19,32 @@ _VT = TypeVar("_VT") __all__ = [ "Distribution", "DistributionFinder", + "PackageMetadata", "PackageNotFoundError", "distribution", "distributions", "entry_points", "files", "metadata", + "packages_distributions", "requires", "version", ] -if sys.version_info >= (3, 10): - __all__ += ["PackageMetadata", "packages_distributions"] +if sys.version_info >= (3, 15): + __all__ += ["PackagePath", "MetadataNotFound", "SimplePath"] -if sys.version_info >= (3, 10): - from importlib.metadata._meta import PackageMetadata as PackageMetadata, SimplePath - def packages_distributions() -> Mapping[str, list[str]]: ... +_SimplePath: TypeAlias = SimplePath - _SimplePath: TypeAlias = SimplePath - -else: - _SimplePath: TypeAlias = Path +def packages_distributions() -> Mapping[str, list[str]]: ... class PackageNotFoundError(ModuleNotFoundError): @property def name(self) -> str: ... # type: ignore[override] +if sys.version_info >= (3, 15): + class MetadataNotFound(FileNotFoundError): ... + if sys.version_info >= (3, 13): _EntryPointBase = object elif sys.version_info >= (3, 11): @@ -104,19 +103,17 @@ else: def module(self) -> str: ... @property def attr(self) -> str: ... - if sys.version_info >= (3, 10): - dist: ClassVar[Distribution | None] - def matches( - self, - *, - name: str = ..., - value: str = ..., - group: str = ..., - module: str = ..., - attr: str = ..., - extras: list[str] = ..., - ) -> bool: ... # undocumented - + dist: ClassVar[Distribution | None] + def matches( + self, + *, + name: str = ..., + value: str = ..., + group: str = ..., + module: str = ..., + attr: str = ..., + extras: list[str] = ..., + ) -> bool: ... # undocumented def __hash__(self) -> int: ... def __iter__(self) -> Iterator[Any]: ... # result of iter((str, Self)), really @@ -139,7 +136,7 @@ if sys.version_info >= (3, 12): @property def groups(self) -> set[str]: ... -elif sys.version_info >= (3, 10): +else: class DeprecatedList(list[_T]): __slots__ = () @@ -162,15 +159,17 @@ elif sys.version_info >= (3, 10): @property def groups(self) -> set[str]: ... -if sys.version_info >= (3, 10) and sys.version_info < (3, 12): +if sys.version_info < (3, 12): class Deprecated(Generic[_KT, _VT]): def __getitem__(self, name: _KT) -> _VT: ... + @overload def get(self, name: _KT, default: None = None) -> _VT | None: ... @overload def get(self, name: _KT, default: _VT) -> _VT: ... @overload def get(self, name: _KT, default: _T) -> _VT | _T: ... + def __iter__(self) -> Iterator[_KT]: ... def __contains__(self, *args: object) -> bool: ... def keys(self) -> dict_keys[_KT, _VT]: ... @@ -184,6 +183,7 @@ if sys.version_info >= (3, 10) and sys.version_info < (3, 12): def groups(self) -> set[str]: ... @property def names(self) -> set[str]: ... + @overload def select(self) -> Self: ... @overload @@ -212,7 +212,9 @@ class FileHash: value: str def __init__(self, spec: str) -> None: ... -if sys.version_info >= (3, 12): +if sys.version_info >= (3, 15): + _distribution_parent = abc.ABC +elif sys.version_info >= (3, 12): class DeprecatedNonAbstract: ... _distribution_parent = DeprecatedNonAbstract else: @@ -225,6 +227,7 @@ class Distribution(_distribution_parent): def locate_file(self, path: StrPath) -> _SimplePath: ... @classmethod def from_name(cls, name: str) -> Distribution: ... + @overload @classmethod def discover(cls, *, context: DistributionFinder.Context) -> Iterable[Distribution]: ... @@ -233,29 +236,21 @@ class Distribution(_distribution_parent): def discover( cls, *, context: None = None, name: str | None = ..., path: list[str] = ..., **kwargs: Any ) -> Iterable[Distribution]: ... + @staticmethod def at(path: StrPath) -> PathDistribution: ... - - if sys.version_info >= (3, 10): - @property - def metadata(self) -> PackageMetadata: ... - @property - def entry_points(self) -> EntryPoints: ... - else: - @property - def metadata(self) -> Message: ... - @property - def entry_points(self) -> list[EntryPoint]: ... - + @property + def metadata(self) -> PackageMetadata: ... + @property + def entry_points(self) -> EntryPoints: ... @property def version(self) -> str: ... @property def files(self) -> list[PackagePath] | None: ... @property def requires(self) -> list[str] | None: ... - if sys.version_info >= (3, 10): - @property - def name(self) -> str: ... + @property + def name(self) -> str: ... if sys.version_info >= (3, 13): @property def origin(self) -> types.SimpleNamespace | None: ... @@ -276,7 +271,7 @@ class MetadataPathFinder(DistributionFinder): if sys.version_info >= (3, 11): @classmethod def invalidate_caches(cls) -> None: ... - elif sys.version_info >= (3, 10): + else: # Yes, this is an instance method that has a parameter named "cls" def invalidate_caches(cls) -> None: ... @@ -287,6 +282,7 @@ class PathDistribution(Distribution): def locate_file(self, path: StrPath) -> _SimplePath: ... def distribution(distribution_name: str) -> Distribution: ... + @overload def distributions(*, context: DistributionFinder.Context) -> Iterable[Distribution]: ... @overload @@ -294,18 +290,14 @@ def distributions( *, context: None = None, name: str | None = ..., path: list[str] = ..., **kwargs: Any ) -> Iterable[Distribution]: ... -if sys.version_info >= (3, 10): - def metadata(distribution_name: str) -> PackageMetadata: ... - -else: - def metadata(distribution_name: str) -> Message: ... +def metadata(distribution_name: str) -> PackageMetadata: ... if sys.version_info >= (3, 12): def entry_points( *, name: str = ..., value: str = ..., group: str = ..., module: str = ..., attr: str = ..., extras: list[str] = ... ) -> EntryPoints: ... -elif sys.version_info >= (3, 10): +else: @overload def entry_points() -> SelectableGroups: ... @overload @@ -313,9 +305,6 @@ elif sys.version_info >= (3, 10): *, name: str = ..., value: str = ..., group: str = ..., module: str = ..., attr: str = ..., extras: list[str] = ... ) -> EntryPoints: ... -else: - def entry_points() -> dict[str, list[EntryPoint]]: ... - def version(distribution_name: str) -> str: ... def files(distribution_name: str) -> list[PackagePath] | None: ... def requires(distribution_name: str) -> list[str] | None: ... diff --git a/mypy/typeshed/stdlib/importlib/metadata/_meta.pyi b/mypy/typeshed/stdlib/importlib/metadata/_meta.pyi index 9f791dab254fd..b9bad7b8a6b0e 100644 --- a/mypy/typeshed/stdlib/importlib/metadata/_meta.pyi +++ b/mypy/typeshed/stdlib/importlib/metadata/_meta.pyi @@ -15,10 +15,12 @@ class PackageMetadata(Protocol): def __iter__(self) -> Iterator[str]: ... @property def json(self) -> dict[str, str | list[str]]: ... + @overload def get_all(self, name: str, failobj: None = None) -> list[Any] | None: ... @overload def get_all(self, name: str, failobj: _T) -> list[Any] | _T: ... + if sys.version_info >= (3, 12): @overload def get(self, name: str, failobj: None = None) -> str | None: ... diff --git a/mypy/typeshed/stdlib/importlib/readers.pyi b/mypy/typeshed/stdlib/importlib/readers.pyi index 0e7f7ce165c3d..50f06ea5434d5 100644 --- a/mypy/typeshed/stdlib/importlib/readers.pyi +++ b/mypy/typeshed/stdlib/importlib/readers.pyi @@ -7,66 +7,63 @@ import sys import zipfile from _typeshed import StrPath from collections.abc import Iterable, Iterator +from importlib._bootstrap_external import FileLoader from io import BufferedReader from typing import Literal, NoReturn, TypeVar from typing_extensions import Never - -if sys.version_info >= (3, 10): - from importlib._bootstrap_external import FileLoader - from zipimport import zipimporter +from zipimport import zipimporter if sys.version_info >= (3, 11): from importlib.resources import abc else: from importlib import abc -if sys.version_info >= (3, 10): - if sys.version_info >= (3, 11): - __all__ = ["FileReader", "ZipReader", "MultiplexedPath", "NamespaceReader"] +if sys.version_info >= (3, 11): + __all__ = ["FileReader", "ZipReader", "MultiplexedPath", "NamespaceReader"] - if sys.version_info < (3, 11): - _T = TypeVar("_T") +if sys.version_info < (3, 11): + _T = TypeVar("_T") - def remove_duplicates(items: Iterable[_T]) -> Iterator[_T]: ... + def remove_duplicates(items: Iterable[_T]) -> Iterator[_T]: ... - class FileReader(abc.TraversableResources): - path: pathlib.Path - def __init__(self, loader: FileLoader) -> None: ... - def resource_path(self, resource: StrPath) -> str: ... - def files(self) -> pathlib.Path: ... +class FileReader(abc.TraversableResources): + path: pathlib.Path + def __init__(self, loader: FileLoader) -> None: ... + def resource_path(self, resource: StrPath) -> str: ... + def files(self) -> pathlib.Path: ... - class ZipReader(abc.TraversableResources): - prefix: str - archive: str - def __init__(self, loader: zipimporter, module: str) -> None: ... - def open_resource(self, resource: str) -> BufferedReader: ... - def is_resource(self, path: StrPath) -> bool: ... - def files(self) -> zipfile.Path: ... +class ZipReader(abc.TraversableResources): + prefix: str + archive: str + def __init__(self, loader: zipimporter, module: str) -> None: ... + def open_resource(self, resource: str) -> BufferedReader: ... + def is_resource(self, path: StrPath) -> bool: ... + def files(self) -> zipfile.Path: ... - class MultiplexedPath(abc.Traversable): - def __init__(self, *paths: abc.Traversable) -> None: ... - def iterdir(self) -> Iterator[abc.Traversable]: ... - def read_bytes(self) -> NoReturn: ... - def read_text(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] - def is_dir(self) -> Literal[True]: ... - def is_file(self) -> Literal[False]: ... +class MultiplexedPath(abc.Traversable): + def __init__(self, *paths: abc.Traversable) -> None: ... + def iterdir(self) -> Iterator[abc.Traversable]: ... + def read_bytes(self) -> NoReturn: ... + def read_text(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] + def is_dir(self) -> Literal[True]: ... + def is_file(self) -> Literal[False]: ... - if sys.version_info >= (3, 12): - def joinpath(self, *descendants: StrPath) -> abc.Traversable: ... - elif sys.version_info >= (3, 11): - def joinpath(self, child: StrPath) -> abc.Traversable: ... # type: ignore[override] - else: - def joinpath(self, child: str) -> abc.Traversable: ... + if sys.version_info >= (3, 12): + def joinpath(self, *descendants: StrPath) -> abc.Traversable: ... + elif sys.version_info >= (3, 11): + def joinpath(self, child: StrPath) -> abc.Traversable: ... # type: ignore[override] + else: + def joinpath(self, child: str) -> abc.Traversable: ... - if sys.version_info < (3, 12): - __truediv__ = joinpath + if sys.version_info < (3, 12): + __truediv__ = joinpath - def open(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] - @property - def name(self) -> str: ... + def open(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] + @property + def name(self) -> str: ... - class NamespaceReader(abc.TraversableResources): - path: MultiplexedPath - def __init__(self, namespace_path: Iterable[str]) -> None: ... - def resource_path(self, resource: str) -> str: ... - def files(self) -> MultiplexedPath: ... +class NamespaceReader(abc.TraversableResources): + path: MultiplexedPath + def __init__(self, namespace_path: Iterable[str]) -> None: ... + def resource_path(self, resource: str) -> str: ... + def files(self) -> MultiplexedPath: ... diff --git a/mypy/typeshed/stdlib/importlib/resources/__init__.pyi b/mypy/typeshed/stdlib/importlib/resources/__init__.pyi index 28adc37da4a42..5e58ddf310c93 100644 --- a/mypy/typeshed/stdlib/importlib/resources/__init__.pyi +++ b/mypy/typeshed/stdlib/importlib/resources/__init__.pyi @@ -4,8 +4,8 @@ from collections.abc import Iterator from contextlib import AbstractContextManager from pathlib import Path from types import ModuleType -from typing import Any, BinaryIO, Literal, TextIO -from typing_extensions import TypeAlias, deprecated +from typing import Any, BinaryIO, Literal, TextIO, TypeAlias +from typing_extensions import deprecated if sys.version_info >= (3, 11): from importlib.resources.abc import Traversable @@ -19,6 +19,7 @@ else: __all__ = [ "Package", + "ResourceReader", "as_file", "contents", "files", @@ -30,9 +31,6 @@ __all__ = [ "read_text", ] -if sys.version_info >= (3, 10): - __all__ += ["ResourceReader"] - if sys.version_info < (3, 13): __all__ += ["Resource"] @@ -64,11 +62,8 @@ else: def read_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> str: ... def path(package: Package, resource: Resource) -> AbstractContextManager[Path, Literal[False]]: ... def is_resource(package: Package, name: str) -> bool: ... - if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.11. Use `files(anchor).iterdir()`.") - def contents(package: Package) -> Iterator[str]: ... - else: - def contents(package: Package) -> Iterator[str]: ... + @deprecated("Deprecated since Python 3.11. Use `files(anchor).iterdir()`.") + def contents(package: Package) -> Iterator[str]: ... if sys.version_info >= (3, 11): from importlib.resources._common import as_file as as_file @@ -82,5 +77,5 @@ else: if sys.version_info >= (3, 11): from importlib.resources.abc import ResourceReader as ResourceReader -elif sys.version_info >= (3, 10): +else: from importlib.abc import ResourceReader as ResourceReader diff --git a/mypy/typeshed/stdlib/importlib/resources/_common.pyi b/mypy/typeshed/stdlib/importlib/resources/_common.pyi index 11a93ca82d8df..447cc1ea33b89 100644 --- a/mypy/typeshed/stdlib/importlib/resources/_common.pyi +++ b/mypy/typeshed/stdlib/importlib/resources/_common.pyi @@ -7,8 +7,8 @@ if sys.version_info >= (3, 11): from contextlib import AbstractContextManager from importlib.resources.abc import ResourceReader, Traversable from pathlib import Path - from typing import Literal, overload - from typing_extensions import TypeAlias, deprecated + from typing import Literal, TypeAlias, overload + from typing_extensions import deprecated Package: TypeAlias = str | types.ModuleType @@ -18,6 +18,7 @@ if sys.version_info >= (3, 11): def package_to_anchor( func: Callable[[Anchor | None], Traversable], ) -> Callable[[Anchor | None, Anchor | None], Traversable]: ... + @overload def files(anchor: Anchor | None = None) -> Traversable: ... @overload diff --git a/mypy/typeshed/stdlib/importlib/resources/_functional.pyi b/mypy/typeshed/stdlib/importlib/resources/_functional.pyi index 71e01bcd3d5ec..cfd15dc87ce85 100644 --- a/mypy/typeshed/stdlib/importlib/resources/_functional.pyi +++ b/mypy/typeshed/stdlib/importlib/resources/_functional.pyi @@ -12,19 +12,23 @@ if sys.version_info >= (3, 13): from typing_extensions import Unpack, deprecated def open_binary(anchor: Anchor, *path_names: StrPath) -> BinaryIO: ... + @overload def open_text( anchor: Anchor, *path_names: Unpack[tuple[StrPath]], encoding: str | None = "utf-8", errors: str | None = "strict" ) -> TextIOWrapper: ... @overload def open_text(anchor: Anchor, *path_names: StrPath, encoding: str | None, errors: str | None = "strict") -> TextIOWrapper: ... + def read_binary(anchor: Anchor, *path_names: StrPath) -> bytes: ... + @overload def read_text( anchor: Anchor, *path_names: Unpack[tuple[StrPath]], encoding: str | None = "utf-8", errors: str | None = "strict" ) -> str: ... @overload def read_text(anchor: Anchor, *path_names: StrPath, encoding: str | None, errors: str | None = "strict") -> str: ... + def path(anchor: Anchor, *path_names: StrPath) -> AbstractContextManager[Path, Literal[False]]: ... def is_resource(anchor: Anchor, *path_names: StrPath) -> bool: ... @deprecated("Deprecated since Python 3.11. Use `files(anchor).iterdir()`.") diff --git a/mypy/typeshed/stdlib/importlib/resources/abc.pyi b/mypy/typeshed/stdlib/importlib/resources/abc.pyi index 477339ea74291..20a4b1fc5dfef 100644 --- a/mypy/typeshed/stdlib/importlib/resources/abc.pyi +++ b/mypy/typeshed/stdlib/importlib/resources/abc.pyi @@ -38,14 +38,20 @@ if sys.version_info >= (3, 11): @overload @abstractmethod def open(self, mode: Literal["rb"]) -> IO[bytes]: ... + @property @abstractmethod def name(self) -> str: ... def __truediv__(self, child: StrPath, /) -> Traversable: ... @abstractmethod def read_bytes(self) -> bytes: ... - @abstractmethod - def read_text(self, encoding: str | None = None) -> str: ... + + if sys.version_info >= (3, 15): + @abstractmethod + def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ... + else: + @abstractmethod + def read_text(self, encoding: str | None = None) -> str: ... class TraversableResources(ResourceReader): @abstractmethod diff --git a/mypy/typeshed/stdlib/importlib/resources/simple.pyi b/mypy/typeshed/stdlib/importlib/resources/simple.pyi index 946987c7312f9..ef9882056a9c2 100644 --- a/mypy/typeshed/stdlib/importlib/resources/simple.pyi +++ b/mypy/typeshed/stdlib/importlib/resources/simple.pyi @@ -27,6 +27,7 @@ if sys.version_info >= (3, 11): def __init__(self, parent: ResourceContainer, name: str) -> None: ... def is_file(self) -> Literal[True]: ... def is_dir(self) -> Literal[False]: ... + @overload def open( self, @@ -41,6 +42,7 @@ if sys.version_info >= (3, 11): def open(self, mode: Literal["rb"]) -> BinaryIO: ... @overload def open(self, mode: str) -> IO[Any]: ... + def joinpath(self, name: Never) -> NoReturn: ... # type: ignore[override] class ResourceContainer(Traversable, metaclass=abc.ABCMeta): diff --git a/mypy/typeshed/stdlib/importlib/util.pyi b/mypy/typeshed/stdlib/importlib/util.pyi index 577d3a667eca8..785ba6b9a08f0 100644 --- a/mypy/typeshed/stdlib/importlib/util.pyi +++ b/mypy/typeshed/stdlib/importlib/util.pyi @@ -13,8 +13,8 @@ from importlib._bootstrap_external import ( ) from importlib.abc import Loader from types import TracebackType -from typing import Literal -from typing_extensions import ParamSpec, Self, deprecated +from typing import Literal, ParamSpec +from typing_extensions import Self, deprecated _P = ParamSpec("_P") diff --git a/mypy/typeshed/stdlib/inspect.pyi b/mypy/typeshed/stdlib/inspect.pyi index c48d63bc4f322..003ecc10f072e 100644 --- a/mypy/typeshed/stdlib/inspect.pyi +++ b/mypy/typeshed/stdlib/inspect.pyi @@ -3,7 +3,6 @@ import enum import sys import types from _typeshed import AnnotationForm, StrPath -from collections import OrderedDict from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine, Generator, Mapping, Sequence, Set as AbstractSet from types import ( AsyncGeneratorType, @@ -25,8 +24,21 @@ from types import ( TracebackType, WrapperDescriptorType, ) -from typing import Any, ClassVar, Final, Literal, NamedTuple, Protocol, TypeVar, overload, type_check_only -from typing_extensions import ParamSpec, Self, TypeAlias, TypeGuard, TypeIs, deprecated, disjoint_base +from typing import ( + Any, + ClassVar, + Final, + Literal, + NamedTuple, + ParamSpec, + Protocol, + TypeAlias, + TypeGuard, + TypeVar, + overload, + type_check_only, +) +from typing_extensions import Self, TypeIs, deprecated, disjoint_base if sys.version_info >= (3, 14): from annotationlib import Format @@ -183,8 +195,8 @@ if sys.version_info >= (3, 14): modulesbyfile: dict[str, Any] -_GetMembersPredicateTypeGuard: TypeAlias = Callable[[Any], TypeGuard[_T]] -_GetMembersPredicateTypeIs: TypeAlias = Callable[[Any], TypeIs[_T]] +_GetMembersPredicateTypeGuard = Callable[[Any], TypeGuard[_T]] +_GetMembersPredicateTypeIs = Callable[[Any], TypeIs[_T]] _GetMembersPredicate: TypeAlias = Callable[[Any], bool] _GetMembersReturn: TypeAlias = list[tuple[str, _T]] @@ -223,6 +235,7 @@ def isgeneratorfunction(obj: Callable[..., Generator[Any, Any, Any]]) -> bool: . def isgeneratorfunction(obj: Callable[_P, Any]) -> TypeGuard[Callable[_P, GeneratorType[Any, Any, Any]]]: ... @overload def isgeneratorfunction(obj: object) -> TypeGuard[Callable[..., GeneratorType[Any, Any, Any]]]: ... + @overload def iscoroutinefunction(obj: Callable[..., Coroutine[Any, Any, Any]]) -> bool: ... @overload @@ -231,15 +244,18 @@ def iscoroutinefunction(obj: Callable[_P, Awaitable[_T]]) -> TypeGuard[Callable[ def iscoroutinefunction(obj: Callable[_P, object]) -> TypeGuard[Callable[_P, CoroutineType[Any, Any, Any]]]: ... @overload def iscoroutinefunction(obj: object) -> TypeGuard[Callable[..., CoroutineType[Any, Any, Any]]]: ... + def isgenerator(object: object) -> TypeIs[GeneratorType[Any, Any, Any]]: ... def iscoroutine(object: object) -> TypeIs[CoroutineType[Any, Any, Any]]: ... def isawaitable(object: object) -> TypeIs[Awaitable[Any]]: ... + @overload def isasyncgenfunction(obj: Callable[..., AsyncGenerator[Any, Any]]) -> bool: ... @overload def isasyncgenfunction(obj: Callable[_P, Any]) -> TypeGuard[Callable[_P, AsyncGeneratorType[Any, Any]]]: ... @overload def isasyncgenfunction(obj: object) -> TypeGuard[Callable[..., AsyncGeneratorType[Any, Any]]]: ... + @type_check_only class _SupportsSet(Protocol[_T_contra, _V_contra]): def __set__(self, instance: _T_contra, value: _V_contra, /) -> None: ... @@ -293,7 +309,13 @@ def getblock(lines: list[str]) -> list[str]: ... def getblock(lines: tuple[str, ...]) -> tuple[str, ...]: ... @overload def getblock(lines: Sequence[str]) -> Sequence[str]: ... -def getdoc(object: object) -> str | None: ... + +if sys.version_info >= (3, 15): + def getdoc(object: object, *, inherit_class_doc: bool = True, fallback_to_class_doc: bool = True) -> str | None: ... + +else: + def getdoc(object: object) -> str | None: ... + def getcomments(object: object) -> str | None: ... def getfile(object: _SourceObjectType) -> str: ... def getmodule(object: object, _filename: str | None = None) -> ModuleType | None: ... @@ -319,7 +341,7 @@ if sys.version_info >= (3, 14): annotation_format: Format = Format.VALUE, # noqa: Y011 ) -> Signature: ... -elif sys.version_info >= (3, 10): +else: def signature( obj: _IntrospectableCallable, *, @@ -329,9 +351,6 @@ elif sys.version_info >= (3, 10): eval_str: bool = False, ) -> Signature: ... -else: - def signature(obj: _IntrospectableCallable, *, follow_wrapped: bool = True) -> Signature: ... - class _void: ... class _empty: ... @@ -361,7 +380,7 @@ class Signature: eval_str: bool = False, annotation_format: Format = Format.VALUE, # noqa: Y011 ) -> Self: ... - elif sys.version_info >= (3, 10): + else: @classmethod def from_callable( cls, @@ -372,9 +391,7 @@ class Signature: locals: Mapping[str, Any] | None = None, eval_str: bool = False, ) -> Self: ... - else: - @classmethod - def from_callable(cls, obj: _IntrospectableCallable, *, follow_wrapped: bool = True) -> Self: ... + if sys.version_info >= (3, 14): def format(self, *, max_width: int | None = None, quote_annotation_strings: bool = True) -> str: ... elif sys.version_info >= (3, 13): @@ -385,7 +402,7 @@ class Signature: if sys.version_info >= (3, 14): from annotationlib import get_annotations as get_annotations -elif sys.version_info >= (3, 10): +else: def get_annotations( obj: Callable[..., object] | type[object] | ModuleType, # any callable, class, or module *, @@ -450,14 +467,14 @@ class Parameter: class BoundArguments: __slots__ = ("arguments", "_signature", "__weakref__") - arguments: OrderedDict[str, Any] + arguments: dict[str, Any] @property def args(self) -> tuple[Any, ...]: ... @property def kwargs(self) -> dict[str, Any]: ... @property def signature(self) -> Signature: ... - def __init__(self, signature: Signature, arguments: OrderedDict[str, Any]) -> None: ... + def __init__(self, signature: Signature, arguments: dict[str, Any]) -> None: ... def apply_defaults(self) -> None: ... def __eq__(self, other: object) -> bool: ... __hash__: ClassVar[None] # type: ignore[assignment] @@ -498,7 +515,11 @@ class FullArgSpec(NamedTuple): kwonlydefaults: dict[str, Any] | None annotations: dict[str, Any] -def getfullargspec(func: object) -> FullArgSpec: ... +if sys.version_info >= (3, 15): + def getfullargspec(func: object, *, annotation_format: Format = Format.VALUE) -> FullArgSpec: ... # noqa: Y011 + +else: + def getfullargspec(func: object) -> FullArgSpec: ... class ArgInfo(NamedTuple): args: list[str] diff --git a/mypy/typeshed/stdlib/ipaddress.pyi b/mypy/typeshed/stdlib/ipaddress.pyi index d09804cb93423..c514abfa569f9 100644 --- a/mypy/typeshed/stdlib/ipaddress.pyi +++ b/mypy/typeshed/stdlib/ipaddress.pyi @@ -1,7 +1,7 @@ import sys from collections.abc import Iterable, Iterator -from typing import Any, Final, Generic, Literal, TypeVar, overload -from typing_extensions import Self, TypeAlias +from typing import Any, Final, Generic, Literal, TypeAlias, TypeVar, overload +from typing_extensions import Self # Undocumented length constants IPV4LENGTH: Final = 32 @@ -235,7 +235,9 @@ def summarize_address_range(first: IPv6Address, last: IPv6Address) -> Iterator[I def summarize_address_range( first: IPv4Address | IPv6Address, last: IPv4Address | IPv6Address ) -> Iterator[IPv4Network] | Iterator[IPv6Network]: ... + def collapse_addresses(addresses: Iterable[_N]) -> Iterator[_N]: ... + @overload def get_mixed_type_key(obj: _A) -> tuple[int, _A]: ... @overload diff --git a/mypy/typeshed/stdlib/itertools.pyi b/mypy/typeshed/stdlib/itertools.pyi index 4713d62cc346f..fd7daf11e8086 100644 --- a/mypy/typeshed/stdlib/itertools.pyi +++ b/mypy/typeshed/stdlib/itertools.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import MaybeNone from collections.abc import Callable, Iterable, Iterator from types import GenericAlias -from typing import Any, Generic, Literal, SupportsComplex, SupportsFloat, SupportsIndex, SupportsInt, TypeVar, overload -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import Any, Generic, Literal, SupportsComplex, SupportsFloat, SupportsIndex, SupportsInt, TypeAlias, TypeVar, overload +from typing_extensions import Self, disjoint_base _T = TypeVar("_T") _S = TypeVar("_S") @@ -23,7 +23,7 @@ _T10 = TypeVar("_T10") _Step: TypeAlias = SupportsFloat | SupportsInt | SupportsIndex | SupportsComplex -_Predicate: TypeAlias = Callable[[_T], object] +_Predicate = Callable[[_T], object] # Technically count can take anything that implements a number protocol and has an add method # but we can't enforce the add method @@ -35,6 +35,7 @@ class count(Iterator[_N]): def __new__(cls, start: _N, step: _Step = 1) -> count[_N]: ... @overload def __new__(cls, *, step: _N) -> count[_N]: ... + def __next__(self) -> _N: ... def __iter__(self) -> Self: ... @@ -50,6 +51,7 @@ class repeat(Iterator[_T]): def __new__(cls, object: _T) -> Self: ... @overload def __new__(cls, object: _T, times: int) -> Self: ... + def __next__(self) -> _T: ... def __iter__(self) -> Self: ... def __length_hint__(self) -> int: ... @@ -60,6 +62,7 @@ class accumulate(Iterator[_T]): def __new__(cls, iterable: Iterable[_T], func: None = None, *, initial: _T | None = None) -> Self: ... @overload def __new__(cls, iterable: Iterable[_S], func: Callable[[_T, _S], _T], *, initial: _T | None = None) -> Self: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T: ... @@ -97,6 +100,7 @@ class groupby(Iterator[tuple[_T_co, Iterator[_S_co]]], Generic[_T_co, _S_co]): def __new__(cls, iterable: Iterable[_T1], key: None = None) -> groupby[_T1, _T1]: ... @overload def __new__(cls, iterable: Iterable[_T1], key: Callable[[_T1], _T2]) -> groupby[_T2, _T1]: ... + def __iter__(self) -> Self: ... def __next__(self) -> tuple[_T_co, Iterator[_S_co]]: ... @@ -106,6 +110,7 @@ class islice(Iterator[_T]): def __new__(cls, iterable: Iterable[_T], stop: int | None, /) -> Self: ... @overload def __new__(cls, iterable: Iterable[_T], start: int | None, stop: int | None, step: int | None = 1, /) -> Self: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T: ... @@ -122,6 +127,7 @@ class takewhile(Iterator[_T]): def __next__(self) -> _T: ... def tee(iterable: Iterable[_T], n: int = 2, /) -> tuple[Iterator[_T], ...]: ... + @disjoint_base class zip_longest(Iterator[_T_co]): # one iterable (fillvalue doesn't matter) @@ -198,6 +204,7 @@ class zip_longest(Iterator[_T_co]): *iterables: Iterable[_T], fillvalue: _T, ) -> zip_longest[tuple[_T, ...]]: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T_co: ... @@ -284,6 +291,7 @@ class product(Iterator[_T_co]): ) -> product[tuple[_T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8, _T9, _T10]]: ... @overload def __new__(cls, *iterables: Iterable[_T1], repeat: int = 1) -> product[tuple[_T1, ...]]: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T_co: ... @@ -299,6 +307,7 @@ class permutations(Iterator[_T_co]): def __new__(cls, iterable: Iterable[_T], r: Literal[5]) -> permutations[tuple[_T, _T, _T, _T, _T]]: ... @overload def __new__(cls, iterable: Iterable[_T], r: int | None = None) -> permutations[tuple[_T, ...]]: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T_co: ... @@ -314,6 +323,7 @@ class combinations(Iterator[_T_co]): def __new__(cls, iterable: Iterable[_T], r: Literal[5]) -> combinations[tuple[_T, _T, _T, _T, _T]]: ... @overload def __new__(cls, iterable: Iterable[_T], r: int) -> combinations[tuple[_T, ...]]: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T_co: ... @@ -329,15 +339,15 @@ class combinations_with_replacement(Iterator[_T_co]): def __new__(cls, iterable: Iterable[_T], r: Literal[5]) -> combinations_with_replacement[tuple[_T, _T, _T, _T, _T]]: ... @overload def __new__(cls, iterable: Iterable[_T], r: int) -> combinations_with_replacement[tuple[_T, ...]]: ... + def __iter__(self) -> Self: ... def __next__(self) -> _T_co: ... -if sys.version_info >= (3, 10): - @disjoint_base - class pairwise(Iterator[_T_co]): - def __new__(cls, iterable: Iterable[_T], /) -> pairwise[tuple[_T, _T]]: ... - def __iter__(self) -> Self: ... - def __next__(self) -> _T_co: ... +@disjoint_base +class pairwise(Iterator[_T_co]): + def __new__(cls, iterable: Iterable[_T], /) -> pairwise[tuple[_T, _T]]: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... if sys.version_info >= (3, 12): @disjoint_base diff --git a/mypy/typeshed/stdlib/json/__init__.pyi b/mypy/typeshed/stdlib/json/__init__.pyi index 454a235ecf703..2342b29cb0c82 100644 --- a/mypy/typeshed/stdlib/json/__init__.pyi +++ b/mypy/typeshed/stdlib/json/__init__.pyi @@ -1,3 +1,4 @@ +import sys from _typeshed import SupportsRead, SupportsWrite from collections.abc import Callable from typing import Any, Literal @@ -36,28 +37,57 @@ def dump( sort_keys: bool = False, **kwds: Any, ) -> None: ... -def loads( - s: str | bytes | bytearray, - *, - cls: type[JSONDecoder] | None = None, - object_hook: Callable[[dict[Any, Any]], Any] | None = None, - parse_float: Callable[[str], Any] | None = None, - parse_int: Callable[[str], Any] | None = None, - parse_constant: Callable[[str], Any] | None = None, - object_pairs_hook: Callable[[list[tuple[Any, Any]]], Any] | None = None, - **kwds: Any, -) -> Any: ... -def load( - fp: SupportsRead[str | bytes], - *, - cls: type[JSONDecoder] | None = None, - object_hook: Callable[[dict[Any, Any]], Any] | None = None, - parse_float: Callable[[str], Any] | None = None, - parse_int: Callable[[str], Any] | None = None, - parse_constant: Callable[[str], Any] | None = None, - object_pairs_hook: Callable[[list[tuple[Any, Any]]], Any] | None = None, - **kwds: Any, -) -> Any: ... + +if sys.version_info >= (3, 15): + def loads( + s: str | bytes | bytearray, + *, + cls: type[JSONDecoder] | None = None, + object_hook: Callable[[dict[Any, Any]], Any] | None = None, + parse_float: Callable[[str], Any] | None = None, + parse_int: Callable[[str], Any] | None = None, + parse_constant: Callable[[str], Any] | None = None, + object_pairs_hook: Callable[[list[tuple[Any, Any]]], Any] | None = None, + array_hook: Callable[[list[Any]], Any] | None = None, + **kwds: Any, + ) -> Any: ... + def load( + fp: SupportsRead[str | bytes], + *, + cls: type[JSONDecoder] | None = None, + object_hook: Callable[[dict[Any, Any]], Any] | None = None, + parse_float: Callable[[str], Any] | None = None, + parse_int: Callable[[str], Any] | None = None, + parse_constant: Callable[[str], Any] | None = None, + object_pairs_hook: Callable[[list[tuple[Any, Any]]], Any] | None = None, + array_hook: Callable[[list[Any]], Any] | None = None, + **kwds: Any, + ) -> Any: ... + +else: + def loads( + s: str | bytes | bytearray, + *, + cls: type[JSONDecoder] | None = None, + object_hook: Callable[[dict[Any, Any]], Any] | None = None, + parse_float: Callable[[str], Any] | None = None, + parse_int: Callable[[str], Any] | None = None, + parse_constant: Callable[[str], Any] | None = None, + object_pairs_hook: Callable[[list[tuple[Any, Any]]], Any] | None = None, + **kwds: Any, + ) -> Any: ... + def load( + fp: SupportsRead[str | bytes], + *, + cls: type[JSONDecoder] | None = None, + object_hook: Callable[[dict[Any, Any]], Any] | None = None, + parse_float: Callable[[str], Any] | None = None, + parse_int: Callable[[str], Any] | None = None, + parse_constant: Callable[[str], Any] | None = None, + object_pairs_hook: Callable[[list[tuple[Any, Any]]], Any] | None = None, + **kwds: Any, + ) -> Any: ... + def detect_encoding( b: bytes | bytearray, ) -> Literal["utf-8", "utf-8-sig", "utf-16", "utf-16-be", "utf-16-le", "utf-32", "utf-32-be", "utf-32-le"]: ... # undocumented diff --git a/mypy/typeshed/stdlib/json/decoder.pyi b/mypy/typeshed/stdlib/json/decoder.pyi index 8debfe6cd65a9..1b09579fb0c41 100644 --- a/mypy/typeshed/stdlib/json/decoder.pyi +++ b/mypy/typeshed/stdlib/json/decoder.pyi @@ -1,3 +1,4 @@ +import sys from collections.abc import Callable from typing import Any @@ -12,21 +13,38 @@ class JSONDecodeError(ValueError): def __init__(self, msg: str, doc: str, pos: int) -> None: ... class JSONDecoder: + if sys.version_info >= (3, 15): + array_hook: Callable[[list[Any]], Any] | None object_hook: Callable[[dict[str, Any]], Any] parse_float: Callable[[str], Any] parse_int: Callable[[str], Any] parse_constant: Callable[[str], Any] strict: bool object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] - def __init__( - self, - *, - object_hook: Callable[[dict[str, Any]], Any] | None = None, - parse_float: Callable[[str], Any] | None = None, - parse_int: Callable[[str], Any] | None = None, - parse_constant: Callable[[str], Any] | None = None, - strict: bool = True, - object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None, - ) -> None: ... + if sys.version_info >= (3, 15): + def __init__( + self, + *, + object_hook: Callable[[dict[str, Any]], Any] | None = None, + parse_float: Callable[[str], Any] | None = None, + parse_int: Callable[[str], Any] | None = None, + parse_constant: Callable[[str], Any] | None = None, + strict: bool = True, + object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None, + array_hook: Callable[[list[Any]], Any] | None = None, + ) -> None: ... + + else: + def __init__( + self, + *, + object_hook: Callable[[dict[str, Any]], Any] | None = None, + parse_float: Callable[[str], Any] | None = None, + parse_int: Callable[[str], Any] | None = None, + parse_constant: Callable[[str], Any] | None = None, + strict: bool = True, + object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None, + ) -> None: ... + def decode(self, s: str, _w: Callable[..., Any] = ...) -> Any: ... # _w is undocumented def raw_decode(self, s: str, idx: int = 0) -> tuple[Any, int]: ... diff --git a/mypy/typeshed/stdlib/lib2to3/pgen2/__init__.pyi b/mypy/typeshed/stdlib/lib2to3/pgen2/__init__.pyi index de8a874f434d0..3b1cbef7727d2 100644 --- a/mypy/typeshed/stdlib/lib2to3/pgen2/__init__.pyi +++ b/mypy/typeshed/stdlib/lib2to3/pgen2/__init__.pyi @@ -1,6 +1,5 @@ from collections.abc import Callable -from typing import Any -from typing_extensions import TypeAlias +from typing import Any, TypeAlias from ..pytree import _RawNode from .grammar import Grammar diff --git a/mypy/typeshed/stdlib/lib2to3/pgen2/grammar.pyi b/mypy/typeshed/stdlib/lib2to3/pgen2/grammar.pyi index bef0a7922683b..5093422ae2364 100644 --- a/mypy/typeshed/stdlib/lib2to3/pgen2/grammar.pyi +++ b/mypy/typeshed/stdlib/lib2to3/pgen2/grammar.pyi @@ -1,5 +1,6 @@ from _typeshed import StrPath -from typing_extensions import Self, TypeAlias +from typing import TypeAlias +from typing_extensions import Self _Label: TypeAlias = tuple[int, str | None] _DFA: TypeAlias = list[list[tuple[int, int]]] diff --git a/mypy/typeshed/stdlib/lib2to3/pgen2/parse.pyi b/mypy/typeshed/stdlib/lib2to3/pgen2/parse.pyi index 320c5f018d43f..9befe9bf879d3 100644 --- a/mypy/typeshed/stdlib/lib2to3/pgen2/parse.pyi +++ b/mypy/typeshed/stdlib/lib2to3/pgen2/parse.pyi @@ -1,6 +1,6 @@ from _typeshed import Incomplete from collections.abc import Sequence -from typing_extensions import TypeAlias +from typing import TypeAlias from ..pytree import _NL, _RawNode from . import _Convert diff --git a/mypy/typeshed/stdlib/lib2to3/pgen2/pgen.pyi b/mypy/typeshed/stdlib/lib2to3/pgen2/pgen.pyi index 5776d100d1da0..4b951e1489e15 100644 --- a/mypy/typeshed/stdlib/lib2to3/pgen2/pgen.pyi +++ b/mypy/typeshed/stdlib/lib2to3/pgen2/pgen.pyi @@ -29,6 +29,7 @@ class ParserGenerator: def parse_atom(self) -> tuple[NFAState, NFAState]: ... def expect(self, type: int, value: str | None = None) -> str: ... def gettoken(self) -> None: ... + @overload def raise_error(self, msg: object) -> NoReturn: ... @overload diff --git a/mypy/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi b/mypy/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi index af54de1b51d33..76ff733163afe 100644 --- a/mypy/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi +++ b/mypy/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi @@ -1,5 +1,5 @@ from collections.abc import Callable, Iterable, Iterator -from typing_extensions import TypeAlias +from typing import TypeAlias from .token import * diff --git a/mypy/typeshed/stdlib/lib2to3/pytree.pyi b/mypy/typeshed/stdlib/lib2to3/pytree.pyi index 51bdbc75e1421..045f7882721a1 100644 --- a/mypy/typeshed/stdlib/lib2to3/pytree.pyi +++ b/mypy/typeshed/stdlib/lib2to3/pytree.pyi @@ -1,8 +1,8 @@ from _typeshed import Incomplete, SupportsGetItem, SupportsLenAndGetItem, Unused from abc import abstractmethod from collections.abc import Iterable, Iterator, MutableSequence -from typing import ClassVar, Final -from typing_extensions import Self, TypeAlias +from typing import ClassVar, Final, TypeAlias +from typing_extensions import Self from .fixer_base import BaseFix from .pgen2.grammar import Grammar diff --git a/mypy/typeshed/stdlib/lib2to3/refactor.pyi b/mypy/typeshed/stdlib/lib2to3/refactor.pyi index c33347ede38fd..dfec28b758b5d 100644 --- a/mypy/typeshed/stdlib/lib2to3/refactor.pyi +++ b/mypy/typeshed/stdlib/lib2to3/refactor.pyi @@ -40,14 +40,17 @@ class RefactoringTool: ) -> None: ... def get_fixers(self) -> tuple[list[BaseFix], list[BaseFix]]: ... def log_error(self, msg: str, *args: Iterable[str], **kwargs: _ExcInfoType) -> NoReturn: ... + @overload def log_message(self, msg: object) -> None: ... @overload def log_message(self, msg: str, *args: object) -> None: ... + @overload def log_debug(self, msg: object) -> None: ... @overload def log_debug(self, msg: str, *args: object) -> None: ... + def print_output(self, old_text: str, new_text: str, filename: StrPath, equal: bool) -> None: ... def refactor(self, items: Iterable[str], write: bool = False, doctests_only: bool = False) -> None: ... def refactor_dir(self, dir_name: str, write: bool = False, doctests_only: bool = False) -> None: ... diff --git a/mypy/typeshed/stdlib/linecache.pyi b/mypy/typeshed/stdlib/linecache.pyi index 5379a21e7d123..f527e7084ced8 100644 --- a/mypy/typeshed/stdlib/linecache.pyi +++ b/mypy/typeshed/stdlib/linecache.pyi @@ -1,6 +1,5 @@ from collections.abc import Callable -from typing import Any -from typing_extensions import TypeAlias +from typing import Any, TypeAlias __all__ = ["getline", "clearcache", "checkcache", "lazycache"] diff --git a/mypy/typeshed/stdlib/locale.pyi b/mypy/typeshed/stdlib/locale.pyi index 80c39a532dc86..1be68ec516344 100644 --- a/mypy/typeshed/stdlib/locale.pyi +++ b/mypy/typeshed/stdlib/locale.pyi @@ -138,11 +138,8 @@ def getpreferredencoding(do_setlocale: bool = True) -> _str: ... def normalize(localename: _str) -> _str: ... if sys.version_info < (3, 13): - if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `locale.setlocale(locale.LC_ALL, '')` instead.") - def resetlocale(category: int = ...) -> None: ... - else: - def resetlocale(category: int = ...) -> None: ... + @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `locale.setlocale(locale.LC_ALL, '')` instead.") + def resetlocale(category: int = ...) -> None: ... if sys.version_info < (3, 12): @deprecated("Deprecated since Python 3.7; removed in Python 3.12. Use `locale.format_string()` instead.") @@ -153,10 +150,7 @@ if sys.version_info < (3, 12): def format_string(f: _str, val: Any, grouping: bool = False, monetary: bool = False) -> _str: ... def currency(val: float | Decimal, symbol: bool = True, grouping: bool = False, international: bool = False) -> _str: ... def delocalize(string: _str) -> _str: ... - -if sys.version_info >= (3, 10): - def localize(string: _str, grouping: bool = False, monetary: bool = False) -> _str: ... - +def localize(string: _str, grouping: bool = False, monetary: bool = False) -> _str: ... def atof(string: _str, func: Callable[[_str], float] = ...) -> float: ... def atoi(string: _str) -> int: ... def str(val: float) -> _str: ... diff --git a/mypy/typeshed/stdlib/logging/__init__.pyi b/mypy/typeshed/stdlib/logging/__init__.pyi index 89c94816a906c..8e90a68d9453c 100644 --- a/mypy/typeshed/stdlib/logging/__init__.pyi +++ b/mypy/typeshed/stdlib/logging/__init__.pyi @@ -7,8 +7,8 @@ from re import Pattern from string import Template from time import struct_time from types import FrameType, GenericAlias, TracebackType -from typing import Any, ClassVar, Final, Generic, Literal, Protocol, TextIO, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated +from typing import Any, ClassVar, Final, Generic, Literal, Protocol, TextIO, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, deprecated __all__ = [ "BASIC_FORMAT", @@ -274,21 +274,15 @@ class Formatter: default_time_format: str default_msec_format: str | None - if sys.version_info >= (3, 10): - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - style: _FormatStyle = "%", - validate: bool = True, - *, - defaults: Mapping[str, Any] | None = None, - ) -> None: ... - else: - def __init__( - self, fmt: str | None = None, datefmt: str | None = None, style: _FormatStyle = "%", validate: bool = True - ) -> None: ... - + def __init__( + self, + fmt: str | None = None, + datefmt: str | None = None, + style: _FormatStyle = "%", + validate: bool = True, + *, + defaults: Mapping[str, Any] | None = None, + ) -> None: ... def format(self, record: LogRecord) -> str: ... def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str: ... def formatException(self, ei: _SysExcInfoType) -> str: ... @@ -362,18 +356,12 @@ _L = TypeVar("_L", bound=Logger | LoggerAdapter[Any]) class LoggerAdapter(Generic[_L]): logger: _L manager: Manager # undocumented + extra: Mapping[str, object] | None if sys.version_info >= (3, 13): def __init__(self, logger: _L, extra: Mapping[str, object] | None = None, merge_extra: bool = False) -> None: ... - elif sys.version_info >= (3, 10): - def __init__(self, logger: _L, extra: Mapping[str, object] | None = None) -> None: ... else: - def __init__(self, logger: _L, extra: Mapping[str, object]) -> None: ... - - if sys.version_info >= (3, 10): - extra: Mapping[str, object] | None - else: - extra: Mapping[str, object] + def __init__(self, logger: _L, extra: Mapping[str, object] | None = None) -> None: ... if sys.version_info >= (3, 13): merge_extra: bool @@ -566,6 +554,7 @@ fatal = critical def disable(level: int = 50) -> None: ... def addLevelName(level: int, levelName: str) -> None: ... + @overload def getLevelName(level: int) -> str: ... @overload @@ -576,6 +565,7 @@ if sys.version_info >= (3, 11): def getLevelNamesMapping() -> dict[str, int]: ... def makeLogRecord(dict: Mapping[str, object]) -> LogRecord: ... + @overload # handlers is non-None def basicConfig( *, @@ -611,6 +601,7 @@ def basicConfig( handlers: None = None, force: bool | None = False, ) -> None: ... + def shutdown(handlerList: Sequence[Any] = ...) -> None: ... # handlerList is undocumented def setLoggerClass(klass: type[Logger]) -> None: ... def captureWarnings(capture: bool) -> None: ... @@ -623,10 +614,12 @@ _StreamT = TypeVar("_StreamT", bound=SupportsWrite[str]) class StreamHandler(Handler, Generic[_StreamT]): stream: _StreamT # undocumented terminator: str + @overload def __init__(self: StreamHandler[TextIO], stream: None = None) -> None: ... @overload def __init__(self: StreamHandler[_StreamT], stream: _StreamT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + def setStream(self, stream: _StreamT) -> _StreamT | None: ... if sys.version_info >= (3, 11): def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @@ -663,11 +656,8 @@ class PercentStyle: # undocumented asctime_search: str validation_pattern: Pattern[str] _fmt: str - if sys.version_info >= (3, 10): - def __init__(self, fmt: str, *, defaults: Mapping[str, Any] | None = None) -> None: ... - else: - def __init__(self, fmt: str) -> None: ... + def __init__(self, fmt: str, *, defaults: Mapping[str, Any] | None = None) -> None: ... def usesTime(self) -> bool: ... def validate(self) -> None: ... def format(self, record: Any) -> str: ... diff --git a/mypy/typeshed/stdlib/logging/config.pyi b/mypy/typeshed/stdlib/logging/config.pyi index e362145516001..1c870edc80f8b 100644 --- a/mypy/typeshed/stdlib/logging/config.pyi +++ b/mypy/typeshed/stdlib/logging/config.pyi @@ -4,8 +4,8 @@ from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence from configparser import RawConfigParser from re import Pattern from threading import Thread -from typing import IO, Any, Final, Literal, SupportsIndex, TypedDict, overload, type_check_only -from typing_extensions import Required, TypeAlias, disjoint_base +from typing import IO, Any, Final, Literal, SupportsIndex, TypeAlias, TypedDict, overload, type_check_only +from typing_extensions import Required, disjoint_base from . import Filter, Filterer, Formatter, Handler, Logger, _FilterType, _FormatStyle, _Level @@ -64,22 +64,12 @@ class _DictConfigArgs(TypedDict, total=False): # Also accept a TypedDict type, to allow callers to use TypedDict # types, and for somewhat stricter type checking of dict literals. def dictConfig(config: _DictConfigArgs | dict[str, Any]) -> None: ... - -if sys.version_info >= (3, 10): - def fileConfig( - fname: StrOrBytesPath | IO[str] | RawConfigParser, - defaults: Mapping[str, str] | None = None, - disable_existing_loggers: bool = True, - encoding: str | None = None, - ) -> None: ... - -else: - def fileConfig( - fname: StrOrBytesPath | IO[str] | RawConfigParser, - defaults: Mapping[str, str] | None = None, - disable_existing_loggers: bool = True, - ) -> None: ... - +def fileConfig( + fname: StrOrBytesPath | IO[str] | RawConfigParser, + defaults: Mapping[str, str] | None = None, + disable_existing_loggers: bool = True, + encoding: str | None = None, +) -> None: ... def valid_ident(s: str) -> Literal[True]: ... # undocumented def listen(port: int = 9030, verify: Callable[[bytes], bytes | None] | None = None) -> Thread: ... def stopListening() -> None: ... @@ -98,6 +88,7 @@ class ConvertingList(list[Any], ConvertingMixin): # undocumented def __getitem__(self, key: SupportsIndex) -> Any: ... @overload def __getitem__(self, key: slice[SupportsIndex | None]) -> Any: ... + def pop(self, idx: SupportsIndex = -1) -> Any: ... if sys.version_info >= (3, 12): diff --git a/mypy/typeshed/stdlib/lzma.pyi b/mypy/typeshed/stdlib/lzma.pyi index b7ef607b75cbf..656ecb168c956 100644 --- a/mypy/typeshed/stdlib/lzma.pyi +++ b/mypy/typeshed/stdlib/lzma.pyi @@ -36,8 +36,8 @@ from _lzma import ( ) from _typeshed import ReadableBuffer, StrOrBytesPath from io import TextIOWrapper -from typing import IO, Literal, overload -from typing_extensions import Self, TypeAlias +from typing import IO, Literal, TypeAlias, overload +from typing_extensions import Self if sys.version_info >= (3, 14): from compression._common._streams import BaseStream @@ -172,6 +172,7 @@ def open( errors: str | None = None, newline: str | None = None, ) -> LZMAFile | TextIOWrapper: ... + def compress( data: ReadableBuffer, format: int = 1, check: int = -1, preset: int | None = None, filters: _FilterChain | None = None ) -> bytes: ... diff --git a/mypy/typeshed/stdlib/mailbox.pyi b/mypy/typeshed/stdlib/mailbox.pyi index 89bd998b4dfeb..961c0f5e81f7a 100644 --- a/mypy/typeshed/stdlib/mailbox.pyi +++ b/mypy/typeshed/stdlib/mailbox.pyi @@ -1,13 +1,12 @@ import email.message import io import sys -from _typeshed import StrPath, SupportsNoArgReadline, SupportsRead +from _typeshed import StrPath, SupportsItems, SupportsNoArgReadline, SupportsRead, SupportsWrite, Unused from abc import ABCMeta, abstractmethod from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence -from email._policybase import _MessageT from types import GenericAlias, TracebackType -from typing import IO, Any, AnyStr, Generic, Literal, Protocol, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, Generic, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self __all__ = [ "Mailbox", @@ -34,25 +33,49 @@ _T = TypeVar("_T") @type_check_only class _SupportsReadAndReadline(SupportsRead[bytes], SupportsNoArgReadline[bytes], Protocol): ... +# As opposed to _MessageT_co in email._policybase, this type is bound to +# mailbox.Message instead of email.message.Message. +_MessageT_co = TypeVar("_MessageT_co", bound=Message, default=Message, covariant=True) + _MessageData: TypeAlias = email.message.Message | bytes | str | io.StringIO | _SupportsReadAndReadline @type_check_only class _HasIteritems(Protocol): def iteritems(self) -> Iterator[tuple[str, _MessageData]]: ... -@type_check_only -class _HasItems(Protocol): - def items(self) -> Iterator[tuple[str, _MessageData]]: ... - linesep: bytes -class Mailbox(Generic[_MessageT]): +# Common interface for get_file() return types. +@type_check_only +class _GetFileReturn(Protocol): + def __iter__(self) -> Iterator[bytes]: ... + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, / + ) -> bool | None: ... + def read(self, size: int | None = None, /) -> bytes: ... + def read1(self, size: int | None = None, /) -> bytes: ... + def readline(self, size: int | None = None, /) -> bytes: ... + def readlines(self, sizehint: int | None = None, /) -> list[bytes]: ... + def tell(self) -> int: ... + def seek(self, offset: int, whence: int = 0, /) -> object: ... + def close(self) -> object: ... + def readable(self) -> bool: ... + def writable(self) -> bool: ... + def seekable(self) -> bool: ... + def flush(self) -> object: ... + @property + def closed(self) -> bool: ... + +class Mailbox(Generic[_MessageT_co]): _path: str # undocumented - _factory: Callable[[IO[Any]], _MessageT] | None # undocumented + _factory: Callable[[_GetFileReturn], _MessageT_co] | None # undocumented + @overload - def __init__(self, path: StrPath, factory: Callable[[IO[Any]], _MessageT], create: bool = True) -> None: ... + def __init__(self, path: StrPath, factory: Callable[[_GetFileReturn], _MessageT_co], create: bool = True) -> None: ... @overload def __init__(self, path: StrPath, factory: None = None, create: bool = True) -> None: ... + @abstractmethod def add(self, message: _MessageData) -> str: ... @abstractmethod @@ -61,38 +84,43 @@ class Mailbox(Generic[_MessageT]): def discard(self, key: str) -> None: ... @abstractmethod def __setitem__(self, key: str, message: _MessageData) -> None: ... + @overload - def get(self, key: str, default: None = None) -> _MessageT | None: ... + def get(self, key: str, default: None = None) -> _MessageT_co | None: ... @overload - def get(self, key: str, default: _T) -> _MessageT | _T: ... - def __getitem__(self, key: str) -> _MessageT: ... + def get(self, key: str, default: _T) -> _MessageT_co | _T: ... + + def __getitem__(self, key: str) -> _MessageT_co: ... @abstractmethod - def get_message(self, key: str) -> _MessageT: ... + def get_message(self, key: str) -> _MessageT_co: ... def get_string(self, key: str) -> str: ... @abstractmethod def get_bytes(self, key: str) -> bytes: ... - # As '_ProxyFile' doesn't implement the full IO spec, and BytesIO is incompatible with it, get_file return is Any here @abstractmethod - def get_file(self, key: str) -> Any: ... + def get_file(self, key: str) -> _GetFileReturn: ... @abstractmethod def iterkeys(self) -> Iterator[str]: ... def keys(self) -> list[str]: ... - def itervalues(self) -> Iterator[_MessageT]: ... - def __iter__(self) -> Iterator[_MessageT]: ... - def values(self) -> list[_MessageT]: ... - def iteritems(self) -> Iterator[tuple[str, _MessageT]]: ... - def items(self) -> list[tuple[str, _MessageT]]: ... + def itervalues(self) -> Iterator[_MessageT_co]: ... + def __iter__(self) -> Iterator[_MessageT_co]: ... + def values(self) -> list[_MessageT_co]: ... + def iteritems(self) -> Iterator[tuple[str, _MessageT_co]]: ... + def items(self) -> list[tuple[str, _MessageT_co]]: ... @abstractmethod def __contains__(self, key: str) -> bool: ... @abstractmethod def __len__(self) -> int: ... def clear(self) -> None: ... + @overload - def pop(self, key: str, default: None = None) -> _MessageT | None: ... + def pop(self, key: str, default: None = None) -> _MessageT_co | None: ... @overload - def pop(self, key: str, default: _T) -> _MessageT | _T: ... - def popitem(self) -> tuple[str, _MessageT]: ... - def update(self, arg: _HasIteritems | _HasItems | Iterable[tuple[str, _MessageData]] | None = None) -> None: ... + def pop(self, key: str, default: _T) -> _MessageT_co | _T: ... + + def popitem(self) -> tuple[str, _MessageT_co]: ... + def update( + self, arg: _HasIteritems | SupportsItems[str, _MessageData] | Iterable[tuple[str, _MessageData]] | None = None + ) -> None: ... @abstractmethod def flush(self) -> None: ... @abstractmethod @@ -101,19 +129,21 @@ class Mailbox(Generic[_MessageT]): def unlock(self) -> None: ... @abstractmethod def close(self) -> None: ... + # Undocumented, called by subclasses to parse added messages. + def _dump_message(self, message: _MessageData, target: SupportsWrite[bytes], mangle_from_: bool = False) -> None: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... class Maildir(Mailbox[MaildirMessage]): colon: str def __init__( - self, dirname: StrPath, factory: Callable[[IO[Any]], MaildirMessage] | None = None, create: bool = True + self, dirname: StrPath, factory: Callable[[_GetFileReturn], MaildirMessage] | None = None, create: bool = True ) -> None: ... - def add(self, message: _MessageData) -> str: ... + def add(self, message: _MessageData | MaildirMessage) -> str: ... def remove(self, key: str) -> None: ... - def __setitem__(self, key: str, message: _MessageData) -> None: ... + def __setitem__(self, key: str, message: _MessageData | MaildirMessage) -> None: ... def get_message(self, key: str) -> MaildirMessage: ... def get_bytes(self, key: str) -> bytes: ... - def get_file(self, key: str) -> _ProxyFile[bytes]: ... + def get_file(self, key: str) -> _ProxyFile: ... if sys.version_info >= (3, 13): def get_info(self, key: str) -> str: ... def set_info(self, key: str, info: str) -> None: ... @@ -136,7 +166,7 @@ class Maildir(Mailbox[MaildirMessage]): def clean(self) -> None: ... def next(self) -> str | None: ... -class _singlefileMailbox(Mailbox[_MessageT], metaclass=ABCMeta): +class _singlefileMailbox(Mailbox[_MessageT_co], metaclass=ABCMeta): def add(self, message: _MessageData) -> str: ... def remove(self, key: str) -> None: ... def __setitem__(self, key: str, message: _MessageData) -> None: ... @@ -148,26 +178,32 @@ class _singlefileMailbox(Mailbox[_MessageT], metaclass=ABCMeta): def flush(self) -> None: ... def close(self) -> None: ... -class _mboxMMDF(_singlefileMailbox[_MessageT]): - def get_message(self, key: str) -> _MessageT: ... - def get_file(self, key: str, from_: bool = False) -> _PartialFile[bytes]: ... +class _mboxMMDF(_singlefileMailbox[_MessageT_co]): + def get_message(self, key: str) -> _MessageT_co: ... + def get_file(self, key: str, from_: bool = False) -> _PartialFile: ... def get_bytes(self, key: str, from_: bool = False) -> bytes: ... def get_string(self, key: str, from_: bool = False) -> str: ... class mbox(_mboxMMDF[mboxMessage]): - def __init__(self, path: StrPath, factory: Callable[[IO[Any]], mboxMessage] | None = None, create: bool = True) -> None: ... + def __init__( + self, path: StrPath, factory: Callable[[_GetFileReturn], mboxMessage] | None = None, create: bool = True + ) -> None: ... class MMDF(_mboxMMDF[MMDFMessage]): - def __init__(self, path: StrPath, factory: Callable[[IO[Any]], MMDFMessage] | None = None, create: bool = True) -> None: ... + def __init__( + self, path: StrPath, factory: Callable[[_GetFileReturn], MMDFMessage] | None = None, create: bool = True + ) -> None: ... class MH(Mailbox[MHMessage]): - def __init__(self, path: StrPath, factory: Callable[[IO[Any]], MHMessage] | None = None, create: bool = True) -> None: ... + def __init__( + self, path: StrPath, factory: Callable[[_GetFileReturn], MHMessage] | None = None, create: bool = True + ) -> None: ... def add(self, message: _MessageData) -> str: ... def remove(self, key: str) -> None: ... def __setitem__(self, key: str, message: _MessageData) -> None: ... def get_message(self, key: str) -> MHMessage: ... def get_bytes(self, key: str) -> bytes: ... - def get_file(self, key: str) -> _ProxyFile[bytes]: ... + def get_file(self, key: str) -> _ProxyFile: ... def iterkeys(self) -> Iterator[str]: ... def __contains__(self, key: str) -> bool: ... def __len__(self) -> int: ... @@ -184,13 +220,15 @@ class MH(Mailbox[MHMessage]): def pack(self) -> None: ... class Babyl(_singlefileMailbox[BabylMessage]): - def __init__(self, path: StrPath, factory: Callable[[IO[Any]], BabylMessage] | None = None, create: bool = True) -> None: ... + def __init__( + self, path: StrPath, factory: Callable[[_GetFileReturn], BabylMessage] | None = None, create: bool = True + ) -> None: ... def get_message(self, key: str) -> BabylMessage: ... def get_bytes(self, key: str) -> bytes: ... - def get_file(self, key: str) -> IO[bytes]: ... + def get_file(self, key: str) -> io.BytesIO: ... def get_labels(self) -> list[str]: ... -class Message(email.message.Message): +class Message(email.message.Message[str, str]): def __init__(self, message: _MessageData | None = None) -> None: ... class MaildirMessage(Message): @@ -232,18 +270,19 @@ class BabylMessage(Message): class MMDFMessage(_mboxMMDFMessage): ... -class _ProxyFile(Generic[AnyStr]): - def __init__(self, f: IO[AnyStr], pos: int | None = None) -> None: ... - def read(self, size: int | None = None) -> AnyStr: ... - def read1(self, size: int | None = None) -> AnyStr: ... - def readline(self, size: int | None = None) -> AnyStr: ... - def readlines(self, sizehint: int | None = None) -> list[AnyStr]: ... - def __iter__(self) -> Iterator[AnyStr]: ... +# Until Python 3.14, this class was technically - but unnecessarily - generic at runtime. +class _ProxyFile: + def __init__(self, f: _GetFileReturn, pos: int | None = None) -> None: ... + def read(self, size: int | None = None) -> bytes: ... + def read1(self, size: int | None = None) -> bytes: ... + def readline(self, size: int | None = None) -> bytes: ... + def readlines(self, sizehint: int | None = None) -> list[bytes]: ... + def __iter__(self) -> Iterator[bytes]: ... def tell(self) -> int: ... def seek(self, offset: int, whence: int = 0) -> None: ... def close(self) -> None: ... def __enter__(self) -> Self: ... - def __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + def __exit__(self, *exc: Unused) -> None: ... def readable(self) -> bool: ... def writable(self) -> bool: ... def seekable(self) -> bool: ... @@ -252,8 +291,8 @@ class _ProxyFile(Generic[AnyStr]): def closed(self) -> bool: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... -class _PartialFile(_ProxyFile[AnyStr]): - def __init__(self, f: IO[AnyStr], start: int | None = None, stop: int | None = None) -> None: ... +class _PartialFile(_ProxyFile): + def __init__(self, f: _GetFileReturn, start: int | None = None, stop: int | None = None) -> None: ... class Error(Exception): ... class NoSuchMailboxError(Error): ... diff --git a/mypy/typeshed/stdlib/mailcap.pyi b/mypy/typeshed/stdlib/mailcap.pyi index ce549e01f528c..74c32694a7f9d 100644 --- a/mypy/typeshed/stdlib/mailcap.pyi +++ b/mypy/typeshed/stdlib/mailcap.pyi @@ -1,5 +1,5 @@ from collections.abc import Mapping, Sequence -from typing_extensions import TypeAlias +from typing import TypeAlias _Cap: TypeAlias = dict[str, str | int] diff --git a/mypy/typeshed/stdlib/marshal.pyi b/mypy/typeshed/stdlib/marshal.pyi index 46c421e4ce307..d72abe7758b76 100644 --- a/mypy/typeshed/stdlib/marshal.pyi +++ b/mypy/typeshed/stdlib/marshal.pyi @@ -2,8 +2,7 @@ import builtins import sys import types from _typeshed import ReadableBuffer, SupportsRead, SupportsWrite -from typing import Any, Final -from typing_extensions import TypeAlias +from typing import Any, Final, TypeAlias version: Final[int] @@ -28,7 +27,11 @@ _Marshallable: TypeAlias = ( | ReadableBuffer ) -if sys.version_info >= (3, 14): +if sys.version_info >= (3, 15): + def dump(value: _Marshallable, file: SupportsWrite[bytes], version: int = 6, /, *, allow_code: bool = True) -> None: ... + def dumps(value: _Marshallable, version: int = 6, /, *, allow_code: bool = True) -> bytes: ... + +elif sys.version_info >= (3, 14): def dump(value: _Marshallable, file: SupportsWrite[bytes], version: int = 5, /, *, allow_code: bool = True) -> None: ... def dumps(value: _Marshallable, version: int = 5, /, *, allow_code: bool = True) -> bytes: ... diff --git a/mypy/typeshed/stdlib/math.pyi b/mypy/typeshed/stdlib/math/__init__.pyi similarity index 92% rename from mypy/typeshed/stdlib/math.pyi rename to mypy/typeshed/stdlib/math/__init__.pyi index 1903d488f7bb3..4839bf7e90df1 100644 --- a/mypy/typeshed/stdlib/math.pyi +++ b/mypy/typeshed/stdlib/math/__init__.pyi @@ -1,8 +1,7 @@ import sys from _typeshed import SupportsMul, SupportsRMul from collections.abc import Iterable -from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypeAlias, TypeVar, overload, type_check_only _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) @@ -34,6 +33,7 @@ class _SupportsCeil(Protocol[_T_co]): def ceil(x: _SupportsCeil[_T], /) -> _T: ... @overload def ceil(x: _SupportsFloatOrIndex, /) -> int: ... + def comb(n: SupportsIndex, k: SupportsIndex, /) -> int: ... def copysign(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ... def cos(x: _SupportsFloatOrIndex, /) -> float: ... @@ -50,6 +50,7 @@ if sys.version_info >= (3, 11): def expm1(x: _SupportsFloatOrIndex, /) -> float: ... def fabs(x: _SupportsFloatOrIndex, /) -> float: ... def factorial(x: SupportsIndex, /) -> int: ... + @type_check_only class _SupportsFloor(Protocol[_T_co]): def __floor__(self) -> _T_co: ... @@ -58,7 +59,13 @@ class _SupportsFloor(Protocol[_T_co]): def floor(x: _SupportsFloor[_T], /) -> _T: ... @overload def floor(x: _SupportsFloatOrIndex, /) -> int: ... + def fmod(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ... + +if sys.version_info >= (3, 15): + def fmax(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ... + def fmin(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ... + def frexp(x: _SupportsFloatOrIndex, /) -> tuple[float, int]: ... def fsum(seq: Iterable[_SupportsFloatOrIndex], /) -> float: ... def gamma(x: _SupportsFloatOrIndex, /) -> float: ... @@ -74,6 +81,11 @@ def isclose( def isinf(x: _SupportsFloatOrIndex, /) -> bool: ... def isfinite(x: _SupportsFloatOrIndex, /) -> bool: ... def isnan(x: _SupportsFloatOrIndex, /) -> bool: ... + +if sys.version_info >= (3, 15): + def isnormal(x: _SupportsFloatOrIndex, /) -> bool: ... + def issubnormal(x: _SupportsFloatOrIndex, /) -> bool: ... + def isqrt(n: SupportsIndex, /) -> int: ... def lcm(*integers: SupportsIndex) -> int: ... def ldexp(x: _SupportsFloatOrIndex, i: int, /) -> float: ... @@ -116,9 +128,14 @@ def prod(iterable: Iterable[bool | _LiteralInteger], /, *, start: int = 1) -> in def prod(iterable: Iterable[_SupportsProdNoDefaultT], /) -> _SupportsProdNoDefaultT | Literal[1]: ... @overload def prod(iterable: Iterable[_MultiplicableT1], /, *, start: _MultiplicableT2) -> _MultiplicableT1 | _MultiplicableT2: ... + def radians(x: _SupportsFloatOrIndex, /) -> float: ... def remainder(x: _SupportsFloatOrIndex, y: _SupportsFloatOrIndex, /) -> float: ... def sin(x: _SupportsFloatOrIndex, /) -> float: ... + +if sys.version_info >= (3, 15): + def signbit(x: _SupportsFloatOrIndex, /) -> bool: ... + def sinh(x: _SupportsFloatOrIndex, /) -> float: ... if sys.version_info >= (3, 12): diff --git a/mypy/typeshed/stdlib/math/integer.pyi b/mypy/typeshed/stdlib/math/integer.pyi new file mode 100644 index 0000000000000..6d6d6b3e82dce --- /dev/null +++ b/mypy/typeshed/stdlib/math/integer.pyi @@ -0,0 +1,8 @@ +from typing import SupportsIndex + +def comb(n: SupportsIndex, k: SupportsIndex, /) -> int: ... +def factorial(n: SupportsIndex, /) -> int: ... +def gcd(*integers: SupportsIndex) -> int: ... +def isqrt(n: SupportsIndex, /) -> int: ... +def lcm(*integers: SupportsIndex) -> int: ... +def perm(n: SupportsIndex, k: SupportsIndex | None = None, /) -> int: ... diff --git a/mypy/typeshed/stdlib/mmap.pyi b/mypy/typeshed/stdlib/mmap.pyi index 12425f703aa1a..587b91d86c084 100644 --- a/mypy/typeshed/stdlib/mmap.pyi +++ b/mypy/typeshed/stdlib/mmap.pyi @@ -15,8 +15,7 @@ ALLOCATIONGRANULARITY: Final[int] if sys.platform == "linux": MAP_DENYWRITE: Final[int] MAP_EXECUTABLE: Final[int] - if sys.version_info >= (3, 10): - MAP_POPULATE: Final[int] + MAP_POPULATE: Final[int] if sys.version_info >= (3, 11) and sys.platform != "win32" and sys.platform != "darwin": MAP_STACK: Final[int] @@ -28,13 +27,29 @@ if sys.platform != "win32": PROT_EXEC: Final[int] PROT_READ: Final[int] PROT_WRITE: Final[int] + if sys.version_info >= (3, 15): + MS_ASYNC: Final[int] + MS_INVALIDATE: Final[int] + MS_SYNC: Final[int] PAGESIZE: Final[int] @disjoint_base class mmap: if sys.platform == "win32": - def __new__(cls, fileno: int, length: int, tagname: str | None = None, access: int = 0, offset: int = 0) -> Self: ... + if sys.version_info >= (3, 15): + def __new__( + cls, + fileno: int, + length: int, + tagname: str | None = None, + access: int = 0, + offset: int = 0, + *, + trackfd: bool = True, + ) -> Self: ... + else: + def __new__(cls, fileno: int, length: int, tagname: str | None = None, access: int = 0, offset: int = 0) -> Self: ... else: if sys.version_info >= (3, 13): def __new__( @@ -54,11 +69,16 @@ class mmap: ) -> Self: ... def close(self) -> None: ... - def flush(self, offset: int = 0, size: int = ..., /) -> None: ... + if sys.version_info >= (3, 15): + def flush(self, offset: int = 0, size: int = ..., /, *, flags: int = 0) -> None: ... + else: + def flush(self, offset: int = 0, size: int = ..., /) -> None: ... + def move(self, dest: int, src: int, count: int, /) -> None: ... def read_byte(self) -> int: ... def readline(self) -> bytes: ... - def resize(self, newsize: int, /) -> None: ... + if sys.version_info < (3, 15) or sys.platform != "darwin": + def resize(self, newsize: int, /) -> None: ... if sys.platform != "win32": def seek(self, pos: int, whence: Literal[0, 1, 2, 3, 4] = os.SEEK_SET, /) -> None: ... else: @@ -70,21 +90,36 @@ class mmap: def __len__(self) -> int: ... closed: bool if sys.platform != "win32": - def madvise(self, option: int, start: int = 0, length: int = ..., /) -> None: ... + if sys.version_info >= (3, 15): + def madvise(self, option: int, start: int = 0, length: int | None = None, /) -> None: ... + else: + def madvise(self, option: int, start: int = 0, length: int = ..., /) -> None: ... + + if sys.version_info >= (3, 15): + def find(self, view: ReadableBuffer, start: int | None = None, end: int | None = None, /) -> int: ... + def rfind(self, view: ReadableBuffer, start: int | None = None, end: int | None = None, /) -> int: ... + + else: + def find(self, view: ReadableBuffer, start: int = ..., end: int = ..., /) -> int: ... + def rfind(self, view: ReadableBuffer, start: int = ..., end: int = ..., /) -> int: ... - def find(self, view: ReadableBuffer, start: int = ..., end: int = ..., /) -> int: ... - def rfind(self, view: ReadableBuffer, start: int = ..., end: int = ..., /) -> int: ... def read(self, n: int | None = None, /) -> bytes: ... def write(self, bytes: ReadableBuffer, /) -> int: ... + if sys.version_info >= (3, 15): + def set_name(self, name: str, /) -> None: ... + @overload def __getitem__(self, key: SupportsIndex, /) -> int: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytes: ... + def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> NoReturn: ... + @overload def __setitem__(self, key: SupportsIndex, value: int, /) -> None: ... @overload def __setitem__(self, key: slice[SupportsIndex | None], value: ReadableBuffer, /) -> None: ... + # Doesn't actually exist, but the object actually supports "in" because it has __getitem__, # so we claim that there is also a __contains__ to help type checkers. def __contains__(self, o: object, /) -> bool: ... @@ -129,7 +164,7 @@ if sys.platform != "linux" and sys.platform != "darwin" and sys.platform != "win MADV_CORE: Final[int] MADV_PROTECT: Final[int] -if sys.version_info >= (3, 10) and sys.platform == "darwin": +if sys.platform == "darwin": MADV_FREE_REUSABLE: Final[int] MADV_FREE_REUSE: Final[int] diff --git a/mypy/typeshed/stdlib/msilib/__init__.pyi b/mypy/typeshed/stdlib/msilib/__init__.pyi index 622f585f5beea..565a52d53499b 100644 --- a/mypy/typeshed/stdlib/msilib/__init__.pyi +++ b/mypy/typeshed/stdlib/msilib/__init__.pyi @@ -1,5 +1,6 @@ import sys -from collections.abc import Container, Iterable, Sequence +from _typeshed import MaybeNone +from collections.abc import Container, Iterable from types import ModuleType from typing import Any, Final @@ -7,6 +8,8 @@ if sys.platform == "win32": from _msi import * from _msi import _Database + from .sequence import _SequenceType + AMD64: Final[bool] Win64: Final[bool] @@ -33,10 +36,7 @@ if sys.platform == "win32": class _Unspecified: ... def change_sequence( - seq: Sequence[tuple[str, str | None, int]], - action: str, - seqno: int | type[_Unspecified] = ..., - cond: str | type[_Unspecified] = ..., + seq: _SequenceType, action: str, seqno: int | type[_Unspecified] = ..., cond: str | type[_Unspecified] = ... ) -> None: ... def add_data(db: _Database, table: str, values: Iterable[tuple[Any, ...]]) -> None: ... def add_stream(db: _Database, name: str, path: str) -> None: ... @@ -54,7 +54,7 @@ if sys.platform == "win32": index: int def __init__(self, name: str) -> None: ... def gen_id(self, file: str) -> str: ... - def append(self, full: str, file: str, logical: str) -> tuple[int, str]: ... + def append(self, full: str, file: str, logical: str | None) -> tuple[int, str] | MaybeNone: ... def commit(self, db: _Database) -> None: ... _directories: set[str] @@ -62,7 +62,7 @@ if sys.platform == "win32": class Directory: db: _Database cab: CAB - basedir: str + basedir: Directory | None physical: str logical: str component: str | None @@ -75,7 +75,7 @@ if sys.platform == "win32": self, db: _Database, cab: CAB, - basedir: str, + basedir: Directory | None, physical: str, _logical: str, default: str, @@ -146,8 +146,8 @@ if sys.platform == "win32": attr: int, title: str, first: str, - default: str, - cancel: str, + default: str | None, + cancel: str | None, ) -> None: ... def control( self, diff --git a/mypy/typeshed/stdlib/msilib/sequence.pyi b/mypy/typeshed/stdlib/msilib/sequence.pyi index a9f5c24717bd3..9b01c416f1d68 100644 --- a/mypy/typeshed/stdlib/msilib/sequence.pyi +++ b/mypy/typeshed/stdlib/msilib/sequence.pyi @@ -1,6 +1,5 @@ import sys -from typing import Final -from typing_extensions import TypeAlias +from typing import Final, TypeAlias if sys.platform == "win32": _SequenceType: TypeAlias = list[tuple[str, str | None, int]] diff --git a/mypy/typeshed/stdlib/msvcrt.pyi b/mypy/typeshed/stdlib/msvcrt.pyi index 5feca8eab5c1c..1518f7974de7c 100644 --- a/mypy/typeshed/stdlib/msvcrt.pyi +++ b/mypy/typeshed/stdlib/msvcrt.pyi @@ -28,5 +28,4 @@ if sys.platform == "win32": def ungetwch(unicode_char: str, /) -> None: ... def heapmin() -> None: ... def SetErrorMode(mode: int, /) -> int: ... - if sys.version_info >= (3, 10): - def GetErrorMode() -> int: ... # undocumented + def GetErrorMode() -> int: ... # undocumented diff --git a/mypy/typeshed/stdlib/multiprocessing/connection.pyi b/mypy/typeshed/stdlib/multiprocessing/connection.pyi index cd4fa102c0f3e..e8366e9a8ba58 100644 --- a/mypy/typeshed/stdlib/multiprocessing/connection.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/connection.pyi @@ -3,8 +3,8 @@ import sys from _typeshed import Incomplete, ReadableBuffer from collections.abc import Iterable from types import TracebackType -from typing import Any, Generic, SupportsIndex, TypeVar -from typing_extensions import Self, TypeAlias +from typing import Any, Generic, SupportsIndex, TypeAlias, TypeVar +from typing_extensions import Self __all__ = ["Client", "Listener", "Pipe", "wait"] @@ -46,7 +46,11 @@ class Listener: def __init__( self, address: _Address | None = None, family: str | None = None, backlog: int = 1, authkey: bytes | None = None ) -> None: ... - def accept(self) -> Connection[Incomplete, Incomplete]: ... + if sys.platform != "win32": + def accept(self) -> Connection[Incomplete, Incomplete]: ... + else: + def accept(self) -> Connection[Incomplete, Incomplete] | PipeConnection[Incomplete, Incomplete]: ... + def close(self) -> None: ... @property def address(self) -> _Address: ... @@ -59,16 +63,23 @@ class Listener: # Any: send and recv methods unused if sys.version_info >= (3, 12): - def deliver_challenge(connection: Connection[Any, Any], authkey: bytes, digest_name: str = "sha256") -> None: ... + def deliver_challenge(connection: _ConnectionBase[Any, Any], authkey: bytes, digest_name: str = "sha256") -> None: ... else: - def deliver_challenge(connection: Connection[Any, Any], authkey: bytes) -> None: ... + def deliver_challenge(connection: _ConnectionBase[Any, Any], authkey: bytes) -> None: ... -def answer_challenge(connection: Connection[Any, Any], authkey: bytes) -> None: ... +def answer_challenge(connection: _ConnectionBase[Any, Any], authkey: bytes) -> None: ... def wait( - object_list: Iterable[Connection[_SendT_contra, _RecvT_co] | socket.socket | int], timeout: float | None = None -) -> list[Connection[_SendT_contra, _RecvT_co] | socket.socket | int]: ... -def Client(address: _Address, family: str | None = None, authkey: bytes | None = None) -> Connection[Any, Any]: ... + object_list: Iterable[_ConnectionBase[_SendT_contra, _RecvT_co] | socket.socket | int], timeout: float | None = None +) -> list[_ConnectionBase[_SendT_contra, _RecvT_co] | socket.socket | int]: ... + +if sys.platform != "win32": + def Client(address: _Address, family: str | None = None, authkey: bytes | None = None) -> Connection[Any, Any]: ... + +else: + def Client( + address: _Address, family: str | None = None, authkey: bytes | None = None + ) -> Connection[Any, Any] | PipeConnection[Any, Any]: ... # N.B. Keep this in sync with multiprocessing.context.BaseContext.Pipe. # _ConnectionBase is the common base class of Connection and PipeConnection diff --git a/mypy/typeshed/stdlib/multiprocessing/context.pyi b/mypy/typeshed/stdlib/multiprocessing/context.pyi index 03d1d2e5c2203..13fd967515d09 100644 --- a/mypy/typeshed/stdlib/multiprocessing/context.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/context.pyi @@ -9,8 +9,7 @@ from multiprocessing.managers import SyncManager from multiprocessing.pool import Pool as _Pool from multiprocessing.process import BaseProcess from multiprocessing.sharedctypes import Synchronized, SynchronizedArray, SynchronizedString -from typing import Any, ClassVar, Literal, TypeVar, overload -from typing_extensions import TypeAlias +from typing import Any, ClassVar, Literal, TypeAlias, TypeVar, overload if sys.platform != "win32": from multiprocessing.connection import Connection @@ -75,14 +74,17 @@ class BaseContext: initargs: Iterable[Any] = (), maxtasksperchild: int | None = None, ) -> _Pool: ... + @overload def RawValue(self, typecode_or_type: type[_CT], *args: Any) -> _CT: ... @overload def RawValue(self, typecode_or_type: str, *args: Any) -> Any: ... + @overload def RawArray(self, typecode_or_type: type[_CT], size_or_initializer: int | Sequence[Any]) -> ctypes.Array[_CT]: ... @overload def RawArray(self, typecode_or_type: str, size_or_initializer: int | Sequence[Any]) -> Any: ... + @overload def Value( self, typecode_or_type: type[_SimpleCData[_T]], *args: Any, lock: Literal[True] | _LockLike = True @@ -95,6 +97,7 @@ class BaseContext: def Value(self, typecode_or_type: str, *args: Any, lock: Literal[True] | _LockLike = True) -> Synchronized[Any]: ... @overload def Value(self, typecode_or_type: str | type[_CData], *args: Any, lock: bool | _LockLike = True) -> Any: ... + @overload def Array( self, typecode_or_type: type[_SimpleCData[_T]], size_or_initializer: int | Sequence[Any], *, lock: Literal[False] @@ -119,40 +122,44 @@ class BaseContext: def Array( self, typecode_or_type: str | type[_CData], size_or_initializer: int | Sequence[Any], *, lock: bool | _LockLike = True ) -> Any: ... + def freeze_support(self) -> None: ... def get_logger(self) -> Logger: ... def log_to_stderr(self, level: _LoggingLevel | None = None) -> Logger: ... def allow_connection_pickling(self) -> None: ... def set_executable(self, executable: str) -> None: ... - def set_forkserver_preload(self, module_names: list[str]) -> None: ... + if sys.version_info >= (3, 15): + def set_forkserver_preload( + self, module_names: list[str], *, on_error: Literal["ignore", "warn", "fail"] = "ignore" + ) -> None: ... + else: + def set_forkserver_preload(self, module_names: list[str]) -> None: ... + + @overload + def get_context(self, method: None = None) -> DefaultContext: ... + @overload + def get_context(self, method: Literal["spawn"]) -> SpawnContext: ... if sys.platform != "win32": - @overload - def get_context(self, method: None = None) -> DefaultContext: ... - @overload - def get_context(self, method: Literal["spawn"]) -> SpawnContext: ... @overload def get_context(self, method: Literal["fork"]) -> ForkContext: ... @overload def get_context(self, method: Literal["forkserver"]) -> ForkServerContext: ... - @overload - def get_context(self, method: str) -> BaseContext: ... - else: - @overload - def get_context(self, method: None = None) -> DefaultContext: ... - @overload - def get_context(self, method: Literal["spawn"]) -> SpawnContext: ... - @overload - def get_context(self, method: str) -> BaseContext: ... + + @overload + def get_context(self, method: str) -> BaseContext: ... @overload def get_start_method(self, allow_none: Literal[False] = False) -> str: ... @overload def get_start_method(self, allow_none: bool) -> str | None: ... + def set_start_method(self, method: str | None, force: bool = False) -> None: ... + @property def reducer(self) -> str: ... @reducer.setter def reducer(self, reduction: str) -> None: ... + def _check_available(self) -> None: ... class Process(BaseProcess): @@ -201,6 +208,13 @@ if sys.platform != "win32": Process: ClassVar[type[ForkServerProcess]] def _force_start_method(method: str) -> None: ... -def get_spawning_popen() -> Any | None: ... -def set_spawning_popen(popen: Any) -> None: ... + +if sys.platform != "win32": + def get_spawning_popen() -> popen_forkserver.Popen | popen_spawn_posix.Popen | None: ... + def set_spawning_popen(popen: popen_forkserver.Popen | popen_spawn_posix.Popen | None) -> None: ... + +else: + def get_spawning_popen() -> popen_spawn_win32.Popen | None: ... + def set_spawning_popen(popen: popen_spawn_win32.Popen | None) -> None: ... + def assert_spawning(obj: Any) -> None: ... diff --git a/mypy/typeshed/stdlib/multiprocessing/forkserver.pyi b/mypy/typeshed/stdlib/multiprocessing/forkserver.pyi index 570b492e9daf3..e48be5f07949e 100644 --- a/mypy/typeshed/stdlib/multiprocessing/forkserver.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/forkserver.pyi @@ -2,7 +2,7 @@ import sys from _typeshed import FileDescriptorLike, Unused from collections.abc import Sequence from struct import Struct -from typing import Any, Final +from typing import Any, Final, Literal __all__ = ["ensure_running", "get_inherited_fds", "connect_to_new_process", "set_forkserver_preload"] @@ -10,12 +10,31 @@ MAXFDS_TO_SEND: Final = 256 SIGNED_STRUCT: Final[Struct] class ForkServer: - def set_forkserver_preload(self, modules_names: list[str]) -> None: ... + if sys.version_info >= (3, 15): + def set_forkserver_preload( + self, modules_names: list[str], *, on_error: Literal["ignore", "warn", "fail"] = "ignore" + ) -> None: ... + else: + def set_forkserver_preload(self, modules_names: list[str]) -> None: ... + def get_inherited_fds(self) -> list[int] | None: ... def connect_to_new_process(self, fds: Sequence[int]) -> tuple[int, int]: ... def ensure_running(self) -> None: ... -if sys.version_info >= (3, 14): +if sys.version_info >= (3, 15): + def main( + listener_fd: int | None, + alive_r: FileDescriptorLike, + preload: Sequence[str], + main_path: str | None = None, + sys_path: list[str] | None = None, + *, + sys_argv: list[str] | None = None, + authkey_r: int | None = None, + on_error: str = "ignore", + ) -> None: ... + +elif sys.version_info >= (3, 14): # `sys_argv` parameter added in Python 3.14.3 def main( listener_fd: int | None, diff --git a/mypy/typeshed/stdlib/multiprocessing/heap.pyi b/mypy/typeshed/stdlib/multiprocessing/heap.pyi index 38191a099f1ec..bf6f853a97608 100644 --- a/mypy/typeshed/stdlib/multiprocessing/heap.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/heap.pyi @@ -1,9 +1,8 @@ import sys -from _typeshed import Incomplete from collections.abc import Callable from mmap import mmap -from typing import Protocol, type_check_only -from typing_extensions import TypeAlias +from multiprocessing import popen_forkserver, popen_spawn_posix, resource_sharer +from typing import Protocol, TypeAlias, type_check_only __all__ = ["BufferWrapper"] @@ -24,7 +23,12 @@ if sys.platform != "win32": class _SupportsDetach(Protocol): def detach(self) -> int: ... - def reduce_arena(a: Arena) -> tuple[Callable[[int, _SupportsDetach], Arena], tuple[int, Incomplete]]: ... + def reduce_arena( + a: Arena, + ) -> tuple[ + Callable[[int, _SupportsDetach], Arena], + tuple[int, popen_forkserver._DupFd | popen_spawn_posix._DupFd | resource_sharer.DupFd], + ]: ... def rebuild_arena(size: int, dupfd: _SupportsDetach) -> Arena: ... class Heap: diff --git a/mypy/typeshed/stdlib/multiprocessing/managers.pyi b/mypy/typeshed/stdlib/multiprocessing/managers.pyi index bb169ed6b2ed4..40639e8678348 100644 --- a/mypy/typeshed/stdlib/multiprocessing/managers.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/managers.pyi @@ -14,8 +14,8 @@ from collections.abc import ( Set as AbstractSet, ) from types import GenericAlias, TracebackType -from typing import Any, AnyStr, ClassVar, Generic, SupportsIndex, TypeVar, overload -from typing_extensions import Self, TypeAlias +from typing import Any, AnyStr, ClassVar, Generic, SupportsIndex, TypeAlias, TypeVar, overload +from typing_extensions import Self from . import pool from .connection import Connection, _Address @@ -81,18 +81,21 @@ if sys.version_info >= (3, 13): def __delitem__(self, key: _KT, /) -> None: ... def __iter__(self) -> Iterator[_KT]: ... def copy(self) -> dict[_KT, _VT]: ... + @overload # type: ignore[override] def get(self, key: _KT, /) -> _VT | None: ... @overload def get(self, key: _KT, default: _VT, /) -> _VT: ... @overload def get(self, key: _KT, default: _T, /) -> _VT | _T: ... + @overload def pop(self, key: _KT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _VT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... + def keys(self) -> list[_KT]: ... # type: ignore[override] def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override] def values(self) -> list[_VT]: ... # type: ignore[override] @@ -102,15 +105,19 @@ if sys.version_info >= (3, 13): def fromkeys(self, iterable: Iterable[_T], value: None = None, /) -> dict[_T, Any | None]: ... @overload def fromkeys(self, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ... + def __reversed__(self) -> Iterator[_KT]: ... + @overload def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... @overload def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... @overload def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload # type: ignore[misc] def __ior__(self, value: SupportsKeysAndGetItem[_KT, _VT], /) -> Self: ... @overload @@ -128,18 +135,21 @@ else: def __delitem__(self, key: _KT, /) -> None: ... def __iter__(self) -> Iterator[_KT]: ... def copy(self) -> dict[_KT, _VT]: ... + @overload # type: ignore[override] def get(self, key: _KT, /) -> _VT | None: ... @overload def get(self, key: _KT, default: _VT, /) -> _VT: ... @overload def get(self, key: _KT, default: _T, /) -> _VT | _T: ... + @overload def pop(self, key: _KT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _VT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... + def keys(self) -> list[_KT]: ... # type: ignore[override] def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override] def values(self) -> list[_VT]: ... # type: ignore[override] @@ -194,14 +204,17 @@ class BaseListProxy(BaseProxy, MutableSequence[_T]): def __len__(self) -> int: ... def __add__(self, x: list[_T], /) -> list[_T]: ... def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None], /) -> None: ... + @overload def __getitem__(self, i: SupportsIndex, /) -> _T: ... @overload def __getitem__(self, s: slice[SupportsIndex | None], /) -> list[_T]: ... + @overload def __setitem__(self, i: SupportsIndex, o: _T, /) -> None: ... @overload def __setitem__(self, s: slice[SupportsIndex | None], o: Iterable[_T], /) -> None: ... + def __mul__(self, n: SupportsIndex, /) -> list[_T]: ... def __rmul__(self, n: SupportsIndex, /) -> list[_T]: ... def __imul__(self, value: SupportsIndex, /) -> Self: ... @@ -217,6 +230,7 @@ class BaseListProxy(BaseProxy, MutableSequence[_T]): # Next methods are copied from builtins.list def clear(self) -> None: ... def copy(self) -> list[_T]: ... + # Use BaseListProxy[SupportsRichComparisonT] for the first overload rather than [SupportsRichComparison] # to work around invariance @overload @@ -250,11 +264,7 @@ class Server: ) -> None: ... def serve_forever(self) -> None: ... def accepter(self) -> None: ... - if sys.version_info >= (3, 10): - def handle_request(self, conn: _ServerConnection) -> None: ... - else: - def handle_request(self, c: _ServerConnection) -> None: ... - + def handle_request(self, conn: _ServerConnection) -> None: ... def serve_client(self, conn: _ServerConnection) -> None: ... def fallback_getvalue(self, conn: _ServerConnection, ident: str, obj: _T) -> _T: ... def fallback_str(self, conn: _ServerConnection, ident: str, obj: Any) -> str: ... @@ -334,6 +344,7 @@ class SyncManager(BaseManager): def Semaphore(self, value: int = 1) -> threading.Semaphore: ... def Array(self, typecode: Any, sequence: Sequence[_T]) -> Sequence[_T]: ... def Value(self, typecode: Any, value: _T) -> ValueProxy[_T]: ... + # Overloads are copied from builtins.dict.__init__ @overload def dict(self) -> DictProxy[Any, Any]: ... @@ -351,11 +362,13 @@ class SyncManager(BaseManager): def dict(self, iterable: Iterable[list[str]], /) -> DictProxy[str, str]: ... @overload def dict(self, iterable: Iterable[list[bytes]], /) -> DictProxy[bytes, bytes]: ... + # Overloads are copied from builtins.list.__init__ @overload def list(self, iterable: Iterable[_T], /) -> ListProxy[_T]: ... @overload def list(self) -> ListProxy[Any]: ... + if sys.version_info >= (3, 14): @overload def set(self, iterable: Iterable[_T], /) -> SetProxy[_T]: ... diff --git a/mypy/typeshed/stdlib/multiprocessing/reduction.pyi b/mypy/typeshed/stdlib/multiprocessing/reduction.pyi index ddc676efccc39..476cf9b26f718 100644 --- a/mypy/typeshed/stdlib/multiprocessing/reduction.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/reduction.pyi @@ -6,7 +6,7 @@ from abc import ABCMeta from builtins import type as Type # alias to avoid name clash from collections.abc import Callable from copyreg import _DispatchTableType -from multiprocessing import connection +from multiprocessing import connection, popen_forkserver, popen_spawn_posix, resource_sharer from socket import socket from typing import Any, Final @@ -57,7 +57,7 @@ else: def send_handle(conn: HasFileno, handle: int, destination_pid: Unused) -> None: ... def recv_handle(conn: HasFileno) -> int: ... def sendfds(sock: socket, fds: list[int]) -> None: ... - def DupFd(fd: int) -> Any: ... # Return type is really hard to get right + def DupFd(fd: int) -> popen_forkserver._DupFd | popen_spawn_posix._DupFd | resource_sharer.DupFd: ... # These aliases are to work around pyright complaints. # Pyright doesn't like it when a class object is defined as an alias diff --git a/mypy/typeshed/stdlib/multiprocessing/shared_memory.pyi b/mypy/typeshed/stdlib/multiprocessing/shared_memory.pyi index f75a372a69a2d..90777a4e771bd 100644 --- a/mypy/typeshed/stdlib/multiprocessing/shared_memory.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/shared_memory.pyi @@ -26,10 +26,12 @@ class SharedMemory: class ShareableList(Generic[_SLT]): shm: SharedMemory + @overload def __init__(self, sequence: None = None, *, name: str | None = None) -> None: ... @overload def __init__(self, sequence: Iterable[_SLT], *, name: str | None = None) -> None: ... + def __getitem__(self, position: int) -> _SLT: ... def __setitem__(self, position: int, value: _SLT) -> None: ... def __reduce__(self) -> tuple[Self, tuple[_SLT, ...]]: ... diff --git a/mypy/typeshed/stdlib/multiprocessing/sharedctypes.pyi b/mypy/typeshed/stdlib/multiprocessing/sharedctypes.pyi index f61ca26aab5d6..693fd2f701396 100644 --- a/mypy/typeshed/stdlib/multiprocessing/sharedctypes.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/sharedctypes.pyi @@ -16,10 +16,12 @@ _CT = TypeVar("_CT", bound=_CData) def RawValue(typecode_or_type: type[_CT], *args: Any) -> _CT: ... @overload def RawValue(typecode_or_type: str, *args: Any) -> Any: ... + @overload def RawArray(typecode_or_type: type[_CT], size_or_initializer: int | Sequence[Any]) -> ctypes.Array[_CT]: ... @overload def RawArray(typecode_or_type: str, size_or_initializer: int | Sequence[Any]) -> Any: ... + @overload def Value(typecode_or_type: type[_CT], *args: Any, lock: Literal[False], ctx: BaseContext | None = None) -> _CT: ... @overload @@ -34,6 +36,7 @@ def Value( def Value( typecode_or_type: str | type[_CData], *args: Any, lock: bool | _LockLike = True, ctx: BaseContext | None = None ) -> Any: ... + @overload def Array( typecode_or_type: type[_CT], size_or_initializer: int | Sequence[Any], *, lock: Literal[False], ctx: BaseContext | None = None @@ -70,7 +73,9 @@ def Array( lock: bool | _LockLike = True, ctx: BaseContext | None = None, ) -> Any: ... + def copy(obj: _CT) -> _CT: ... + @overload def synchronized(obj: _SimpleCData[_T], lock: _LockLike | None = None, ctx: Any | None = None) -> Synchronized[_T]: ... @overload @@ -81,6 +86,7 @@ def synchronized( ) -> SynchronizedArray[_T]: ... @overload def synchronized(obj: _CT, lock: _LockLike | None = None, ctx: Any | None = None) -> SynchronizedBase[_CT]: ... + @type_check_only class _AcquireFunc(Protocol): def __call__(self, block: bool = ..., timeout: float | None = ..., /) -> bool: ... @@ -102,14 +108,17 @@ class Synchronized(SynchronizedBase[_SimpleCData[_T]], Generic[_T]): class SynchronizedArray(SynchronizedBase[ctypes.Array[_SimpleCData[_T]]], Generic[_T]): def __len__(self) -> int: ... + @overload def __getitem__(self, i: slice[SupportsIndex | None]) -> list[_T]: ... @overload def __getitem__(self, i: SupportsIndex) -> _T: ... + @overload def __setitem__(self, i: slice[SupportsIndex | None], value: Iterable[_T]) -> None: ... @overload def __setitem__(self, i: SupportsIndex, value: _T) -> None: ... + def __getslice__(self, start: SupportsIndex, stop: SupportsIndex) -> list[_T]: ... def __setslice__(self, start: SupportsIndex, stop: SupportsIndex, values: Iterable[_T]) -> None: ... @@ -118,10 +127,12 @@ class SynchronizedString(SynchronizedArray[bytes]): def __getitem__(self, i: slice[SupportsIndex | None]) -> bytes: ... @overload def __getitem__(self, i: SupportsIndex) -> bytes: ... + @overload # type: ignore[override] def __setitem__(self, i: slice[SupportsIndex | None], value: bytes) -> None: ... @overload def __setitem__(self, i: SupportsIndex, value: bytes) -> None: ... + def __getslice__(self, start: SupportsIndex, stop: SupportsIndex) -> bytes: ... # type: ignore[override] def __setslice__(self, start: SupportsIndex, stop: SupportsIndex, values: bytes) -> None: ... # type: ignore[override] diff --git a/mypy/typeshed/stdlib/multiprocessing/synchronize.pyi b/mypy/typeshed/stdlib/multiprocessing/synchronize.pyi index 541e0b05dd8a6..889e71c061e11 100644 --- a/mypy/typeshed/stdlib/multiprocessing/synchronize.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/synchronize.pyi @@ -3,7 +3,7 @@ import threading from collections.abc import Callable from multiprocessing.context import BaseContext from types import TracebackType -from typing_extensions import TypeAlias +from typing import TypeAlias __all__ = ["Lock", "RLock", "Semaphore", "BoundedSemaphore", "Condition", "Event"] diff --git a/mypy/typeshed/stdlib/multiprocessing/util.pyi b/mypy/typeshed/stdlib/multiprocessing/util.pyi index 3583194c77e29..5eb31de77f0f7 100644 --- a/mypy/typeshed/stdlib/multiprocessing/util.pyi +++ b/mypy/typeshed/stdlib/multiprocessing/util.pyi @@ -82,6 +82,7 @@ class Finalize(Generic[_R_co]): kwargs: Mapping[str, Any] | None = None, exitpriority: int | None = None, ) -> None: ... + def __call__( self, wr: Unused = None, diff --git a/mypy/typeshed/stdlib/netrc.pyi b/mypy/typeshed/stdlib/netrc.pyi index 480f55a46d645..4b7035c3e565e 100644 --- a/mypy/typeshed/stdlib/netrc.pyi +++ b/mypy/typeshed/stdlib/netrc.pyi @@ -1,6 +1,6 @@ import sys from _typeshed import StrOrBytesPath -from typing_extensions import TypeAlias +from typing import TypeAlias __all__ = ["netrc", "NetrcParseError"] diff --git a/mypy/typeshed/stdlib/nntplib.pyi b/mypy/typeshed/stdlib/nntplib.pyi index 1fb1e79f69a1f..50b633ce1ad12 100644 --- a/mypy/typeshed/stdlib/nntplib.pyi +++ b/mypy/typeshed/stdlib/nntplib.pyi @@ -4,8 +4,8 @@ import ssl from _typeshed import Unused from builtins import list as _list # conflicts with a method named "list" from collections.abc import Iterable -from typing import IO, Any, Final, NamedTuple -from typing_extensions import Self, TypeAlias +from typing import IO, Any, Final, NamedTuple, TypeAlias +from typing_extensions import Self __all__ = [ "NNTP", diff --git a/mypy/typeshed/stdlib/ntpath.pyi b/mypy/typeshed/stdlib/ntpath.pyi index 074df075b9727..c912d77158ecc 100644 --- a/mypy/typeshed/stdlib/ntpath.pyi +++ b/mypy/typeshed/stdlib/ntpath.pyi @@ -51,6 +51,8 @@ if sys.version_info >= (3, 12): from posixpath import isjunction as isjunction, splitroot as splitroot if sys.version_info >= (3, 13): from genericpath import isdevdrive as isdevdrive +if sys.version_info >= (3, 15): + from genericpath import ALL_BUT_LAST as ALL_BUT_LAST __all__ = [ "normcase", @@ -97,6 +99,8 @@ if sys.version_info >= (3, 12): __all__ += ["isjunction", "splitroot"] if sys.version_info >= (3, 13): __all__ += ["isdevdrive", "isreserved"] +if sys.version_info >= (3, 15): + __all__ += ["ALL_BUT_LAST"] altsep: LiteralString @@ -110,14 +114,21 @@ def join(path: StrPath, /, *paths: StrPath) -> str: ... @overload def join(path: BytesPath, /, *paths: BytesPath) -> bytes: ... -if sys.platform == "win32": +if sys.version_info >= (3, 15): @overload - def realpath(path: PathLike[AnyStr], *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + def realpath(path: PathLike[AnyStr], /, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... @overload - def realpath(path: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + def realpath(path: AnyStr, /, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... else: - realpath = abspath + if sys.platform == "win32": + @overload + def realpath(path: PathLike[AnyStr], *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + @overload + def realpath(path: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + + else: + realpath = abspath if sys.version_info >= (3, 13): def isreserved(path: StrOrBytesPath) -> bool: ... diff --git a/mypy/typeshed/stdlib/nturl2path.pyi b/mypy/typeshed/stdlib/nturl2path.pyi index 014af8a0fd2ed..98686ed65420f 100644 --- a/mypy/typeshed/stdlib/nturl2path.pyi +++ b/mypy/typeshed/stdlib/nturl2path.pyi @@ -1,12 +1,6 @@ -import sys from typing_extensions import deprecated -if sys.version_info >= (3, 14): - @deprecated("The `nturl2path` module is deprecated since Python 3.14.") - def url2pathname(url: str) -> str: ... - @deprecated("The `nturl2path` module is deprecated since Python 3.14.") - def pathname2url(p: str) -> str: ... - -else: - def url2pathname(url: str) -> str: ... - def pathname2url(p: str) -> str: ... +@deprecated("The `nturl2path` module is deprecated since Python 3.14.") +def url2pathname(url: str) -> str: ... +@deprecated("The `nturl2path` module is deprecated since Python 3.14.") +def pathname2url(p: str) -> str: ... diff --git a/mypy/typeshed/stdlib/numbers.pyi b/mypy/typeshed/stdlib/numbers.pyi index 64fb16581e952..434e52c4cac80 100644 --- a/mypy/typeshed/stdlib/numbers.pyi +++ b/mypy/typeshed/stdlib/numbers.pyi @@ -120,12 +120,14 @@ class Real(Complex, _RealLike): def __floor__(self) -> _IntegralLike: ... @abstractmethod def __ceil__(self) -> _IntegralLike: ... + @abstractmethod @overload def __round__(self, ndigits: None = None) -> _IntegralLike: ... @abstractmethod @overload def __round__(self, ndigits: int) -> _RealLike: ... + def __divmod__(self, other) -> tuple[_RealLike, _RealLike]: ... def __rdivmod__(self, other) -> tuple[_RealLike, _RealLike]: ... @abstractmethod @@ -209,6 +211,7 @@ class Integral(Rational, _IntegralLike): def __neg__(self) -> _IntegralLike: ... @abstractmethod def __abs__(self) -> _IntegralLike: ... + @abstractmethod @overload def __round__(self, ndigits: None = None) -> _IntegralLike: ... diff --git a/mypy/typeshed/stdlib/opcode.pyi b/mypy/typeshed/stdlib/opcode.pyi index 67c2ef27ff360..3bc41db42bb63 100644 --- a/mypy/typeshed/stdlib/opcode.pyi +++ b/mypy/typeshed/stdlib/opcode.pyi @@ -1,6 +1,9 @@ import sys from typing import Final, Literal +if sys.version_info >= (3, 15): + from builtins import frozendict + __all__ = [ "cmp_op", "hasconst", @@ -40,7 +43,10 @@ if sys.version_info >= (3, 13): hasjump: Final[list[int]] opname: Final[list[str]] -opmap: Final[dict[str, int]] +if sys.version_info >= (3, 15): + opmap: Final[frozendict[str, int]] +else: + opmap: Final[dict[str, int]] HAVE_ARGUMENT: Final[int] EXTENDED_ARG: Final[int] diff --git a/mypy/typeshed/stdlib/operator.pyi b/mypy/typeshed/stdlib/operator.pyi index 2f919514b0b8b..5998d6d16e98b 100644 --- a/mypy/typeshed/stdlib/operator.pyi +++ b/mypy/typeshed/stdlib/operator.pyi @@ -192,6 +192,7 @@ class attrgetter(Generic[_T_co]): def __new__(cls, attr: str, attr2: str, attr3: str, attr4: str, /) -> attrgetter[tuple[Any, Any, Any, Any]]: ... @overload def __new__(cls, attr: str, /, *attrs: str) -> attrgetter[tuple[Any, ...]]: ... + def __call__(self, obj: Any, /) -> _T_co: ... @final @@ -200,6 +201,7 @@ class itemgetter(Generic[_T_co]): def __new__(cls, item: _T, /) -> itemgetter[_T]: ... @overload def __new__(cls, item1: _T1, item2: _T2, /, *items: Unpack[_Ts]) -> itemgetter[tuple[_T1, _T2, Unpack[_Ts]]]: ... + # __key: _KT_contra in SupportsGetItem seems to be causing variance issues, ie: # TypeVar "_KT_contra@SupportsGetItem" is contravariant # "tuple[int, int]" is incompatible with protocol "SupportsIndex" diff --git a/mypy/typeshed/stdlib/optparse.pyi b/mypy/typeshed/stdlib/optparse.pyi index 305b6a4f06d66..9e5536610272e 100644 --- a/mypy/typeshed/stdlib/optparse.pyi +++ b/mypy/typeshed/stdlib/optparse.pyi @@ -184,6 +184,7 @@ class OptionContainer: def _check_conflict(self, option: Option) -> None: ... def _create_option_mappings(self) -> None: ... def _share_option_mappings(self, parser: OptionParser) -> None: ... + @overload def add_option(self, opt: Option, /) -> Option: ... @overload @@ -206,6 +207,7 @@ class OptionContainer: metavar: str | None = None, **kwargs: Any, # Allow arbitrary keyword arguments for user defined option_class ) -> Option: ... + def add_options(self, option_list: Iterable[Option]) -> None: ... def destroy(self) -> None: ... def format_option_help(self, formatter: HelpFormatter) -> str: ... @@ -280,10 +282,12 @@ class OptionParser(OptionContainer): def _process_args(self, largs: list[str], rargs: list[str], values: Values) -> None: ... def _process_long_opt(self, rargs: list[str], values: Values) -> None: ... def _process_short_opts(self, rargs: list[str], values: Values) -> None: ... + @overload def add_option_group(self, opt_group: OptionGroup, /) -> OptionGroup: ... @overload def add_option_group(self, title: str, /, description: str | None = None) -> OptionGroup: ... + def check_values(self, values: Values, args: list[str]) -> tuple[Values, list[str]]: ... def disable_interspersed_args(self) -> None: ... def enable_interspersed_args(self) -> None: ... diff --git a/mypy/typeshed/stdlib/os/__init__.pyi b/mypy/typeshed/stdlib/os/__init__.pyi index 66a9d1dd3bc6d..726e9702fb16b 100644 --- a/mypy/typeshed/stdlib/os/__init__.pyi +++ b/mypy/typeshed/stdlib/os/__init__.pyi @@ -35,13 +35,14 @@ from typing import ( Literal, NoReturn, Protocol, + TypeAlias, TypeVar, final, overload, runtime_checkable, type_check_only, ) -from typing_extensions import LiteralString, Self, TypeAlias, Unpack, deprecated +from typing_extensions import LiteralString, Self, Unpack, deprecated from . import path as _path @@ -176,9 +177,11 @@ __all__ = [ if sys.version_info >= (3, 14): # reload_environ was added to __all__ in Python 3.14.1 __all__ += ["readinto", "reload_environ"] +if sys.platform == "linux" and sys.version_info >= (3, 15): + __all__ += ["_clearenv"] if sys.platform == "darwin" and sys.version_info >= (3, 12): __all__ += ["PRIO_DARWIN_BG", "PRIO_DARWIN_NONUI", "PRIO_DARWIN_PROCESS", "PRIO_DARWIN_THREAD"] -if sys.platform == "darwin" and sys.version_info >= (3, 10): +if sys.platform == "darwin": __all__ += ["O_EVTONLY", "O_NOFOLLOW_ANY", "O_SYMLINK"] if sys.platform == "linux": __all__ += [ @@ -226,6 +229,31 @@ if sys.platform == "linux": ] if sys.platform == "linux" and sys.version_info >= (3, 14): __all__ += ["SCHED_DEADLINE", "SCHED_NORMAL"] +if sys.platform == "linux" and sys.version_info >= (3, 15): + __all__ += [ + "AT_NO_AUTOMOUNT", + "AT_STATX_DONT_SYNC", + "AT_STATX_FORCE_SYNC", + "AT_STATX_SYNC_AS_STAT", + "STATX_ATIME", + "STATX_BASIC_STATS", + "STATX_BLOCKS", + "STATX_BTIME", + "STATX_CTIME", + "STATX_DIOALIGN", + "STATX_GID", + "STATX_INO", + "STATX_MNT_ID", + "STATX_MNT_ID_UNIQUE", + "STATX_MODE", + "STATX_MTIME", + "STATX_NLINK", + "STATX_SIZE", + "STATX_TYPE", + "STATX_UID", + "statx", + "statx_result", + ] if sys.platform == "linux" and sys.version_info >= (3, 13): __all__ += [ "POSIX_SPAWN_CLOSEFROM", @@ -259,7 +287,7 @@ if sys.platform == "linux" and sys.version_info >= (3, 12): "unshare", "PIDFD_NONBLOCK", ] -if sys.platform == "linux" and sys.version_info >= (3, 10): +if sys.platform == "linux": __all__ += [ "EFD_CLOEXEC", "EFD_NONBLOCK", @@ -446,7 +474,9 @@ if sys.platform != "win32" and sys.version_info >= (3, 13): __all__ += ["grantpt", "posix_openpt", "ptsname", "unlockpt"] if sys.platform != "win32" and sys.version_info >= (3, 11): __all__ += ["login_tty"] -if sys.platform != "win32" and sys.version_info >= (3, 10): +if sys.platform != "win32" and sys.version_info >= (3, 15): + __all__ += ["NODEV", "O_FSYNC"] +elif sys.platform != "win32": __all__ += ["O_FSYNC"] if sys.platform != "darwin" and sys.platform != "win32": __all__ += [ @@ -661,12 +691,37 @@ if sys.platform != "linux" and sys.platform != "win32": O_SHLOCK: Final[int] O_EXLOCK: Final[int] -if sys.platform == "darwin" and sys.version_info >= (3, 10): +if sys.platform == "darwin": O_EVTONLY: Final[int] O_NOFOLLOW_ANY: Final[int] O_SYMLINK: Final[int] -if sys.platform != "win32" and sys.version_info >= (3, 10): +if sys.platform != "win32" and sys.version_info >= (3, 15): + NODEV: Final[int] + +if sys.platform == "linux" and sys.version_info >= (3, 15): + AT_NO_AUTOMOUNT: Final[int] + AT_STATX_DONT_SYNC: Final[int] + AT_STATX_FORCE_SYNC: Final[int] + AT_STATX_SYNC_AS_STAT: Final[int] + STATX_ATIME: Final[int] + STATX_BASIC_STATS: Final[int] + STATX_BLOCKS: Final[int] + STATX_BTIME: Final[int] + STATX_CTIME: Final[int] + STATX_DIOALIGN: Final[int] + STATX_GID: Final[int] + STATX_INO: Final[int] + STATX_MNT_ID: Final[int] + STATX_MNT_ID_UNIQUE: Final[int] + STATX_MODE: Final[int] + STATX_MTIME: Final[int] + STATX_NLINK: Final[int] + STATX_SIZE: Final[int] + STATX_TYPE: Final[int] + STATX_UID: Final[int] + +if sys.platform != "win32": O_FSYNC: Final[int] if sys.platform != "linux" and sys.platform != "win32" and sys.version_info >= (3, 13): @@ -713,18 +768,21 @@ class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): encodevalue: _EnvironCodeFunc[AnyStr], decodevalue: _EnvironCodeFunc[AnyStr], ) -> None: ... + @overload def get(self, key: AnyStr, default: None = None) -> AnyStr | None: ... @overload def get(self, key: AnyStr, default: AnyStr) -> AnyStr: ... @overload def get(self, key: AnyStr, default: _T) -> AnyStr | _T: ... + @overload def pop(self, key: AnyStr) -> AnyStr: ... @overload def pop(self, key: AnyStr, default: AnyStr) -> AnyStr: ... @overload def pop(self, key: AnyStr, default: _T) -> AnyStr | _T: ... + def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... def copy(self) -> dict[AnyStr, AnyStr]: ... def __delitem__(self, key: AnyStr) -> None: ... @@ -734,6 +792,7 @@ class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): def __len__(self) -> int: ... def __or__(self, other: Mapping[_T1, _T2]) -> dict[AnyStr | _T1, AnyStr | _T2]: ... def __ror__(self, other: Mapping[_T1, _T2]) -> dict[AnyStr | _T1, AnyStr | _T2]: ... + # We use @overload instead of a Union for reasons similar to those given for # overloading MutableMapping.update in stdlib/typing.pyi # The type: ignore is needed due to incompatible __or__/__ior__ signatures @@ -749,6 +808,9 @@ if sys.platform != "win32": if sys.version_info >= (3, 14): def reload_environ() -> None: ... +if sys.platform == "linux" and sys.version_info >= (3, 15): + def _clearenv() -> None: ... + if sys.version_info >= (3, 11) or sys.platform != "win32": EX_OK: Final[int] @@ -805,8 +867,7 @@ class stat_result(structseq[float], tuple[int, int, int, int, int, int, int, flo # st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime. # # More items may be added at the end by some implementations. - if sys.version_info >= (3, 10): - __match_args__: Final = ("st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size") + __match_args__: Final = ("st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size") @property def st_mode(self) -> int: ... # protection bits, @@ -890,6 +951,7 @@ def listdir(path: StrPath | None = None) -> list[str]: ... def listdir(path: BytesPath) -> list[bytes]: ... @overload def listdir(path: int) -> list[str]: ... + @final class DirEntry(Generic[AnyStr]): # This is what the scandir iterator yields @@ -911,19 +973,18 @@ class DirEntry(Generic[AnyStr]): @final class statvfs_result(structseq[int], tuple[int, int, int, int, int, int, int, int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ( - "f_bsize", - "f_frsize", - "f_blocks", - "f_bfree", - "f_bavail", - "f_files", - "f_ffree", - "f_favail", - "f_flag", - "f_namemax", - ) + __match_args__: Final = ( + "f_bsize", + "f_frsize", + "f_blocks", + "f_bfree", + "f_bavail", + "f_files", + "f_ffree", + "f_favail", + "f_flag", + "f_namemax", + ) @property def f_bsize(self) -> int: ... @@ -951,22 +1012,24 @@ class statvfs_result(structseq[int], tuple[int, int, int, int, int, int, int, in # ----- os function stubs ----- def fsencode(filename: StrOrBytesPath) -> bytes: ... def fsdecode(filename: StrOrBytesPath) -> str: ... + @overload def fspath(path: str) -> str: ... @overload def fspath(path: bytes) -> bytes: ... @overload def fspath(path: PathLike[AnyStr]) -> AnyStr: ... + def get_exec_path(env: Mapping[str, str] | None = None) -> list[str]: ... def getlogin() -> str: ... def getpid() -> int: ... def getppid() -> int: ... def strerror(code: int, /) -> str: ... def umask(mask: int, /) -> int: ... + @final class uname_result(structseq[str], tuple[str, str, str, str, str]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("sysname", "nodename", "release", "version", "machine") + __match_args__: Final = ("sysname", "nodename", "release", "version", "machine") @property def sysname(self) -> str: ... @@ -1023,6 +1086,7 @@ if sys.platform != "win32": def getenvb(key: bytes) -> bytes | None: ... @overload def getenvb(key: bytes, default: _T) -> bytes | _T: ... + def putenv(name: StrOrBytesPath, value: StrOrBytesPath, /) -> None: ... def unsetenv(name: StrOrBytesPath, /) -> None: ... @@ -1109,6 +1173,7 @@ def fdopen( closefd: bool = True, opener: _Opener | None = None, ) -> IO[Any]: ... + def close(fd: int) -> None: ... def closerange(fd_low: int, fd_high: int, /) -> None: ... def device_encoding(fd: int) -> str | None: ... @@ -1154,8 +1219,7 @@ if sys.platform != "win32": def preadv(fd: int, buffers: SupportsLenAndGetItem[WriteableBuffer], offset: int, flags: int = 0, /) -> int: ... def pwritev(fd: int, buffers: SupportsLenAndGetItem[ReadableBuffer], offset: int, flags: int = 0, /) -> int: ... if sys.platform != "darwin": - if sys.version_info >= (3, 10): - RWF_APPEND: Final[int] # docs say available on 3.7+, stubtest says otherwise + RWF_APPEND: Final[int] RWF_DSYNC: Final[int] RWF_SYNC: Final[int] RWF_HIPRI: Final[int] @@ -1182,8 +1246,7 @@ if sys.version_info >= (3, 14): @final class terminal_size(structseq[int], tuple[int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("columns", "lines") + __match_args__: Final = ("columns", "lines") @property def columns(self) -> int: ... @@ -1242,7 +1305,11 @@ def mkdir(path: StrOrBytesPath, mode: int = 0o777, *, dir_fd: int | None = None) if sys.platform != "win32": def mkfifo(path: StrOrBytesPath, mode: int = 0o666, *, dir_fd: int | None = None) -> None: ... # Unix only -def makedirs(name: StrOrBytesPath, mode: int = 0o777, exist_ok: bool = False) -> None: ... +if sys.version_info >= (3, 15): + def makedirs(name: StrOrBytesPath, mode: int = 0o777, exist_ok: bool = False, *, parent_mode: int | None = None) -> None: ... + +else: + def makedirs(name: StrOrBytesPath, mode: int = 0o777, exist_ok: bool = False) -> None: ... if sys.platform != "win32": def mknod(path: StrOrBytesPath, mode: int = 0o600, device: int = 0, *, dir_fd: int | None = None) -> None: ... @@ -1260,6 +1327,7 @@ def replace( src: StrOrBytesPath, dst: StrOrBytesPath, *, src_dir_fd: int | None = None, dst_dir_fd: int | None = None ) -> None: ... def rmdir(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... + @final @type_check_only class _ScandirIterator(Generic[AnyStr]): @@ -1276,11 +1344,76 @@ def scandir(path: None = None) -> _ScandirIterator[str]: ... def scandir(path: int) -> _ScandirIterator[str]: ... @overload def scandir(path: GenericPath[AnyStr]) -> _ScandirIterator[AnyStr]: ... + def stat(path: FileDescriptorOrPath, *, dir_fd: int | None = None, follow_symlinks: bool = True) -> stat_result: ... if sys.platform != "win32": def statvfs(path: FileDescriptorOrPath) -> statvfs_result: ... # Unix only +if sys.platform == "linux" and sys.version_info >= (3, 15): + @final + class statx_result: + @property + def stx_mask(self) -> int: ... + @property + def stx_blksize(self) -> int: ... + @property + def stx_attributes(self) -> int: ... + @property + def stx_attributes_mask(self) -> int: ... + @property + def stx_rdev_major(self) -> int: ... + @property + def stx_rdev_minor(self) -> int: ... + @property + def stx_rdev(self) -> int: ... + @property + def stx_dev_major(self) -> int: ... + @property + def stx_dev_minor(self) -> int: ... + @property + def stx_dev(self) -> int: ... + @property + def stx_mode(self) -> int | None: ... + @property + def stx_nlink(self) -> int | None: ... + @property + def stx_uid(self) -> int | None: ... + @property + def stx_gid(self) -> int | None: ... + @property + def stx_ino(self) -> int | None: ... + @property + def stx_size(self) -> int | None: ... + @property + def stx_blocks(self) -> int | None: ... + @property + def stx_atime(self) -> float | None: ... + @property + def stx_atime_ns(self) -> int | None: ... + @property + def stx_btime(self) -> float | None: ... + @property + def stx_btime_ns(self) -> int | None: ... + @property + def stx_ctime(self) -> float | None: ... + @property + def stx_ctime_ns(self) -> int | None: ... + @property + def stx_mtime(self) -> float | None: ... + @property + def stx_mtime_ns(self) -> int | None: ... + @property + def stx_mnt_id(self) -> int | None: ... + @property + def stx_dio_mem_align(self) -> int | None: ... + @property + def stx_dio_offset_align(self) -> int | None: ... + + def statx( + path: FileDescriptorOrPath, mask: int, *, flags: int = 0, dir_fd: int | None = None, follow_symlinks: bool = True + ) -> statx_result: ... + def symlink( src: StrOrBytesPath, dst: StrOrBytesPath, target_is_directory: bool = False, *, dir_fd: int | None = None ) -> None: ... @@ -1324,6 +1457,7 @@ if sys.platform != "win32": follow_symlinks: bool = False, dir_fd: int | None = None, ) -> Iterator[tuple[bytes, list[bytes], list[bytes], int]]: ... + if sys.platform == "linux": def getxattr(path: FileDescriptorOrPath, attribute: StrOrBytesPath, *, follow_symlinks: bool = True) -> bytes: ... def listxattr(path: FileDescriptorOrPath | None = None, *, follow_symlinks: bool = True) -> list[str]: ... @@ -1408,52 +1542,30 @@ class _wrap_close: def write(self, s: str, /) -> int: ... def writelines(self, lines: Iterable[str], /) -> None: ... -if sys.version_info >= (3, 14): - @deprecated("Soft deprecated. Use the subprocess module instead.") - def popen(cmd: str, mode: str = "r", buffering: int = -1) -> _wrap_close: ... - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnl(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnle(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise sig - -else: - def popen(cmd: str, mode: str = "r", buffering: int = -1) -> _wrap_close: ... - def spawnl(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... - def spawnle(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise sig +@deprecated("Soft deprecated. Use the subprocess module instead.") +def popen(cmd: str, mode: str = "r", buffering: int = -1) -> _wrap_close: ... +@deprecated("Soft deprecated. Use the subprocess module instead.") +def spawnl(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... +@deprecated("Soft deprecated. Use the subprocess module instead.") +def spawnle(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise sig if sys.platform != "win32": - if sys.version_info >= (3, 14): - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnv(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnve(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... - - else: - def spawnv(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... - def spawnve(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... - + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnv(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnve(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... else: - if sys.version_info >= (3, 14): - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnv(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, /) -> int: ... - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnve(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, env: _ExecEnv, /) -> int: ... - - else: - def spawnv(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, /) -> int: ... - def spawnve(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, env: _ExecEnv, /) -> int: ... - -if sys.version_info >= (3, 14): @deprecated("Soft deprecated. Use the subprocess module instead.") - def system(command: StrOrBytesPath) -> int: ... + def spawnv(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, /) -> int: ... + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnve(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, env: _ExecEnv, /) -> int: ... -else: - def system(command: StrOrBytesPath) -> int: ... +@deprecated("Soft deprecated. Use the subprocess module instead.") +def system(command: StrOrBytesPath) -> int: ... @final class times_result(structseq[float], tuple[float, float, float, float, float]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("user", "system", "children_user", "children_system", "elapsed") + __match_args__: Final = ("user", "system", "children_user", "children_system", "elapsed") @property def user(self) -> float: ... @@ -1470,41 +1582,25 @@ def times() -> times_result: ... def waitpid(pid: int, options: int, /) -> tuple[int, int]: ... if sys.platform == "win32": - if sys.version_info >= (3, 10): - def startfile( - filepath: StrOrBytesPath, - operation: str = ..., - arguments: str = "", - cwd: StrOrBytesPath | None = None, - show_cmd: int = 1, - ) -> None: ... - else: - def startfile(filepath: StrOrBytesPath, operation: str = ...) -> None: ... + def startfile( + filepath: StrOrBytesPath, operation: str = ..., arguments: str = "", cwd: StrOrBytesPath | None = None, show_cmd: int = 1 + ) -> None: ... else: - if sys.version_info >= (3, 14): - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnlp(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnlpe(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise signature - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnvp(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... - @deprecated("Soft deprecated. Use the subprocess module instead.") - def spawnvpe(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... - - else: - def spawnlp(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... - def spawnlpe(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise signature - def spawnvp(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... - def spawnvpe(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... - + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnlp(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnlpe(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise signature + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnvp(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... + @deprecated("Soft deprecated. Use the subprocess module instead.") + def spawnvpe(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... def wait() -> tuple[int, int]: ... # Unix only # Added to MacOS in 3.13 if sys.platform != "darwin" or sys.version_info >= (3, 13): @final class waitid_result(structseq[int], tuple[int, int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("si_pid", "si_uid", "si_signo", "si_status", "si_code") + __match_args__: Final = ("si_pid", "si_uid", "si_signo", "si_status", "si_code") @property def si_pid(self) -> int: ... @@ -1627,8 +1723,7 @@ else: if sys.platform != "win32": @final class sched_param(structseq[int], tuple[int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("sched_priority",) + __match_args__: Final = ("sched_priority",) def __new__(cls, sched_priority: int) -> Self: ... @property @@ -1719,7 +1814,7 @@ if sys.version_info >= (3, 12) and sys.platform == "win32": def listmounts(volume: str) -> list[str]: ... def listvolumes() -> list[str]: ... -if sys.version_info >= (3, 10) and sys.platform == "linux": +if sys.platform == "linux": EFD_CLOEXEC: Final[int] EFD_NONBLOCK: Final[int] EFD_SEMAPHORE: Final[int] diff --git a/mypy/typeshed/stdlib/parser.pyi b/mypy/typeshed/stdlib/parser.pyi deleted file mode 100644 index 9b287fcc6529d..0000000000000 --- a/mypy/typeshed/stdlib/parser.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from _typeshed import StrOrBytesPath -from collections.abc import Sequence -from types import CodeType -from typing import Any, ClassVar, final - -def expr(source: str) -> STType: ... -def suite(source: str) -> STType: ... -def sequence2st(sequence: Sequence[Any]) -> STType: ... -def tuple2st(sequence: Sequence[Any]) -> STType: ... -def st2list(st: STType, line_info: bool = False, col_info: bool = False) -> list[Any]: ... -def st2tuple(st: STType, line_info: bool = False, col_info: bool = False) -> tuple[Any, ...]: ... -def compilest(st: STType, filename: StrOrBytesPath = ...) -> CodeType: ... -def isexpr(st: STType) -> bool: ... -def issuite(st: STType) -> bool: ... - -class ParserError(Exception): ... - -@final -class STType: - __hash__: ClassVar[None] # type: ignore[assignment] - def compile(self, filename: StrOrBytesPath = ...) -> CodeType: ... - def isexpr(self) -> bool: ... - def issuite(self) -> bool: ... - def tolist(self, line_info: bool = False, col_info: bool = False) -> list[Any]: ... - def totuple(self, line_info: bool = False, col_info: bool = False) -> tuple[Any, ...]: ... diff --git a/mypy/typeshed/stdlib/pathlib/__init__.pyi b/mypy/typeshed/stdlib/pathlib/__init__.pyi index 4f094130665c8..ca0d41f649583 100644 --- a/mypy/typeshed/stdlib/pathlib/__init__.pyi +++ b/mypy/typeshed/stdlib/pathlib/__init__.pyi @@ -29,31 +29,32 @@ if sys.version_info >= (3, 13): __all__ += ["UnsupportedOperation"] class PurePath(PathLike[str]): - if sys.version_info >= (3, 13): - __slots__ = ( - "_raw_paths", - "_drv", - "_root", - "_tail_cached", - "_str", - "_str_normcase_cached", - "_parts_normcase_cached", - "_hash", - ) - elif sys.version_info >= (3, 12): - __slots__ = ( - "_raw_paths", - "_drv", - "_root", - "_tail_cached", - "_str", - "_str_normcase_cached", - "_parts_normcase_cached", - "_lines_cached", - "_hash", - ) - else: - __slots__ = ("_drv", "_root", "_parts", "_str", "_hash", "_pparts", "_cached_cparts") + if sys.version_info < (3, 15): + if sys.version_info >= (3, 13): + __slots__ = ( + "_raw_paths", + "_drv", + "_root", + "_tail_cached", + "_str", + "_str_normcase_cached", + "_parts_normcase_cached", + "_hash", + ) + elif sys.version_info >= (3, 12): + __slots__ = ( + "_raw_paths", + "_drv", + "_root", + "_tail_cached", + "_str", + "_str_normcase_cached", + "_parts_normcase_cached", + "_lines_cached", + "_hash", + ) + else: + __slots__ = ("_drv", "_root", "_parts", "_str", "_hash", "_pparts", "_cached_cparts") if sys.version_info >= (3, 13): parser: ClassVar[types.ModuleType] def full_match(self, pattern: StrPath, *, case_sensitive: bool | None = None) -> bool: ... @@ -82,6 +83,9 @@ class PurePath(PathLike[str]): def __hash__(self) -> int: ... def __fspath__(self) -> str: ... + if sys.version_info >= (3, 15): + def __vfspath__(self) -> str: ... + def __lt__(self, other: PurePath) -> bool: ... def __le__(self, other: PurePath) -> bool: ... def __gt__(self, other: PurePath) -> bool: ... @@ -93,24 +97,23 @@ class PurePath(PathLike[str]): @deprecated("Deprecated since Python 3.14; will be removed in Python 3.19. Use `Path.as_uri()` instead.") def as_uri(self) -> str: ... def is_absolute(self) -> bool: ... - if sys.version_info >= (3, 13): - @deprecated( - "Deprecated since Python 3.13; will be removed in Python 3.15. " - "Use `os.path.isreserved()` to detect reserved paths on Windows." - ) - def is_reserved(self) -> bool: ... - else: - def is_reserved(self) -> bool: ... + if sys.version_info < (3, 15): + if sys.version_info >= (3, 13): + @deprecated( + "Deprecated since Python 3.13; will be removed in Python 3.15. " + "Use `os.path.isreserved()` to detect reserved paths on Windows." + ) + def is_reserved(self) -> bool: ... + else: + def is_reserved(self) -> bool: ... if sys.version_info >= (3, 14): def is_relative_to(self, other: StrPath) -> bool: ... - elif sys.version_info >= (3, 12): + else: @overload def is_relative_to(self, other: StrPath, /) -> bool: ... @overload @deprecated("Passing additional arguments is deprecated since Python 3.12; removed in Python 3.14.") def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool: ... - else: - def is_relative_to(self, *other: StrPath) -> bool: ... if sys.version_info >= (3, 12): def match(self, path_pattern: str, *, case_sensitive: bool | None = None) -> bool: ... @@ -151,10 +154,8 @@ class PureWindowsPath(PurePath): class Path(PurePath): if sys.version_info >= (3, 14): __slots__ = ("_info",) - elif sys.version_info >= (3, 10): - __slots__ = () else: - __slots__ = ("_accessor",) + __slots__ = () if sys.version_info >= (3, 12): def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... # pyright: ignore[reportInconsistentConstructor] @@ -163,12 +164,8 @@ class Path(PurePath): @classmethod def cwd(cls) -> Self: ... - if sys.version_info >= (3, 10): - def stat(self, *, follow_symlinks: bool = True) -> stat_result: ... - def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ... - else: - def stat(self) -> stat_result: ... - def chmod(self, mode: int) -> None: ... + def stat(self, *, follow_symlinks: bool = True) -> stat_result: ... + def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ... if sys.version_info >= (3, 13): @classmethod @@ -211,23 +208,32 @@ class Path(PurePath): def iterdir(self) -> Generator[Self]: ... def lchmod(self, mode: int) -> None: ... def lstat(self) -> stat_result: ... - def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... + if sys.version_info >= (3, 15): + def mkdir( + self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, *, parent_mode: int | None = None + ) -> None: ... + else: + def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... if sys.version_info >= (3, 14): @property def info(self) -> PathInfo: ... + @overload def move_into(self, target_dir: _PathT) -> _PathT: ... # type: ignore[overload-overlap] @overload def move_into(self, target_dir: StrPath) -> Self: ... # type: ignore[overload-overlap] + @overload def move(self, target: _PathT) -> _PathT: ... # type: ignore[overload-overlap] @overload def move(self, target: StrPath) -> Self: ... # type: ignore[overload-overlap] + @overload def copy_into(self, target_dir: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ... # type: ignore[overload-overlap] @overload def copy_into(self, target_dir: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> Self: ... # type: ignore[overload-overlap] + @overload def copy(self, target: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ... # type: ignore[overload-overlap] @overload @@ -314,20 +320,12 @@ class Path(PurePath): def is_mount(self) -> bool: ... def readlink(self) -> Self: ... - - if sys.version_info >= (3, 10): - def rename(self, target: StrPath) -> Self: ... - def replace(self, target: StrPath) -> Self: ... - else: - def rename(self, target: str | PurePath) -> Self: ... - def replace(self, target: str | PurePath) -> Self: ... - + def rename(self, target: StrPath) -> Self: ... + def replace(self, target: StrPath) -> Self: ... def resolve(self, strict: bool = False) -> Self: ... def rmdir(self) -> None: ... def symlink_to(self, target: StrOrBytesPath, target_is_directory: bool = False) -> None: ... - if sys.version_info >= (3, 10): - def hardlink_to(self, target: StrOrBytesPath) -> None: ... - + def hardlink_to(self, target: StrOrBytesPath) -> None: ... def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ... def unlink(self, missing_ok: bool = False) -> None: ... @classmethod @@ -337,18 +335,12 @@ class Path(PurePath): def read_bytes(self) -> bytes: ... def samefile(self, other_path: StrPath) -> bool: ... def write_bytes(self, data: ReadableBuffer) -> int: ... - if sys.version_info >= (3, 10): - def write_text( - self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None - ) -> int: ... - else: - def write_text(self, data: str, encoding: str | None = None, errors: str | None = None) -> int: ... + def write_text( + self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None + ) -> int: ... if sys.version_info < (3, 12): - if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10; removed in Python 3.12. Use `hardlink_to()` instead.") - def link_to(self, target: StrOrBytesPath) -> None: ... - else: - def link_to(self, target: StrOrBytesPath) -> None: ... + @deprecated("Deprecated since Python 3.10; removed in Python 3.12. Use `hardlink_to()` instead.") + def link_to(self, target: StrOrBytesPath) -> None: ... if sys.version_info >= (3, 12): def walk( self, top_down: bool = True, on_error: Callable[[OSError], object] | None = None, follow_symlinks: bool = False diff --git a/mypy/typeshed/stdlib/pdb.pyi b/mypy/typeshed/stdlib/pdb.pyi index dc1cf3b280860..289637cb5f048 100644 --- a/mypy/typeshed/stdlib/pdb.pyi +++ b/mypy/typeshed/stdlib/pdb.pyi @@ -1,14 +1,14 @@ import signal import sys +from _typeshed import ReadableBuffer from bdb import Bdb, _Backend from cmd import Cmd from collections.abc import Callable, Iterable, Mapping, Sequence -from inspect import _SourceObjectType from linecache import _ModuleGlobals from rlcompleter import Completer from types import CodeType, FrameType, TracebackType -from typing import IO, Any, ClassVar, Final, Literal, TypeVar -from typing_extensions import ParamSpec, Self, TypeAlias, deprecated +from typing import IO, Any, ClassVar, Final, Literal, ParamSpec, TypeAlias, TypeVar +from typing_extensions import Self, deprecated __all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace", "post_mortem", "help"] if sys.version_info >= (3, 14): @@ -22,9 +22,15 @@ line_prefix: Final[str] # undocumented class Restart(Exception): ... -def run(statement: str, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None) -> None: ... -def runeval(expression: str, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None) -> Any: ... -def runctx(statement: str, globals: dict[str, Any], locals: Mapping[str, Any]) -> None: ... +def run( # matches `builtins.exec` + statement: str | ReadableBuffer | CodeType, globals: dict[str, Any] | None = None, locals: Mapping[str, object] | None = None +) -> None: ... +def runctx( # matches `builtins.exec` + statement: str | ReadableBuffer | CodeType, globals: dict[str, Any], locals: Mapping[str, object] +) -> None: ... +def runeval( # matches `builtins.eval` + expression: str | ReadableBuffer | CodeType, globals: dict[str, Any] | None = None, locals: Mapping[str, object] | None = None +) -> Any: ... def runcall(func: Callable[_P, _T], *args: _P.args, **kwds: _P.kwargs) -> _T | None: ... if sys.version_info >= (3, 14): @@ -130,7 +136,11 @@ class Pdb(Bdb, Cmd): else: def print_stack_trace(self) -> None: ... - def print_stack_entry(self, frame_lineno: tuple[FrameType, int], prompt_prefix: str = "\n-> ") -> None: ... + if sys.version_info >= (3, 15): + def print_stack_entry(self, frame_lineno: tuple[FrameType, int], prompt_prefix: str | None = None) -> None: ... + else: + def print_stack_entry(self, frame_lineno: tuple[FrameType, int], prompt_prefix: str = "\n-> ") -> None: ... + def lookupmodule(self, filename: str) -> str | None: ... if sys.version_info < (3, 11): def _runscript(self, filename: str) -> None: ... @@ -259,10 +269,6 @@ class Pdb(Bdb, Cmd): def find_function(funcname: str, filename: str) -> tuple[str, str, int] | None: ... def main() -> None: ... def help() -> None: ... - -if sys.version_info < (3, 10): - def getsourcelines(obj: _SourceObjectType) -> tuple[list[str], int]: ... - def lasti2lineno(code: CodeType, lasti: int) -> int: ... class _rstr(str): diff --git a/mypy/typeshed/stdlib/pickletools.pyi b/mypy/typeshed/stdlib/pickletools.pyi index 8bbfaba31b671..98353d9608880 100644 --- a/mypy/typeshed/stdlib/pickletools.pyi +++ b/mypy/typeshed/stdlib/pickletools.pyi @@ -1,7 +1,6 @@ import sys from collections.abc import Callable, Iterator, MutableMapping -from typing import IO, Any, Final -from typing_extensions import TypeAlias +from typing import IO, Any, Final, TypeAlias __all__ = ["dis", "genops", "optimize"] diff --git a/mypy/typeshed/stdlib/pkgutil.pyi b/mypy/typeshed/stdlib/pkgutil.pyi index 7c70dcc4c5ab1..365792bed9528 100644 --- a/mypy/typeshed/stdlib/pkgutil.pyi +++ b/mypy/typeshed/stdlib/pkgutil.pyi @@ -39,14 +39,10 @@ if sys.version_info < (3, 12): def __init__(self, fullname: str, file: IO[str], filename: StrOrBytesPath, etc: tuple[str, str, int]) -> None: ... if sys.version_info < (3, 14): - if sys.version_info >= (3, 12): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") - def find_loader(fullname: str) -> LoaderProtocol | None: ... - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") - def get_loader(module_or_name: str) -> LoaderProtocol | None: ... - else: - def find_loader(fullname: str) -> LoaderProtocol | None: ... - def get_loader(module_or_name: str) -> LoaderProtocol | None: ... + @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") + def find_loader(fullname: str) -> LoaderProtocol | None: ... + @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") + def get_loader(module_or_name: str) -> LoaderProtocol | None: ... def get_importer(path_item: StrOrBytesPath) -> PathEntryFinderProtocol | None: ... def iter_importers(fullname: str = "") -> Iterator[MetaPathFinderProtocol | PathEntryFinderProtocol]: ... @@ -56,4 +52,9 @@ def walk_packages( path: Iterable[StrOrBytesPath] | None = None, prefix: str = "", onerror: Callable[[str], object] | None = None ) -> Iterator[ModuleInfo]: ... def get_data(package: str, resource: str) -> bytes | None: ... -def resolve_name(name: str) -> Any: ... + +if sys.version_info >= (3, 15): + def resolve_name(name: str, *, strict: bool = False) -> Any: ... + +else: + def resolve_name(name: str) -> Any: ... diff --git a/mypy/typeshed/stdlib/platform.pyi b/mypy/typeshed/stdlib/platform.pyi index 69d702bb155cd..7d837436ef794 100644 --- a/mypy/typeshed/stdlib/platform.pyi +++ b/mypy/typeshed/stdlib/platform.pyi @@ -10,7 +10,7 @@ def mac_ver( release: str = "", versioninfo: tuple[str, str, str] = ("", "", ""), machine: str = "" ) -> tuple[str, tuple[str, str, str], str]: ... -if sys.version_info >= (3, 13): +if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def java_ver( release: str = "", @@ -19,14 +19,6 @@ if sys.version_info >= (3, 13): osinfo: tuple[str, str, str] = ("", "", ""), ) -> tuple[str, str, tuple[str, str, str], tuple[str, str, str]]: ... -else: - def java_ver( - release: str = "", - vendor: str = "", - vminfo: tuple[str, str, str] = ("", "", ""), - osinfo: tuple[str, str, str] = ("", "", ""), - ) -> tuple[str, str, tuple[str, str, str], tuple[str, str, str]]: ... - def system_alias(system: str, release: str, version: str) -> tuple[str, str, str]: ... def architecture(executable: str = sys.executable, bits: str = "", linkage: str = "") -> tuple[str, str]: ... @@ -57,9 +49,7 @@ if sys.version_info >= (3, 12): else: @disjoint_base class uname_result(_uname_result_base): - if sys.version_info >= (3, 10): - __match_args__ = ("system", "node", "release", "version", "machine") # pyright: ignore[reportAssignmentType] - + __match_args__ = ("system", "node", "release", "version", "machine") # pyright: ignore[reportAssignmentType] def __new__(_cls, system: str, node: str, release: str, version: str, machine: str) -> Self: ... @property def processor(self) -> str: ... @@ -79,9 +69,7 @@ def python_revision() -> str: ... def python_build() -> tuple[str, str]: ... def python_compiler() -> str: ... def platform(aliased: bool = False, terse: bool = False) -> str: ... - -if sys.version_info >= (3, 10): - def freedesktop_os_release() -> dict[str, str]: ... +def freedesktop_os_release() -> dict[str, str]: ... if sys.version_info >= (3, 13): class AndroidVer(NamedTuple): diff --git a/mypy/typeshed/stdlib/poplib.pyi b/mypy/typeshed/stdlib/poplib.pyi index f5669ec87e87e..d4555a6b5ec0c 100644 --- a/mypy/typeshed/stdlib/poplib.pyi +++ b/mypy/typeshed/stdlib/poplib.pyi @@ -4,8 +4,8 @@ import sys from _typeshed import StrOrBytesPath from builtins import list as _list # conflicts with a method named "list" from re import Pattern -from typing import Any, BinaryIO, Final, NoReturn, overload -from typing_extensions import TypeAlias, deprecated +from typing import Any, BinaryIO, Final, NoReturn, TypeAlias, overload +from typing_extensions import deprecated __all__ = ["POP3", "error_proto", "POP3_SSL"] @@ -44,10 +44,12 @@ class POP3: timestamp: Pattern[str] def apop(self, user: str, password: str) -> bytes: ... def top(self, which: Any, howmuch: int) -> _LongResp: ... + @overload def uidl(self) -> _LongResp: ... @overload def uidl(self, which: Any) -> bytes: ... + def utf8(self) -> bytes: ... def capa(self) -> dict[str, _list[str]]: ... def stls(self, context: ssl.SSLContext | None = None) -> bytes: ... @@ -83,6 +85,7 @@ class POP3_SSL(POP3): timeout: float = ..., context: None = None, ) -> None: ... + keyfile: StrOrBytesPath | None certfile: StrOrBytesPath | None # "context" is actually the last argument, diff --git a/mypy/typeshed/stdlib/posix.pyi b/mypy/typeshed/stdlib/posix.pyi index 6d0d76ab82176..36bb5be18e88f 100644 --- a/mypy/typeshed/stdlib/posix.pyi +++ b/mypy/typeshed/stdlib/posix.pyi @@ -39,6 +39,7 @@ if sys.platform != "win32": O_DIRECTORY as O_DIRECTORY, O_DSYNC as O_DSYNC, O_EXCL as O_EXCL, + O_FSYNC as O_FSYNC, O_NDELAY as O_NDELAY, O_NOCTTY as O_NOCTTY, O_NOFOLLOW as O_NOFOLLOW, @@ -227,9 +228,6 @@ if sys.platform != "win32": writev as writev, ) - if sys.version_info >= (3, 10): - from os import O_FSYNC as O_FSYNC - if sys.version_info >= (3, 11): from os import login_tty as login_tty @@ -265,6 +263,36 @@ if sys.platform != "win32": if sys.platform != "linux" and sys.version_info >= (3, 13): from os import O_EXEC as O_EXEC, O_SEARCH as O_SEARCH + if sys.version_info >= (3, 15): + from os import NODEV as NODEV + + if sys.version_info >= (3, 15) and sys.platform == "linux": + from os import ( + AT_NO_AUTOMOUNT as AT_NO_AUTOMOUNT, + AT_STATX_DONT_SYNC as AT_STATX_DONT_SYNC, + AT_STATX_FORCE_SYNC as AT_STATX_FORCE_SYNC, + AT_STATX_SYNC_AS_STAT as AT_STATX_SYNC_AS_STAT, + STATX_ATIME as STATX_ATIME, + STATX_BASIC_STATS as STATX_BASIC_STATS, + STATX_BLOCKS as STATX_BLOCKS, + STATX_BTIME as STATX_BTIME, + STATX_CTIME as STATX_CTIME, + STATX_DIOALIGN as STATX_DIOALIGN, + STATX_GID as STATX_GID, + STATX_INO as STATX_INO, + STATX_MNT_ID as STATX_MNT_ID, + STATX_MNT_ID_UNIQUE as STATX_MNT_ID_UNIQUE, + STATX_MODE as STATX_MODE, + STATX_MTIME as STATX_MTIME, + STATX_NLINK as STATX_NLINK, + STATX_SIZE as STATX_SIZE, + STATX_TYPE as STATX_TYPE, + STATX_UID as STATX_UID, + _clearenv as _clearenv, + statx as statx, + statx_result as statx_result, + ) + if sys.platform != "darwin": from os import ( POSIX_FADV_DONTNEED as POSIX_FADV_DONTNEED, @@ -273,6 +301,7 @@ if sys.platform != "win32": POSIX_FADV_RANDOM as POSIX_FADV_RANDOM, POSIX_FADV_SEQUENTIAL as POSIX_FADV_SEQUENTIAL, POSIX_FADV_WILLNEED as POSIX_FADV_WILLNEED, + RWF_APPEND as RWF_APPEND, RWF_DSYNC as RWF_DSYNC, RWF_HIPRI as RWF_HIPRI, RWF_NOWAIT as RWF_NOWAIT, @@ -303,14 +332,14 @@ if sys.platform != "win32": setresuid as setresuid, ) - if sys.version_info >= (3, 10): - from os import RWF_APPEND as RWF_APPEND - if sys.platform != "darwin" or sys.version_info >= (3, 13): from os import waitid as waitid, waitid_result as waitid_result if sys.platform == "linux": from os import ( + EFD_CLOEXEC as EFD_CLOEXEC, + EFD_NONBLOCK as EFD_NONBLOCK, + EFD_SEMAPHORE as EFD_SEMAPHORE, GRND_NONBLOCK as GRND_NONBLOCK, GRND_RANDOM as GRND_RANDOM, MFD_ALLOW_SEALING as MFD_ALLOW_SEALING, @@ -341,10 +370,16 @@ if sys.platform != "win32": SCHED_BATCH as SCHED_BATCH, SCHED_IDLE as SCHED_IDLE, SCHED_RESET_ON_FORK as SCHED_RESET_ON_FORK, + SPLICE_F_MORE as SPLICE_F_MORE, + SPLICE_F_MOVE as SPLICE_F_MOVE, + SPLICE_F_NONBLOCK as SPLICE_F_NONBLOCK, XATTR_CREATE as XATTR_CREATE, XATTR_REPLACE as XATTR_REPLACE, XATTR_SIZE_MAX as XATTR_SIZE_MAX, copy_file_range as copy_file_range, + eventfd as eventfd, + eventfd_read as eventfd_read, + eventfd_write as eventfd_write, getrandom as getrandom, getxattr as getxattr, listxattr as listxattr, @@ -352,22 +387,9 @@ if sys.platform != "win32": pidfd_open as pidfd_open, removexattr as removexattr, setxattr as setxattr, + splice as splice, ) - if sys.version_info >= (3, 10): - from os import ( - EFD_CLOEXEC as EFD_CLOEXEC, - EFD_NONBLOCK as EFD_NONBLOCK, - EFD_SEMAPHORE as EFD_SEMAPHORE, - SPLICE_F_MORE as SPLICE_F_MORE, - SPLICE_F_MOVE as SPLICE_F_MOVE, - SPLICE_F_NONBLOCK as SPLICE_F_NONBLOCK, - eventfd as eventfd, - eventfd_read as eventfd_read, - eventfd_write as eventfd_write, - splice as splice, - ) - if sys.version_info >= (3, 12): from os import ( CLONE_FILES as CLONE_FILES, @@ -390,6 +412,8 @@ if sys.platform != "win32": ) if sys.platform == "darwin": + from os import O_EVTONLY as O_EVTONLY, O_NOFOLLOW_ANY as O_NOFOLLOW_ANY, O_SYMLINK as O_SYMLINK + if sys.version_info >= (3, 12): from os import ( PRIO_DARWIN_BG as PRIO_DARWIN_BG, @@ -397,8 +421,6 @@ if sys.platform != "win32": PRIO_DARWIN_PROCESS as PRIO_DARWIN_PROCESS, PRIO_DARWIN_THREAD as PRIO_DARWIN_THREAD, ) - if sys.platform == "darwin" and sys.version_info >= (3, 10): - from os import O_EVTONLY as O_EVTONLY, O_NOFOLLOW_ANY as O_NOFOLLOW_ANY, O_SYMLINK as O_SYMLINK # Not same as os.environ or os.environb # Because of this variable, we can't do "from posix import *" in os/__init__.pyi diff --git a/mypy/typeshed/stdlib/posixpath.pyi b/mypy/typeshed/stdlib/posixpath.pyi index 84e1b1e028bde..b4068629b698e 100644 --- a/mypy/typeshed/stdlib/posixpath.pyi +++ b/mypy/typeshed/stdlib/posixpath.pyi @@ -17,6 +17,9 @@ from genericpath import ( samestat as samestat, ) +if sys.version_info >= (3, 15): + from genericpath import ALL_BUT_LAST as ALL_BUT_LAST + if sys.version_info >= (3, 13): from genericpath import isdevdrive as isdevdrive from os import PathLike @@ -64,6 +67,8 @@ __all__ = [ "commonpath", ] __all__ += ["ALLOW_MISSING"] +if sys.version_info >= (3, 15): + __all__ += ["ALL_BUT_LAST"] if sys.version_info >= (3, 12): __all__ += ["isjunction", "splitroot"] if sys.version_info >= (3, 13): @@ -85,30 +90,54 @@ devnull: LiteralString def abspath(path: PathLike[AnyStr]) -> AnyStr: ... @overload def abspath(path: AnyStr) -> AnyStr: ... -@overload -def basename(p: PathLike[AnyStr]) -> AnyStr: ... -@overload -def basename(p: AnyOrLiteralStr) -> AnyOrLiteralStr: ... -@overload -def dirname(p: PathLike[AnyStr]) -> AnyStr: ... -@overload -def dirname(p: AnyOrLiteralStr) -> AnyOrLiteralStr: ... + +if sys.version_info >= (3, 15): + @overload + def basename(p: PathLike[AnyStr], /) -> AnyStr: ... + @overload + def basename(p: AnyOrLiteralStr, /) -> AnyOrLiteralStr: ... + + @overload + def dirname(p: PathLike[AnyStr], /) -> AnyStr: ... + @overload + def dirname(p: AnyOrLiteralStr, /) -> AnyOrLiteralStr: ... +else: + @overload + def basename(p: PathLike[AnyStr]) -> AnyStr: ... + @overload + def basename(p: AnyOrLiteralStr) -> AnyOrLiteralStr: ... + + @overload + def dirname(p: PathLike[AnyStr]) -> AnyStr: ... + @overload + def dirname(p: AnyOrLiteralStr) -> AnyOrLiteralStr: ... + @overload def expanduser(path: PathLike[AnyStr]) -> AnyStr: ... @overload def expanduser(path: AnyStr) -> AnyStr: ... + @overload def expandvars(path: PathLike[AnyStr]) -> AnyStr: ... @overload def expandvars(path: AnyStr) -> AnyStr: ... -@overload -def normcase(s: PathLike[AnyStr]) -> AnyStr: ... -@overload -def normcase(s: AnyOrLiteralStr) -> AnyOrLiteralStr: ... + +if sys.version_info >= (3, 15): + @overload + def normcase(s: PathLike[AnyStr], /) -> AnyStr: ... + @overload + def normcase(s: AnyOrLiteralStr, /) -> AnyOrLiteralStr: ... +else: + @overload + def normcase(s: PathLike[AnyStr]) -> AnyStr: ... + @overload + def normcase(s: AnyOrLiteralStr) -> AnyOrLiteralStr: ... + @overload def normpath(path: PathLike[AnyStr]) -> AnyStr: ... @overload def normpath(path: AnyOrLiteralStr) -> AnyOrLiteralStr: ... + @overload def commonpath(paths: Iterable[LiteralString]) -> LiteralString: ... @overload @@ -116,7 +145,7 @@ def commonpath(paths: Iterable[StrPath]) -> str: ... @overload def commonpath(paths: Iterable[BytesPath]) -> bytes: ... -# First parameter is not actually pos-only, +# First parameter is not actually pos-only before Python 3.15, # but must be defined as pos-only in the stub or cross-platform code doesn't type-check, # as the parameter name is different in ntpath.join() @overload @@ -125,36 +154,77 @@ def join(a: LiteralString, /, *paths: LiteralString) -> LiteralString: ... def join(a: StrPath, /, *paths: StrPath) -> str: ... @overload def join(a: BytesPath, /, *paths: BytesPath) -> bytes: ... -@overload -def realpath(filename: PathLike[AnyStr], *, strict: bool | _AllowMissingType = False) -> AnyStr: ... -@overload -def realpath(filename: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + +if sys.version_info >= (3, 15): + @overload + def realpath(filename: PathLike[AnyStr], /, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + @overload + def realpath(filename: AnyStr, /, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... +else: + @overload + def realpath(filename: PathLike[AnyStr], *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + @overload + def realpath(filename: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + @overload def relpath(path: LiteralString, start: LiteralString | None = None) -> LiteralString: ... @overload def relpath(path: BytesPath, start: BytesPath | None = None) -> bytes: ... @overload def relpath(path: StrPath, start: StrPath | None = None) -> str: ... -@overload -def split(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ... -@overload -def split(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... -@overload -def splitdrive(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ... -@overload -def splitdrive(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... -@overload -def splitext(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ... -@overload -def splitext(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... -def isabs(s: StrOrBytesPath) -> bool: ... + +if sys.version_info >= (3, 15): + @overload + def split(p: PathLike[AnyStr], /) -> tuple[AnyStr, AnyStr]: ... + @overload + def split(p: AnyOrLiteralStr, /) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... + + @overload + def splitdrive(p: PathLike[AnyStr], /) -> tuple[AnyStr, AnyStr]: ... + @overload + def splitdrive(p: AnyOrLiteralStr, /) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... +else: + @overload + def split(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ... + @overload + def split(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... + + @overload + def splitdrive(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ... + @overload + def splitdrive(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... + +if sys.version_info >= (3, 15): + @overload + def splitext(p: PathLike[AnyStr], /) -> tuple[AnyStr, AnyStr]: ... + @overload + def splitext(p: AnyOrLiteralStr, /) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... +else: + @overload + def splitext(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr]: ... + @overload + def splitext(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr]: ... + +if sys.version_info >= (3, 15): + def isabs(s: StrOrBytesPath, /) -> bool: ... + +else: + def isabs(s: StrOrBytesPath) -> bool: ... + def islink(path: FileDescriptorOrPath) -> bool: ... def ismount(path: FileDescriptorOrPath) -> bool: ... def lexists(path: FileDescriptorOrPath) -> bool: ... if sys.version_info >= (3, 12): def isjunction(path: StrOrBytesPath) -> bool: ... - @overload - def splitroot(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr, AnyOrLiteralStr]: ... - @overload - def splitroot(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr, AnyStr]: ... + + if sys.version_info >= (3, 15): + @overload + def splitroot(path: AnyOrLiteralStr, /) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr, AnyOrLiteralStr]: ... + @overload + def splitroot(path: PathLike[AnyStr], /) -> tuple[AnyStr, AnyStr, AnyStr]: ... + else: + @overload + def splitroot(p: AnyOrLiteralStr) -> tuple[AnyOrLiteralStr, AnyOrLiteralStr, AnyOrLiteralStr]: ... + @overload + def splitroot(p: PathLike[AnyStr]) -> tuple[AnyStr, AnyStr, AnyStr]: ... diff --git a/mypy/typeshed/stdlib/pprint.pyi b/mypy/typeshed/stdlib/pprint.pyi index 1e80462e25657..dd29020216574 100644 --- a/mypy/typeshed/stdlib/pprint.pyi +++ b/mypy/typeshed/stdlib/pprint.pyi @@ -5,7 +5,8 @@ from typing import IO __all__ = ["pprint", "pformat", "isreadable", "isrecursive", "saferepr", "PrettyPrinter", "pp"] -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 15): + # The `expand` parameter was added in Python 3.15. def pformat( object: object, indent: int = 1, @@ -13,6 +14,7 @@ if sys.version_info >= (3, 10): depth: int | None = None, *, compact: bool = False, + expand: bool = False, sort_dicts: bool = True, underscore_numbers: bool = False, ) -> str: ... @@ -26,9 +28,11 @@ else: *, compact: bool = False, sort_dicts: bool = True, + underscore_numbers: bool = False, ) -> str: ... -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 15): + # The `expand` parameter was added in Python 3.15. def pp( object: object, stream: IO[str] | None = None, @@ -37,6 +41,7 @@ if sys.version_info >= (3, 10): depth: int | None = None, *, compact: bool = False, + expand: bool = False, sort_dicts: bool = False, underscore_numbers: bool = False, ) -> None: ... @@ -51,9 +56,11 @@ else: *, compact: bool = False, sort_dicts: bool = False, + underscore_numbers: bool = False, ) -> None: ... -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 15): + # The `expand` parameter was added in Python 3.15. def pprint( object: object, stream: IO[str] | None = None, @@ -62,6 +69,7 @@ if sys.version_info >= (3, 10): depth: int | None = None, *, compact: bool = False, + expand: bool = False, sort_dicts: bool = True, underscore_numbers: bool = False, ) -> None: ... @@ -76,6 +84,7 @@ else: *, compact: bool = False, sort_dicts: bool = True, + underscore_numbers: bool = False, ) -> None: ... def isreadable(object: object) -> bool: ... @@ -83,7 +92,8 @@ def isrecursive(object: object) -> bool: ... def saferepr(object: object) -> str: ... class PrettyPrinter: - if sys.version_info >= (3, 10): + if sys.version_info >= (3, 15): + # The `expand` parameter was added in Python 3.15. def __init__( self, indent: int = 1, @@ -92,6 +102,7 @@ class PrettyPrinter: stream: IO[str] | None = None, *, compact: bool = False, + expand: bool = False, sort_dicts: bool = True, underscore_numbers: bool = False, ) -> None: ... @@ -105,6 +116,7 @@ class PrettyPrinter: *, compact: bool = False, sort_dicts: bool = True, + underscore_numbers: bool = False, ) -> None: ... def pformat(self, object: object) -> str: ... @@ -155,5 +167,4 @@ class PrettyPrinter: self, items: list[object], stream: SupportsWrite[str], indent: int, allowance: int, context: dict[int, int], level: int ) -> None: ... def _repr(self, object: object, context: dict[int, int], level: int) -> str: ... - if sys.version_info >= (3, 10): - def _safe_repr(self, object: object, context: dict[int, int], maxlevels: int, level: int) -> tuple[str, bool, bool]: ... + def _safe_repr(self, object: object, context: dict[int, int], maxlevels: int, level: int) -> tuple[str, bool, bool]: ... diff --git a/mypy/typeshed/stdlib/profile.pyi b/mypy/typeshed/stdlib/profile.pyi index 696193d9dc169..06ce9a0e44c82 100644 --- a/mypy/typeshed/stdlib/profile.pyi +++ b/mypy/typeshed/stdlib/profile.pyi @@ -1,7 +1,7 @@ from _typeshed import StrOrBytesPath from collections.abc import Callable, Mapping -from typing import Any, TypeVar -from typing_extensions import ParamSpec, Self, TypeAlias +from typing import Any, ParamSpec, TypeAlias, TypeVar +from typing_extensions import Self __all__ = ["run", "runctx", "Profile"] diff --git a/mypy/typeshed/stdlib/profiling/__init__.pyi b/mypy/typeshed/stdlib/profiling/__init__.pyi new file mode 100644 index 0000000000000..435f5a6cc66c3 --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/__init__.pyi @@ -0,0 +1,3 @@ +from . import sampling as sampling, tracing as tracing + +__all__ = ("tracing", "sampling") diff --git a/mypy/typeshed/stdlib/profiling/sampling/__init__.pyi b/mypy/typeshed/stdlib/profiling/sampling/__init__.pyi new file mode 100644 index 0000000000000..1f8b3f7d98fef --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/__init__.pyi @@ -0,0 +1,17 @@ +from .collector import Collector as Collector +from .gecko_collector import GeckoCollector as GeckoCollector +from .heatmap_collector import HeatmapCollector as HeatmapCollector +from .jsonl_collector import JsonlCollector as JsonlCollector +from .pstats_collector import PstatsCollector as PstatsCollector +from .stack_collector import CollapsedStackCollector as CollapsedStackCollector +from .string_table import StringTable as StringTable + +__all__ = ( + "Collector", + "PstatsCollector", + "CollapsedStackCollector", + "HeatmapCollector", + "GeckoCollector", + "JsonlCollector", + "StringTable", +) diff --git a/mypy/typeshed/stdlib/profiling/sampling/collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/collector.pyi new file mode 100644 index 0000000000000..a72a185a2fb74 --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/collector.pyi @@ -0,0 +1,25 @@ +from _typeshed import StrOrBytesPath +from abc import ABC, abstractmethod +from collections.abc import Sequence +from typing import ClassVar, TypeAlias + +from _remote_debugging import AwaitedInfo, FrameInfo, InterpreterInfo, LocationInfo + +_Location: TypeAlias = int | tuple[int, int, int, int] | LocationInfo | None +_Frame: TypeAlias = FrameInfo | tuple[str, _Location, str, int | None] +_Timestamps: TypeAlias = Sequence[int] | None + +def normalize_location(location: _Location) -> tuple[int, int, int, int]: ... +def extract_lineno(location: _Location) -> int: ... +def filter_internal_frames(frames: Sequence[_Frame]) -> list[_Frame]: ... +def iter_async_frames(awaited_info_list: Sequence[AwaitedInfo]) -> object: ... + +class Collector(ABC): + aggregating: ClassVar[bool] # undocumented + @abstractmethod + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def collect_failed_sample(self) -> None: ... + @abstractmethod + def export(self, filename: StrOrBytesPath) -> None: ... diff --git a/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi new file mode 100644 index 0000000000000..6072d4f2359af --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi @@ -0,0 +1,13 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Timestamps + +class GeckoCollector(Collector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False, opcodes: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... diff --git a/mypy/typeshed/stdlib/profiling/sampling/heatmap_collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/heatmap_collector.pyi new file mode 100644 index 0000000000000..bd523bc382458 --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/heatmap_collector.pyi @@ -0,0 +1,24 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Frame, _Timestamps + +class HeatmapCollector(Collector): + FILE_INDEX_FORMAT: str + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, output_path: StrOrBytesPath) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + def set_stats( + self, + sample_interval_usec: int, + duration_sec: float, + sample_rate: float, + error_rate: float | None = None, + missed_samples: float | None = None, + **kwargs: object, + ) -> None: ... diff --git a/mypy/typeshed/stdlib/profiling/sampling/jsonl_collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/jsonl_collector.pyi new file mode 100644 index 0000000000000..3bdc4b81c01dc --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/jsonl_collector.pyi @@ -0,0 +1,15 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import _Frame, _Timestamps +from .stack_collector import StackTraceCollector + +class JsonlCollector(StackTraceCollector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False, mode: int | None = None) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + def process_frames(self, frames: Sequence[_Frame], _thread_id: int, weight: int = 1) -> None: ... diff --git a/mypy/typeshed/stdlib/profiling/sampling/pstats_collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/pstats_collector.pyi new file mode 100644 index 0000000000000..178d55a7af8e0 --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/pstats_collector.pyi @@ -0,0 +1,17 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Timestamps + +class PstatsCollector(Collector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + def create_stats(self) -> None: ... + def print_stats( + self, sort: int = -1, limit: int | None = None, show_summary: bool = True, mode: int | None = None + ) -> None: ... diff --git a/mypy/typeshed/stdlib/profiling/sampling/stack_collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/stack_collector.pyi new file mode 100644 index 0000000000000..0788e08295ad6 --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/stack_collector.pyi @@ -0,0 +1,39 @@ +from _typeshed import StrOrBytesPath +from abc import ABCMeta +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Frame, _Timestamps + +class StackTraceCollector(Collector, metaclass=ABCMeta): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + +class CollapsedStackCollector(StackTraceCollector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + +class FlamegraphCollector(StackTraceCollector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def set_stats( + self, + sample_interval_usec: int, + duration_sec: float, + sample_rate: float, + error_rate: float | None = None, + missed_samples: float | None = None, + mode: int | None = None, + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + +class DiffFlamegraphCollector(FlamegraphCollector): + def __init__(self, sample_interval_usec: int, *, baseline_binary_path: StrOrBytesPath, skip_idle: bool = False) -> None: ... diff --git a/mypy/typeshed/stdlib/profiling/sampling/string_table.pyi b/mypy/typeshed/stdlib/profiling/sampling/string_table.pyi new file mode 100644 index 0000000000000..cb71e82ec036d --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/sampling/string_table.pyi @@ -0,0 +1,5 @@ +class StringTable: + def intern(self, string: object) -> int: ... + def get_string(self, index: int) -> str: ... + def get_strings(self) -> list[str]: ... + def __len__(self) -> int: ... diff --git a/mypy/typeshed/stdlib/profiling/tracing.pyi b/mypy/typeshed/stdlib/profiling/tracing.pyi new file mode 100644 index 0000000000000..af4ab508406ac --- /dev/null +++ b/mypy/typeshed/stdlib/profiling/tracing.pyi @@ -0,0 +1,9 @@ +from cProfile import Profile as Profile, run as run, runctx as runctx +from types import CodeType +from typing import TypeAlias + +__all__ = ("run", "runctx", "Profile") + +_Label: TypeAlias = tuple[str, int, str] + +def label(code: str | CodeType) -> _Label: ... # undocumented diff --git a/mypy/typeshed/stdlib/pstats.pyi b/mypy/typeshed/stdlib/pstats.pyi index c1da2aea0fc5a..19eb683df1666 100644 --- a/mypy/typeshed/stdlib/pstats.pyi +++ b/mypy/typeshed/stdlib/pstats.pyi @@ -4,8 +4,8 @@ from collections.abc import Iterable from cProfile import Profile as _cProfile from dataclasses import dataclass from profile import Profile -from typing import IO, Any, Literal, overload -from typing_extensions import Self, TypeAlias +from typing import IO, Any, Literal, TypeAlias, overload +from typing_extensions import Self if sys.version_info >= (3, 11): from enum import StrEnum @@ -72,10 +72,12 @@ class Stats: def add(self, *arg_list: None | str | Profile | _cProfile | Self) -> Self: ... def dump_stats(self, filename: StrOrBytesPath) -> None: ... def get_sort_arg_defs(self) -> _SortArgDict: ... + @overload def sort_stats(self, field: Literal[-1, 0, 1, 2]) -> Self: ... @overload def sort_stats(self, *field: str) -> Self: ... + def reverse_order(self) -> Self: ... def strip_dirs(self) -> Self: ... def calc_callees(self) -> None: ... @@ -86,6 +88,9 @@ class Stats: def print_callees(self, *amount: _Selector) -> Self: ... def print_callers(self, *amount: _Selector) -> Self: ... def print_call_heading(self, name_size: int, column_title: str) -> None: ... + if sys.version_info >= (3, 15): + def print_call_subheading(self, name_size: int) -> None: ... + def print_call_line(self, name_size: int, source: str, call_dict: dict[str, Any], arrow: str = "->") -> None: ... def print_title(self) -> None: ... def print_line(self, func: str) -> None: ... diff --git a/mypy/typeshed/stdlib/pty.pyi b/mypy/typeshed/stdlib/pty.pyi index d1c78f9e3dd67..e74c02ab1e1e6 100644 --- a/mypy/typeshed/stdlib/pty.pyi +++ b/mypy/typeshed/stdlib/pty.pyi @@ -1,7 +1,7 @@ import sys from collections.abc import Callable, Iterable -from typing import Final -from typing_extensions import TypeAlias, deprecated +from typing import Final, TypeAlias +from typing_extensions import deprecated if sys.platform != "win32": __all__ = ["openpty", "fork", "spawn"] @@ -15,14 +15,10 @@ if sys.platform != "win32": def openpty() -> tuple[int, int]: ... if sys.version_info < (3, 14): - if sys.version_info >= (3, 12): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") - def master_open() -> tuple[int, str]: ... - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") - def slave_open(tty_name: str) -> int: ... - else: - def master_open() -> tuple[int, str]: ... - def slave_open(tty_name: str) -> int: ... + @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") + def master_open() -> tuple[int, str]: ... + @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") + def slave_open(tty_name: str) -> int: ... def fork() -> tuple[int, int]: ... def spawn(argv: str | Iterable[str], master_read: _Reader = ..., stdin_read: _Reader = ...) -> int: ... diff --git a/mypy/typeshed/stdlib/pwd.pyi b/mypy/typeshed/stdlib/pwd.pyi index a84ba324718af..6a7e24f78125d 100644 --- a/mypy/typeshed/stdlib/pwd.pyi +++ b/mypy/typeshed/stdlib/pwd.pyi @@ -5,8 +5,7 @@ from typing import Any, Final, final if sys.platform != "win32": @final class struct_passwd(structseq[Any], tuple[str, str, int, int, str, str, str]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("pw_name", "pw_passwd", "pw_uid", "pw_gid", "pw_gecos", "pw_dir", "pw_shell") + __match_args__: Final = ("pw_name", "pw_passwd", "pw_uid", "pw_gid", "pw_gecos", "pw_dir", "pw_shell") @property def pw_name(self) -> str: ... diff --git a/mypy/typeshed/stdlib/py_compile.pyi b/mypy/typeshed/stdlib/py_compile.pyi index 334ce79b5dd04..e0ee67c5e93fe 100644 --- a/mypy/typeshed/stdlib/py_compile.pyi +++ b/mypy/typeshed/stdlib/py_compile.pyi @@ -1,5 +1,4 @@ import enum -import sys from typing import AnyStr __all__ = ["compile", "main", "PyCompileError", "PycInvalidationMode"] @@ -26,9 +25,4 @@ def compile( invalidation_mode: PycInvalidationMode | None = None, quiet: int = 0, ) -> AnyStr | None: ... - -if sys.version_info >= (3, 10): - def main() -> None: ... - -else: - def main(args: list[str] | None = None) -> int: ... +def main() -> None: ... diff --git a/mypy/typeshed/stdlib/pyclbr.pyi b/mypy/typeshed/stdlib/pyclbr.pyi index 504a5d5f115a0..541f962d33e1c 100644 --- a/mypy/typeshed/stdlib/pyclbr.pyi +++ b/mypy/typeshed/stdlib/pyclbr.pyi @@ -1,4 +1,3 @@ -import sys from collections.abc import Mapping, Sequence __all__ = ["readmodule", "readmodule_ex", "Class", "Function"] @@ -8,44 +7,33 @@ class _Object: name: str file: int lineno: int - - if sys.version_info >= (3, 10): - end_lineno: int | None - + end_lineno: int | None parent: _Object | None # This is a dict at runtime, but we're typing it as Mapping to # avoid variance issues in the subclasses children: Mapping[str, _Object] - if sys.version_info >= (3, 10): - def __init__( - self, module: str, name: str, file: str, lineno: int, end_lineno: int | None, parent: _Object | None - ) -> None: ... - else: - def __init__(self, module: str, name: str, file: str, lineno: int, parent: _Object | None) -> None: ... + def __init__( + self, module: str, name: str, file: str, lineno: int, end_lineno: int | None, parent: _Object | None + ) -> None: ... class Function(_Object): - if sys.version_info >= (3, 10): - is_async: bool - + is_async: bool parent: Function | Class | None children: dict[str, Class | Function] - if sys.version_info >= (3, 10): - def __init__( - self, - module: str, - name: str, - file: str, - lineno: int, - parent: Function | Class | None = None, - is_async: bool = False, - *, - end_lineno: int | None = None, - ) -> None: ... - else: - def __init__(self, module: str, name: str, file: str, lineno: int, parent: Function | Class | None = None) -> None: ... + def __init__( + self, + module: str, + name: str, + file: str, + lineno: int, + parent: Function | Class | None = None, + is_async: bool = False, + *, + end_lineno: int | None = None, + ) -> None: ... class Class(_Object): super: list[Class | str] | None @@ -53,22 +41,17 @@ class Class(_Object): parent: Class | None children: dict[str, Class | Function] - if sys.version_info >= (3, 10): - def __init__( - self, - module: str, - name: str, - super_: list[Class | str] | None, - file: str, - lineno: int, - parent: Class | None = None, - *, - end_lineno: int | None = None, - ) -> None: ... - else: - def __init__( - self, module: str, name: str, super: list[Class | str] | None, file: str, lineno: int, parent: Class | None = None - ) -> None: ... + def __init__( + self, + module: str, + name: str, + super_: list[Class | str] | None, + file: str, + lineno: int, + parent: Class | None = None, + *, + end_lineno: int | None = None, + ) -> None: ... def readmodule(module: str, path: Sequence[str] | None = None) -> dict[str, Class]: ... def readmodule_ex(module: str, path: Sequence[str] | None = None) -> dict[str, Class | Function | list[str]]: ... diff --git a/mypy/typeshed/stdlib/pydoc.pyi b/mypy/typeshed/stdlib/pydoc.pyi index f8129ac20ade7..2e423dd3d425e 100644 --- a/mypy/typeshed/stdlib/pydoc.pyi +++ b/mypy/typeshed/stdlib/pydoc.pyi @@ -1,12 +1,12 @@ import sys -from _typeshed import OptExcInfo, SupportsWrite, Unused +from _typeshed import OptExcInfo, StrPath, SupportsWrite, Unused from abc import abstractmethod from builtins import list as _list # "list" conflicts with method name from collections.abc import Callable, Container, Mapping, MutableMapping from reprlib import Repr from types import MethodType, ModuleType, TracebackType -from typing import IO, Any, AnyStr, Final, NoReturn, Protocol, TypeVar, overload, type_check_only -from typing_extensions import TypeGuard, deprecated +from typing import IO, Any, AnyStr, Final, NoReturn, Protocol, TypeGuard, TypeVar, overload, type_check_only +from typing_extensions import deprecated __all__ = ["help"] @@ -32,14 +32,8 @@ def stripid(text: str) -> str: ... def allmethods(cl: type) -> MutableMapping[str, MethodType]: ... def visiblename(name: str, all: Container[str] | None = None, obj: object = None) -> bool: ... def classify_class_attrs(object: object) -> list[tuple[str, str, type, str]]: ... - -if sys.version_info >= (3, 13): - @deprecated("Deprecated since Python 3.13.") - def ispackage(path: str) -> bool: ... # undocumented - -else: - def ispackage(path: str) -> bool: ... # undocumented - +@deprecated("Deprecated since Python 3.13.") +def ispackage(path: StrPath) -> bool: ... # undocumented def source_synopsis(file: IO[AnyStr]) -> AnyStr | None: ... def synopsis(filename: str, cache: MutableMapping[str, tuple[int, str]] = {}) -> str | None: ... @@ -62,6 +56,9 @@ def safeimport(path: str, forceload: bool = ..., cache: MutableMapping[str, Modu class Doc: PYTHONDOCS: str + if sys.version_info >= (3, 15): + STDLIB_DIR: str + def document(self, object: object, name: str | None = None, *args: Any) -> str: ... def fail(self, object: object, name: str | None = None, *args: Any) -> NoReturn: ... @abstractmethod @@ -76,7 +73,10 @@ class Doc: def docproperty(self, object: object, name: str | None = None, *args: Any) -> str: ... @abstractmethod def docdata(self, object: object, name: str | None = None, *args: Any) -> str: ... - def getdocloc(self, object: object, basedir: str = ...) -> str | None: ... + if sys.version_info >= (3, 15): + def getdocloc(self, object: object, basedir: str | None = None) -> str | None: ... + else: + def getdocloc(self, object: object, basedir: str = ...) -> str | None: ... class HTMLRepr(Repr): def __init__(self) -> None: ... diff --git a/mypy/typeshed/stdlib/pyexpat/__init__.pyi b/mypy/typeshed/stdlib/pyexpat/__init__.pyi index bc522d5f3c92d..806db63693b50 100644 --- a/mypy/typeshed/stdlib/pyexpat/__init__.pyi +++ b/mypy/typeshed/stdlib/pyexpat/__init__.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import ReadableBuffer, SupportsRead from collections.abc import Callable from pyexpat import errors as errors, model as model -from typing import Any, Final, final -from typing_extensions import CapsuleType, TypeAlias +from typing import Any, Final, TypeAlias, final +from typing_extensions import CapsuleType from xml.parsers.expat import ExpatError as ExpatError EXPAT_VERSION: Final[str] # undocumented @@ -30,10 +30,12 @@ class XMLParserType: def UseForeignDTD(self, flag: bool = True, /) -> None: ... def GetReparseDeferralEnabled(self) -> bool: ... def SetReparseDeferralEnabled(self, enabled: bool, /) -> None: ... - if sys.version_info >= (3, 10): - # Added in Python 3.10.20, 3.11.15, 3.12.3, 3.13.10, 3.14.1 - def SetAllocTrackerActivationThreshold(self, threshold: int, /) -> None: ... - def SetAllocTrackerMaximumAmplification(self, max_factor: float, /) -> None: ... + # Added in Python 3.10.20, 3.11.15, 3.12.3, 3.13.10, 3.14.1 + def SetAllocTrackerActivationThreshold(self, threshold: int, /) -> None: ... + def SetAllocTrackerMaximumAmplification(self, max_factor: float, /) -> None: ... + if sys.version_info >= (3, 15): + def SetBillionLaughsAttackProtectionActivationThreshold(self, threshold: int, /) -> None: ... + def SetBillionLaughsAttackProtectionMaximumAmplification(self, max_factor: float, /) -> None: ... @property def intern(self) -> dict[str, str]: ... diff --git a/mypy/typeshed/stdlib/random.pyi b/mypy/typeshed/stdlib/random.pyi index 08619bf66351a..80fd2c7628a52 100644 --- a/mypy/typeshed/stdlib/random.pyi +++ b/mypy/typeshed/stdlib/random.pyi @@ -4,7 +4,7 @@ from _typeshed import SupportsLenAndGetItem from collections.abc import Callable, Iterable, MutableSequence, Sequence, Set as AbstractSet from fractions import Fraction from typing import Any, ClassVar, NoReturn, TypeVar, overload -from typing_extensions import Self, deprecated +from typing_extensions import deprecated __all__ = [ "Random", @@ -45,9 +45,6 @@ class Random(_random.Random): # Using other `seed` types is deprecated since 3.9 and removed in 3.11 # Ignore Y041, since random.seed doesn't treat int like a float subtype. Having an explicit # int better documents conventional usage of random.seed. - if sys.version_info < (3, 10): - # this is a workaround for pyright correctly flagging an inconsistent inherited constructor, see #14624 - def __new__(cls, x: int | float | str | bytes | bytearray | None = None) -> Self: ... # noqa: Y041 def seed(self, a: int | float | str | bytes | bytearray | None = None, version: int = 2) -> None: ... # type: ignore[override] # noqa: Y041 def getstate(self) -> tuple[Any, ...]: ... @@ -72,6 +69,7 @@ class Random(_random.Random): @overload @deprecated("The `random` parameter is deprecated since Python 3.9; removed in Python 3.11.") def shuffle(self, x: MutableSequence[Any], random: Callable[[], float] | None = None) -> None: ... + if sys.version_info >= (3, 11): def sample(self, population: Sequence[_T], k: int, *, counts: Iterable[int] | None = None) -> list[_T]: ... else: diff --git a/mypy/typeshed/stdlib/re.pyi b/mypy/typeshed/stdlib/re.pyi index fb2a06d5e4c81..0183d7a813318 100644 --- a/mypy/typeshed/stdlib/re.pyi +++ b/mypy/typeshed/stdlib/re.pyi @@ -1,12 +1,10 @@ import enum -import sre_compile -import sre_constants import sys from _typeshed import MaybeNone, ReadableBuffer from collections.abc import Callable, Iterator, Mapping from types import GenericAlias -from typing import Any, AnyStr, Final, Generic, Literal, TypeVar, final, overload -from typing_extensions import TypeAlias, deprecated +from typing import Any, AnyStr, Final, Generic, Literal, TypeAlias, TypeVar, final, overload +from typing_extensions import deprecated __all__ = [ "match", @@ -38,6 +36,8 @@ __all__ = [ "Match", "Pattern", ] +if sys.version_info >= (3, 15): + __all__ += ["prefixmatch"] if sys.version_info < (3, 13): __all__ += ["template"] @@ -47,8 +47,6 @@ if sys.version_info >= (3, 11): if sys.version_info >= (3, 13): __all__ += ["PatternError"] - PatternError = sre_constants.error - _T = TypeVar("_T") # The implementation defines this in re._constants (version_info >= 3, 11) or @@ -61,6 +59,9 @@ class error(Exception): colno: int def __init__(self, msg: str, pattern: str | bytes | None = None, pos: int | None = None) -> None: ... +if sys.version_info >= (3, 13): + PatternError = error + @final class Match(Generic[AnyStr]): @property @@ -78,12 +79,14 @@ class Match(Generic[AnyStr]): # this match instance. @property def re(self) -> Pattern[AnyStr]: ... + @overload def expand(self: Match[str], template: str) -> str: ... @overload def expand(self: Match[bytes], template: ReadableBuffer) -> bytes: ... @overload def expand(self, template: AnyStr) -> AnyStr: ... + # group() returns "AnyStr" or "AnyStr | None", depending on the pattern. @overload def group(self, group: Literal[0] = 0, /) -> AnyStr: ... @@ -91,28 +94,33 @@ class Match(Generic[AnyStr]): def group(self, group: str | int, /) -> AnyStr | MaybeNone: ... @overload def group(self, group1: str | int, group2: str | int, /, *groups: str | int) -> tuple[AnyStr | MaybeNone, ...]: ... + # Each item of groups()'s return tuple is either "AnyStr" or # "AnyStr | None", depending on the pattern. @overload def groups(self) -> tuple[AnyStr | MaybeNone, ...]: ... @overload def groups(self, default: _T) -> tuple[AnyStr | _T, ...]: ... + # Each value in groupdict()'s return dict is either "AnyStr" or # "AnyStr | None", depending on the pattern. @overload def groupdict(self) -> dict[str, AnyStr | MaybeNone]: ... @overload def groupdict(self, default: _T) -> dict[str, AnyStr | _T]: ... + def start(self, group: int | str = 0, /) -> int: ... def end(self, group: int | str = 0, /) -> int: ... def span(self, group: int | str = 0, /) -> tuple[int, int]: ... @property def regs(self) -> tuple[tuple[int, int], ...]: ... # undocumented + # __getitem__() returns "AnyStr" or "AnyStr | None", depending on the pattern. @overload def __getitem__(self, key: Literal[0], /) -> AnyStr: ... @overload def __getitem__(self, key: int | str, /) -> AnyStr | MaybeNone: ... + def __copy__(self) -> Match[AnyStr]: ... def __deepcopy__(self, memo: Any, /) -> Match[AnyStr]: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @@ -127,18 +135,24 @@ class Pattern(Generic[AnyStr]): def groups(self) -> int: ... @property def pattern(self) -> AnyStr: ... + @overload def search(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Match[str] | None: ... @overload def search(self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize) -> Match[bytes] | None: ... @overload def search(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Match[AnyStr] | None: ... + @overload def match(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Match[str] | None: ... @overload def match(self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize) -> Match[bytes] | None: ... @overload def match(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Match[AnyStr] | None: ... + + if sys.version_info >= (3, 15): + prefixmatch = match + @overload def fullmatch(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Match[str] | None: ... @overload @@ -147,12 +161,14 @@ class Pattern(Generic[AnyStr]): ) -> Match[bytes] | None: ... @overload def fullmatch(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Match[AnyStr] | None: ... + @overload def split(self: Pattern[str], string: str, maxsplit: int = 0) -> list[str | MaybeNone]: ... @overload def split(self: Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0) -> list[bytes | MaybeNone]: ... @overload def split(self, string: AnyStr, maxsplit: int = 0) -> list[AnyStr | MaybeNone]: ... + # return type depends on the number of groups in the pattern @overload def findall(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> list[Any]: ... @@ -160,6 +176,7 @@ class Pattern(Generic[AnyStr]): def findall(self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize) -> list[Any]: ... @overload def findall(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> list[AnyStr]: ... + @overload def finditer(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> Iterator[Match[str]]: ... @overload @@ -168,6 +185,7 @@ class Pattern(Generic[AnyStr]): ) -> Iterator[Match[bytes]]: ... @overload def finditer(self, string: AnyStr, pos: int = 0, endpos: int = sys.maxsize) -> Iterator[Match[AnyStr]]: ... + @overload def sub(self: Pattern[str], repl: str | Callable[[Match[str]], str], string: str, count: int = 0) -> str: ... @overload @@ -179,6 +197,7 @@ class Pattern(Generic[AnyStr]): ) -> bytes: ... @overload def sub(self, repl: AnyStr | Callable[[Match[AnyStr]], AnyStr], string: AnyStr, count: int = 0) -> AnyStr: ... + @overload def subn(self: Pattern[str], repl: str | Callable[[Match[str]], str], string: str, count: int = 0) -> tuple[str, int]: ... @overload @@ -190,6 +209,7 @@ class Pattern(Generic[AnyStr]): ) -> tuple[bytes, int]: ... @overload def subn(self, repl: AnyStr | Callable[[Match[AnyStr]], AnyStr], string: AnyStr, count: int = 0) -> tuple[AnyStr, int]: ... + def __copy__(self) -> Pattern[AnyStr]: ... def __deepcopy__(self, memo: Any, /) -> Pattern[AnyStr]: ... def __eq__(self, value: object, /) -> bool: ... @@ -199,23 +219,23 @@ class Pattern(Generic[AnyStr]): # ----- re variables and constants ----- class RegexFlag(enum.IntFlag): - A = sre_compile.SRE_FLAG_ASCII + A = 256 ASCII = A - DEBUG = sre_compile.SRE_FLAG_DEBUG - I = sre_compile.SRE_FLAG_IGNORECASE + DEBUG = 128 + I = 2 IGNORECASE = I - L = sre_compile.SRE_FLAG_LOCALE + L = 4 LOCALE = L - M = sre_compile.SRE_FLAG_MULTILINE + M = 8 MULTILINE = M - S = sre_compile.SRE_FLAG_DOTALL + S = 16 DOTALL = S - X = sre_compile.SRE_FLAG_VERBOSE + X = 64 VERBOSE = X - U = sre_compile.SRE_FLAG_UNICODE + U = 32 UNICODE = U if sys.version_info < (3, 13): - T = sre_compile.SRE_FLAG_TEMPLATE + T = 1 TEMPLATE = T if sys.version_info >= (3, 11): NOFLAG = 0 @@ -253,32 +273,45 @@ _FlagsType: TypeAlias = int | RegexFlag def compile(pattern: AnyStr, flags: _FlagsType = 0) -> Pattern[AnyStr]: ... @overload def compile(pattern: Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ... + @overload def search(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ... @overload def search(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Match[bytes] | None: ... + @overload def match(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ... @overload def match(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Match[bytes] | None: ... + +if sys.version_info >= (3, 15): + @overload + def prefixmatch(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ... + @overload + def prefixmatch(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Match[bytes] | None: ... + @overload def fullmatch(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Match[str] | None: ... @overload def fullmatch(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Match[bytes] | None: ... + @overload def split(pattern: str | Pattern[str], string: str, maxsplit: int = 0, flags: _FlagsType = 0) -> list[str | MaybeNone]: ... @overload def split( pattern: bytes | Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0, flags: _FlagsType = 0 ) -> list[bytes | MaybeNone]: ... + @overload def findall(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> list[Any]: ... @overload def findall(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> list[Any]: ... + @overload def finditer(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> Iterator[Match[str]]: ... @overload def finditer(pattern: bytes | Pattern[bytes], string: ReadableBuffer, flags: _FlagsType = 0) -> Iterator[Match[bytes]]: ... + @overload def sub( pattern: str | Pattern[str], repl: str | Callable[[Match[str]], str], string: str, count: int = 0, flags: _FlagsType = 0 @@ -291,6 +324,7 @@ def sub( count: int = 0, flags: _FlagsType = 0, ) -> bytes: ... + @overload def subn( pattern: str | Pattern[str], repl: str | Callable[[Match[str]], str], string: str, count: int = 0, flags: _FlagsType = 0 @@ -303,12 +337,10 @@ def subn( count: int = 0, flags: _FlagsType = 0, ) -> tuple[bytes, int]: ... + def escape(pattern: AnyStr) -> AnyStr: ... def purge() -> None: ... if sys.version_info < (3, 13): - if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `re.compile()` instead.") - def template(pattern: AnyStr | Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ... # undocumented - else: - def template(pattern: AnyStr | Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ... # undocumented + @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `re.compile()` instead.") + def template(pattern: AnyStr | Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: ... # undocumented diff --git a/mypy/typeshed/stdlib/readline.pyi b/mypy/typeshed/stdlib/readline.pyi index 7325c267b32c2..aff1e504a174a 100644 --- a/mypy/typeshed/stdlib/readline.pyi +++ b/mypy/typeshed/stdlib/readline.pyi @@ -1,8 +1,7 @@ import sys from _typeshed import StrOrBytesPath from collections.abc import Callable, Sequence -from typing import Literal -from typing_extensions import TypeAlias +from typing import Literal, TypeAlias if sys.platform != "win32": _Completer: TypeAlias = Callable[[str, int], str | None] diff --git a/mypy/typeshed/stdlib/reprlib.pyi b/mypy/typeshed/stdlib/reprlib.pyi index 68ada65693485..d990c2708ae95 100644 --- a/mypy/typeshed/stdlib/reprlib.pyi +++ b/mypy/typeshed/stdlib/reprlib.pyi @@ -2,8 +2,7 @@ import sys from array import array from collections import deque from collections.abc import Callable -from typing import Any -from typing_extensions import TypeAlias +from typing import Any, TypeAlias __all__ = ["Repr", "repr", "recursive_repr"] diff --git a/mypy/typeshed/stdlib/resource.pyi b/mypy/typeshed/stdlib/resource.pyi index f99cd5b088056..7e72b28cc6dd1 100644 --- a/mypy/typeshed/stdlib/resource.pyi +++ b/mypy/typeshed/stdlib/resource.pyi @@ -15,6 +15,9 @@ if sys.platform != "win32": RLIMIT_RSS: Final[int] RLIMIT_STACK: Final[int] RLIM_INFINITY: Final[int] + if sys.version_info >= (3, 15): + RLIM_SAVED_CUR: Final[int] + RLIM_SAVED_MAX: Final[int] RUSAGE_CHILDREN: Final[int] RUSAGE_SELF: Final[int] if sys.platform == "linux": @@ -25,30 +28,34 @@ if sys.platform != "win32": RLIMIT_RTTIME: Final[int] RLIMIT_SIGPENDING: Final[int] RUSAGE_THREAD: Final[int] + if sys.version_info >= (3, 15) and sys.platform != "linux" and sys.platform != "darwin": + RLIMIT_NTHR: Final[int] + RLIMIT_PIPEBUF: Final[int] + RLIMIT_THREADS: Final[int] + RLIMIT_UMTXP: Final[int] @final class struct_rusage( structseq[float], tuple[float, float, int, int, int, int, int, int, int, int, int, int, int, int, int, int] ): - if sys.version_info >= (3, 10): - __match_args__: Final = ( - "ru_utime", - "ru_stime", - "ru_maxrss", - "ru_ixrss", - "ru_idrss", - "ru_isrss", - "ru_minflt", - "ru_majflt", - "ru_nswap", - "ru_inblock", - "ru_oublock", - "ru_msgsnd", - "ru_msgrcv", - "ru_nsignals", - "ru_nvcsw", - "ru_nivcsw", - ) + __match_args__: Final = ( + "ru_utime", + "ru_stime", + "ru_maxrss", + "ru_ixrss", + "ru_idrss", + "ru_isrss", + "ru_minflt", + "ru_majflt", + "ru_nswap", + "ru_inblock", + "ru_oublock", + "ru_msgsnd", + "ru_msgrcv", + "ru_nsignals", + "ru_nvcsw", + "ru_nivcsw", + ) @property def ru_utime(self) -> float: ... diff --git a/mypy/typeshed/stdlib/sched.pyi b/mypy/typeshed/stdlib/sched.pyi index 436d9984ee4da..54eb6ed0cd537 100644 --- a/mypy/typeshed/stdlib/sched.pyi +++ b/mypy/typeshed/stdlib/sched.pyi @@ -1,33 +1,18 @@ -import sys import time from collections.abc import Callable -from typing import Any, ClassVar, NamedTuple, type_check_only -from typing_extensions import TypeAlias +from typing import Any, NamedTuple, TypeAlias __all__ = ["scheduler"] _ActionCallback: TypeAlias = Callable[..., Any] -if sys.version_info >= (3, 10): - class Event(NamedTuple): - time: float - priority: Any - sequence: int - action: _ActionCallback - argument: tuple[Any, ...] - kwargs: dict[str, Any] - -else: - @type_check_only - class _EventBase(NamedTuple): - time: float - priority: Any - action: _ActionCallback - argument: tuple[Any, ...] - kwargs: dict[str, Any] - - class Event(_EventBase): - __hash__: ClassVar[None] # type: ignore[assignment] +class Event(NamedTuple): + time: float + priority: Any + sequence: int + action: _ActionCallback + argument: tuple[Any, ...] + kwargs: dict[str, Any] class scheduler: timefunc: Callable[[], float] diff --git a/mypy/typeshed/stdlib/select.pyi b/mypy/typeshed/stdlib/select.pyi index d2b1d6d676e57..ad93cb0b7055b 100644 --- a/mypy/typeshed/stdlib/select.pyi +++ b/mypy/typeshed/stdlib/select.pyi @@ -24,6 +24,7 @@ if sys.platform != "win32": # This is actually a function that returns an instance of a class. # The class is not accessible directly, and also calls itself select.poll. + @final class poll: # default value is select.POLLIN | select.POLLPRI | select.POLLOUT def register(self, fd: FileDescriptorLike, eventmask: int = 7, /) -> None: ... @@ -31,9 +32,9 @@ if sys.platform != "win32": def unregister(self, fd: FileDescriptorLike, /) -> None: ... def poll(self, timeout: float | None = None, /) -> list[tuple[int, int]]: ... -_R = TypeVar("_R", default=Never) -_W = TypeVar("_W", default=Never) -_X = TypeVar("_X", default=Never) +_R = TypeVar("_R", default=Never, bound=FileDescriptorLike) +_W = TypeVar("_W", default=Never, bound=FileDescriptorLike) +_X = TypeVar("_X", default=Never, bound=FileDescriptorLike) def select( rlist: Iterable[_R], wlist: Iterable[_W], xlist: Iterable[_X], timeout: float | None = None, / @@ -120,6 +121,7 @@ if sys.platform == "linux": "Use `os.set_inheritable()` to make the file descriptor inheritable." ) def __new__(self, sizehint: int = -1, flags: int = 0) -> Self: ... + def __enter__(self) -> Self: ... def __exit__( self, @@ -158,6 +160,7 @@ if sys.platform == "linux": if sys.platform != "linux" and sys.platform != "darwin" and sys.platform != "win32": # Solaris only + @final class devpoll: def close(self) -> None: ... closed: bool diff --git a/mypy/typeshed/stdlib/selectors.pyi b/mypy/typeshed/stdlib/selectors.pyi index bcca4e341b9a1..4a3478a67552a 100644 --- a/mypy/typeshed/stdlib/selectors.pyi +++ b/mypy/typeshed/stdlib/selectors.pyi @@ -3,9 +3,7 @@ from _typeshed import FileDescriptor, FileDescriptorLike, Unused from abc import ABCMeta, abstractmethod from collections.abc import Mapping from typing import Any, Final, NamedTuple -from typing_extensions import Self, TypeAlias - -_EventMask: TypeAlias = int +from typing_extensions import Self EVENT_READ: Final = 1 EVENT_WRITE: Final = 2 @@ -13,17 +11,17 @@ EVENT_WRITE: Final = 2 class SelectorKey(NamedTuple): fileobj: FileDescriptorLike fd: FileDescriptor - events: _EventMask + events: int data: Any class BaseSelector(metaclass=ABCMeta): @abstractmethod - def register(self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None) -> SelectorKey: ... + def register(self, fileobj: FileDescriptorLike, events: int, data: Any = None) -> SelectorKey: ... @abstractmethod def unregister(self, fileobj: FileDescriptorLike) -> SelectorKey: ... - def modify(self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None) -> SelectorKey: ... + def modify(self, fileobj: FileDescriptorLike, events: int, data: Any = None) -> SelectorKey: ... @abstractmethod - def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... + def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, int]]: ... def close(self) -> None: ... def get_key(self, fileobj: FileDescriptorLike) -> SelectorKey: ... @abstractmethod @@ -32,16 +30,16 @@ class BaseSelector(metaclass=ABCMeta): def __exit__(self, *args: Unused) -> None: ... class _BaseSelectorImpl(BaseSelector, metaclass=ABCMeta): - def register(self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None) -> SelectorKey: ... + def register(self, fileobj: FileDescriptorLike, events: int, data: Any = None) -> SelectorKey: ... def unregister(self, fileobj: FileDescriptorLike) -> SelectorKey: ... - def modify(self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None) -> SelectorKey: ... + def modify(self, fileobj: FileDescriptorLike, events: int, data: Any = None) -> SelectorKey: ... def get_map(self) -> Mapping[FileDescriptorLike, SelectorKey]: ... class SelectSelector(_BaseSelectorImpl): - def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... + def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, int]]: ... class _PollLikeSelector(_BaseSelectorImpl): - def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... + def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, int]]: ... if sys.platform != "win32": class PollSelector(_PollLikeSelector): ... @@ -58,12 +56,12 @@ if sys.platform != "linux" and sys.platform != "darwin" and sys.platform != "win if sys.platform != "win32" and sys.platform != "linux": class KqueueSelector(_BaseSelectorImpl): def fileno(self) -> int: ... - def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... + def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, int]]: ... # Not a real class at runtime, it is just a conditional alias to other real selectors. # The runtime logic is more fine-grained than a `sys.platform` check; # not really expressible in the stubs class DefaultSelector(_BaseSelectorImpl): - def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... + def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, int]]: ... if sys.platform != "win32": def fileno(self) -> int: ... diff --git a/mypy/typeshed/stdlib/shelve.pyi b/mypy/typeshed/stdlib/shelve.pyi index 654c2ea097f78..c599b0af61384 100644 --- a/mypy/typeshed/stdlib/shelve.pyi +++ b/mypy/typeshed/stdlib/shelve.pyi @@ -1,28 +1,53 @@ import sys from _typeshed import StrOrBytesPath -from collections.abc import Iterator, MutableMapping +from collections.abc import Callable, Iterator, MutableMapping from dbm import _TFlags from types import TracebackType from typing import Any, TypeVar, overload from typing_extensions import Self __all__ = ["Shelf", "BsdDbShelf", "DbfilenameShelf", "open"] +if sys.version_info >= (3, 15): + __all__ += ["ShelveError"] _T = TypeVar("_T") _VT = TypeVar("_VT") +if sys.version_info >= (3, 15): + class ShelveError(Exception): ... + class Shelf(MutableMapping[str, _VT]): - def __init__( - self, dict: MutableMapping[bytes, bytes], protocol: int | None = None, writeback: bool = False, keyencoding: str = "utf-8" - ) -> None: ... + if sys.version_info >= (3, 15): + def __init__( + self, + dict: MutableMapping[bytes, bytes], + protocol: int | None = None, + writeback: bool = False, + keyencoding: str = "utf-8", + *, + serializer: Callable[[Any], bytes] | None = None, + deserializer: Callable[[bytes], Any] | None = None, + ) -> None: ... + + else: + def __init__( + self, + dict: MutableMapping[bytes, bytes], + protocol: int | None = None, + writeback: bool = False, + keyencoding: str = "utf-8", + ) -> None: ... + def __iter__(self) -> Iterator[str]: ... def __len__(self) -> int: ... + @overload # type: ignore[override] def get(self, key: str, default: None = None) -> _VT | None: ... @overload def get(self, key: str, default: _VT) -> _VT: ... @overload def get(self, key: str, default: _T) -> _VT | _T: ... + def __getitem__(self, key: str) -> _VT: ... def __setitem__(self, key: str, value: _VT) -> None: ... def __delitem__(self, key: str) -> None: ... @@ -34,6 +59,8 @@ class Shelf(MutableMapping[str, _VT]): def __del__(self) -> None: ... def close(self) -> None: ... def sync(self) -> None: ... + if sys.version_info >= (3, 15): + def reorganize(self) -> None: ... class BsdDbShelf(Shelf[_VT]): def set_location(self, key: str) -> tuple[str, _VT]: ... @@ -43,14 +70,38 @@ class BsdDbShelf(Shelf[_VT]): def last(self) -> tuple[str, _VT]: ... class DbfilenameShelf(Shelf[_VT]): - if sys.version_info >= (3, 11): + if sys.version_info >= (3, 15): + def __init__( + self, + filename: StrOrBytesPath, + flag: _TFlags = "c", + protocol: int | None = None, + writeback: bool = False, + *, + serializer: Callable[[Any], bytes] | None = None, + deserializer: Callable[[bytes], Any] | None = None, + ) -> None: ... + + elif sys.version_info >= (3, 11): def __init__( self, filename: StrOrBytesPath, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False ) -> None: ... + else: def __init__(self, filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> None: ... -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 15): + def open( + filename: StrOrBytesPath, + flag: _TFlags = "c", + protocol: int | None = None, + writeback: bool = False, + *, + serializer: Callable[[Any], bytes] | None = None, + deserializer: Callable[[bytes], Any] | None = None, + ) -> Shelf[Any]: ... + +elif sys.version_info >= (3, 11): def open( filename: StrOrBytesPath, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False ) -> Shelf[Any]: ... diff --git a/mypy/typeshed/stdlib/shutil.pyi b/mypy/typeshed/stdlib/shutil.pyi index cc26cfc556a00..badb0bae220dc 100644 --- a/mypy/typeshed/stdlib/shutil.pyi +++ b/mypy/typeshed/stdlib/shutil.pyi @@ -3,8 +3,8 @@ import sys from _typeshed import BytesPath, ExcInfo, FileDescriptorOrPath, MaybeNone, StrOrBytesPath, StrPath, SupportsRead, SupportsWrite from collections.abc import Callable, Iterable, Sequence from tarfile import _TarfileFilter -from typing import Any, AnyStr, NamedTuple, NoReturn, Protocol, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias, deprecated +from typing import Any, AnyStr, NamedTuple, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import deprecated __all__ = [ "copyfileobj", @@ -57,14 +57,17 @@ def copyfileobj(fsrc: SupportsRead[AnyStr], fdst: SupportsWrite[AnyStr], length: def copyfile(src: StrOrBytesPath, dst: _StrOrBytesPathT, *, follow_symlinks: bool = True) -> _StrOrBytesPathT: ... def copymode(src: StrOrBytesPath, dst: StrOrBytesPath, *, follow_symlinks: bool = True) -> None: ... def copystat(src: StrOrBytesPath, dst: StrOrBytesPath, *, follow_symlinks: bool = True) -> None: ... + @overload def copy(src: StrPath, dst: _StrPathT, *, follow_symlinks: bool = True) -> _StrPathT | str: ... @overload def copy(src: BytesPath, dst: _BytesPathT, *, follow_symlinks: bool = True) -> _BytesPathT | bytes: ... + @overload def copy2(src: StrPath, dst: _StrPathT, *, follow_symlinks: bool = True) -> _StrPathT | str: ... @overload def copy2(src: BytesPath, dst: _BytesPathT, *, follow_symlinks: bool = True) -> _BytesPathT | bytes: ... + def ignore_patterns(*patterns: StrPath) -> Callable[[Any, list[str]], set[str]]: ... def copytree( src: StrPath, @@ -175,7 +178,6 @@ if sys.version_info >= (3, 13): def chown( path: FileDescriptorOrPath, user: str | int, group: str | int, *, dir_fd: int | None = None, follow_symlinks: bool = True ) -> None: ... - else: @overload def chown(path: FileDescriptorOrPath, user: str | int, group: None = None) -> None: ... @@ -195,6 +197,7 @@ if sys.platform == "win32" and sys.version_info < (3, 12): def which(cmd: StrPath, mode: int = 1, path: StrPath | None = None) -> str | None: ... @overload def which(cmd: bytes, mode: int = 1, path: StrPath | None = None) -> bytes | None: ... + def make_archive( base_name: str, format: str, @@ -207,6 +210,7 @@ def make_archive( logger: Any | None = None, ) -> str: ... def get_archive_formats() -> list[tuple[str, str]]: ... + @overload def register_archive_format( name: str, function: Callable[..., object], extra_args: Sequence[tuple[str, Any] | list[Any]], description: str = "" @@ -215,10 +219,12 @@ def register_archive_format( def register_archive_format( name: str, function: Callable[[str, str], object], extra_args: None = None, description: str = "" ) -> None: ... + def unregister_archive_format(name: str) -> None: ... def unpack_archive( filename: StrPath, extract_dir: StrPath | None = None, format: str | None = None, *, filter: _TarfileFilter | None = None ) -> None: ... + @overload def register_unpack_format( name: str, @@ -231,6 +237,7 @@ def register_unpack_format( def register_unpack_format( name: str, extensions: list[str], function: Callable[[str, str], object], extra_args: None = None, description: str = "" ) -> None: ... + def unregister_unpack_format(name: str) -> None: ... def get_unpack_formats() -> list[tuple[str, list[str], str]]: ... def get_terminal_size(fallback: tuple[int, int] = (80, 24)) -> os.terminal_size: ... diff --git a/mypy/typeshed/stdlib/signal.pyi b/mypy/typeshed/stdlib/signal.pyi index 3bf1d31293787..a6a8853671e23 100644 --- a/mypy/typeshed/stdlib/signal.pyi +++ b/mypy/typeshed/stdlib/signal.pyi @@ -3,8 +3,8 @@ from _typeshed import structseq from collections.abc import Callable, Iterable from enum import IntEnum from types import FrameType -from typing import Any, Final, final -from typing_extensions import Never, TypeAlias +from typing import Any, Final, TypeAlias, final +from typing_extensions import Never NSIG: int @@ -69,14 +69,8 @@ _SIGNUM: TypeAlias = int | Signals _HANDLER: TypeAlias = Callable[[int, FrameType | None], Any] | int | Handlers | None def default_int_handler(signalnum: int, frame: FrameType | None, /) -> Never: ... - -if sys.version_info >= (3, 10): # arguments changed in 3.10.2 - def getsignal(signalnum: _SIGNUM) -> _HANDLER: ... - def signal(signalnum: _SIGNUM, handler: _HANDLER) -> _HANDLER: ... - -else: - def getsignal(signalnum: _SIGNUM, /) -> _HANDLER: ... - def signal(signalnum: _SIGNUM, handler: _HANDLER, /) -> _HANDLER: ... +def getsignal(signalnum: _SIGNUM) -> _HANDLER: ... +def signal(signalnum: _SIGNUM, handler: _HANDLER) -> _HANDLER: ... SIGABRT: Final = Signals.SIGABRT SIGFPE: Final = Signals.SIGFPE @@ -135,18 +129,11 @@ else: def getitimer(which: int, /) -> tuple[float, float]: ... def pause() -> None: ... def pthread_kill(thread_id: int, signalnum: int, /) -> None: ... - if sys.version_info >= (3, 10): # arguments changed in 3.10.2 - def pthread_sigmask(how: int, mask: Iterable[int]) -> set[_SIGNUM]: ... - else: - def pthread_sigmask(how: int, mask: Iterable[int], /) -> set[_SIGNUM]: ... - + def pthread_sigmask(how: int, mask: Iterable[int]) -> set[_SIGNUM]: ... def setitimer(which: int, seconds: float, interval: float = 0.0, /) -> tuple[float, float]: ... def siginterrupt(signalnum: int, flag: bool, /) -> None: ... def sigpending() -> Any: ... - if sys.version_info >= (3, 10): # argument changed in 3.10.2 - def sigwait(sigset: Iterable[int]) -> _SIGNUM: ... - else: - def sigwait(sigset: Iterable[int], /) -> _SIGNUM: ... + def sigwait(sigset: Iterable[int]) -> _SIGNUM: ... if sys.platform != "darwin": SIGCLD: Final = Signals.SIGCHLD # alias SIGPOLL: Final = Signals.SIGIO # alias @@ -158,8 +145,7 @@ else: @final class struct_siginfo(structseq[int], tuple[int, int, int, int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("si_signo", "si_code", "si_errno", "si_pid", "si_uid", "si_status", "si_band") + __match_args__: Final = ("si_signo", "si_code", "si_errno", "si_pid", "si_uid", "si_status", "si_band") @property def si_signo(self) -> int: ... diff --git a/mypy/typeshed/stdlib/site.pyi b/mypy/typeshed/stdlib/site.pyi index 6e39677aaea0e..46e82b0655c10 100644 --- a/mypy/typeshed/stdlib/site.pyi +++ b/mypy/typeshed/stdlib/site.pyi @@ -10,6 +10,16 @@ USER_BASE: str | None def main() -> None: ... def abs_paths() -> None: ... # undocumented def addpackage(sitedir: StrPath, name: StrPath, known_paths: set[str] | None) -> set[str] | None: ... # undocumented + +if sys.version_info >= (3, 15): + class StartupState: + __slots__ = ("_known_paths", "_processed_sitedirs", "_path_entries", "_importexecs", "_entrypoints") + def __init__(self, known_paths: set[str] | None = None) -> None: ... + def addsitedir(self, sitedir: str) -> None: ... + def addusersitepackages(self) -> None: ... + def addsitepackages(self, prefixes: Iterable[str] | None = None) -> None: ... + def process(self) -> None: ... + def addsitedir(sitedir: str, known_paths: set[str] | None = None) -> None: ... def addsitepackages(known_paths: set[str] | None, prefixes: Iterable[str] | None = None) -> set[str] | None: ... # undocumented def addusersitepackages(known_paths: set[str] | None) -> set[str] | None: ... # undocumented diff --git a/mypy/typeshed/stdlib/smtpd.pyi b/mypy/typeshed/stdlib/smtpd.pyi index dee7e949f42fa..cc9ac391a441f 100644 --- a/mypy/typeshed/stdlib/smtpd.pyi +++ b/mypy/typeshed/stdlib/smtpd.pyi @@ -3,8 +3,8 @@ import asyncore import socket import sys from collections import defaultdict -from typing import Any -from typing_extensions import TypeAlias, deprecated +from typing import Any, TypeAlias +from typing_extensions import deprecated if sys.version_info >= (3, 11): __all__ = ["SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy"] diff --git a/mypy/typeshed/stdlib/smtplib.pyi b/mypy/typeshed/stdlib/smtplib.pyi index 74b5ea2cb6fce..1aaa5b49664b0 100644 --- a/mypy/typeshed/stdlib/smtplib.pyi +++ b/mypy/typeshed/stdlib/smtplib.pyi @@ -7,8 +7,8 @@ from re import Pattern from socket import socket from ssl import SSLContext from types import TracebackType -from typing import Any, Final, Protocol, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated +from typing import Any, Final, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import Self, deprecated __all__ = [ "SMTPException", @@ -65,6 +65,7 @@ class SMTPAuthenticationError(SMTPResponseException): ... def quoteaddr(addrstring: str) -> str: ... def quotedata(data: str) -> str: ... + @type_check_only class _AuthObject(Protocol): @overload @@ -121,10 +122,12 @@ class SMTP: user: str password: str def auth(self, mechanism: str, authobject: _AuthObject, *, initial_response_ok: bool = True) -> _Reply: ... + @overload def auth_cram_md5(self, challenge: None = None) -> None: ... @overload def auth_cram_md5(self, challenge: ReadableBuffer) -> str: ... + def auth_plain(self, challenge: ReadableBuffer | None = None) -> str: ... def auth_login(self, challenge: ReadableBuffer | None = None) -> str: ... def login(self, user: str, password: str, *, initial_response_ok: bool = True) -> _Reply: ... @@ -203,6 +206,7 @@ class SMTP_SSL(SMTP): source_address: _SourceAddress | None = None, context: None = None, ) -> None: ... + keyfile: StrOrBytesPath | None certfile: StrOrBytesPath | None diff --git a/mypy/typeshed/stdlib/socket.pyi b/mypy/typeshed/stdlib/socket.pyi index a55273a8772c9..76ba0a85c128c 100644 --- a/mypy/typeshed/stdlib/socket.pyi +++ b/mypy/typeshed/stdlib/socket.pyi @@ -28,6 +28,7 @@ from _socket import ( IP_MULTICAST_LOOP as IP_MULTICAST_LOOP, IP_MULTICAST_TTL as IP_MULTICAST_TTL, IP_OPTIONS as IP_OPTIONS, + IP_RECVTOS as IP_RECVTOS, IP_TOS as IP_TOS, IP_TTL as IP_TTL, IPPORT_RESERVED as IPPORT_RESERVED, @@ -223,6 +224,7 @@ __all__ = [ "IP_MULTICAST_LOOP", "IP_MULTICAST_TTL", "IP_OPTIONS", + "IP_RECVTOS", "IP_TOS", "IP_TTL", "MSG_CTRUNC", @@ -357,11 +359,6 @@ if sys.platform != "darwin": __all__ += ["TCP_KEEPIDLE", "AF_IRDA", "MSG_ERRQUEUE"] -if sys.version_info >= (3, 10): - from _socket import IP_RECVTOS as IP_RECVTOS - - __all__ += ["IP_RECVTOS"] - if sys.platform != "win32" and sys.platform != "darwin": from _socket import ( IP_TRANSPARENT as IP_TRANSPARENT, @@ -525,7 +522,7 @@ if sys.platform != "darwin": if sys.platform != "darwin" and sys.platform != "linux": __all__ += ["BDADDR_ANY", "BDADDR_LOCAL", "BTPROTO_RFCOMM"] -if sys.platform == "darwin" and sys.version_info >= (3, 10): +if sys.platform == "darwin": from _socket import TCP_KEEPALIVE as TCP_KEEPALIVE __all__ += ["TCP_KEEPALIVE"] @@ -779,6 +776,68 @@ if sys.platform == "linux": from _socket import CAN_RAW_ERR_FILTER as CAN_RAW_ERR_FILTER __all__ += ["CAN_RAW_ERR_FILTER"] + if sys.version_info >= (3, 15): + from _socket import ( + CAN_ISOTP_CHK_PAD_DATA as CAN_ISOTP_CHK_PAD_DATA, + CAN_ISOTP_CHK_PAD_LEN as CAN_ISOTP_CHK_PAD_LEN, + CAN_ISOTP_DEFAULT_EXT_ADDRESS as CAN_ISOTP_DEFAULT_EXT_ADDRESS, + CAN_ISOTP_DEFAULT_FLAGS as CAN_ISOTP_DEFAULT_FLAGS, + CAN_ISOTP_DEFAULT_FRAME_TXTIME as CAN_ISOTP_DEFAULT_FRAME_TXTIME, + CAN_ISOTP_DEFAULT_LL_MTU as CAN_ISOTP_DEFAULT_LL_MTU, + CAN_ISOTP_DEFAULT_LL_TX_DL as CAN_ISOTP_DEFAULT_LL_TX_DL, + CAN_ISOTP_DEFAULT_LL_TX_FLAGS as CAN_ISOTP_DEFAULT_LL_TX_FLAGS, + CAN_ISOTP_DEFAULT_PAD_CONTENT as CAN_ISOTP_DEFAULT_PAD_CONTENT, + CAN_ISOTP_DEFAULT_RECV_BS as CAN_ISOTP_DEFAULT_RECV_BS, + CAN_ISOTP_DEFAULT_RECV_STMIN as CAN_ISOTP_DEFAULT_RECV_STMIN, + CAN_ISOTP_DEFAULT_RECV_WFTMAX as CAN_ISOTP_DEFAULT_RECV_WFTMAX, + CAN_ISOTP_EXTEND_ADDR as CAN_ISOTP_EXTEND_ADDR, + CAN_ISOTP_FORCE_RXSTMIN as CAN_ISOTP_FORCE_RXSTMIN, + CAN_ISOTP_FORCE_TXSTMIN as CAN_ISOTP_FORCE_TXSTMIN, + CAN_ISOTP_HALF_DUPLEX as CAN_ISOTP_HALF_DUPLEX, + CAN_ISOTP_LISTEN_MODE as CAN_ISOTP_LISTEN_MODE, + CAN_ISOTP_LL_OPTS as CAN_ISOTP_LL_OPTS, + CAN_ISOTP_OPTS as CAN_ISOTP_OPTS, + CAN_ISOTP_RECV_FC as CAN_ISOTP_RECV_FC, + CAN_ISOTP_RX_EXT_ADDR as CAN_ISOTP_RX_EXT_ADDR, + CAN_ISOTP_RX_PADDING as CAN_ISOTP_RX_PADDING, + CAN_ISOTP_RX_STMIN as CAN_ISOTP_RX_STMIN, + CAN_ISOTP_SF_BROADCAST as CAN_ISOTP_SF_BROADCAST, + CAN_ISOTP_TX_PADDING as CAN_ISOTP_TX_PADDING, + CAN_ISOTP_TX_STMIN as CAN_ISOTP_TX_STMIN, + CAN_ISOTP_WAIT_TX_DONE as CAN_ISOTP_WAIT_TX_DONE, + SOL_CAN_ISOTP as SOL_CAN_ISOTP, + ) + + __all__ += [ + "CAN_ISOTP_CHK_PAD_DATA", + "CAN_ISOTP_CHK_PAD_LEN", + "CAN_ISOTP_DEFAULT_EXT_ADDRESS", + "CAN_ISOTP_DEFAULT_FLAGS", + "CAN_ISOTP_DEFAULT_FRAME_TXTIME", + "CAN_ISOTP_DEFAULT_LL_MTU", + "CAN_ISOTP_DEFAULT_LL_TX_DL", + "CAN_ISOTP_DEFAULT_LL_TX_FLAGS", + "CAN_ISOTP_DEFAULT_PAD_CONTENT", + "CAN_ISOTP_DEFAULT_RECV_BS", + "CAN_ISOTP_DEFAULT_RECV_STMIN", + "CAN_ISOTP_DEFAULT_RECV_WFTMAX", + "CAN_ISOTP_EXTEND_ADDR", + "CAN_ISOTP_FORCE_RXSTMIN", + "CAN_ISOTP_FORCE_TXSTMIN", + "CAN_ISOTP_HALF_DUPLEX", + "CAN_ISOTP_LL_OPTS", + "CAN_ISOTP_LISTEN_MODE", + "CAN_ISOTP_OPTS", + "CAN_ISOTP_RECV_FC", + "CAN_ISOTP_RX_EXT_ADDR", + "CAN_ISOTP_RX_PADDING", + "CAN_ISOTP_RX_STMIN", + "CAN_ISOTP_SF_BROADCAST", + "CAN_ISOTP_TX_PADDING", + "CAN_ISOTP_TX_STMIN", + "CAN_ISOTP_WAIT_TX_DONE", + "SOL_CAN_ISOTP", + ] if sys.platform == "linux": from _socket import ( @@ -842,7 +901,7 @@ if sys.platform == "linux": "UDPLITE_RECV_CSCOV", "UDPLITE_SEND_CSCOV", ] -if sys.platform == "linux" and sys.version_info >= (3, 10): +if sys.platform == "linux": from _socket import IPPROTO_MPTCP as IPPROTO_MPTCP __all__ += ["IPPROTO_MPTCP"] @@ -1044,6 +1103,12 @@ if sys.platform != "linux": __all__ += ["IPPROTO_GGP", "IPPROTO_IPV4", "IPPROTO_MAX", "IPPROTO_ND", "IP_RECVDSTADDR", "SO_USELOOPBACK"] +if sys.version_info >= (3, 15): + if sys.platform == "win32" or sys.platform == "linux": + from _socket import IPV6_HDRINCL as IPV6_HDRINCL + + __all__ += ["IPV6_HDRINCL"] + if sys.version_info >= (3, 14): from _socket import IP_RECVTTL as IP_RECVTTL @@ -1087,10 +1152,7 @@ error = OSError class herror(error): ... class gaierror(error): ... -if sys.version_info >= (3, 10): - timeout = TimeoutError -else: - class timeout(error): ... +timeout = TimeoutError class AddressFamily(IntEnum): AF_INET = 2 @@ -1325,6 +1387,7 @@ class socket(_socket.socket): def __exit__(self, *args: Unused) -> None: ... def dup(self) -> Self: ... def accept(self) -> tuple[socket, _RetAddress]: ... + # Note that the makefile's documented windows-specific behavior is not represented # mode strings with duplicates are intentionally excluded @overload @@ -1387,6 +1450,7 @@ class socket(_socket.socket): errors: str | None = None, newline: str | None = None, ) -> TextIOWrapper: ... + def sendfile(self, file: _SendableFile, offset: int = 0, count: int | None = None) -> int: ... @property def family(self) -> AddressFamily: ... diff --git a/mypy/typeshed/stdlib/socketserver.pyi b/mypy/typeshed/stdlib/socketserver.pyi index f321d14a792b2..05e0025d6a15b 100644 --- a/mypy/typeshed/stdlib/socketserver.pyi +++ b/mypy/typeshed/stdlib/socketserver.pyi @@ -5,8 +5,8 @@ from _typeshed import ReadableBuffer from collections.abc import Callable from io import BufferedIOBase from socket import socket as _socket -from typing import Any, ClassVar -from typing_extensions import Self, TypeAlias +from typing import Any, ClassVar, TypeAlias +from typing_extensions import Self __all__ = [ "BaseServer", diff --git a/mypy/typeshed/stdlib/spwd.pyi b/mypy/typeshed/stdlib/spwd.pyi index 3a5d39997dcc7..0a06cdfeef642 100644 --- a/mypy/typeshed/stdlib/spwd.pyi +++ b/mypy/typeshed/stdlib/spwd.pyi @@ -5,18 +5,17 @@ from typing import Any, Final, final if sys.platform != "win32": @final class struct_spwd(structseq[Any], tuple[str, str, int, int, int, int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ( - "sp_namp", - "sp_pwdp", - "sp_lstchg", - "sp_min", - "sp_max", - "sp_warn", - "sp_inact", - "sp_expire", - "sp_flag", - ) + __match_args__: Final = ( + "sp_namp", + "sp_pwdp", + "sp_lstchg", + "sp_min", + "sp_max", + "sp_warn", + "sp_inact", + "sp_expire", + "sp_flag", + ) @property def sp_namp(self) -> str: ... diff --git a/mypy/typeshed/stdlib/sqlite3/__init__.pyi b/mypy/typeshed/stdlib/sqlite3/__init__.pyi index ec37eed8c9277..7bf020558199e 100644 --- a/mypy/typeshed/stdlib/sqlite3/__init__.pyi +++ b/mypy/typeshed/stdlib/sqlite3/__init__.pyi @@ -62,12 +62,15 @@ from sqlite3.dbapi2 import ( threadsafety as threadsafety, ) from types import TracebackType -from typing import Any, Literal, Protocol, SupportsIndex, TypeVar, final, overload, type_check_only -from typing_extensions import Self, TypeAlias, disjoint_base +from typing import Any, Literal, Protocol, SupportsIndex, TypeAlias, TypeVar, final, overload, type_check_only +from typing_extensions import Self, disjoint_base if sys.version_info < (3, 14): from sqlite3.dbapi2 import version_info as version_info +if sys.version_info >= (3, 15): + from sqlite3.dbapi2 import SQLITE_KEYWORDS as SQLITE_KEYWORDS + if sys.version_info >= (3, 12): from sqlite3.dbapi2 import ( LEGACY_TRANSACTION_CONTROL as LEGACY_TRANSACTION_CONTROL, @@ -211,9 +214,6 @@ if sys.version_info >= (3, 11): if sys.version_info < (3, 12): from sqlite3.dbapi2 import enable_shared_cache as enable_shared_cache, version as version -if sys.version_info < (3, 10): - from sqlite3.dbapi2 import OptimizedUnicode as OptimizedUnicode - _CursorT = TypeVar("_CursorT", bound=Cursor) _SqliteData: TypeAlias = str | ReadableBuffer | int | float | None # Data that is passed through adapters can be of any type accepted by an adapter. @@ -301,6 +301,7 @@ class Connection: def autocommit(self) -> int: ... @autocommit.setter def autocommit(self, val: int) -> None: ... + row_factory: _RowFactoryOptions text_factory: Any if sys.version_info >= (3, 12): @@ -334,7 +335,10 @@ class Connection: def blobopen(self, table: str, column: str, row: int, /, *, readonly: bool = False, name: str = "main") -> Blob: ... def commit(self) -> None: ... - def create_aggregate(self, name: str, n_arg: int, aggregate_class: Callable[[], _AggregateProtocol]) -> None: ... + if sys.version_info >= (3, 15): + def create_aggregate(self, name: str, n_arg: int, aggregate_class: Callable[[], _AggregateProtocol], /) -> None: ... + else: + def create_aggregate(self, name: str, n_arg: int, aggregate_class: Callable[[], _AggregateProtocol]) -> None: ... if sys.version_info >= (3, 11): # num_params determines how many params will be passed to the aggregate class. We provide an overload # for the case where num_params = 1, which is expected to be the common case. @@ -353,13 +357,20 @@ class Connection: ) -> None: ... def create_collation(self, name: str, callback: Callable[[str, str], SupportsIndex] | None, /) -> None: ... - def create_function( - self, name: str, narg: int, func: Callable[..., _SqliteData] | None, *, deterministic: bool = False - ) -> None: ... + if sys.version_info >= (3, 15): + def create_function( + self, name: str, narg: int, func: Callable[..., _SqliteData] | None, /, *, deterministic: bool = False + ) -> None: ... + else: + def create_function( + self, name: str, narg: int, func: Callable[..., _SqliteData] | None, *, deterministic: bool = False + ) -> None: ... + @overload def cursor(self, factory: None = None) -> Cursor: ... @overload def cursor(self, factory: Callable[[Connection], _CursorT]) -> _CursorT: ... + def execute(self, sql: str, parameters: _Parameters = ..., /) -> Cursor: ... def executemany(self, sql: str, parameters: Iterable[_Parameters], /) -> Cursor: ... def executescript(self, sql_script: str, /) -> Cursor: ... @@ -370,11 +381,18 @@ class Connection: def iterdump(self) -> Generator[str]: ... def rollback(self) -> None: ... - def set_authorizer( - self, authorizer_callback: Callable[[int, str | None, str | None, str | None, str | None], int] | None - ) -> None: ... - def set_progress_handler(self, progress_handler: Callable[[], int | None] | None, n: int) -> None: ... - def set_trace_callback(self, trace_callback: Callable[[str], object] | None) -> None: ... + if sys.version_info >= (3, 15): + def set_authorizer( + self, authorizer_callback: Callable[[int, str | None, str | None, str | None, str | None], int] | None, / + ) -> None: ... + def set_progress_handler(self, progress_handler: Callable[[], int | None] | None, /, n: int) -> None: ... + def set_trace_callback(self, trace_callback: Callable[[str], object] | None, /) -> None: ... + else: + def set_authorizer( + self, authorizer_callback: Callable[[int, str | None, str | None, str | None, str | None], int] | None + ) -> None: ... + def set_progress_handler(self, progress_handler: Callable[[], int | None] | None, n: int) -> None: ... + def set_trace_callback(self, trace_callback: Callable[[str], object] | None) -> None: ... # enable_load_extension and load_extension is not available on python distributions compiled # without sqlite3 loadable extension support. see footnotes https://docs.python.org/3/library/sqlite3.html#f1 def enable_load_extension(self, enable: bool, /) -> None: ... @@ -443,10 +461,12 @@ class PrepareProtocol: class Row(Sequence[Any]): def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> Self: ... def keys(self) -> list[str]: ... + @overload # Note: really needs int instead of SupportsIndex def __getitem__(self, key: int | str, /) -> Any: ... @overload # Note: SupportsIndex does work within slices. def __getitem__(self, key: slice[SupportsIndex | None], /) -> tuple[Any, ...]: ... + def __hash__(self) -> int: ... def __iter__(self) -> Iterator[Any]: ... def __len__(self) -> int: ... diff --git a/mypy/typeshed/stdlib/sqlite3/dbapi2.pyi b/mypy/typeshed/stdlib/sqlite3/dbapi2.pyi index 9e170a81243d8..0cd676f9bfc87 100644 --- a/mypy/typeshed/stdlib/sqlite3/dbapi2.pyi +++ b/mypy/typeshed/stdlib/sqlite3/dbapi2.pyi @@ -90,6 +90,9 @@ if sys.version_info >= (3, 12): SQLITE_DBCONFIG_WRITABLE_SCHEMA as SQLITE_DBCONFIG_WRITABLE_SCHEMA, ) +if sys.version_info >= (3, 15): + from _sqlite3 import SQLITE_KEYWORDS as SQLITE_KEYWORDS + if sys.version_info >= (3, 11): from _sqlite3 import ( SQLITE_ABORT as SQLITE_ABORT, @@ -215,18 +218,12 @@ if sys.version_info < (3, 14): version: Final[str] if sys.version_info < (3, 12): - if sys.version_info >= (3, 10): - # deprecation wrapper that has a different name for the argument... - @deprecated( - "Deprecated since Python 3.10; removed in Python 3.12. " - "Open database in URI mode using `cache=shared` parameter instead." - ) - def enable_shared_cache(enable: int) -> None: ... - else: - from _sqlite3 import enable_shared_cache as enable_shared_cache - -if sys.version_info < (3, 10): - from _sqlite3 import OptimizedUnicode as OptimizedUnicode + # deprecation wrapper that has a different name for the argument... + @deprecated( + "Deprecated since Python 3.10; removed in Python 3.12. " + "Open database in URI mode using `cache=shared` parameter instead." + ) + def enable_shared_cache(enable: int) -> None: ... paramstyle: Final = "qmark" threadsafety: Literal[0, 1, 3] diff --git a/mypy/typeshed/stdlib/sre_parse.pyi b/mypy/typeshed/stdlib/sre_parse.pyi index eaacbff312a92..6b873f4043b07 100644 --- a/mypy/typeshed/stdlib/sre_parse.pyi +++ b/mypy/typeshed/stdlib/sre_parse.pyi @@ -3,8 +3,7 @@ from collections.abc import Iterable from re import Match, Pattern as _Pattern from sre_constants import * from sre_constants import _NamedIntConstant as _NIC, error as _Error -from typing import Any, Final, overload -from typing_extensions import TypeAlias +from typing import Any, Final, TypeAlias, overload SPECIAL_CHARS: Final = ".\\[{()*+?^$|" REPEAT_CHARS: Final = "*+?{" @@ -91,7 +90,6 @@ if sys.version_info >= (3, 12): def parse_template(source: str, pattern: _Pattern[Any]) -> _TemplateType: ... @overload def parse_template(source: bytes, pattern: _Pattern[Any]) -> _TemplateByteType: ... - else: @overload def parse_template(source: str, state: _Pattern[Any]) -> _TemplateType: ... diff --git a/mypy/typeshed/stdlib/ssl.pyi b/mypy/typeshed/stdlib/ssl.pyi index 57952cf19bbed..590642d99607d 100644 --- a/mypy/typeshed/stdlib/ssl.pyi +++ b/mypy/typeshed/stdlib/ssl.pyi @@ -27,21 +27,21 @@ from _ssl import ( ) from _typeshed import ReadableBuffer, StrOrBytesPath, WriteableBuffer from collections.abc import Callable, Iterable -from typing import Any, Final, Literal, NamedTuple, TypedDict, overload, type_check_only -from typing_extensions import Never, Self, TypeAlias, deprecated +from typing import Any, Final, Literal, NamedTuple, TypeAlias, TypedDict, overload, type_check_only +from typing_extensions import Never, Self, deprecated if sys.version_info >= (3, 13): from _ssl import HAS_PSK as HAS_PSK +if sys.version_info >= (3, 15): + from _ssl import HAS_PSK_TLS13 as HAS_PSK_TLS13 + if sys.version_info >= (3, 14): from _ssl import HAS_PHA as HAS_PHA if sys.version_info < (3, 12): from _ssl import RAND_pseudo_bytes as RAND_pseudo_bytes -if sys.version_info < (3, 10): - from _ssl import RAND_egd as RAND_egd - if sys.platform == "win32": from _ssl import enum_certificates as enum_certificates, enum_crls as enum_crls @@ -108,19 +108,16 @@ class VerifyFlags(enum.IntFlag): VERIFY_CRL_CHECK_CHAIN = 0x0C VERIFY_X509_STRICT = 0x20 VERIFY_X509_TRUSTED_FIRST = 0x8000 - if sys.version_info >= (3, 10): - VERIFY_ALLOW_PROXY_CERTS = 0x40 - VERIFY_X509_PARTIAL_CHAIN = 0x80000 + VERIFY_ALLOW_PROXY_CERTS = 0x40 + VERIFY_X509_PARTIAL_CHAIN = 0x80000 VERIFY_DEFAULT: Final = VerifyFlags.VERIFY_DEFAULT VERIFY_CRL_CHECK_LEAF: Final = VerifyFlags.VERIFY_CRL_CHECK_LEAF VERIFY_CRL_CHECK_CHAIN: Final = VerifyFlags.VERIFY_CRL_CHECK_CHAIN VERIFY_X509_STRICT: Final = VerifyFlags.VERIFY_X509_STRICT VERIFY_X509_TRUSTED_FIRST: Final = VerifyFlags.VERIFY_X509_TRUSTED_FIRST - -if sys.version_info >= (3, 10): - VERIFY_ALLOW_PROXY_CERTS: Final = VerifyFlags.VERIFY_ALLOW_PROXY_CERTS - VERIFY_X509_PARTIAL_CHAIN: Final = VerifyFlags.VERIFY_X509_PARTIAL_CHAIN +VERIFY_ALLOW_PROXY_CERTS: Final = VerifyFlags.VERIFY_ALLOW_PROXY_CERTS +VERIFY_X509_PARTIAL_CHAIN: Final = VerifyFlags.VERIFY_X509_PARTIAL_CHAIN class _SSLMethod(enum.IntEnum): PROTOCOL_SSLv23 = 2 @@ -286,33 +283,38 @@ class SSLSocket(socket.socket): ) -> tuple[int, socket._RetAddress]: ... def send(self, data: ReadableBuffer, flags: int = 0) -> int: ... def sendall(self, data: ReadableBuffer, flags: int = 0) -> None: ... + @overload def sendto(self, data: ReadableBuffer, flags_or_addr: socket._Address, addr: None = None) -> int: ... @overload def sendto(self, data: ReadableBuffer, flags_or_addr: int, addr: socket._Address) -> int: ... + def shutdown(self, how: int) -> None: ... @deprecated("Deprecated since Python 3.6. Use `SSLSocket.recv` method instead.") - def read(self, len: int = 1024, buffer: bytearray | None = None) -> bytes: ... + def read(self, len: int = 1024, buffer: WriteableBuffer | None = None) -> bytes: ... @deprecated("Deprecated since Python 3.6. Use `SSLSocket.send` method instead.") def write(self, data: ReadableBuffer) -> int: ... def do_handshake(self, block: bool = False) -> None: ... # block is undocumented + @overload def getpeercert(self, binary_form: Literal[False] = False) -> _PeerCertRetDictType | None: ... @overload def getpeercert(self, binary_form: Literal[True]) -> bytes | None: ... @overload def getpeercert(self, binary_form: bool) -> _PeerCertRetType: ... + def cipher(self) -> tuple[str, str, int] | None: ... def shared_ciphers(self) -> list[tuple[str, str, int]] | None: ... def compression(self) -> str | None: ... + if sys.version_info >= (3, 15): + def group(self) -> str | None: ... + def client_sigalg(self) -> str | None: ... + def server_sigalg(self) -> str | None: ... + def get_channel_binding(self, cb_type: str = "tls-unique") -> bytes | None: ... def selected_alpn_protocol(self) -> str | None: ... - if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10. Use ALPN instead.") - def selected_npn_protocol(self) -> str | None: ... - else: - def selected_npn_protocol(self) -> str | None: ... - + @deprecated("Deprecated since Python 3.10. Use ALPN instead.") + def selected_npn_protocol(self) -> str | None: ... def accept(self) -> tuple[SSLSocket, socket._RetAddress]: ... def unwrap(self) -> socket.socket: ... def version(self) -> str | None: ... @@ -346,19 +348,9 @@ if sys.version_info < (3, 12): def cert_time_to_seconds(cert_time: str) -> int: ... def DER_cert_to_PEM_cert(der_cert_bytes: ReadableBuffer) -> str: ... def PEM_cert_to_DER_cert(pem_cert_string: str) -> bytes: ... - -if sys.version_info >= (3, 10): - def get_server_certificate( - addr: tuple[str, int], - ssl_version: int = _SSLMethod.PROTOCOL_TLS_CLIENT, - ca_certs: str | None = None, - timeout: float = ..., - ) -> str: ... - -else: - def get_server_certificate( - addr: tuple[str, int], ssl_version: int = _SSLMethod.PROTOCOL_TLS_CLIENT, ca_certs: str | None = None - ) -> str: ... +def get_server_certificate( + addr: tuple[str, int], ssl_version: int = _SSLMethod.PROTOCOL_TLS_CLIENT, ca_certs: str | None = None, timeout: float = ... +) -> str: ... class TLSVersion(enum.IntEnum): MINIMUM_SUPPORTED = -2 @@ -385,16 +377,13 @@ class SSLContext(_SSLContext): sslsocket_class: type[SSLSocket] keylog_filename: str post_handshake_auth: bool - if sys.version_info >= (3, 10): - security_level: int - if sys.version_info >= (3, 10): - @overload - def __new__(cls, protocol: int, *args: Any, **kwargs: Any) -> Self: ... - @overload - @deprecated("Deprecated since Python 3.10. Use a specific version of the SSL protocol.") - def __new__(cls, protocol: None = None, *args: Any, **kwargs: Any) -> Self: ... - else: - def __new__(cls, protocol: int = ..., *args: Any, **kwargs: Any) -> Self: ... + security_level: int + + @overload + def __new__(cls, protocol: int, *args: Any, **kwargs: Any) -> Self: ... + @overload + @deprecated("Deprecated since Python 3.10. Use a specific version of the SSL protocol.") + def __new__(cls, protocol: None = None, *args: Any, **kwargs: Any) -> Self: ... def load_default_certs(self, purpose: Purpose = Purpose.SERVER_AUTH) -> None: ... def load_verify_locations( @@ -403,22 +392,27 @@ class SSLContext(_SSLContext): capath: StrOrBytesPath | None = None, cadata: str | ReadableBuffer | None = None, ) -> None: ... + @overload def get_ca_certs(self, binary_form: Literal[False] = False) -> list[_PeerCertRetDictType]: ... @overload def get_ca_certs(self, binary_form: Literal[True]) -> list[bytes]: ... @overload def get_ca_certs(self, binary_form: bool = False) -> Any: ... + def get_ciphers(self) -> list[_Cipher]: ... + if sys.version_info >= (3, 15): + def set_ciphersuites(self, ciphersuites: str, /) -> None: ... + def get_groups(self, /, *, include_aliases: bool = False) -> list[str]: ... + def set_groups(self, grouplist: str, /) -> None: ... + def set_client_sigalgs(self, sigalgs: str, /) -> None: ... + def set_server_sigalgs(self, sigalgs: str, /) -> None: ... + def set_default_verify_paths(self) -> None: ... def set_ciphers(self, cipherlist: str, /) -> None: ... def set_alpn_protocols(self, alpn_protocols: Iterable[str]) -> None: ... - if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10. Use ALPN instead.") - def set_npn_protocols(self, npn_protocols: Iterable[str]) -> None: ... - else: - def set_npn_protocols(self, npn_protocols: Iterable[str]) -> None: ... - + @deprecated("Deprecated since Python 3.10. Use ALPN instead.") + def set_npn_protocols(self, npn_protocols: Iterable[str]) -> None: ... def set_servername_callback(self, server_name_callback: _SrvnmeCbType | None) -> None: ... def load_dh_params(self, path: str, /) -> None: ... def set_ecdh_curve(self, name: str, /) -> None: ... @@ -447,34 +441,18 @@ def create_default_context( capath: StrOrBytesPath | None = None, cadata: str | ReadableBuffer | None = None, ) -> SSLContext: ... - -if sys.version_info >= (3, 10): - def _create_unverified_context( - protocol: int | None = None, - *, - cert_reqs: int = VerifyMode.CERT_NONE, - check_hostname: bool = False, - purpose: Purpose = Purpose.SERVER_AUTH, - certfile: StrOrBytesPath | None = None, - keyfile: StrOrBytesPath | None = None, - cafile: StrOrBytesPath | None = None, - capath: StrOrBytesPath | None = None, - cadata: str | ReadableBuffer | None = None, - ) -> SSLContext: ... - -else: - def _create_unverified_context( - protocol: int = ..., - *, - cert_reqs: int = VerifyMode.CERT_NONE, - check_hostname: bool = False, - purpose: Purpose = Purpose.SERVER_AUTH, - certfile: StrOrBytesPath | None = None, - keyfile: StrOrBytesPath | None = None, - cafile: StrOrBytesPath | None = None, - capath: StrOrBytesPath | None = None, - cadata: str | ReadableBuffer | None = None, - ) -> SSLContext: ... +def _create_unverified_context( + protocol: int | None = None, + *, + cert_reqs: int = VerifyMode.CERT_NONE, + check_hostname: bool = False, + purpose: Purpose = Purpose.SERVER_AUTH, + certfile: StrOrBytesPath | None = None, + keyfile: StrOrBytesPath | None = None, + cafile: StrOrBytesPath | None = None, + capath: StrOrBytesPath | None = None, + cadata: str | ReadableBuffer | None = None, +) -> SSLContext: ... _create_default_https_context = create_default_context @@ -488,24 +466,27 @@ class SSLObject: @property def session_reused(self) -> bool: ... def __init__(self, *args: Any, **kwargs: Any) -> None: ... - def read(self, len: int = 1024, buffer: bytearray | None = None) -> bytes: ... + def read(self, len: int = 1024, buffer: WriteableBuffer | None = None) -> bytes: ... def write(self, data: ReadableBuffer) -> int: ... + @overload def getpeercert(self, binary_form: Literal[False] = False) -> _PeerCertRetDictType | None: ... @overload def getpeercert(self, binary_form: Literal[True]) -> bytes | None: ... @overload def getpeercert(self, binary_form: bool) -> _PeerCertRetType: ... - def selected_alpn_protocol(self) -> str | None: ... - if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10. Use ALPN instead.") - def selected_npn_protocol(self) -> str | None: ... - else: - def selected_npn_protocol(self) -> str | None: ... + def selected_alpn_protocol(self) -> str | None: ... + @deprecated("Deprecated since Python 3.10. Use ALPN instead.") + def selected_npn_protocol(self) -> str | None: ... def cipher(self) -> tuple[str, str, int] | None: ... def shared_ciphers(self) -> list[tuple[str, str, int]] | None: ... def compression(self) -> str | None: ... + if sys.version_info >= (3, 15): + def group(self) -> str | None: ... + def client_sigalg(self) -> str | None: ... + def server_sigalg(self) -> str | None: ... + def pending(self) -> int: ... def do_handshake(self) -> None: ... def unwrap(self) -> None: ... @@ -539,6 +520,9 @@ SSL_ERROR_ZERO_RETURN: Final = SSLErrorNumber.SSL_ERROR_ZERO_RETURN # undocumen def get_protocol_name(protocol_code: int) -> str: ... +if sys.version_info >= (3, 15): + def get_sigalgs() -> list[str]: ... + PEM_FOOTER: Final[str] PEM_HEADER: Final[str] SOCK_STREAM: Final = socket.SOCK_STREAM diff --git a/mypy/typeshed/stdlib/stat.pyi b/mypy/typeshed/stdlib/stat.pyi index 6c26080e06653..155d765d2b160 100644 --- a/mypy/typeshed/stdlib/stat.pyi +++ b/mypy/typeshed/stdlib/stat.pyi @@ -112,3 +112,15 @@ FILE_ATTRIBUTE_VIRTUAL: Final = 65536 if sys.version_info >= (3, 13): # https://github.com/python/cpython/issues/114081#issuecomment-2119017790 SF_RESTRICTED: Final = 0x00080000 + +if sys.version_info >= (3, 15): + STATX_ATTR_COMPRESSED: Final = 0x00000004 + STATX_ATTR_IMMUTABLE: Final = 0x00000010 + STATX_ATTR_APPEND: Final = 0x00000020 + STATX_ATTR_NODUMP: Final = 0x00000040 + STATX_ATTR_ENCRYPTED: Final = 0x00000800 + STATX_ATTR_AUTOMOUNT: Final = 0x00001000 + STATX_ATTR_MOUNT_ROOT: Final = 0x00002000 + STATX_ATTR_VERITY: Final = 0x00100000 + STATX_ATTR_DAX: Final = 0x00200000 + STATX_ATTR_WRITE_ATOMIC: Final = 0x00400000 diff --git a/mypy/typeshed/stdlib/statistics.pyi b/mypy/typeshed/stdlib/statistics.pyi index d9f282b99b662..8cae237f7e0b9 100644 --- a/mypy/typeshed/stdlib/statistics.pyi +++ b/mypy/typeshed/stdlib/statistics.pyi @@ -3,13 +3,16 @@ from _typeshed import SupportsRichComparisonT from collections.abc import Callable, Hashable, Iterable, Sequence, Sized from decimal import Decimal from fractions import Fraction -from typing import Literal, NamedTuple, Protocol, SupportsFloat, SupportsIndex, TypeVar, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Literal, NamedTuple, Protocol, SupportsFloat, SupportsIndex, TypeAlias, TypeVar, type_check_only +from typing_extensions import Self __all__ = [ "StatisticsError", + "covariance", + "correlation", "fmean", "geometric_mean", + "linear_regression", "mean", "harmonic_mean", "pstdev", @@ -26,8 +29,6 @@ __all__ = [ "quantiles", ] -if sys.version_info >= (3, 10): - __all__ += ["covariance", "correlation", "linear_regression"] if sys.version_info >= (3, 13): __all__ += ["kde", "kde_random"] @@ -57,13 +58,7 @@ else: def geometric_mean(data: Iterable[SupportsFloat]) -> float: ... def mean(data: Iterable[_NumberT]) -> _NumberT: ... - -if sys.version_info >= (3, 10): - def harmonic_mean(data: Iterable[_NumberT], weights: Iterable[_Number] | None = None) -> _NumberT: ... - -else: - def harmonic_mean(data: Iterable[_NumberT]) -> _NumberT: ... - +def harmonic_mean(data: Iterable[_NumberT], weights: Iterable[_Number] | None = None) -> _NumberT: ... def median(data: Iterable[_NumberT]) -> _NumberT: ... def median_low(data: Iterable[SupportsRichComparisonT]) -> SupportsRichComparisonT: ... def median_high(data: Iterable[SupportsRichComparisonT]) -> SupportsRichComparisonT: ... @@ -123,22 +118,21 @@ if sys.version_info >= (3, 12): x: Sequence[_Number], y: Sequence[_Number], /, *, method: Literal["linear", "ranked"] = "linear" ) -> float: ... -elif sys.version_info >= (3, 10): +else: def correlation(x: Sequence[_Number], y: Sequence[_Number], /) -> float: ... -if sys.version_info >= (3, 10): - def covariance(x: Sequence[_Number], y: Sequence[_Number], /) -> float: ... +def covariance(x: Sequence[_Number], y: Sequence[_Number], /) -> float: ... - class LinearRegression(NamedTuple): - slope: float - intercept: float +class LinearRegression(NamedTuple): + slope: float + intercept: float if sys.version_info >= (3, 11): def linear_regression( regressor: _SizedIterable[_Number], dependent_variable: _SizedIterable[_Number], /, *, proportional: bool = False ) -> LinearRegression: ... -elif sys.version_info >= (3, 10): +else: def linear_regression( regressor: _SizedIterable[_Number], dependent_variable: _SizedIterable[_Number], / ) -> LinearRegression: ... diff --git a/mypy/typeshed/stdlib/string/__init__.pyi b/mypy/typeshed/stdlib/string/__init__.pyi index c8b32a98e26d7..df70cdc6b21c8 100644 --- a/mypy/typeshed/stdlib/string/__init__.pyi +++ b/mypy/typeshed/stdlib/string/__init__.pyi @@ -54,12 +54,14 @@ class Formatter: def format(self, format_string: LiteralString, /, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ... @overload def format(self, format_string: str, /, *args: Any, **kwargs: Any) -> str: ... + @overload def vformat( self, format_string: LiteralString, args: Sequence[LiteralString], kwargs: Mapping[LiteralString, LiteralString] ) -> LiteralString: ... @overload def vformat(self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> str: ... + def _vformat( # undocumented self, format_string: str, diff --git a/mypy/typeshed/stdlib/string/templatelib.pyi b/mypy/typeshed/stdlib/string/templatelib.pyi index 9906d31c63915..ee901cdc43b8e 100644 --- a/mypy/typeshed/stdlib/string/templatelib.pyi +++ b/mypy/typeshed/stdlib/string/templatelib.pyi @@ -1,24 +1,24 @@ from collections.abc import Iterator from types import GenericAlias -from typing import Any, Literal, TypeVar, final, overload +from typing import Any, Generic, Literal, TypeVar, final, overload _T = TypeVar("_T") @final class Template: # TODO: consider making `Template` generic on `TypeVarTuple` strings: tuple[str, ...] - interpolations: tuple[Interpolation, ...] + interpolations: tuple[Interpolation[Any], ...] - def __new__(cls, *args: str | Interpolation) -> Template: ... - def __iter__(self) -> Iterator[str | Interpolation]: ... + def __new__(cls, *args: str | Interpolation[Any]) -> Template: ... + def __iter__(self) -> Iterator[str | Interpolation[Any]]: ... def __add__(self, other: Template, /) -> Template: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @property def values(self) -> tuple[Any, ...]: ... # Tuple of interpolation values, which can have any type @final -class Interpolation: - value: Any # TODO: consider making `Interpolation` generic in runtime +class Interpolation(Generic[_T]): + value: _T expression: str conversion: Literal["a", "r", "s"] | None format_spec: str @@ -26,8 +26,8 @@ class Interpolation: __match_args__ = ("value", "expression", "conversion", "format_spec") def __new__( - cls, value: Any, expression: str = "", conversion: Literal["a", "r", "s"] | None = None, format_spec: str = "" - ) -> Interpolation: ... + cls, value: _T, expression: str = "", conversion: Literal["a", "r", "s"] | None = None, format_spec: str = "" + ) -> Interpolation[_T]: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @overload diff --git a/mypy/typeshed/stdlib/subprocess.pyi b/mypy/typeshed/stdlib/subprocess.pyi index f6d7b88193ec3..c191f0e35de9b 100644 --- a/mypy/typeshed/stdlib/subprocess.pyi +++ b/mypy/typeshed/stdlib/subprocess.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import MaybeNone, ReadableBuffer, StrOrBytesPath from collections.abc import Callable, Collection, Iterable, Mapping, Sequence from types import GenericAlias, TracebackType -from typing import IO, Any, AnyStr, Final, Generic, Literal, TypeVar, overload -from typing_extensions import Self, TypeAlias +from typing import IO, Any, AnyStr, Final, Generic, Literal, TypeAlias, TypeVar, overload +from typing_extensions import Self __all__ = [ "Popen", @@ -88,7 +88,7 @@ class CompletedProcess(Generic[_T]): if sys.version_info >= (3, 11): # 3.11 adds "process_group" argument - @overload + @overload # text is True def run( args: _CMD, bufsize: int = -1, @@ -101,7 +101,7 @@ if sys.version_info >= (3, 11): shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: bool | None = None, + universal_newlines: Literal[True] | None = None, startupinfo: Any = None, creationflags: int = 0, restore_signals: bool = True, @@ -122,7 +122,7 @@ if sys.version_info >= (3, 11): pipesize: int = -1, process_group: int | None = None, ) -> CompletedProcess[str]: ... - @overload + @overload # encoding is str def run( args: _CMD, bufsize: int = -1, @@ -156,7 +156,7 @@ if sys.version_info >= (3, 11): pipesize: int = -1, process_group: int | None = None, ) -> CompletedProcess[str]: ... - @overload + @overload # errors is str def run( args: _CMD, bufsize: int = -1, @@ -190,7 +190,7 @@ if sys.version_info >= (3, 11): pipesize: int = -1, process_group: int | None = None, ) -> CompletedProcess[str]: ... - @overload + @overload # universal_newlines is True def run( args: _CMD, bufsize: int = -1, @@ -216,7 +216,7 @@ if sys.version_info >= (3, 11): encoding: str | None = None, errors: str | None = None, input: str | None = None, - text: bool | None = None, + text: Literal[True] | None = None, timeout: float | None = None, user: str | int | None = None, group: str | int | None = None, @@ -225,7 +225,7 @@ if sys.version_info >= (3, 11): pipesize: int = -1, process_group: int | None = None, ) -> CompletedProcess[str]: ... - @overload + @overload # universal_newlines and text are False, None, or missing def run( args: _CMD, bufsize: int = -1, @@ -259,7 +259,7 @@ if sys.version_info >= (3, 11): pipesize: int = -1, process_group: int | None = None, ) -> CompletedProcess[bytes]: ... - @overload + @overload # fallback def run( args: _CMD, bufsize: int = -1, @@ -293,10 +293,9 @@ if sys.version_info >= (3, 11): pipesize: int = -1, process_group: int | None = None, ) -> CompletedProcess[Any]: ... - -elif sys.version_info >= (3, 10): +else: # 3.10 adds "pipesize" argument - @overload + @overload # text is True def run( args: _CMD, bufsize: int = -1, @@ -309,7 +308,7 @@ elif sys.version_info >= (3, 10): shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: bool | None = None, + universal_newlines: Literal[True] | None = None, startupinfo: Any = None, creationflags: int = 0, restore_signals: bool = True, @@ -329,7 +328,7 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> CompletedProcess[str]: ... - @overload + @overload # encoding is str def run( args: _CMD, bufsize: int = -1, @@ -362,7 +361,7 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> CompletedProcess[str]: ... - @overload + @overload # errors is str def run( args: _CMD, bufsize: int = -1, @@ -395,7 +394,7 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> CompletedProcess[str]: ... - @overload + @overload # universal_newlines is True def run( args: _CMD, bufsize: int = -1, @@ -421,7 +420,7 @@ elif sys.version_info >= (3, 10): encoding: str | None = None, errors: str | None = None, input: str | None = None, - text: bool | None = None, + text: Literal[True] | None = None, timeout: float | None = None, user: str | int | None = None, group: str | int | None = None, @@ -429,7 +428,7 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> CompletedProcess[str]: ... - @overload + @overload # universal_newlines and text are False, None, or missing def run( args: _CMD, bufsize: int = -1, @@ -462,7 +461,7 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> CompletedProcess[bytes]: ... - @overload + @overload # fallback def run( args: _CMD, bufsize: int = -1, @@ -495,458 +494,16 @@ elif sys.version_info >= (3, 10): umask: int = -1, pipesize: int = -1, ) -> CompletedProcess[Any]: ... - -else: - @overload - def run( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - capture_output: bool = False, - check: bool = False, - encoding: str | None = None, - errors: str | None = None, - input: str | None = None, - text: Literal[True], - timeout: float | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> CompletedProcess[str]: ... - @overload - def run( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - capture_output: bool = False, - check: bool = False, - encoding: str, - errors: str | None = None, - input: str | None = None, - text: bool | None = None, - timeout: float | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> CompletedProcess[str]: ... - @overload - def run( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - capture_output: bool = False, - check: bool = False, - encoding: str | None = None, - errors: str, - input: str | None = None, - text: bool | None = None, - timeout: float | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> CompletedProcess[str]: ... - @overload - def run( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - *, - universal_newlines: Literal[True], - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - # where the *real* keyword only args start - capture_output: bool = False, - check: bool = False, - encoding: str | None = None, - errors: str | None = None, - input: str | None = None, - text: bool | None = None, - timeout: float | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> CompletedProcess[str]: ... - @overload - def run( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: Literal[False] | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - capture_output: bool = False, - check: bool = False, - encoding: None = None, - errors: None = None, - input: ReadableBuffer | None = None, - text: Literal[False] | None = None, - timeout: float | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> CompletedProcess[bytes]: ... - @overload - def run( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - capture_output: bool = False, - check: bool = False, - encoding: str | None = None, - errors: str | None = None, - input: _InputString | None = None, - text: bool | None = None, - timeout: float | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> CompletedProcess[Any]: ... - -# Same args as Popen.__init__ -if sys.version_info >= (3, 11): - # 3.11 adds "process_group" argument - def call( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - encoding: str | None = None, - timeout: float | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - pipesize: int = -1, - process_group: int | None = None, - ) -> int: ... - -elif sys.version_info >= (3, 10): - # 3.10 adds "pipesize" argument - def call( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - encoding: str | None = None, - timeout: float | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - pipesize: int = -1, - ) -> int: ... - -else: - def call( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - encoding: str | None = None, - timeout: float | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> int: ... - -# Same args as Popen.__init__ -if sys.version_info >= (3, 11): - # 3.11 adds "process_group" argument - def check_call( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - timeout: float | None = None, - *, - encoding: str | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - pipesize: int = -1, - process_group: int | None = None, - ) -> int: ... - -elif sys.version_info >= (3, 10): - # 3.10 adds "pipesize" argument - def check_call( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - timeout: float | None = None, - *, - encoding: str | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - pipesize: int = -1, - ) -> int: ... - -else: - def check_call( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stdout: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - timeout: float | None = None, - *, - encoding: str | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> int: ... - -if sys.version_info >= (3, 11): - # 3.11 adds "process_group" argument - @overload - def check_output( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - timeout: float | None = None, - input: _InputString | None = None, - encoding: str | None = None, - errors: str | None = None, - text: Literal[True], - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - pipesize: int = -1, - process_group: int | None = None, - ) -> str: ... - @overload - def check_output( - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE = None, - stderr: _FILE = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - timeout: float | None = None, - input: _InputString | None = None, - encoding: str, - errors: str | None = None, - text: bool | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - pipesize: int = -1, - process_group: int | None = None, - ) -> str: ... - @overload - def check_output( + +# Same args as Popen.__init__ +if sys.version_info >= (3, 11): + # 3.11 adds "process_group" argument + def call( args: _CMD, bufsize: int = -1, executable: StrOrBytesPath | None = None, stdin: _FILE = None, + stdout: _FILE = None, stderr: _FILE = None, preexec_fn: Callable[[], object] | None = None, close_fds: bool = True, @@ -960,10 +517,8 @@ if sys.version_info >= (3, 11): start_new_session: bool = False, pass_fds: Collection[int] = (), *, - timeout: float | None = None, - input: _InputString | None = None, encoding: str | None = None, - errors: str, + timeout: float | None = None, text: bool | None = None, user: str | int | None = None, group: str | int | None = None, @@ -971,76 +526,80 @@ if sys.version_info >= (3, 11): umask: int = -1, pipesize: int = -1, process_group: int | None = None, - ) -> str: ... - @overload - def check_output( + ) -> int: ... + +else: + # 3.10 adds "pipesize" argument + def call( args: _CMD, bufsize: int = -1, executable: StrOrBytesPath | None = None, stdin: _FILE = None, + stdout: _FILE = None, stderr: _FILE = None, preexec_fn: Callable[[], object] | None = None, close_fds: bool = True, shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - *, - universal_newlines: Literal[True], + universal_newlines: bool | None = None, startupinfo: Any = None, creationflags: int = 0, restore_signals: bool = True, start_new_session: bool = False, pass_fds: Collection[int] = (), - # where the real keyword only ones start - timeout: float | None = None, - input: _InputString | None = None, + *, encoding: str | None = None, - errors: str | None = None, + timeout: float | None = None, text: bool | None = None, user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, - process_group: int | None = None, - ) -> str: ... - @overload - def check_output( + ) -> int: ... + +# Same args as Popen.__init__ +if sys.version_info >= (3, 11): + # 3.11 adds "process_group" argument + def check_call( args: _CMD, bufsize: int = -1, executable: StrOrBytesPath | None = None, stdin: _FILE = None, + stdout: _FILE = None, stderr: _FILE = None, preexec_fn: Callable[[], object] | None = None, close_fds: bool = True, shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: Literal[False] | None = None, + universal_newlines: bool | None = None, startupinfo: Any = None, creationflags: int = 0, restore_signals: bool = True, start_new_session: bool = False, pass_fds: Collection[int] = (), - *, timeout: float | None = None, - input: _InputString | None = None, - encoding: None = None, - errors: None = None, - text: Literal[False] | None = None, + *, + encoding: str | None = None, + text: bool | None = None, user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, process_group: int | None = None, - ) -> bytes: ... - @overload - def check_output( + ) -> int: ... + +else: + # 3.10 adds "pipesize" argument + def check_call( args: _CMD, bufsize: int = -1, executable: StrOrBytesPath | None = None, stdin: _FILE = None, + stdout: _FILE = None, stderr: _FILE = None, preexec_fn: Callable[[], object] | None = None, close_fds: bool = True, @@ -1053,23 +612,20 @@ if sys.version_info >= (3, 11): restore_signals: bool = True, start_new_session: bool = False, pass_fds: Collection[int] = (), - *, timeout: float | None = None, - input: _InputString | None = None, + *, encoding: str | None = None, - errors: str | None = None, text: bool | None = None, user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, - process_group: int | None = None, - ) -> Any: ... # morally: -> str | bytes + ) -> int: ... -elif sys.version_info >= (3, 10): - # 3.10 adds "pipesize" argument - @overload +if sys.version_info >= (3, 11): + # 3.11 adds "process_group" argument + @overload # text is True def check_output( args: _CMD, bufsize: int = -1, @@ -1081,7 +637,7 @@ elif sys.version_info >= (3, 10): shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: bool | None = None, + universal_newlines: Literal[True] | None = None, startupinfo: Any = None, creationflags: int = 0, restore_signals: bool = True, @@ -1098,8 +654,9 @@ elif sys.version_info >= (3, 10): extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, + process_group: int | None = None, ) -> str: ... - @overload + @overload # encoding is str def check_output( args: _CMD, bufsize: int = -1, @@ -1128,8 +685,9 @@ elif sys.version_info >= (3, 10): extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, + process_group: int | None = None, ) -> str: ... - @overload + @overload # errors is str def check_output( args: _CMD, bufsize: int = -1, @@ -1158,8 +716,9 @@ elif sys.version_info >= (3, 10): extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, + process_group: int | None = None, ) -> str: ... - @overload + @overload # universal_newlines is True def check_output( args: _CMD, bufsize: int = -1, @@ -1183,14 +742,15 @@ elif sys.version_info >= (3, 10): input: _InputString | None = None, encoding: str | None = None, errors: str | None = None, - text: bool | None = None, + text: Literal[True] | None = None, user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, + process_group: int | None = None, ) -> str: ... - @overload + @overload # universal_newlines and text are False, None, or missing def check_output( args: _CMD, bufsize: int = -1, @@ -1219,8 +779,9 @@ elif sys.version_info >= (3, 10): extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, + process_group: int | None = None, ) -> bytes: ... - @overload + @overload # fallback def check_output( args: _CMD, bufsize: int = -1, @@ -1249,10 +810,11 @@ elif sys.version_info >= (3, 10): extra_groups: Iterable[str | int] | None = None, umask: int = -1, pipesize: int = -1, + process_group: int | None = None, ) -> Any: ... # morally: -> str | bytes - else: - @overload + # 3.10 adds "pipesize" argument + @overload # text is True def check_output( args: _CMD, bufsize: int = -1, @@ -1264,7 +826,7 @@ else: shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: bool | None = None, + universal_newlines: Literal[True] | None = None, startupinfo: Any = None, creationflags: int = 0, restore_signals: bool = True, @@ -1280,8 +842,9 @@ else: group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, + pipesize: int = -1, ) -> str: ... - @overload + @overload # encoding is str def check_output( args: _CMD, bufsize: int = -1, @@ -1309,8 +872,9 @@ else: group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, + pipesize: int = -1, ) -> str: ... - @overload + @overload # errors is str def check_output( args: _CMD, bufsize: int = -1, @@ -1338,8 +902,9 @@ else: group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, + pipesize: int = -1, ) -> str: ... - @overload + @overload # universal_newlines is True def check_output( args: _CMD, bufsize: int = -1, @@ -1363,13 +928,14 @@ else: input: _InputString | None = None, encoding: str | None = None, errors: str | None = None, - text: bool | None = None, + text: Literal[True] | None = None, user: str | int | None = None, group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, + pipesize: int = -1, ) -> str: ... - @overload + @overload # universal_newlines and text are False, None, or missing def check_output( args: _CMD, bufsize: int = -1, @@ -1397,8 +963,9 @@ else: group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, + pipesize: int = -1, ) -> bytes: ... - @overload + @overload # fallback def check_output( args: _CMD, bufsize: int = -1, @@ -1426,6 +993,7 @@ else: group: str | int | None = None, extra_groups: Iterable[str | int] | None = None, umask: int = -1, + pipesize: int = -1, ) -> Any: ... # morally: -> str | bytes PIPE: Final[int] @@ -1462,16 +1030,16 @@ class CalledProcessError(SubprocessError): class Popen(Generic[AnyStr]): args: _CMD - stdin: IO[AnyStr] | None - stdout: IO[AnyStr] | None - stderr: IO[AnyStr] | None + stdin: IO[Any] | None + stdout: IO[Any] | None + stderr: IO[Any] | None pid: int returncode: int | MaybeNone universal_newlines: bool if sys.version_info >= (3, 11): # process_group is added in 3.11 - @overload + @overload # encoding is str def __init__( self: Popen[str], args: _CMD, @@ -1502,7 +1070,7 @@ class Popen(Generic[AnyStr]): pipesize: int = -1, process_group: int | None = None, ) -> None: ... - @overload + @overload # errors is str def __init__( self: Popen[str], args: _CMD, @@ -1533,7 +1101,7 @@ class Popen(Generic[AnyStr]): pipesize: int = -1, process_group: int | None = None, ) -> None: ... - @overload + @overload # universal_newlines is True def __init__( self: Popen[str], args: _CMD, @@ -1565,7 +1133,7 @@ class Popen(Generic[AnyStr]): pipesize: int = -1, process_group: int | None = None, ) -> None: ... - @overload + @overload # text is True def __init__( self: Popen[str], args: _CMD, @@ -1579,7 +1147,7 @@ class Popen(Generic[AnyStr]): shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: bool | None = None, + universal_newlines: Literal[True] | None = None, startupinfo: Any | None = None, creationflags: int = 0, restore_signals: bool = True, @@ -1596,7 +1164,7 @@ class Popen(Generic[AnyStr]): pipesize: int = -1, process_group: int | None = None, ) -> None: ... - @overload + @overload # universal_newlines and text are False, None, or missing def __init__( self: Popen[bytes], args: _CMD, @@ -1627,7 +1195,7 @@ class Popen(Generic[AnyStr]): pipesize: int = -1, process_group: int | None = None, ) -> None: ... - @overload + @overload # fallback def __init__( self: Popen[Any], args: _CMD, @@ -1658,9 +1226,9 @@ class Popen(Generic[AnyStr]): pipesize: int = -1, process_group: int | None = None, ) -> None: ... - elif sys.version_info >= (3, 10): + else: # pipesize is added in 3.10 - @overload + @overload # encoding is str def __init__( self: Popen[str], args: _CMD, @@ -1690,7 +1258,7 @@ class Popen(Generic[AnyStr]): umask: int = -1, pipesize: int = -1, ) -> None: ... - @overload + @overload # errors is str def __init__( self: Popen[str], args: _CMD, @@ -1720,7 +1288,7 @@ class Popen(Generic[AnyStr]): umask: int = -1, pipesize: int = -1, ) -> None: ... - @overload + @overload # universal_newlines is True def __init__( self: Popen[str], args: _CMD, @@ -1751,7 +1319,7 @@ class Popen(Generic[AnyStr]): umask: int = -1, pipesize: int = -1, ) -> None: ... - @overload + @overload # text is True def __init__( self: Popen[str], args: _CMD, @@ -1765,7 +1333,7 @@ class Popen(Generic[AnyStr]): shell: bool = False, cwd: StrOrBytesPath | None = None, env: _ENV | None = None, - universal_newlines: bool | None = None, + universal_newlines: Literal[True] | None = None, startupinfo: Any | None = None, creationflags: int = 0, restore_signals: bool = True, @@ -1781,7 +1349,7 @@ class Popen(Generic[AnyStr]): umask: int = -1, pipesize: int = -1, ) -> None: ... - @overload + @overload # universal_newlines and text are False, None, or missing def __init__( self: Popen[bytes], args: _CMD, @@ -1811,7 +1379,7 @@ class Popen(Generic[AnyStr]): umask: int = -1, pipesize: int = -1, ) -> None: ... - @overload + @overload # fallback def __init__( self: Popen[Any], args: _CMD, @@ -1841,182 +1409,6 @@ class Popen(Generic[AnyStr]): umask: int = -1, pipesize: int = -1, ) -> None: ... - else: - @overload - def __init__( - self: Popen[str], - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE | None = None, - stdout: _FILE | None = None, - stderr: _FILE | None = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - text: bool | None = None, - encoding: str, - errors: str | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> None: ... - @overload - def __init__( - self: Popen[str], - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE | None = None, - stdout: _FILE | None = None, - stderr: _FILE | None = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - text: bool | None = None, - encoding: str | None = None, - errors: str, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> None: ... - @overload - def __init__( - self: Popen[str], - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE | None = None, - stdout: _FILE | None = None, - stderr: _FILE | None = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - *, - universal_newlines: Literal[True], - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - # where the *real* keyword only args start - text: bool | None = None, - encoding: str | None = None, - errors: str | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> None: ... - @overload - def __init__( - self: Popen[str], - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE | None = None, - stdout: _FILE | None = None, - stderr: _FILE | None = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - text: Literal[True], - encoding: str | None = None, - errors: str | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> None: ... - @overload - def __init__( - self: Popen[bytes], - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE | None = None, - stdout: _FILE | None = None, - stderr: _FILE | None = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: Literal[False] | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - text: Literal[False] | None = None, - encoding: None = None, - errors: None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> None: ... - @overload - def __init__( - self: Popen[Any], - args: _CMD, - bufsize: int = -1, - executable: StrOrBytesPath | None = None, - stdin: _FILE | None = None, - stdout: _FILE | None = None, - stderr: _FILE | None = None, - preexec_fn: Callable[[], object] | None = None, - close_fds: bool = True, - shell: bool = False, - cwd: StrOrBytesPath | None = None, - env: _ENV | None = None, - universal_newlines: bool | None = None, - startupinfo: Any | None = None, - creationflags: int = 0, - restore_signals: bool = True, - start_new_session: bool = False, - pass_fds: Collection[int] = (), - *, - text: bool | None = None, - encoding: str | None = None, - errors: str | None = None, - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, - ) -> None: ... def poll(self) -> int | None: ... def wait(self, timeout: float | None = None) -> int: ... diff --git a/mypy/typeshed/stdlib/sunau.pyi b/mypy/typeshed/stdlib/sunau.pyi index f83a0a4c520e7..1f18c041465ef 100644 --- a/mypy/typeshed/stdlib/sunau.pyi +++ b/mypy/typeshed/stdlib/sunau.pyi @@ -1,6 +1,6 @@ from _typeshed import Unused -from typing import IO, Any, Final, Literal, NamedTuple, NoReturn, overload -from typing_extensions import Self, TypeAlias +from typing import IO, Any, Final, Literal, NamedTuple, NoReturn, TypeAlias, overload +from typing_extensions import Self _File: TypeAlias = str | IO[bytes] diff --git a/mypy/typeshed/stdlib/symbol.pyi b/mypy/typeshed/stdlib/symbol.pyi deleted file mode 100644 index 5344ce504c6c7..0000000000000 --- a/mypy/typeshed/stdlib/symbol.pyi +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Final - -single_input: Final[int] -file_input: Final[int] -eval_input: Final[int] -decorator: Final[int] -decorators: Final[int] -decorated: Final[int] -async_funcdef: Final[int] -funcdef: Final[int] -parameters: Final[int] -typedargslist: Final[int] -tfpdef: Final[int] -varargslist: Final[int] -vfpdef: Final[int] -stmt: Final[int] -simple_stmt: Final[int] -small_stmt: Final[int] -expr_stmt: Final[int] -annassign: Final[int] -testlist_star_expr: Final[int] -augassign: Final[int] -del_stmt: Final[int] -pass_stmt: Final[int] -flow_stmt: Final[int] -break_stmt: Final[int] -continue_stmt: Final[int] -return_stmt: Final[int] -yield_stmt: Final[int] -raise_stmt: Final[int] -import_stmt: Final[int] -import_name: Final[int] -import_from: Final[int] -import_as_name: Final[int] -dotted_as_name: Final[int] -import_as_names: Final[int] -dotted_as_names: Final[int] -dotted_name: Final[int] -global_stmt: Final[int] -nonlocal_stmt: Final[int] -assert_stmt: Final[int] -compound_stmt: Final[int] -async_stmt: Final[int] -if_stmt: Final[int] -while_stmt: Final[int] -for_stmt: Final[int] -try_stmt: Final[int] -with_stmt: Final[int] -with_item: Final[int] -except_clause: Final[int] -suite: Final[int] -test: Final[int] -test_nocond: Final[int] -lambdef: Final[int] -lambdef_nocond: Final[int] -or_test: Final[int] -and_test: Final[int] -not_test: Final[int] -comparison: Final[int] -comp_op: Final[int] -star_expr: Final[int] -expr: Final[int] -xor_expr: Final[int] -and_expr: Final[int] -shift_expr: Final[int] -arith_expr: Final[int] -term: Final[int] -factor: Final[int] -power: Final[int] -atom_expr: Final[int] -atom: Final[int] -testlist_comp: Final[int] -trailer: Final[int] -subscriptlist: Final[int] -subscript: Final[int] -sliceop: Final[int] -exprlist: Final[int] -testlist: Final[int] -dictorsetmaker: Final[int] -classdef: Final[int] -arglist: Final[int] -argument: Final[int] -comp_iter: Final[int] -comp_for: Final[int] -comp_if: Final[int] -encoding_decl: Final[int] -yield_expr: Final[int] -yield_arg: Final[int] -sync_comp_for: Final[int] -func_body_suite: Final[int] -func_type: Final[int] -func_type_input: Final[int] -namedexpr_test: Final[int] -typelist: Final[int] -sym_name: Final[dict[int, str]] diff --git a/mypy/typeshed/stdlib/symtable.pyi b/mypy/typeshed/stdlib/symtable.pyi index a727b878688ed..d41ef8419dec8 100644 --- a/mypy/typeshed/stdlib/symtable.pyi +++ b/mypy/typeshed/stdlib/symtable.pyi @@ -9,7 +9,11 @@ __all__ = ["symtable", "SymbolTable", "Class", "Function", "Symbol"] if sys.version_info >= (3, 13): __all__ += ["SymbolTableType"] -def symtable(code: str, filename: str, compile_type: str) -> SymbolTable: ... +if sys.version_info >= (3, 15): + def symtable(code: str, filename: str, compile_type: str, *, module: str | None = None) -> SymbolTable: ... + +else: + def symtable(code: str, filename: str, compile_type: str) -> SymbolTable: ... if sys.version_info >= (3, 13): from enum import StrEnum @@ -46,14 +50,14 @@ class Function(SymbolTable): def get_locals(self) -> tuple[str, ...]: ... def get_globals(self) -> tuple[str, ...]: ... def get_frees(self) -> tuple[str, ...]: ... + if sys.version_info >= (3, 15): + def get_cells(self) -> tuple[str, ...]: ... + def get_nonlocals(self) -> tuple[str, ...]: ... class Class(SymbolTable): - if sys.version_info >= (3, 14): - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") - def get_methods(self) -> tuple[str, ...]: ... - else: - def get_methods(self) -> tuple[str, ...]: ... + @deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") + def get_methods(self) -> tuple[str, ...]: ... class Symbol: def __init__( @@ -79,6 +83,8 @@ class Symbol: if sys.version_info >= (3, 14): def is_comp_iter(self) -> bool: ... def is_comp_cell(self) -> bool: ... + if sys.version_info >= (3, 15): + def is_cell(self) -> bool: ... def is_namespace(self) -> bool: ... def get_namespaces(self) -> Sequence[SymbolTable]: ... diff --git a/mypy/typeshed/stdlib/sys/__init__.pyi b/mypy/typeshed/stdlib/sys/__init__.pyi index 6abef85dfb7f1..80fb9348a6046 100644 --- a/mypy/typeshed/stdlib/sys/__init__.pyi +++ b/mypy/typeshed/stdlib/sys/__init__.pyi @@ -4,18 +4,30 @@ from _typeshed.importlib import MetaPathFinderProtocol, PathEntryFinderProtocol from builtins import object as _object from collections.abc import AsyncGenerator, Callable, Sequence from io import TextIOWrapper -from types import FrameType, ModuleType, TracebackType -from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeVar, final, overload, type_check_only -from typing_extensions import LiteralString, TypeAlias, deprecated +from types import FrameType, ModuleType, SimpleNamespace, TracebackType +from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeAlias, TypeVar, final, overload, type_check_only +from typing_extensions import LiteralString, deprecated _T = TypeVar("_T") +_LazyImportMode: TypeAlias = Literal["normal", "all", "none"] +_LazyImportFilter: TypeAlias = Callable[[str, str, tuple[str, ...] | None], bool] # see https://github.com/python/typeshed/issues/8513#issue-1333671093 for the rationale behind this alias _ExitCode: TypeAlias = str | int | None +if sys.version_info >= (3, 15): + @type_check_only + class _AbiInfo(SimpleNamespace): + pointer_bits: int + free_threaded: bool + debug: bool + byteorder: Literal["little", "big"] + # ----- sys variables ----- if sys.platform != "win32": abiflags: str +if sys.version_info >= (3, 15): + abi_info: _AbiInfo argv: list[str] base_exec_prefix: str base_prefix: str @@ -40,8 +52,9 @@ maxsize: int maxunicode: int meta_path: list[MetaPathFinderProtocol] modules: dict[str, ModuleType] -if sys.version_info >= (3, 10): - orig_argv: list[str] +if sys.version_info >= (3, 15): + lazy_modules: set[str] +orig_argv: list[str] path: list[str] path_hooks: list[Callable[[str], PathEntryFinderProtocol]] path_importer_cache: dict[str, PathEntryFinderProtocol | None] @@ -65,9 +78,7 @@ ps2: object stdin: TextIO | MaybeNone stdout: TextIO | MaybeNone stderr: TextIO | MaybeNone - -if sys.version_info >= (3, 10): - stdlib_module_names: frozenset[str] +stdlib_module_names: frozenset[str] __stdin__: Final[TextIOWrapper | None] # Contains the original value of stdin __stdout__: Final[TextIOWrapper | None] # Contains the original value of stdout @@ -124,7 +135,7 @@ class _flags(_UninstantiableStructseq, tuple[int, ...]): "safe_path", "int_max_str_digits", ) - elif sys.version_info >= (3, 10): + else: __match_args__: Final = ( "debug", "inspect", @@ -175,9 +186,8 @@ class _flags(_UninstantiableStructseq, tuple[int, ...]): def dev_mode(self) -> bool: ... @property def utf8_mode(self) -> int: ... - if sys.version_info >= (3, 10): - @property - def warn_default_encoding(self) -> int: ... + @property + def warn_default_encoding(self) -> int: ... if sys.version_info >= (3, 11): @property def safe_path(self) -> bool: ... @@ -203,20 +213,19 @@ float_info: _float_info @final @type_check_only class _float_info(structseq[float], tuple[float, int, int, float, int, int, int, int, float, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ( - "max", - "max_exp", - "max_10_exp", - "min", - "min_exp", - "min_10_exp", - "dig", - "mant_dig", - "epsilon", - "radix", - "rounds", - ) + __match_args__: Final = ( + "max", + "max_exp", + "max_10_exp", + "min", + "min_exp", + "min_10_exp", + "dig", + "mant_dig", + "epsilon", + "radix", + "rounds", + ) @property def max(self) -> float: ... # DBL_MAX @@ -247,8 +256,7 @@ hash_info: _hash_info @final @type_check_only class _hash_info(structseq[Any | int], tuple[int, int, int, int, int, str, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("width", "modulus", "inf", "nan", "imag", "algorithm", "hash_bits", "seed_bits", "cutoff") + __match_args__: Final = ("width", "modulus", "inf", "nan", "imag", "algorithm", "hash_bits", "seed_bits", "cutoff") @property def width(self) -> int: ... @@ -290,8 +298,7 @@ int_info: _int_info @final @type_check_only class _int_info(structseq[int], tuple[int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("bits_per_digit", "sizeof_digit", "default_max_str_digits", "str_digits_check_threshold") + __match_args__: Final = ("bits_per_digit", "sizeof_digit", "default_max_str_digits", "str_digits_check_threshold") @property def bits_per_digit(self) -> int: ... @@ -309,8 +316,7 @@ _ThreadInfoLock: TypeAlias = Literal["semaphore", "mutex+cond"] | None @final @type_check_only class _thread_info(_UninstantiableStructseq, tuple[_ThreadInfoName, _ThreadInfoLock, str | None]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("name", "lock", "version") + __match_args__: Final = ("name", "lock", "version") @property def name(self) -> _ThreadInfoName: ... @@ -326,8 +332,7 @@ _ReleaseLevel: TypeAlias = Literal["alpha", "beta", "candidate", "final"] @final @type_check_only class _version_info(_UninstantiableStructseq, tuple[int, int, int, _ReleaseLevel, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("major", "minor", "micro", "releaselevel", "serial") + __match_args__: Final = ("major", "minor", "micro", "releaselevel", "serial") @property def major(self) -> int: ... @@ -385,6 +390,11 @@ if sys.platform != "win32": def getfilesystemencoding() -> LiteralString: ... def getfilesystemencodeerrors() -> LiteralString: ... + +if sys.version_info >= (3, 15): + def get_lazy_imports() -> _LazyImportMode: ... + def get_lazy_imports_filter() -> _LazyImportFilter | None: ... + def getrefcount(object: Any, /) -> int: ... def getrecursionlimit() -> int: ... def getsizeof(obj: object, default: int = ...) -> int: ... @@ -467,8 +477,7 @@ _AsyncgenHook: TypeAlias = Callable[[AsyncGenerator[Any, Any]], None] | None @final @type_check_only class _asyncgen_hooks(structseq[_AsyncgenHook], tuple[_AsyncgenHook, _AsyncgenHook]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("firstiter", "finalizer") + __match_args__: Final = ("firstiter", "finalizer") @property def firstiter(self) -> _AsyncgenHook: ... @@ -496,6 +505,10 @@ def set_coroutine_origin_tracking_depth(depth: int) -> None: ... def set_int_max_str_digits(maxdigits: int) -> None: ... def get_int_max_str_digits() -> int: ... +if sys.version_info >= (3, 15): + def set_lazy_imports(mode: _LazyImportMode) -> None: ... + def set_lazy_imports_filter(filter: _LazyImportFilter | None) -> None: ... + if sys.version_info >= (3, 12): if sys.version_info >= (3, 13): def getunicodeinternedsize(*, _only_immortal: bool = False) -> int: ... @@ -518,3 +531,7 @@ if sys.version_info >= (3, 14): def is_remote_debug_enabled() -> bool: ... def remote_exec(pid: int, script: StrOrBytesPath) -> None: ... def _is_immortal(op: object, /) -> bool: ... + + from . import __jit + + _jit = __jit diff --git a/mypy/typeshed/stdlib/sys/__jit.pyi b/mypy/typeshed/stdlib/sys/__jit.pyi new file mode 100644 index 0000000000000..90fb65c1d9efe --- /dev/null +++ b/mypy/typeshed/stdlib/sys/__jit.pyi @@ -0,0 +1,11 @@ +# This py314+ module provides annotations for `sys._jit`. +# It's named `sys.__jit` in typeshed, +# because trying to import `sys._jit` will fail at runtime! +# At runtime, `sys._jit` has the unusual status +# of being a `types.ModuleType` instance that cannot be directly imported, +# (same as sys.monitoring) +# and exists in the `sys`-module namespace despite `sys` not being a package. + +def is_available() -> bool: ... +def is_enabled() -> bool: ... +def is_active() -> bool: ... diff --git a/mypy/typeshed/stdlib/sys/_monitoring.pyi b/mypy/typeshed/stdlib/sys/_monitoring.pyi index 83f1e7dd0f1df..b999d63f64709 100644 --- a/mypy/typeshed/stdlib/sys/_monitoring.pyi +++ b/mypy/typeshed/stdlib/sys/_monitoring.pyi @@ -1,8 +1,9 @@ # This py312+ module provides annotations for `sys.monitoring`. # It's named `sys._monitoring` in typeshed, # because trying to import `sys.monitoring` will fail at runtime! -# At runtime, `sys.monitoring` has the unique status +# At runtime, `sys.monitoring` has the unusual status # of being a `types.ModuleType` instance that cannot be directly imported, +# (same as sys._jit) # and exists in the `sys`-module namespace despite `sys` not being a package. import sys diff --git a/mypy/typeshed/stdlib/sysconfig.pyi b/mypy/typeshed/stdlib/sysconfig.pyi index 8de7ddc4255f2..cfa3fbaceb731 100644 --- a/mypy/typeshed/stdlib/sysconfig.pyi +++ b/mypy/typeshed/stdlib/sysconfig.pyi @@ -21,35 +21,37 @@ __all__ = [ def get_config_var(name: Literal["SO"]) -> Any: ... @overload def get_config_var(name: str) -> Any: ... + @overload def get_config_vars() -> dict[str, Any]: ... @overload def get_config_vars(arg: str, /, *args: str) -> list[Any]: ... -def get_scheme_names() -> tuple[str, ...]: ... -if sys.version_info >= (3, 10): - def get_default_scheme() -> LiteralString: ... - def get_preferred_scheme(key: Literal["prefix", "home", "user"]) -> LiteralString: ... - # Documented -- see https://docs.python.org/3/library/sysconfig.html#sysconfig._get_preferred_schemes - def _get_preferred_schemes() -> dict[Literal["prefix", "home", "user"], LiteralString]: ... +def get_scheme_names() -> tuple[str, ...]: ... +def get_default_scheme() -> LiteralString: ... +def get_preferred_scheme(key: Literal["prefix", "home", "user"]) -> LiteralString: ... +# Documented -- see https://docs.python.org/3/library/sysconfig.html#sysconfig._get_preferred_schemes +def _get_preferred_schemes() -> dict[Literal["prefix", "home", "user"], LiteralString]: ... def get_path_names() -> tuple[str, ...]: ... def get_path(name: str, scheme: str = ..., vars: dict[str, Any] | None = None, expand: bool = True) -> str: ... def get_paths(scheme: str = ..., vars: dict[str, Any] | None = None, expand: bool = True) -> dict[str, str]: ... def get_python_version() -> str: ... def get_platform() -> str: ... -if sys.version_info >= (3, 12): +if sys.version_info >= (3, 15): + def is_python_build() -> bool: ... +elif sys.version_info >= (3, 11): @overload def is_python_build() -> bool: ... @overload @deprecated("The `check_home` parameter is deprecated since Python 3.12; removed in Python 3.15.") def is_python_build(check_home: object = None) -> bool: ... - -elif sys.version_info >= (3, 11): - def is_python_build(check_home: object = None) -> bool: ... - else: + @overload + def is_python_build() -> bool: ... + @overload + @deprecated("The `check_home` parameter is deprecated since Python 3.12; removed in Python 3.15.") def is_python_build(check_home: bool = False) -> bool: ... def parse_config_h(fp: IO[Any], vars: dict[str, Any] | None = None) -> dict[str, Any]: ... diff --git a/mypy/typeshed/stdlib/syslog.pyi b/mypy/typeshed/stdlib/syslog.pyi index b639c28c7f2d7..0cb97dcd3ca7e 100644 --- a/mypy/typeshed/stdlib/syslog.pyi +++ b/mypy/typeshed/stdlib/syslog.pyi @@ -51,6 +51,7 @@ if sys.platform != "win32": def closelog() -> None: ... def openlog(ident: str = ..., logoption: int = 0, facility: int = ...) -> None: ... def setlogmask(maskpri: int, /) -> int: ... + @overload def syslog(priority: int, message: str) -> None: ... @overload diff --git a/mypy/typeshed/stdlib/tarfile.pyi b/mypy/typeshed/stdlib/tarfile.pyi index 7d6bb341db314..6c55683fbaa37 100644 --- a/mypy/typeshed/stdlib/tarfile.pyi +++ b/mypy/typeshed/stdlib/tarfile.pyi @@ -6,8 +6,8 @@ from builtins import list as _list # aliases to avoid name clashes with fields from collections.abc import Callable, Iterable, Iterator, Mapping from gzip import _ReadableFileobj as _GzipReadableFileobj, _WritableFileobj as _GzipWritableFileobj from types import TracebackType -from typing import IO, ClassVar, Final, Literal, Protocol, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated +from typing import IO, ClassVar, Final, Literal, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import Self, deprecated if sys.version_info >= (3, 14): from compression.zstd import ZstdDict @@ -134,6 +134,26 @@ class TarFile: extraction_filter: _FilterFunction | None if sys.version_info >= (3, 13): stream: bool + if sys.version_info >= (3, 15): + def __init__( + self, + name: StrOrBytesPath | None = None, + mode: Literal["r", "a", "w", "x"] = "r", + fileobj: _Fileobj | None = None, + format: int | None = None, + tarinfo: type[TarInfo] | None = None, + dereference: bool | None = None, + ignore_zeros: bool | None = None, + encoding: str | None = None, + errors: str = "surrogateescape", + pax_headers: Mapping[str, str] | None = None, + debug: Literal[0, 1, 2, 3] | None = None, # default 0 + errorlevel: Literal[0, 1, 2] | None = None, # default 1 + copybufsize: int | None = None, # undocumented + stream: bool = False, + mtime: float | None = None, + ) -> None: ... + elif sys.version_info >= (3, 13): def __init__( self, name: StrOrBytesPath | None = None, @@ -193,6 +213,7 @@ class TarFile: debug: Literal[0, 1, 2, 3] | None = None, # default 0 errorlevel: Literal[0, 1, 2] | None = None, # default 1 ) -> Self: ... + if sys.version_info >= (3, 14): @overload @classmethod @@ -495,6 +516,7 @@ class TarFile: errorlevel: Literal[0, 1, 2] | None = None, # default 1 compresslevel: int = 9, ) -> Self: ... + @classmethod def taropen( cls, @@ -512,6 +534,7 @@ class TarFile: debug: Literal[0, 1, 2, 3] | None = None, # default 0 errorlevel: Literal[0, 1, 2] | None = None, # default 1 ) -> Self: ... + @overload @classmethod def gzopen( @@ -548,6 +571,7 @@ class TarFile: debug: Literal[0, 1, 2, 3] | None = None, # default 0 errorlevel: Literal[0, 1, 2] | None = None, # default 1 ) -> Self: ... + @overload @classmethod def bz2open( @@ -584,6 +608,7 @@ class TarFile: debug: Literal[0, 1, 2, 3] | None = None, # default 0 errorlevel: Literal[0, 1, 2] | None = None, # default 1 ) -> Self: ... + @classmethod def xzopen( cls, @@ -785,24 +810,24 @@ class TarInfo: gname: str pax_headers: Mapping[str, str] def __init__(self, name: str = "") -> None: ... - if sys.version_info >= (3, 13): - @property - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") - def tarfile(self) -> TarFile | None: ... - @tarfile.setter - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") - def tarfile(self, tarfile: TarFile | None) -> None: ... - else: - tarfile: TarFile | None + + @property + @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") + def tarfile(self) -> TarFile | None: ... + @tarfile.setter + @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") + def tarfile(self, tarfile: TarFile | None) -> None: ... @classmethod def frombuf(cls, buf: bytes | bytearray, encoding: str, errors: str) -> Self: ... @classmethod def fromtarfile(cls, tarfile: TarFile) -> Self: ... + @property def linkpath(self) -> str: ... @linkpath.setter def linkpath(self, linkname: str) -> None: ... + def replace( self, *, diff --git a/mypy/typeshed/stdlib/tempfile.pyi b/mypy/typeshed/stdlib/tempfile.pyi index 26491074ff71d..1df04a3d3667a 100644 --- a/mypy/typeshed/stdlib/tempfile.pyi +++ b/mypy/typeshed/stdlib/tempfile.pyi @@ -81,7 +81,6 @@ if sys.version_info >= (3, 12): errors: str | None = None, delete_on_close: bool = True, ) -> _TemporaryFileWrapper[Any]: ... - else: @overload def NamedTemporaryFile( @@ -249,18 +248,21 @@ class _TemporaryFileWrapper(IO[AnyStr]): def tell(self) -> int: ... def truncate(self, size: int | None = ...) -> int: ... def writable(self) -> bool: ... + @overload def write(self: _TemporaryFileWrapper[str], s: str, /) -> int: ... @overload def write(self: _TemporaryFileWrapper[bytes], s: ReadableBuffer, /) -> int: ... @overload def write(self, s: AnyStr, /) -> int: ... + @overload def writelines(self: _TemporaryFileWrapper[str], lines: Iterable[str]) -> None: ... @overload def writelines(self: _TemporaryFileWrapper[bytes], lines: Iterable[ReadableBuffer]) -> None: ... @overload def writelines(self, lines: Iterable[AnyStr]) -> None: ... + @property def closed(self) -> bool: ... @@ -277,6 +279,7 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase): def encoding(self) -> str: ... # undocumented @property def newlines(self) -> str | tuple[str, ...] | None: ... # undocumented + # bytes needs to go first, as default mode is to open as bytes @overload def __init__( @@ -348,6 +351,7 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase): dir: str | None = None, errors: str | None = None, ) -> None: ... + @property def errors(self) -> str | None: ... def rollover(self) -> None: ... @@ -384,12 +388,14 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase): def write(self: SpooledTemporaryFile[bytes], s: ReadableBuffer) -> int: ... @overload def write(self, s: AnyStr) -> int: ... + @overload # type: ignore[override] def writelines(self: SpooledTemporaryFile[str], iterable: Iterable[str]) -> None: ... @overload def writelines(self: SpooledTemporaryFile[bytes], iterable: Iterable[ReadableBuffer]) -> None: ... @overload def writelines(self, iterable: Iterable[AnyStr]) -> None: ... + def __iter__(self) -> Iterator[AnyStr]: ... # type: ignore[override] # These exist at runtime only on 3.11+. def readable(self) -> bool: ... @@ -421,7 +427,7 @@ class TemporaryDirectory(Generic[AnyStr]): *, delete: bool = True, ) -> None: ... - elif sys.version_info >= (3, 10): + else: @overload def __init__( self: TemporaryDirectory[str], @@ -438,18 +444,6 @@ class TemporaryDirectory(Generic[AnyStr]): dir: BytesPath | None = None, ignore_cleanup_errors: bool = False, ) -> None: ... - else: - @overload - def __init__( - self: TemporaryDirectory[str], suffix: str | None = None, prefix: str | None = None, dir: StrPath | None = None - ) -> None: ... - @overload - def __init__( - self: TemporaryDirectory[bytes], - suffix: bytes | None = None, - prefix: bytes | None = None, - dir: BytesPath | None = None, - ) -> None: ... def cleanup(self) -> None: ... def __enter__(self) -> AnyStr: ... @@ -471,6 +465,7 @@ def mkstemp( def mkdtemp(suffix: str | None = None, prefix: str | None = None, dir: StrPath | None = None) -> str: ... @overload def mkdtemp(suffix: bytes | None = None, prefix: bytes | None = None, dir: BytesPath | None = None) -> bytes: ... + @deprecated("Deprecated since Python 2.3. Use `mkstemp()` or `NamedTemporaryFile(delete=False)` instead.") def mktemp(suffix: str = "", prefix: str = "tmp", dir: StrPath | None = None) -> str: ... def gettempdirb() -> bytes: ... diff --git a/mypy/typeshed/stdlib/termios.pyi b/mypy/typeshed/stdlib/termios.pyi index a35be5dfe740a..971c9ad46bf45 100644 --- a/mypy/typeshed/stdlib/termios.pyi +++ b/mypy/typeshed/stdlib/termios.pyi @@ -1,7 +1,6 @@ import sys from _typeshed import FileDescriptorLike -from typing import Any, Final -from typing_extensions import TypeAlias +from typing import Any, Final, TypeAlias # Must be a list of length 7, containing 6 ints and a list of NCCS 1-character bytes or ints. _Attr: TypeAlias = list[int | list[bytes | int]] | list[int | list[bytes]] | list[int | list[int]] diff --git a/mypy/typeshed/stdlib/threading.pyi b/mypy/typeshed/stdlib/threading.pyi index 03c8865d3c0a4..6b51b424cee6d 100644 --- a/mypy/typeshed/stdlib/threading.pyi +++ b/mypy/typeshed/stdlib/threading.pyi @@ -2,11 +2,11 @@ import _thread import sys from _thread import _ExceptHookArgs, get_native_id as get_native_id from _typeshed import ProfileFunction, TraceFunction -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Iterable, Iterator, Mapping from contextvars import Context from types import TracebackType from typing import Any, Final, TypeVar, final -from typing_extensions import deprecated +from typing_extensions import Self, deprecated _T = TypeVar("_T") @@ -29,6 +29,8 @@ __all__ = [ "Timer", "ThreadError", "ExceptHookArgs", + "getprofile", + "gettrace", "setprofile", "settrace", "local", @@ -37,12 +39,12 @@ __all__ = [ "get_native_id", ] -if sys.version_info >= (3, 10): - __all__ += ["getprofile", "gettrace"] - if sys.version_info >= (3, 12): __all__ += ["setprofile_all_threads", "settrace_all_threads"] +if sys.version_info >= (3, 15): + __all__ += ["concurrent_tee", "serialize_iterator", "synchronized_iterator"] + _profile_hook: ProfileFunction | None def active_count() -> int: ... @@ -61,9 +63,21 @@ if sys.version_info >= (3, 12): def setprofile_all_threads(func: ProfileFunction | None) -> None: ... def settrace_all_threads(func: TraceFunction | None) -> None: ... -if sys.version_info >= (3, 10): - def gettrace() -> TraceFunction | None: ... - def getprofile() -> ProfileFunction | None: ... +def gettrace() -> TraceFunction | None: ... +def getprofile() -> ProfileFunction | None: ... + +if sys.version_info >= (3, 15): + @final + class serialize_iterator(Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + def send(self, value: Any, /) -> _T: ... + def throw(self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ...) -> _T: ... + def close(self) -> None: ... + + def synchronized_iterator(func: Callable[..., Iterable[_T]]) -> Callable[..., Iterator[_T]]: ... + def concurrent_tee(iterable: Iterable[_T], n: int = 2) -> tuple[Iterator[_T], ...]: ... def stack_size(size: int = 0, /) -> int: ... @@ -173,8 +187,7 @@ class Event: def wait(self, timeout: float | None = None) -> bool: ... excepthook: Callable[[_ExceptHookArgs], object] -if sys.version_info >= (3, 10): - __excepthook__: Callable[[_ExceptHookArgs], object] +__excepthook__: Callable[[_ExceptHookArgs], object] ExceptHookArgs = _ExceptHookArgs class Timer(Thread): diff --git a/mypy/typeshed/stdlib/time.pyi b/mypy/typeshed/stdlib/time.pyi index d0853792b636d..9b5344b326cec 100644 --- a/mypy/typeshed/stdlib/time.pyi +++ b/mypy/typeshed/stdlib/time.pyi @@ -1,7 +1,6 @@ import sys from _typeshed import structseq -from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, Union, final, type_check_only -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypeAlias, Union, final, type_check_only _TimeTuple: TypeAlias = tuple[int, int, int, int, int, int, int, int, int] @@ -47,8 +46,7 @@ if sys.platform == "linux": # https://github.com/python/typeshed/pull/6560#discussion_r767162532 @final class struct_time(structseq[Any | int], _TimeTuple): - if sys.version_info >= (3, 10): - __match_args__: Final = ("tm_year", "tm_mon", "tm_mday", "tm_hour", "tm_min", "tm_sec", "tm_wday", "tm_yday", "tm_isdst") + __match_args__: Final = ("tm_year", "tm_mon", "tm_mday", "tm_hour", "tm_min", "tm_sec", "tm_wday", "tm_yday", "tm_isdst") @property def tm_year(self) -> int: ... diff --git a/mypy/typeshed/stdlib/timeit.pyi b/mypy/typeshed/stdlib/timeit.pyi index 61935815f68b2..cc2b2027a8201 100644 --- a/mypy/typeshed/stdlib/timeit.pyi +++ b/mypy/typeshed/stdlib/timeit.pyi @@ -1,7 +1,7 @@ +import sys import time from collections.abc import Callable, Sequence -from typing import IO, Any -from typing_extensions import TypeAlias +from typing import IO, Any, TypeAlias __all__ = ["Timer", "timeit", "repeat", "default_timer"] @@ -21,7 +21,12 @@ class Timer: def print_exc(self, file: IO[str] | None = None) -> None: ... def timeit(self, number: int = 1000000) -> float: ... def repeat(self, repeat: int = 5, number: int = 1000000) -> list[float]: ... - def autorange(self, callback: Callable[[int, float], object] | None = None) -> tuple[int, float]: ... + if sys.version_info >= (3, 15): + def autorange( + self, callback: Callable[[int, float], object] | None = None, target_time: float = 0.2 + ) -> tuple[int, float]: ... + else: + def autorange(self, callback: Callable[[int, float], object] | None = None) -> tuple[int, float]: ... def timeit( stmt: _Stmt = "pass", diff --git a/mypy/typeshed/stdlib/tkinter/__init__.pyi b/mypy/typeshed/stdlib/tkinter/__init__.pyi index a70ef2351f3d4..94787911df8d4 100644 --- a/mypy/typeshed/stdlib/tkinter/__init__.pyi +++ b/mypy/typeshed/stdlib/tkinter/__init__.pyi @@ -5,8 +5,22 @@ from collections.abc import Callable, Iterable, Mapping, Sequence from tkinter.constants import * from tkinter.font import _FontDescription from types import GenericAlias, TracebackType -from typing import Any, ClassVar, Final, Generic, Literal, NamedTuple, Protocol, TypedDict, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias, TypeVarTuple, Unpack, deprecated, disjoint_base +from typing import ( + Any, + ClassVar, + Final, + Generic, + Literal, + NamedTuple, + ParamSpec, + Protocol, + TypeAlias, + TypedDict, + TypeVar, + overload, + type_check_only, +) +from typing_extensions import TypeVarTuple, Unpack, deprecated, disjoint_base if sys.version_info >= (3, 11): from enum import StrEnum @@ -299,6 +313,9 @@ class Event(Generic[_W_co]): type: EventType widget: _W_co delta: int + if sys.version_info >= (3, 15): + detail: str + user_data: str if sys.version_info >= (3, 14): def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @@ -312,21 +329,14 @@ class Variable: def trace_add(self, mode: Literal["array", "read", "write", "unset"], callback: Callable[[str, str, str], object]) -> str: ... def trace_remove(self, mode: Literal["array", "read", "write", "unset"], cbname: str) -> None: ... def trace_info(self) -> list[tuple[tuple[Literal["array", "read", "write", "unset"], ...], str]]: ... - if sys.version_info >= (3, 14): - @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") - def trace(self, mode, callback) -> str: ... - @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") - def trace_variable(self, mode, callback) -> str: ... - @deprecated("Deprecated since Python 3.14. Use `trace_remove()` instead.") - def trace_vdelete(self, mode, cbname) -> None: ... - @deprecated("Deprecated since Python 3.14. Use `trace_info()` instead.") - def trace_vinfo(self) -> list[Incomplete]: ... - else: - def trace(self, mode, callback) -> str: ... - def trace_variable(self, mode, callback) -> str: ... - def trace_vdelete(self, mode, cbname) -> None: ... - def trace_vinfo(self) -> list[Incomplete]: ... - + @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") + def trace(self, mode, callback) -> str: ... + @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") + def trace_variable(self, mode, callback) -> str: ... + @deprecated("Deprecated since Python 3.14. Use `trace_remove()` instead.") + def trace_vdelete(self, mode, cbname) -> None: ... + @deprecated("Deprecated since Python 3.14. Use `trace_info()` instead.") + def trace_vinfo(self) -> list[Incomplete]: ... def __eq__(self, other: object) -> bool: ... def __del__(self) -> None: ... __hash__: ClassVar[None] # type: ignore[assignment] @@ -363,6 +373,7 @@ getdouble = float def getboolean(s) -> bool: ... _Ts = TypeVarTuple("_Ts") +_P = ParamSpec("_P") @type_check_only class _GridIndexInfo(TypedDict, total=False): @@ -402,11 +413,19 @@ class Misc: def tk_focusFollowsMouse(self) -> None: ... def tk_focusNext(self) -> Misc | None: ... def tk_focusPrev(self) -> Misc | None: ... - # .after() can be called without the "func" argument, but it is basically never what you want. - # It behaves like time.sleep() and freezes the GUI app. - def after(self, ms: int | Literal["idle"], func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... - # after_idle is essentially partialmethod(after, "idle") - def after_idle(self, func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... + if sys.version_info >= (3, 14): + # .after() can be called without the "func" argument, but it is basically never what you want. + # It behaves like time.sleep() and freezes the GUI app. + def after(self, ms: int | Literal["idle"], func: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs) -> str: ... + # after_idle is essentially partialmethod(after, "idle") + def after_idle(self, func: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs) -> str: ... + else: + # .after() can be called without the "func" argument, but it is basically never what you want. + # It behaves like time.sleep() and freezes the GUI app. + def after(self, ms: int | Literal["idle"], func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... + # after_idle is essentially partialmethod(after, "idle") + def after_idle(self, func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... + def after_cancel(self, id: str) -> None: ... if sys.version_info >= (3, 13): def after_info(self, id: str | None = None) -> tuple[str, ...]: ... @@ -508,10 +527,12 @@ class Misc: def winfo_y(self) -> int: ... def update(self) -> None: ... def update_idletasks(self) -> None: ... + @overload def bindtags(self, tagList: None = None) -> tuple[str, ...]: ... @overload def bindtags(self, tagList: list[str] | tuple[str, ...]) -> None: ... + # bind with isinstance(func, str) doesn't return anything, but all other # binds do. The default value of func is not str. @overload @@ -525,6 +546,7 @@ class Misc: def bind(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... @overload def bind(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + # There's no way to know what type of widget bind_all and bind_class # callbacks will get, so those are Misc. @overload @@ -538,6 +560,7 @@ class Misc: def bind_all(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... @overload def bind_all(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + @overload def bind_class( self, @@ -550,6 +573,7 @@ class Misc: def bind_class(self, className: str, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... @overload def bind_class(self, className: str, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + def unbind(self, sequence: str, funcid: str | None = None) -> None: ... def unbind_all(self, sequence: str) -> None: ... def unbind_class(self, className: str, sequence: str) -> None: ... @@ -562,13 +586,16 @@ class Misc: self, func: Callable[..., object], subst: Callable[..., Sequence[Any]] | None = None, needcleanup: int = 1 ) -> str: ... def keys(self) -> list[str]: ... + @overload def pack_propagate(self, flag: bool) -> bool | None: ... @overload def pack_propagate(self) -> None: ... + propagate = pack_propagate def grid_anchor(self, anchor: Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] | None = None) -> None: ... anchor = grid_anchor + @overload def grid_bbox( self, column: None = None, row: None = None, col2: None = None, row2: None = None @@ -577,6 +604,7 @@ class Misc: def grid_bbox(self, column: int, row: int, col2: None = None, row2: None = None) -> tuple[int, int, int, int] | None: ... @overload def grid_bbox(self, column: int, row: int, col2: int, row2: int) -> tuple[int, int, int, int] | None: ... + bbox = grid_bbox def grid_columnconfigure( self, @@ -601,10 +629,12 @@ class Misc: columnconfigure = grid_columnconfigure rowconfigure = grid_rowconfigure def grid_location(self, x: float | str, y: float | str) -> tuple[int, int]: ... + @overload def grid_propagate(self, flag: bool) -> None: ... @overload def grid_propagate(self) -> bool: ... + def grid_size(self) -> tuple[int, int]: ... size = grid_size # Widget because Toplevel or Tk is never a slave @@ -612,6 +642,12 @@ class Misc: def grid_slaves(self, row: int | None = None, column: int | None = None) -> list[Widget]: ... def place_slaves(self) -> list[Widget]: ... slaves = pack_slaves + if sys.version_info >= (3, 15): + def pack_content(self) -> list[Widget]: ... + def grid_content(self, row: int | None = None, column: int | None = None) -> list[Widget]: ... + def place_content(self) -> list[Widget]: ... + content = pack_content + def event_add(self, virtual: str, *sequences: str) -> None: ... def event_delete(self, virtual: str, *sequences: str) -> None: ... def event_generate( @@ -654,8 +690,7 @@ class Misc: def __getitem__(self, key: str) -> Any: ... def cget(self, key: str) -> Any: ... def configure(self, cnf: Any = None) -> Any: ... - # TODO: config is an alias of configure, but adding that here creates - # conflict with the type of config in the subclasses. See #13149 + config = configure class CallWrapper: func: Incomplete @@ -669,7 +704,9 @@ class XView: def xview(self) -> tuple[float, float]: ... @overload def xview(self, *args) -> None: ... + def xview_moveto(self, fraction: float) -> None: ... + @overload def xview_scroll(self, number: int, what: Literal["units", "pages"]) -> None: ... @overload @@ -680,7 +717,9 @@ class YView: def yview(self) -> tuple[float, float]: ... @overload def yview(self, *args) -> None: ... + def yview_moveto(self, fraction: float) -> None: ... + @overload def yview_scroll(self, number: int, what: Literal["units", "pages"]) -> None: ... @overload @@ -725,17 +764,20 @@ class Wm: def wm_aspect( self, minNumer: None = None, minDenom: None = None, maxNumer: None = None, maxDenom: None = None ) -> tuple[int, int, int, int] | None: ... + aspect = wm_aspect + + # wm_attributes: Get all attributes if sys.version_info >= (3, 13): @overload def wm_attributes(self, *, return_python_dict: Literal[False] = False) -> tuple[Any, ...]: ... @overload def wm_attributes(self, *, return_python_dict: Literal[True]) -> _WmAttributes: ... - else: @overload def wm_attributes(self) -> tuple[Any, ...]: ... + # wm_attributes: Get one attribute (old variant using string that starts with "-") @overload def wm_attributes(self, option: Literal["-alpha"], /) -> float: ... @overload @@ -767,6 +809,7 @@ class Wm: @overload def wm_attributes(self, option: Literal["-type"], /) -> str: ... if sys.version_info >= (3, 13): + # wm_attributes: Get one attribute (new variant without "-") @overload def wm_attributes(self, option: Literal["alpha"], /) -> float: ... @overload @@ -798,6 +841,7 @@ class Wm: @overload def wm_attributes(self, option: Literal["type"], /) -> str: ... + # wm_attributes: Set an attribute (old variant using string that starts with "-") @overload def wm_attributes(self, option: str, /): ... @overload @@ -829,8 +873,11 @@ class Wm: @overload def wm_attributes(self, option: Literal["-type"], value: str, /) -> Literal[""]: ... + # wm_attributes: Set multiple attributes (old variant using strings that start with "-") @overload def wm_attributes(self, option: str, value, /, *__other_option_value_pairs: Any) -> Literal[""]: ... + + # wm_attributes: Set an attribute (new variant with kwarg instead of string) if sys.version_info >= (3, 13): if sys.platform == "darwin": @overload @@ -867,12 +914,14 @@ class Wm: attributes = wm_attributes def wm_client(self, name: str | None = None) -> str: ... client = wm_client + @overload def wm_colormapwindows(self) -> list[Misc]: ... @overload def wm_colormapwindows(self, wlist: list[Misc] | tuple[Misc, ...], /) -> None: ... @overload def wm_colormapwindows(self, first_wlist_item: Misc, /, *other_wlist_items: Misc) -> None: ... + colormapwindows = wm_colormapwindows def wm_command(self, value: str | None = None) -> str: ... command = wm_command @@ -885,10 +934,12 @@ class Wm: forget = wm_forget def wm_frame(self) -> str: ... frame = wm_frame + @overload def wm_geometry(self, newGeometry: None = None) -> str: ... @overload def wm_geometry(self, newGeometry: str) -> None: ... + geometry = wm_geometry def wm_grid(self, baseWidth=None, baseHeight=None, widthInc=None, heightInc=None): ... grid = wm_grid @@ -910,51 +961,67 @@ class Wm: iconwindow = wm_iconwindow def wm_manage(self, widget) -> None: ... manage = wm_manage + @overload def wm_maxsize(self, width: None = None, height: None = None) -> tuple[int, int]: ... @overload def wm_maxsize(self, width: int, height: int) -> None: ... + maxsize = wm_maxsize + @overload def wm_minsize(self, width: None = None, height: None = None) -> tuple[int, int]: ... @overload def wm_minsize(self, width: int, height: int) -> None: ... + minsize = wm_minsize + @overload def wm_overrideredirect(self, boolean: None = None) -> bool | None: ... # returns True or None @overload def wm_overrideredirect(self, boolean: bool) -> None: ... + overrideredirect = wm_overrideredirect def wm_positionfrom(self, who: Literal["program", "user"] | None = None) -> Literal["", "program", "user"]: ... positionfrom = wm_positionfrom + @overload def wm_protocol(self, name: str, func: Callable[[], object] | str) -> None: ... @overload def wm_protocol(self, name: str, func: None = None) -> str: ... @overload def wm_protocol(self, name: None = None, func: None = None) -> tuple[str, ...]: ... + protocol = wm_protocol + @overload def wm_resizable(self, width: None = None, height: None = None) -> tuple[bool, bool]: ... @overload def wm_resizable(self, width: bool, height: bool) -> None: ... + resizable = wm_resizable def wm_sizefrom(self, who: Literal["program", "user"] | None = None) -> Literal["", "program", "user"]: ... sizefrom = wm_sizefrom + @overload def wm_state(self, newstate: None = None) -> str: ... @overload def wm_state(self, newstate: str) -> None: ... + state = wm_state + @overload def wm_title(self, string: None = None) -> str: ... @overload def wm_title(self, string: str) -> None: ... + title = wm_title + @overload def wm_transient(self, master: None = None) -> _tkinter.Tcl_Obj: ... @overload def wm_transient(self, master: Wm | _tkinter.Tcl_Obj) -> None: ... + transient = wm_transient def wm_withdraw(self) -> None: ... withdraw = wm_withdraw @@ -973,6 +1040,7 @@ class Tk(Misc, Wm): sync: bool = False, use: str | None = None, ) -> None: ... + # Keep this in sync with ttktheme.ThemedTk. See issue #13858 @overload def configure( @@ -998,6 +1066,7 @@ class Tk(Misc, Wm): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def destroy(self) -> None: ... def readprofile(self, baseName: str, className: str) -> None: ... @@ -1033,6 +1102,7 @@ class Tk(Misc, Wm): def splitlist(self, arg, /) -> tuple[Incomplete, ...]: ... def unsetvar(self, *args, **kwargs): ... + if sys.version_info >= (3, 14): @overload def wantobjects(self) -> Literal[0, 1]: ... @@ -1042,6 +1112,7 @@ class Tk(Misc, Wm): @overload def wantobjects(self, wantobjects: Literal[0, 1] | bool, /) -> None: ... + def willdispatch(self) -> None: ... def Tcl(screenName: str | None = None, baseName: str | None = None, className: str = "Tk", useTk: bool = False) -> Tk: ... @@ -1222,6 +1293,7 @@ class Toplevel(BaseWidget, Wm): visual: str | tuple[str, int] = "", width: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -1246,6 +1318,7 @@ class Toplevel(BaseWidget, Wm): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Button(Widget): @@ -1297,6 +1370,7 @@ class Button(Widget): width: float | str = 0, wraplength: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -1341,6 +1415,7 @@ class Button(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def flash(self) -> None: ... def invoke(self) -> Any: ... @@ -1386,6 +1461,7 @@ class Canvas(Widget, XView, YView): yscrollcommand: str | Callable[[float, float], object] = "", yscrollincrement: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -1424,6 +1500,7 @@ class Canvas(Widget, XView, YView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def addtag(self, *args): ... # internal method def addtag_above(self, newtag: str, tagOrId: str | int) -> None: ... @@ -1447,6 +1524,7 @@ class Canvas(Widget, XView, YView): def find_withtag(self, tagOrId: str | int) -> tuple[int, ...]: ... # Incompatible with Misc.bbox(), tkinter violates LSP def bbox(self, *args: str | int) -> tuple[int, int, int, int]: ... # type: ignore[override] + @overload def tag_bind( self, @@ -1461,21 +1539,25 @@ class Canvas(Widget, XView, YView): ) -> None: ... @overload def tag_bind(self, tagOrId: str | int, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + def tag_unbind(self, tagOrId: str | int, sequence: str, funcid: str | None = None) -> None: ... - def canvasx(self, screenx, gridspacing=None): ... - def canvasy(self, screeny, gridspacing=None): ... + def canvasx(self, screenx: float | str, gridspacing: float | str | None = None) -> float: ... + def canvasy(self, screeny: float | str, gridspacing: float | str | None = None) -> float: ... + @overload def coords(self, tagOrId: str | int, /) -> list[float]: ... @overload def coords(self, tagOrId: str | int, args: list[int] | list[float] | tuple[float, ...], /) -> None: ... @overload def coords(self, tagOrId: str | int, x1: float, y1: float, /, *args: float) -> None: ... + # create_foo() methods accept coords as a list or tuple, or as separate arguments. # Lists and tuples can be flat as in [1, 2, 3, 4], or nested as in [(1, 2), (3, 4)]. # Keyword arguments should be the same in all overloads of each method. def create_arc(self, *args, **kw) -> int: ... def create_bitmap(self, *args, **kw) -> int: ... def create_image(self, *args, **kw) -> int: ... + @overload def create_line( self, @@ -1574,6 +1656,7 @@ class Canvas(Widget, XView, YView): tags: str | list[str] | tuple[str, ...] = ..., width: float | str = ..., ) -> int: ... + @overload def create_oval( self, @@ -1675,6 +1758,7 @@ class Canvas(Widget, XView, YView): tags: str | list[str] | tuple[str, ...] = ..., width: float | str = ..., ) -> int: ... + @overload def create_polygon( self, @@ -1785,6 +1869,7 @@ class Canvas(Widget, XView, YView): tags: str | list[str] | tuple[str, ...] = ..., width: float | str = ..., ) -> int: ... + @overload def create_rectangle( self, @@ -1886,6 +1971,7 @@ class Canvas(Widget, XView, YView): tags: str | list[str] | tuple[str, ...] = ..., width: float | str = ..., ) -> int: ... + @overload def create_text( self, @@ -1931,6 +2017,7 @@ class Canvas(Widget, XView, YView): text: float | str = ..., width: float | str = ..., ) -> int: ... + @overload def create_window( self, @@ -1958,12 +2045,15 @@ class Canvas(Widget, XView, YView): width: float | str = ..., window: Widget = ..., ) -> int: ... + def dchars(self, *args) -> None: ... def delete(self, *tagsOrCanvasIds: str | int) -> None: ... + @overload def dtag(self, tag: str, tag_to_delete: str | None = ..., /) -> None: ... @overload def dtag(self, id: int, tag_to_delete: str, /) -> None: ... + def focus(self, *args): ... def gettags(self, tagOrId: str | int, /) -> tuple[str, ...]: ... def icursor(self, *args) -> None: ... @@ -2058,6 +2148,7 @@ class Checkbutton(Widget): width: float | str = 0, wraplength: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -2108,6 +2199,7 @@ class Checkbutton(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def deselect(self) -> None: ... def flash(self) -> None: ... @@ -2160,6 +2252,7 @@ class Entry(Widget, XView): width: int = 20, xscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload def configure( self, @@ -2205,6 +2298,7 @@ class Entry(Widget, XView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def delete(self, first: str | int, last: str | int | None = None) -> None: ... def get(self) -> str: ... @@ -2253,6 +2347,7 @@ class Frame(Widget): visual: str | tuple[str, int] = "", # can't be changed with configure() width: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -2276,6 +2371,7 @@ class Frame(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Label(Widget): @@ -2317,6 +2413,7 @@ class Label(Widget): width: float | str = 0, wraplength: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -2356,6 +2453,7 @@ class Label(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Listbox(Widget, XView, YView): @@ -2410,6 +2508,7 @@ class Listbox(Widget, XView, YView): xscrollcommand: str | Callable[[float, float], object] = "", yscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload def configure( self, @@ -2447,6 +2546,7 @@ class Listbox(Widget, XView, YView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def activate(self, index: str | int) -> None: ... def bbox(self, index: str | int) -> tuple[int, int, int, int] | None: ... # type: ignore[override] @@ -2504,6 +2604,7 @@ class Menu(Widget): title: str = "", type: Literal["menubar", "tearoff", "normal"] = "normal", ) -> None: ... + @overload def configure( self, @@ -2533,6 +2634,7 @@ class Menu(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def tk_popup(self, x: int, y: int, entry: str | int = "") -> None: ... def activate(self, index: str | int) -> None: ... @@ -2784,6 +2886,7 @@ class Menubutton(Widget): width: float | str = 0, wraplength: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -2826,6 +2929,7 @@ class Menubutton(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Message(Widget): @@ -2859,6 +2963,7 @@ class Message(Widget): # there's width but no height width: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -2889,6 +2994,7 @@ class Message(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Radiobutton(Widget): @@ -2940,6 +3046,7 @@ class Radiobutton(Widget): width: float | str = 0, wraplength: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -2989,6 +3096,7 @@ class Radiobutton(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def deselect(self) -> None: ... def flash(self) -> None: ... @@ -3038,6 +3146,7 @@ class Scale(Widget): variable: IntVar | DoubleVar = ..., width: float | str = 15, ) -> None: ... + @overload def configure( self, @@ -3080,6 +3189,7 @@ class Scale(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def get(self) -> float: ... def set(self, value) -> None: ... @@ -3119,6 +3229,7 @@ class Scrollbar(Widget): troughcolor: str = ..., width: float | str = ..., ) -> None: ... + @overload def configure( self, @@ -3148,6 +3259,7 @@ class Scrollbar(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def activate(self, index=None): ... def delta(self, deltax: int, deltay: int) -> float: ... @@ -3217,6 +3329,7 @@ class Text(Widget, XView, YView): xscrollcommand: str | Callable[[float, float], object] = "", yscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload def configure( self, @@ -3270,6 +3383,7 @@ class Text(Widget, XView, YView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def bbox(self, index: str | float | _tkinter.Tcl_Obj | Widget) -> tuple[int, int, int, int] | None: ... # type: ignore[override] def compare( @@ -3278,6 +3392,7 @@ class Text(Widget, XView, YView): op: Literal["<", "<=", "==", ">=", ">", "!="], index2: str | float | _tkinter.Tcl_Obj | Widget, ) -> bool: ... + if sys.version_info >= (3, 13): @overload def count( @@ -3461,10 +3576,12 @@ class Text(Widget, XView, YView): def debug(self, boolean: None = None) -> bool: ... @overload def debug(self, boolean: bool) -> None: ... + def delete( self, index1: str | float | _tkinter.Tcl_Obj | Widget, index2: str | float | _tkinter.Tcl_Obj | Widget | None = None ) -> None: ... def dlineinfo(self, index: str | float | _tkinter.Tcl_Obj | Widget) -> tuple[int, int, int, int, int] | None: ... + @overload def dump( self, @@ -3507,11 +3624,14 @@ class Text(Widget, XView, YView): text: bool = ..., window: bool = ..., ) -> None: ... + def edit(self, *args): ... # docstring says "Internal method" + @overload def edit_modified(self, arg: None = None) -> bool: ... # actually returns Literal[0, 1] @overload def edit_modified(self, arg: bool) -> None: ... # actually returns empty string + def edit_redo(self) -> None: ... # actually returns empty string def edit_reset(self) -> None: ... # actually returns empty string def edit_separator(self) -> None: ... # actually returns empty string @@ -3519,6 +3639,7 @@ class Text(Widget, XView, YView): def get( self, index1: str | float | _tkinter.Tcl_Obj | Widget, index2: str | float | _tkinter.Tcl_Obj | Widget | None = None ) -> str: ... + @overload def image_cget(self, index: str | float | _tkinter.Tcl_Obj | Widget, option: Literal["image", "name"]) -> str: ... @overload @@ -3529,6 +3650,7 @@ class Text(Widget, XView, YView): ) -> Literal["baseline", "bottom", "center", "top"]: ... @overload def image_cget(self, index: str | float | _tkinter.Tcl_Obj | Widget, option: str) -> Any: ... + @overload def image_configure( self, index: str | float | _tkinter.Tcl_Obj | Widget, cnf: str @@ -3545,6 +3667,7 @@ class Text(Widget, XView, YView): padx: float | str = ..., pady: float | str = ..., ) -> dict[str, tuple[str, str, str, str, str | int]] | None: ... + def image_create( self, index: str | float | _tkinter.Tcl_Obj | Widget, @@ -3561,10 +3684,12 @@ class Text(Widget, XView, YView): def insert( self, index: str | float | _tkinter.Tcl_Obj | Widget, chars: str, *args: str | list[str] | tuple[str, ...] ) -> None: ... + @overload def mark_gravity(self, markName: str, direction: None = None) -> Literal["left", "right"]: ... @overload def mark_gravity(self, markName: str, direction: Literal["left", "right"]) -> None: ... # actually returns empty string + def mark_names(self) -> tuple[str, ...]: ... def mark_set(self, markName: str, index: str | float | _tkinter.Tcl_Obj | Widget) -> None: ... def mark_unset(self, *markNames: str) -> None: ... @@ -3582,23 +3707,60 @@ class Text(Widget, XView, YView): ) -> None: ... def scan_mark(self, x: int, y: int) -> None: ... def scan_dragto(self, x: int, y: int) -> None: ... - def search( - self, - pattern: str, - index: str | float | _tkinter.Tcl_Obj | Widget, - stopindex: str | float | _tkinter.Tcl_Obj | Widget | None = None, - forwards: bool | None = None, - backwards: bool | None = None, - exact: bool | None = None, - regexp: bool | None = None, - nocase: bool | None = None, - count: Variable | None = None, - elide: bool | None = None, - ) -> str: ... # returns empty string for not found + if sys.version_info >= (3, 15): + def search( + self, + pattern: str, + index: str | float | _tkinter.Tcl_Obj | Widget, + stopindex: str | float | _tkinter.Tcl_Obj | Widget | None = None, + forwards: bool | None = None, + backwards: bool | None = None, + exact: bool | None = None, + regexp: bool | None = None, + nocase: bool | None = None, + count: Variable | None = None, + elide: bool | None = None, + *, + nolinestop: bool | None = None, + strictlimits: bool | None = None, + ) -> str: ... # returns empty string for not found + def search_all( + self, + pattern: str, + index: str | float | _tkinter.Tcl_Obj | Widget, + stopindex: str | float | _tkinter.Tcl_Obj | Widget | None = None, + *, + forwards: bool | None = None, + backwards: bool | None = None, + exact: bool | None = None, + regexp: bool | None = None, + nocase: bool | None = None, + count: Variable | None = None, + elide: bool | None = None, + nolinestop: bool | None = None, + overlap: bool | None = None, + strictlimits: bool | None = None, + ) -> tuple[_tkinter.Tcl_Obj, ...]: ... + else: + def search( + self, + pattern: str, + index: str | float | _tkinter.Tcl_Obj | Widget, + stopindex: str | float | _tkinter.Tcl_Obj | Widget | None = None, + forwards: bool | None = None, + backwards: bool | None = None, + exact: bool | None = None, + regexp: bool | None = None, + nocase: bool | None = None, + count: Variable | None = None, + elide: bool | None = None, + ) -> str: ... # returns empty string for not found + def see(self, index: str | float | _tkinter.Tcl_Obj | Widget) -> None: ... def tag_add( self, tagName: str, index1: str | float | _tkinter.Tcl_Obj | Widget, *args: str | float | _tkinter.Tcl_Obj | Widget ) -> None: ... + # tag_bind stuff is very similar to Canvas @overload def tag_bind( @@ -3610,9 +3772,11 @@ class Text(Widget, XView, YView): ) -> str: ... @overload def tag_bind(self, tagName: str, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + def tag_unbind(self, tagName: str, sequence: str, funcid: str | None = None) -> None: ... # allowing any string for cget instead of just Literals because there's no other way to look up tag options def tag_cget(self, tagName: str, option: str): ... + @overload def tag_configure( self, @@ -3650,6 +3814,7 @@ class Text(Widget, XView, YView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def tag_configure(self, tagName: str, cnf: str) -> tuple[str, str, str, Any, Any]: ... + tag_config = tag_configure def tag_delete(self, first_tag_name: str, /, *tagNames: str) -> None: ... # error if no tag names given def tag_lower(self, tagName: str, belowThis: str | None = None) -> None: ... @@ -3675,6 +3840,7 @@ class Text(Widget, XView, YView): index1: str | float | _tkinter.Tcl_Obj | Widget, index2: str | float | _tkinter.Tcl_Obj | Widget | None = None, ) -> None: ... + @overload def window_cget(self, index: str | float | _tkinter.Tcl_Obj | Widget, option: Literal["padx", "pady"]) -> int: ... @overload @@ -3689,6 +3855,7 @@ class Text(Widget, XView, YView): def window_cget(self, index: str | float | _tkinter.Tcl_Obj | Widget, option: Literal["create", "window"]) -> str: ... @overload def window_cget(self, index: str | float | _tkinter.Tcl_Obj | Widget, option: str) -> Any: ... + @overload def window_configure( self, index: str | float | _tkinter.Tcl_Obj | Widget, cnf: str @@ -3706,6 +3873,7 @@ class Text(Widget, XView, YView): stretch: bool | Literal[0, 1] = ..., window: Misc | str = ..., ) -> dict[str, tuple[str, str, str, str, str | int]] | None: ... + window_config = window_configure def window_create( self, @@ -3729,16 +3897,27 @@ class _setit: # manual page: tk_optionMenu class OptionMenu(Menubutton): menuname: Incomplete - def __init__( - # differs from other widgets - self, - master: Misc | None, - variable: StringVar, - value: str, - *values: str, - # kwarg only from now on - command: Callable[[StringVar], object] | None = ..., - ) -> None: ... + if sys.version_info >= (3, 14): + def __init__( + # differs from other widgets + self, + master: Misc | None, + variable: StringVar, + value: str, + *values: str, + command: Callable[[StringVar], object] | None = ..., + name: str | None = None, + ) -> None: ... + else: + def __init__( + # differs from other widgets + self, + master: Misc | None, + variable: StringVar, + value: str, + *values: str, + command: Callable[[StringVar], object] | None = ..., + ) -> None: ... # configure, config, cget are inherited from Menubutton # destroy and __getitem__ are overridden, signature does not change @@ -3807,8 +3986,8 @@ class PhotoImage(Image, _PhotoImageLike): zoom: int | tuple[int, int] | list[int] | None = None, subsample: int | tuple[int, int] | list[int] | None = None, ) -> PhotoImage: ... - def subsample(self, x: int, y: Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... - def zoom(self, x: int, y: Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... + def subsample(self, x: int, y: int | Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... + def zoom(self, x: int, y: int | Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... def copy_replace( self, sourceImage: PhotoImage | str, @@ -3860,6 +4039,7 @@ class PhotoImage(Image, _PhotoImageLike): background: str | None = None, grayscale: bool = False, ) -> None: ... + @overload def data( self, format: str, *, from_coords: Iterable[int] | None = None, background: str | None = None, grayscale: bool = False @@ -3960,6 +4140,7 @@ class Spinbox(Widget, XView): wrap: bool = False, xscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload def configure( self, @@ -4018,6 +4199,7 @@ class Spinbox(Widget, XView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def bbox(self, index) -> tuple[int, int, int, int] | None: ... # type: ignore[override] def delete(self, first, last=None) -> Literal[""]: ... @@ -4074,6 +4256,7 @@ class LabelFrame(Widget): visual: str | tuple[str, int] = "", # can't be changed with configure() width: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -4103,6 +4286,7 @@ class LabelFrame(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class PanedWindow(Widget): @@ -4134,6 +4318,7 @@ class PanedWindow(Widget): showhandle: bool = False, width: float | str = "", ) -> None: ... + @overload def configure( self, @@ -4163,6 +4348,7 @@ class PanedWindow(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def add(self, child: Widget, **kw) -> None: ... def remove(self, child) -> None: ... diff --git a/mypy/typeshed/stdlib/tkinter/font.pyi b/mypy/typeshed/stdlib/tkinter/font.pyi index 327ba7a2432e0..d9e0b13155909 100644 --- a/mypy/typeshed/stdlib/tkinter/font.pyi +++ b/mypy/typeshed/stdlib/tkinter/font.pyi @@ -1,9 +1,8 @@ import _tkinter import itertools -import sys import tkinter -from typing import Any, ClassVar, Final, Literal, TypedDict, overload, type_check_only -from typing_extensions import TypeAlias, Unpack +from typing import Any, ClassVar, Final, Literal, TypeAlias, TypedDict, overload, type_check_only +from typing_extensions import Unpack __all__ = ["NORMAL", "ROMAN", "BOLD", "ITALIC", "nametofont", "Font", "families", "names"] @@ -61,6 +60,7 @@ class Font: ) -> None: ... __hash__: ClassVar[None] # type: ignore[assignment] def __setitem__(self, key: str, value: Any) -> None: ... + @overload def cget(self, option: Literal["family"]) -> str: ... @overload @@ -73,7 +73,9 @@ class Font: def cget(self, option: Literal["underline", "overstrike"]) -> bool: ... @overload def cget(self, option: str) -> Any: ... + __getitem__ = cget + @overload def actual(self, option: Literal["family"], displayof: tkinter.Misc | None = None) -> str: ... @overload @@ -88,6 +90,7 @@ class Font: def actual(self, option: None, displayof: tkinter.Misc | None = None) -> _FontDict: ... @overload def actual(self, *, displayof: tkinter.Misc | None = None) -> _FontDict: ... + def config( self, *, @@ -100,21 +103,18 @@ class Font: ) -> _FontDict | None: ... configure = config def copy(self) -> Font: ... + @overload def metrics(self, option: Literal["ascent", "descent", "linespace"], /, *, displayof: tkinter.Misc | None = ...) -> int: ... @overload def metrics(self, option: Literal["fixed"], /, *, displayof: tkinter.Misc | None = ...) -> bool: ... @overload def metrics(self, *, displayof: tkinter.Misc | None = ...) -> _MetricsDict: ... + def measure(self, text: str, displayof: tkinter.Misc | None = None) -> int: ... def __eq__(self, other: object) -> bool: ... def __del__(self) -> None: ... def families(root: tkinter.Misc | None = None, displayof: tkinter.Misc | None = None) -> tuple[str, ...]: ... def names(root: tkinter.Misc | None = None) -> tuple[str, ...]: ... - -if sys.version_info >= (3, 10): - def nametofont(name: str, root: tkinter.Misc | None = None) -> Font: ... - -else: - def nametofont(name: str) -> Font: ... +def nametofont(name: str, root: tkinter.Misc | None = None) -> Font: ... diff --git a/mypy/typeshed/stdlib/tkinter/simpledialog.pyi b/mypy/typeshed/stdlib/tkinter/simpledialog.pyi index 45dce21a6b1c3..6f66f0237b458 100644 --- a/mypy/typeshed/stdlib/tkinter/simpledialog.pyi +++ b/mypy/typeshed/stdlib/tkinter/simpledialog.pyi @@ -1,5 +1,9 @@ +import sys from tkinter import Event, Frame, Misc, Toplevel +if sys.version_info >= (3, 15): + __all__ = ["SimpleDialog", "Dialog", "askinteger", "askfloat", "askstring"] + class Dialog(Toplevel): def __init__(self, parent: Misc | None, title: str | None = None) -> None: ... def body(self, master: Frame) -> Misc | None: ... diff --git a/mypy/typeshed/stdlib/tkinter/ttk.pyi b/mypy/typeshed/stdlib/tkinter/ttk.pyi index 0a6837201035c..06d2be13dc283 100644 --- a/mypy/typeshed/stdlib/tkinter/ttk.pyi +++ b/mypy/typeshed/stdlib/tkinter/ttk.pyi @@ -4,8 +4,8 @@ import tkinter from _typeshed import MaybeNone from collections.abc import Callable, Iterable, Sequence from tkinter.font import _FontDescription -from typing import Any, Literal, TypedDict, TypeVar, overload, type_check_only -from typing_extensions import Never, ParamSpec, TypeAlias, Unpack +from typing import Any, Literal, ParamSpec, TypeAlias, TypedDict, TypeVar, overload, type_check_only +from typing_extensions import Never, Unpack __all__ = [ "Button", @@ -123,6 +123,7 @@ class Style: master: tkinter.Misc tk: _tkinter.TkappType def __init__(self, master: tkinter.Misc | None = None) -> None: ... + # For these methods, values given vary between options. Returned values # seem to be str, but this might not always be the case. @overload @@ -131,15 +132,19 @@ class Style: def configure(self, style: str, query_opt: str, **kw: Any) -> Any: ... @overload def configure(self, style: str, query_opt: None = None, **kw: Any) -> None: ... + @overload def map(self, style: str, query_opt: str) -> _Statespec: ... @overload def map(self, style: str, query_opt: None = None, **kw: Iterable[_Statespec]) -> dict[str, _Statespec]: ... + def lookup(self, style: str, option: str, state: Iterable[str] | None = None, default: Any | None = None) -> Any: ... + @overload def layout(self, style: str, layoutspec: _LayoutSpec) -> list[Never]: ... # Always seems to return an empty list @overload def layout(self, style: str, layoutspec: None = None) -> _LayoutSpec: ... + @overload def element_create( self, @@ -203,6 +208,7 @@ class Style: def theme_create(self, themename: str, parent: str | None = None, settings: _ThemeSettings | None = None) -> None: ... def theme_settings(self, themename: str, settings: _ThemeSettings) -> None: ... def theme_names(self) -> tuple[str, ...]: ... + @overload def theme_use(self, themename: str) -> None: ... @overload @@ -211,12 +217,14 @@ class Style: class Widget(tkinter.Widget): def __init__(self, master: tkinter.Misc | None, widgetname: str | None, kw: dict[str, Any] | None = None) -> None: ... def identify(self, x: int, y: int) -> str: ... + @overload def instate(self, statespec: Sequence[str], callback: None = None) -> bool: ... @overload def instate( self, statespec: Sequence[str], callback: Callable[_P, _T], *args: _P.args, **kw: _P.kwargs ) -> Literal[False] | _T: ... + def state(self, statespec: Sequence[str] | None = None) -> tuple[str, ...]: ... class Button(Widget): @@ -240,6 +248,7 @@ class Button(Widget): underline: int = -1, width: int | Literal[""] = "", ) -> None: ... + @overload def configure( self, @@ -261,6 +270,7 @@ class Button(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def invoke(self) -> Any: ... @@ -290,6 +300,7 @@ class Checkbutton(Widget): variable: tkinter.Variable = ..., width: int | Literal[""] = "", ) -> None: ... + @overload def configure( self, @@ -313,6 +324,7 @@ class Checkbutton(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def invoke(self) -> Any: ... @@ -341,6 +353,7 @@ class Entry(Widget, tkinter.Entry): width: int = 20, xscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload # type: ignore[override] def configure( self, @@ -365,6 +378,7 @@ class Entry(Widget, tkinter.Entry): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + # config must be copy/pasted, otherwise ttk.Entry().config is mypy error (don't know why) @overload # type: ignore[override] def config( @@ -390,6 +404,7 @@ class Entry(Widget, tkinter.Entry): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def config(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + def bbox(self, index) -> tuple[int, int, int, int]: ... # type: ignore[override] def identify(self, x: int, y: int) -> str: ... def validate(self): ... @@ -421,6 +436,7 @@ class Combobox(Entry): width: int = 20, xscrollcommand: str | Callable[[float, float], object] = ..., # undocumented ) -> None: ... + @overload # type: ignore[override] def configure( self, @@ -448,6 +464,7 @@ class Combobox(Entry): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + # config must be copy/pasted, otherwise ttk.Combobox().config is mypy error (don't know why) @overload # type: ignore[override] def config( @@ -476,6 +493,7 @@ class Combobox(Entry): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def config(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + def current(self, newindex: int | None = None) -> int: ... def set(self, value: Any) -> None: ... @@ -498,6 +516,7 @@ class Frame(Widget): takefocus: bool | Literal[0, 1, ""] | Callable[[str], bool | None] = "", width: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -515,6 +534,7 @@ class Frame(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Label(Widget): @@ -545,6 +565,7 @@ class Label(Widget): width: int | Literal[""] = "", wraplength: float | str = ..., ) -> None: ... + @overload def configure( self, @@ -573,6 +594,7 @@ class Label(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Labelframe(Widget): @@ -596,6 +618,7 @@ class Labelframe(Widget): underline: int = -1, width: float | str = 0, ) -> None: ... + @overload def configure( self, @@ -617,6 +640,7 @@ class Labelframe(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure LabelFrame = Labelframe @@ -642,6 +666,7 @@ class Menubutton(Widget): underline: int = -1, width: int | Literal[""] = "", ) -> None: ... + @overload def configure( self, @@ -663,6 +688,7 @@ class Menubutton(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Notebook(Widget): @@ -679,6 +705,7 @@ class Notebook(Widget): takefocus: bool | Literal[0, 1, ""] | Callable[[str], bool | None] = ..., width: int = 0, ) -> None: ... + @overload def configure( self, @@ -693,6 +720,7 @@ class Notebook(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def add( self, @@ -734,6 +762,7 @@ class Panedwindow(Widget, tkinter.PanedWindow): width: int = 0, ) -> None: ... def add(self, child: tkinter.Widget, *, weight: int = ..., **kw) -> None: ... + @overload # type: ignore[override] def configure( self, @@ -747,6 +776,7 @@ class Panedwindow(Widget, tkinter.PanedWindow): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + # config must be copy/pasted, otherwise ttk.Panedwindow().config is mypy error (don't know why) @overload # type: ignore[override] def config( @@ -761,6 +791,7 @@ class Panedwindow(Widget, tkinter.PanedWindow): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def config(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + forget = tkinter.PanedWindow.forget def insert(self, pos, child, **kw) -> None: ... def pane(self, pane, option=None, **kw): ... @@ -786,6 +817,7 @@ class Progressbar(Widget): value: float = 0.0, variable: tkinter.IntVar | tkinter.DoubleVar = ..., ) -> None: ... + @overload def configure( self, @@ -804,6 +836,7 @@ class Progressbar(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def start(self, interval: Literal["idle"] | int | None = None) -> None: ... def step(self, amount: float | None = None) -> None: ... @@ -831,6 +864,7 @@ class Radiobutton(Widget): variable: tkinter.Variable | Literal[""] = ..., width: int | Literal[""] = "", ) -> None: ... + @overload def configure( self, @@ -853,6 +887,7 @@ class Radiobutton(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def invoke(self) -> Any: ... @@ -876,6 +911,7 @@ class Scale(Widget, tkinter.Scale): # type: ignore[misc] value: float = 0, variable: tkinter.IntVar | tkinter.DoubleVar = ..., ) -> None: ... + @overload # type: ignore[override] def configure( self, @@ -895,6 +931,7 @@ class Scale(Widget, tkinter.Scale): # type: ignore[misc] ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + # config must be copy/pasted, otherwise ttk.Scale().config is mypy error (don't know why) @overload # type: ignore[override] def config( @@ -915,6 +952,7 @@ class Scale(Widget, tkinter.Scale): # type: ignore[misc] ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def config(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + def get(self, x: int | None = None, y: int | None = None) -> float: ... # type ignore, because identify() methods of Widget and tkinter.Scale are incompatible @@ -931,6 +969,7 @@ class Scrollbar(Widget, tkinter.Scrollbar): # type: ignore[misc] style: str = "", takefocus: bool | Literal[0, 1, ""] | Callable[[str], bool | None] = "", ) -> None: ... + @overload # type: ignore[override] def configure( self, @@ -944,6 +983,7 @@ class Scrollbar(Widget, tkinter.Scrollbar): # type: ignore[misc] ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + # config must be copy/pasted, otherwise ttk.Scrollbar().config is mypy error (don't know why) @overload # type: ignore[override] def config( @@ -971,6 +1011,7 @@ class Separator(Widget): style: str = "", takefocus: bool | Literal[0, 1, ""] | Callable[[str], bool | None] = "", ) -> None: ... + @overload def configure( self, @@ -983,6 +1024,7 @@ class Separator(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Sizegrip(Widget): @@ -996,6 +1038,7 @@ class Sizegrip(Widget): style: str = "", takefocus: bool | Literal[0, 1, ""] | Callable[[str], bool | None] = "", ) -> None: ... + @overload def configure( self, @@ -1007,6 +1050,7 @@ class Sizegrip(Widget): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure class Spinbox(Entry): @@ -1040,6 +1084,7 @@ class Spinbox(Entry): wrap: bool = False, xscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload # type: ignore[override] def configure( self, @@ -1071,6 +1116,7 @@ class Spinbox(Entry): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure # type: ignore[assignment] def set(self, value: Any) -> None: ... @@ -1129,6 +1175,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): xscrollcommand: str | Callable[[float, float], object] = "", yscrollcommand: str | Callable[[float, float], object] = "", ) -> None: ... + @overload def configure( self, @@ -1148,10 +1195,12 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... @overload def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure def bbox(self, item: str | int, column: str | int | None = None) -> tuple[int, int, int, int] | Literal[""]: ... # type: ignore[override] def get_children(self, item: str | int | None = None) -> tuple[str, ...]: ... def set_children(self, item: str | int, *newchildren: str | int) -> None: ... + @overload def column(self, column: str | int, option: Literal["width", "minwidth"]) -> int: ... @overload @@ -1174,13 +1223,16 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): anchor: Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] = ..., # id is read-only ) -> _TreeviewColumnDict | None: ... + def delete(self, *items: str | int) -> None: ... def detach(self, *items: str | int) -> None: ... def exists(self, item: str | int) -> bool: ... + @overload # type: ignore[override] def focus(self, item: None = None) -> str: ... # can return empty string @overload def focus(self, item: str | int) -> Literal[""]: ... + @overload def heading(self, column: str | int, option: Literal["text"]) -> str: ... @overload @@ -1204,6 +1256,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): anchor: Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] = ..., command: str | Callable[[], object] = ..., ) -> None: ... + # Internal Method. Leave untyped: def identify(self, component, x, y): ... # type: ignore[override] def identify_row(self, y: int) -> str: ... @@ -1224,6 +1277,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): open: bool = ..., tags: str | list[str] | tuple[str, ...] = ..., ) -> str: ... + @overload def item(self, item: str | int, option: Literal["text"]) -> str: ... @overload @@ -1250,6 +1304,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): open: bool = ..., tags: str | list[str] | tuple[str, ...] = ..., ) -> None: ... + def move(self, item: str | int, parent: str, index: int | Literal["end"]) -> None: ... reattach = move def next(self, item: str | int) -> str: ... # returning empty string means last item @@ -1257,28 +1312,34 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): def prev(self, item: str | int) -> str: ... # returning empty string means first item def see(self, item: str | int) -> None: ... def selection(self) -> tuple[str, ...]: ... + @overload def selection_set(self, items: list[str] | tuple[str, ...] | list[int] | tuple[int, ...], /) -> None: ... @overload def selection_set(self, *items: str | int) -> None: ... + @overload def selection_add(self, items: list[str] | tuple[str, ...] | list[int] | tuple[int, ...], /) -> None: ... @overload def selection_add(self, *items: str | int) -> None: ... + @overload def selection_remove(self, items: list[str] | tuple[str, ...] | list[int] | tuple[int, ...], /) -> None: ... @overload def selection_remove(self, *items: str | int) -> None: ... + @overload def selection_toggle(self, items: list[str] | tuple[str, ...] | list[int] | tuple[int, ...], /) -> None: ... @overload def selection_toggle(self, *items: str | int) -> None: ... + @overload def set(self, item: str | int, column: None = None, value: None = None) -> dict[str, Any]: ... @overload def set(self, item: str | int, column: str | int, value: None = None) -> Any: ... @overload def set(self, item: str | int, column: str | int, value: Any) -> Literal[""]: ... + # There's no tag_unbind() or 'add' argument for whatever reason. # Also, it's 'callback' instead of 'func' here. @overload @@ -1289,6 +1350,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): def tag_bind(self, tagname: str, sequence: str | None, callback: str) -> None: ... @overload def tag_bind(self, tagname: str, *, callback: str) -> None: ... + @overload def tag_configure(self, tagname: str, option: Literal["foreground", "background"]) -> str: ... @overload @@ -1307,6 +1369,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): font: _FontDescription = ..., image: tkinter._Image | str = ..., ) -> _TreeviewTagDict | MaybeNone: ... # can be None but annoying to check + @overload def tag_has(self, tagname: str, item: None = None) -> tuple[str, ...]: ... @overload @@ -1341,17 +1404,31 @@ class LabeledScale(Frame): value: Any class OptionMenu(Menubutton): - def __init__( - self, - master: tkinter.Misc | None, - variable: tkinter.StringVar, - default: str | None = None, - *values: str, - # rest of these are keyword-only because *args syntax used above - style: str = "", - direction: Literal["above", "below", "left", "right", "flush"] = "below", - command: Callable[[tkinter.StringVar], object] | None = None, - ) -> None: ... + if sys.version_info >= (3, 14): + def __init__( + self, + master: tkinter.Misc | None, + variable: tkinter.StringVar, + default: str | None = None, + *values: str, + # rest of these are keyword-only because *args syntax used above + style: str = "", + direction: Literal["above", "below", "left", "right", "flush"] = "below", + command: Callable[[tkinter.StringVar], object] | None = None, + name: str | None = None, + ) -> None: ... + else: + def __init__( + self, + master: tkinter.Misc | None, + variable: tkinter.StringVar, + default: str | None = None, + *values: str, + # rest of these are keyword-only because *args syntax used above + style: str = "", + direction: Literal["above", "below", "left", "right", "flush"] = "below", + command: Callable[[tkinter.StringVar], object] | None = None, + ) -> None: ... # configure, config, cget, destroy are inherited from Menubutton # destroy and __setitem__ are overridden, signature does not change def set_menu(self, default: str | None = None, *values: str) -> None: ... diff --git a/mypy/typeshed/stdlib/token.pyi b/mypy/typeshed/stdlib/token.pyi index fd1b10da1d12e..23b43250d60fe 100644 --- a/mypy/typeshed/stdlib/token.pyi +++ b/mypy/typeshed/stdlib/token.pyi @@ -57,6 +57,7 @@ __all__ = [ "SEMI", "SLASH", "SLASHEQUAL", + "SOFT_KEYWORD", "STAR", "STAREQUAL", "STRING", @@ -73,9 +74,6 @@ __all__ = [ if sys.version_info < (3, 13): __all__ += ["ASYNC", "AWAIT"] -if sys.version_info >= (3, 10): - __all__ += ["SOFT_KEYWORD"] - if sys.version_info >= (3, 12): __all__ += ["EXCLAMATION", "FSTRING_END", "FSTRING_MIDDLE", "FSTRING_START", "EXACT_TOKEN_TYPES"] @@ -150,8 +148,7 @@ TYPE_COMMENT: Final[int] TYPE_IGNORE: Final[int] COLONEQUAL: Final[int] EXACT_TOKEN_TYPES: Final[dict[str, int]] -if sys.version_info >= (3, 10): - SOFT_KEYWORD: Final[int] +SOFT_KEYWORD: Final[int] if sys.version_info >= (3, 12): EXCLAMATION: Final[int] diff --git a/mypy/typeshed/stdlib/tokenize.pyi b/mypy/typeshed/stdlib/tokenize.pyi index 0df8062d56891..0aa3947a178d5 100644 --- a/mypy/typeshed/stdlib/tokenize.pyi +++ b/mypy/typeshed/stdlib/tokenize.pyi @@ -3,8 +3,8 @@ from _typeshed import FileDescriptorOrPath from collections.abc import Callable, Generator, Iterable, Sequence from re import Pattern from token import * -from typing import Any, Final, NamedTuple, TextIO, type_check_only -from typing_extensions import TypeAlias, disjoint_base +from typing import Any, Final, NamedTuple, TextIO, TypeAlias, type_check_only +from typing_extensions import disjoint_base if sys.version_info < (3, 12): # Avoid double assignment to Final name by imports, which pyright objects to. @@ -71,6 +71,7 @@ __all__ = [ "SEMI", "SLASH", "SLASHEQUAL", + "SOFT_KEYWORD", "STAR", "STAREQUAL", "STRING", @@ -89,9 +90,6 @@ __all__ = [ if sys.version_info < (3, 13): __all__ += ["ASYNC", "AWAIT"] -if sys.version_info >= (3, 10): - __all__ += ["SOFT_KEYWORD"] - if sys.version_info >= (3, 12): __all__ += ["EXCLAMATION", "FSTRING_END", "FSTRING_MIDDLE", "FSTRING_START", "EXACT_TOKEN_TYPES"] diff --git a/mypy/typeshed/stdlib/tomllib.pyi b/mypy/typeshed/stdlib/tomllib.pyi index 4ff4097f8313a..7f6df1d9380ca 100644 --- a/mypy/typeshed/stdlib/tomllib.pyi +++ b/mypy/typeshed/stdlib/tomllib.pyi @@ -13,6 +13,7 @@ if sys.version_info >= (3, 14): pos: int lineno: int colno: int + @overload def __init__(self, msg: str, doc: str, pos: int) -> None: ... @overload diff --git a/mypy/typeshed/stdlib/trace.pyi b/mypy/typeshed/stdlib/trace.pyi index 7e7cc1e9ac54a..708233efe3e6a 100644 --- a/mypy/typeshed/stdlib/trace.pyi +++ b/mypy/typeshed/stdlib/trace.pyi @@ -2,8 +2,7 @@ import sys import types from _typeshed import Incomplete, StrPath, TraceFunction from collections.abc import Callable, Iterable, Mapping, Sequence -from typing import Any, TypeVar -from typing_extensions import ParamSpec, TypeAlias +from typing import Any, ParamSpec, TypeAlias, TypeVar __all__ = ["Trace", "CoverageResults"] diff --git a/mypy/typeshed/stdlib/traceback.pyi b/mypy/typeshed/stdlib/traceback.pyi index f9d88f25afd97..e5b410afdcbc0 100644 --- a/mypy/typeshed/stdlib/traceback.pyi +++ b/mypy/typeshed/stdlib/traceback.pyi @@ -2,8 +2,8 @@ import sys from _typeshed import SupportsWrite, Unused from collections.abc import Generator, Iterable, Iterator, Mapping from types import FrameType, TracebackType -from typing import Any, ClassVar, Literal, SupportsIndex, overload -from typing_extensions import Self, TypeAlias, deprecated +from typing import Any, ClassVar, Literal, SupportsIndex, TypeAlias, overload +from typing_extensions import Self, deprecated __all__ = [ "extract_stack", @@ -34,49 +34,32 @@ _FrameSummaryTuple: TypeAlias = tuple[str, int, str, str | None] def print_tb(tb: TracebackType | None, limit: int | None = None, file: SupportsWrite[str] | None = None) -> None: ... -if sys.version_info >= (3, 10): - @overload - def print_exception( - exc: type[BaseException] | None, - /, - value: BaseException | None = ..., - tb: TracebackType | None = ..., - limit: int | None = None, - file: SupportsWrite[str] | None = None, - chain: bool = True, - ) -> None: ... - @overload - def print_exception( - exc: BaseException, /, *, limit: int | None = None, file: SupportsWrite[str] | None = None, chain: bool = True - ) -> None: ... - @overload - def format_exception( - exc: type[BaseException] | None, - /, - value: BaseException | None = ..., - tb: TracebackType | None = ..., - limit: int | None = None, - chain: bool = True, - ) -> list[str]: ... - @overload - def format_exception(exc: BaseException, /, *, limit: int | None = None, chain: bool = True) -> list[str]: ... +@overload +def print_exception( + exc: type[BaseException] | None, + /, + value: BaseException | None = ..., + tb: TracebackType | None = ..., + limit: int | None = None, + file: SupportsWrite[str] | None = None, + chain: bool = True, +) -> None: ... +@overload +def print_exception( + exc: BaseException, /, *, limit: int | None = None, file: SupportsWrite[str] | None = None, chain: bool = True +) -> None: ... -else: - def print_exception( - etype: type[BaseException] | None, - value: BaseException | None, - tb: TracebackType | None, - limit: int | None = None, - file: SupportsWrite[str] | None = None, - chain: bool = True, - ) -> None: ... - def format_exception( - etype: type[BaseException] | None, - value: BaseException | None, - tb: TracebackType | None, - limit: int | None = None, - chain: bool = True, - ) -> list[str]: ... +@overload +def format_exception( + exc: type[BaseException] | None, + /, + value: BaseException | None = ..., + tb: TracebackType | None = ..., + limit: int | None = None, + chain: bool = True, +) -> list[str]: ... +@overload +def format_exception(exc: BaseException, /, *, limit: int | None = None, chain: bool = True) -> list[str]: ... def print_exc(limit: int | None = None, file: SupportsWrite[str] | None = None, chain: bool = True) -> None: ... def print_last(limit: int | None = None, file: SupportsWrite[str] | None = None, chain: bool = True) -> None: ... @@ -91,16 +74,12 @@ if sys.version_info >= (3, 13): def format_exception_only(exc: BaseException | None, /, *, show_group: bool = False) -> list[str]: ... @overload def format_exception_only(exc: Unused, /, value: BaseException | None, *, show_group: bool = False) -> list[str]: ... - -elif sys.version_info >= (3, 10): +else: @overload def format_exception_only(exc: BaseException | None, /) -> list[str]: ... @overload def format_exception_only(exc: Unused, /, value: BaseException | None) -> list[str]: ... -else: - def format_exception_only(etype: type[BaseException] | None, value: BaseException | None) -> list[str]: ... - def format_exc(limit: int | None = None, chain: bool = True) -> str: ... def format_tb(tb: TracebackType | None, limit: int | None = None) -> list[str]: ... def format_stack(f: FrameType | None = None, limit: int | None = None) -> list[str]: ... @@ -126,12 +105,10 @@ class TracebackException: # These fields only exist for `SyntaxError`s, but there is no way to express that in the type system. filename: str lineno: str | None - if sys.version_info >= (3, 10): - end_lineno: str | None + end_lineno: str | None text: str offset: int - if sys.version_info >= (3, 10): - end_offset: int | None + end_offset: int | None msg: str if sys.version_info >= (3, 13): @@ -173,19 +150,6 @@ class TracebackException: max_group_depth: int = 10, _seen: set[int] | None = None, ) -> None: ... - elif sys.version_info >= (3, 10): - def __init__( - self, - exc_type: type[BaseException], - exc_value: BaseException, - exc_traceback: TracebackType | None, - *, - limit: int | None = None, - lookup_lines: bool = True, - capture_locals: bool = False, - compact: bool = False, - _seen: set[int] | None = None, - ) -> None: ... else: def __init__( self, @@ -196,6 +160,7 @@ class TracebackException: limit: int | None = None, lookup_lines: bool = True, capture_locals: bool = False, + compact: bool = False, _seen: set[int] | None = None, ) -> None: ... @@ -212,7 +177,7 @@ class TracebackException: max_group_width: int = 15, max_group_depth: int = 10, ) -> Self: ... - elif sys.version_info >= (3, 10): + else: @classmethod def from_exception( cls, @@ -223,11 +188,6 @@ class TracebackException: capture_locals: bool = False, compact: bool = False, ) -> Self: ... - else: - @classmethod - def from_exception( - cls, exc: BaseException, *, limit: int | None = None, lookup_lines: bool = True, capture_locals: bool = False - ) -> Self: ... def __eq__(self, other: object) -> bool: ... __hash__: ClassVar[None] # type: ignore[assignment] @@ -296,6 +256,7 @@ class FrameSummary: locals: dict[str, str] | None @property def line(self) -> str | None: ... + @overload def __getitem__(self, pos: Literal[0]) -> str: ... @overload @@ -308,6 +269,7 @@ class FrameSummary: def __getitem__(self, pos: SupportsIndex) -> Any: ... @overload def __getitem__(self, pos: slice[SupportsIndex | None]) -> tuple[Any, ...]: ... + def __iter__(self) -> Iterator[Any]: ... def __eq__(self, other: object) -> bool: ... def __len__(self) -> Literal[4]: ... diff --git a/mypy/typeshed/stdlib/tracemalloc.pyi b/mypy/typeshed/stdlib/tracemalloc.pyi index 2a7ee0051af9c..e56aa19240fda 100644 --- a/mypy/typeshed/stdlib/tracemalloc.pyi +++ b/mypy/typeshed/stdlib/tracemalloc.pyi @@ -1,8 +1,7 @@ import sys from _tracemalloc import * from collections.abc import Sequence -from typing import Any, SupportsIndex, overload -from typing_extensions import TypeAlias +from typing import Any, SupportsIndex, TypeAlias, overload def get_object_traceback(obj: object) -> Traceback | None: ... def take_snapshot() -> Snapshot: ... @@ -92,10 +91,12 @@ class Traceback(Sequence[Frame]): def total_nframe(self) -> int | None: ... def __init__(self, frames: Sequence[_FrameTuple], total_nframe: int | None = None) -> None: ... def format(self, limit: int | None = None, most_recent_first: bool = False) -> list[str]: ... + @overload def __getitem__(self, index: SupportsIndex) -> Frame: ... @overload def __getitem__(self, index: slice[SupportsIndex | None]) -> Sequence[Frame]: ... + def __contains__(self, frame: Frame) -> bool: ... # type: ignore[override] def __len__(self) -> int: ... def __eq__(self, other: object) -> bool: ... diff --git a/mypy/typeshed/stdlib/tty.pyi b/mypy/typeshed/stdlib/tty.pyi index ca3f0013b20ec..a0478335a3f32 100644 --- a/mypy/typeshed/stdlib/tty.pyi +++ b/mypy/typeshed/stdlib/tty.pyi @@ -1,7 +1,6 @@ import sys import termios -from typing import IO, Final -from typing_extensions import TypeAlias +from typing import IO, Final, TypeAlias if sys.platform != "win32": __all__ = ["setraw", "setcbreak"] diff --git a/mypy/typeshed/stdlib/turtle.pyi b/mypy/typeshed/stdlib/turtle.pyi index b5f536d0e28e5..b1f5198a89c06 100644 --- a/mypy/typeshed/stdlib/turtle.pyi +++ b/mypy/typeshed/stdlib/turtle.pyi @@ -3,8 +3,8 @@ from _typeshed import StrPath from collections.abc import Callable, Generator, Sequence from contextlib import contextmanager from tkinter import Canvas, Frame, Misc, PhotoImage, Scrollbar -from typing import Any, ClassVar, Literal, TypedDict, overload, type_check_only -from typing_extensions import Self, TypeAlias, deprecated, disjoint_base +from typing import Any, ClassVar, Literal, TypeAlias, TypedDict, overload, type_check_only +from typing_extensions import Self, deprecated, disjoint_base __all__ = [ "ScrolledCanvas", @@ -167,10 +167,12 @@ if sys.version_info >= (3, 12): class Vec2D(tuple[float, float]): def __new__(cls, x: float, y: float) -> Self: ... def __add__(self, other: tuple[float, float]) -> Vec2D: ... # type: ignore[override] + @overload # type: ignore[override] def __mul__(self, other: Vec2D) -> float: ... @overload def __mul__(self, other: float) -> Vec2D: ... + def __rmul__(self, other: float) -> Vec2D: ... # type: ignore[override] def __sub__(self, other: tuple[float, float]) -> Vec2D: ... def __neg__(self) -> Vec2D: ... @@ -182,10 +184,12 @@ else: class Vec2D(tuple[float, float]): def __new__(cls, x: float, y: float) -> Self: ... def __add__(self, other: tuple[float, float]) -> Vec2D: ... # type: ignore[override] + @overload # type: ignore[override] def __mul__(self, other: Vec2D) -> float: ... @overload def __mul__(self, other: float) -> Vec2D: ... + def __rmul__(self, other: float) -> Vec2D: ... # type: ignore[override] def __sub__(self, other: tuple[float, float]) -> Vec2D: ... def __neg__(self) -> Vec2D: ... @@ -231,32 +235,40 @@ class TurtleScreen(TurtleScreenBase): self, cv: Canvas, mode: Literal["standard", "logo", "world"] = "standard", colormode: float = 1.0, delay: int = 10 ) -> None: ... def clear(self) -> None: ... + @overload def mode(self, mode: None = None) -> str: ... @overload def mode(self, mode: Literal["standard", "logo", "world"]) -> None: ... + def setworldcoordinates(self, llx: float, lly: float, urx: float, ury: float) -> None: ... def register_shape(self, name: str, shape: _PolygonCoords | Shape | None = None) -> None: ... + @overload def colormode(self, cmode: None = None) -> float: ... @overload def colormode(self, cmode: float) -> None: ... + def reset(self) -> None: ... def turtles(self) -> list[Turtle]: ... + @overload def bgcolor(self) -> _AnyColor: ... @overload def bgcolor(self, color: _Color) -> None: ... @overload def bgcolor(self, r: float, g: float, b: float) -> None: ... + @overload def tracer(self, n: None = None) -> int: ... @overload def tracer(self, n: int, delay: int | None = None) -> None: ... + @overload def delay(self, delay: None = None) -> int: ... @overload def delay(self, delay: int) -> None: ... + if sys.version_info >= (3, 14): @contextmanager def no_animation(self) -> Generator[None]: ... @@ -270,15 +282,18 @@ class TurtleScreen(TurtleScreenBase): def onkey(self, fun: Callable[[], object], key: str) -> None: ... def listen(self, xdummy: float | None = None, ydummy: float | None = None) -> None: ... def ontimer(self, fun: Callable[[], object], t: int = 0) -> None: ... + @overload def bgpic(self, picname: None = None) -> str: ... @overload def bgpic(self, picname: str) -> None: ... + @overload def screensize(self, canvwidth: None = None, canvheight: None = None, bg: None = None) -> tuple[int, int]: ... # Looks like if self.cv is not a ScrolledCanvas, this could return a tuple as well @overload def screensize(self, canvwidth: int, canvheight: int, bg: _Color | None = None) -> None: ... + if sys.version_info >= (3, 14): def save(self, filename: StrPath, *, overwrite: bool = False) -> None: ... onscreenclick = onclick @@ -307,21 +322,26 @@ class TNavigator: def pos(self) -> Vec2D: ... def xcor(self) -> float: ... def ycor(self) -> float: ... + @overload def goto(self, x: tuple[float, float], y: None = None) -> None: ... @overload def goto(self, x: float, y: float) -> None: ... + def home(self) -> None: ... def setx(self, x: float) -> None: ... def sety(self, y: float) -> None: ... + @overload def distance(self, x: TNavigator | tuple[float, float], y: None = None) -> float: ... @overload def distance(self, x: float, y: float) -> float: ... + @overload def towards(self, x: TNavigator | tuple[float, float], y: None = None) -> float: ... @overload def towards(self, x: float, y: float) -> float: ... + def heading(self) -> float: ... def setheading(self, to_angle: float) -> None: ... def circle(self, radius: float, extent: float | None = None, steps: int | None = None) -> None: ... @@ -338,33 +358,40 @@ class TNavigator: class TPen: def __init__(self, resizemode: Literal["auto", "user", "noresize"] = "noresize") -> None: ... + @overload def resizemode(self, rmode: None = None) -> str: ... @overload def resizemode(self, rmode: Literal["auto", "user", "noresize"]) -> None: ... + @overload def pensize(self, width: None = None) -> int: ... @overload def pensize(self, width: int) -> None: ... + def penup(self) -> None: ... def pendown(self) -> None: ... def isdown(self) -> bool: ... + @overload def speed(self, speed: None = None) -> int: ... @overload def speed(self, speed: _Speed) -> None: ... + @overload def pencolor(self) -> _AnyColor: ... @overload def pencolor(self, color: _Color) -> None: ... @overload def pencolor(self, r: float, g: float, b: float) -> None: ... + @overload def fillcolor(self) -> _AnyColor: ... @overload def fillcolor(self, color: _Color) -> None: ... @overload def fillcolor(self, r: float, g: float, b: float) -> None: ... + @overload def color(self) -> tuple[_AnyColor, _AnyColor]: ... @overload @@ -373,12 +400,14 @@ class TPen: def color(self, r: float, g: float, b: float) -> None: ... @overload def color(self, color1: _Color, color2: _Color) -> None: ... + if sys.version_info >= (3, 12): def teleport(self, x: float | None = None, y: float | None = None, *, fill_gap: bool = False) -> None: ... def showturtle(self) -> None: ... def hideturtle(self) -> None: ... def isvisible(self) -> bool: ... + # Note: signatures 1 and 2 overlap unsafely when no arguments are provided @overload def pen(self) -> _PenState: ... @@ -398,6 +427,7 @@ class TPen: outline: int = ..., tilt: float = ..., ) -> None: ... + width = pensize up = penup pu = penup @@ -421,10 +451,12 @@ class RawTurtle(TPen, TNavigator): # type: ignore[misc] # Conflicting methods def undobufferentries(self) -> int: ... def clear(self) -> None: ... def clone(self) -> Self: ... + @overload def shape(self, name: None = None) -> str: ... @overload def shape(self, name: str) -> None: ... + # Unsafely overlaps when no arguments are provided @overload def shapesize(self) -> tuple[float, float, float]: ... @@ -432,10 +464,12 @@ class RawTurtle(TPen, TNavigator): # type: ignore[misc] # Conflicting methods def shapesize( self, stretch_wid: float | None = None, stretch_len: float | None = None, outline: float | None = None ) -> None: ... + @overload def shearfactor(self, shear: None = None) -> float: ... @overload def shearfactor(self, shear: float) -> None: ... + # Unsafely overlaps when no arguments are provided @overload def shapetransform(self) -> tuple[float, float, float, float]: ... @@ -443,6 +477,7 @@ class RawTurtle(TPen, TNavigator): # type: ignore[misc] # Conflicting methods def shapetransform( self, t11: float | None = None, t12: float | None = None, t21: float | None = None, t22: float | None = None ) -> None: ... + def get_shapepoly(self) -> _PolygonCoords | None: ... if sys.version_info < (3, 13): @@ -453,6 +488,7 @@ class RawTurtle(TPen, TNavigator): # type: ignore[misc] # Conflicting methods def tiltangle(self, angle: None = None) -> float: ... @overload def tiltangle(self, angle: float) -> None: ... + def tilt(self, angle: float) -> None: ... # Can return either 'int' or Tuple[int, ...] based on if the stamp is # a compound stamp or not. So, as per the "no Union return" policy, @@ -467,12 +503,14 @@ class RawTurtle(TPen, TNavigator): # type: ignore[misc] # Conflicting methods def begin_fill(self) -> None: ... def end_fill(self) -> None: ... + @overload def dot(self, size: int | _Color | None = None) -> None: ... @overload def dot(self, size: int | None, color: _Color, /) -> None: ... @overload def dot(self, size: int | None, r: float, g: float, b: float, /) -> None: ... + def write( self, arg: object, move: bool = False, align: str = "left", font: tuple[str, int, str] = ("Arial", 8, "normal") ) -> None: ... @@ -525,28 +563,35 @@ def numinput( # Functions copied from TurtleScreen: def clear() -> None: ... + @overload def mode(mode: None = None) -> str: ... @overload def mode(mode: Literal["standard", "logo", "world"]) -> None: ... + def setworldcoordinates(llx: float, lly: float, urx: float, ury: float) -> None: ... def register_shape(name: str, shape: _PolygonCoords | Shape | None = None) -> None: ... + @overload def colormode(cmode: None = None) -> float: ... @overload def colormode(cmode: float) -> None: ... + def reset() -> None: ... def turtles() -> list[Turtle]: ... + @overload def bgcolor() -> _AnyColor: ... @overload def bgcolor(color: _Color) -> None: ... @overload def bgcolor(r: float, g: float, b: float) -> None: ... + @overload def tracer(n: None = None) -> int: ... @overload def tracer(n: int, delay: int | None = None) -> None: ... + @overload def delay(delay: None = None) -> int: ... @overload @@ -565,10 +610,12 @@ def onclick(fun: Callable[[float, float], object], btn: int = 1, add: bool | Non def onkey(fun: Callable[[], object], key: str) -> None: ... def listen(xdummy: float | None = None, ydummy: float | None = None) -> None: ... def ontimer(fun: Callable[[], object], t: int = 0) -> None: ... + @overload def bgpic(picname: None = None) -> str: ... @overload def bgpic(picname: str) -> None: ... + @overload def screensize(canvwidth: None = None, canvheight: None = None, bg: None = None) -> tuple[int, int]: ... @overload @@ -605,21 +652,26 @@ def left(angle: float) -> None: ... def pos() -> Vec2D: ... def xcor() -> float: ... def ycor() -> float: ... + @overload def goto(x: tuple[float, float], y: None = None) -> None: ... @overload def goto(x: float, y: float) -> None: ... + def home() -> None: ... def setx(x: float) -> None: ... def sety(y: float) -> None: ... + @overload def distance(x: TNavigator | tuple[float, float], y: None = None) -> float: ... @overload def distance(x: float, y: float) -> float: ... + @overload def towards(x: TNavigator | tuple[float, float], y: None = None) -> float: ... @overload def towards(x: float, y: float) -> float: ... + def heading() -> float: ... def setheading(to_angle: float) -> None: ... def circle(radius: float, extent: float | None = None, steps: int | None = None) -> None: ... @@ -639,29 +691,35 @@ seth = setheading def resizemode(rmode: None = None) -> str: ... @overload def resizemode(rmode: Literal["auto", "user", "noresize"]) -> None: ... + @overload def pensize(width: None = None) -> int: ... @overload def pensize(width: int) -> None: ... + def penup() -> None: ... def pendown() -> None: ... def isdown() -> bool: ... + @overload def speed(speed: None = None) -> int: ... @overload def speed(speed: _Speed) -> None: ... + @overload def pencolor() -> _AnyColor: ... @overload def pencolor(color: _Color) -> None: ... @overload def pencolor(r: float, g: float, b: float) -> None: ... + @overload def fillcolor() -> _AnyColor: ... @overload def fillcolor(color: _Color) -> None: ... @overload def fillcolor(r: float, g: float, b: float) -> None: ... + @overload def color() -> tuple[_AnyColor, _AnyColor]: ... @overload @@ -670,6 +728,7 @@ def color(color: _Color) -> None: ... def color(r: float, g: float, b: float) -> None: ... @overload def color(color1: _Color, color2: _Color) -> None: ... + def showturtle() -> None: ... def hideturtle() -> None: ... def isvisible() -> bool: ... @@ -705,6 +764,7 @@ ht = hideturtle def setundobuffer(size: int | None) -> None: ... def undobufferentries() -> int: ... + @overload def shape(name: None = None) -> str: ... @overload @@ -718,6 +778,7 @@ if sys.version_info >= (3, 12): def shapesize() -> tuple[float, float, float]: ... @overload def shapesize(stretch_wid: float | None = None, stretch_len: float | None = None, outline: float | None = None) -> None: ... + @overload def shearfactor(shear: None = None) -> float: ... @overload @@ -730,6 +791,7 @@ def shapetransform() -> tuple[float, float, float, float]: ... def shapetransform( t11: float | None = None, t12: float | None = None, t21: float | None = None, t22: float | None = None ) -> None: ... + def get_shapepoly() -> _PolygonCoords | None: ... if sys.version_info < (3, 13): @@ -740,6 +802,7 @@ if sys.version_info < (3, 13): def tiltangle(angle: None = None) -> float: ... @overload def tiltangle(angle: float) -> None: ... + def tilt(angle: float) -> None: ... # Can return either 'int' or Tuple[int, ...] based on if the stamp is @@ -756,12 +819,14 @@ if sys.version_info >= (3, 14): def begin_fill() -> None: ... def end_fill() -> None: ... + @overload def dot(size: int | _Color | None = None) -> None: ... @overload def dot(size: int | None, color: _Color, /) -> None: ... @overload def dot(size: int | None, r: float, g: float, b: float, /) -> None: ... + def write(arg: object, move: bool = False, align: str = "left", font: tuple[str, int, str] = ("Arial", 8, "normal")) -> None: ... if sys.version_info >= (3, 14): diff --git a/mypy/typeshed/stdlib/types.pyi b/mypy/typeshed/stdlib/types.pyi index e26c9447d2f75..b9771ffc72dad 100644 --- a/mypy/typeshed/stdlib/types.pyi +++ b/mypy/typeshed/stdlib/types.pyi @@ -12,12 +12,13 @@ from collections.abc import ( Iterator, KeysView, Mapping, + MutableMapping, MutableSequence, ValuesView, ) from importlib.machinery import ModuleSpec -from typing import Any, ClassVar, Literal, TypeVar, final, overload -from typing_extensions import ParamSpec, Self, TypeAliasType, TypeVarTuple, deprecated, disjoint_base +from typing import Any, ClassVar, Literal, ParamSpec, TypeVar, final, overload +from typing_extensions import Self, TypeAliasType, TypeVarTuple, deprecated, disjoint_base if sys.version_info >= (3, 14): from _typeshed import AnnotateFunc @@ -50,17 +51,21 @@ __all__ = [ "resolve_bases", "CellType", "GenericAlias", + "EllipsisType", + "NoneType", + "NotImplementedType", + "UnionType", ] -if sys.version_info >= (3, 10): - __all__ += ["EllipsisType", "NoneType", "NotImplementedType", "UnionType"] - if sys.version_info >= (3, 12): __all__ += ["get_original_bases"] if sys.version_info >= (3, 13): __all__ += ["CapsuleType"] +if sys.version_info >= (3, 15): + __all__ += ["FrameLocalsProxyType", "LazyImportType"] + # Note, all classes "defined" here require special handling. _T1 = TypeVar("_T1") @@ -84,9 +89,8 @@ class FunctionType: if sys.version_info >= (3, 14): __annotate__: AnnotateFunc | None __kwdefaults__: dict[str, Any] | None - if sys.version_info >= (3, 10): - @property - def __builtins__(self) -> dict[str, Any]: ... + @property + def __builtins__(self) -> dict[str, Any]: ... if sys.version_info >= (3, 12): __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] @@ -112,6 +116,7 @@ class FunctionType: ) -> Self: ... def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + @overload def __get__(self, instance: None, owner: type, /) -> FunctionType: ... @overload @@ -149,22 +154,18 @@ class CodeType: def co_name(self) -> str: ... @property def co_firstlineno(self) -> int: ... - if sys.version_info >= (3, 10): + if sys.version_info < (3, 15): @property @deprecated("Deprecated since Python 3.10; will be removed in Python 3.15. Use `CodeType.co_lines()` instead.") def co_lnotab(self) -> bytes: ... - else: - @property - def co_lnotab(self) -> bytes: ... @property def co_freevars(self) -> tuple[str, ...]: ... @property def co_cellvars(self) -> tuple[str, ...]: ... - if sys.version_info >= (3, 10): - @property - def co_linetable(self) -> bytes: ... - def co_lines(self) -> Iterator[tuple[int, int, int | None]]: ... + @property + def co_linetable(self) -> bytes: ... + def co_lines(self) -> Iterator[tuple[int, int, int | None]]: ... if sys.version_info >= (3, 11): @property def co_exceptiontable(self) -> bytes: ... @@ -197,27 +198,6 @@ class CodeType: cellvars: tuple[str, ...] = ..., /, ) -> Self: ... - elif sys.version_info >= (3, 10): - def __new__( - cls, - argcount: int, - posonlyargcount: int, - kwonlyargcount: int, - nlocals: int, - stacksize: int, - flags: int, - codestring: bytes, - constants: tuple[object, ...], - names: tuple[str, ...], - varnames: tuple[str, ...], - filename: str, - name: str, - firstlineno: int, - linetable: bytes, - freevars: tuple[str, ...] = ..., - cellvars: tuple[str, ...] = ..., - /, - ) -> Self: ... else: def __new__( cls, @@ -234,7 +214,7 @@ class CodeType: filename: str, name: str, firstlineno: int, - lnotab: bytes, + linetable: bytes, freevars: tuple[str, ...] = ..., cellvars: tuple[str, ...] = ..., /, @@ -262,27 +242,6 @@ class CodeType: co_linetable: bytes = ..., co_exceptiontable: bytes = ..., ) -> Self: ... - elif sys.version_info >= (3, 10): - def replace( - self, - *, - co_argcount: int = -1, - co_posonlyargcount: int = -1, - co_kwonlyargcount: int = -1, - co_nlocals: int = -1, - co_stacksize: int = -1, - co_flags: int = -1, - co_firstlineno: int = -1, - co_code: bytes = ..., - co_consts: tuple[object, ...] = ..., - co_names: tuple[str, ...] = ..., - co_varnames: tuple[str, ...] = ..., - co_freevars: tuple[str, ...] = ..., - co_cellvars: tuple[str, ...] = ..., - co_filename: str = ..., - co_name: str = ..., - co_linetable: bytes = ..., - ) -> Self: ... else: def replace( self, @@ -302,7 +261,7 @@ class CodeType: co_cellvars: tuple[str, ...] = ..., co_filename: str = ..., co_name: str = ..., - co_lnotab: bytes = ..., + co_linetable: bytes = ..., ) -> Self: ... if sys.version_info >= (3, 13): @@ -320,12 +279,14 @@ class MappingProxyType(Mapping[_KT_co, _VT_co]): # type: ignore[type-var] # py def keys(self) -> KeysView[_KT_co]: ... def values(self) -> ValuesView[_VT_co]: ... def items(self) -> ItemsView[_KT_co, _VT_co]: ... + @overload def get(self, key: _KT_co, /) -> _VT_co | None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter @overload def get(self, key: _KT_co, default: _VT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter @overload def get(self, key: _KT_co, default: _T2, /) -> _VT_co | _T2: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... def __reversed__(self) -> Iterator[_KT_co]: ... def __or__(self, value: Mapping[_T1, _T2], /) -> dict[_KT_co | _T1, _VT_co | _T2]: ... @@ -408,17 +369,22 @@ class GeneratorType(Generator[_YieldT_co, _SendT_contra, _ReturnT_co]): if sys.version_info >= (3, 11): @property def gi_suspended(self) -> bool: ... + if sys.version_info >= (3, 15): + @property + def gi_state(self) -> Literal["GEN_CREATED", "GEN_SUSPENDED", "GEN_RUNNING", "GEN_CLOSED"]: ... __name__: str __qualname__: str def __iter__(self) -> Self: ... def __next__(self) -> _YieldT_co: ... def send(self, arg: _SendT_contra, /) -> _YieldT_co: ... + @overload def throw( self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / ) -> _YieldT_co: ... @overload def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... + if sys.version_info >= (3, 13): def __class_getitem__(cls, item: Any, /) -> Any: ... @@ -437,16 +403,21 @@ class AsyncGeneratorType(AsyncGenerator[_YieldT_co, _SendT_contra]): if sys.version_info >= (3, 12): @property def ag_suspended(self) -> bool: ... + if sys.version_info >= (3, 15): + @property + def ag_state(self) -> Literal["AGEN_CREATED", "AGEN_SUSPENDED", "AGEN_RUNNING", "AGEN_CLOSED"]: ... def __aiter__(self) -> Self: ... def __anext__(self) -> Coroutine[Any, Any, _YieldT_co]: ... def asend(self, val: _SendT_contra, /) -> Coroutine[Any, Any, _YieldT_co]: ... + @overload async def athrow( self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / ) -> _YieldT_co: ... @overload async def athrow(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... + def aclose(self) -> Coroutine[Any, Any, None]: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @@ -471,16 +442,21 @@ class CoroutineType(Coroutine[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co]): if sys.version_info >= (3, 11): @property def cr_suspended(self) -> bool: ... + if sys.version_info >= (3, 15): + @property + def cr_state(self) -> Literal["CORO_CREATED", "CORO_SUSPENDED", "CORO_RUNNING", "CORO_CLOSED"]: ... def close(self) -> None: ... def __await__(self) -> Generator[Any, None, _ReturnT_nd_co]: ... def send(self, arg: _SendT_nd_contra, /) -> _YieldT_co: ... + @overload def throw( self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / ) -> _YieldT_co: ... @overload def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... + if sys.version_info >= (3, 13): def __class_getitem__(cls, item: Any, /) -> Any: ... @@ -600,8 +576,14 @@ class FrameType: # An `int | None` annotation here causes too many false-positive errors, so applying `int | Any`. @property def f_lineno(self) -> int | MaybeNone: ... - @property - def f_locals(self) -> dict[str, Any]: ... + + if sys.version_info >= (3, 15): + @property + def f_locals(self) -> FrameLocalsProxyType | dict[str, Any]: ... + else: + @property + def f_locals(self) -> dict[str, Any]: ... + f_trace: Callable[[FrameType, str, Any], Any] | None f_trace_lines: bool f_trace_opcodes: bool @@ -610,6 +592,28 @@ class FrameType: @property def f_generator(self) -> GeneratorType[Any, Any, Any] | CoroutineType[Any, Any, Any] | None: ... +if sys.version_info >= (3, 15): + @final + class FrameLocalsProxyType(MutableMapping[str, Any]): + def __new__(cls, frame: FrameType, /) -> Self: ... + def __getitem__(self, key: str, /) -> Any: ... + def __setitem__(self, key: str, value: Any, /) -> None: ... + def __delitem__(self, key: str, /) -> None: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __contains__(self, key: object, /) -> bool: ... + def __reversed__(self) -> Iterator[str]: ... + def copy(self) -> dict[str, Any]: ... + def pop(self, key: str, default: Any = ..., /) -> Any: ... + def setdefault(self, key: str, default: Any = ..., /) -> Any: ... + def update(self, object: SupportsKeysAndGetItem[str, Any] | Iterable[tuple[str, Any]], /) -> None: ... # type: ignore[override] + + @final + class LazyImportType: + @property + def __name__(self) -> str: ... + def resolve(self) -> Any: ... + @final class GetSetDescriptorType: @property @@ -679,6 +683,7 @@ _P = ParamSpec("_P") def coroutine(func: Callable[_P, Generator[Any, Any, _R]]) -> Callable[_P, Awaitable[_R]]: ... @overload def coroutine(func: _Fn) -> _Fn: ... + @disjoint_base class GenericAlias: @property @@ -697,43 +702,42 @@ class GenericAlias: def __unpacked__(self) -> bool: ... @property def __typing_unpacked_tuple_args__(self) -> tuple[Any, ...] | None: ... - if sys.version_info >= (3, 10): - def __or__(self, value: Any, /) -> UnionType: ... - def __ror__(self, value: Any, /) -> UnionType: ... + + def __or__(self, value: Any, /) -> UnionType: ... + def __ror__(self, value: Any, /) -> UnionType: ... # GenericAlias delegates attr access to `__origin__` def __getattr__(self, name: str) -> Any: ... -if sys.version_info >= (3, 10): - @final - class NoneType: - def __bool__(self) -> Literal[False]: ... +@final +class NoneType: + def __bool__(self) -> Literal[False]: ... - @final - class EllipsisType: ... +@final +class EllipsisType: ... - @final - class NotImplementedType(Any): ... +@final +class NotImplementedType(Any): ... - @final - class UnionType: - @property - def __args__(self) -> tuple[Any, ...]: ... - @property - def __parameters__(self) -> tuple[Any, ...]: ... - # `(int | str) | Literal["foo"]` returns a generic alias to an instance of `_SpecialForm` (`Union`). - # Normally we'd express this using the return type of `_SpecialForm.__ror__`, - # but because `UnionType.__or__` accepts `Any`, type checkers will use - # the return type of `UnionType.__or__` to infer the result of this operation - # rather than `_SpecialForm.__ror__`. To mitigate this, we use `| Any` - # in the return type of `UnionType.__(r)or__`. - def __or__(self, value: Any, /) -> UnionType | Any: ... - def __ror__(self, value: Any, /) -> UnionType | Any: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - # you can only subscript a `UnionType` instance if at least one of the elements - # in the union is a generic alias instance that has a non-empty `__parameters__` - def __getitem__(self, parameters: Any, /) -> object: ... +@final +class UnionType: + @property + def __args__(self) -> tuple[Any, ...]: ... + @property + def __parameters__(self) -> tuple[Any, ...]: ... + # `(int | str) | Literal["foo"]` returns a generic alias to an instance of `_SpecialForm` (`Union`). + # Normally we'd express this using the return type of `_SpecialForm.__ror__`, + # but because `UnionType.__or__` accepts `Any`, type checkers will use + # the return type of `UnionType.__or__` to infer the result of this operation + # rather than `_SpecialForm.__ror__`. To mitigate this, we use `| Any` + # in the return type of `UnionType.__(r)or__`. + def __or__(self, value: Any, /) -> UnionType | Any: ... + def __ror__(self, value: Any, /) -> UnionType | Any: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + # you can only subscript a `UnionType` instance if at least one of the elements + # in the union is a generic alias instance that has a non-empty `__parameters__` + def __getitem__(self, parameters: Any, /) -> object: ... if sys.version_info >= (3, 13): @final diff --git a/mypy/typeshed/stdlib/typing.pyi b/mypy/typeshed/stdlib/typing.pyi index 0bced03866439..2379bec348a82 100644 --- a/mypy/typeshed/stdlib/typing.pyi +++ b/mypy/typeshed/stdlib/typing.pyi @@ -18,18 +18,16 @@ from types import ( MethodWrapperType, ModuleType, TracebackType, + UnionType, WrapperDescriptorType, ) -from typing_extensions import Never as _Never, ParamSpec as _ParamSpec, deprecated +from typing_extensions import Never as _Never, deprecated if sys.version_info >= (3, 14): from _typeshed import EvaluateFunc from annotationlib import Format -if sys.version_info >= (3, 10): - from types import UnionType - __all__ = [ "AbstractSet", "Annotated", @@ -41,11 +39,11 @@ __all__ = [ "AsyncIterator", "Awaitable", "BinaryIO", - "ByteString", "Callable", "ChainMap", "ClassVar", "Collection", + "Concatenate", "Container", "ContextManager", "Coroutine", @@ -77,6 +75,9 @@ __all__ = [ "NoReturn", "Optional", "OrderedDict", + "ParamSpec", + "ParamSpecArgs", + "ParamSpecKwargs", "Pattern", "Protocol", "Reversible", @@ -94,6 +95,8 @@ __all__ = [ "TextIO", "Tuple", "Type", + "TypeAlias", + "TypeGuard", "TypeVar", "TypedDict", "Union", @@ -104,17 +107,20 @@ __all__ = [ "get_args", "get_origin", "get_type_hints", + "is_typeddict", "no_type_check", - "no_type_check_decorator", "overload", "runtime_checkable", ] +if sys.version_info < (3, 15): + __all__ += ["ByteString", "no_type_check_decorator"] + if sys.version_info >= (3, 14): __all__ += ["evaluate_forward_ref"] -if sys.version_info >= (3, 10): - __all__ += ["Concatenate", "ParamSpec", "ParamSpecArgs", "ParamSpecKwargs", "TypeAlias", "TypeGuard", "is_typeddict"] +if sys.version_info >= (3, 15): + __all__ += ["NoExtraItems", "TypeForm", "disjoint_base"] if sys.version_info >= (3, 11): __all__ += [ @@ -149,6 +155,7 @@ class _Final: __slots__ = ("__weakref__",) def final(f: _T) -> _T: ... + @final class TypeVar: @property @@ -206,9 +213,9 @@ class TypeVar: covariant: bool = False, contravariant: bool = False, ) -> None: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any, /) -> _SpecialForm: ... # AnnotationForm - def __ror__(self, left: Any, /) -> _SpecialForm: ... # AnnotationForm + + def __or__(self, right: Any, /) -> _SpecialForm: ... # AnnotationForm + def __ror__(self, left: Any, /) -> _SpecialForm: ... # AnnotationForm if sys.version_info >= (3, 11): def __typing_subst__(self, arg: Any, /) -> Any: ... if sys.version_info >= (3, 13): @@ -227,9 +234,8 @@ class TypeVar: class _SpecialForm(_Final): __slots__ = ("_name", "__doc__", "_getitem") def __getitem__(self, parameters: Any) -> object: ... - if sys.version_info >= (3, 10): - def __or__(self, other: Any) -> _SpecialForm: ... - def __ror__(self, other: Any) -> _SpecialForm: ... + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... Union: _SpecialForm Protocol: _SpecialForm @@ -257,11 +263,31 @@ if sys.version_info >= (3, 11): class TypeVarTuple: @property def __name__(self) -> str: ... + if sys.version_info >= (3, 15): + @property + def __bound__(self) -> Any | None: ... # AnnotationForm + @property + def __covariant__(self) -> bool: ... + @property + def __contravariant__(self) -> bool: ... + @property + def __infer_variance__(self) -> bool: ... if sys.version_info >= (3, 13): @property def __default__(self) -> Any: ... # AnnotationForm def has_default(self) -> bool: ... - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 15): + def __new__( + cls, + name: str, + *, + bound: Any | None = None, # AnnotationForm + covariant: bool = False, + contravariant: bool = False, + default: Any = ..., # AnnotationForm + infer_variance: bool = False, + ) -> Self: ... + elif sys.version_info >= (3, 13): def __new__(cls, name: str, *, default: Any = ...) -> Self: ... # AnnotationForm elif sys.version_info >= (3, 12): def __new__(cls, name: str) -> Self: ... @@ -275,125 +301,111 @@ if sys.version_info >= (3, 11): @property def evaluate_default(self) -> EvaluateFunc | None: ... -if sys.version_info >= (3, 10): - @final - class ParamSpecArgs: - @property - def __origin__(self) -> ParamSpec: ... - if sys.version_info >= (3, 12): - def __new__(cls, origin: ParamSpec) -> Self: ... - else: - def __init__(self, origin: ParamSpec) -> None: ... - - def __eq__(self, other: object, /) -> bool: ... - __hash__: ClassVar[None] # type: ignore[assignment] +@final +class ParamSpecArgs: + @property + def __origin__(self) -> ParamSpec: ... + if sys.version_info >= (3, 12): + def __new__(cls, origin: ParamSpec) -> Self: ... + else: + def __init__(self, origin: ParamSpec) -> None: ... - @final - class ParamSpecKwargs: - @property - def __origin__(self) -> ParamSpec: ... - if sys.version_info >= (3, 12): - def __new__(cls, origin: ParamSpec) -> Self: ... - else: - def __init__(self, origin: ParamSpec) -> None: ... + def __eq__(self, other: object, /) -> bool: ... + __hash__: ClassVar[None] # type: ignore[assignment] - def __eq__(self, other: object, /) -> bool: ... - __hash__: ClassVar[None] # type: ignore[assignment] +@final +class ParamSpecKwargs: + @property + def __origin__(self) -> ParamSpec: ... + if sys.version_info >= (3, 12): + def __new__(cls, origin: ParamSpec) -> Self: ... + else: + def __init__(self, origin: ParamSpec) -> None: ... - @final - class ParamSpec: - @property - def __name__(self) -> str: ... - @property - def __bound__(self) -> Any | None: ... # AnnotationForm - @property - def __covariant__(self) -> bool: ... - @property - def __contravariant__(self) -> bool: ... - if sys.version_info >= (3, 12): - @property - def __infer_variance__(self) -> bool: ... - if sys.version_info >= (3, 13): - @property - def __default__(self) -> Any: ... # AnnotationForm - if sys.version_info >= (3, 13): - def __new__( - cls, - name: str, - *, - bound: Any | None = None, # AnnotationForm - contravariant: bool = False, - covariant: bool = False, - infer_variance: bool = False, - default: Any = ..., # AnnotationForm - ) -> Self: ... - elif sys.version_info >= (3, 12): - def __new__( - cls, - name: str, - *, - bound: Any | None = None, # AnnotationForm - contravariant: bool = False, - covariant: bool = False, - infer_variance: bool = False, - ) -> Self: ... - elif sys.version_info >= (3, 11): - def __new__( - cls, - name: str, - *, - bound: Any | None = None, # AnnotationForm - contravariant: bool = False, - covariant: bool = False, - ) -> Self: ... - else: - def __init__( - self, - name: str, - *, - bound: Any | None = None, # AnnotationForm - contravariant: bool = False, - covariant: bool = False, - ) -> None: ... + def __eq__(self, other: object, /) -> bool: ... + __hash__: ClassVar[None] # type: ignore[assignment] +@final +class ParamSpec: + @property + def __name__(self) -> str: ... + @property + def __bound__(self) -> Any | None: ... # AnnotationForm + @property + def __covariant__(self) -> bool: ... + @property + def __contravariant__(self) -> bool: ... + if sys.version_info >= (3, 12): @property - def args(self) -> ParamSpecArgs: ... + def __infer_variance__(self) -> bool: ... + if sys.version_info >= (3, 13): @property - def kwargs(self) -> ParamSpecKwargs: ... - if sys.version_info >= (3, 11): - def __typing_subst__(self, arg: Any, /) -> Any: ... - def __typing_prepare_subst__(self, alias: Any, args: Any, /) -> tuple[Any, ...]: ... + def __default__(self) -> Any: ... # AnnotationForm + if sys.version_info >= (3, 13): + def __new__( + cls, + name: str, + *, + bound: Any | None = None, # AnnotationForm + contravariant: bool = False, + covariant: bool = False, + infer_variance: bool = False, + default: Any = ..., # AnnotationForm + ) -> Self: ... + elif sys.version_info >= (3, 12): + def __new__( + cls, + name: str, + *, + bound: Any | None = None, # AnnotationForm + contravariant: bool = False, + covariant: bool = False, + infer_variance: bool = False, + ) -> Self: ... + elif sys.version_info >= (3, 11): + def __new__( + cls, name: str, *, bound: Any | None = None, contravariant: bool = False, covariant: bool = False # AnnotationForm + ) -> Self: ... + else: + def __init__( + self, name: str, *, bound: Any | None = None, contravariant: bool = False, covariant: bool = False # AnnotationForm + ) -> None: ... - def __or__(self, right: Any, /) -> _SpecialForm: ... - def __ror__(self, left: Any, /) -> _SpecialForm: ... - if sys.version_info >= (3, 13): - def has_default(self) -> bool: ... - if sys.version_info >= (3, 14): - @property - def evaluate_default(self) -> EvaluateFunc | None: ... + @property + def args(self) -> ParamSpecArgs: ... + @property + def kwargs(self) -> ParamSpecKwargs: ... + if sys.version_info >= (3, 11): + def __typing_subst__(self, arg: Any, /) -> Any: ... + def __typing_prepare_subst__(self, alias: Any, args: Any, /) -> tuple[Any, ...]: ... - Concatenate: _SpecialForm - TypeAlias: _SpecialForm - TypeGuard: _SpecialForm + def __or__(self, right: Any, /) -> _SpecialForm: ... + def __ror__(self, left: Any, /) -> _SpecialForm: ... + if sys.version_info >= (3, 13): + def has_default(self) -> bool: ... + if sys.version_info >= (3, 14): + @property + def evaluate_default(self) -> EvaluateFunc | None: ... - class NewType: - def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm - if sys.version_info >= (3, 11): - @staticmethod - def __call__(x: _T, /) -> _T: ... - else: - def __call__(self, x: _T) -> _T: ... +Concatenate: _SpecialForm +TypeAlias: _SpecialForm +TypeGuard: _SpecialForm - def __or__(self, other: Any) -> _SpecialForm: ... - def __ror__(self, other: Any) -> _SpecialForm: ... - __supertype__: type | NewType - __name__: str +class NewType: + def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm + if sys.version_info >= (3, 11): + @staticmethod + def __call__(x: _T, /) -> _T: ... + else: + def __call__(self, x: _T) -> _T: ... -else: - def NewType(name: str, tp: Any) -> Any: ... + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... + __supertype__: type | NewType + __name__: str _F = TypeVar("_F", bound=Callable[..., Any]) -_P = _ParamSpec("_P") +_P = ParamSpec("_P") _T = TypeVar("_T") _FT = TypeVar("_FT", bound=Callable[..., Any] | type) @@ -410,12 +422,12 @@ _TC = TypeVar("_TC", bound=type[object]) def overload(func: _F) -> _F: ... def no_type_check(arg: _F) -> _F: ... -if sys.version_info >= (3, 13): +if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.13; removed in Python 3.15.") def no_type_check_decorator(decorator: Callable[_P, _T]) -> Callable[_P, _T]: ... -else: - def no_type_check_decorator(decorator: Callable[_P, _T]) -> Callable[_P, _T]: ... +if sys.version_info >= (3, 15): + def disjoint_base(cls: _TC) -> _TC: ... # This itself is only available during type checking def type_check_only(func_or_cls: _FT) -> _FT: ... @@ -439,6 +451,13 @@ ChainMap = _Alias() OrderedDict = _Alias() Annotated: _SpecialForm +if sys.version_info >= (3, 15): + @type_check_only + class _NoExtraItemsType: ... + + NoExtraItems: _NoExtraItemsType + + TypeForm: _SpecialForm # Predefined type variables. AnyStr = TypeVar("AnyStr", str, bytes) # noqa: Y001 @@ -448,12 +467,8 @@ class _Generic: if sys.version_info < (3, 12): __slots__ = () - if sys.version_info >= (3, 10): - @classmethod - def __class_getitem__(cls, args: TypeVar | ParamSpec | tuple[TypeVar | ParamSpec, ...]) -> _Final: ... - else: - @classmethod - def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ... + @classmethod + def __class_getitem__(cls, args: TypeVar | ParamSpec | tuple[TypeVar | ParamSpec, ...]) -> _Final: ... Generic: type[_Generic] @@ -464,6 +479,7 @@ class _ProtocolMeta(ABCMeta): # Abstract base classes. def runtime_checkable(cls: _TC) -> _TC: ... + @runtime_checkable class SupportsInt(Protocol, metaclass=ABCMeta): __slots__ = () @@ -503,6 +519,7 @@ class SupportsAbs(Protocol[_T_co]): @runtime_checkable class SupportsRound(Protocol[_T_co]): __slots__ = () + @overload @abstractmethod def __round__(self) -> int: ... @@ -548,6 +565,7 @@ class Generator(Iterator[_YieldT_co], Protocol[_YieldT_co, _SendT_contra, _Retur def __next__(self) -> _YieldT_co: ... @abstractmethod def send(self, value: _SendT_contra, /) -> _YieldT_co: ... + @overload @abstractmethod def throw( @@ -556,6 +574,7 @@ class Generator(Iterator[_YieldT_co], Protocol[_YieldT_co, _SendT_contra, _Retur @overload @abstractmethod def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = None, /) -> _YieldT_co: ... + if sys.version_info >= (3, 13): def close(self) -> _ReturnT_co | None: ... else: @@ -590,6 +609,7 @@ class Coroutine(Awaitable[_ReturnT_nd_co], Generic[_YieldT_co, _SendT_nd_contra, @abstractmethod def send(self, value: _SendT_nd_contra, /) -> _YieldT_co: ... + @overload @abstractmethod def throw( @@ -598,6 +618,7 @@ class Coroutine(Awaitable[_ReturnT_nd_co], Generic[_YieldT_co, _SendT_nd_contra, @overload @abstractmethod def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = None, /) -> _YieldT_co: ... + @abstractmethod def close(self) -> None: ... @@ -628,6 +649,7 @@ class AsyncGenerator(AsyncIterator[_YieldT_co], Protocol[_YieldT_co, _SendT_cont def __anext__(self) -> Coroutine[Any, Any, _YieldT_co]: ... @abstractmethod def asend(self, value: _SendT_contra, /) -> Coroutine[Any, Any, _YieldT_co]: ... + @overload @abstractmethod def athrow( @@ -638,6 +660,7 @@ class AsyncGenerator(AsyncIterator[_YieldT_co], Protocol[_YieldT_co, _SendT_cont def athrow( self, typ: BaseException, val: None = None, tb: TracebackType | None = None, / ) -> Coroutine[Any, Any, _YieldT_co]: ... + def aclose(self) -> Coroutine[Any, Any, None]: ... @runtime_checkable @@ -659,6 +682,7 @@ class Sequence(Reversible[_T_co], Collection[_T_co]): @overload @abstractmethod def __getitem__(self, index: slice[int | None], /) -> Sequence[_T_co]: ... + # Mixin methods def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... def count(self, value: Any, /) -> int: ... @@ -669,24 +693,28 @@ class Sequence(Reversible[_T_co], Collection[_T_co]): class MutableSequence(Sequence[_T]): @abstractmethod def insert(self, index: int, value: _T, /) -> None: ... + @overload @abstractmethod def __getitem__(self, index: int, /) -> _T: ... @overload @abstractmethod def __getitem__(self, index: slice[int | None], /) -> MutableSequence[_T]: ... + @overload @abstractmethod def __setitem__(self, index: int, value: _T, /) -> None: ... @overload @abstractmethod def __setitem__(self, index: slice[int | None], value: Iterable[_T], /) -> None: ... + @overload @abstractmethod def __delitem__(self, index: int, /) -> None: ... @overload @abstractmethod def __delitem__(self, index: slice[int | None], /) -> None: ... + # Mixin methods def append(self, value: _T, /) -> None: ... def clear(self) -> None: ... @@ -778,6 +806,7 @@ class Mapping(Collection[_KT], Generic[_KT, _VT_co]): # see discussion in https://github.com/python/typing/pull/273. @abstractmethod def __getitem__(self, key: _KT, /) -> _VT_co: ... + # Mixin methods @overload def get(self, key: _KT, /) -> _VT_co | None: ... @@ -785,6 +814,7 @@ class Mapping(Collection[_KT], Generic[_KT, _VT_co]): def get(self, key: _KT, default: _VT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter @overload def get(self, key: _KT, default: _T, /) -> _VT_co | _T: ... + def items(self) -> ItemsView[_KT, _VT_co]: ... def keys(self) -> KeysView[_KT]: ... def values(self) -> ValuesView[_VT_co]: ... @@ -797,13 +827,16 @@ class MutableMapping(Mapping[_KT, _VT]): @abstractmethod def __delitem__(self, key: _KT, /) -> None: ... def clear(self) -> None: ... + @overload def pop(self, key: _KT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _VT, /) -> _VT: ... @overload def pop(self, key: _KT, default: _T, /) -> _VT | _T: ... + def popitem(self) -> tuple[_KT, _VT]: ... + # This overload should be allowed only if the value type is compatible with None. # # Keep the following methods in line with MutableMapping.setdefault, modulo positional-only differences: @@ -814,6 +847,7 @@ class MutableMapping(Mapping[_KT, _VT]): def setdefault(self: MutableMapping[_KT, _T | None], key: _KT, default: None = None, /) -> _T | None: ... @overload def setdefault(self, key: _KT, default: _VT, /) -> _VT: ... + # 'update' used to take a Union, but using overloading is better. # The second overloaded type here is a bit too general, because # Mapping[tuple[_KT, _VT], W] is a subclass of Iterable[tuple[_KT, _VT]], @@ -891,18 +925,21 @@ class IO(Generic[AnyStr]): def truncate(self, size: int | None = None, /) -> int: ... @abstractmethod def writable(self) -> bool: ... + @abstractmethod @overload def write(self: IO[bytes], s: ReadableBuffer, /) -> int: ... @abstractmethod @overload def write(self, s: AnyStr, /) -> int: ... + @abstractmethod @overload def writelines(self: IO[bytes], lines: Iterable[ReadableBuffer], /) -> None: ... @abstractmethod @overload def writelines(self, lines: Iterable[AnyStr], /) -> None: ... + @abstractmethod def __next__(self) -> AnyStr: ... @abstractmethod @@ -971,16 +1008,15 @@ else: def get_args(tp: Any) -> tuple[Any, ...]: ... # AnnotationForm -if sys.version_info >= (3, 10): - @overload - def get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ... - @overload - def get_origin(tp: UnionType) -> type[UnionType]: ... - +@overload +def get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ... +@overload +def get_origin(tp: UnionType) -> type[UnionType]: ... @overload def get_origin(tp: GenericAlias) -> type: ... @overload def get_origin(tp: Any) -> Any | None: ... # AnnotationForm + @overload def cast(typ: type[_T], val: Any) -> _T: ... @overload @@ -1020,6 +1056,7 @@ class NamedTuple(tuple[Any, ...]): @overload @deprecated("Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15") def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ... + @classmethod def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... def _asdict(self) -> dict[str, Any]: ... @@ -1054,14 +1091,17 @@ class _TypedDict(Mapping[str, object], metaclass=ABCMeta): def items(self) -> dict_items[str, object]: ... def keys(self) -> dict_keys[str, object]: ... def values(self) -> dict_values[str, object]: ... + @overload def __or__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... @overload def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ... + @overload def __ror__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... @overload def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ... + # supposedly incompatible definitions of __or__ and __ior__ def __ior__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... # type: ignore[misc] @@ -1139,9 +1179,7 @@ else: def __or__(self, other: Any) -> _SpecialForm: ... def __ror__(self, other: Any) -> _SpecialForm: ... -if sys.version_info >= (3, 10): - def is_typeddict(tp: object) -> bool: ... - +def is_typeddict(tp: object) -> bool: ... def _type_repr(obj: object) -> str: ... if sys.version_info >= (3, 12): @@ -1155,6 +1193,7 @@ if sys.version_info >= (3, 12): ) def override(method: _F, /) -> _F: ... + @final class TypeAliasType: def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ... @@ -1166,6 +1205,9 @@ if sys.version_info >= (3, 12): def __parameters__(self) -> tuple[Any, ...]: ... # AnnotationForm @property def __name__(self) -> str: ... + if sys.version_info >= (3, 15): + @property + def __qualname__(self) -> str: ... # It's writable on types, but not on instances of TypeAliasType. @property def __module__(self) -> str | None: ... # type: ignore[override] @@ -1173,12 +1215,14 @@ if sys.version_info >= (3, 12): def __or__(self, right: Any, /) -> _SpecialForm: ... def __ror__(self, left: Any, /) -> _SpecialForm: ... if sys.version_info >= (3, 14): + def __iter__(self) -> Any: ... # Unpack[Self] @property def evaluate_value(self) -> EvaluateFunc: ... if sys.version_info >= (3, 13): def is_protocol(tp: type, /) -> bool: ... def get_protocol_members(tp: type, /) -> frozenset[str]: ... + @final @type_check_only class _NoDefaultType: ... diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index 406005cc4c561..fdbba495c579f 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -29,8 +29,8 @@ from collections.abc import ( ) from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager from re import Match as Match, Pattern as Pattern -from types import GenericAlias, ModuleType -from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035,RUF100 +from types import GenericAlias, ModuleType, UnionType +from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 IO as IO, TYPE_CHECKING as TYPE_CHECKING, AbstractSet as AbstractSet, @@ -40,6 +40,7 @@ from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035,RUF100 Callable as Callable, ChainMap as ChainMap, ClassVar as ClassVar, + Concatenate as Concatenate, Counter as Counter, DefaultDict as DefaultDict, Deque as Deque, @@ -50,26 +51,27 @@ from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035,RUF100 List as List, NoReturn as NoReturn, Optional as Optional, + ParamSpecArgs as ParamSpecArgs, + ParamSpecKwargs as ParamSpecKwargs, Set as Set, Text as Text, TextIO as TextIO, Tuple as Tuple, Type as Type, + TypeAlias as TypeAlias, TypedDict as TypedDict, + TypeGuard as TypeGuard, TypeVar as _TypeVar, Union as Union, _Alias, _SpecialForm, cast as cast, + is_typeddict as is_typeddict, no_type_check as no_type_check, - no_type_check_decorator as no_type_check_decorator, overload as overload, type_check_only, ) -if sys.version_info >= (3, 10): - from types import UnionType - # Please keep order the same as at runtime. __all__ = [ # Super-special typing primitives. @@ -207,6 +209,9 @@ _TC = _TypeVar("_TC", bound=type[object]) _T_co = _TypeVar("_T_co", covariant=True) # Any type covariant containers. _T_contra = _TypeVar("_T_contra", contravariant=True) +if sys.version_info < (3, 15): + def no_type_check_decorator(decorator: _F) -> _F: ... + # Do not import (and re-export) Protocol or runtime_checkable from # typing module because type checkers need to be able to distinguish # typing.Protocol and typing_extensions.Protocol so they can properly @@ -252,14 +257,17 @@ class _TypedDict(Mapping[str, object], metaclass=abc.ABCMeta): def keys(self) -> dict_keys[str, object]: ... def values(self) -> dict_values[str, object]: ... def __delitem__(self, k: Never) -> None: ... + @overload def __or__(self, value: Self, /) -> Self: ... @overload def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ... + @overload def __ror__(self, value: Self, /) -> Self: ... @overload def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ... + # supposedly incompatible definitions of `__ior__` and `__or__`: # Since this module defines "Self" it is not recognized by Ruff as typing_extensions.Self def __ior__(self, value: Self, /) -> Self: ... # type: ignore[misc] @@ -275,10 +283,8 @@ else: def get_args(tp: AnnotationForm) -> tuple[AnnotationForm, ...]: ... -if sys.version_info >= (3, 10): - @overload - def get_origin(tp: UnionType) -> type[UnionType]: ... - +@overload +def get_origin(tp: UnionType) -> type[UnionType]: ... @overload def get_origin(tp: GenericAlias) -> type: ... @overload @@ -289,34 +295,6 @@ def get_origin(tp: AnnotationForm) -> AnnotationForm | None: ... Annotated: _SpecialForm _AnnotatedAlias: Any # undocumented -# New and changed things in 3.10 -if sys.version_info >= (3, 10): - from typing import ( - Concatenate as Concatenate, - ParamSpecArgs as ParamSpecArgs, - ParamSpecKwargs as ParamSpecKwargs, - TypeAlias as TypeAlias, - TypeGuard as TypeGuard, - is_typeddict as is_typeddict, - ) -else: - @final - class ParamSpecArgs: - @property - def __origin__(self) -> ParamSpec: ... - def __init__(self, origin: ParamSpec) -> None: ... - - @final - class ParamSpecKwargs: - @property - def __origin__(self) -> ParamSpec: ... - def __init__(self, origin: ParamSpec) -> None: ... - - Concatenate: _SpecialForm - TypeAlias: _SpecialForm - TypeGuard: _SpecialForm - def is_typeddict(tp: object) -> bool: ... - # New and changed things in 3.11 if sys.version_info >= (3, 11): from typing import ( @@ -363,10 +341,12 @@ else: _field_defaults: ClassVar[dict[str, Any]] _fields: ClassVar[tuple[str, ...]] __orig_bases__: ClassVar[tuple[Any, ...]] + @overload def __init__(self, typename: str, fields: Iterable[tuple[str, Any]] = ...) -> None: ... @overload def __init__(self, typename: str, fields: None = None, **kwargs: Any) -> None: ... + @classmethod def _make(cls, iterable: Iterable[Any]) -> Self: ... def _asdict(self) -> dict[str, Any]: ... @@ -375,11 +355,10 @@ else: class NewType: def __init__(self, name: str, tp: AnnotationForm) -> None: ... def __call__(self, obj: _T, /) -> _T: ... + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... __supertype__: type | NewType __name__: str - if sys.version_info >= (3, 10): - def __or__(self, other: Any) -> _SpecialForm: ... - def __ror__(self, other: Any) -> _SpecialForm: ... if sys.version_info >= (3, 12): from collections.abc import Buffer as Buffer @@ -446,6 +425,7 @@ else: @runtime_checkable class SupportsRound(Protocol[_T_co]): __slots__ = () + @overload @abc.abstractmethod def __round__(self) -> int: ... @@ -484,6 +464,7 @@ if sys.version_info >= (3, 13): else: def is_protocol(tp: type, /) -> bool: ... def get_protocol_members(tp: type, /) -> frozenset[str]: ... + @final @type_check_only class _NoDefaultType: ... @@ -527,9 +508,8 @@ else: ) -> None: ... def has_default(self) -> bool: ... def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any) -> _SpecialForm: ... - def __ror__(self, left: Any) -> _SpecialForm: ... + def __or__(self, right: Any) -> _SpecialForm: ... + def __ror__(self, left: Any) -> _SpecialForm: ... if sys.version_info >= (3, 11): def __typing_subst__(self, arg: Any) -> Any: ... @@ -556,15 +536,14 @@ else: covariant: bool = False, default: AnnotationForm = ..., ) -> None: ... + def __or__(self, right: Any) -> _SpecialForm: ... + def __ror__(self, left: Any) -> _SpecialForm: ... @property def args(self) -> ParamSpecArgs: ... @property def kwargs(self) -> ParamSpecKwargs: ... def has_default(self) -> bool: ... def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any) -> _SpecialForm: ... - def __ror__(self, left: Any) -> _SpecialForm: ... @final class TypeVarTuple: @@ -605,9 +584,8 @@ else: # Returns typing._GenericAlias, which isn't stubbed. def __getitem__(self, parameters: Incomplete | tuple[Incomplete, ...]) -> AnnotationForm: ... def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> NoReturn: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any, /) -> _SpecialForm: ... - def __ror__(self, left: Any, /) -> _SpecialForm: ... + def __or__(self, right: Any, /) -> _SpecialForm: ... + def __ror__(self, left: Any, /) -> _SpecialForm: ... # PEP 727 class Doc: @@ -664,6 +642,7 @@ else: eval_str: bool = False, format: Format = Format.VALUE, # noqa: Y011 ) -> dict[str, AnnotationForm]: ... + @overload def evaluate_forward_ref( forward_ref: ForwardRef, @@ -697,6 +676,7 @@ else: format: Format | None = None, _recursive_guard: Container[str] = ..., ) -> AnnotationForm: ... + def type_repr(value: object) -> str: ... # PEP 661 @@ -705,6 +685,6 @@ class Sentinel: if sys.version_info >= (3, 14): def __or__(self, other: Any) -> UnionType: ... # other can be any type form legal for unions def __ror__(self, other: Any) -> UnionType: ... # other can be any type form legal for unions - elif sys.version_info >= (3, 10): + else: def __or__(self, other: Any) -> _SpecialForm: ... # other can be any type form legal for unions def __ror__(self, other: Any) -> _SpecialForm: ... # other can be any type form legal for unions diff --git a/mypy/typeshed/stdlib/unicodedata.pyi b/mypy/typeshed/stdlib/unicodedata.pyi index 9fff042f0b964..82a73181eed3b 100644 --- a/mypy/typeshed/stdlib/unicodedata.pyi +++ b/mypy/typeshed/stdlib/unicodedata.pyi @@ -1,14 +1,11 @@ import sys from _typeshed import ReadOnlyBuffer -from typing import Any, Final, Literal, TypeVar, final, overload -from typing_extensions import TypeAlias +from collections.abc import Iterator +from typing import Final, Literal, TypeAlias, TypeVar, final, overload ucd_3_2_0: UCD unidata_version: Final[str] -if sys.version_info < (3, 10): - ucnhash_CAPI: Any - _T = TypeVar("_T") _NormalizationForm: TypeAlias = Literal["NFC", "NFD", "NFKC", "NFKD"] @@ -16,11 +13,14 @@ _NormalizationForm: TypeAlias = Literal["NFC", "NFD", "NFKC", "NFKD"] def bidirectional(chr: str, /) -> str: ... def category(chr: str, /) -> str: ... def combining(chr: str, /) -> int: ... + @overload def decimal(chr: str, /) -> int: ... @overload def decimal(chr: str, default: _T, /) -> int | _T: ... + def decomposition(chr: str, /) -> str: ... + @overload def digit(chr: str, /) -> int: ... @overload @@ -30,17 +30,31 @@ _EastAsianWidth: TypeAlias = Literal["F", "H", "W", "Na", "A", "N"] def east_asian_width(chr: str, /) -> _EastAsianWidth: ... def is_normalized(form: _NormalizationForm, unistr: str, /) -> bool: ... + +if sys.version_info >= (3, 15): + def block(chr: str, /) -> str: ... + def extended_pictographic(chr: str, /) -> bool: ... + def grapheme_cluster_break(chr: str, /) -> str: ... + def indic_conjunct_break(chr: str, /) -> str: ... + def isxidstart(chr: str, /) -> bool: ... + def isxidcontinue(chr: str, /) -> bool: ... + def iter_graphemes(unistr: str, start: int = 0, end: int = sys.maxsize, /) -> Iterator[str]: ... + def lookup(name: str | ReadOnlyBuffer, /) -> str: ... def mirrored(chr: str, /) -> int: ... + @overload def name(chr: str, /) -> str: ... @overload def name(chr: str, default: _T, /) -> str | _T: ... + def normalize(form: _NormalizationForm, unistr: str, /) -> str: ... + @overload def numeric(chr: str, /) -> float: ... @overload def numeric(chr: str, default: _T, /) -> float | _T: ... + @final class UCD: # The methods below are constructed from the same array in C @@ -49,24 +63,31 @@ class UCD: def bidirectional(self, chr: str, /) -> str: ... def category(self, chr: str, /) -> str: ... def combining(self, chr: str, /) -> int: ... + @overload def decimal(self, chr: str, /) -> int: ... @overload def decimal(self, chr: str, default: _T, /) -> int | _T: ... + def decomposition(self, chr: str, /) -> str: ... + @overload def digit(self, chr: str, /) -> int: ... @overload def digit(self, chr: str, default: _T, /) -> int | _T: ... + def east_asian_width(self, chr: str, /) -> _EastAsianWidth: ... def is_normalized(self, form: _NormalizationForm, unistr: str, /) -> bool: ... def lookup(self, name: str | ReadOnlyBuffer, /) -> str: ... def mirrored(self, chr: str, /) -> int: ... + @overload def name(self, chr: str, /) -> str: ... @overload def name(self, chr: str, default: _T, /) -> str | _T: ... + def normalize(self, form: _NormalizationForm, unistr: str, /) -> str: ... + @overload def numeric(self, chr: str, /) -> float: ... @overload diff --git a/mypy/typeshed/stdlib/unittest/_log.pyi b/mypy/typeshed/stdlib/unittest/_log.pyi index 011a970d8bbce..da6ce5a5ac7ae 100644 --- a/mypy/typeshed/stdlib/unittest/_log.pyi +++ b/mypy/typeshed/stdlib/unittest/_log.pyi @@ -15,11 +15,13 @@ class _AssertLogsContext(_BaseTestCaseContext, Generic[_L]): logger_name: str level: int msg: None - if sys.version_info >= (3, 10): - def __init__(self, test_case: TestCase, logger_name: str, level: int, no_logs: bool) -> None: ... - no_logs: bool + no_logs: bool + if sys.version_info >= (3, 15): + def __init__( + self, test_case: TestCase, logger_name: str, level: int, no_logs: bool, formatter: logging.Formatter | None = None + ) -> None: ... else: - def __init__(self, test_case: TestCase, logger_name: str, level: int) -> None: ... + def __init__(self, test_case: TestCase, logger_name: str, level: int, no_logs: bool) -> None: ... def __enter__(self) -> _L: ... def __exit__( diff --git a/mypy/typeshed/stdlib/unittest/async_case.pyi b/mypy/typeshed/stdlib/unittest/async_case.pyi index 0b3fb9122c7b9..77627a85ef197 100644 --- a/mypy/typeshed/stdlib/unittest/async_case.pyi +++ b/mypy/typeshed/stdlib/unittest/async_case.pyi @@ -1,8 +1,7 @@ import sys from asyncio.events import AbstractEventLoop from collections.abc import Awaitable, Callable -from typing import TypeVar -from typing_extensions import ParamSpec +from typing import ParamSpec, TypeVar from .case import TestCase diff --git a/mypy/typeshed/stdlib/unittest/case.pyi b/mypy/typeshed/stdlib/unittest/case.pyi index a602196e73c64..942154ea53aea 100644 --- a/mypy/typeshed/stdlib/unittest/case.pyi +++ b/mypy/typeshed/stdlib/unittest/case.pyi @@ -7,8 +7,21 @@ from collections.abc import Callable, Container, Iterable, Mapping, Sequence, Se from contextlib import AbstractContextManager from re import Pattern from types import GenericAlias, TracebackType -from typing import Any, AnyStr, Final, Generic, NoReturn, Protocol, SupportsAbs, SupportsRound, TypeVar, overload, type_check_only -from typing_extensions import Never, ParamSpec, Self +from typing import ( + Any, + AnyStr, + Final, + Generic, + NoReturn, + ParamSpec, + Protocol, + SupportsAbs, + SupportsRound, + TypeVar, + overload, + type_check_only, +) +from typing_extensions import Never, Self from unittest._log import _AssertLogsContext, _LoggingWatcher from warnings import WarningMessage @@ -54,7 +67,7 @@ def skipIf(condition: object, reason: str) -> Callable[[_FT], _FT]: ... def skipUnless(condition: object, reason: str) -> Callable[[_FT], _FT]: ... class SkipTest(Exception): - def __init__(self, reason: str) -> None: ... + def __init__(self, reason: str, /) -> None: ... @type_check_only class _SupportsAbsAndDunderGE(SupportsDunderGE[Any], SupportsAbs[Any], Protocol): ... @@ -96,22 +109,27 @@ class TestCase: def assertNotIn(self, member: Any, container: Iterable[Any] | Container[Any], msg: Any = None) -> None: ... def assertIsInstance(self, obj: object, cls: _ClassInfo, msg: Any = None) -> None: ... def assertNotIsInstance(self, obj: object, cls: _ClassInfo, msg: Any = None) -> None: ... + @overload def assertGreater(self, a: SupportsDunderGT[_T], b: _T, msg: Any = None) -> None: ... @overload def assertGreater(self, a: _T, b: SupportsDunderLT[_T], msg: Any = None) -> None: ... + @overload def assertGreaterEqual(self, a: SupportsDunderGE[_T], b: _T, msg: Any = None) -> None: ... @overload def assertGreaterEqual(self, a: _T, b: SupportsDunderLE[_T], msg: Any = None) -> None: ... + @overload def assertLess(self, a: SupportsDunderLT[_T], b: _T, msg: Any = None) -> None: ... @overload def assertLess(self, a: _T, b: SupportsDunderGT[_T], msg: Any = None) -> None: ... + @overload def assertLessEqual(self, a: SupportsDunderLE[_T], b: _T, msg: Any = None) -> None: ... @overload def assertLessEqual(self, a: _T, b: SupportsDunderGE[_T], msg: Any = None) -> None: ... + # `assertRaises`, `assertRaisesRegex`, and `assertRaisesRegexp` # are not using `ParamSpec` intentionally, # because they might be used with explicitly wrong arg types to raise some error in tests. @@ -127,6 +145,7 @@ class TestCase: def assertRaises( self, expected_exception: type[_E] | tuple[type[_E], ...], *, msg: Any = ... ) -> _AssertRaisesContext[_E]: ... + @overload def assertRaisesRegex( self, @@ -140,6 +159,7 @@ class TestCase: def assertRaisesRegex( self, expected_exception: type[_E] | tuple[type[_E], ...], expected_regex: str | Pattern[str], *, msg: Any = ... ) -> _AssertRaisesContext[_E]: ... + @overload def assertWarns( self, @@ -152,6 +172,7 @@ class TestCase: def assertWarns( self, expected_warning: type[Warning] | tuple[type[Warning], ...], *, msg: Any = ... ) -> _AssertWarnsContext: ... + @overload def assertWarnsRegex( self, @@ -165,13 +186,22 @@ class TestCase: def assertWarnsRegex( self, expected_warning: type[Warning] | tuple[type[Warning], ...], expected_regex: str | Pattern[str], *, msg: Any = ... ) -> _AssertWarnsContext: ... - def assertLogs( - self, logger: str | logging.Logger | None = None, level: int | str | None = None - ) -> _AssertLogsContext[_LoggingWatcher]: ... - if sys.version_info >= (3, 10): - def assertNoLogs( + + if sys.version_info >= (3, 15): + def assertLogs( + self, + logger: str | logging.Logger | None = None, + level: int | str | None = None, + formatter: logging.Formatter | None = None, + ) -> _AssertLogsContext[_LoggingWatcher]: ... + else: + def assertLogs( self, logger: str | logging.Logger | None = None, level: int | str | None = None - ) -> _AssertLogsContext[None]: ... + ) -> _AssertLogsContext[_LoggingWatcher]: ... + + def assertNoLogs( + self, logger: str | logging.Logger | None = None, level: int | str | None = None + ) -> _AssertLogsContext[None]: ... @overload def assertAlmostEqual(self, first: _S, second: _S, places: None, msg: Any, delta: _SupportsAbsAndDunderGE) -> None: ... @@ -197,6 +227,7 @@ class TestCase: msg: Any = None, delta: None = None, ) -> None: ... + @overload def assertNotAlmostEqual(self, first: _S, second: _S, places: None, msg: Any, delta: _SupportsAbsAndDunderGE) -> None: ... @overload @@ -221,6 +252,7 @@ class TestCase: msg: Any = None, delta: None = None, ) -> None: ... + def assertRegex(self, text: AnyStr, expected_regex: AnyStr | Pattern[AnyStr], msg: Any = None) -> None: ... def assertNotRegex(self, text: AnyStr, unexpected_regex: AnyStr | Pattern[AnyStr], msg: Any = None) -> None: ... def assertCountEqual(self, first: Iterable[Any], second: Iterable[Any], msg: Any = None) -> None: ... @@ -277,9 +309,8 @@ class TestCase: self, subset: Mapping[Any, Any], dictionary: Mapping[Any, Any], msg: object = None ) -> None: ... - if sys.version_info >= (3, 10): - # Runtime has *args, **kwargs, but will error if any are supplied - def __init_subclass__(cls, *args: Never, **kwargs: Never) -> None: ... + # Runtime has *args, **kwargs, but will error if any are supplied + def __init_subclass__(cls, *args: Never, **kwargs: Never) -> None: ... if sys.version_info >= (3, 14): def assertIsSubclass(self, cls: type, superclass: type | tuple[type, ...], msg: Any = None) -> None: ... diff --git a/mypy/typeshed/stdlib/unittest/loader.pyi b/mypy/typeshed/stdlib/unittest/loader.pyi index 81de40c898496..0d92b78f3461d 100644 --- a/mypy/typeshed/stdlib/unittest/loader.pyi +++ b/mypy/typeshed/stdlib/unittest/loader.pyi @@ -4,8 +4,8 @@ import unittest.suite from collections.abc import Callable, Sequence from re import Pattern from types import ModuleType -from typing import Any, Final -from typing_extensions import TypeAlias, deprecated +from typing import Any, Final, TypeAlias +from typing_extensions import deprecated _SortComparisonMethod: TypeAlias = Callable[[str, str], int] _SuiteClass: TypeAlias = Callable[[list[unittest.case.TestCase]], unittest.suite.TestSuite] @@ -35,38 +35,21 @@ class TestLoader: defaultTestLoader: TestLoader if sys.version_info < (3, 13): - if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") - def getTestCaseNames( - testCaseClass: type[unittest.case.TestCase], - prefix: str, - sortUsing: _SortComparisonMethod = ..., - testNamePatterns: list[str] | None = None, - ) -> Sequence[str]: ... - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") - def makeSuite( - testCaseClass: type[unittest.case.TestCase], - prefix: str = "test", - sortUsing: _SortComparisonMethod = ..., - suiteClass: _SuiteClass = ..., - ) -> unittest.suite.TestSuite: ... - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") - def findTestCases( - module: ModuleType, prefix: str = "test", sortUsing: _SortComparisonMethod = ..., suiteClass: _SuiteClass = ... - ) -> unittest.suite.TestSuite: ... - else: - def getTestCaseNames( - testCaseClass: type[unittest.case.TestCase], - prefix: str, - sortUsing: _SortComparisonMethod = ..., - testNamePatterns: list[str] | None = None, - ) -> Sequence[str]: ... - def makeSuite( - testCaseClass: type[unittest.case.TestCase], - prefix: str = "test", - sortUsing: _SortComparisonMethod = ..., - suiteClass: _SuiteClass = ..., - ) -> unittest.suite.TestSuite: ... - def findTestCases( - module: ModuleType, prefix: str = "test", sortUsing: _SortComparisonMethod = ..., suiteClass: _SuiteClass = ... - ) -> unittest.suite.TestSuite: ... + @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + def getTestCaseNames( + testCaseClass: type[unittest.case.TestCase], + prefix: str, + sortUsing: _SortComparisonMethod = ..., + testNamePatterns: list[str] | None = None, + ) -> Sequence[str]: ... + @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + def makeSuite( + testCaseClass: type[unittest.case.TestCase], + prefix: str = "test", + sortUsing: _SortComparisonMethod = ..., + suiteClass: _SuiteClass = ..., + ) -> unittest.suite.TestSuite: ... + @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + def findTestCases( + module: ModuleType, prefix: str = "test", sortUsing: _SortComparisonMethod = ..., suiteClass: _SuiteClass = ... + ) -> unittest.suite.TestSuite: ... diff --git a/mypy/typeshed/stdlib/unittest/main.pyi b/mypy/typeshed/stdlib/unittest/main.pyi index e1aeac32c5910..f48347c16ec39 100644 --- a/mypy/typeshed/stdlib/unittest/main.pyi +++ b/mypy/typeshed/stdlib/unittest/main.pyi @@ -64,11 +64,8 @@ class TestProgram: ) -> None: ... if sys.version_info < (3, 13): - if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") - def usageExit(self, msg: Any = None) -> None: ... - else: - def usageExit(self, msg: Any = None) -> None: ... + @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + def usageExit(self, msg: Any = None) -> None: ... def parseArgs(self, argv: list[str]) -> None: ... def createTests(self, from_discovery: bool = False, Loader: unittest.loader.TestLoader | None = None) -> None: ... diff --git a/mypy/typeshed/stdlib/unittest/mock.pyi b/mypy/typeshed/stdlib/unittest/mock.pyi index ef51d721297a1..1b6ab756d2533 100644 --- a/mypy/typeshed/stdlib/unittest/mock.pyi +++ b/mypy/typeshed/stdlib/unittest/mock.pyi @@ -3,8 +3,8 @@ from _typeshed import MaybeNone from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping, Sequence from contextlib import _GeneratorContextManager from types import TracebackType -from typing import Any, ClassVar, Final, Generic, Literal, TypeVar, overload, type_check_only -from typing_extensions import ParamSpec, Self, TypeAlias, disjoint_base +from typing import Any, ClassVar, Final, Generic, Literal, ParamSpec, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self, disjoint_base _T = TypeVar("_T") _TT = TypeVar("_TT", bound=type[Any]) @@ -244,42 +244,29 @@ class _patch(Generic[_T]): additional_patchers: Any # If new==DEFAULT, self is _patch[Any]. Ideally we'd be able to add an overload for it so that self is _patch[MagicMock], # but that's impossible with the current type system. - if sys.version_info >= (3, 10): - def __init__( - self: _patch[_T], # pyright: ignore[reportInvalidTypeVarUse] #11780 - getter: Callable[[], Any], - attribute: str, - new: _T, - spec: Any | None, - create: bool, - spec_set: Any | None, - autospec: Any | None, - new_callable: Any | None, - kwargs: Mapping[str, Any], - *, - unsafe: bool = False, - ) -> None: ... - else: - def __init__( - self: _patch[_T], # pyright: ignore[reportInvalidTypeVarUse] #11780 - getter: Callable[[], Any], - attribute: str, - new: _T, - spec: Any | None, - create: bool, - spec_set: Any | None, - autospec: Any | None, - new_callable: Any | None, - kwargs: Mapping[str, Any], - ) -> None: ... - + def __init__( + self: _patch[_T], # pyright: ignore[reportInvalidTypeVarUse] #11780 + getter: Callable[[], Any], + attribute: str, + new: _T, + spec: Any | None, + create: bool, + spec_set: Any | None, + autospec: Any | None, + new_callable: Any | None, + kwargs: Mapping[str, Any], + *, + unsafe: bool = False, + ) -> None: ... def copy(self) -> _patch[_T]: ... + @overload def __call__(self, func: _TT) -> _TT: ... # If new==DEFAULT, this should add a MagicMock parameter to the function # arguments. See the _patch_default_new class below for this functionality. @overload def __call__(self, func: Callable[_P, _R]) -> Callable[_P, _R]: ... + def decoration_helper( self, patched: _patch[Any], args: Sequence[Any], keywargs: Any ) -> _GeneratorContextManager[tuple[Sequence[Any], Any]]: ... @@ -315,13 +302,11 @@ class _patch_dict: clear: Any def __init__(self, in_dict: Any, values: Any = (), clear: Any = False, **kwargs: Any) -> None: ... def __call__(self, f: Any) -> Any: ... - if sys.version_info >= (3, 10): - def decorate_callable(self, f: _F) -> _F: ... - def decorate_async_callable(self, f: _AF) -> _AF: ... - - def decorate_class(self, klass: Any) -> Any: ... def __enter__(self) -> Any: ... def __exit__(self, *args: object) -> Any: ... + def decorate_callable(self, f: _F) -> _F: ... + def decorate_async_callable(self, f: _AF) -> _AF: ... + def decorate_class(self, klass: Any) -> Any: ... start: Any stop: Any @@ -331,6 +316,7 @@ class _patch_dict: class _patcher: TEST_PREFIX: str dict: type[_patch_dict] + # This overload also covers the case, where new==DEFAULT. In this case, the return type is _patch[Any]. # Ideally we'd be able to add an overload for it so that the return type is _patch[MagicMock], # but that's impossible with the current type system. @@ -377,6 +363,7 @@ class _patcher: # kwargs are passed to the MagicMock/AsyncMock constructor **kwargs: Any, ) -> _patch_pass_arg[MagicMock | AsyncMock]: ... + # This overload also covers the case, where new==DEFAULT. In this case, the return type is _patch[Any]. # Ideally we'd be able to add an overload for it so that the return type is _patch[MagicMock], # but that's impossible with the current type system. @@ -426,6 +413,7 @@ class _patcher: # kwargs are passed to the MagicMock/AsyncMock constructor **kwargs: Any, ) -> _patch_pass_arg[MagicMock | AsyncMock]: ... + @overload @staticmethod def multiple( @@ -467,6 +455,7 @@ class _patcher: # The kwargs are the mock objects or DEFAULT **kwargs: Any, ) -> _patch[Any]: ... + @staticmethod def stopall() -> None: ... @@ -517,27 +506,16 @@ class _ANY(Any): ANY: _ANY -if sys.version_info >= (3, 10): - def create_autospec( - spec: Any, - spec_set: Any = False, - instance: Any = False, - _parent: Any | None = None, - _name: Any | None = None, - *, - unsafe: bool = False, - **kwargs: Any, - ) -> Any: ... - -else: - def create_autospec( - spec: Any, - spec_set: Any = False, - instance: Any = False, - _parent: Any | None = None, - _name: Any | None = None, - **kwargs: Any, - ) -> Any: ... +def create_autospec( + spec: Any, + spec_set: Any = False, + instance: Any = False, + _parent: Any | None = None, + _name: Any | None = None, + *, + unsafe: bool = False, + **kwargs: Any, +) -> Any: ... class _SpecState: spec: Any diff --git a/mypy/typeshed/stdlib/unittest/result.pyi b/mypy/typeshed/stdlib/unittest/result.pyi index 0761baaa2830b..081f6e1328e47 100644 --- a/mypy/typeshed/stdlib/unittest/result.pyi +++ b/mypy/typeshed/stdlib/unittest/result.pyi @@ -2,8 +2,7 @@ import sys import unittest.case from _typeshed import OptExcInfo from collections.abc import Callable -from typing import Any, Final, TextIO, TypeVar -from typing_extensions import TypeAlias +from typing import Any, Final, TextIO, TypeAlias, TypeVar _F = TypeVar("_F", bound=Callable[..., Any]) _DurationsType: TypeAlias = list[tuple[str, float]] diff --git a/mypy/typeshed/stdlib/unittest/runner.pyi b/mypy/typeshed/stdlib/unittest/runner.pyi index f76771f55e131..3a2e08749e9b6 100644 --- a/mypy/typeshed/stdlib/unittest/runner.pyi +++ b/mypy/typeshed/stdlib/unittest/runner.pyi @@ -4,8 +4,8 @@ import unittest.result import unittest.suite from _typeshed import SupportsFlush, SupportsWrite from collections.abc import Callable, Iterable -from typing import Any, Generic, Protocol, TypeVar, type_check_only -from typing_extensions import Never, TypeAlias +from typing import Any, Generic, Protocol, TypeAlias, TypeVar, type_check_only +from typing_extensions import Never from warnings import _ActionKind _ResultClassType: TypeAlias = Callable[[_TextTestStream, bool, int], TextTestResult[Any]] diff --git a/mypy/typeshed/stdlib/unittest/signals.pyi b/mypy/typeshed/stdlib/unittest/signals.pyi index a60133ada9d95..928ab68ae65d1 100644 --- a/mypy/typeshed/stdlib/unittest/signals.pyi +++ b/mypy/typeshed/stdlib/unittest/signals.pyi @@ -1,7 +1,6 @@ import unittest.result from collections.abc import Callable -from typing import TypeVar, overload -from typing_extensions import ParamSpec +from typing import ParamSpec, TypeVar, overload _P = ParamSpec("_P") _T = TypeVar("_T") @@ -9,6 +8,7 @@ _T = TypeVar("_T") def installHandler() -> None: ... def registerResult(result: unittest.result.TestResult) -> None: ... def removeResult(result: unittest.result.TestResult) -> bool: ... + @overload def removeHandler(method: None = None) -> None: ... @overload diff --git a/mypy/typeshed/stdlib/unittest/suite.pyi b/mypy/typeshed/stdlib/unittest/suite.pyi index 443396164b6fe..b7cf75c682712 100644 --- a/mypy/typeshed/stdlib/unittest/suite.pyi +++ b/mypy/typeshed/stdlib/unittest/suite.pyi @@ -1,8 +1,7 @@ import unittest.case import unittest.result from collections.abc import Iterable, Iterator -from typing import ClassVar -from typing_extensions import TypeAlias +from typing import ClassVar, TypeAlias _TestType: TypeAlias = unittest.case.TestCase | TestSuite diff --git a/mypy/typeshed/stdlib/unittest/util.pyi b/mypy/typeshed/stdlib/unittest/util.pyi index 763c1478f5e6d..11a6f903932fb 100644 --- a/mypy/typeshed/stdlib/unittest/util.pyi +++ b/mypy/typeshed/stdlib/unittest/util.pyi @@ -1,6 +1,5 @@ from collections.abc import MutableSequence, Sequence -from typing import Any, Final, Literal, Protocol, TypeVar, type_check_only -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, Protocol, TypeAlias, TypeVar, type_check_only @type_check_only class _SupportsDunderLT(Protocol): diff --git a/mypy/typeshed/stdlib/urllib/error.pyi b/mypy/typeshed/stdlib/urllib/error.pyi index 2173d7e6efaa5..6255f1ae7db0d 100644 --- a/mypy/typeshed/stdlib/urllib/error.pyi +++ b/mypy/typeshed/stdlib/urllib/error.pyi @@ -15,6 +15,7 @@ class HTTPError(URLError, addinfourl): def headers(self) -> Message: ... @headers.setter def headers(self, headers: Message) -> None: ... + @property def reason(self) -> str: ... # type: ignore[override] code: int diff --git a/mypy/typeshed/stdlib/urllib/parse.pyi b/mypy/typeshed/stdlib/urllib/parse.pyi index 364892ecdf698..b83a0f4e8678c 100644 --- a/mypy/typeshed/stdlib/urllib/parse.pyi +++ b/mypy/typeshed/stdlib/urllib/parse.pyi @@ -1,8 +1,8 @@ import sys from collections.abc import Iterable, Mapping, Sequence from types import GenericAlias -from typing import Any, AnyStr, Final, Generic, Literal, NamedTuple, Protocol, overload, type_check_only -from typing_extensions import TypeAlias +from typing import Any, AnyStr, Final, Generic, Literal, NamedTuple, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import TypeVar __all__ = [ "urlparse", @@ -38,6 +38,11 @@ scheme_chars: Final[str] if sys.version_info < (3, 11): MAX_CACHE_SIZE: Final[int] +_ResultStrT = TypeVar("_ResultStrT", str, bytes) +_ResultComponentT = TypeVar("_ResultComponentT", str, bytes, str | None, bytes | None) +_StrComponentT = TypeVar("_StrComponentT", str, str | None, default=str) +_BytesComponentT = TypeVar("_BytesComponentT", bytes, bytes | None, default=bytes) + class _ResultMixinStr: __slots__ = () def encode(self, encoding: str = "ascii", errors: str = "strict") -> _ResultMixinBytes: ... @@ -64,44 +69,88 @@ class _NetlocResultMixinStr(_NetlocResultMixinBase[str], _ResultMixinStr): class _NetlocResultMixinBytes(_NetlocResultMixinBase[bytes], _ResultMixinBytes): __slots__ = () -class _DefragResultBase(NamedTuple, Generic[AnyStr]): - url: AnyStr - fragment: AnyStr +# Need to duplicate the whole class because mypy rejects version-specific +# branches in namedtuple bodies. +if sys.version_info >= (3, 15): + class _DefragResultBase(NamedTuple, Generic[_ResultStrT, _ResultComponentT]): + url: _ResultStrT + fragment: _ResultComponentT + # Ignore needed due to mypy#21453. + def geturl(self) -> _ResultStrT: ... # type: ignore[misc] + +else: + class _DefragResultBase(NamedTuple, Generic[_ResultStrT, _ResultComponentT]): + url: _ResultStrT + fragment: _ResultComponentT + +if sys.version_info >= (3, 15): + class _SplitResultBase(NamedTuple, Generic[_ResultStrT, _ResultComponentT]): + scheme: _ResultComponentT + netloc: _ResultComponentT + path: _ResultStrT + query: _ResultComponentT + fragment: _ResultComponentT + # Ignore needed due to mypy#21453. + def geturl(self) -> _ResultStrT: ... # type: ignore[misc] + +else: + class _SplitResultBase(NamedTuple, Generic[_ResultStrT, _ResultComponentT]): + scheme: _ResultComponentT + netloc: _ResultComponentT + path: _ResultStrT + query: _ResultComponentT + fragment: _ResultComponentT + +if sys.version_info >= (3, 15): + class _ParseResultBase(NamedTuple, Generic[_ResultStrT, _ResultComponentT]): + scheme: _ResultComponentT + netloc: _ResultComponentT + path: _ResultStrT + params: _ResultComponentT + query: _ResultComponentT + fragment: _ResultComponentT + # Ignore needed due to mypy#21453. + def geturl(self) -> _ResultStrT: ... # type: ignore[misc] -class _SplitResultBase(NamedTuple, Generic[AnyStr]): - scheme: AnyStr - netloc: AnyStr - path: AnyStr - query: AnyStr - fragment: AnyStr +else: + class _ParseResultBase(NamedTuple, Generic[_ResultStrT, _ResultComponentT]): + scheme: _ResultComponentT + netloc: _ResultComponentT + path: _ResultStrT + params: _ResultComponentT + query: _ResultComponentT + fragment: _ResultComponentT -class _ParseResultBase(NamedTuple, Generic[AnyStr]): - scheme: AnyStr - netloc: AnyStr - path: AnyStr - params: AnyStr - query: AnyStr - fragment: AnyStr +if sys.version_info >= (3, 15): + # Structured result objects for string data + class DefragResult(_DefragResultBase[str, _StrComponentT], _ResultMixinStr, Generic[_StrComponentT]): ... + class SplitResult(_SplitResultBase[str, _StrComponentT], _NetlocResultMixinStr, Generic[_StrComponentT]): ... + class ParseResult(_ParseResultBase[str, _StrComponentT], _NetlocResultMixinStr, Generic[_StrComponentT]): ... + # Structured result objects for bytes data + class DefragResultBytes(_DefragResultBase[bytes, _BytesComponentT], _ResultMixinBytes, Generic[_BytesComponentT]): ... + class SplitResultBytes(_SplitResultBase[bytes, _BytesComponentT], _NetlocResultMixinBytes, Generic[_BytesComponentT]): ... + class ParseResultBytes(_ParseResultBase[bytes, _BytesComponentT], _NetlocResultMixinBytes, Generic[_BytesComponentT]): ... -# Structured result objects for string data -class DefragResult(_DefragResultBase[str], _ResultMixinStr): - def geturl(self) -> str: ... +else: + # Structured result objects for string data + class DefragResult(_DefragResultBase[str, str], _ResultMixinStr): + def geturl(self) -> str: ... -class SplitResult(_SplitResultBase[str], _NetlocResultMixinStr): - def geturl(self) -> str: ... + class SplitResult(_SplitResultBase[str, str], _NetlocResultMixinStr): + def geturl(self) -> str: ... -class ParseResult(_ParseResultBase[str], _NetlocResultMixinStr): - def geturl(self) -> str: ... + class ParseResult(_ParseResultBase[str, str], _NetlocResultMixinStr): + def geturl(self) -> str: ... -# Structured result objects for bytes data -class DefragResultBytes(_DefragResultBase[bytes], _ResultMixinBytes): - def geturl(self) -> bytes: ... + # Structured result objects for bytes data + class DefragResultBytes(_DefragResultBase[bytes, bytes], _ResultMixinBytes): + def geturl(self) -> bytes: ... -class SplitResultBytes(_SplitResultBase[bytes], _NetlocResultMixinBytes): - def geturl(self) -> bytes: ... + class SplitResultBytes(_SplitResultBase[bytes, bytes], _NetlocResultMixinBytes): + def geturl(self) -> bytes: ... -class ParseResultBytes(_ParseResultBase[bytes], _NetlocResultMixinBytes): - def geturl(self) -> bytes: ... + class ParseResultBytes(_ParseResultBase[bytes, bytes], _NetlocResultMixinBytes): + def geturl(self) -> bytes: ... def parse_qs( qs: AnyStr | None, @@ -121,22 +170,40 @@ def parse_qsl( max_num_fields: int | None = None, separator: str = "&", ) -> list[tuple[AnyStr, AnyStr]]: ... + @overload def quote(string: str, safe: str | Iterable[int] = "/", encoding: str | None = None, errors: str | None = None) -> str: ... @overload def quote(string: bytes | bytearray, safe: str | Iterable[int] = "/") -> str: ... + def quote_from_bytes(bs: bytes | bytearray, safe: str | Iterable[int] = "/") -> str: ... + @overload def quote_plus(string: str, safe: str | Iterable[int] = "", encoding: str | None = None, errors: str | None = None) -> str: ... @overload def quote_plus(string: bytes | bytearray, safe: str | Iterable[int] = "") -> str: ... + def unquote(string: str | bytes, encoding: str = "utf-8", errors: str = "replace") -> str: ... def unquote_to_bytes(string: str | bytes | bytearray) -> bytes: ... def unquote_plus(string: str, encoding: str = "utf-8", errors: str = "replace") -> str: ... + @overload def urldefrag(url: str) -> DefragResult: ... @overload def urldefrag(url: bytes | bytearray | None) -> DefragResultBytes: ... +if sys.version_info >= (3, 15): + @overload + def urldefrag(url: str, *, missing_as_none: Literal[True]) -> DefragResult[str | None]: ... + @overload + def urldefrag(url: str, *, missing_as_none: Literal[False] = False) -> DefragResult[str]: ... + @overload + def urldefrag(url: bytes | bytearray | None, *, missing_as_none: Literal[True]) -> DefragResultBytes[bytes | None]: ... + @overload + def urldefrag(url: bytes | bytearray | None, *, missing_as_none: Literal[False] = False) -> DefragResultBytes[bytes]: ... + @overload + def urldefrag(url: str, *, missing_as_none: bool) -> DefragResult[str | None]: ... + @overload + def urldefrag(url: bytes | bytearray | None, *, missing_as_none: bool) -> DefragResultBytes[bytes | None]: ... # The values are passed through `str()` (unless they are bytes), so anything is valid. _QueryType: TypeAlias = ( @@ -166,12 +233,51 @@ def urlencode( quote_via: _QuoteVia = ..., ) -> str: ... def urljoin(base: AnyStr, url: AnyStr | None, allow_fragments: bool = True) -> AnyStr: ... + @overload def urlparse(url: str, scheme: str = "", allow_fragments: bool = True) -> ParseResult: ... @overload def urlparse( url: bytes | bytearray | None, scheme: bytes | bytearray | None | Literal[""] = "", allow_fragments: bool = True ) -> ParseResultBytes: ... +if sys.version_info >= (3, 15): + @overload + def urlparse( + url: str, scheme: str = "", allow_fragments: bool = True, *, missing_as_none: Literal[True] + ) -> ParseResult[str | None]: ... + @overload + def urlparse( + url: str, scheme: str = "", allow_fragments: bool = True, *, missing_as_none: Literal[False] = False + ) -> ParseResult[str]: ... + @overload + def urlparse( + url: bytes | bytearray | None, + scheme: bytes | bytearray | None | Literal[""] = "", + allow_fragments: bool = True, + *, + missing_as_none: Literal[True], + ) -> ParseResultBytes[bytes | None]: ... + @overload + def urlparse( + url: bytes | bytearray | None, + scheme: bytes | bytearray | None | Literal[""] = "", + allow_fragments: bool = True, + *, + missing_as_none: Literal[False] = False, + ) -> ParseResultBytes[bytes]: ... + @overload + def urlparse( + url: str, scheme: str = "", allow_fragments: bool = True, *, missing_as_none: bool + ) -> ParseResult[str | None]: ... + @overload + def urlparse( + url: bytes | bytearray | None, + scheme: bytes | bytearray | None | Literal[""] = "", + allow_fragments: bool = True, + *, + missing_as_none: bool, + ) -> ParseResultBytes[bytes | None]: ... + @overload def urlsplit(url: str, scheme: str = "", allow_fragments: bool = True) -> SplitResult: ... @@ -180,22 +286,69 @@ if sys.version_info >= (3, 11): def urlsplit( url: bytes | None, scheme: bytes | None | Literal[""] = "", allow_fragments: bool = True ) -> SplitResultBytes: ... - else: @overload def urlsplit( url: bytes | bytearray | None, scheme: bytes | bytearray | None | Literal[""] = "", allow_fragments: bool = True ) -> SplitResultBytes: ... +if sys.version_info >= (3, 15): + @overload + def urlsplit( + url: str, scheme: str = "", allow_fragments: bool = True, *, missing_as_none: Literal[True] + ) -> SplitResult[str | None]: ... + @overload + def urlsplit( + url: str, scheme: str = "", allow_fragments: bool = True, *, missing_as_none: Literal[False] = False + ) -> SplitResult[str]: ... + @overload + def urlsplit( + url: bytes | None, + scheme: bytes | None | Literal[""] = "", + allow_fragments: bool = True, + *, + missing_as_none: Literal[True], + ) -> SplitResultBytes[bytes | None]: ... + @overload + def urlsplit( + url: bytes | None, + scheme: bytes | None | Literal[""] = "", + allow_fragments: bool = True, + *, + missing_as_none: Literal[False] = False, + ) -> SplitResultBytes[bytes]: ... + @overload + def urlsplit( + url: str, scheme: str = "", allow_fragments: bool = True, *, missing_as_none: bool + ) -> SplitResult[str | None]: ... + @overload + def urlsplit( + url: bytes | None, scheme: bytes | None | Literal[""] = "", allow_fragments: bool = True, *, missing_as_none: bool + ) -> SplitResultBytes[bytes | None]: ... -# Requires an iterable of length 6 -@overload -def urlunparse(components: Iterable[None]) -> Literal[b""]: ... # type: ignore[overload-overlap] -@overload -def urlunparse(components: Iterable[AnyStr | None]) -> AnyStr: ... +if sys.version_info >= (3, 15): + # Requires an iterable of length 6 + @overload + def urlunparse(components: Iterable[None], *, keep_empty: bool = ...) -> Literal[b""]: ... # type: ignore[overload-overlap] + @overload + def urlunparse(components: Iterable[AnyStr | None], *, keep_empty: bool = ...) -> AnyStr: ... +else: + # Requires an iterable of length 6 + @overload + def urlunparse(components: Iterable[None]) -> Literal[b""]: ... # type: ignore[overload-overlap] + @overload + def urlunparse(components: Iterable[AnyStr | None]) -> AnyStr: ... + +if sys.version_info >= (3, 15): + # Requires an iterable of length 5 + @overload + def urlunsplit(components: Iterable[None], *, keep_empty: bool = ...) -> Literal[b""]: ... # type: ignore[overload-overlap] + @overload + def urlunsplit(components: Iterable[AnyStr | None], *, keep_empty: bool = ...) -> AnyStr: ... +else: + # Requires an iterable of length 5 + @overload + def urlunsplit(components: Iterable[None]) -> Literal[b""]: ... # type: ignore[overload-overlap] + @overload + def urlunsplit(components: Iterable[AnyStr | None]) -> AnyStr: ... -# Requires an iterable of length 5 -@overload -def urlunsplit(components: Iterable[None]) -> Literal[b""]: ... # type: ignore[overload-overlap] -@overload -def urlunsplit(components: Iterable[AnyStr | None]) -> AnyStr: ... def unwrap(url: str) -> str: ... diff --git a/mypy/typeshed/stdlib/urllib/request.pyi b/mypy/typeshed/stdlib/urllib/request.pyi index d6bb2f9647824..0ec26052b23c6 100644 --- a/mypy/typeshed/stdlib/urllib/request.pyi +++ b/mypy/typeshed/stdlib/urllib/request.pyi @@ -6,8 +6,8 @@ from email.message import Message from http.client import HTTPConnection, HTTPMessage, HTTPResponse from http.cookiejar import CookieJar from re import Pattern -from typing import IO, Any, ClassVar, Literal, NoReturn, Protocol, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias, deprecated +from typing import IO, Any, ClassVar, Literal, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import deprecated from urllib.error import HTTPError as HTTPError from urllib.response import addclosehook, addinfourl @@ -124,6 +124,7 @@ class Request: def full_url(self, value: str) -> None: ... @full_url.deleter def full_url(self) -> None: ... + type: str host: str origin_req_host: str @@ -150,10 +151,12 @@ class Request: def remove_header(self, header_name: str) -> None: ... def get_full_url(self) -> str: ... def set_proxy(self, host: str, type: str) -> None: ... + @overload def get_header(self, header_name: str) -> str | None: ... @overload def get_header(self, header_name: str, default: _T) -> str | _T: ... + def header_items(self) -> list[tuple[str, str]]: ... def has_proxy(self) -> bool: ... diff --git a/mypy/typeshed/stdlib/uu.pyi b/mypy/typeshed/stdlib/uu.pyi index 324053e04337c..62bf6cb05f59d 100644 --- a/mypy/typeshed/stdlib/uu.pyi +++ b/mypy/typeshed/stdlib/uu.pyi @@ -1,5 +1,4 @@ -from typing import BinaryIO -from typing_extensions import TypeAlias +from typing import BinaryIO, TypeAlias __all__ = ["Error", "encode", "decode"] diff --git a/mypy/typeshed/stdlib/uuid.pyi b/mypy/typeshed/stdlib/uuid.pyi index 055f4def311cd..06285e03aea7e 100644 --- a/mypy/typeshed/stdlib/uuid.pyi +++ b/mypy/typeshed/stdlib/uuid.pyi @@ -2,8 +2,8 @@ import builtins import sys from _typeshed import Unused from enum import Enum -from typing import Final, NoReturn -from typing_extensions import LiteralString, TypeAlias +from typing import Final, NoReturn, TypeAlias +from typing_extensions import LiteralString _FieldsType: TypeAlias = tuple[int, int, int, int, int, int] diff --git a/mypy/typeshed/stdlib/warnings.pyi b/mypy/typeshed/stdlib/warnings.pyi index 49c98cb07540e..e17b6e3a25b32 100644 --- a/mypy/typeshed/stdlib/warnings.pyi +++ b/mypy/typeshed/stdlib/warnings.pyi @@ -3,8 +3,8 @@ import sys from _warnings import warn as warn, warn_explicit as warn_explicit from collections.abc import Sequence from types import ModuleType, TracebackType -from typing import Any, Generic, Literal, TextIO, overload -from typing_extensions import LiteralString, TypeAlias, TypeVar +from typing import Any, Generic, Literal, TextIO, TypeAlias, overload +from typing_extensions import LiteralString, TypeVar __all__ = [ "warn", @@ -27,7 +27,9 @@ if sys.version_info >= (3, 14): _ActionKind: TypeAlias = Literal["default", "error", "ignore", "always", "module", "once"] else: _ActionKind: TypeAlias = Literal["default", "error", "ignore", "always", "all", "module", "once"] -filters: Sequence[tuple[str, re.Pattern[str] | None, type[Warning], re.Pattern[str] | None, int]] # undocumented, do not mutate +filters: Sequence[ + tuple[str, re.Pattern[str] | None, type[Warning] | tuple[type[Warning], ...], re.Pattern[str] | None, int] +] # undocumented, do not mutate def showwarning( message: Warning | str, @@ -43,7 +45,9 @@ def formatwarning( def filterwarnings( action: _ActionKind, message: str = "", category: type[Warning] = ..., module: str = "", lineno: int = 0, append: bool = False ) -> None: ... -def simplefilter(action: _ActionKind, category: type[Warning] = ..., lineno: int = 0, append: bool = False) -> None: ... +def simplefilter( + action: _ActionKind, category: type[Warning] | tuple[type[Warning], ...] = ..., lineno: int = 0, append: bool = False +) -> None: ... def resetwarnings() -> None: ... class _OptionError(Exception): ... @@ -56,16 +60,32 @@ class WarningMessage: file: TextIO | None line: str | None source: Any | None - def __init__( - self, - message: Warning | str, - category: type[Warning], - filename: str, - lineno: int, - file: TextIO | None = None, - line: str | None = None, - source: Any | None = None, - ) -> None: ... + if sys.version_info >= (3, 15): + module: str | None + if sys.version_info >= (3, 15): + def __init__( + self, + message: Warning | str, + category: type[Warning], + filename: str, + lineno: int, + file: TextIO | None = None, + line: str | None = None, + source: Any | None = None, + module: str | None = None, + ) -> None: ... + + else: + def __init__( + self, + message: Warning | str, + category: type[Warning], + filename: str, + lineno: int, + file: TextIO | None = None, + line: str | None = None, + source: Any | None = None, + ) -> None: ... class catch_warnings(Generic[_W_co]): if sys.version_info >= (3, 11): @@ -76,7 +96,7 @@ class catch_warnings(Generic[_W_co]): record: Literal[False] = False, module: ModuleType | None = None, action: _ActionKind | None = None, - category: type[Warning] = ..., + category: type[Warning] | tuple[type[Warning], ...] = ..., lineno: int = 0, append: bool = False, ) -> None: ... @@ -87,7 +107,7 @@ class catch_warnings(Generic[_W_co]): record: Literal[True], module: ModuleType | None = None, action: _ActionKind | None = None, - category: type[Warning] = ..., + category: type[Warning] | tuple[type[Warning], ...] = ..., lineno: int = 0, append: bool = False, ) -> None: ... @@ -98,7 +118,7 @@ class catch_warnings(Generic[_W_co]): record: bool, module: ModuleType | None = None, action: _ActionKind | None = None, - category: type[Warning] = ..., + category: type[Warning] | tuple[type[Warning], ...] = ..., lineno: int = 0, append: bool = False, ) -> None: ... diff --git a/mypy/typeshed/stdlib/wave.pyi b/mypy/typeshed/stdlib/wave.pyi index fd7dbfade884b..74f8b8a9e3cf7 100644 --- a/mypy/typeshed/stdlib/wave.pyi +++ b/mypy/typeshed/stdlib/wave.pyi @@ -1,15 +1,23 @@ import sys -from _typeshed import ReadableBuffer, Unused -from typing import IO, Any, BinaryIO, Final, Literal, NamedTuple, NoReturn, overload -from typing_extensions import Self, TypeAlias, deprecated +from _typeshed import ReadableBuffer, StrOrBytesPath, Unused +from typing import IO, Any, BinaryIO, Final, Literal, NamedTuple, NoReturn, TypeAlias, overload +from typing_extensions import Self, deprecated __all__ = ["open", "Error", "Wave_read", "Wave_write"] +if sys.version_info >= (3, 15): + __all__ += ["WAVE_FORMAT_PCM", "WAVE_FORMAT_IEEE_FLOAT", "WAVE_FORMAT_EXTENSIBLE"] -_File: TypeAlias = str | IO[bytes] +if sys.version_info >= (3, 15): + _File: TypeAlias = StrOrBytesPath | IO[bytes] +else: + _File: TypeAlias = str | IO[bytes] class Error(Exception): ... WAVE_FORMAT_PCM: Final = 0x0001 +if sys.version_info >= (3, 15): + WAVE_FORMAT_IEEE_FLOAT: Final = 0x0003 + WAVE_FORMAT_EXTENSIBLE: Final = 0xFFFE class _wave_params(NamedTuple): nchannels: int @@ -32,17 +40,17 @@ class Wave_read: def getnframes(self) -> int: ... def getsampwidth(self) -> int: ... def getframerate(self) -> int: ... + if sys.version_info >= (3, 15): + def getformat(self) -> int: ... + def getcomptype(self) -> str: ... def getcompname(self) -> str: ... def getparams(self) -> _wave_params: ... - if sys.version_info >= (3, 13): + if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def getmarkers(self) -> None: ... @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def getmark(self, id: Any) -> NoReturn: ... - else: - def getmarkers(self) -> None: ... - def getmark(self, id: Any) -> NoReturn: ... def setpos(self, pos: int) -> None: ... def readframes(self, nframes: int) -> bytes: ... @@ -58,24 +66,31 @@ class Wave_write: def getsampwidth(self) -> int: ... def setframerate(self, framerate: float) -> None: ... def getframerate(self) -> int: ... + if sys.version_info >= (3, 15): + def setformat(self, format: int) -> None: ... + def getformat(self) -> int: ... + def setnframes(self, nframes: int) -> None: ... def getnframes(self) -> int: ... def setcomptype(self, comptype: str, compname: str) -> None: ... def getcomptype(self) -> str: ... def getcompname(self) -> str: ... - def setparams(self, params: _wave_params | tuple[int, int, int, int, str, str]) -> None: ... + if sys.version_info >= (3, 15): + def setparams( + self, params: _wave_params | tuple[int, int, int, int, str, str] | tuple[int, int, int, int, str, str, int] + ) -> None: ... + else: + def setparams(self, params: _wave_params | tuple[int, int, int, int, str, str]) -> None: ... + def getparams(self) -> _wave_params: ... - if sys.version_info >= (3, 13): + + if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def setmark(self, id: Any, pos: Any, name: Any) -> NoReturn: ... @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def getmark(self, id: Any) -> NoReturn: ... @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") def getmarkers(self) -> None: ... - else: - def setmark(self, id: Any, pos: Any, name: Any) -> NoReturn: ... - def getmark(self, id: Any) -> NoReturn: ... - def getmarkers(self) -> None: ... def tell(self) -> int: ... def writeframesraw(self, data: ReadableBuffer) -> None: ... diff --git a/mypy/typeshed/stdlib/weakref.pyi b/mypy/typeshed/stdlib/weakref.pyi index 76ab86b957a13..3308ae42e1cfb 100644 --- a/mypy/typeshed/stdlib/weakref.pyi +++ b/mypy/typeshed/stdlib/weakref.pyi @@ -3,8 +3,8 @@ from _weakref import getweakrefcount as getweakrefcount, getweakrefs as getweakr from _weakrefset import WeakSet as WeakSet from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping from types import GenericAlias -from typing import Any, ClassVar, Generic, TypeVar, final, overload -from typing_extensions import ParamSpec, Self, disjoint_base +from typing import Any, ClassVar, Generic, ParamSpec, TypeVar, final, overload +from typing_extensions import Self, disjoint_base __all__ = [ "ref", @@ -89,6 +89,7 @@ class WeakValueDictionary(MutableMapping[_KT, _VT]): /, **kwargs: _VT, ) -> None: ... + def __len__(self) -> int: ... def __getitem__(self, key: _KT) -> _VT: ... def __setitem__(self, key: _KT, value: _VT) -> None: ... @@ -98,12 +99,14 @@ class WeakValueDictionary(MutableMapping[_KT, _VT]): def copy(self) -> WeakValueDictionary[_KT, _VT]: ... __copy__ = copy def __deepcopy__(self, memo: Any) -> Self: ... + @overload def get(self, key: _KT, default: None = None) -> _VT | None: ... @overload def get(self, key: _KT, default: _VT) -> _VT: ... @overload def get(self, key: _KT, default: _T) -> _VT | _T: ... + # These are incompatible with Mapping def keys(self) -> Iterator[_KT]: ... # type: ignore[override] def values(self) -> Iterator[_VT]: ... # type: ignore[override] @@ -111,20 +114,24 @@ class WeakValueDictionary(MutableMapping[_KT, _VT]): def itervaluerefs(self) -> Iterator[KeyedRef[_KT, _VT]]: ... def valuerefs(self) -> list[KeyedRef[_KT, _VT]]: ... def setdefault(self, key: _KT, default: _VT) -> _VT: ... + @overload def pop(self, key: _KT) -> _VT: ... @overload def pop(self, key: _KT, default: _VT) -> _VT: ... @overload def pop(self, key: _KT, default: _T) -> _VT | _T: ... + @overload def update(self, other: SupportsKeysAndGetItem[_KT, _VT], /, **kwargs: _VT) -> None: ... @overload def update(self, other: Iterable[tuple[_KT, _VT]], /, **kwargs: _VT) -> None: ... @overload def update(self, other: None = None, /, **kwargs: _VT) -> None: ... + def __or__(self, other: Mapping[_T1, _T2]) -> WeakValueDictionary[_KT | _T1, _VT | _T2]: ... def __ror__(self, other: Mapping[_T1, _T2]) -> WeakValueDictionary[_KT | _T1, _VT | _T2]: ... + # WeakValueDictionary.__ior__ should be kept roughly in line with MutableMapping.update() @overload # type: ignore[misc] def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... @@ -142,6 +149,7 @@ class WeakKeyDictionary(MutableMapping[_KT, _VT]): def __init__(self, dict: None = None) -> None: ... @overload def __init__(self, dict: Mapping[_KT, _VT] | Iterable[tuple[_KT, _VT]]) -> None: ... + def __len__(self) -> int: ... def __getitem__(self, key: _KT) -> _VT: ... def __setitem__(self, key: _KT, value: _VT) -> None: ... @@ -151,36 +159,43 @@ class WeakKeyDictionary(MutableMapping[_KT, _VT]): def copy(self) -> WeakKeyDictionary[_KT, _VT]: ... __copy__ = copy def __deepcopy__(self, memo: Any) -> Self: ... + @overload def get(self, key: _KT, default: None = None) -> _VT | None: ... @overload def get(self, key: _KT, default: _VT) -> _VT: ... @overload def get(self, key: _KT, default: _T) -> _VT | _T: ... + # These are incompatible with Mapping def keys(self) -> Iterator[_KT]: ... # type: ignore[override] def values(self) -> Iterator[_VT]: ... # type: ignore[override] def items(self) -> Iterator[tuple[_KT, _VT]]: ... # type: ignore[override] def keyrefs(self) -> list[ref[_KT]]: ... + # Keep WeakKeyDictionary.setdefault in line with MutableMapping.setdefault, modulo positional-only differences @overload def setdefault(self: WeakKeyDictionary[_KT, _VT | None], key: _KT, default: None = None) -> _VT: ... @overload def setdefault(self, key: _KT, default: _VT) -> _VT: ... + @overload def pop(self, key: _KT) -> _VT: ... @overload def pop(self, key: _KT, default: _VT) -> _VT: ... @overload def pop(self, key: _KT, default: _T) -> _VT | _T: ... + @overload def update(self, dict: SupportsKeysAndGetItem[_KT, _VT], /, **kwargs: _VT) -> None: ... @overload def update(self, dict: Iterable[tuple[_KT, _VT]], /, **kwargs: _VT) -> None: ... @overload def update(self, dict: None = None, /, **kwargs: _VT) -> None: ... + def __or__(self, other: Mapping[_T1, _T2]) -> WeakKeyDictionary[_KT | _T1, _VT | _T2]: ... def __ror__(self, other: Mapping[_T1, _T2]) -> WeakKeyDictionary[_KT | _T1, _VT | _T2]: ... + # WeakKeyDictionary.__ior__ should be kept roughly in line with MutableMapping.update() @overload # type: ignore[misc] def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... diff --git a/mypy/typeshed/stdlib/webbrowser.pyi b/mypy/typeshed/stdlib/webbrowser.pyi index 56c30f8727277..40b57c3c6a097 100644 --- a/mypy/typeshed/stdlib/webbrowser.pyi +++ b/mypy/typeshed/stdlib/webbrowser.pyi @@ -64,16 +64,10 @@ if sys.platform == "win32": if sys.platform == "darwin": if sys.version_info < (3, 13): - if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") - class MacOSX(BaseBrowser): - def __init__(self, name: str) -> None: ... - def open(self, url: str, new: int = 0, autoraise: bool = True) -> bool: ... - - else: - class MacOSX(BaseBrowser): - def __init__(self, name: str) -> None: ... - def open(self, url: str, new: int = 0, autoraise: bool = True) -> bool: ... + @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + class MacOSX(BaseBrowser): + def __init__(self, name: str) -> None: ... + def open(self, url: str, new: int = 0, autoraise: bool = True) -> bool: ... class MacOSXOSAScript(BaseBrowser): # In runtime this class does not have `name` and `basename` if sys.version_info >= (3, 11): diff --git a/mypy/typeshed/stdlib/winreg.pyi b/mypy/typeshed/stdlib/winreg.pyi index a654bbcdfb615..8a886112f8a14 100644 --- a/mypy/typeshed/stdlib/winreg.pyi +++ b/mypy/typeshed/stdlib/winreg.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import ReadableBuffer, Unused from types import TracebackType -from typing import Any, Final, Literal, final, overload -from typing_extensions import Self, TypeAlias +from typing import Any, Final, Literal, TypeAlias, final, overload +from typing_extensions import Self if sys.platform == "win32": _KeyType: TypeAlias = HKEYType | int @@ -12,6 +12,9 @@ if sys.platform == "win32": def CreateKeyEx(key: _KeyType, sub_key: str | None, reserved: int = 0, access: int = 131078) -> HKEYType: ... def DeleteKey(key: _KeyType, sub_key: str, /) -> None: ... def DeleteKeyEx(key: _KeyType, sub_key: str, access: int = 256, reserved: int = 0) -> None: ... + if sys.version_info >= (3, 15): + def DeleteTree(key: _KeyType, sub_key: str | None = None, /) -> None: ... + def DeleteValue(key: _KeyType, value: str, /) -> None: ... def EnumKey(key: _KeyType, index: int, /) -> str: ... def EnumValue(key: _KeyType, index: int, /) -> tuple[str, Any, int]: ... @@ -25,6 +28,7 @@ if sys.platform == "win32": def QueryValueEx(key: _KeyType, name: str, /) -> tuple[Any, int]: ... def SaveKey(key: _KeyType, file_name: str, /) -> None: ... def SetValue(key: _KeyType, sub_key: str | None, type: int, value: str, /) -> None: ... + @overload # type=REG_DWORD|REG_QWORD def SetValueEx( key: _KeyType, value_name: str | None, reserved: Unused, type: Literal[4, 5], value: int | None, / @@ -55,6 +59,7 @@ if sys.platform == "win32": value: int | str | list[str] | ReadableBuffer | None, /, ) -> None: ... + def DisableReflectionKey(key: _KeyType, /) -> None: ... def EnableReflectionKey(key: _KeyType, /) -> None: ... def QueryReflectionKey(key: _KeyType, /) -> bool: ... diff --git a/mypy/typeshed/stdlib/winsound.pyi b/mypy/typeshed/stdlib/winsound.pyi index 39dfa7b8b9c42..9c7f314fd6ca4 100644 --- a/mypy/typeshed/stdlib/winsound.pyi +++ b/mypy/typeshed/stdlib/winsound.pyi @@ -30,9 +30,11 @@ if sys.platform == "win32": MB_ICONWARNING: Final = 48 def Beep(frequency: int, duration: int) -> None: ... + # Can actually accept anything ORed with 4, and if not it's definitely str, but that's inexpressible @overload def PlaySound(sound: ReadableBuffer | None, flags: Literal[4]) -> None: ... @overload def PlaySound(sound: str | ReadableBuffer | None, flags: int) -> None: ... + def MessageBeep(type: int = 0) -> None: ... diff --git a/mypy/typeshed/stdlib/wsgiref/headers.pyi b/mypy/typeshed/stdlib/wsgiref/headers.pyi index 9febad4b32775..6a0fb571a0d08 100644 --- a/mypy/typeshed/stdlib/wsgiref/headers.pyi +++ b/mypy/typeshed/stdlib/wsgiref/headers.pyi @@ -1,6 +1,5 @@ from re import Pattern -from typing import Final, overload -from typing_extensions import TypeAlias +from typing import Final, TypeAlias, overload _HeaderList: TypeAlias = list[tuple[str, str]] @@ -14,10 +13,12 @@ class Headers: def __getitem__(self, name: str) -> str | None: ... def __contains__(self, name: str) -> bool: ... def get_all(self, name: str) -> list[str]: ... + @overload def get(self, name: str, default: str) -> str: ... @overload def get(self, name: str, default: str | None = None) -> str | None: ... + def keys(self) -> list[str]: ... def values(self) -> list[str]: ... def items(self) -> _HeaderList: ... diff --git a/mypy/typeshed/stdlib/wsgiref/types.pyi b/mypy/typeshed/stdlib/wsgiref/types.pyi index 57276fd05ea84..b7fd998098568 100644 --- a/mypy/typeshed/stdlib/wsgiref/types.pyi +++ b/mypy/typeshed/stdlib/wsgiref/types.pyi @@ -1,7 +1,6 @@ from _typeshed import OptExcInfo from collections.abc import Callable, Iterable, Iterator -from typing import Any, Protocol -from typing_extensions import TypeAlias +from typing import Any, Protocol, TypeAlias __all__ = ["StartResponse", "WSGIEnvironment", "WSGIApplication", "InputStream", "ErrorStream", "FileWrapper"] diff --git a/mypy/typeshed/stdlib/wsgiref/validate.pyi b/mypy/typeshed/stdlib/wsgiref/validate.pyi index fa8a6bbb8d039..498e03aa36731 100644 --- a/mypy/typeshed/stdlib/wsgiref/validate.pyi +++ b/mypy/typeshed/stdlib/wsgiref/validate.pyi @@ -1,7 +1,6 @@ from _typeshed.wsgi import ErrorStream, InputStream, WSGIApplication from collections.abc import Callable, Iterable, Iterator -from typing import Any, NoReturn -from typing_extensions import TypeAlias +from typing import Any, NoReturn, TypeAlias __all__ = ["validator"] diff --git a/mypy/typeshed/stdlib/xml/__init__.pyi b/mypy/typeshed/stdlib/xml/__init__.pyi index 7a240965136e5..555d9b8f90a95 100644 --- a/mypy/typeshed/stdlib/xml/__init__.pyi +++ b/mypy/typeshed/stdlib/xml/__init__.pyi @@ -1,3 +1,9 @@ # At runtime, listing submodules in __all__ without them being imported is # valid, and causes them to be included in a star import. See #6523 +import sys + __all__ = ["dom", "parsers", "sax", "etree"] # noqa: F822 # pyright: ignore[reportUnsupportedDunderAll] + +if sys.version_info >= (3, 15): + __all__ += ["is_valid_name"] # pyright: ignore[reportUnsupportedDunderAll] + from xml.utils import is_valid_name as is_valid_name, is_valid_text as is_valid_text diff --git a/mypy/typeshed/stdlib/xml/dom/expatbuilder.pyi b/mypy/typeshed/stdlib/xml/dom/expatbuilder.pyi index 2b9ac88769700..e410671829358 100644 --- a/mypy/typeshed/stdlib/xml/dom/expatbuilder.pyi +++ b/mypy/typeshed/stdlib/xml/dom/expatbuilder.pyi @@ -1,6 +1,5 @@ from _typeshed import ReadableBuffer, SupportsRead -from typing import Any, Final, NoReturn -from typing_extensions import TypeAlias +from typing import Any, Final, NoReturn, TypeAlias from xml.dom.minidom import Document, DocumentFragment, DOMImplementation, Element, Node, TypeInfo from xml.dom.xmlbuilder import DOMBuilderFilter, Options from xml.parsers.expat import XMLParserType diff --git a/mypy/typeshed/stdlib/xml/dom/minidom.pyi b/mypy/typeshed/stdlib/xml/dom/minidom.pyi index e0431417aa3c0..c54b0de86e556 100644 --- a/mypy/typeshed/stdlib/xml/dom/minidom.pyi +++ b/mypy/typeshed/stdlib/xml/dom/minidom.pyi @@ -3,8 +3,8 @@ from _collections_abc import dict_keys, dict_values from _typeshed import Incomplete, ReadableBuffer, SupportsRead, SupportsWrite from collections.abc import Iterable, Sequence from types import TracebackType -from typing import Any, ClassVar, Generic, Literal, NoReturn, Protocol, TypeVar, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, ClassVar, Generic, Literal, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Self from xml.dom.minicompat import EmptyNodeList, NodeList from xml.dom.xmlbuilder import DocumentLS, DOMImplementationLS from xml.sax.xmlreader import XMLReader @@ -52,6 +52,7 @@ def parse( file: str | SupportsRead[ReadableBuffer | str], parser: XMLReader | None = None, bufsize: int | None = None ) -> Document: ... def parseString(string: str | ReadableBuffer, parser: XMLReader | None = None) -> Document: ... + @overload def getDOMImplementation(features: None = None) -> DOMImplementation: ... @overload @@ -89,10 +90,12 @@ class Node(xml.dom.Node): @property def localName(self) -> str | None: ... # non-null only for Element and Attr def __bool__(self) -> Literal[True]: ... + @overload def toxml(self, encoding: str, standalone: bool | None = None) -> bytes: ... @overload def toxml(self, encoding: None = None, standalone: bool | None = None) -> str: ... + @overload def toprettyxml( self, @@ -122,6 +125,7 @@ class Node(xml.dom.Node): encoding: str, standalone: bool | None = None, ) -> bytes: ... + def hasChildNodes(self) -> bool: ... def insertBefore( # type: ignore[misc] self: _NodesWithChildren, # pyright: ignore[reportGeneralTypeIssues] @@ -131,6 +135,7 @@ class Node(xml.dom.Node): def appendChild( # type: ignore[misc] self: _NodesWithChildren, node: _ChildNodePlusFragmentVar # pyright: ignore[reportGeneralTypeIssues] ) -> _ChildNodePlusFragmentVar: ... + @overload def replaceChild( # type: ignore[misc] self: _NodesWithChildren, newChild: DocumentFragment, oldChild: _ChildNodeVar @@ -139,6 +144,7 @@ class Node(xml.dom.Node): def replaceChild( # type: ignore[misc] self: _NodesWithChildren, newChild: _NodesThatAreChildren, oldChild: _ChildNodeVar ) -> _ChildNodeVar | None: ... + def removeChild(self: _NodesWithChildren, oldChild: _ChildNodeVar) -> _ChildNodeVar: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] def normalize(self: _NodesWithChildren) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] def cloneNode(self, deep: bool) -> Self | None: ... @@ -178,10 +184,12 @@ class DocumentFragment(Node): self, newChild: _DFChildrenPlusFragment, refChild: _DocumentFragmentChildren | None ) -> _DFChildrenPlusFragment: ... def appendChild(self, node: _DFChildrenPlusFragment) -> _DFChildrenPlusFragment: ... # type: ignore[override] + @overload # type: ignore[override] def replaceChild(self, newChild: DocumentFragment, oldChild: _DFChildrenVar) -> _DFChildrenVar | DocumentFragment: ... @overload def replaceChild(self, newChild: _DocumentFragmentChildren, oldChild: _DFChildrenVar) -> _DFChildrenVar | None: ... # type: ignore[override] + def removeChild(self, oldChild: _DFChildrenVar) -> _DFChildrenVar: ... # type: ignore[override] _AttrChildrenVar = TypeVar("_AttrChildrenVar", bound=_AttrChildren) @@ -223,10 +231,12 @@ class Attr(Node): def schemaType(self) -> TypeInfo: ... def insertBefore(self, newChild: _AttrChildrenPlusFragment, refChild: _AttrChildren | None) -> _AttrChildrenPlusFragment: ... # type: ignore[override] def appendChild(self, node: _AttrChildrenPlusFragment) -> _AttrChildrenPlusFragment: ... # type: ignore[override] + @overload # type: ignore[override] def replaceChild(self, newChild: DocumentFragment, oldChild: _AttrChildrenVar) -> _AttrChildrenVar | DocumentFragment: ... @overload def replaceChild(self, newChild: _AttrChildren, oldChild: _AttrChildrenVar) -> _AttrChildrenVar | None: ... # type: ignore[override] + def removeChild(self, oldChild: _AttrChildrenVar) -> _AttrChildrenVar: ... # type: ignore[override] # In the DOM, this interface isn't specific to Attr, but our implementation is @@ -339,12 +349,14 @@ class Element(Node): self, newChild: _ElementChildrenPlusFragment, refChild: _ElementChildren | None ) -> _ElementChildrenPlusFragment: ... def appendChild(self, node: _ElementChildrenPlusFragment) -> _ElementChildrenPlusFragment: ... # type: ignore[override] + @overload # type: ignore[override] def replaceChild( self, newChild: DocumentFragment, oldChild: _ElementChildrenVar ) -> _ElementChildrenVar | DocumentFragment: ... @overload def replaceChild(self, newChild: _ElementChildren, oldChild: _ElementChildrenVar) -> _ElementChildrenVar | None: ... # type: ignore[override] + def removeChild(self, oldChild: _ElementChildrenVar) -> _ElementChildrenVar: ... # type: ignore[override] class Childless: @@ -661,15 +673,18 @@ class Document(Node, DocumentLS): encoding: str | None = None, standalone: bool | None = None, ) -> None: ... + @overload def renameNode(self, n: Element, namespaceURI: str, name: str) -> Element: ... @overload def renameNode(self, n: Attr, namespaceURI: str, name: str) -> Attr: ... @overload def renameNode(self, n: Element | Attr, namespaceURI: str, name: str) -> Element | Attr: ... + def insertBefore( self, newChild: _DocumentChildrenPlusFragment, refChild: _DocumentChildren | None # type: ignore[override] ) -> _DocumentChildrenPlusFragment: ... + @overload # type: ignore[override] def replaceChild( self, newChild: DocumentFragment, oldChild: _DocumentChildrenVar diff --git a/mypy/typeshed/stdlib/xml/dom/pulldom.pyi b/mypy/typeshed/stdlib/xml/dom/pulldom.pyi index df7a3ad0eddb0..4ede390155855 100644 --- a/mypy/typeshed/stdlib/xml/dom/pulldom.pyi +++ b/mypy/typeshed/stdlib/xml/dom/pulldom.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import Incomplete, Unused from collections.abc import MutableSequence, Sequence -from typing import Final, Literal, NoReturn -from typing_extensions import Self, TypeAlias +from typing import Final, Literal, NoReturn, TypeAlias +from typing_extensions import Self from xml.dom.minidom import Comment, Document, DOMImplementation, Element, ProcessingInstruction, Text from xml.sax import _SupportsReadClose from xml.sax.handler import ContentHandler diff --git a/mypy/typeshed/stdlib/xml/etree/ElementInclude.pyi b/mypy/typeshed/stdlib/xml/etree/ElementInclude.pyi index 10784e7d40214..5db08fb0df053 100644 --- a/mypy/typeshed/stdlib/xml/etree/ElementInclude.pyi +++ b/mypy/typeshed/stdlib/xml/etree/ElementInclude.pyi @@ -22,6 +22,7 @@ class FatalIncludeError(SyntaxError): ... def default_loader(href: FileDescriptorOrPath, parse: Literal["xml"], encoding: str | None = None) -> Element: ... @overload def default_loader(href: FileDescriptorOrPath, parse: Literal["text"], encoding: str | None = None) -> str: ... + def include(elem: Element, loader: _Loader | None = None, base_url: str | None = None, max_depth: int | None = 6) -> None: ... class LimitedRecursiveIncludeError(FatalIncludeError): ... diff --git a/mypy/typeshed/stdlib/xml/etree/ElementPath.pyi b/mypy/typeshed/stdlib/xml/etree/ElementPath.pyi index 5c03dd014b639..1dd6f86cead63 100644 --- a/mypy/typeshed/stdlib/xml/etree/ElementPath.pyi +++ b/mypy/typeshed/stdlib/xml/etree/ElementPath.pyi @@ -1,7 +1,6 @@ from collections.abc import Callable, Generator, Iterable from re import Pattern -from typing import Any, Final, Literal, TypeVar, overload -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, TypeAlias, TypeVar, overload from xml.etree.ElementTree import Element xpath_tokenizer_re: Final[Pattern[str]] @@ -33,8 +32,10 @@ def iterfind( # type: ignore[overload-overlap] ) -> None: ... @overload def iterfind(elem: Element[Any], path: str, namespaces: dict[str, str] | None = None) -> Generator[Element]: ... + def find(elem: Element[Any], path: str, namespaces: dict[str, str] | None = None) -> Element | None: ... def findall(elem: Element[Any], path: str, namespaces: dict[str, str] | None = None) -> list[Element]: ... + @overload def findtext(elem: Element[Any], path: str, default: None = None, namespaces: dict[str, str] | None = None) -> str | None: ... @overload diff --git a/mypy/typeshed/stdlib/xml/etree/ElementTree.pyi b/mypy/typeshed/stdlib/xml/etree/ElementTree.pyi index 6340a44bd51c8..c77af2cb4dd11 100644 --- a/mypy/typeshed/stdlib/xml/etree/ElementTree.pyi +++ b/mypy/typeshed/stdlib/xml/etree/ElementTree.pyi @@ -2,8 +2,8 @@ import sys from _collections_abc import dict_keys from _typeshed import FileDescriptorOrPath, ReadableBuffer, SupportsRead, SupportsWrite from collections.abc import Callable, Generator, ItemsView, Iterable, Iterator, Mapping, Sequence -from typing import Any, Final, Generic, Literal, Protocol, SupportsIndex, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias, TypeGuard, deprecated, disjoint_base +from typing import Any, Final, Generic, Literal, Protocol, SupportsIndex, TypeAlias, TypeGuard, TypeVar, overload, type_check_only +from typing_extensions import deprecated, disjoint_base from xml.parsers.expat import XMLParserType __all__ = [ @@ -27,13 +27,14 @@ __all__ = [ "tostring", "tostringlist", "TreeBuilder", - "VERSION", "XML", "XMLID", "XMLParser", "XMLPullParser", "register_namespace", ] +if sys.version_info < (3, 15): + __all__ += ["VERSION"] _T = TypeVar("_T") _FileRead: TypeAlias = FileDescriptorOrPath | SupportsRead[bytes] | SupportsRead[str] @@ -48,6 +49,7 @@ class ParseError(SyntaxError): # In reality it works based on `.tag` attribute duck typing. def iselement(element: object) -> TypeGuard[Element]: ... + @overload def canonicalize( xml_data: str | ReadableBuffer | None = None, @@ -96,21 +98,26 @@ class Element(Generic[_Tag]): def extend(self, elements: Iterable[Element[Any]], /) -> None: ... def find(self, path: str, namespaces: dict[str, str] | None = None) -> Element | None: ... def findall(self, path: str, namespaces: dict[str, str] | None = None) -> list[Element]: ... + @overload def findtext(self, path: str, default: None = None, namespaces: dict[str, str] | None = None) -> str | None: ... @overload def findtext(self, path: str, default: _T, namespaces: dict[str, str] | None = None) -> _T | str: ... + @overload def get(self, key: str, default: None = None) -> str | None: ... @overload def get(self, key: str, default: _T) -> str | _T: ... + def insert(self, index: int, subelement: Element[Any], /) -> None: ... def items(self) -> ItemsView[str, str]: ... def iter(self, tag: str | None = None) -> Generator[Element]: ... + @overload def iterfind(self, path: Literal[""], namespaces: dict[str, str] | None = None) -> None: ... # type: ignore[overload-overlap] @overload def iterfind(self, path: str, namespaces: dict[str, str] | None = None) -> Generator[Element]: ... + def itertext(self) -> Generator[str]: ... def keys(self) -> dict_keys[str, str]: ... # makeelement returns the type of self in Python impl, but not in C impl @@ -120,13 +127,16 @@ class Element(Generic[_Tag]): def __copy__(self) -> Element[_Tag]: ... # returns the type of self in Python impl, but not in C impl def __deepcopy__(self, memo: Any, /) -> Element: ... # Only exists in C impl def __delitem__(self, key: SupportsIndex | slice, /) -> None: ... + @overload def __getitem__(self, key: SupportsIndex, /) -> Element: ... @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> list[Element]: ... + def __len__(self) -> int: ... # Doesn't actually exist at runtime, but instance of the class are indeed iterable due to __getitem__. def __iter__(self) -> Iterator[Element]: ... + @overload def __setitem__(self, key: SupportsIndex, value: Element[Any], /) -> None: ... @overload @@ -161,15 +171,19 @@ class ElementTree(Generic[_Root]): def parse(self, source: _FileRead, parser: XMLParser | None = None) -> Element: ... def iter(self, tag: str | None = None) -> Generator[Element]: ... def find(self, path: str, namespaces: dict[str, str] | None = None) -> Element | None: ... + @overload def findtext(self, path: str, default: None = None, namespaces: dict[str, str] | None = None) -> str | None: ... @overload def findtext(self, path: str, default: _T, namespaces: dict[str, str] | None = None) -> _T | str: ... + def findall(self, path: str, namespaces: dict[str, str] | None = None) -> list[Element]: ... + @overload def iterfind(self, path: Literal[""], namespaces: dict[str, str] | None = None) -> None: ... # type: ignore[overload-overlap] @overload def iterfind(self, path: str, namespaces: dict[str, str] | None = None) -> Generator[Element]: ... + def write( self, file_or_filename: _FileWrite, @@ -185,6 +199,7 @@ class ElementTree(Generic[_Root]): HTML_EMPTY: Final[set[str]] def register_namespace(prefix: str, uri: str) -> None: ... + @overload def tostring( element: Element[Any], @@ -215,6 +230,7 @@ def tostring( default_namespace: str | None = None, short_empty_elements: bool = True, ) -> Any: ... + @overload def tostringlist( element: Element[Any], @@ -245,32 +261,47 @@ def tostringlist( default_namespace: str | None = None, short_empty_elements: bool = True, ) -> list[Any]: ... + def dump(elem: Element[Any] | ElementTree[Any]) -> None: ... def indent(tree: Element[Any] | ElementTree[Any], space: str = " ", level: int = 0) -> None: ... def parse(source: _FileRead, parser: XMLParser[Any] | None = None) -> ElementTree[Element]: ... -# This class is defined inside the body of iterparse +# The type of the second element of the tuple yielded by iterparse depends +# on the event type in the first element of the tuple: +# * start, end: Element[str] +# * comment, pi: Element[_ElementCallable] +# * start-ns: tuple[str, str] (prefix, uri) +# * end-ns: None +_EventT_co = TypeVar("_EventT_co", bound=Element[str] | Element[_ElementCallable] | tuple[str, str] | None, covariant=True) +_EventType: TypeAlias = Literal["start", "end", "comment", "pi", "start-ns", "end-ns"] + +# This class is defined inside the body of iterparse. @type_check_only -class _IterParseIterator(Iterator[tuple[str, Element]], Protocol): - def __next__(self) -> tuple[str, Element]: ... +class _IterParseIterator(Iterator[tuple[_EventType, _EventT_co]], Protocol[_EventT_co]): if sys.version_info >= (3, 13): def close(self) -> None: ... if sys.version_info >= (3, 11): def __del__(self) -> None: ... +# See the comment for _EventT_co above for possible iterator types. @overload -def iterparse(source: _FileRead, events: Sequence[str] | None = None) -> _IterParseIterator: ... +def iterparse(source: _FileRead, events: Iterable[_EventType]) -> _IterParseIterator[Any]: ... +@overload +def iterparse(source: _FileRead, events: None = None) -> _IterParseIterator[Element[str]]: ... + +# In case a custom parser is passed, the type of the second element of the tuple +# yielded by iterparse depends on the parser. @overload @deprecated("The `parser` parameter is deprecated since Python 3.4.") -def iterparse(source: _FileRead, events: Sequence[str] | None = None, parser: XMLParser | None = None) -> _IterParseIterator: ... +def iterparse(source: _FileRead, events: Iterable[_EventType], parser: XMLParser | None = None) -> _IterParseIterator[Any]: ... _EventQueue: TypeAlias = tuple[str] | tuple[str, tuple[str, str]] | tuple[str, None] -class XMLPullParser(Generic[_E]): - def __init__(self, events: Sequence[str] | None = None, *, _parser: XMLParser[_E] | None = None) -> None: ... +class XMLPullParser(Generic[_EventT_co]): + def __init__(self, events: Iterable[_EventType] | None = None, *, _parser: XMLParser[_EventT_co] | None = None) -> None: ... def feed(self, data: str | ReadableBuffer) -> None: ... def close(self) -> None: ... - def read_events(self) -> Iterator[_EventQueue | tuple[str, _E]]: ... + def read_events(self) -> Iterator[_EventQueue | tuple[_EventType, _EventT_co]]: ... def flush(self) -> None: ... def XML(text: str | ReadableBuffer, parser: XMLParser | None = None) -> Element: ... diff --git a/mypy/typeshed/stdlib/xml/sax/__init__.pyi b/mypy/typeshed/stdlib/xml/sax/__init__.pyi index 679466fa34d2c..9b5c3bf4ddea9 100644 --- a/mypy/typeshed/stdlib/xml/sax/__init__.pyi +++ b/mypy/typeshed/stdlib/xml/sax/__init__.pyi @@ -1,8 +1,7 @@ import sys from _typeshed import ReadableBuffer, StrPath, SupportsRead, _T_co from collections.abc import Iterable -from typing import Final, Protocol, type_check_only -from typing_extensions import TypeAlias +from typing import Final, Protocol, TypeAlias, type_check_only from xml.sax._exceptions import ( SAXException as SAXException, SAXNotRecognizedException as SAXNotRecognizedException, diff --git a/mypy/typeshed/stdlib/xml/sax/expatreader.pyi b/mypy/typeshed/stdlib/xml/sax/expatreader.pyi index 3f9573a25f9aa..e29853ebc3fe3 100644 --- a/mypy/typeshed/stdlib/xml/sax/expatreader.pyi +++ b/mypy/typeshed/stdlib/xml/sax/expatreader.pyi @@ -1,13 +1,8 @@ -import sys from _typeshed import ReadableBuffer from collections.abc import Mapping -from typing import Any, Final, Literal, overload -from typing_extensions import TypeAlias +from typing import Any, Final, Literal, TypeAlias, overload from xml.sax import _Source, xmlreader -from xml.sax.handler import _ContentHandlerProtocol - -if sys.version_info >= (3, 10): - from xml.sax.handler import LexicalHandler +from xml.sax.handler import LexicalHandler, _ContentHandlerProtocol _BoolType: TypeAlias = Literal[0, 1] | bool @@ -33,26 +28,25 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator): def setContentHandler(self, handler: _ContentHandlerProtocol) -> None: ... def getFeature(self, name: str) -> _BoolType: ... def setFeature(self, name: str, state: _BoolType) -> None: ... - if sys.version_info >= (3, 10): - @overload - def getProperty(self, name: Literal["http://xml.org/sax/properties/lexical-handler"]) -> LexicalHandler | None: ... + @overload + def getProperty(self, name: Literal["http://xml.org/sax/properties/lexical-handler"]) -> LexicalHandler | None: ... @overload def getProperty(self, name: Literal["http://www.python.org/sax/properties/interning-dict"]) -> dict[str, Any] | None: ... @overload def getProperty(self, name: Literal["http://xml.org/sax/properties/xml-string"]) -> bytes | None: ... @overload def getProperty(self, name: str) -> object: ... - if sys.version_info >= (3, 10): - @overload - def setProperty(self, name: Literal["http://xml.org/sax/properties/lexical-handler"], value: LexicalHandler) -> None: ... + @overload + def setProperty(self, name: Literal["http://xml.org/sax/properties/lexical-handler"], value: LexicalHandler) -> None: ... @overload def setProperty( self, name: Literal["http://www.python.org/sax/properties/interning-dict"], value: dict[str, Any] ) -> None: ... @overload def setProperty(self, name: str, value: object) -> None: ... + def feed(self, data: str | ReadableBuffer, isFinal: bool = False) -> None: ... def flush(self) -> None: ... def close(self) -> None: ... diff --git a/mypy/typeshed/stdlib/xml/sax/handler.pyi b/mypy/typeshed/stdlib/xml/sax/handler.pyi index 5ecbfa6f1272c..e1e080ac2d1aa 100644 --- a/mypy/typeshed/stdlib/xml/sax/handler.pyi +++ b/mypy/typeshed/stdlib/xml/sax/handler.pyi @@ -1,4 +1,3 @@ -import sys from typing import Final, NoReturn, Protocol, type_check_only from xml.sax import xmlreader @@ -77,10 +76,9 @@ property_encoding: Final = "http://www.python.org/sax/properties/encoding" property_interning_dict: Final[str] # too long string all_properties: Final[list[str]] -if sys.version_info >= (3, 10): - class LexicalHandler: - def comment(self, content: str) -> None: ... - def startDTD(self, name: str, public_id: str | None, system_id: str | None) -> None: ... - def endDTD(self) -> None: ... - def startCDATA(self) -> None: ... - def endCDATA(self) -> None: ... +class LexicalHandler: + def comment(self, content: str) -> None: ... + def startDTD(self, name: str, public_id: str | None, system_id: str | None) -> None: ... + def endDTD(self) -> None: ... + def startCDATA(self) -> None: ... + def endCDATA(self) -> None: ... diff --git a/mypy/typeshed/stdlib/xml/sax/xmlreader.pyi b/mypy/typeshed/stdlib/xml/sax/xmlreader.pyi index e7d04ddeadb80..a7ae5edc55d35 100644 --- a/mypy/typeshed/stdlib/xml/sax/xmlreader.pyi +++ b/mypy/typeshed/stdlib/xml/sax/xmlreader.pyi @@ -1,7 +1,7 @@ from _typeshed import ReadableBuffer from collections.abc import Mapping -from typing import Generic, Literal, TypeVar, overload -from typing_extensions import Self, TypeAlias +from typing import Generic, Literal, TypeAlias, TypeVar, overload +from typing_extensions import Self from xml.sax import _Source, _SupportsReadClose from xml.sax.handler import _ContentHandlerProtocol, _DTDHandlerProtocol, _EntityResolverProtocol, _ErrorHandlerProtocol @@ -64,10 +64,12 @@ class AttributesImpl(Generic[_AttrKey]): def __getitem__(self, name: _AttrKey) -> str: ... def keys(self) -> list[_AttrKey]: ... def __contains__(self, name: _AttrKey) -> bool: ... + @overload def get(self, name: _AttrKey, alternative: None = None) -> str | None: ... @overload def get(self, name: _AttrKey, alternative: str) -> str: ... + def copy(self) -> Self: ... def items(self) -> list[tuple[_AttrKey, str]]: ... def values(self) -> list[str]: ... @@ -83,8 +85,10 @@ class AttributesNSImpl(AttributesImpl[_NSName]): def __getitem__(self, name: _NSName) -> str: ... def keys(self) -> list[_NSName]: ... def __contains__(self, name: _NSName) -> bool: ... + @overload def get(self, name: _NSName, alternative: None = None) -> str | None: ... @overload def get(self, name: _NSName, alternative: str) -> str: ... + def items(self) -> list[tuple[_NSName, str]]: ... diff --git a/mypy/typeshed/stdlib/xml/utils.pyi b/mypy/typeshed/stdlib/xml/utils.pyi new file mode 100644 index 0000000000000..1c3bb877a7ccf --- /dev/null +++ b/mypy/typeshed/stdlib/xml/utils.pyi @@ -0,0 +1,2 @@ +def is_valid_name(name: str) -> bool: ... +def is_valid_text(data: str) -> bool: ... diff --git a/mypy/typeshed/stdlib/xmlrpc/client.pyi b/mypy/typeshed/stdlib/xmlrpc/client.pyi index 42420ee85848f..573401f18d082 100644 --- a/mypy/typeshed/stdlib/xmlrpc/client.pyi +++ b/mypy/typeshed/stdlib/xmlrpc/client.pyi @@ -6,8 +6,8 @@ from collections.abc import Callable, Iterable, Mapping from datetime import datetime from io import BytesIO from types import TracebackType -from typing import Any, ClassVar, Final, Literal, Protocol, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import Any, ClassVar, Final, Literal, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import Self @type_check_only class _SupportsTimeTuple(Protocol): @@ -282,12 +282,14 @@ class ServerProxy: context: Any | None = None, ) -> None: ... def __getattr__(self, name: str) -> _Method: ... + @overload def __call__(self, attr: Literal["close"]) -> Callable[[], None]: ... @overload def __call__(self, attr: Literal["transport"]) -> Transport: ... @overload def __call__(self, attr: str) -> Callable[[], None] | Transport: ... + def __enter__(self) -> Self: ... def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None diff --git a/mypy/typeshed/stdlib/xmlrpc/server.pyi b/mypy/typeshed/stdlib/xmlrpc/server.pyi index 286aaf980fbf5..7bf397d2b1a43 100644 --- a/mypy/typeshed/stdlib/xmlrpc/server.pyi +++ b/mypy/typeshed/stdlib/xmlrpc/server.pyi @@ -4,8 +4,7 @@ import socketserver from _typeshed import ReadableBuffer from collections.abc import Callable, Iterable, Mapping from re import Pattern -from typing import Any, ClassVar, Protocol, type_check_only -from typing_extensions import TypeAlias +from typing import Any, ClassVar, Protocol, TypeAlias, type_check_only from xmlrpc.client import Fault, _Marshallable # The dispatch accepts anywhere from 0 to N arguments, no easy way to allow this in mypy diff --git a/mypy/typeshed/stdlib/xxlimited.pyi b/mypy/typeshed/stdlib/xxlimited.pyi index 78a50b85f405a..503caf0183f31 100644 --- a/mypy/typeshed/stdlib/xxlimited.pyi +++ b/mypy/typeshed/stdlib/xxlimited.pyi @@ -1,5 +1,5 @@ import sys -from typing import Any, ClassVar, final +from typing import Any, final class Str(str): ... @@ -12,13 +12,4 @@ class Xxo: def foo(i: int, j: int, /) -> Any: ... def new() -> Xxo: ... -if sys.version_info >= (3, 10): - class Error(Exception): ... - -else: - class error(Exception): ... - - class Null: - __hash__: ClassVar[None] # type: ignore[assignment] - - def roj(b: Any, /) -> None: ... +class Error(Exception): ... diff --git a/mypy/typeshed/stdlib/zipapp.pyi b/mypy/typeshed/stdlib/zipapp.pyi index c7cf1704b1359..48713bced8921 100644 --- a/mypy/typeshed/stdlib/zipapp.pyi +++ b/mypy/typeshed/stdlib/zipapp.pyi @@ -1,7 +1,6 @@ from collections.abc import Callable from pathlib import Path -from typing import BinaryIO -from typing_extensions import TypeAlias +from typing import BinaryIO, TypeAlias __all__ = ["ZipAppError", "create_archive", "get_interpreter"] diff --git a/mypy/typeshed/stdlib/zipfile/__init__.pyi b/mypy/typeshed/stdlib/zipfile/__init__.pyi index 19d8117a621fc..0039a05b5d6fe 100644 --- a/mypy/typeshed/stdlib/zipfile/__init__.pyi +++ b/mypy/typeshed/stdlib/zipfile/__init__.pyi @@ -5,8 +5,8 @@ from collections.abc import Callable, Iterable, Iterator from io import TextIOWrapper from os import PathLike from types import TracebackType -from typing import IO, Final, Literal, Protocol, overload, type_check_only -from typing_extensions import Self, TypeAlias +from typing import IO, Final, Literal, Protocol, TypeAlias, overload, type_check_only +from typing_extensions import Self __all__ = [ "BadZipFile", @@ -67,6 +67,7 @@ class ZipExtFile(io.BufferedIOBase): newlines: list[bytes] | None mode: _ReadWriteMode name: str + @overload def __init__( self, fileobj: _ClosableZipStream, mode: _ReadWriteMode, zipinfo: ZipInfo, pwd: bytes | None, close_fileobj: Literal[True] @@ -90,6 +91,7 @@ class ZipExtFile(io.BufferedIOBase): pwd: bytes | None = None, close_fileobj: Literal[False] = False, ) -> None: ... + def read(self, n: int | None = -1) -> bytes: ... def readline(self, limit: int = -1) -> bytes: ... # type: ignore[override] def peek(self, n: int = 1) -> bytes: ... @@ -330,6 +332,7 @@ if sys.version_info >= (3, 12): else: class CompleteDirs(ZipFile): def resolve_dir(self, name: str) -> str: ... + @overload @classmethod def make(cls, source: ZipFile) -> CompleteDirs: ... @@ -345,9 +348,8 @@ else: def name(self) -> str: ... @property def parent(self) -> PathLike[str]: ... # undocumented - if sys.version_info >= (3, 10): - @property - def filename(self) -> PathLike[str]: ... # undocumented + @property + def filename(self) -> PathLike[str]: ... # undocumented if sys.version_info >= (3, 11): @property def suffix(self) -> str: ... @@ -371,11 +373,7 @@ else: @overload def open(self, mode: Literal["rb", "wb"], *, pwd: bytes | None = None) -> IO[bytes]: ... - if sys.version_info >= (3, 10): - def iterdir(self) -> Iterator[Self]: ... - else: - def iterdir(self) -> Iterator[Path]: ... - + def iterdir(self) -> Iterator[Self]: ... def is_dir(self) -> bool: ... def is_file(self) -> bool: ... def exists(self) -> bool: ... @@ -388,11 +386,7 @@ else: write_through: bool = False, ) -> str: ... def read_bytes(self) -> bytes: ... - if sys.version_info >= (3, 10): - def joinpath(self, *other: StrPath) -> Path: ... - else: - def joinpath(self, add: StrPath) -> Path: ... # undocumented - + def joinpath(self, *other: StrPath) -> Path: ... def __truediv__(self, add: StrPath) -> Path: ... def is_zipfile(filename: StrOrBytesPath | _SupportsReadSeekTell) -> bool: ... diff --git a/mypy/typeshed/stdlib/zipfile/_path/__init__.pyi b/mypy/typeshed/stdlib/zipfile/_path/__init__.pyi index c936c4494c7cb..e1449dc681ad5 100644 --- a/mypy/typeshed/stdlib/zipfile/_path/__init__.pyi +++ b/mypy/typeshed/stdlib/zipfile/_path/__init__.pyi @@ -19,12 +19,14 @@ if sys.version_info >= (3, 12): class CompleteDirs(InitializedState, ZipFile): def resolve_dir(self, name: str) -> str: ... + @overload @classmethod def make(cls, source: ZipFile) -> CompleteDirs: ... @overload @classmethod def make(cls, source: StrPath | IO[bytes]) -> Self: ... + if sys.version_info >= (3, 13): @classmethod def inject(cls, zf: _ZF) -> _ZF: ... @@ -45,6 +47,7 @@ if sys.version_info >= (3, 12): def suffixes(self) -> list[str]: ... @property def stem(self) -> str: ... + @overload def open( self, @@ -59,6 +62,7 @@ if sys.version_info >= (3, 12): ) -> TextIOWrapper: ... @overload def open(self, mode: Literal["rb", "wb"], *, pwd: bytes | None = None) -> IO[bytes]: ... + def iterdir(self) -> Iterator[Self]: ... def is_dir(self) -> bool: ... def is_file(self) -> bool: ... diff --git a/mypy/typeshed/stdlib/zipimport.pyi b/mypy/typeshed/stdlib/zipimport.pyi index 22af3c272759b..4b34f1f2ad3e7 100644 --- a/mypy/typeshed/stdlib/zipimport.pyi +++ b/mypy/typeshed/stdlib/zipimport.pyi @@ -1,19 +1,11 @@ import sys +from _frozen_importlib_external import _LoaderBasics from _typeshed import StrOrBytesPath from importlib.machinery import ModuleSpec +from importlib.readers import ZipReader from types import CodeType, ModuleType from typing_extensions import deprecated -if sys.version_info >= (3, 10): - from importlib.readers import ZipReader -else: - from importlib.abc import ResourceReader - -if sys.version_info >= (3, 10): - from _frozen_importlib_external import _LoaderBasics -else: - _LoaderBasics = object - __all__ = ["ZipImportError", "zipimporter"] class ZipImportError(ImportError): ... @@ -27,33 +19,26 @@ class zipimporter(_LoaderBasics): def __init__(self, path: StrOrBytesPath) -> None: ... if sys.version_info < (3, 12): - if sys.version_info >= (3, 10): - @deprecated("Deprecated since Python 3.10; removed in Python 3.12. Use `find_spec()` instead.") - def find_loader(self, fullname: str, path: str | None = None) -> tuple[zipimporter | None, list[str]]: ... - @deprecated("Deprecated since Python 3.10; removed in Python 3.12. Use `find_spec()` instead.") - def find_module(self, fullname: str, path: str | None = None) -> zipimporter | None: ... - else: - def find_loader(self, fullname: str, path: str | None = None) -> tuple[zipimporter | None, list[str]]: ... - def find_module(self, fullname: str, path: str | None = None) -> zipimporter | None: ... + @deprecated("Deprecated since Python 3.10; removed in Python 3.12. Use `find_spec()` instead.") + def find_loader(self, fullname: str, path: str | None = None) -> tuple[zipimporter | None, list[str]]: ... + @deprecated("Deprecated since Python 3.10; removed in Python 3.12. Use `find_spec()` instead.") + def find_module(self, fullname: str, path: str | None = None) -> zipimporter | None: ... def get_code(self, fullname: str) -> CodeType: ... def get_data(self, pathname: str) -> bytes: ... def get_filename(self, fullname: str) -> str: ... if sys.version_info >= (3, 14): def get_resource_reader(self, fullname: str) -> ZipReader: ... # undocumented - elif sys.version_info >= (3, 10): - def get_resource_reader(self, fullname: str) -> ZipReader | None: ... # undocumented else: - def get_resource_reader(self, fullname: str) -> ResourceReader | None: ... # undocumented + def get_resource_reader(self, fullname: str) -> ZipReader | None: ... # undocumented def get_source(self, fullname: str) -> str | None: ... def is_package(self, fullname: str) -> bool: ... - if sys.version_info >= (3, 10): + if sys.version_info < (3, 15): @deprecated("Deprecated since Python 3.10; removed in Python 3.15. Use `exec_module()` instead.") def load_module(self, fullname: str) -> ModuleType: ... - def exec_module(self, module: ModuleType) -> None: ... - def create_module(self, spec: ModuleSpec) -> None: ... - def find_spec(self, fullname: str, target: ModuleType | None = None) -> ModuleSpec | None: ... - def invalidate_caches(self) -> None: ... - else: - def load_module(self, fullname: str) -> ModuleType: ... + + def exec_module(self, module: ModuleType) -> None: ... + def create_module(self, spec: ModuleSpec) -> None: ... + def find_spec(self, fullname: str, target: ModuleType | None = None) -> ModuleSpec | None: ... + def invalidate_caches(self) -> None: ... diff --git a/mypy/typeshed/stdlib/zlib.pyi b/mypy/typeshed/stdlib/zlib.pyi index d5998cab90fef..557fa761feebf 100644 --- a/mypy/typeshed/stdlib/zlib.pyi +++ b/mypy/typeshed/stdlib/zlib.pyi @@ -60,6 +60,9 @@ class _Decompress: def adler32(data: ReadableBuffer, value: int = 1, /) -> int: ... +if sys.version_info >= (3, 15): + def adler32_combine(adler1: int, adler2: int, len2: int, /) -> int: ... + if sys.version_info >= (3, 11): def compress(data: ReadableBuffer, /, level: int = -1, wbits: int = 15) -> bytes: ... @@ -70,5 +73,9 @@ def compressobj( level: int = -1, method: int = 8, wbits: int = 15, memLevel: int = 8, strategy: int = 0, zdict: ReadableBuffer | None = None ) -> _Compress: ... def crc32(data: ReadableBuffer, value: int = 0, /) -> int: ... + +if sys.version_info >= (3, 15): + def crc32_combine(crc1: int, crc2: int, len2: int, /) -> int: ... + def decompress(data: ReadableBuffer, /, wbits: int = 15, bufsize: int = 16384) -> bytes: ... def decompressobj(wbits: int = 15, zdict: ReadableBuffer = b"") -> _Decompress: ... diff --git a/mypy/typeshed/stdlib/zoneinfo/__init__.pyi b/mypy/typeshed/stdlib/zoneinfo/__init__.pyi index b7433f835f83d..def31546becbf 100644 --- a/mypy/typeshed/stdlib/zoneinfo/__init__.pyi +++ b/mypy/typeshed/stdlib/zoneinfo/__init__.pyi @@ -19,6 +19,7 @@ class ZoneInfo(tzinfo): def __new__(cls, key: str) -> Self: ... @classmethod def no_cache(cls, key: str) -> Self: ... + if sys.version_info >= (3, 12): @classmethod def from_file(cls, file_obj: _IOBytes, /, key: str | None = None) -> Self: ... From e0c375a97105ecf43c9ff9b858e01cd6e938a077 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 10 Jun 2026 01:23:34 +0100 Subject: [PATCH 073/127] Support floats in JSON in fixed-format cache (#21603) Fixes https://github.com/python/mypy/issues/21600 Obviously, `float` should be allowed in a JSON value. --- mypy/cache.py | 9 +++++++-- test-data/unit/check-dataclasses.test | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/mypy/cache.py b/mypy/cache.py index e90c933fdab9a..ebe36e8940b81 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -69,7 +69,7 @@ from mypy_extensions import u8 # High-level cache layout format -CACHE_VERSION: Final = 9 +CACHE_VERSION: Final = 10 # Type used internally to represent errors: # (path, line, column, end_line, end_column, severity, message, code) @@ -478,7 +478,7 @@ def write_str_opt_list(data: WriteBuffer, value: list[str | None]) -> None: write_str_opt(data, item) -Value: _TypeAlias = None | int | str | bool +Value: _TypeAlias = None | int | float | str | bool # Our JSON format is somewhat non-standard as we distinguish lists and tuples. # This is convenient for some internal things, like mypyc plugin and error serialization. @@ -508,6 +508,8 @@ def read_json_value(data: ReadBuffer) -> JsonValue: if tag == DICT_STR_GEN: size = read_int_bare(data) return {read_str_bare(data): read_json_value(data) for _ in range(size)} + if tag == LITERAL_FLOAT: + return read_float_bare(data) assert False, f"Invalid JSON tag: {tag}" @@ -538,6 +540,9 @@ def write_json_value(data: WriteBuffer, value: JsonValue) -> None: for key in sorted(value): write_str_bare(data, key) write_json_value(data, value[key]) + elif isinstance(value, float): + write_tag(data, LITERAL_FLOAT) + write_float_bare(data, value) else: assert False, f"Invalid JSON value: {value}" diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 762585806b85d..54b3afadc8b32 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1252,6 +1252,26 @@ class Person: [builtins fixtures/dataclasses.pyi] +[case testDataclassesFloatSerializationIncremental] +import m +[file m.py] +from lib import MyDataClass +[file m.py.2] +from lib import MyDataClass + +reveal_type(MyDataClass.MY_CONSTANT) +[file lib.py] +from dataclasses import dataclass +from typing import Final + +@dataclass(kw_only=True, repr=False) +class MyDataClass: + MY_CONSTANT: Final = 1.234 +[builtins fixtures/dataclasses.pyi] +[out] +[out2] +tmp/m.py:3: note: Revealed type is "Literal[1.234]?" + [case testDataclassesDefaultsMroOtherFile] import a From f8bf7ab5778e5c68417c850aa7e4ca2e0b74f149 Mon Sep 17 00:00:00 2001 From: Kevin Kannammalil Date: Fri, 12 Jun 2026 12:52:37 -0400 Subject: [PATCH 074/127] Memoize the options snapshot (#21354) The `options_snapshot` is about 6% of CPU on incremental runs in a very large codebase. This caches the `(platform, hash_digest)` tuple keyed by `id()` of the cloned `Options`, and keeps each cloned object alive in a parallel `_options_snapshot_keepalive` list so `id()` cannot be reused by the GC after a transient clone is collected. This improved the overall time by roughly 5%, the time taken in the function dropped by ~67% on a warm run, and the ~110k calls dropped to ~220 distinct cloned `Options` without any change in output --- mypy/build.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 3d0dc8df8b1c2..97db8fe1646cd 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -998,6 +998,10 @@ def __init__( self.import_options: dict[str, bytes] = {} # Cache for transitive dependency check (expensive). self.transitive_deps_cache: dict[tuple[int, int], bool] = {} + # Cache for options_snapshot() keyed by the cloned Options. Options is + # hashed by identity, and most modules share a handful of distinct + # configs, so this collapses ~all calls onto a few entries. + self.options_snapshot_cache: dict[Options, tuple[str, str]] = {} # Packages for which we know presence or absence of __getattr__(). self.known_partial_packages: dict[str, bool] = {} @@ -1957,23 +1961,29 @@ def get_cache_names(id: str, path: str, options: Options) -> tuple[str, str, str return prefix + meta_suffix, prefix + data_suffix, deps_json -def options_snapshot(id: str, manager: BuildManager) -> dict[str, object]: +def options_snapshot(module: str, manager: BuildManager) -> dict[str, object]: """Make compact snapshot of options for a module. Separately store only the options we may compare individually, and take a hash of everything else. If --debug-cache is specified, fall back to full snapshot. """ - platform_opt, values = manager.options.clone_for_module(id).select_options_affecting_cache() + cloned = manager.options.clone_for_module(module) if manager.options.debug_cache: # Build full options snapshot for debugging purposes. + platform_opt, values = cloned.select_options_affecting_cache() result: dict[str, object] = {"platform": platform_opt} for key, val in zip(OPTIONS_AFFECTING_CACHE_NO_PLATFORM, values): result[key] = val return result - # Process most options quickly, since this is performance critical. - buf = WriteBuffer() - write_json_value(buf, cast(JsonValue, values)) - return {"platform": platform_opt, "other_options": hash_digest(buf.getvalue())} + cache = manager.options_snapshot_cache + cached = cache.get(cloned) + if cached is None: + platform_opt, values = cloned.select_options_affecting_cache() + buf = WriteBuffer() + write_json_value(buf, cast(JsonValue, values)) + cached = (platform_opt, hash_digest(buf.getvalue())) + cache[cloned] = cached + return {"platform": cached[0], "other_options": cached[1]} def find_cache_meta( From 0f9676e3999afdf17ccdb67f2a8c26062fb44407 Mon Sep 17 00:00:00 2001 From: Kevin Kannammalil Date: Mon, 15 Jun 2026 05:36:52 -0400 Subject: [PATCH 075/127] Add function definition notes for `too_many_positional_arguments` errors (#21410) Followup to https://github.com/python/mypy/pull/20794 Adds the "foo" is defined in "bar" note for the `too_many_positional_arguments ` error case Also update some call-related errors to generate `call-arg` error code (sub-code of `misc`) instead of `misc`, for consistency. --- mypy/errorcodes.py | 7 +++++++ mypy/messages.py | 9 ++++++--- mypy/test/testtypes.py | 2 +- test-data/unit/check-errorcodes.test | 11 +++++++++++ test-data/unit/check-kwargs.test | 15 +++++++++++++++ test-data/unit/check-namedtuple.test | 1 + test-data/unit/check-type-aliases.test | 1 + 7 files changed, 42 insertions(+), 4 deletions(-) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 5c28e8332a76c..06e716372f248 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -287,6 +287,13 @@ def __hash__(self) -> int: # This is a catch-all for remaining uncategorized errors. MISC: Final = ErrorCode("misc", "Miscellaneous other checks", "General") +CALL_ARG_MISC: Final = ErrorCode( + "call-arg", "Check number, names and kinds of arguments in calls", "General", sub_code_of=MISC +) +# CALL_ARG_MISC reuses the "call-arg" code string, so keep CALL_ARG as the canonical +# code that "call-arg" resolves to in the registry. +error_codes[CALL_ARG.code] = CALL_ARG + OVERLOAD_CANNOT_MATCH: Final = ErrorCode( "overload-cannot-match", "Warn if an @overload signature can never be matched", diff --git a/mypy/messages.py b/mypy/messages.py index 93d5f2c212d0e..ffac78201a6cd 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -970,8 +970,9 @@ def too_many_positional_arguments(self, callee: CallableType, context: Context) msg = "Too many positional arguments" else: msg = "Too many positional arguments" + for_function(callee) - self.fail(msg, context) + self.fail(msg, context, code=codes.CALL_ARG_MISC) self.maybe_note_about_special_args(callee, context) + self.note_defined_here(callee, context, code=codes.CALL_ARG_MISC) def maybe_note_about_special_args(self, callee: CallableType, context: Context) -> None: if self.prefer_simple_messages(): @@ -1018,7 +1019,9 @@ def unexpected_keyword_argument( ) self.note_defined_here(callee, context) - def note_defined_here(self, callee: CallableType, context: Context) -> None: + def note_defined_here( + self, callee: CallableType, context: Context, code: ErrorCode = codes.CALL_ARG + ) -> None: module = find_defining_module(self.modules, callee) if ( module @@ -1031,7 +1034,7 @@ def note_defined_here(self, callee: CallableType, context: Context) -> None: fname = "Called function" else: fname = fname.split(" of ")[0] # use short method names in the note - self.note(f'{fname} defined in "{module.fullname}"', context, code=codes.CALL_ARG) + self.note(f'{fname} defined in "{module.fullname}"', context, code=code) def duplicate_argument_value(self, callee: CallableType, index: int, context: Context) -> None: self.fail( diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index ac6d24b1ef4c0..6de922cb7f234 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -236,7 +236,7 @@ def test_typeddict_type_constructor_signature(self) -> None: assert closed.is_closed with self.assertRaises(TypeError): - TypedDictType( # type: ignore[misc] + TypedDictType( # type: ignore[call-arg] {"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20, True ) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 85a2264c2088b..5296d813334e4 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -331,6 +331,17 @@ def h(x: int, y: int, z: int) -> None: pass h(y=1, z=1) # E: Missing positional argument "x" in call to "h" [call-arg] h(y=1) # E: Missing positional arguments "x", "z" in call to "h" [call-arg] +[case testTooManyPositionalArgumentsErrorCode] +def f(a: int, *, b: int) -> None: pass +f(1, 2) # E: Too many positional arguments for "f" [call-arg] +f(1, 2) # type: ignore[call-arg] +f(1, 2) # type: ignore[misc] + +[case testTooManyPositionalArgumentsCoveredByMiscUnused] +# flags: --warn-unused-ignores +def f(a: int, *, b: int) -> None: pass +f(1, 2) # type: ignore[misc] # E: Unused "type: ignore" comment, use narrower [call-arg] instead of [misc] code [unused-ignore] + [case testErrorCodeArgType] def f(x: int) -> None: pass f('') # E: Argument 1 to "f" has incompatible type "str"; expected "int" [arg-type] diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index f11c2b6f4fc40..6f5e11d85ee48 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -495,6 +495,21 @@ def f(a: int, *, b: str) -> None: pass f(1) # E: Missing named argument "b" for "f" +[case testTooManyPositionalArgumentsFromOtherModule] +import m +m.f(1, 2) +[file m.py] +def f(a: int, *, b: int) -> None: + pass +[out] +main:2: error: Too many positional arguments for "f" +main:2: note: "f" defined in "m" + +[case testTooManyPositionalArgumentsForSameModule] +def f(a: int, *, b: int) -> None: + pass +f(1, 2) # E: Too many positional arguments for "f" + [case testStarArgsAndKwArgsSpecialCase] from typing import Dict, Mapping diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 285ae92325d8e..158b95aa598d0 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -132,6 +132,7 @@ main:5: error: Argument "rename" to "namedtuple" has incompatible type "str"; ex main:6: error: Unexpected keyword argument "unrecognized_arg" for "namedtuple" main:6: note: "namedtuple" defined in "collections" main:7: error: Too many positional arguments for "namedtuple" +main:7: note: "namedtuple" defined in "collections" [case testNamedTupleDefaults] from collections import namedtuple diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 4d68f93a21eda..e48700c69e38e 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -1112,6 +1112,7 @@ reveal_type(t3) # N: Revealed type is "Any" T4 = TypeAliasType("T4") # E: Missing positional argument "value" in call to "TypeAliasType" T5 = TypeAliasType("T5", int, str) # E: Too many positional arguments for "TypeAliasType" \ + # N: "TypeAliasType" defined in "typing_extensions" \ # E: Argument 3 to "TypeAliasType" has incompatible type "type[str]"; expected "tuple[TypeVar? | ParamSpec? | TypeVarTuple?, ...]" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] From 8b6642db9791428e158c31b4fd3ed8bc96832ccb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 15 Jun 2026 14:47:10 +0100 Subject: [PATCH 076/127] [mypyc] Fix free-threading race condition in argument parsing (#21613) Global argument parser state initialization was missing synchronization. Don't use a mutex on the hot path as an optimization. Fixes #21578 (probably). I added a regression test that failed about 30% of time on master (on macOS). I used coding agent assist. --- mypyc/lib-rt/getargsfast.c | 72 +++++++++++++++++++++++--- mypyc/test-data/run-functions.test | 83 ++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 8 deletions(-) diff --git a/mypyc/lib-rt/getargsfast.c b/mypyc/lib-rt/getargsfast.c index 590306a7c52c2..e62c533649162 100644 --- a/mypyc/lib-rt/getargsfast.c +++ b/mypyc/lib-rt/getargsfast.c @@ -18,7 +18,22 @@ #include #include "CPy.h" -#define PARSER_INITED(parser) ((parser)->kwtuple != NULL) +// The kwtuple field doubles as the "parser has been initialized" flag: it is +// written last (after all other parser fields) and read first. On free-threaded +// builds the fast paths read it without holding any lock, so the write must be a +// release store and the reads acquire loads. That way a thread that observes a +// non-NULL kwtuple is guaranteed to also see the fully-initialized min/max/etc. +// fields. On GIL builds these are plain accesses with no overhead. +#ifdef Py_GIL_DISABLED +#define PARSER_KWTUPLE(parser) _Py_atomic_load_ptr_acquire(&(parser)->kwtuple) +#define SET_PARSER_KWTUPLE(parser, value) \ + _Py_atomic_store_ptr_release(&(parser)->kwtuple, (value)) +#else +#define PARSER_KWTUPLE(parser) ((parser)->kwtuple) +#define SET_PARSER_KWTUPLE(parser, value) ((parser)->kwtuple = (value)) +#endif + +#define PARSER_INITED(parser) (PARSER_KWTUPLE(parser) != NULL) /* Forward */ static int @@ -115,19 +130,21 @@ CPyArg_ParseStackAndKeywordsSimple(PyObject *const *args, Py_ssize_t nargs, PyOb /* List of static parsers. */ static struct CPyArg_Parser *static_arg_parsers = NULL; +#ifdef Py_GIL_DISABLED +// Serializes one-time initialization of parsers and insertion into the +// static_arg_parsers list. Only contended the first time a given compiled +// function is called; once a parser is initialized the fast paths never lock. +static PyMutex static_arg_parsers_mutex; +#endif + static int -parser_init(CPyArg_Parser *parser) +parser_init_locked(CPyArg_Parser *parser) { const char * const *keywords; const char *format; int i, len, min, max, nkw; PyObject *kwtuple; - assert(parser->keywords != NULL); - if (PARSER_INITED(parser)) { - return 1; - } - keywords = parser->keywords; /* scan keywords and count the number of positional-only parameters */ for (i = 0; keywords[i] && !*keywords[i]; i++) { @@ -244,14 +261,53 @@ parser_init(CPyArg_Parser *parser) PyUnicode_InternInPlace(&str); PyTuple_SET_ITEM(kwtuple, i, str); } - parser->kwtuple = kwtuple; assert(parser->next == NULL); parser->next = static_arg_parsers; static_arg_parsers = parser; + + // Publish the parser last: storing kwtuple marks it as initialized, so all + // other fields (and the list insertion above) must already be in place. On + // free-threaded builds this is a release store paired with the acquire loads + // in PARSER_INITED/PARSER_KWTUPLE. + SET_PARSER_KWTUPLE(parser, kwtuple); return 1; } +// Cold path of parser_init: perform the one-time initialization. On +// free-threaded builds this is serialized so that only one thread builds the +// parser and inserts it into the static_arg_parsers list. +static CPy_NOINLINE int +parser_init_slow(CPyArg_Parser *parser) +{ +#ifdef Py_GIL_DISABLED + PyMutex_Lock(&static_arg_parsers_mutex); + // Re-check now that we hold the lock: another thread may have initialized + // the parser while we were waiting. + if (PARSER_INITED(parser)) { + PyMutex_Unlock(&static_arg_parsers_mutex); + return 1; + } + int retval = parser_init_locked(parser); + PyMutex_Unlock(&static_arg_parsers_mutex); + return retval; +#else + return parser_init_locked(parser); +#endif +} + +// Hot path: a parser is almost always already initialized, so keep the common +// case inline and branch out to parser_init_slow only on first use. +static inline int +parser_init(CPyArg_Parser *parser) +{ + assert(parser->keywords != NULL); + if (likely(PARSER_INITED(parser))) { + return 1; + } + return parser_init_slow(parser); +} + static PyObject* find_keyword(PyObject *kwnames, PyObject *const *kwstack, PyObject *key) { diff --git a/mypyc/test-data/run-functions.test b/mypyc/test-data/run-functions.test index a59d7f729e9fc..8932d0f110069 100644 --- a/mypyc/test-data/run-functions.test +++ b/mypyc/test-data/run-functions.test @@ -1524,3 +1524,86 @@ def test_multiple_params() -> None: # Different parameter name than loop variable funcs2 = uses_multiple_params_different_name(["a", "b"], "!") assert [f() for f in funcs2] == ["a!", "b!"] + +[case testConcurrentFirstCallWithKeywordArgs] +# Regression test for a free-threading data race in argument parser +# initialization. The first call to a compiled function with keyword arguments +# lazily initializes a static CPyArg_Parser and pushes it onto a global list. +# When many threads race that first call on a free-threaded build, the +# initialization and list insertion must be synchronized, or the runtime hits +# a failed assertion (parser->next == NULL) and aborts. +import sys +import threading + +# A pool of distinct functions, none of which is called before the concurrent +# test. Each function's first call lazily initializes a static CPyArg_Parser, +# and the test races that initialization across threads. Keyword arguments +# force the call through the slow path that runs parser_init. Using many +# functions gives many independent races per run, so the test reliably triggers +# the bug on an unfixed free-threaded build instead of depending on a single +# narrow timing window. +def g0(a: int, b: int, c: int) -> int: return a + b + c +def g1(a: int, b: int, c: int) -> int: return a + b + c +def g2(a: int, b: int, c: int) -> int: return a + b + c +def g3(a: int, b: int, c: int) -> int: return a + b + c +def g4(a: int, b: int, c: int) -> int: return a + b + c +def g5(a: int, b: int, c: int) -> int: return a + b + c +def g6(a: int, b: int, c: int) -> int: return a + b + c +def g7(a: int, b: int, c: int) -> int: return a + b + c +def g8(a: int, b: int, c: int) -> int: return a + b + c +def g9(a: int, b: int, c: int) -> int: return a + b + c +def g10(a: int, b: int, c: int) -> int: return a + b + c +def g11(a: int, b: int, c: int) -> int: return a + b + c +def g12(a: int, b: int, c: int) -> int: return a + b + c +def g13(a: int, b: int, c: int) -> int: return a + b + c +def g14(a: int, b: int, c: int) -> int: return a + b + c +def g15(a: int, b: int, c: int) -> int: return a + b + c +def g16(a: int, b: int, c: int) -> int: return a + b + c +def g17(a: int, b: int, c: int) -> int: return a + b + c +def g18(a: int, b: int, c: int) -> int: return a + b + c +def g19(a: int, b: int, c: int) -> int: return a + b + c +def g20(a: int, b: int, c: int) -> int: return a + b + c +def g21(a: int, b: int, c: int) -> int: return a + b + c +def g22(a: int, b: int, c: int) -> int: return a + b + c +def g23(a: int, b: int, c: int) -> int: return a + b + c +def g24(a: int, b: int, c: int) -> int: return a + b + c +def g25(a: int, b: int, c: int) -> int: return a + b + c +def g26(a: int, b: int, c: int) -> int: return a + b + c +def g27(a: int, b: int, c: int) -> int: return a + b + c +def g28(a: int, b: int, c: int) -> int: return a + b + c +def g29(a: int, b: int, c: int) -> int: return a + b + c + +FUNCS = [g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, g11, g12, g13, g14, + g15, g16, g17, g18, g19, g20, g21, g22, g23, g24, g25, g26, g27, + g28, g29] + +def is_gil_disabled() -> bool: + return hasattr(sys, "_is_gil_enabled") and not sys._is_gil_enabled() + +def test_concurrent_first_call() -> None: + if not is_gil_disabled(): + # The race can only happen without the GIL. + return + + num_threads = 16 + barrier = threading.Barrier(num_threads) + errors: list[str] = [] + + def run() -> None: + # Line up all threads, then let them race freely through the list. The + # first call to each function lazily initializes its parser, so every + # function is a fresh race under real parallel pressure. + barrier.wait() + try: + for fn in FUNCS: + assert fn(a=1, b=2, c=3) == 6 + except BaseException as e: + errors.append(repr(e)) + + threads = [threading.Thread(target=run) for _ in range(num_threads)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors, errors From 0da2bbdb9e90f264738e24fa7881c27d97d8bf65 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 15 Jun 2026 19:04:28 +0100 Subject: [PATCH 077/127] [mypyc] Make some dict primitives thread-safe on free-threading builds (#21616) Use thread-safe `PyDict_GetItemRef` instead of `PyDict_GetItemWithError`. Also fix an unrelated memory leak in a setdefault primitive. I used coding agent assist. --- mypyc/lib-rt/dict_ops.c | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/mypyc/lib-rt/dict_ops.c b/mypyc/lib-rt/dict_ops.c index dd93acdbe9267..e3631c63b6997 100644 --- a/mypyc/lib-rt/dict_ops.c +++ b/mypyc/lib-rt/dict_ops.c @@ -15,13 +15,10 @@ // some indirections. PyObject *CPyDict_GetItem(PyObject *dict, PyObject *key) { if (PyDict_CheckExact(dict)) { - PyObject *res = PyDict_GetItemWithError(dict, key); - if (!res) { - if (!PyErr_Occurred()) { - PyErr_SetObject(PyExc_KeyError, key); - } - } else { - Py_INCREF(res); + PyObject *res; + int found = PyDict_GetItemRef(dict, key, &res); + if (found == 0) { + PyErr_SetObject(PyExc_KeyError, key); } return res; } else { @@ -56,14 +53,15 @@ PyObject *CPyDict_Build(Py_ssize_t size, ...) { PyObject *CPyDict_Get(PyObject *dict, PyObject *key, PyObject *fallback) { // We are dodgily assuming that get on a subclass doesn't have // different behavior. - PyObject *res = PyDict_GetItemWithError(dict, key); - if (!res) { - if (PyErr_Occurred()) { - return NULL; - } - res = fallback; + PyObject *res; + int found = PyDict_GetItemRef(dict, key, &res); + if (found < 0) { + return NULL; + } + if (found == 0) { + Py_INCREF(fallback); + return fallback; } - Py_INCREF(res); return res; } @@ -100,17 +98,19 @@ PyObject *CPyDict_SetDefaultWithEmptyDatatype(PyObject *dict, PyObject *key, } else if (data_type == 3) { new_obj = PySet_New(NULL); } else { - return NULL; + new_obj = NULL; } - if (CPyDict_SetItem(dict, key, new_obj) == -1) { - return NULL; + if (new_obj == NULL) { + res = NULL; + } else if (CPyDict_SetItem(dict, key, new_obj) == -1) { + Py_DECREF(new_obj); + res = NULL; } else { - return new_obj; + res = new_obj; } - } else { - return res; } + return res; } int CPyDict_SetItem(PyObject *dict, PyObject *key, PyObject *value) { From 27d7a353b1bf54060657857fe72d2bb12ce7d480 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 15 Jun 2026 19:04:44 +0100 Subject: [PATCH 078/127] [mypyc] Fix dict iteration memory safety on free-threaded builds (#21617) `PyDict_Next()` returns a borrowed reference, so we need to add critical sections around the calls to ensure memory safety. I used coding agent assist. --- mypyc/lib-rt/dict_ops.c | 48 ++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/mypyc/lib-rt/dict_ops.c b/mypyc/lib-rt/dict_ops.c index e3631c63b6997..8a9202c403f8a 100644 --- a/mypyc/lib-rt/dict_ops.c +++ b/mypyc/lib-rt/dict_ops.c @@ -346,16 +346,24 @@ tuple_T3CIO CPyDict_NextKey(PyObject *dict_or_iter, CPyTagged offset) { PyObject *dummy; if (PyDict_CheckExact(dict_or_iter)) { - ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &ret.f2, &dummy); + PyObject *key; + // PyDict_Next() returns a borrowed reference. On free-threaded builds, + // hold the dict lock until we have converted it to a strong reference. + Py_BEGIN_CRITICAL_SECTION(dict_or_iter); + ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &key, &dummy); + if (ret.f0) { + ret.f2 = Py_NewRef(key); + } + Py_END_CRITICAL_SECTION(); + if (ret.f0) { ret.f1 = CPyTagged_FromSsize_t(py_offset); } else { // Set key to None, so mypyc can manage refcounts. ret.f1 = 0; ret.f2 = Py_None; + Py_INCREF(ret.f2); } - // PyDict_Next() returns borrowed references. - Py_INCREF(ret.f2); } else { // offset is dummy in this case, just use the old value. ret.f1 = offset; @@ -370,16 +378,24 @@ tuple_T3CIO CPyDict_NextValue(PyObject *dict_or_iter, CPyTagged offset) { PyObject *dummy; if (PyDict_CheckExact(dict_or_iter)) { - ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &dummy, &ret.f2); + PyObject *value; + // PyDict_Next() returns a borrowed reference. On free-threaded builds, + // hold the dict lock until we have converted it to a strong reference. + Py_BEGIN_CRITICAL_SECTION(dict_or_iter); + ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &dummy, &value); + if (ret.f0) { + ret.f2 = Py_NewRef(value); + } + Py_END_CRITICAL_SECTION(); + if (ret.f0) { ret.f1 = CPyTagged_FromSsize_t(py_offset); } else { // Set value to None, so mypyc can manage refcounts. ret.f1 = 0; ret.f2 = Py_None; + Py_INCREF(ret.f2); } - // PyDict_Next() returns borrowed references. - Py_INCREF(ret.f2); } else { // offset is dummy in this case, just use the old value. ret.f1 = offset; @@ -393,7 +409,18 @@ tuple_T4CIOO CPyDict_NextItem(PyObject *dict_or_iter, CPyTagged offset) { Py_ssize_t py_offset = CPyTagged_AsSsize_t(offset); if (PyDict_CheckExact(dict_or_iter)) { - ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &ret.f2, &ret.f3); + PyObject *key; + PyObject *value; + // PyDict_Next() returns borrowed references. On free-threaded builds, + // hold the dict lock until we have converted them to strong references. + Py_BEGIN_CRITICAL_SECTION(dict_or_iter); + ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &key, &value); + if (ret.f0) { + ret.f2 = Py_NewRef(key); + ret.f3 = Py_NewRef(value); + } + Py_END_CRITICAL_SECTION(); + if (ret.f0) { ret.f1 = CPyTagged_FromSsize_t(py_offset); } else { @@ -401,6 +428,8 @@ tuple_T4CIOO CPyDict_NextItem(PyObject *dict_or_iter, CPyTagged offset) { ret.f1 = 0; ret.f2 = Py_None; ret.f3 = Py_None; + Py_INCREF(ret.f2); + Py_INCREF(ret.f3); } } else { ret.f1 = offset; @@ -413,6 +442,8 @@ tuple_T4CIOO CPyDict_NextItem(PyObject *dict_or_iter, CPyTagged offset) { ret.f0 = 0; ret.f2 = Py_None; ret.f3 = Py_None; + Py_INCREF(ret.f2); + Py_INCREF(ret.f3); } else { ret.f0 = 1; ret.f2 = PyTuple_GET_ITEM(item, 0); @@ -423,9 +454,6 @@ tuple_T4CIOO CPyDict_NextItem(PyObject *dict_or_iter, CPyTagged offset) { return ret; } } - // PyDict_Next() returns borrowed references. - Py_INCREF(ret.f2); - Py_INCREF(ret.f3); return ret; } From 22a9cfd1595798810e27ef145ea4c989ccdcc883 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 15 Jun 2026 19:05:05 +0100 Subject: [PATCH 079/127] [mypyc] Make list remove and index thread-safe on free-threaded builds (#21614) The primitives for `list.remove` and `list.index` were missing critical sections. --- mypyc/lib-rt/list_ops.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/mypyc/lib-rt/list_ops.c b/mypyc/lib-rt/list_ops.c index ae1bc0cb1e6ec..8b7eaa1acf3f0 100644 --- a/mypyc/lib-rt/list_ops.c +++ b/mypyc/lib-rt/list_ops.c @@ -301,6 +301,8 @@ PyObject *CPyList_Extend(PyObject *o1, PyObject *o2) { } // Return -2 or error, -1 if not found, or index of first match otherwise. +// +// The caller must hold a critical section on the list. static Py_ssize_t _CPyList_Find(PyObject *list, PyObject *obj) { Py_ssize_t i; for (i = 0; i < Py_SIZE(list); i++) { @@ -320,27 +322,35 @@ static Py_ssize_t _CPyList_Find(PyObject *list, PyObject *obj) { } int CPyList_Remove(PyObject *list, PyObject *obj) { + int retval; + Py_BEGIN_CRITICAL_SECTION(list); Py_ssize_t index = _CPyList_Find(list, obj); if (index == -2) { - return -1; - } - if (index == -1) { + retval = -1; + } else if (index == -1) { PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); - return -1; + retval = -1; + } else { + retval = PyList_SetSlice(list, index, index + 1, NULL); } - return PyList_SetSlice(list, index, index + 1, NULL); + Py_END_CRITICAL_SECTION(); + return retval; } CPyTagged CPyList_Index(PyObject *list, PyObject *obj) { + CPyTagged retval; + Py_BEGIN_CRITICAL_SECTION(list); Py_ssize_t index = _CPyList_Find(list, obj); if (index == -2) { - return CPY_INT_TAG; - } - if (index == -1) { + retval = CPY_INT_TAG; + } else if (index == -1) { PyErr_SetString(PyExc_ValueError, "value is not in list"); - return CPY_INT_TAG; + retval = CPY_INT_TAG; + } else { + retval = index << 1; } - return index << 1; + Py_END_CRITICAL_SECTION(); + return retval; } PyObject *CPySequence_Sort(PyObject *seq) { From 0cd154158ab2829a2f8c7a2728c7d4ed55e8f193 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 16 Jun 2026 16:42:51 +0100 Subject: [PATCH 080/127] [mypyc] Make function wrappers thread-safe on free-threaded builds (#21620) The implementation had multiple race conditions. I used coding agent assist. --- mypyc/lib-rt/function_wrapper.c | 45 ++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/mypyc/lib-rt/function_wrapper.c b/mypyc/lib-rt/function_wrapper.c index 348c3316cd258..16b1b59303481 100644 --- a/mypyc/lib-rt/function_wrapper.c +++ b/mypyc/lib-rt/function_wrapper.c @@ -29,7 +29,14 @@ static void CPyFunction_dealloc(CPyFunction *m) { } static PyObject* CPyFunction_repr(CPyFunction *op) { - return PyUnicode_FromFormat("", op->func_name, (void *)op); + // Use helper to get name for free threading safety. + PyObject *name = CPyFunction_get_name((PyObject *)op, NULL); + if (unlikely(name == NULL)) { + return NULL; + } + PyObject *result = PyUnicode_FromFormat("", name, (void *)op); + Py_DECREF(name); + return result; } static PyObject* CPyFunction_call(PyObject *func, PyObject *args, PyObject *kw) { @@ -59,13 +66,15 @@ static PyMemberDef CPyFunction_members[] = { PyObject* CPyFunction_get_name(PyObject *op, void *context) { (void)context; CPyFunction *func = (CPyFunction *)op; + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(op); if (unlikely(func->func_name == NULL)) { func->func_name = PyUnicode_InternFromString(((PyCFunctionObject *)func)->m_ml->ml_name); - if (unlikely(func->func_name == NULL)) - return NULL; } - Py_INCREF(func->func_name); - return func->func_name; + result = func->func_name; + Py_XINCREF(result); + Py_END_CRITICAL_SECTION(); + return result; } int CPyFunction_set_name(PyObject *op, PyObject *value, void *context) { @@ -77,8 +86,13 @@ int CPyFunction_set_name(PyObject *op, PyObject *value, void *context) { } Py_INCREF(value); - Py_XDECREF(func->func_name); + // Decref outside critical section, since it could run arbitrary code. + PyObject *old; + Py_BEGIN_CRITICAL_SECTION(op); + old = func->func_name; func->func_name = value; + Py_END_CRITICAL_SECTION(); + Py_XDECREF(old); return 0; } @@ -232,12 +246,31 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu PyObject *code = NULL, *op = NULL; bool set_self = false; +#ifdef Py_GIL_DISABLED + // Double-checked locking: the common case (type already created) is a + // lock-free atomic load. Only the first-time initialization takes the + // mutex, which serializes concurrent creators. + if (!_Py_atomic_load_ptr_acquire(&CPyFunctionType)) { + static PyMutex type_init_mutex = {0}; + PyMutex_Lock(&type_init_mutex); + if (!CPyFunctionType) { + PyTypeObject *type = (PyTypeObject *)PyType_FromSpec(&CPyFunction_spec); + if (unlikely(!type)) { + PyMutex_Unlock(&type_init_mutex); + goto err; + } + _Py_atomic_store_ptr_release(&CPyFunctionType, type); + } + PyMutex_Unlock(&type_init_mutex); + } +#else if (!CPyFunctionType) { CPyFunctionType = (PyTypeObject *)PyType_FromSpec(&CPyFunction_spec); if (unlikely(!CPyFunctionType)) { goto err; } } +#endif method = CPyMethodDef_New(funcname, func, func_flags, func_doc); if (unlikely(!method)) { From 74ecdd8ecc9bee271eab49948436af63751ed1ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:52:19 -0700 Subject: [PATCH 081/127] Sync typeshed (#21612) Source commit: https://github.com/python/typeshed/commit/feeb9aa8dde3ae9269b13f3bae435b82d8538b76 --- mypy/typeshed/stdlib/_socket.pyi | 73 ++++++++++++++++++- .../_typeshed/_type_checker_internals.pyi | 6 ++ mypy/typeshed/stdlib/base64.pyi | 7 +- mypy/typeshed/stdlib/calendar.pyi | 12 +-- mypy/typeshed/stdlib/pyexpat/__init__.pyi | 3 +- mypy/typeshed/stdlib/socket.pyi | 67 +++++++++++++++++ mypy/typeshed/stdlib/sys/__init__.pyi | 2 +- mypy/typeshed/stdlib/typing.pyi | 8 ++ 8 files changed, 165 insertions(+), 13 deletions(-) diff --git a/mypy/typeshed/stdlib/_socket.pyi b/mypy/typeshed/stdlib/_socket.pyi index bb9b08e2f79ad..4450d46e6027e 100644 --- a/mypy/typeshed/stdlib/_socket.pyi +++ b/mypy/typeshed/stdlib/_socket.pyi @@ -11,7 +11,11 @@ _CMSGArg: TypeAlias = tuple[int, int, ReadableBuffer] # Addresses can be either tuples of varying lengths (AF_INET, AF_INET6, # AF_NETLINK, AF_TIPC) or strings/buffers (AF_UNIX). # See getsockaddrarg() in socketmodule.c. -_Address: TypeAlias = tuple[Any, ...] | str | ReadableBuffer +if sys.version_info >= (3, 14): + # A bare int is accepted for Bluetooth HCI device IDs. + _Address: TypeAlias = tuple[Any, ...] | str | ReadableBuffer | int +else: + _Address: TypeAlias = tuple[Any, ...] | str | ReadableBuffer _RetAddress: TypeAlias = Any # ===== Constants ===== @@ -250,8 +254,75 @@ if sys.version_info >= (3, 14): TCP_QUICKACK: Final[int] if sys.platform == "linux": + BDADDR_BREDR: Final[int] + BDADDR_LE_PUBLIC: Final[int] + BDADDR_LE_RANDOM: Final[int] + BT_CHANNEL_POLICY: Final[int] + BT_CHANNEL_POLICY_BREDR_ONLY: Final[int] + BT_CHANNEL_POLICY_BREDR_PREFERRED: Final[int] + BT_CODEC: Final[int] + BT_DEFER_SETUP: Final[int] + BT_FLUSHABLE: Final[int] + BT_FLUSHABLE_OFF: Final[int] + BT_FLUSHABLE_ON: Final[int] + BT_ISO_QOS: Final[int] + BT_MODE: Final[int] + BT_MODE_BASIC: Final[int] + BT_MODE_ERTM: Final[int] + BT_MODE_EXT_FLOWCTL: Final[int] + BT_MODE_LE_FLOWCTL: Final[int] + BT_MODE_STREAMING: Final[int] + BT_PHY: Final[int] + BT_PHY_BR_1M_1SLOT: Final[int] + BT_PHY_BR_1M_3SLOT: Final[int] + BT_PHY_BR_1M_5SLOT: Final[int] + BT_PHY_EDR_2M_1SLOT: Final[int] + BT_PHY_EDR_2M_3SLOT: Final[int] + BT_PHY_EDR_2M_5SLOT: Final[int] + BT_PHY_EDR_3M_1SLOT: Final[int] + BT_PHY_EDR_3M_3SLOT: Final[int] + BT_PHY_EDR_3M_5SLOT: Final[int] + BT_PHY_LE_1M_RX: Final[int] + BT_PHY_LE_1M_TX: Final[int] + BT_PHY_LE_2M_RX: Final[int] + BT_PHY_LE_2M_TX: Final[int] + BT_PHY_LE_CODED_RX: Final[int] + BT_PHY_LE_CODED_TX: Final[int] + BT_PKT_STATUS: Final[int] + BT_POWER: Final[int] + BT_POWER_FORCE_ACTIVE_OFF: Final[int] + BT_POWER_FORCE_ACTIVE_ON: Final[int] + BT_RCVMTU: Final[int] + BT_SECURITY: Final[int] + BT_SECURITY_FIPS: Final[int] + BT_SECURITY_HIGH: Final[int] + BT_SECURITY_LOW: Final[int] + BT_SECURITY_MEDIUM: Final[int] + BT_SECURITY_SDP: Final[int] + BT_SNDMTU: Final[int] + BT_VOICE: Final[int] + BT_VOICE_CVSD_16BIT: Final[int] + BT_VOICE_TRANSPARENT: Final[int] + BT_VOICE_TRANSPARENT_16BIT: Final[int] + HCI_CHANNEL_CONTROL: Final[int] + HCI_CHANNEL_LOGGING: Final[int] + HCI_CHANNEL_MONITOR: Final[int] + HCI_CHANNEL_RAW: Final[int] + HCI_CHANNEL_USER: Final[int] + HCI_DEV_NONE: Final[int] IP_FREEBIND: Final[int] IP_RECVORIGDSTADDR: Final[int] + L2CAP_LM: Final[int] + L2CAP_LM_AUTH: Final[int] + L2CAP_LM_ENCRYPT: Final[int] + L2CAP_LM_MASTER: Final[int] + L2CAP_LM_RELIABLE: Final[int] + L2CAP_LM_SECURE: Final[int] + L2CAP_LM_TRUSTED: Final[int] + SOL_BLUETOOTH: Final[int] + SOL_L2CAP: Final[int] + SOL_RFCOMM: Final[int] + SOL_SCO: Final[int] VMADDR_CID_LOCAL: Final[int] if sys.platform != "win32" and sys.platform != "darwin": diff --git a/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi b/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi index 375e997e2c932..a8411192a898d 100644 --- a/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi +++ b/mypy/typeshed/stdlib/_typeshed/_type_checker_internals.pyi @@ -5,6 +5,7 @@ import sys import typing_extensions from _collections_abc import dict_items, dict_keys, dict_values +from _typeshed import AnnotationForm from abc import ABCMeta from collections.abc import Awaitable, Generator, Iterable, Mapping from typing import Any, ClassVar, Generic, TypeVar, overload @@ -28,6 +29,10 @@ class TypedDictFallback(Mapping[str, object], metaclass=ABCMeta): if sys.version_info >= (3, 13): __readonly_keys__: ClassVar[frozenset[str]] __mutable_keys__: ClassVar[frozenset[str]] + if sys.version_info >= (3, 15): + # PEP 728 + __closed__: ClassVar[bool | None] + __extra_items__: ClassVar[AnnotationForm] def copy(self) -> typing_extensions.Self: ... # Using Never so that only calls using mypy plugin hook that specialize the signature @@ -58,6 +63,7 @@ class TypedDictFallback(Mapping[str, object], metaclass=ABCMeta): class NamedTupleFallback(tuple[Any, ...]): _field_defaults: ClassVar[dict[str, Any]] _fields: ClassVar[tuple[str, ...]] + __match_args__: ClassVar[tuple[str, ...]] = ... # __orig_bases__ sometimes exists on <3.12, but not consistently # So we only add it to the stub on 3.12+. if sys.version_info >= (3, 12): diff --git a/mypy/typeshed/stdlib/base64.pyi b/mypy/typeshed/stdlib/base64.pyi index 67bc37309a976..dd4782142852f 100644 --- a/mypy/typeshed/stdlib/base64.pyi +++ b/mypy/typeshed/stdlib/base64.pyi @@ -1,6 +1,5 @@ import sys -from _typeshed import ReadableBuffer -from typing import IO +from _typeshed import ReadableBuffer, SupportsNoArgReadline, SupportsRead, SupportsWrite __all__ = [ "encode", @@ -111,8 +110,8 @@ else: def b85encode(b: ReadableBuffer, pad: bool = False) -> bytes: ... def b85decode(b: str | ReadableBuffer) -> bytes: ... -def decode(input: IO[bytes], output: IO[bytes]) -> None: ... -def encode(input: IO[bytes], output: IO[bytes]) -> None: ... +def decode(input: SupportsNoArgReadline[bytes], output: SupportsWrite[bytes]) -> None: ... +def encode(input: SupportsRead[bytes], output: SupportsWrite[bytes]) -> None: ... def encodebytes(s: ReadableBuffer) -> bytes: ... def decodebytes(s: ReadableBuffer) -> bytes: ... diff --git a/mypy/typeshed/stdlib/calendar.pyi b/mypy/typeshed/stdlib/calendar.pyi index 63ec715fb51be..fb75d9559e977 100644 --- a/mypy/typeshed/stdlib/calendar.pyi +++ b/mypy/typeshed/stdlib/calendar.pyi @@ -80,18 +80,18 @@ class Calendar: def __init__(self, firstweekday: int = 0) -> None: ... def getfirstweekday(self) -> int: ... def setfirstweekday(self, firstweekday: int) -> None: ... - def iterweekdays(self) -> Iterable[int]: ... - def itermonthdates(self, year: int, month: int) -> Iterable[datetime.date]: ... - def itermonthdays2(self, year: int, month: int) -> Iterable[tuple[int, int]]: ... - def itermonthdays(self, year: int, month: int) -> Iterable[int]: ... + def iterweekdays(self) -> Iterator[int]: ... + def itermonthdates(self, year: int, month: int) -> Iterator[datetime.date]: ... + def itermonthdays2(self, year: int, month: int) -> Iterator[tuple[int, int]]: ... + def itermonthdays(self, year: int, month: int) -> Iterator[int]: ... def monthdatescalendar(self, year: int, month: int) -> list[list[datetime.date]]: ... def monthdays2calendar(self, year: int, month: int) -> list[list[tuple[int, int]]]: ... def monthdayscalendar(self, year: int, month: int) -> list[list[int]]: ... def yeardatescalendar(self, year: int, width: int = 3) -> list[list[list[list[datetime.date]]]]: ... def yeardays2calendar(self, year: int, width: int = 3) -> list[list[list[list[tuple[int, int]]]]]: ... def yeardayscalendar(self, year: int, width: int = 3) -> list[list[list[list[int]]]]: ... - def itermonthdays3(self, year: int, month: int) -> Iterable[tuple[int, int, int]]: ... - def itermonthdays4(self, year: int, month: int) -> Iterable[tuple[int, int, int, int]]: ... + def itermonthdays3(self, year: int, month: int) -> Iterator[tuple[int, int, int]]: ... + def itermonthdays4(self, year: int, month: int) -> Iterator[tuple[int, int, int, int]]: ... class TextCalendar(Calendar): def prweek(self, theweek: Iterable[tuple[int, int]], width: int) -> None: ... diff --git a/mypy/typeshed/stdlib/pyexpat/__init__.pyi b/mypy/typeshed/stdlib/pyexpat/__init__.pyi index 806db63693b50..83841a226a53c 100644 --- a/mypy/typeshed/stdlib/pyexpat/__init__.pyi +++ b/mypy/typeshed/stdlib/pyexpat/__init__.pyi @@ -33,7 +33,8 @@ class XMLParserType: # Added in Python 3.10.20, 3.11.15, 3.12.3, 3.13.10, 3.14.1 def SetAllocTrackerActivationThreshold(self, threshold: int, /) -> None: ... def SetAllocTrackerMaximumAmplification(self, max_factor: float, /) -> None: ... - if sys.version_info >= (3, 15): + if sys.version_info >= (3, 13): + # Added in Python 3.13.4, 3.14.6 def SetBillionLaughsAttackProtectionActivationThreshold(self, threshold: int, /) -> None: ... def SetBillionLaughsAttackProtectionMaximumAmplification(self, max_factor: float, /) -> None: ... diff --git a/mypy/typeshed/stdlib/socket.pyi b/mypy/typeshed/stdlib/socket.pyi index 76ba0a85c128c..87e2bd353730e 100644 --- a/mypy/typeshed/stdlib/socket.pyi +++ b/mypy/typeshed/stdlib/socket.pyi @@ -1133,8 +1133,75 @@ if sys.version_info >= (3, 14): if sys.platform == "linux": from _socket import ( + BDADDR_BREDR as BDADDR_BREDR, + BDADDR_LE_PUBLIC as BDADDR_LE_PUBLIC, + BDADDR_LE_RANDOM as BDADDR_LE_RANDOM, + BT_CHANNEL_POLICY as BT_CHANNEL_POLICY, + BT_CHANNEL_POLICY_BREDR_ONLY as BT_CHANNEL_POLICY_BREDR_ONLY, + BT_CHANNEL_POLICY_BREDR_PREFERRED as BT_CHANNEL_POLICY_BREDR_PREFERRED, + BT_CODEC as BT_CODEC, + BT_DEFER_SETUP as BT_DEFER_SETUP, + BT_FLUSHABLE as BT_FLUSHABLE, + BT_FLUSHABLE_OFF as BT_FLUSHABLE_OFF, + BT_FLUSHABLE_ON as BT_FLUSHABLE_ON, + BT_ISO_QOS as BT_ISO_QOS, + BT_MODE as BT_MODE, + BT_MODE_BASIC as BT_MODE_BASIC, + BT_MODE_ERTM as BT_MODE_ERTM, + BT_MODE_EXT_FLOWCTL as BT_MODE_EXT_FLOWCTL, + BT_MODE_LE_FLOWCTL as BT_MODE_LE_FLOWCTL, + BT_MODE_STREAMING as BT_MODE_STREAMING, + BT_PHY as BT_PHY, + BT_PHY_BR_1M_1SLOT as BT_PHY_BR_1M_1SLOT, + BT_PHY_BR_1M_3SLOT as BT_PHY_BR_1M_3SLOT, + BT_PHY_BR_1M_5SLOT as BT_PHY_BR_1M_5SLOT, + BT_PHY_EDR_2M_1SLOT as BT_PHY_EDR_2M_1SLOT, + BT_PHY_EDR_2M_3SLOT as BT_PHY_EDR_2M_3SLOT, + BT_PHY_EDR_2M_5SLOT as BT_PHY_EDR_2M_5SLOT, + BT_PHY_EDR_3M_1SLOT as BT_PHY_EDR_3M_1SLOT, + BT_PHY_EDR_3M_3SLOT as BT_PHY_EDR_3M_3SLOT, + BT_PHY_EDR_3M_5SLOT as BT_PHY_EDR_3M_5SLOT, + BT_PHY_LE_1M_RX as BT_PHY_LE_1M_RX, + BT_PHY_LE_1M_TX as BT_PHY_LE_1M_TX, + BT_PHY_LE_2M_RX as BT_PHY_LE_2M_RX, + BT_PHY_LE_2M_TX as BT_PHY_LE_2M_TX, + BT_PHY_LE_CODED_RX as BT_PHY_LE_CODED_RX, + BT_PHY_LE_CODED_TX as BT_PHY_LE_CODED_TX, + BT_PKT_STATUS as BT_PKT_STATUS, + BT_POWER as BT_POWER, + BT_POWER_FORCE_ACTIVE_OFF as BT_POWER_FORCE_ACTIVE_OFF, + BT_POWER_FORCE_ACTIVE_ON as BT_POWER_FORCE_ACTIVE_ON, + BT_RCVMTU as BT_RCVMTU, + BT_SECURITY as BT_SECURITY, + BT_SECURITY_FIPS as BT_SECURITY_FIPS, + BT_SECURITY_HIGH as BT_SECURITY_HIGH, + BT_SECURITY_LOW as BT_SECURITY_LOW, + BT_SECURITY_MEDIUM as BT_SECURITY_MEDIUM, + BT_SECURITY_SDP as BT_SECURITY_SDP, + BT_SNDMTU as BT_SNDMTU, + BT_VOICE as BT_VOICE, + BT_VOICE_CVSD_16BIT as BT_VOICE_CVSD_16BIT, + BT_VOICE_TRANSPARENT as BT_VOICE_TRANSPARENT, + BT_VOICE_TRANSPARENT_16BIT as BT_VOICE_TRANSPARENT_16BIT, + HCI_CHANNEL_CONTROL as HCI_CHANNEL_CONTROL, + HCI_CHANNEL_LOGGING as HCI_CHANNEL_LOGGING, + HCI_CHANNEL_MONITOR as HCI_CHANNEL_MONITOR, + HCI_CHANNEL_RAW as HCI_CHANNEL_RAW, + HCI_CHANNEL_USER as HCI_CHANNEL_USER, + HCI_DEV_NONE as HCI_DEV_NONE, IP_FREEBIND as IP_FREEBIND, IP_RECVORIGDSTADDR as IP_RECVORIGDSTADDR, + L2CAP_LM as L2CAP_LM, + L2CAP_LM_AUTH as L2CAP_LM_AUTH, + L2CAP_LM_ENCRYPT as L2CAP_LM_ENCRYPT, + L2CAP_LM_MASTER as L2CAP_LM_MASTER, + L2CAP_LM_RELIABLE as L2CAP_LM_RELIABLE, + L2CAP_LM_SECURE as L2CAP_LM_SECURE, + L2CAP_LM_TRUSTED as L2CAP_LM_TRUSTED, + SOL_BLUETOOTH as SOL_BLUETOOTH, + SOL_L2CAP as SOL_L2CAP, + SOL_RFCOMM as SOL_RFCOMM, + SOL_SCO as SOL_SCO, VMADDR_CID_LOCAL as VMADDR_CID_LOCAL, ) diff --git a/mypy/typeshed/stdlib/sys/__init__.pyi b/mypy/typeshed/stdlib/sys/__init__.pyi index 80fb9348a6046..72e894b9305fd 100644 --- a/mypy/typeshed/stdlib/sys/__init__.pyi +++ b/mypy/typeshed/stdlib/sys/__init__.pyi @@ -10,7 +10,7 @@ from typing_extensions import LiteralString, deprecated _T = TypeVar("_T") _LazyImportMode: TypeAlias = Literal["normal", "all", "none"] -_LazyImportFilter: TypeAlias = Callable[[str, str, tuple[str, ...] | None], bool] +_LazyImportFilter: TypeAlias = Callable[[str | None, str, tuple[str, ...] | None], bool] # see https://github.com/python/typeshed/issues/8513#issue-1333671093 for the rationale behind this alias _ExitCode: TypeAlias = str | int | None diff --git a/mypy/typeshed/stdlib/typing.pyi b/mypy/typeshed/stdlib/typing.pyi index 2379bec348a82..811a77df84b76 100644 --- a/mypy/typeshed/stdlib/typing.pyi +++ b/mypy/typeshed/stdlib/typing.pyi @@ -1046,6 +1046,7 @@ if sys.version_info >= (3, 11): class NamedTuple(tuple[Any, ...]): _field_defaults: ClassVar[dict[str, Any]] _fields: ClassVar[tuple[str, ...]] + __match_args__: ClassVar[tuple[str, ...]] = ... # __orig_bases__ sometimes exists on <3.12, but not consistently # So we only add it to the stub on 3.12+. if sys.version_info >= (3, 12): @@ -1057,9 +1058,12 @@ class NamedTuple(tuple[Any, ...]): @deprecated("Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15") def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ... + @final @classmethod def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... + @final def _asdict(self) -> dict[str, Any]: ... + @final def _replace(self, **kwargs: Any) -> typing_extensions.Self: ... if sys.version_info >= (3, 13): def __replace__(self, **kwargs: Any) -> typing_extensions.Self: ... @@ -1079,6 +1083,10 @@ class _TypedDict(Mapping[str, object], metaclass=ABCMeta): if sys.version_info >= (3, 13): __readonly_keys__: ClassVar[frozenset[str]] __mutable_keys__: ClassVar[frozenset[str]] + if sys.version_info >= (3, 15): + # PEP 728 + __closed__: ClassVar[bool | None] + __extra_items__: ClassVar[Any] # AnnotationForm def copy(self) -> typing_extensions.Self: ... # Using Never so that only calls using mypy plugin hook that specialize the signature From 8d621adb954731ba76b25bf7130275f3cadbb81a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jun 2026 15:50:17 +0100 Subject: [PATCH 082/127] Support --shadow-file with --native-parser (#21623) --- mypy/build.py | 4 +- test-data/unit/check-incremental.test | 65 +++++++++++++++++++++++++++ test-data/unit/cmdline.test | 29 ++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index 97db8fe1646cd..7fc9526ccb2fe 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -1030,7 +1030,9 @@ def parse_all(self, states: list[State], post_parse: bool = True) -> None: parallel_states = [] for state in states: - if not self.fscache.exists(state.xpath, real_only=True): + if not self.fscache.exists(state.xpath, real_only=True) or ( + self.shadow_map and self.maybe_swap_for_shadow_path(state.xpath) != state.xpath + ): state.source = state.get_source() if state.tree is not None: # The file was already parsed. diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 9647393cdac66..7ff9fbef06a53 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -8139,3 +8139,68 @@ reveal_type(test("int")) [out] [out2] tmp/b.py:2: note: Revealed type is "builtins.int" + +[case testIncrementalNativeParserShadowFile] +# flags: --native-parser --shadow-file tmp/a.py tmp/a_shadow.py +import a +import b +-- a and b don't depend on each other, so they can be checked in parallel. +[file a.py] +x: int = 1 +reveal_type(x) +[file b.py] +y: int = 2 +[file a_shadow.py] +x: str = "y" +reveal_type(x) +[rechecked] +[stale] +[out1] +tmp/a.py:2: note: Revealed type is "builtins.str" +[out2] +tmp/a.py:2: note: Revealed type is "builtins.str" + +[case testIncrementalNativeParserShadowFileChanged] +# flags: --native-parser --shadow-file tmp/a.py tmp/a_shadow.py +import a +import b +-- a and b don't depend on each other, so they can be checked in parallel. +[file a.py] +x: int = 1 +reveal_type(x) +[file b.py] +y: int = 2 +[file a_shadow.py] +x: str = "y" +reveal_type(x) +[file a_shadow.py.2] +x: bytes = b"y" +reveal_type(x) +[rechecked a] +[stale a] +[out1] +tmp/a.py:2: note: Revealed type is "builtins.str" +[out2] +tmp/a.py:2: note: Revealed type is "builtins.bytes" + +[case testIncrementalNativeParserShadowFileIntroduced] +# flags: --native-parser +# flags2: --native-parser --shadow-file tmp/a.py tmp/a_shadow.py +import a +import b +-- a and b don't depend on each other, so they can be checked in parallel. +-- The first run has no shadow file; the second run introduces one for a.py. +[file a.py] +x: int = 1 +reveal_type(x) +[file b.py] +y: int = 2 +[file a_shadow.py.2] +x: str = "y" +reveal_type(x) +[rechecked a] +[stale a] +[out1] +tmp/a.py:2: note: Revealed type is "builtins.int" +[out2] +tmp/a.py:2: note: Revealed type is "builtins.str" diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index cfba7a81e9285..7066034b3e39c 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -530,6 +530,35 @@ variable has type "bytes") b: bytes = 1 ^ +[case testShadowFileWithNativeParser] +# cmd: mypy --native-parser --shadow-file source.py shadow.py source.py +[file source.py] +x: int = 1 +reveal_type(x) +[file shadow.py] +x: str = "y" +reveal_type(x) +[out] +source.py:2: note: Revealed type is "str" +== Return code: 0 + +[case testShadowFileWithNativeParserParallel] +# cmd: mypy --native-parser --num-workers=4 --shadow-file a.py a_shadow.py main.py a.py b.py +[file main.py] +import a +import b +[file a.py] +x: int = 1 +reveal_type(x) +[file b.py] +y: int = 2 +[file a_shadow.py] +x: str = "y" +reveal_type(x) +[out] +a.py:2: note: Revealed type is "str" +== Return code: 0 + [case testConfigWarnUnusedSection1] # cmd: mypy foo.py quux.py spam/eggs.py [file mypy.ini] From 5ef090270579013c337a9bcc79e2c58417f09538 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jun 2026 18:16:54 +0100 Subject: [PATCH 083/127] Fix the exportjson tool (.ff cache to .json conversion) (#21628) It was broken by lazy cache deserialization, and the tests didn't catch it since the fixup state was persisted within the test. Both fix the exportjson tool and make the test more realistic. --- mypy/exportjson.py | 8 ++++++-- mypy/nodes.py | 11 +++++++++++ mypy/test/testexportjson.py | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mypy/exportjson.py b/mypy/exportjson.py index c08f0f9f29118..7bd341401ce9d 100644 --- a/mypy/exportjson.py +++ b/mypy/exportjson.py @@ -128,8 +128,12 @@ def convert_symbol_table_node(self: SymbolTableNode, cfg: Config) -> Json: data["plugin_generated"] = True if self.cross_ref: data["cross_ref"] = self.cross_ref - elif self.node is not None: - data["node"] = convert_symbol_node(self.node, cfg) + else: + # Read the raw node without cross-reference fixup, since exportjson reads + # cache files in isolation and no node fixer is available. + node = self.read_node_no_fixup() + if node is not None: + data["node"] = convert_symbol_node(node, cfg) return data diff --git a/mypy/nodes.py b/mypy/nodes.py index f837185b858a7..e2ea348d2df11 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -4952,6 +4952,17 @@ def node(self) -> SymbolNode | None: self.unfixed = False return self._node + def read_node_no_fixup(self) -> SymbolNode | None: + """Return the deserialized node without performing cross-reference fixup. + + This is intended for introspection tools (such as mypy.exportjson) that read + cache files in isolation, where no node fixer is available. + """ + if self._node is None and self._node_bytes: + self._node = read_symbol(ReadBuffer(self._node_bytes), self._node_tag) + self._node_bytes = b"" + return self._node + def copy(self) -> SymbolTableNode: new = SymbolTableNode( self.kind, self._node, self.module_public, self.implicit, self.module_hidden diff --git a/mypy/test/testexportjson.py b/mypy/test/testexportjson.py index 58b0dea5e5a6d..294befcb3731c 100644 --- a/mypy/test/testexportjson.py +++ b/mypy/test/testexportjson.py @@ -11,6 +11,7 @@ from mypy.errors import CompileError from mypy.exportjson import convert_binary_cache_meta_to_json, convert_binary_cache_to_json from mypy.modulefinder import BuildSource +from mypy.modules_state import modules_state from mypy.options import Options from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite @@ -44,6 +45,11 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: major, minor = sys.version_info[:2] cache_dir = os.path.join(".mypy_cache", f"{major}.{minor}") + # Reset the global fixup state, since the exportjson tool + # reads cache files in isolation (no node fixer available). + modules_state.node_fixer = None + modules_state.modules = {} + for module in result.files: if module in ( "builtins", From 9d5e32c76ef28862cb373ee1a33db9268406b83f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 22 Jun 2026 13:39:02 +0100 Subject: [PATCH 084/127] [mypyc] Fix non-deterministic compiler output due to frozensets (#21631) Frozenset literals were emitted in an unpredictable order due to hash randomization. Make the order deterministic. I used some coding agent assist (esp. with tests). --------- Co-authored-by: Piotr Sawicki --- mypyc/codegen/emit.py | 44 ++++++++++++++++++++-------------- mypyc/codegen/literals.py | 24 +++++++++++++++---- mypyc/test/test_emit.py | 26 ++++++++++++++------ mypyc/test/test_literals.py | 48 +++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 29 deletions(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index b89c91343e66c..57cce6a3fe8fe 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -2,14 +2,12 @@ from __future__ import annotations -import pprint import sys -import textwrap from collections.abc import Callable from typing import Final from mypyc.codegen.cstring import c_string_initializer -from mypyc.codegen.literals import Literals +from mypyc.codegen.literals import Literals, literal_sort_key from mypyc.common import ( ATTR_PREFIX, BITMAP_BITS, @@ -237,24 +235,16 @@ def attr(self, name: str) -> str: return ATTR_PREFIX + name def object_annotation(self, obj: object, line: str) -> str: - """Build a C comment with an object's string representation. + """Build a C comment with a literal value's string representation. - If the comment exceeds the line length limit, it's wrapped into a - multiline string (with the extra lines indented to be aligned with - the first line's comment). + This is a debugging aid that makes generated C easier to read. - If it contains illegal characters, an empty string is returned.""" - line_width = self._indent + len(line) - formatted = pprint.pformat(obj, compact=True, indent=1, width=max(90 - line_width, 20)) - if any(x in formatted for x in ("/*", "*/", "\0")): + If it contains illegal characters or is too long, return an empty string. + """ + formatted = stable_literal_repr(obj) + if any(x in formatted for x in ("/*", "*/", "\0")) or len(formatted) >= 256: return "" - - if "\n" in formatted: - first_line, rest = formatted.split("\n", maxsplit=1) - comment_continued = textwrap.indent(rest, (line_width + 3) * " ") - return f" /* {first_line}\n{comment_continued} */" - else: - return f" /* {formatted} */" + return f" /* {formatted} */" def emit_line(self, line: str = "", *, ann: object = None) -> None: if line.startswith("}"): @@ -1486,3 +1476,21 @@ def native_function_doc_initializer(func: FuncIR) -> str: return "NULL" docstring = f"{text_sig}\n--\n\n" return c_string_initializer(docstring.encode("ascii", errors="backslashreplace")) + + +def stable_literal_repr(obj: object) -> str: + """Return a single-line repr of a literal value. + + Behaves like repr() for most values, but renders frozenset members in a + deterministic order (frozenset iteration order is hash-seed dependent). + """ + if isinstance(obj, frozenset): + if not obj: + return "frozenset()" + items = ", ".join(stable_literal_repr(item) for item in sorted(obj, key=literal_sort_key)) + return "frozenset({" + items + "})" + elif isinstance(obj, tuple): + if len(obj) == 1: + return "(" + stable_literal_repr(obj[0]) + ",)" + return "(" + ", ".join(stable_literal_repr(item) for item in obj) + ")" + return repr(obj) diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index ed1ff93277167..a8d4650b5c294 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -65,7 +65,8 @@ def record_literal(self, value: LiteralValue) -> None: elif isinstance(value, frozenset): frozenset_literals = self.frozenset_literals if value not in frozenset_literals: - for item in value: + # Sort members so that we don't depend on frozenset iteration order. + for item in sorted(value, key=literal_sort_key): assert _is_literal_value(item) self.record_literal(item) frozenset_literals[value] = len(frozenset_literals) @@ -140,10 +141,14 @@ def encoded_tuple_values(self) -> list[str]: return self._encode_collection_values(self.tuple_literals) def encoded_frozenset_values(self) -> list[str]: - return self._encode_collection_values(self.frozenset_literals) + # Ensure deterministic frozenset item order by sorting items. + return self._encode_collection_values(self.frozenset_literals, sort_items=True) def _encode_collection_values( - self, values: dict[tuple[object, ...], int] | dict[frozenset[object], int] + self, + values: dict[tuple[object, ...], int] | dict[frozenset[object], int], + *, + sort_items: bool = False, ) -> list[str]: """Encode tuple/frozenset values into a C array. @@ -164,7 +169,8 @@ def _encode_collection_values( for i in range(count): value = value_by_index[i] result.append(str(len(value))) - for item in value: + items = sorted(value, key=literal_sort_key) if sort_items else value + for item in items: assert _is_literal_value(item) index = self.literal_index(item) result.append(str(index)) @@ -299,3 +305,13 @@ def _encode_complex_values(values: dict[complex, int]) -> list[str]: result.append(float_to_c(value.real)) result.append(float_to_c(value.imag)) return result + + +def literal_sort_key(value: object) -> tuple[object, ...]: + """Return a sort key for a literal value.""" + if isinstance(value, frozenset): + # Sort items to avoid depending on the unpredictable iteration order. + return ("frozenset", tuple(sorted(literal_sort_key(item) for item in value))) + elif isinstance(value, tuple): + return ("tuple", tuple(literal_sort_key(item) for item in value)) + return (type(value).__name__, repr(value)) diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index 285488e03c9ae..b2199b2dcb3b2 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -42,9 +42,23 @@ def test_reg(self) -> None: def test_object_annotation(self) -> None: assert self.emitter.object_annotation("hello, world", "line;") == " /* 'hello, world' */" - assert self.emitter.object_annotation(list(range(30)), "line;") == """\ - /* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29] */""" + assert self.emitter.object_annotation(42, "line;") == " /* 42 */" + assert self.emitter.object_annotation((1, "x", None), "line;") == " /* (1, 'x', None) */" + # Annotations containing illegal C comment characters are dropped. + assert self.emitter.object_annotation("a /* b */ c", "line;") == "" + + def test_object_annotation_frozenset_is_deterministic(self) -> None: + assert ( + self.emitter.object_annotation(frozenset({"self", "cls"}), "line;") + == self.emitter.object_annotation(frozenset({"cls", "self"}), "line;") + == " /* frozenset({'cls', 'self'}) */" + ) + assert ( + self.emitter.object_annotation((frozenset({"b", "a"}),), "line;") + == self.emitter.object_annotation((frozenset({"a", "b"}),), "line;") + == " /* (frozenset({'a', 'b'}),) */" + ) + assert self.emitter.object_annotation(frozenset(), "line;") == " /* frozenset() */" def test_emit_line(self) -> None: emitter = self.emitter @@ -55,11 +69,9 @@ def test_emit_line(self) -> None: assert emitter.fragments == ["line;\n", "a {\n", " f();\n", "}\n"] emitter = Emitter(self.context, {}) emitter.emit_line("CPyStatics[0];", ann="hello, world") - emitter.emit_line("CPyStatics[1];", ann=list(range(30))) + emitter.emit_line("CPyStatics[1];", ann=42) assert emitter.fragments[0] == "CPyStatics[0]; /* 'hello, world' */\n" - assert emitter.fragments[1] == """\ -CPyStatics[1]; /* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29] */\n""" + assert emitter.fragments[1] == "CPyStatics[1]; /* 42 */\n" def test_emit_undefined_value_for_simple_type(self) -> None: emitter = self.emitter diff --git a/mypyc/test/test_literals.py b/mypyc/test/test_literals.py index a8c17d10d30d0..f1dc7f2434414 100644 --- a/mypyc/test/test_literals.py +++ b/mypyc/test/test_literals.py @@ -10,6 +10,7 @@ _encode_int_values, _encode_str_values, format_str_literal, + literal_sort_key, ) @@ -88,3 +89,50 @@ def test_tuple_literal(self) -> None: "7", # Second tuple (length=4) "0", # Third tuple (length=0) ] + + def test_frozenset_literal_index_is_deterministic(self) -> None: + # Index assignment for members must not depend on frozenset iteration + # order (which is hash-seed dependent), so that generated code is + # reproducible. + lit1 = Literals() + lit1.record_literal(frozenset({"self", "cls"})) + lit2 = Literals() + lit2.record_literal(frozenset({"cls", "self"})) + for s in ("self", "cls"): + assert lit1.literal_index(s) == lit2.literal_index(s) + # Members are recorded in sorted order. + assert lit1.literal_index("cls") == 3 + assert lit1.literal_index("self") == 4 + + def test_frozenset_encoding_is_deterministic(self) -> None: + lit1 = Literals() + lit1.record_literal(frozenset({"self", "cls"})) + lit2 = Literals() + lit2.record_literal(frozenset({"cls", "self"})) + assert lit1.encoded_frozenset_values() == lit2.encoded_frozenset_values() + + def test_literal_sort_key_is_total_over_types(self) -> None: + # Heterogeneous, individually unorderable items must still be sorted. + values = ["x", b"y", 1, None, (1, 2), frozenset({1, 2})] + values_reversed = list(reversed(values)) + assert sorted(values, key=literal_sort_key) == sorted( + values_reversed, key=literal_sort_key + ) + + def test_literal_sort_key_with_frozenset(self) -> None: + assert literal_sort_key(frozenset({"a", "b"})) == literal_sort_key(frozenset({"b", "a"})) + assert literal_sort_key((frozenset({"a", "b"}),)) == literal_sort_key( + (frozenset({"b", "a"}),) + ) + assert literal_sort_key(frozenset({"a", frozenset({"b", "c"})})) == literal_sort_key( + frozenset({frozenset({"c", "b"}), "a"}) + ) + + def test_nested_frozenset_literal_index_is_deterministic(self) -> None: + lit1 = Literals() + lit1.record_literal(frozenset({frozenset({"a", "b"}), frozenset({"c", "d"})})) + lit2 = Literals() + lit2.record_literal(frozenset({frozenset({"d", "c"}), frozenset({"b", "a"})})) + for s in ("a", "b", "c", "d"): + assert lit1.literal_index(s) == lit2.literal_index(s) + assert lit1.encoded_frozenset_values() == lit2.encoded_frozenset_values() From 862af99f68160d783b289905f3907dc693a40af2 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 22 Jun 2026 14:15:24 +0100 Subject: [PATCH 085/127] [mypyc] Fix non-deterministic ordering of spilled registers (#21632) Sort ops by their order in the basic blocks. --- mypyc/transform/spill.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/mypyc/transform/spill.py b/mypyc/transform/spill.py index d92dd661e7eb4..c2d4d4311375d 100644 --- a/mypyc/transform/spill.py +++ b/mypyc/transform/spill.py @@ -2,6 +2,9 @@ from __future__ import annotations +from collections.abc import Collection +from typing import cast + from mypyc.analysis.dataflow import AnalysisResult, analyze_live_regs, get_cfg from mypyc.common import TEMP_ATTR_NAME from mypyc.ir.class_ir import ClassIR @@ -13,6 +16,7 @@ GetAttr, IncRef, LoadErrorValue, + Op, Register, SetAttr, Value, @@ -31,6 +35,19 @@ def insert_spills(ir: FuncIR, env: ClassIR) -> None: ir.blocks = spill_regs(ir.blocks, env, entry_live, live, ir.arg_regs[0]) +def sort_values(values: Collection[Op], blocks: list[BasicBlock]) -> list[Op]: + if len(values) > 1: + order = {} + i = 0 + for block in blocks: + for op in block.ops: + order[op] = i + i += 1 + return sorted(values, key=lambda v: order[v]) + else: + return list(values) + + def spill_regs( blocks: list[BasicBlock], env: ClassIR, @@ -48,7 +65,9 @@ def spill_regs( env_reg = self_reg spill_locs = {} - for i, val in enumerate(to_spill): + # Sort values to make the order deterministic. All the spilled values are + # known to be Op instances, so the cast is safe. + for i, val in enumerate(sort_values(cast(set[Op], to_spill), blocks)): name = f"{TEMP_ATTR_NAME}2_{i}" env.attributes[name] = val.type if val.type.error_overlap: From 1b206eb2850e0cad975476ec4753e60860c3b96a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 22 Jun 2026 15:36:32 +0100 Subject: [PATCH 086/127] Support .ff files with --cache-map (#21633) Previously only the .json extension was accepted, but we are now generating .ff cache files by default. --- mypy/main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index 9227e24680ce6..0cf624a3a5d7b 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1677,13 +1677,15 @@ def process_cache_map( parser.error(f"Duplicate --cache-map source {source})") if not source.endswith(".py") and not source.endswith(".pyi"): parser.error(f"Invalid --cache-map source {source} (triple[0] must be *.py[i])") - if not meta_file.endswith(".meta.json"): + if not meta_file.endswith((".meta.json", ".meta.ff")): parser.error( - "Invalid --cache-map meta_file %s (triple[1] must be *.meta.json)" % meta_file + "Invalid --cache-map meta_file %s (triple[1] must be *.meta.json or *.meta.ff)" + % meta_file ) - if not data_file.endswith(".data.json"): + if not data_file.endswith((".data.json", ".data.ff")): parser.error( - "Invalid --cache-map data_file %s (triple[2] must be *.data.json)" % data_file + "Invalid --cache-map data_file %s (triple[2] must be *.data.json or *.data.ff)" + % data_file ) options.cache_map[source] = (meta_file, data_file) From 3179030edcee070153b29cf21e06a0966beb088c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 22 Jun 2026 19:06:50 +0100 Subject: [PATCH 087/127] [mypyc] Fix handling of invalid codepoint values in librt.strings (#21634) `isidentifier`, `tolower` and `toupper` would terminate the process if passed a too high Unicode codepoint value. Fix so that all the `is*` functions return false for invalid codepoints and `tolower`/`toupper` functions return them unchanged. I used coding agent assist. --- mypy/typeshed/stubs/librt/librt/strings.pyi | 8 ++++-- mypyc/build.py | 7 ++++- mypyc/lib-rt/strings/librt_strings.h | 31 ++++++++++++++------- mypyc/test-data/run-librt-strings.test | 18 +++++++++--- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 94e3b69abf243..af9ae60936d8e 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -42,7 +42,8 @@ def read_f64_le(b: bytes, index: i64, /) -> float: ... def read_f64_be(b: bytes, index: i64, /) -> float: ... # Codepoint classification helpers operating on i32 codepoints (typically -# obtained via ord(s[i])). Negative inputs return False. +# obtained via ord(s[i])). Out-of-range inputs (negative, or past the maximum +# Unicode code point 0x10FFFF) return False. def isspace(c: i32, /) -> bool: ... def isdigit(c: i32, /) -> bool: ... def isalnum(c: i32, /) -> bool: ... @@ -53,7 +54,8 @@ def isidentifier(c: i32, /) -> bool: ... # uppercase / lowercase expands to multiple codepoints (e.g. U+00DF # uppercases to "SS", U+FB01 to "FI"), returns the input unchanged so # the signature stays i32 -> i32. Use str.upper() / str.lower() for full -# Unicode case conversion when those cases matter. Negative inputs are -# returned unchanged. +# Unicode case conversion when those cases matter. Out-of-range inputs +# (negative, or past the maximum Unicode code point 0x10FFFF) are returned +# unchanged. def toupper(c: i32, /) -> i32: ... def tolower(c: i32, /) -> i32: ... diff --git a/mypyc/build.py b/mypyc/build.py index 13bd50fef3b1a..57438c7d5f52b 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -54,7 +54,12 @@ class ModDesc(NamedTuple): LIBRT_MODULES = [ ModDesc("librt.internal", ["internal/librt_internal.c"], [], ["internal"]), - ModDesc("librt.strings", ["strings/librt_strings.c"], [], ["strings"]), + ModDesc( + "librt.strings", + ["strings/librt_strings.c"], + ["strings/librt_strings.h", "strings/librt_strings_common.h"], + ["strings"], + ), ModDesc( "librt.base64", [ diff --git a/mypyc/lib-rt/strings/librt_strings.h b/mypyc/lib-rt/strings/librt_strings.h index 6c1942667ba44..33af57dc2a2a8 100644 --- a/mypyc/lib-rt/strings/librt_strings.h +++ b/mypyc/lib-rt/strings/librt_strings.h @@ -31,8 +31,9 @@ typedef struct { } StringWriterObject; // Codepoint classification helpers. Inputs are signed i32 for compatibility -// with mypyc's int32_rprimitive; negative values are non-codepoints and -// return false. Defined `static inline` so they compile statically into +// with mypyc's int32_rprimitive; out-of-range values (negative, or past the +// maximum Unicode code point 0x10FFFF) are non-codepoints and return false. +// Defined `static inline` so they compile statically into // both the librt.strings module and any mypyc-compiled extension that // includes this header, avoiding the capsule indirection that would dwarf // the work of a single Py_UNICODE_IS* macro call. @@ -58,12 +59,14 @@ static inline bool LibRTStrings_IsAlpha(int32_t c) { // PyUnicode_IsIdentifier on a 1-character string. Aborts via // CPyError_OutOfMemory on allocation failure to keep this ERR_NEVER. static inline bool LibRTStrings_IsIdentifier(int32_t c) { - if (c < 0) return false; - if (c < 128) { + // Unsigned compare: negatives wrap to large values and skip the fast path. + if ((uint32_t)c < 128) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } + // Reject negatives and code points past the Unicode maximum. + if ((uint32_t)c > 0x10FFFF) return false; PyObject *s = PyUnicode_FromOrdinal((int)c); if (s == NULL) { CPyError_OutOfMemory(); @@ -101,9 +104,13 @@ static inline int32_t LibRTStrings_ChangeCase_slow(int32_t c, const char *method // non-ASCII delegates to str.upper on a 1-character string. Returns the // input unchanged when uppercasing expands to multiple codepoints. static inline int32_t LibRTStrings_ToUpper(int32_t c) { - if (c < 0) return c; - if (c >= 'a' && c <= 'z') return c - 32; - if (c < 128) return c; + // Unsigned compare: negatives wrap to large values and skip the fast path. + if ((uint32_t)c < 128) { + if (c >= 'a' && c <= 'z') return c - 32; + return c; + } + // Negatives and code points past the Unicode maximum are returned unchanged. + if ((uint32_t)c > 0x10FFFF) return c; return LibRTStrings_ChangeCase_slow(c, "upper"); } @@ -111,9 +118,13 @@ static inline int32_t LibRTStrings_ToUpper(int32_t c) { // non-ASCII delegates to str.lower on a 1-character string. Returns the // input unchanged when lowercasing expands to multiple codepoints. static inline int32_t LibRTStrings_ToLower(int32_t c) { - if (c < 0) return c; - if (c >= 'A' && c <= 'Z') return c + 32; - if (c < 128) return c; + // Unsigned compare: negatives wrap to large values and skip the fast path. + if ((uint32_t)c < 128) { + if (c >= 'A' && c <= 'Z') return c + 32; + return c; + } + // Negatives and code points past the Unicode maximum are returned unchanged. + if ((uint32_t)c > 0x10FFFF) return c; return LibRTStrings_ChangeCase_slow(c, "lower"); } diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 7efff12667d87..333b5962c4c99 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1449,8 +1449,9 @@ from testutil import assertRaises def test_codepoint_classifiers() -> None: - # Negative values are not codepoints. - for bad in (i32(-1), i32(-113)): + # Out-of-range values are not codepoints: negative, just past the maximum + # valid code point (0x10FFFF), and the largest i32. + for bad in (i32(-1), i32(-113), i32(0x110000), i32(0x7FFFFFFF)): assert not isspace(bad) assert not isdigit(bad) assert not isalnum(bad) @@ -1485,6 +1486,10 @@ def test_codepoint_classifiers_via_any() -> None: assert f(ord(false_input)) is False # Negative values are valid i32, just not codepoints. assert f(-1) is False + # Values within i32 range but past the maximum code point (0x10FFFF) + # are not codepoints either. + assert f(0x110000) is False + assert f(0x7FFFFFFF) is False # Inputs outside i32 range raise OverflowError through the wrapper. with assertRaises(OverflowError, "codepoint out of i32 range"): f(1 << 40) @@ -1509,8 +1514,9 @@ def _expect(c: str, method: str) -> int: def test_codepoint_case_conversion() -> None: - # Negative inputs return unchanged. - for bad in (i32(-1), i32(-113)): + # Out-of-range inputs return unchanged: negative, just past the maximum + # valid code point (0x10FFFF), and the largest i32. + for bad in (i32(-1), i32(-113), i32(0x110000), i32(0x7FFFFFFF)): assert toupper(bad) == bad assert tolower(bad) == bad # Agree with str.upper / str.lower across the full Unicode range @@ -1534,6 +1540,10 @@ def test_codepoint_case_conversion_via_any() -> None: assert f(in_cp) == out_cp # Negative values are valid i32, returned unchanged. assert f(-1) == -1 + # Values within i32 range but past the maximum code point (0x10FFFF) + # are returned unchanged. + assert f(0x110000) == 0x110000 + assert f(0x7FFFFFFF) == 0x7FFFFFFF # Inputs outside i32 range raise OverflowError through the wrapper. with assertRaises(OverflowError, "codepoint out of i32 range"): f(1 << 40) From 24335663f560c3e2b1311464f0edd80e88ea5ff8 Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Wed, 24 Jun 2026 17:04:30 +0200 Subject: [PATCH 088/127] Fix skipped imports considered stale (#21639) Fixes https://github.com/python/mypy/issues/21102 When the option `follow_imports = skip` is used, dependency states that are initially considered skipped might have their reason changed to not found in `load_graph`. If on the first mypy run, a given suppressed module is written to cache with its suppression reason set to skipped, and it's overridden to not found in a subsequent mypy run, then `suppressed_deps_opts` between the cache and the current run won't match and the module will be considered stale. To fix the issue, change the unconditional overwrite to `setdefault`. This also affects mypyc as the dependency being considered stale means that mypyc will needlessly recompile it instead of reading from cache. Add a unit test to confirm that the dependency output files are not overwritten with the fix. --- mypy/build.py | 2 +- mypyc/build.py | 54 ++++++++++++++------------- mypyc/codegen/emitmodule.py | 19 ++++++---- mypyc/test-data/run-multimodule.test | 33 ++++++++++++++++ mypyc/test/test_run.py | 4 ++ test-data/unit/check-incremental.test | 16 ++++++++ 6 files changed, 93 insertions(+), 35 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 7fc9526ccb2fe..a03a6eb8972cc 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -4345,7 +4345,7 @@ def load_graph( for dep in st.ancestors + dependencies + st.suppressed: ignored = dep in st.suppressed_set and dep not in entry_points if ignored and dep not in added: - manager.missing_modules[dep] = SuppressionReason.NOT_FOUND + manager.missing_modules.setdefault(dep, SuppressionReason.NOT_FOUND) # TODO: for now we skip this in the daemon as a performance optimization. # This however creates a correctness issue, see #7777 and State.is_fresh(). if not manager.use_fine_grained_cache() or manager.options.warn_unused_configs: diff --git a/mypyc/build.py b/mypyc/build.py index 57438c7d5f52b..b46365d263e6a 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -328,34 +328,36 @@ def generate_c( emit_messages(options, e.messages, time.time() - t0, serious=(not e.use_stdout)) sys.exit(1) - t1 = time.time() - if result.errors: - emit_messages(options, result.errors, t1 - t0) - sys.exit(1) - - if compiler_options.verbose: - print(f"Parsed and typechecked in {t1 - t0:.3f}s") - - errors = Errors(options) - modules, ctext, mapper = emitmodule.compile_modules_to_c( - result, compiler_options=compiler_options, errors=errors, groups=groups - ) - t2 = time.time() - emit_messages(options, errors.new_messages(), t2 - t1) - if errors.num_errors: - # No need to stop the build if only warnings were emitted. - sys.exit(1) - - if compiler_options.verbose: - print(f"Compiled to C in {t2 - t1:.3f}s") - - if options.mypyc_annotation_file: - generate_annotated_html(options.mypyc_annotation_file, result, modules, mapper) + try: + t1 = time.time() + if result.errors: + emit_messages(options, result.errors, t1 - t0) + sys.exit(1) - # Collect SourceDep dependencies - source_deps = sorted(emitmodule.collect_source_dependencies(modules), key=lambda d: d.path) + if compiler_options.verbose: + print(f"Parsed and typechecked in {t1 - t0:.3f}s") - return ctext, "\n".join(format_modules(modules)), source_deps + errors = Errors(options) + modules, ctext, mapper = emitmodule.compile_modules_to_c( + result, compiler_options=compiler_options, errors=errors, groups=groups + ) + t2 = time.time() + emit_messages(options, errors.new_messages(), t2 - t1) + if errors.num_errors: + # No need to stop the build if only warnings were emitted. + sys.exit(1) + + if compiler_options.verbose: + print(f"Compiled to C in {t2 - t1:.3f}s") + + if options.mypyc_annotation_file: + generate_annotated_html(options.mypyc_annotation_file, result, modules, mapper) + + # Collect SourceDep dependencies + source_deps = sorted(emitmodule.collect_source_dependencies(modules), key=lambda d: d.path) + return ctext, "\n".join(format_modules(modules)), source_deps + finally: + result.manager.metastore.close() def build_using_shared_lib( diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index e2cd0a829dd77..6e3c122aa13f6 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -210,15 +210,18 @@ def parse_and_typecheck( ) -> BuildResult: assert options.strict_optional, "strict_optional must be turned on" mypyc_plugin = MypycPlugin(options, compiler_options, groups) - result = build( - sources=sources, - options=options, - alt_lib_path=alt_lib_path, - fscache=fscache, - extra_plugins=[mypyc_plugin], - ) - mypyc_plugin.metastore.close() + try: + result = build( + sources=sources, + options=options, + alt_lib_path=alt_lib_path, + fscache=fscache, + extra_plugins=[mypyc_plugin], + ) + finally: + mypyc_plugin.metastore.close() if result.errors: + result.manager.metastore.close() raise CompileError(result.errors) return result diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index e586a3cba22e9..4cf391d312dff 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -1246,6 +1246,39 @@ import native [rechecked other_a] +-- Test that importing a skipped module does not force rechecks. +[case testIncrementalCompilationFollowImportsSkip] +from other_dep import f +assert f() == 1 + +[file other_dep.py] +import skipped + +def f() -> int: + return 1 + +def g() -> int: + return 2 + +# Files under "skipped" are not typechecked because of the "--follow_imports = skip" option. +[file skipped/__init__.py] +x = 1 + +[file skipped/other_child.py] +import skipped + +def child() -> int: + return 1 + +[file native.py.2] +from other_dep import g +assert g() == 2 + +[file driver.py] +import native + +[rechecked native] + [case testSeparateCompilationWithUndefinedAttribute] from other_a import A diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index e7be5fcf8425a..9004a28ebf598 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -234,6 +234,10 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) -> # Avoid checking modules/packages named 'unchecked', to provide a way # to test interacting with code we don't have types for. options.per_module_options["unchecked.*"] = {"follow_imports": "error"} + # Avoid checking modules/packages named 'skipped', to provide a way + # to test interacting with code ignored by follow_imports=skip. + options.per_module_options["skipped"] = {"follow_imports": "skip"} + options.per_module_options["skipped.*"] = {"follow_imports": "skip"} source = build.BuildSource("native.py", "native", None) sources = [source] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 7ff9fbef06a53..18931dd9f152f 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -8204,3 +8204,19 @@ reveal_type(x) tmp/a.py:2: note: Revealed type is "builtins.int" [out2] tmp/a.py:2: note: Revealed type is "builtins.str" + +[case testIncrementalFollowImportsSkipStubWithSkippedRuntimeDependency] +# flags: --follow-imports=skip +import pkg.foo +[file pkg/__init__.py] + +[file pkg/foo.pyi] +from pkg import helper + +x = 1 +[file pkg/helper.py] +y = 2 +[rechecked] +[stale] +[out2] +[out3] From 5374fec1e0e676db587bebc36a1192b9819bc559 Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Mon, 29 Jun 2026 17:31:02 +0200 Subject: [PATCH 089/127] [mypyc] Fix function wrapper memory leak (#21654) The `name` and `code` variables passed to the function wrapper init function are increfed inside it before storing them in the function wrapper object but they are not decrefed after the init function returns which creates a leak. Change the init function to instead steal `name` and `code` and document it. --- mypyc/lib-rt/function_wrapper.c | 21 +++++++++++++------- mypyc/test-data/run-async.test | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/mypyc/lib-rt/function_wrapper.c b/mypyc/lib-rt/function_wrapper.c index 16b1b59303481..dfea6aed1d1c3 100644 --- a/mypyc/lib-rt/function_wrapper.c +++ b/mypyc/lib-rt/function_wrapper.c @@ -196,6 +196,7 @@ static PyObject* CPyFunction_Vectorcall(PyObject *func, PyObject *const *args, s } +// Steals ml, name, and code. Borrows module. static CPyFunction* CPyFunction_Init(CPyFunction *op, PyMethodDef *ml, PyObject* name, PyObject *module, PyObject* code, bool set_self) { PyCFunctionObject *cf = (PyCFunctionObject *)op; @@ -206,12 +207,10 @@ static CPyFunction* CPyFunction_Init(CPyFunction *op, PyMethodDef *ml, PyObject* Py_XINCREF(module); cf->m_module = module; - Py_INCREF(name); op->func_name = name; ((PyCMethodObject *)op)->mm_class = NULL; - Py_XINCREF(code); op->func_code = code; CPyFunction_func_vectorcall(op) = CPyFunction_Vectorcall; @@ -243,7 +242,7 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu PyCFunction func, int func_flags, const char *func_doc, int first_line, int code_flags, bool has_self_arg) { PyMethodDef *method = NULL; - PyObject *code = NULL, *op = NULL; + PyObject *code = NULL, *name = NULL, *op = NULL; bool set_self = false; #ifdef Py_GIL_DISABLED @@ -280,18 +279,24 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu if (unlikely(!code)) { goto err; } + name = PyUnicode_FromString(funcname); + if (unlikely(!name)) { + goto err; + } // Set m_self inside the function wrapper only if the wrapped function has no self arg // to pass m_self as the self arg when the function is called. // When the function has a self arg, it will come in the args vector passed to the // vectorcall handler. set_self = !has_self_arg; - op = (PyObject *)CPyFunction_Init(PyObject_GC_New(CPyFunction, CPyFunctionType), - method, PyUnicode_FromString(funcname), module, - code, set_self); - if (unlikely(!op)) { + CPyFunction *raw = PyObject_GC_New(CPyFunction, CPyFunctionType); + if (unlikely(!raw)) { goto err; } + op = (PyObject *)CPyFunction_Init(raw, method, name, module, code, set_self); + method = NULL; + name = NULL; + code = NULL; PyObject_GC_Track(op); return op; @@ -300,5 +305,7 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu if (method) { PyMem_Free(method); } + Py_XDECREF(name); + Py_XDECREF(code); return NULL; } diff --git a/mypyc/test-data/run-async.test b/mypyc/test-data/run-async.test index 2733a31f3af2a..cf9b54368c277 100644 --- a/mypyc/test-data/run-async.test +++ b/mypyc/test-data/run-async.test @@ -1416,6 +1416,7 @@ def run(x: object) -> object: ... [case testAsyncIntrospection] import asyncio +import gc import inspect import sys import weakref @@ -1423,6 +1424,8 @@ import weakref from functools import wraps from typing import Any, Callable, TypeVar, cast +from testutil import is_gil_disabled + def identity(val: int) -> int: return val @@ -1589,6 +1592,37 @@ def test_nested() -> None: assert is_coroutine(nested_wrapped_async) assert asyncio.run(nested_wrapped_async()) == 4 +def test_async_function_wrapper_code_refcount() -> None: + if is_gil_disabled(): + # On free-threaded builds the code object might be immortal, so the ref count test doesn't work. + return + code = getattr(identity_async, "__code__") + getrefcount = getattr(sys, "getrefcount") + # getrefcount sees the local code variable plus the wrapper-owned reference. + assert getrefcount(code) == 2, getrefcount(code) + +def test_nested_async_function_wrapper_code_refcount() -> None: + if is_gil_disabled(): + # On free-threaded builds the code object might be immortal, so the ref count test doesn't work. + return + def make_nested() -> Any: + async def nested_refcounted() -> int: + return 1 + + return nested_refcounted + + getrefcount = getattr(sys, "getrefcount") + fn = make_nested() + code = getattr(fn, "__code__") + before = getrefcount(code) + assert asyncio.run(fn()) == 1 + + del fn + gc.collect() + after = getrefcount(code) + assert before == after + 1, (before, after) + assert after == 1, after + [file asyncio/__init__.pyi] def run(x: object) -> object: ... From 629f456429d39abdf4754f517503200a7a4f8b17 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 29 Jun 2026 17:21:20 +0100 Subject: [PATCH 090/127] [mypyc] Support generic primitives and add some generic primitives for vec (#21656) Generic primitives have `RTypeVar` types in parameter and/or return types, and these get expanded away when the primitive is added to IR. Generic primitives let us use lowering for various `vec` operations, many of which are generic. Using higher-level operations in the IR helps with various optimizations. It's also easier to verify that the generated IR is correct when the IR is less verbose. Add generic primitives for unsafe `vec` get item op as an initial use case. We can later use these for other `vec` operations as well. Used some coding agent assist (mostly for tests). --- mypyc/ir/ops.py | 21 ++- mypyc/ir/pprint.py | 9 +- mypyc/ir/rtypes.py | 53 ++++++++ mypyc/irbuild/ll_builder.py | 27 +++- mypyc/irbuild/vec.py | 23 +++- mypyc/lower/registry.py | 2 +- mypyc/lower/vec_ops.py | 18 +++ mypyc/primitives/librt_vecs_ops.py | 23 +++- mypyc/primitives/registry.py | 8 +- mypyc/rt_expandtype.py | 32 +++++ mypyc/test-data/irbuild-vec-i64.test | 173 ++++++++---------------- mypyc/test-data/irbuild-vec-misc.test | 51 ++----- mypyc/test-data/irbuild-vec-nested.test | 160 +++++++++++++--------- mypyc/test-data/irbuild-vec-t.test | 75 +++++++--- mypyc/test-data/lowering-vec.test | 155 +++++++++++++++++++++ mypyc/test-data/refcount.test | 110 +++++---------- mypyc/test/test_expand_rtype.py | 48 +++++++ mypyc/test/test_lowering.py | 2 +- 18 files changed, 665 insertions(+), 325 deletions(-) create mode 100644 mypyc/lower/vec_ops.py create mode 100644 mypyc/rt_expandtype.py create mode 100644 mypyc/test-data/lowering-vec.test create mode 100644 mypyc/test/test_expand_rtype.py diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 485aa84886b32..4bc7671b82082 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -39,6 +39,7 @@ class to enable the new behavior. Sometimes adding a new abstract RStruct, RTuple, RType, + RTypeVar, RUnion, RVec, RVoid, @@ -703,7 +704,7 @@ def __init__( self, name: str, arg_types: list[RType], - return_type: RType, # TODO: What about generic? + return_type: RType, var_arg_type: RType | None, truncated_type: RType | None, c_function_name: str | None, @@ -716,6 +717,7 @@ def __init__( is_pure: bool, experimental: bool, dependencies: list[Dependency] | None, + type_params: list[RTypeVar] | None, ) -> None: # Each primitive much have a distinct name, but otherwise they are arbitrary. self.name: Final = name @@ -749,6 +751,7 @@ def __init__( # If this flag is set, the primitive has native integer types and must # be matched using more complex rules. self.is_ambiguous = any(has_fixed_width_int(t) for t in arg_types) + self.type_params = None if not type_params else type_params def __repr__(self) -> str: return f"" @@ -776,11 +779,23 @@ class PrimitiveOp(RegisterOp): code paths for short and long representations. """ - def __init__(self, args: list[Value], desc: PrimitiveDescription, line: int = -1) -> None: + def __init__( + self, + args: list[Value], + desc: PrimitiveDescription, + line: int = -1, + *, + arg_types: list[RType] | None = None, + return_type: RType | None = None, + type_args: list[RType] | None = None, + ) -> None: self.error_kind = desc.error_kind super().__init__(line) self.args = args - self.type = desc.return_type + self.arg_types = arg_types if arg_types is not None else desc.arg_types + self.type = return_type if return_type is not None else desc.return_type + self.is_borrowed = desc.is_borrowed + self.type_args = type_args self.desc = desc def sources(self) -> list[Value]: diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py index d0db9f2460a1d..734426ca42de9 100644 --- a/mypyc/ir/pprint.py +++ b/mypyc/ir/pprint.py @@ -231,10 +231,15 @@ def visit_call_c(self, op: CallC) -> str: def visit_primitive_op(self, op: PrimitiveOp) -> str: args_str = ", ".join(self.format("%r", arg) for arg in op.args) + if op.type_args: + joined = ", ".join(str(arg) for arg in op.type_args) + type_args = f"[{joined}]" + else: + type_args = "" if op.is_void: - return self.format("%s %s", op.desc.name, args_str) + return self.format("%s%s %s", op.desc.name, type_args, args_str) else: - return self.format("%r = %s %s", op, op.desc.name, args_str) + return self.format("%r = %s%s %s", op, op.desc.name, type_args, args_str) def visit_truncate(self, op: Truncate) -> str: return self.format("%r = truncate %r: %t to %t", op, op.src, op.src_type, op.type) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index db29f9e304d8d..9d13ecc83175d 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -152,6 +152,9 @@ def visit_rprimitive(self, typ: RPrimitive, /) -> T: def visit_rinstance(self, typ: RInstance, /) -> T: raise NotImplementedError + def visit_rtypevar(self, typ: RTypeVar, /) -> T: + raise RuntimeError("RTypeVar should not be encountered here") + @abstractmethod def visit_rvec(self, typ: RVec, /) -> T: raise NotImplementedError @@ -747,6 +750,12 @@ def visit_rarray(self, t: RArray) -> str: def visit_rvoid(self, t: RVoid) -> str: assert False, "rvoid in tuple?" + def visit_rtypevar(self, typ: RTypeVar) -> str: + # We need to return something to support generic RTuples, etc. Make sure + # the return value is invalid C so that generic RTuples must be expanded + # before they can be used in IR. + return f"!RTypeVar {typ.id} invalid!" + @final class RTuple(RType): @@ -1013,6 +1022,50 @@ def serialize(self) -> str: return self.name +@final +class RTypeVar(RType): + """Type variable type used for generic primitive ops. + + This allows having generic primitive operations like vec get item, which is + parametrized by the vec item type. + + These types are not valid in any other context outside PrimitiveDescription, + and they will always be substituted during the construction of a PrimitiveOp. + + NOTE: This is not related to mypy's TypeVarType! + """ + + def __init__(self, id: int) -> None: + self.id = id + + @property + def may_be_immortal(self) -> bool: + # RTypeVar must always be substituted before use, so this should never matter. + return False + + def accept(self, visitor: RTypeVisitor[T]) -> T: + return visitor.visit_rtypevar(self) + + def __str__(self) -> str: + return f"" + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> TypeGuard[RTypeVar]: + return isinstance(other, RTypeVar) and other.id == self.id + + def __hash__(self) -> int: + return self.id ^ 12345 + + def serialize(self) -> JsonDict: + return {".class": "RTypeVar", "id": self.id} + + @classmethod + def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> RTypeVar: + return RTypeVar(data["id"]) + + @final class RVec(RType): """librt.vecs.vec[T]""" diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index c19eded77464e..c0ad1cf1f8264 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -206,6 +206,7 @@ new_tuple_with_length_op, sequence_tuple_op, ) +from mypyc.rt_expandtype import expand_rtype from mypyc.rt_subtype import is_runtime_subtype from mypyc.sametype import is_same_type from mypyc.subtype import is_subtype @@ -2334,6 +2335,7 @@ def primitive_op( args: list[Value], line: int, result_type: RType | None = None, + type_args: list[RType] | None = None, ) -> Value: """Add a primitive op.""" # Does this primitive map into calling a Python C API @@ -2363,10 +2365,20 @@ def primitive_op( # This primitive gets transformed in a lowering pass to # lower-level IR ops using a custom transform function. + # Evaluate argument and return types for generic primitives + return_type = None + if desc.type_params is not None: + assert type_args is not None, "Generic primitive op requires explicit type arguments" + assert len(type_args) == len(desc.type_params) + arg_types = [expand_rtype(arg_type, type_args) for arg_type in desc.arg_types] + return_type = expand_rtype(desc.return_type, type_args) + else: + arg_types = desc.arg_types + coerced = [] # Coerce fixed number arguments - for i in range(min(len(args), len(desc.arg_types))): - formal_type = desc.arg_types[i] + for i in range(min(len(args), len(arg_types))): + formal_type = arg_types[i] arg = args[i] assert formal_type is not None # TODO arg = self.coerce(arg, formal_type, line) @@ -2374,7 +2386,16 @@ def primitive_op( assert desc.ordering is None assert desc.var_arg_type is None assert not desc.extra_int_constants - target = self.add(PrimitiveOp(coerced, desc, line=line)) + target = self.add( + PrimitiveOp( + coerced, + desc, + line=line, + arg_types=arg_types, + return_type=return_type, + type_args=type_args, + ) + ) if desc.is_borrowed: # If the result is borrowed, force the arguments to be # kept alive afterwards, as otherwise the result might be diff --git a/mypyc/irbuild/vec.py b/mypyc/irbuild/vec.py index bfcfabee45c21..38615ebe16026 100644 --- a/mypyc/irbuild/vec.py +++ b/mypyc/irbuild/vec.py @@ -52,6 +52,7 @@ vec_api_by_item_type, vec_item_type_tags, ) +from mypyc.primitives.librt_vecs_ops import vec_get_item_unsafe_borrow_op, vec_get_item_unsafe_op if TYPE_CHECKING: from mypyc.irbuild.ll_builder import LowLevelIRBuilder @@ -316,9 +317,10 @@ def vec_get_item( ) -> Value: """Generate inlined vec __getitem__ call. - We inline this, since it's simple but performance-critical. + We inline the length and bounds check, since they are simple but + performance-critical. The actual item load is emitted as a generic primitive + op that is lowered later. """ - # TODO: Support more item types # TODO: Support more index types len_val = vec_len(builder, base) index = vec_check_and_adjust_index(builder, len_val, index, line) @@ -328,7 +330,22 @@ def vec_get_item( def vec_get_item_unsafe( builder: LowLevelIRBuilder, base: Value, index: Value, line: int, *, can_borrow: bool = False ) -> Value: - """Get vec item, assuming index is non-negative and within bounds.""" + """Get vec item, assuming index is non-negative and within bounds. + + This emits a generic primitive op that is inlined during lowering. + """ + assert isinstance(base.type, RVec) + if can_borrow: + desc = vec_get_item_unsafe_borrow_op + else: + desc = vec_get_item_unsafe_op + return builder.primitive_op(desc, [base, index], line, type_args=[base.type.item_type]) + + +def vec_get_item_unsafe_lower( + builder: LowLevelIRBuilder, base: Value, index: Value, line: int, *, can_borrow: bool = False +) -> Value: + """Generate the low-level IR for an unsafe vec item load.""" assert isinstance(base.type, RVec) index = as_platform_int(builder, index, line) vtype = base.type diff --git a/mypyc/lower/registry.py b/mypyc/lower/registry.py index dec6a24b9417a..36262c4a011a3 100644 --- a/mypyc/lower/registry.py +++ b/mypyc/lower/registry.py @@ -26,4 +26,4 @@ def wrapper(f: LF) -> LF: # Import various modules that set up global state. -from mypyc.lower import int_ops, list_ops, misc_ops # noqa: F401 +from mypyc.lower import int_ops, list_ops, misc_ops, vec_ops # noqa: F401 diff --git a/mypyc/lower/vec_ops.py b/mypyc/lower/vec_ops.py new file mode 100644 index 0000000000000..768c8e0073af8 --- /dev/null +++ b/mypyc/lower/vec_ops.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from mypyc.ir.ops import Value +from mypyc.irbuild.ll_builder import LowLevelIRBuilder +from mypyc.irbuild.vec import vec_get_item_unsafe_lower +from mypyc.lower.registry import lower_primitive_op + + +@lower_primitive_op("vec_get_item_unsafe") +def vec_get_item_unsafe(builder: LowLevelIRBuilder, args: list[Value], line: int) -> Value: + base, index = args + return vec_get_item_unsafe_lower(builder, base, index, line, can_borrow=False) + + +@lower_primitive_op("vec_get_item_unsafe_borrow") +def vec_get_item_unsafe_borrow(builder: LowLevelIRBuilder, args: list[Value], line: int) -> Value: + base, index = args + return vec_get_item_unsafe_lower(builder, base, index, line, can_borrow=True) diff --git a/mypyc/primitives/librt_vecs_ops.py b/mypyc/primitives/librt_vecs_ops.py index e4852d5387069..901c779a2bfd2 100644 --- a/mypyc/primitives/librt_vecs_ops.py +++ b/mypyc/primitives/librt_vecs_ops.py @@ -1,13 +1,15 @@ from mypyc.ir.deps import LIBRT_VECS, VECS_EXTRA_OPS from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER from mypyc.ir.rtypes import ( + RTypeVar, RVec, bit_rprimitive, bytes_rprimitive, + int64_rprimitive, object_rprimitive, uint8_rprimitive, ) -from mypyc.primitives.registry import function_op +from mypyc.primitives.registry import custom_primitive_op, function_op # isinstance(obj, vec) isinstance_vec = function_op( @@ -28,3 +30,22 @@ error_kind=ERR_MAGIC, dependencies=[LIBRT_VECS, VECS_EXTRA_OPS], ) + +# Get vec item, assuming the index is valid (no bounds check) +vec_get_item_unsafe_op = custom_primitive_op( + name="vec_get_item_unsafe", + arg_types=[RVec(RTypeVar(0)), int64_rprimitive], + return_type=RTypeVar(0), + error_kind=ERR_NEVER, + type_params=[RTypeVar(0)], +) + +# Like vec_get_item_unsafe, but the result is a borrowed reference +vec_get_item_unsafe_borrow_op = custom_primitive_op( + name="vec_get_item_unsafe_borrow", + arg_types=[RVec(RTypeVar(0)), int64_rprimitive], + is_borrowed=True, + return_type=RTypeVar(0), + error_kind=ERR_NEVER, + type_params=[RTypeVar(0)], +) diff --git a/mypyc/primitives/registry.py b/mypyc/primitives/registry.py index e22a044d9bb27..22422987b4277 100644 --- a/mypyc/primitives/registry.py +++ b/mypyc/primitives/registry.py @@ -41,7 +41,7 @@ from mypyc.ir.deps import Dependency from mypyc.ir.ops import PrimitiveDescription, StealsDescription -from mypyc.ir.rtypes import RType +from mypyc.ir.rtypes import RType, RTypeVar # Error kind for functions that return negative integer on exception. This # is only used for primitives. We translate it away during IR building. @@ -154,6 +154,7 @@ def method_op( is_pure=is_pure, experimental=experimental, dependencies=dependencies, + type_params=None, ) ops.append(desc) return desc @@ -204,6 +205,7 @@ def function_op( is_pure=False, experimental=experimental, dependencies=dependencies, + type_params=None, ) ops.append(desc) return desc @@ -253,6 +255,7 @@ def binary_op( is_pure=False, experimental=False, dependencies=dependencies, + type_params=None, ) ops.append(desc) return desc @@ -313,6 +316,7 @@ def custom_primitive_op( is_pure: bool = False, experimental: bool = False, dependencies: list[Dependency] | None = None, + type_params: list[RTypeVar] | None = None, ) -> PrimitiveDescription: """Define a primitive op that can't be automatically generated based on the AST. @@ -336,6 +340,7 @@ def custom_primitive_op( is_pure=is_pure, experimental=experimental, dependencies=dependencies, + type_params=type_params, ) @@ -380,6 +385,7 @@ def unary_op( is_pure=is_pure, experimental=False, dependencies=dependencies, + type_params=None, ) ops.append(desc) return desc diff --git a/mypyc/rt_expandtype.py b/mypyc/rt_expandtype.py new file mode 100644 index 0000000000000..8537e6777ccb5 --- /dev/null +++ b/mypyc/rt_expandtype.py @@ -0,0 +1,32 @@ +from mypyc.ir.rtypes import ( + RArray, + RInstance, + RPrimitive, + RStruct, + RTuple, + RType, + RTypeVar, + RUnion, + RVec, + RVoid, +) + + +def expand_rtype(typ: RType, type_args: list[RType]) -> RType: + if isinstance(typ, (RPrimitive, RInstance, RVoid)): + # Atomic types can't contain type variables + return typ + elif isinstance(typ, RTypeVar): + return type_args[typ.id] + elif isinstance(typ, RVec): + return RVec(expand_rtype(typ.item_type, type_args)) + elif isinstance(typ, RUnion): + return RUnion([expand_rtype(item, type_args) for item in typ.items]) + elif isinstance(typ, RTuple): + return RTuple([expand_rtype(item, type_args) for item in typ.types]) + elif isinstance(typ, RStruct): + assert False, "Generic RStruct type not supported" + elif isinstance(typ, RArray): + assert False, "Generic RArray type not supported" + else: + assert False, r"Unexpected type {typ!r}" diff --git a/mypyc/test-data/irbuild-vec-i64.test b/mypyc/test-data/irbuild-vec-i64.test index aeab3ed9f2a8b..af69f30924d46 100644 --- a/mypyc/test-data/irbuild-vec-i64.test +++ b/mypyc/test-data/irbuild-vec-i64.test @@ -64,11 +64,7 @@ def f(v, i): r2 :: i64 r3 :: bit r4 :: bool - r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: i64 + r5, r6 :: i64 L0: r0 = v.len r1 = i < r0 :: unsigned @@ -86,15 +82,10 @@ L3: L4: r5 = i L5: - r6 = v.items - r7 = r5 * 8 - r8 = r6 + r7 - r9 = load_mem r8 :: i64* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[i64] v, r5 + return r6 [case testVecI64GetItem_32bit] -# The IR is quite verbose, but it's acceptable since 32-bit targets are not common any more from librt.vecs import vec from mypy_extensions import i64 @@ -110,13 +101,7 @@ def f(v, i): r3 :: i64 r4 :: bit r5 :: bool - r6 :: i64 - r7, r8 :: bit - r9 :: native_int - r10 :: ptr - r11 :: native_int - r12 :: ptr - r13 :: i64 + r6, r7 :: i64 L0: r0 = v.len r1 = extend signed r0: native_int to i64 @@ -135,24 +120,8 @@ L3: L4: r6 = i L5: - r7 = r6 < 2147483648 :: signed - if r7 goto L6 else goto L8 :: bool -L6: - r8 = r6 >= -2147483648 :: signed - if r8 goto L7 else goto L8 :: bool -L7: - r9 = truncate r6: i64 to native_int - goto L9 -L8: - CPyInt32_Overflow() - unreachable -L9: - r10 = v.items - r11 = r9 * 8 - r12 = r10 + r11 - r13 = load_mem r12 :: i64* - keep_alive v - return r13 + r7 = vec_get_item_unsafe[i64] v, r6 + return r7 [case testVecI64Append] from librt.vecs import vec, append @@ -411,7 +380,7 @@ L3: L4: return r1 -[case testVecI64FastComprehensionFromVec] +[case testVecI64FastComprehensionFromVec_64bit] from librt.vecs import vec from mypy_extensions import i64 from typing import List @@ -426,14 +395,11 @@ def f(n, v): r1 :: vec[i64] r2, r3 :: native_int r4 :: bit - r5 :: ptr - r6 :: native_int + r5, x, r6 :: i64 r7 :: ptr - r8, x, r9 :: i64 - r10 :: ptr - r11 :: native_int - r12 :: ptr - r13 :: native_int + r8 :: native_int + r9 :: ptr + r10 :: native_int L0: r0 = v.len r1 = VecI64Api.alloc(r0, r0) @@ -443,21 +409,17 @@ L1: r4 = r2 < r3 :: signed if r4 goto L2 else goto L4 :: bool L2: - r5 = v.items - r6 = r2 * 8 - r7 = r5 + r6 - r8 = load_mem r7 :: i64* - x = r8 - keep_alive v - r9 = x + 1 - r10 = r1.items - r11 = r2 * 8 - r12 = r10 + r11 - set_mem r12, r9 :: i64* + r5 = vec_get_item_unsafe[i64] v, r2 + x = r5 + r6 = x + 1 + r7 = r1.items + r8 = r2 * 8 + r9 = r7 + r8 + set_mem r9, r6 :: i64* keep_alive r1 L3: - r13 = r2 + 1 - r2 = r13 + r10 = r2 + 1 + r2 = r10 goto L1 L4: return r1 @@ -499,7 +461,7 @@ L3: L4: return r1 -[case testVecI64ForLoop] +[case testVecI64ForLoop_64bit] from librt.vecs import vec from mypy_extensions import i64 @@ -514,11 +476,8 @@ def f(v): t :: i64 r0, r1 :: native_int r2 :: bit - r3 :: ptr - r4 :: native_int - r5 :: ptr - r6, x, r7 :: i64 - r8 :: native_int + r3, x, r4 :: i64 + r5 :: native_int L0: t = 0 r0 = 0 @@ -527,17 +486,13 @@ L1: r2 = r0 < r1 :: signed if r2 goto L2 else goto L4 :: bool L2: - r3 = v.items - r4 = r0 * 8 - r5 = r3 + r4 - r6 = load_mem r5 :: i64* - x = r6 - keep_alive v - r7 = t + 1 - t = r7 + r3 = vec_get_item_unsafe[i64] v, r0 + x = r3 + r4 = t + 1 + t = r4 L3: - r8 = r0 + 1 - r0 = r8 + r5 = r0 + 1 + r0 = r5 goto L1 L4: return t @@ -601,11 +556,7 @@ def f(v): r2 :: i64 r3 :: bit r4 :: bool - r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: i64 + r5, r6 :: i64 L0: r0 = v.len r1 = 0 < r0 :: unsigned @@ -623,12 +574,8 @@ L3: L4: r5 = 0 L5: - r6 = v.items - r7 = r5 * 8 - r8 = r6 + r7 - r9 = load_mem r8 :: i64* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[i64] v, r5 + return r6 [case testVecI64Slicing_64bit] from librt.vecs import vec @@ -720,20 +667,16 @@ def inplace(v, n, m): r2 :: i64 r3 :: bit r4 :: bool - r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9, r10 :: i64 - r11 :: native_int - r12 :: bit + r5, r6, r7 :: i64 + r8 :: native_int + r9 :: bit + r10 :: i64 + r11 :: bit + r12 :: bool r13 :: i64 - r14 :: bit - r15 :: bool - r16 :: i64 - r17 :: ptr - r18 :: i64 - r19 :: ptr + r14 :: ptr + r15 :: i64 + r16 :: ptr L0: r0 = v.len r1 = n < r0 :: unsigned @@ -751,32 +694,28 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 8 - r8 = r6 + r7 - r9 = load_mem r8 :: i64* - keep_alive v - r10 = r9 + m - r11 = v.len - r12 = n < r11 :: unsigned - if r12 goto L9 else goto L6 :: bool + r6 = vec_get_item_unsafe[i64] v, r5 + r7 = r6 + m + r8 = v.len + r9 = n < r8 :: unsigned + if r9 goto L9 else goto L6 :: bool L6: - r13 = n + r11 - r14 = r13 < r11 :: unsigned - if r14 goto L8 else goto L7 :: bool + r10 = n + r8 + r11 = r10 < r8 :: unsigned + if r11 goto L8 else goto L7 :: bool L7: - r15 = raise IndexError + r12 = raise IndexError unreachable L8: - r16 = r13 + r13 = r10 goto L10 L9: - r16 = n + r13 = n L10: - r17 = v.items - r18 = r16 * 8 - r19 = r17 + r18 - set_mem r19, r10 :: i64* + r14 = v.items + r15 = r13 * 8 + r16 = r14 + r15 + set_mem r16, r7 :: i64* keep_alive v return 1 diff --git a/mypyc/test-data/irbuild-vec-misc.test b/mypyc/test-data/irbuild-vec-misc.test index c0d4325e38fcc..22037d8597a73 100644 --- a/mypyc/test-data/irbuild-vec-misc.test +++ b/mypyc/test-data/irbuild-vec-misc.test @@ -125,10 +125,7 @@ def get_item_bool(v, i): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: bool + r6 :: bool L0: r0 = v.len r1 = i < r0 :: unsigned @@ -146,12 +143,8 @@ L3: L4: r5 = i L5: - r6 = v.items - r7 = r5 * 1 - r8 = r6 + r7 - r9 = load_mem r8 :: builtins.bool* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[bool] v, r5 + return r6 [case testVecMiscPop] from librt.vecs import vec, pop @@ -209,7 +202,7 @@ L0: r0 = VecFloatApi.slice(v, x, y) return r0 -[case testVecMiscForLoop] +[case testVecMiscForLoop_64bit] from librt.vecs import vec, remove from mypy_extensions import i64, i16 @@ -225,11 +218,8 @@ def for_bool(v): s :: i16 r0, r1 :: native_int r2 :: bit - r3 :: ptr - r4 :: native_int - r5 :: ptr - r6, x, r7 :: i16 - r8 :: native_int + r3, x, r4 :: i16 + r5 :: native_int L0: s = 0 r0 = 0 @@ -238,17 +228,13 @@ L1: r2 = r0 < r1 :: signed if r2 goto L2 else goto L4 :: bool L2: - r3 = v.items - r4 = r0 * 2 - r5 = r3 + r4 - r6 = load_mem r5 :: i16* - x = r6 - keep_alive v - r7 = s + x - s = r7 + r3 = vec_get_item_unsafe[i16] v, r0 + x = r3 + r4 = s + x + s = r4 L3: - r8 = r0 + 1 - r0 = r8 + r5 = r0 + 1 + r0 = r5 goto L1 L4: return s @@ -270,10 +256,7 @@ def get_item_nested(v, i): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: vec[i32] + r6 :: vec[i32] L0: r0 = v.len r1 = i < r0 :: unsigned @@ -291,12 +274,8 @@ L3: L4: r5 = i L5: - r6 = v.items - r7 = r5 * 16 - r8 = r6 + r7 - r9 = load_mem r8 :: vec[i32]* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[vec[i32]] v, r5 + return r6 [case testVecMiscNestedPop_64bit] from librt.vecs import vec, pop diff --git a/mypyc/test-data/irbuild-vec-nested.test b/mypyc/test-data/irbuild-vec-nested.test index 1fe42a880d5b0..dd49d9475812f 100644 --- a/mypyc/test-data/irbuild-vec-nested.test +++ b/mypyc/test-data/irbuild-vec-nested.test @@ -206,10 +206,7 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: vec[str] + r6 :: vec[str] L0: r0 = v.len r1 = n < r0 :: unsigned @@ -227,12 +224,8 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 16 - r8 = r6 + r7 - r9 = load_mem r8 :: vec[str]* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[vec[str]] v, r5 + return r6 [case testVecNestedI64GetItem_64bit] from librt.vecs import vec @@ -250,10 +243,7 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: vec[i64] + r6 :: vec[i64] L0: r0 = v.len r1 = n < r0 :: unsigned @@ -271,12 +261,8 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 16 - r8 = r6 + r7 - r9 = load_mem r8 :: vec[i64]* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[vec[i64]] v, r5 + return r6 [case testVecNestedI64GetItemWithBorrow_64bit] from librt.vecs import vec @@ -294,20 +280,13 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: vec[i64] - r10 :: native_int - r11 :: bit - r12 :: i64 - r13 :: bit - r14 :: bool - r15 :: i64 - r16 :: ptr - r17 :: i64 - r18 :: ptr - r19 :: i64 + r6 :: vec[i64] + r7 :: native_int + r8 :: bit + r9 :: i64 + r10 :: bit + r11 :: bool + r12, r13 :: i64 L0: r0 = v.len r1 = n < r0 :: unsigned @@ -325,32 +304,94 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 16 - r8 = r6 + r7 - r9 = borrow load_mem r8 :: vec[i64]* - r10 = r9.len - r11 = n < r10 :: unsigned - if r11 goto L9 else goto L6 :: bool + r6 = vec_get_item_unsafe_borrow[vec[i64]] v, r5 + r7 = r6.len + r8 = n < r7 :: unsigned + if r8 goto L9 else goto L6 :: bool +L6: + r9 = n + r7 + r10 = r9 < r7 :: unsigned + if r10 goto L8 else goto L7 :: bool +L7: + r11 = raise IndexError + unreachable +L8: + r12 = r9 + goto L10 +L9: + r12 = n +L10: + r13 = vec_get_item_unsafe[i64] r6, r12 + keep_alive v, r5 + return r13 + +[case testVecNestedStrGetItemWithBorrow_64bit] +from librt.vecs import vec +from mypy_extensions import i64 + +class C: + v: vec[vec[str]] + + def f(self, n: i64) -> str: + # The intermediate vec is borrowed, but the result must be owned. + return self.v[n][n] +[out] +def C.f(self, n): + self :: __main__.C + n :: i64 + r0 :: vec[vec[str]] + r1 :: native_int + r2 :: bit + r3 :: i64 + r4 :: bit + r5 :: bool + r6 :: i64 + r7 :: vec[str] + r8 :: native_int + r9 :: bit + r10 :: i64 + r11 :: bit + r12 :: bool + r13 :: i64 + r14 :: str +L0: + r0 = borrow self.v + r1 = r0.len + r2 = n < r1 :: unsigned + if r2 goto L4 else goto L1 :: bool +L1: + r3 = n + r1 + r4 = r3 < r1 :: unsigned + if r4 goto L3 else goto L2 :: bool +L2: + r5 = raise IndexError + unreachable +L3: + r6 = r3 + goto L5 +L4: + r6 = n +L5: + r7 = vec_get_item_unsafe_borrow[vec[str]] r0, r6 + r8 = r7.len + r9 = n < r8 :: unsigned + if r9 goto L9 else goto L6 :: bool L6: - r12 = n + r10 - r13 = r12 < r10 :: unsigned - if r13 goto L8 else goto L7 :: bool + r10 = n + r8 + r11 = r10 < r8 :: unsigned + if r11 goto L8 else goto L7 :: bool L7: - r14 = raise IndexError + r12 = raise IndexError unreachable L8: - r15 = r12 + r13 = r10 goto L10 L9: - r15 = n + r13 = n L10: - r16 = r9.items - r17 = r15 * 8 - r18 = r16 + r17 - r19 = load_mem r18 :: i64* - keep_alive v, r9 - return r19 + r14 = vec_get_item_unsafe[str] r7, r13 + keep_alive self, r0, r6 + return r14 [case testVecDoublyNestedGetItem_64bit] from librt.vecs import vec @@ -368,10 +409,7 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: vec[vec[str]] + r6 :: vec[vec[str]] L0: r0 = v.len r1 = n < r0 :: unsigned @@ -389,12 +427,8 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 16 - r8 = r6 + r7 - r9 = load_mem r8 :: vec[vec[str]]* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[vec[vec[str]]] v, r5 + return r6 [case testVecNestedCreateWithCap_64bit] from librt.vecs import vec diff --git a/mypyc/test-data/irbuild-vec-t.test b/mypyc/test-data/irbuild-vec-t.test index 63ad14bc2d7a2..ee48d81fc29c8 100644 --- a/mypyc/test-data/irbuild-vec-t.test +++ b/mypyc/test-data/irbuild-vec-t.test @@ -215,10 +215,7 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: str + r6 :: str L0: r0 = v.len r1 = n < r0 :: unsigned @@ -236,12 +233,8 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 8 - r8 = r6 + r7 - r9 = load_mem r8 :: builtins.str* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[str] v, r5 + return r6 [case testVecTOptionalGetItem_64bit] from librt.vecs import vec @@ -260,10 +253,7 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: union[str, None] + r6 :: union[str, None] L0: r0 = v.len r1 = n < r0 :: unsigned @@ -281,12 +271,8 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 8 - r8 = r6 + r7 - r9 = load_mem r8 :: union* - keep_alive v - return r9 + r6 = vec_get_item_unsafe[union[str, None]] v, r5 + return r6 [case testNewTPopLast] from typing import Tuple @@ -524,3 +510,52 @@ L0: r1 = r0 r2 = VecTApi.from_iterable(r1, a, 0) return r2 + +[case testVecTBorrowGetItem_64bit] +from librt.vecs import vec +from mypy_extensions import i64 + +class A: + x: str + +def f(v: vec[A], n: i64) -> int: + return len(v[n].x) +[out] +def f(v, n): + v :: vec[__main__.A] + n :: i64 + r0 :: native_int + r1 :: bit + r2 :: i64 + r3 :: bit + r4 :: bool + r5 :: i64 + r6 :: __main__.A + r7 :: str + r8 :: native_int + r9 :: bit + r10 :: short_int +L0: + r0 = v.len + r1 = n < r0 :: unsigned + if r1 goto L4 else goto L1 :: bool +L1: + r2 = n + r0 + r3 = r2 < r0 :: unsigned + if r3 goto L3 else goto L2 :: bool +L2: + r4 = raise IndexError + unreachable +L3: + r5 = r2 + goto L5 +L4: + r5 = n +L5: + r6 = vec_get_item_unsafe_borrow[__main__.A] v, r5 + r7 = r6.x + keep_alive v, r5 + r8 = CPyStr_Size_size_t(r7) + r9 = r8 >= 0 :: signed + r10 = r8 << 1 + return r10 diff --git a/mypyc/test-data/lowering-vec.test b/mypyc/test-data/lowering-vec.test new file mode 100644 index 0000000000000..374d336b98d8e --- /dev/null +++ b/mypyc/test-data/lowering-vec.test @@ -0,0 +1,155 @@ +[case testLowerVecI64GetItem_64bit] +from librt.vecs import vec +from mypy_extensions import i64 + +def f(i: i64) -> i64: + v = vec[i64]() + return v[i] +[out] +def f(i): + i :: i64 + r0, v :: vec[i64] + r1 :: native_int + r2 :: bit + r3 :: i64 + r4 :: bit + r5 :: bool + r6 :: i64 + r7 :: ptr + r8 :: i64 + r9 :: ptr + r10, r11 :: i64 +L0: + r0 = VecI64Api.alloc(0, 0) + if is_error(r0) goto L8 (error at f:5) else goto L1 +L1: + v = r0 + r1 = v.len + r2 = i < r1 :: unsigned + if r2 goto L6 else goto L2 :: bool +L2: + r3 = i + r1 + r4 = r3 < r1 :: unsigned + if r4 goto L5 else goto L9 :: bool +L3: + r5 = raise IndexError + if not r5 goto L8 (error at f:6) else goto L4 :: bool +L4: + unreachable +L5: + r6 = r3 + goto L7 +L6: + r6 = i +L7: + r7 = v.items + r8 = r6 * 8 + r9 = r7 + r8 + r10 = load_mem r9 :: i64* + dec_ref v + return r10 +L8: + r11 = :: i64 + return r11 +L9: + dec_ref v + goto L3 + +[case testLowerVecNestedGetItem_64bit] +from librt.vecs import vec +from mypy_extensions import i64 + +def f(i: i64, j: i64) -> str: + v = vec[vec[str]]([]) + return v[i][j] +[out] +def f(i, j): + i, j :: i64 + r0 :: object + r1 :: ptr + r2 :: vec[vec[str]] + r3 :: ptr + v :: vec[vec[str]] + r4 :: native_int + r5 :: bit + r6 :: i64 + r7 :: bit + r8 :: bool + r9 :: i64 + r10 :: ptr + r11 :: i64 + r12 :: ptr + r13 :: vec[str] + r14 :: native_int + r15 :: bit + r16 :: i64 + r17 :: bit + r18 :: bool + r19 :: i64 + r20 :: ptr + r21 :: i64 + r22 :: ptr + r23, r24 :: str +L0: + r0 = load_address PyUnicode_Type + r1 = r0 + r2 = VecNestedApi.alloc(0, 0, r1, 1) + if is_error(r2) goto L14 (error at f:5) else goto L1 +L1: + r3 = r2.items + v = r2 + r4 = v.len + r5 = i < r4 :: unsigned + if r5 goto L6 else goto L2 :: bool +L2: + r6 = i + r4 + r7 = r6 < r4 :: unsigned + if r7 goto L5 else goto L15 :: bool +L3: + r8 = raise IndexError + if not r8 goto L14 (error at f:6) else goto L4 :: bool +L4: + unreachable +L5: + r9 = r6 + goto L7 +L6: + r9 = i +L7: + r10 = v.items + r11 = r9 * 16 + r12 = r10 + r11 + r13 = borrow load_mem r12 :: vec[str]* + r14 = r13.len + r15 = j < r14 :: unsigned + if r15 goto L12 else goto L8 :: bool +L8: + r16 = j + r14 + r17 = r16 < r14 :: unsigned + if r17 goto L11 else goto L16 :: bool +L9: + r18 = raise IndexError + if not r18 goto L14 (error at f:6) else goto L10 :: bool +L10: + unreachable +L11: + r19 = r16 + goto L13 +L12: + r19 = j +L13: + r20 = r13.items + r21 = r19 * 8 + r22 = r20 + r21 + r23 = load_mem r22 :: builtins.str* + dec_ref v + return r23 +L14: + r24 = :: str + return r24 +L15: + dec_ref v + goto L3 +L16: + dec_ref v + goto L9 diff --git a/mypyc/test-data/refcount.test b/mypyc/test-data/refcount.test index 918c84ee3b0ad..cefa050e8f0e9 100644 --- a/mypyc/test-data/refcount.test +++ b/mypyc/test-data/refcount.test @@ -1606,12 +1606,9 @@ def f(v): t :: i64 r0, r1 :: native_int r2 :: bit - r3 :: ptr - r4 :: native_int - r5 :: ptr - r6, s :: str - r7 :: None - r8 :: native_int + r3, s :: str + r4 :: None + r5 :: native_int L0: t = 0 r0 = 0 @@ -1620,16 +1617,13 @@ L1: r2 = r0 < r1 :: signed if r2 goto L2 else goto L4 :: bool L2: - r3 = v.items - r4 = r0 * 8 - r5 = r3 + r4 - r6 = load_mem r5 :: builtins.str* - s = r6 - r7 = g(s) + r3 = vec_get_item_unsafe[str] v, r0 + s = r3 + r4 = g(s) dec_ref s L3: - r8 = r0 + 1 - r0 = r8 + r5 = r0 + 1 + r0 = r5 goto L1 L4: return t @@ -1684,11 +1678,7 @@ def C.f(self, x): r3 :: i64 r4 :: bit r5 :: bool - r6 :: i64 - r7 :: ptr - r8 :: i64 - r9 :: ptr - r10 :: i64 + r6, r7 :: i64 L0: r0 = borrow self.v r1 = r0.len @@ -1707,11 +1697,8 @@ L3: L4: r6 = x L5: - r7 = r0.items - r8 = r6 * 8 - r9 = r7 + r8 - r10 = load_mem r9 :: i64* - return r10 + r7 = vec_get_item_unsafe[i64] r0, r6 + return r7 [case testVecI64LenBorrowVec_64bit] from librt.vecs import vec @@ -1771,10 +1758,7 @@ def f(v, n): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: str + r6 :: str L0: r0 = v.len r1 = n < r0 :: unsigned @@ -1792,11 +1776,8 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 8 - r8 = r6 + r7 - r9 = load_mem r8 :: builtins.str* - return r9 + r6 = vec_get_item_unsafe[str] v, r5 + return r6 [case testVecNestedGetItem_64bit] from librt.vecs import vec @@ -1817,10 +1798,7 @@ def f(v, n): r6 :: bit r7 :: bool r8 :: i64 - r9 :: ptr - r10 :: i64 - r11 :: ptr - r12, vv :: vec[str] + r9, vv :: vec[str] L0: r0 = load_address PyUnicode_Type r1 = r0 @@ -1841,12 +1819,9 @@ L3: L4: r8 = n L5: - r9 = r2.items - r10 = r8 * 16 - r11 = r9 + r10 - r12 = load_mem r11 :: vec[str]* + r9 = vec_get_item_unsafe[vec[str]] r2, r8 dec_ref r2 - vv = r12 + vv = r9 dec_ref vv return 1 L6: @@ -1869,20 +1844,13 @@ def f(v, n, m): r3 :: bit r4 :: bool r5 :: i64 - r6 :: ptr - r7 :: i64 - r8 :: ptr - r9 :: vec[i64] - r10 :: native_int - r11 :: bit - r12 :: i64 - r13 :: bit - r14 :: bool - r15 :: i64 - r16 :: ptr - r17 :: i64 - r18 :: ptr - r19 :: i64 + r6 :: vec[i64] + r7 :: native_int + r8 :: bit + r9 :: i64 + r10 :: bit + r11 :: bool + r12, r13 :: i64 L0: r0 = v.len r1 = n < r0 :: unsigned @@ -1900,31 +1868,25 @@ L3: L4: r5 = n L5: - r6 = v.items - r7 = r5 * 16 - r8 = r6 + r7 - r9 = borrow load_mem r8 :: vec[i64]* - r10 = r9.len - r11 = m < r10 :: unsigned - if r11 goto L9 else goto L6 :: bool + r6 = vec_get_item_unsafe_borrow[vec[i64]] v, r5 + r7 = r6.len + r8 = m < r7 :: unsigned + if r8 goto L9 else goto L6 :: bool L6: - r12 = m + r10 - r13 = r12 < r10 :: unsigned - if r13 goto L8 else goto L7 :: bool + r9 = m + r7 + r10 = r9 < r7 :: unsigned + if r10 goto L8 else goto L7 :: bool L7: - r14 = raise IndexError + r11 = raise IndexError unreachable L8: - r15 = r12 + r12 = r9 goto L10 L9: - r15 = m + r12 = m L10: - r16 = r9.items - r17 = r15 * 8 - r18 = r16 + r17 - r19 = load_mem r18 :: i64* - return r19 + r13 = vec_get_item_unsafe[i64] r6, r12 + return r13 [case testVecPop] from librt.vecs import vec, pop, append diff --git a/mypyc/test/test_expand_rtype.py b/mypyc/test/test_expand_rtype.py new file mode 100644 index 0000000000000..90acc61f625d9 --- /dev/null +++ b/mypyc/test/test_expand_rtype.py @@ -0,0 +1,48 @@ +import unittest + +from mypyc.ir.class_ir import ClassIR +from mypyc.ir.rtypes import ( + RInstance, + RTuple, + RTypeVar, + RUnion, + RVec, + int_rprimitive, + str_rprimitive, + void_rtype, +) +from mypyc.rt_expandtype import expand_rtype + + +class TestExpandRType(unittest.TestCase): + def test_trivial(self) -> None: + assert expand_rtype(str_rprimitive, []) == str_rprimitive + assert expand_rtype(str_rprimitive, [int_rprimitive]) == str_rprimitive + assert expand_rtype(void_rtype, []) == void_rtype + + def test_instance(self) -> None: + inst = RInstance(ClassIR("A", "__main__")) + assert expand_rtype(inst, [int_rprimitive]) == inst + + def test_simple_expansion(self) -> None: + assert expand_rtype(RTypeVar(0), [str_rprimitive]) == str_rprimitive + + def test_tuple_expansion(self) -> None: + assert expand_rtype( + RTuple([RTypeVar(0), RTypeVar(1)]), [str_rprimitive, int_rprimitive] + ) == RTuple([str_rprimitive, int_rprimitive]) + + def test_union_expansion(self) -> None: + assert expand_rtype( + RUnion([RTypeVar(0), RTypeVar(1)]), [str_rprimitive, int_rprimitive] + ) == RUnion([str_rprimitive, int_rprimitive]) + + def test_vec_expansion(self) -> None: + assert expand_rtype(RVec(RTypeVar(0)), [str_rprimitive]) == RVec(str_rprimitive) + + def test_nested_expansion(self) -> None: + typ = RUnion([RTuple([RVec(RTypeVar(0)), RTypeVar(1)]), RVec(RVec(RTypeVar(0)))]) + expected = RUnion( + [RTuple([RVec(str_rprimitive), int_rprimitive]), RVec(RVec(str_rprimitive))] + ) + assert expand_rtype(typ, [str_rprimitive, int_rprimitive]) == expected diff --git a/mypyc/test/test_lowering.py b/mypyc/test/test_lowering.py index e27b4e77eea8a..3eb4698c68793 100644 --- a/mypyc/test/test_lowering.py +++ b/mypyc/test/test_lowering.py @@ -28,7 +28,7 @@ class TestLowering(MypycDataSuite): - files = ["lowering-int.test", "lowering-list.test"] + files = ["lowering-int.test", "lowering-list.test", "lowering-vec.test"] base_path = test_temp_dir def run_case(self, testcase: DataDrivenTestCase) -> None: From 79a769ac7834aeada0a9af516f8bd8cdacd28542 Mon Sep 17 00:00:00 2001 From: Tom Bannink Date: Tue, 30 Jun 2026 13:06:28 +0200 Subject: [PATCH 091/127] [mypyc] Fix reference leak when setting unboxed refcounted attrs (#21657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I ran into this memory leak which can reproduced with: ```python from dataclasses import dataclass @dataclass class MyClass: v: int c = MyClass(1 << 70) ``` This PR adds a fix and a test that fails without the fix. I am no expert on mypyc internals, so I asked Claude Code to fix this bug. It is a one-line change so I believe it will be easy to review. From quick inspection it seems that the rest of the code has fewer comments than what Claude wrote, so if you prefer, I'll remove the verbose comment that this PR adds. Similarly for the test code, it can be shortened if wanted.
Long explanation from Claude code ### Description A native attribute setter generated by mypyc over-increfs the stored value when the attribute has a **refcounted unboxed type** — most importantly `int` (`CPyTagged`), and also tuples with refcounted items. For an unboxed type, `generate_setter` emitted: ```c tmp = CPyTagged_FromObject(value); // already creates a NEW (owned) reference CPyTagged_INCREF(tmp); // ...and then takes a second one self->_v = tmp; ``` CPyTagged_FromObject already increfs in the heap-boxed case, so the setter takes two references while the deallocator releases only one — leaking one reference on every set through the setter. The other two branches of generate_setter (object and the emit_cast path) are correct because they produce a borrowed value and rely on the single emit_inc_ref to take ownership; only the unboxed branch was inconsistent. Why this shows up with dataclasses This is reached whenever an attribute is set from interpreted code. The clearest real-world case is the __init__ that the stdlib dataclasses module synthesizes for a mypyc-compiled @dataclass: its self.v = v runs as interpreted code and goes through the generated descriptor setter. So every constructed instance of a compiled dataclass with a heap-boxed int field (value ≥ 2**62) leaked one PyLong — silent, unbounded growth in long-lived programs that build many such objects (e.g. 64-bit ids). It does not depend on slots, frozen, eq, field count, or field position; a hand-written native __init__ is unaffected because it stores via SetAttr rather than the descriptor. Small (inline-tagged) ints, floats, and object-typed fields are also unaffected since they aren't refcounted through this path. Reproducer ```python from dataclasses import dataclass import sys @dataclass class C: v: int big = 1 << 70 # heap-boxed int (>= 2**62), so it is refcounted base = sys.getrefcount(big) xs = [C(big) for _ in range(1000)] del xs print(sys.getrefcount(big) - base) # before: 1000 after: 0 ``` Fix Unbox with borrow=True in the setter's unboxed branch, so all three branches of generate_setter produce a borrowed value and the single emit_inc_ref takes exactly one owned reference. borrow=True is a no-op for non-refcounted unboxed types (float, fixed-width ints, bool) and is propagated correctly through RTuple unboxing, which fixes the analogous leak for tuple-typed attributes too. Tests Added testNativeAttrSetterRefcountLeak to mypyc/test-data/run-classes.test, covering a boxed-int dataclass field, an unboxed Tuple[int, int] field, and setter re-assignment. It fails before the fix (expected 100 live refs, got 200) and passes after.
--------- Co-authored-by: Claude Opus 4.8 (1M context) --- mypyc/codegen/emitclass.py | 10 +++++- mypyc/test-data/run-classes.test | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index db94f1de9406e..6054a934b25e1 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -1211,7 +1211,15 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N emitter.emit_line("if (value != NULL) {") if rtype.is_unboxed: - emitter.emit_unbox("value", "tmp", rtype, error=ReturnHandler("-1"), declare_dest=True) + # Borrow the unboxed value: emit_inc_ref below takes the single owned + # reference, matching the borrowed-then-incref pattern of the other two + # branches. Without borrow=True, emit_unbox already creates a new + # reference for refcounted unboxed types (e.g. CPyTagged boxed ints, + # tuples with refcounted fields), so the emit_inc_ref would double the + # reference and leak the stored value on every set via this setter. + emitter.emit_unbox( + "value", "tmp", rtype, error=ReturnHandler("-1"), declare_dest=True, borrow=True + ) elif is_same_type(rtype, object_rprimitive): emitter.emit_line("PyObject *tmp = value;") else: diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 7722cf26ca910..d830b0c04fa6f 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -6006,3 +6006,61 @@ for _ in range(100): check(foo) after = sys.getrefcount(foo.obj) assert after - init == 0, f"Leaked {after - init} refs" + +[case testNativeAttrSetterRefcountLeak] +# Setting a native attribute from interpreted code goes through the generated +# getset descriptor setter. For refcounted unboxed types (heap-boxed ints, +# tuples with refcounted items) the setter must take exactly one reference to +# the stored value, not two. +from dataclasses import dataclass +from typing import Tuple + +@dataclass +class IntField: + v: int + +@dataclass +class TupleField: + v: Tuple[int, int] + +[file driver.py] +import sys +from native import IntField, TupleField + +# A heap-boxed int (>= 2**62) is stored as a refcounted PyObject*, unlike small +# inline-tagged ints, so an over-incref strands a real reference. Compute the +# values at runtime (not as folded literals): on free-threaded builds code +# constants are immortal, and getrefcount could not observe a leak on them. +shift = 70 +BIG = 1 << shift + +def check_no_leak(make, value) -> None: + base = sys.getrefcount(value) + objs = [make(value) for _ in range(100)] + alive = sys.getrefcount(value) + # Each live instance must hold exactly one reference to the field value. + assert alive - base == 100, f"expected 100 live refs, got {alive - base}" + del objs + after = sys.getrefcount(value) + assert after == base, f"leaked {after - base} refs" + +# The dataclass-generated __init__ stores self.v = v via the descriptor setter. +check_no_leak(IntField, BIG) + +# Tuple[int, int] is stored as an unboxed RTuple; its boxed-int elements are +# refcounted, so the setter must not over-incref them either. Use the same int +# in both slots so each instance holds exactly two references to it. +ELEM = 1 << (shift + 1) +base = sys.getrefcount(ELEM) +objs = [TupleField((ELEM, ELEM)) for _ in range(100)] +alive = sys.getrefcount(ELEM) +assert alive - base == 200, f"expected 200 live refs, got {alive - base}" +del objs +assert sys.getrefcount(ELEM) == base, f"tuple field leaked {sys.getrefcount(ELEM) - base} refs" + +# Re-assigning through the setter must release the previous value too. +o = IntField(BIG) +base = sys.getrefcount(BIG) +o.v = BIG +o.v = BIG +assert sys.getrefcount(BIG) == base, "reassignment leaked refs" From 47f0df577f42c726f847163f9c0a15df681a8479 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 30 Jun 2026 21:28:36 +0100 Subject: [PATCH 092/127] Bump librt to 0.12.0 (#21663) This bump includes: * A mypy sync, including recent free-threading fixes. * New experimental Emscripten wheels. --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index 04381db913521..9505cf14af452 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -5,5 +5,5 @@ typing_extensions>=4.14.0; python_version>='3.15' mypy_extensions>=1.0.0 pathspec>=1.0.0 tomli>=1.1.0; python_version<'3.11' -librt>=0.11.0; platform_python_implementation != 'PyPy' +librt>=0.12.0; platform_python_implementation != 'PyPy' ast-serialize>=0.5.0,<1.0.0 diff --git a/pyproject.toml b/pyproject.toml index a89bac5c356eb..6fb487a676d4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires = [ "mypy_extensions>=1.0.0", "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.11.0; platform_python_implementation != 'PyPy'", + "librt>=0.12.0; platform_python_implementation != 'PyPy'", # the following is from build-requirements.txt "types-psutil", "types-setuptools", @@ -58,7 +58,7 @@ dependencies = [ "mypy_extensions>=1.0.0", "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.11.0; platform_python_implementation != 'PyPy'", + "librt>=0.12.0; platform_python_implementation != 'PyPy'", "ast-serialize>=0.5.0,<1.0.0", ] dynamic = ["version"] diff --git a/test-requirements.txt b/test-requirements.txt index 254c6b8aaaa28..64092a392b296 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -25,7 +25,7 @@ identify==2.6.19 # via pre-commit iniconfig==2.3.0 # via pytest -librt==0.11.0 ; platform_python_implementation != "PyPy" +librt==0.12.0 ; platform_python_implementation != "PyPy" # via -r mypy-requirements.txt lxml==6.1.0 ; python_version < "3.15" # via -r test-requirements.in From d67a13887480ac9a06523139167a4f772c5cb49f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 1 Jul 2026 09:30:29 +0100 Subject: [PATCH 093/127] Bump ast-serialize to 0.6.0 (#21664) No semantic changes in this PR, just the new experimental Emscripten wheels. --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index 9505cf14af452..d2991309e391e 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -6,4 +6,4 @@ mypy_extensions>=1.0.0 pathspec>=1.0.0 tomli>=1.1.0; python_version<'3.11' librt>=0.12.0; platform_python_implementation != 'PyPy' -ast-serialize>=0.5.0,<1.0.0 +ast-serialize>=0.6.0,<1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 6fb487a676d4e..f166a58086821 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ requires = [ "types-psutil", "types-setuptools", # required to work around a mypyc import bug - "ast-serialize>=0.5.0,<1.0.0", + "ast-serialize>=0.6.0,<1.0.0", ] build-backend = "setuptools.build_meta" @@ -59,7 +59,7 @@ dependencies = [ "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", "librt>=0.12.0; platform_python_implementation != 'PyPy'", - "ast-serialize>=0.5.0,<1.0.0", + "ast-serialize>=0.6.0,<1.0.0", ] dynamic = ["version"] diff --git a/test-requirements.txt b/test-requirements.txt index 64092a392b296..e79319fdbeece 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=test-requirements.txt --strip-extras test-requirements.in # -ast-serialize==0.5.0 +ast-serialize==0.6.0 # via -r mypy-requirements.txt attrs==26.1.0 # via -r test-requirements.in From a6eadee843fd1c549d4c8e086d88997f2c7f525c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 1 Jul 2026 16:59:22 +0100 Subject: [PATCH 094/127] [mypyc] Make instance attribute read-only at runtime if Final (#21666) Previously this was only enforced statically. I plan to add more optimizations that take advantage of Final in the future. --- mypyc/codegen/emitclass.py | 30 +++++++++++++++--------- mypyc/ir/class_ir.py | 4 ++++ mypyc/irbuild/prepare.py | 2 ++ mypyc/test-data/run-classes.test | 40 ++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 6054a934b25e1..9baaec06a4611 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -1065,12 +1065,14 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None: getter_name(cl, attr, emitter.names), cl.struct_name(emitter.names) ) ) - emitter.emit_line("static int") - emitter.emit_line( - "{}({} *self, PyObject *value, void *closure);".format( - setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names) + # Final attributes are read-only, so they have no setter. + if attr not in cl.final_attributes: + emitter.emit_line("static int") + emitter.emit_line( + "{}({} *self, PyObject *value, void *closure);".format( + setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names) + ) ) - ) for prop, (getter, setter) in cl.properties.items(): if getter.decl.implicit: @@ -1099,11 +1101,15 @@ def generate_getseters_table(cl: ClassIR, name: str, emitter: Emitter) -> None: if not cl.is_trait: for attr in cl.attributes: emitter.emit_line(f'{{"{attr}",') - emitter.emit_line( - " (getter){}, (setter){},".format( - getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names) + if attr in cl.final_attributes: + # Final attributes are read-only, so emit a NULL setter. + emitter.emit_line(f" (getter){getter_name(cl, attr, emitter.names)}, NULL,") + else: + emitter.emit_line( + " (getter){}, (setter){},".format( + getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names) + ) ) - ) emitter.emit_line(" NULL, NULL},") for prop, (getter, setter) in cl.properties.items(): if getter.decl.implicit: @@ -1129,8 +1135,10 @@ def generate_getseters(cl: ClassIR, emitter: Emitter) -> None: if not cl.is_trait: for i, (attr, rtype) in enumerate(cl.attributes.items()): generate_getter(cl, attr, rtype, emitter) - emitter.emit_line("") - generate_setter(cl, attr, rtype, emitter) + # Final attributes are read-only, so they have no setter. + if attr not in cl.final_attributes: + emitter.emit_line("") + generate_setter(cl, attr, rtype, emitter) if i < len(cl.attributes) - 1: emitter.emit_line("") for prop, (getter, setter) in cl.properties.items(): diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index f754275480edc..028df5898d920 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -145,6 +145,8 @@ def __init__( ) # Attributes defined in the class (not inherited) self.attributes: dict[str, RType] = {} + # Final attributes defined in the class (not inherited) + self.final_attributes: set[str] = set() # Deletable attributes self.deletable: list[str] = [] # We populate method_types with the signatures of every method before @@ -396,6 +398,7 @@ def serialize(self) -> JsonDict: "ctor": self.ctor.serialize(), # We serialize dicts as lists to ensure order is preserved "attributes": [(k, t.serialize()) for k, t in self.attributes.items()], + "final_attributes": sorted(self.final_attributes), # We try to serialize a name reference, but if the decl isn't in methods # then we can't be sure that will work so we serialize the whole decl. "method_decls": [ @@ -456,6 +459,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR: ir.builtin_base = data["builtin_base"] ir.ctor = FuncDecl.deserialize(data["ctor"], ctx) ir.attributes = {k: deserialize_type(t, ctx) for k, t in data["attributes"]} + ir.final_attributes = set(data["final_attributes"]) ir.method_decls = { k: ctx.functions[v].decl if isinstance(v, str) else FuncDecl.deserialize(v, ctx) for k, v in data["method_decls"] diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 8b73b10bf8064..6e697823593a6 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -645,6 +645,8 @@ def prepare_methods_and_attributes( add_getter_declaration(ir, name, attr_rtype, module_name) add_setter_declaration(ir, name, attr_rtype, module_name) ir.attributes[name] = attr_rtype + if node.node.is_final: + ir.final_attributes.add(name) elif isinstance(node.node, (FuncDef, Decorator)): prepare_method_def(ir, module_name, cdef, mapper, node.node, options) elif isinstance(node.node, OverloadedFuncDef): diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index d830b0c04fa6f..73927bb037a71 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2825,6 +2825,46 @@ def test_final_attribute() -> None: assert C.b['x'] == 'y' assert C.a is C.b +[case testFinalInstanceAttributeCannotBeRebound] +from typing import Any, Final + +from testutil import assertRaises + +class C: + def __init__(self, x: int) -> None: + self.x: Final[int] = x + +class D(C): + def __init__(self, x: int, y: int) -> None: + super().__init__(x) + self.y: Final[int] = y + +def rebind_via_any(o: Any, value: int) -> None: + o.x = value + +def test_rebind_via_any() -> None: + c = C(1) + with assertRaises(AttributeError): + rebind_via_any(c, 2) + assert c.x == 1 + +def test_rebind_via_setattr() -> None: + c = C(1) + with assertRaises(AttributeError): + setattr(c, "x", 3) + assert c.x == 1 + +def test_rebind_inherited_via_setattr() -> None: + d = D(1, 2) + # Inherited Final attribute can't be modified. + with assertRaises(AttributeError): + setattr(d, "x", 3) + # The subclass's own Final attribute can't be modified either. + with assertRaises(AttributeError): + setattr(d, "y", 4) + assert d.x == 1 + assert d.y == 2 + [case testClassDerivedFromIntEnum] from enum import IntEnum, auto From 1462b4ec41ea12514b89e919ab15587378eb04c2 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 2 Jul 2026 13:05:31 +0100 Subject: [PATCH 095/127] Fix error code of note about unbound type variable (#21668) The note used the default `misc` error code, which was different from the error message. --- mypy/checker.py | 1 + test-data/unit/check-typevar-unbound.test | 3 +++ 2 files changed, 4 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 33705c98e10c3..d13b927b28f2f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1825,6 +1825,7 @@ def check_unbound_return_typevar(self, typ: CallableType) -> None: "Consider using the upper bound " f"{format_type(typ.ret_type.upper_bound, self.options)} instead", context=typ.ret_type, + code=TYPE_VAR, ) def check_default_params(self, item: FuncItem, body_is_trivial: bool | None = None) -> None: diff --git a/test-data/unit/check-typevar-unbound.test b/test-data/unit/check-typevar-unbound.test index 79d326f9fedf3..2dffaee6f2fe7 100644 --- a/test-data/unit/check-typevar-unbound.test +++ b/test-data/unit/check-typevar-unbound.test @@ -13,6 +13,9 @@ def g() -> U: # E: A function returning TypeVar should receive at least one argu # N: Consider using the upper bound "int" instead ... +def g2() -> U: # type: ignore[type-var] + ... + V = TypeVar('V', int, str) def h() -> V: # E: A function returning TypeVar should receive at least one argument containing the same TypeVar From 0c6013d74ec64b1ae4f77fbadfe4e1fb8093809d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 6 Jul 2026 00:25:56 +0100 Subject: [PATCH 096/127] Upload wasm wheels to PyPI (#21671) PyPI supports wasm platform tags since recently, see also https://github.com/mypyc/mypy_mypyc-wheels/pull/116 --- misc/upload-pypi.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/misc/upload-pypi.py b/misc/upload-pypi.py index 8ea86bbea584b..52092bd288f0f 100644 --- a/misc/upload-pypi.py +++ b/misc/upload-pypi.py @@ -31,16 +31,7 @@ def is_whl_or_tar(name: str) -> bool: def item_ok_for_pypi(name: str) -> bool: - if not is_whl_or_tar(name): - return False - - name = name.removesuffix(".tar.gz") - name = name.removesuffix(".whl") - - if name.endswith("wasm32"): - return False - - return True + return is_whl_or_tar(name) def get_release_for_tag(tag: str) -> dict[str, Any]: From c3d9b5cc52534f9908940a810596afe50b4ea3bf Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 11:18:34 +0100 Subject: [PATCH 097/127] Fix star import dependencies in mypy daemon (#21673) Changes weren't propagated reliably when exporting names using `from <...> import *`. This could result in false negatives and false positives. I used coding agent assist with small incremental changes. --- mypy/server/deps.py | 7 ++ mypy/server/update.py | 49 +++++++----- test-data/unit/deps.test | 13 +++ test-data/unit/fine-grained.test | 131 +++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 21 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b2c91d8db4888..29cd8ac8b4635 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -671,6 +671,13 @@ def visit_member_expr(self, e: MemberExpr) -> None: if e.kind is not None: # Reference to a module attribute self.process_global_ref_expr(e) + if isinstance(e.expr, RefExpr) and isinstance(e.expr.node, MypyFile): + # Also depend on the name as accessed through this module. The + # fullname above may point to the original definition (e.g. via + # a re-export using "from ... import *"), but we must also + # recheck if the name is removed from the accessed module's + # namespace. + self.add_dependency(make_trigger(e.expr.node.fullname + "." + e.name)) else: # Reference to a non-module (or missing) attribute if e.expr not in self.type_map: diff --git a/mypy/server/update.py b/mypy/server/update.py index 64ce0fb5d3f8a..cb7ff60e14cba 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -783,31 +783,37 @@ def calculate_active_triggers( else: snapshot2 = snapshot_symbol_table(id, new.names) diff = compare_symbol_table_snapshots(id, snapshot1, snapshot2) - package_nesting_level = id.count(".") - for item in diff.copy(): - if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in ( - "__builtins__", - "__file__", - "__name__", - "__package__", - "__doc__", - ): - # Activate catch-all wildcard trigger for top-level module changes (used for - # "from m import *"). This also gets triggered by changes to module-private - # entries, but as these unneeded dependencies only result in extra processing, - # it's a minor problem. - # - # TODO: Some __* names cause mistriggers. Fix the underlying issue instead of - # special casing them here. - diff.add(id + WILDCARD_TAG) - if item.count(".") > package_nesting_level + 1: - # These are for changes within classes, used by protocols. - diff.add(item.rsplit(".", 1)[0] + WILDCARD_TAG) - + diff |= wildcard_triggers_for_changes(id, diff) names |= diff return {make_trigger(name) for name in names} +def wildcard_triggers_for_changes(module_id: str, diff: set[str]) -> set[str]: + """Return catch-all wildcard triggers activated by a set of changed names.""" + result: set[str] = set() + package_nesting_level = module_id.count(".") + for item in diff: + if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in ( + "__builtins__", + "__file__", + "__name__", + "__package__", + "__doc__", + ): + # Activate catch-all wildcard trigger for top-level module changes (used for + # "from m import *"). This also gets triggered by changes to module-private + # entries, but as these unneeded dependencies only result in extra processing, + # it's a minor problem. + # + # TODO: Some __* names cause mistriggers. Fix the underlying issue instead of + # special casing them here. + result.add(module_id + WILDCARD_TAG) + if item.count(".") > package_nesting_level + 1: + # These are for changes within classes, used by protocols. + result.add(item.rsplit(".", 1)[0] + WILDCARD_TAG) + return result + + def replace_modules_with_new_variants( manager: BuildManager, graph: dict[str, State], @@ -1044,6 +1050,7 @@ def key(node: FineGrainedDeferredNode) -> int: changed = compare_symbol_table_snapshots( file_node.fullname, old_symbols_snapshot, new_symbols_snapshot ) + changed |= wildcard_triggers_for_changes(module_id, changed) new_triggered = {make_trigger(name) for name in changed} # Dependencies may have changed. diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index ab2c5f1c43e74..6850eabf09b6a 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -69,6 +69,19 @@ x = 1 -> m.f -> m, m.f +[case testAccessReexportedModuleAttribute] +import pkg +pkg.f() +[file pkg/__init__.py] +from pkg.sub import * +[file pkg/sub.py] +def f() -> None: pass +[out] + -> m + -> m + -> pkg + -> m + [case testImport] import n [file n.py] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index bc02f49e5d835..ef2e8c0b343bf 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11839,3 +11839,134 @@ def bar() -> str: return "a" main:2: note: Revealed type is "None | builtins.int" == main:2: note: Revealed type is "None | builtins.str" + +[case testStarExportedNameDeleted1] +import pkg + +pkg.loads() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +def loads() -> None: ... + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: "object" has no attribute "loads" + +[case testStarExportedNameDeleted2] +import pkg + +pkg.C() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +class C: pass + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: "object" has no attribute "C" + +[case testStarExportedNameDeleted3] +from pkg import loads + +loads() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +def loads() -> None: ... + +[file pkg/sub.pyi.2] +[out] +== +main:1: error: Module "pkg" has no attribute "loads" + +[case testStarExportedNameDeleted4] +from pkg import sub + +a = sub.x + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +x = 1 + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: "object" has no attribute "x" + +[case testStarExportedNameDeleted5] +from pkg import sub + +x: sub.N = 1 + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +N = int + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: Name "sub.N" is not defined + +[case testStarExportedNameDeleted6] +from pkg import * + +loads() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +def loads() -> None: ... + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: Name "loads" is not defined + +[case testStarExportedNameDeleted7] +from pkg import * + +C() + +[file pkg/__init__.pyi] +from .sub import * + +[file pkg/sub.pyi] +class C: pass + +[file pkg/sub.pyi.2] +[out] +== +main:3: error: Name "C" is not defined + +[case testStarExportedNameDeleted8] +from a import * + +C() + +[file a.py] +from b import * + +[file b.py] +from c import * + +[file c.py] +class C: pass + +[file c.py.2] +[out] +== +main:3: error: Name "C" is not defined From 50e04b28a207662d5a672cdd9ddc04f3600d2e61 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 14:38:31 +0100 Subject: [PATCH 098/127] [mypyc] Make vec creation from list memory safe on free-threaded builds (#21681) I used coding agent assist (in particular, to write the second variant based on the first). Work on mypyc/mypyc#1202. --- mypyc/lib-rt/vecs/vec_t.c | 30 ++++++++++++++++++++++++++++++ mypyc/lib-rt/vecs/vec_template.c | 24 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/mypyc/lib-rt/vecs/vec_t.c b/mypyc/lib-rt/vecs/vec_t.c index 3cfd1756201ce..5c0865ed7772e 100644 --- a/mypyc/lib-rt/vecs/vec_t.c +++ b/mypyc/lib-rt/vecs/vec_t.c @@ -756,6 +756,36 @@ static inline VecT vec_from_sequence( VecT v = vec_alloc(alloc_size, item_type); if (VEC_IS_ERROR(v)) return vec_error(); +#ifdef Py_GIL_DISABLED + if (is_list) { + // Other threads could be mutating the list concurrently, so use strong references + Py_ssize_t actual = 0; + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(seq) && i < n; i++) { + PyObject *item = PyList_GetItemRef(seq, i); + if (unlikely(item == NULL)) { + // Race condition: list shrank between size read and get item + for (Py_ssize_t j = actual; j < alloc_size; j++) + v.items[j] = NULL; + VEC_DECREF(v); + return vec_error(); + } + if (!VecT_ItemCheck(v, item, item_type)) { + Py_DECREF(item); + for (Py_ssize_t j = actual; j < alloc_size; j++) + v.items[j] = NULL; + VEC_DECREF(v); + return vec_error(); + } + v.items[actual++] = item; + } + for (Py_ssize_t j = actual; j < alloc_size; j++) + v.items[j] = NULL; + vec_track_buffer(&v); + v.len = actual; + return v; + } + // Tuples are immutable, so fall through to the shared loop below +#endif for (Py_ssize_t i = 0; i < n; i++) { PyObject *item = is_list ? PyList_GET_ITEM(seq, i) : PyTuple_GET_ITEM(seq, i); if (!VecT_ItemCheck(v, item, item_type)) { diff --git a/mypyc/lib-rt/vecs/vec_template.c b/mypyc/lib-rt/vecs/vec_template.c index b3e80261fec38..a6b09ba2a7b29 100644 --- a/mypyc/lib-rt/vecs/vec_template.c +++ b/mypyc/lib-rt/vecs/vec_template.c @@ -148,6 +148,30 @@ static inline VEC vec_from_sequence(PyObject *seq, int64_t cap, const int is_lis VEC v = vec_alloc(alloc_size); if (VEC_IS_ERROR(v)) return vec_error(); +#ifdef Py_GIL_DISABLED + if (is_list) { + // Other threads could be mutating the list concurrently, so use strong references + Py_ssize_t actual = 0; + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(seq) && i < n; i++) { + PyObject *item = PyList_GetItemRef(seq, i); + if (unlikely(item == NULL)) { + // Race condition: list shrank between size read and get item + VEC_DECREF(v); + return vec_error(); + } + ITEM_C_TYPE x = UNBOX_ITEM(item); + Py_DECREF(item); + if (IS_UNBOX_ERROR(x)) { + VEC_DECREF(v); + return vec_error(); + } + v.items[actual++] = x; + } + v.len = actual; + return v; + } + // Tuples are immutable, so fall through to the shared loop below +#endif for (Py_ssize_t i = 0; i < n; i++) { PyObject *item = is_list ? PyList_GET_ITEM(seq, i) : PyTuple_GET_ITEM(seq, i); ITEM_C_TYPE x = UNBOX_ITEM(item); From 1cc7e8fdc3c7c7086ea46db996fd7d64f13a758d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 14:38:39 +0100 Subject: [PATCH 099/127] [mypyc] Fix memory safety of list.count on free-threaded builds (#21680) Work on mypyc/mypyc#1202. --- mypyc/lib-rt/pythonsupport.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mypyc/lib-rt/pythonsupport.h b/mypyc/lib-rt/pythonsupport.h index 1b0583543fe48..35f1e78df3915 100644 --- a/mypyc/lib-rt/pythonsupport.h +++ b/mypyc/lib-rt/pythonsupport.h @@ -205,6 +205,25 @@ list_count(PyListObject *self, PyObject *value) Py_ssize_t count = 0; Py_ssize_t i; +#ifdef Py_GIL_DISABLED + for (i = 0; i < PyList_GET_SIZE(self); i++) { + PyObject *item = PyList_GetItemRef((PyObject *)self, i); + if (unlikely(item == NULL)) { + // Race condition: list shrank between size read and get item + if (PyErr_ExceptionMatches(PyExc_IndexError)) { + PyErr_Clear(); + break; + } + return CPY_INT_TAG; + } + int cmp = PyObject_RichCompareBool(item, value, Py_EQ); + Py_DECREF(item); + if (cmp > 0) + count++; + else if (cmp < 0) + return CPY_INT_TAG; + } +#else for (i = 0; i < Py_SIZE(self); i++) { int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ); if (cmp > 0) @@ -212,6 +231,7 @@ list_count(PyListObject *self, PyObject *value) else if (cmp < 0) return CPY_INT_TAG; } +#endif return CPyTagged_ShortFromSsize_t(count); } From ee076569870fc9a281551c7f7f1e4ab8b00e33b7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 14:38:48 +0100 Subject: [PATCH 100/127] [mypyc] Don't borrow list items on free-threaded builds (#21679) Borrowing list items is unsafe on free-threaded builds, since another thread could be concurrently mutating the list. Also make it possible to have different expected test case outputs on free-threaded and GIL-enabled builds. Work on mypyc/mypyc#1202. --- mypyc/irbuild/expression.py | 14 +++-- mypyc/primitives/list_ops.py | 66 +++++++++++------------ mypyc/test-data/exceptions.test | 2 +- mypyc/test-data/irbuild-basic.test | 2 +- mypyc/test-data/irbuild-classes.test | 2 +- mypyc/test-data/irbuild-i64.test | 60 ++++++++++++++++++++- mypyc/test-data/irbuild-lists.test | 20 ++++++- mypyc/test-data/refcount.test | 78 +++++++++++++++++++++++++++- mypyc/test/test_exceptions.py | 10 +++- mypyc/test/test_irbuild.py | 3 ++ mypyc/test/test_refcount.py | 8 ++- 11 files changed, 219 insertions(+), 46 deletions(-) diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index cd7295ef709ad..21cb2f8df1b8f 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -58,7 +58,7 @@ TypeType, get_proper_type, ) -from mypyc.common import MAX_SHORT_INT +from mypyc.common import IS_FREE_THREADED, MAX_SHORT_INT from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD from mypyc.ir.ops import ( @@ -773,8 +773,14 @@ def try_optimize_int_floor_divide(builder: IRBuilder, expr: OpExpr) -> OpExpr: def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value: index = expr.index base_type = builder.node_type(expr.base) - can_borrow = is_list_rprimitive(base_type) or isinstance(base_type, RVec) - can_borrow_base = can_borrow and is_borrow_friendly_expr(builder, index) + # We can borrow a list item safely only if GIL is enabled. The vec type is optimized for + # performance, so we'll do unsafe borrowing. + can_borrow = (is_list_rprimitive(base_type) and not IS_FREE_THREADED) or isinstance( + base_type, RVec + ) + can_borrow_base = ( + is_list_rprimitive(base_type) or isinstance(base_type, RVec) + ) and is_borrow_friendly_expr(builder, index) # Check for dunder specialization for non-slice indexing if not isinstance(index, SliceExpr): @@ -796,7 +802,7 @@ def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value: if value: return value - index_reg = builder.accept(expr.index, can_borrow=can_borrow) + index_reg = builder.accept(expr.index, can_borrow=can_borrow or can_borrow_base) return builder.builder.get_item( base, index_reg, builder.node_type(expr), expr.line, can_borrow=builder.can_borrow ) diff --git a/mypyc/primitives/list_ops.py b/mypyc/primitives/list_ops.py index efd40469606f3..25c9cd4b4c319 100644 --- a/mypyc/primitives/list_ops.py +++ b/mypyc/primitives/list_ops.py @@ -2,6 +2,7 @@ from __future__ import annotations +from mypyc.common import IS_FREE_THREADED from mypyc.ir.ops import ERR_FALSE, ERR_MAGIC, ERR_NEVER from mypyc.ir.rtypes import ( bit_rprimitive, @@ -108,28 +109,6 @@ priority=2, ) -# list[index] that produces a borrowed result -method_op( - name="__getitem__", - arg_types=[list_rprimitive, int_rprimitive], - return_type=object_rprimitive, - c_function_name="CPyList_GetItemBorrow", - error_kind=ERR_MAGIC, - is_borrowed=True, - priority=3, -) - -# list[index] that produces a borrowed result and index is known to be short -method_op( - name="__getitem__", - arg_types=[list_rprimitive, short_int_rprimitive], - return_type=object_rprimitive, - c_function_name="CPyList_GetItemShortBorrow", - error_kind=ERR_MAGIC, - is_borrowed=True, - priority=4, -) - # Version with native int index method_op( name="__getitem__", @@ -140,16 +119,39 @@ priority=5, ) -# Version with native int index -method_op( - name="__getitem__", - arg_types=[list_rprimitive, int64_rprimitive], - return_type=object_rprimitive, - c_function_name="CPyList_GetItemInt64Borrow", - is_borrowed=True, - error_kind=ERR_MAGIC, - priority=6, -) +if not IS_FREE_THREADED: + # list[index] that produces a borrowed result + method_op( + name="__getitem__", + arg_types=[list_rprimitive, int_rprimitive], + return_type=object_rprimitive, + c_function_name="CPyList_GetItemBorrow", + error_kind=ERR_MAGIC, + is_borrowed=True, + priority=3, + ) + + # list[index] that produces a borrowed result and index is known to be short + method_op( + name="__getitem__", + arg_types=[list_rprimitive, short_int_rprimitive], + return_type=object_rprimitive, + c_function_name="CPyList_GetItemShortBorrow", + error_kind=ERR_MAGIC, + is_borrowed=True, + priority=4, + ) + + # Version with native int index + method_op( + name="__getitem__", + arg_types=[list_rprimitive, int64_rprimitive], + return_type=object_rprimitive, + c_function_name="CPyList_GetItemInt64Borrow", + is_borrowed=True, + error_kind=ERR_MAGIC, + priority=6, + ) # This is unsafe because it assumes that the index is a non-negative integer # that is in-bounds for the list. diff --git a/mypyc/test-data/exceptions.test b/mypyc/test-data/exceptions.test index c271c9ae63d5e..1fea2a1eb9203 100644 --- a/mypyc/test-data/exceptions.test +++ b/mypyc/test-data/exceptions.test @@ -98,7 +98,7 @@ L6: r5 = :: int return r5 -[case testListSum] +[case testListSum_withgil] from typing import List def sum(a: List[int], l: int) -> int: sum = 0 diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index a9d7ee2357b68..4e015c80a71d3 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -2114,7 +2114,7 @@ L0: r1 = CPyTagged_Multiply(4, r0) return r1 -[case testNativeIndex] +[case testNativeIndex_withgil] from typing import List class A: def __getitem__(self, index: int) -> int: pass diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index a7fdc09399009..0310d2f69d444 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1287,7 +1287,7 @@ L0: r2 = r1.x return r2 -[case testBorrowResultOfCustomGetItemInIfStatement] +[case testBorrowResultOfCustomGetItemInIfStatement_withgil] from typing import List class C: diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index 0fcadefd46c27..44157f5a820a0 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1136,7 +1136,7 @@ L0: keep_alive d, d return r4 -[case testBorrowOverI64ListGetItem1] +[case testBorrowOverI64ListGetItem1_withgil] from mypy_extensions import i64 def f(n: i64) -> str: @@ -1168,7 +1168,38 @@ L0: keep_alive a, n, r3 return r5 -[case testBorrowOverI64ListGetItem2] +[case testBorrowOverI64ListGetItem1_nogil] +from mypy_extensions import i64 + +def f(n: i64) -> str: + a = [C()] + return a[n].s + +class C: + s: str +[out] +def f(n): + n :: i64 + r0 :: __main__.C + r1 :: list + r2 :: ptr + a :: list + r3 :: object + r4 :: __main__.C + r5 :: str +L0: + r0 = C() + r1 = PyList_New(1) + r2 = list_items r1 + buf_init_item r2, 0, r0 + keep_alive r1 + a = r1 + r3 = CPyList_GetItemInt64(a, n) + r4 = cast(__main__.C, r3) + r5 = r4.s + return r5 + +[case testBorrowOverI64ListGetItem2_withgil] from typing import List from mypy_extensions import i64 @@ -1194,6 +1225,31 @@ L1: L2: return 0 +[case testBorrowOverI64ListGetItem2_nogil] +from typing import List +from mypy_extensions import i64 + +def f(a: List[i64], n: i64) -> bool: + if a[n] == 0: + return True + return False +[out] +def f(a, n): + a :: list + n :: i64 + r0 :: object + r1 :: i64 + r2 :: bit +L0: + r0 = CPyList_GetItemInt64(a, n) + r1 = unbox(i64, r0) + r2 = r1 == 0 + if r2 goto L1 else goto L2 :: bool +L1: + return 1 +L2: + return 0 + [case testCoerceShortIntToI64] from mypy_extensions import i64 from typing import List diff --git a/mypyc/test-data/irbuild-lists.test b/mypyc/test-data/irbuild-lists.test index f6cdbcaf3a96e..6050931497a7b 100644 --- a/mypyc/test-data/irbuild-lists.test +++ b/mypyc/test-data/irbuild-lists.test @@ -26,7 +26,7 @@ L0: r1 = cast(list, r0) return r1 -[case testListOfListGet2] +[case testListOfListGet2_withgil] from typing import List def f(x: List[List[int]]) -> int: return x[0][1] @@ -45,6 +45,24 @@ L0: keep_alive x, r0 return r3 +[case testListOfListGet2_nogil] +from typing import List +def f(x: List[List[int]]) -> int: + return x[0][1] +[out] +def f(x): + x :: list + r0 :: object + r1 :: list + r2 :: object + r3 :: int +L0: + r0 = CPyList_GetItemShort(x, 0) + r1 = cast(list, r0) + r2 = CPyList_GetItemShort(r1, 2) + r3 = unbox(int, r2) + return r3 + [case testListSet] from typing import List def f(x: List[int]) -> None: diff --git a/mypyc/test-data/refcount.test b/mypyc/test-data/refcount.test index cefa050e8f0e9..7d88cb7d11623 100644 --- a/mypyc/test-data/refcount.test +++ b/mypyc/test-data/refcount.test @@ -592,6 +592,28 @@ L0: dec_ref d return 1 +[case testBorrowListObjectWithLiteralIndex_nogil] +from typing import cast + +def f() -> str: + o = cast(object, []) + return cast(list[str], o)[0] +[out] +def f(): + r0 :: list + o :: object + r1 :: list + r2 :: object + r3 :: str +L0: + r0 = PyList_New(0) + o = r0 + r1 = borrow cast(list, o) + r2 = CPyList_GetItemShort(r1, 0) + r3 = cast(str, r2) + dec_ref o + return r3 + [case testUnaryBranchSpecialCase] def f(x: bool) -> int: if x: @@ -1178,7 +1200,7 @@ L0: r2 = cast(str, r1) return r2 -[case testBorrowListGetItem2] +[case testBorrowListGetItem2_withgil] from typing import List def attr_before_index(x: C) -> str: @@ -1228,6 +1250,58 @@ L0: r2 = r1.n return r2 +[case testBorrowListGetItem2_nogil] +from typing import List + +def attr_before_index(x: C) -> str: + return x.a[x.n] + +def attr_after_index(a: List[C], i: int) -> int: + return a[i].n + +def attr_after_index_literal(a: List[C]) -> int: + return a[0].n + +class C: + a: List[str] + n: int +[out] +def attr_before_index(x): + x :: __main__.C + r0 :: list + r1 :: int + r2 :: object + r3 :: str +L0: + r0 = borrow x.a + r1 = borrow x.n + r2 = CPyList_GetItem(r0, r1) + r3 = cast(str, r2) + return r3 +def attr_after_index(a, i): + a :: list + i :: int + r0 :: object + r1 :: __main__.C + r2 :: int +L0: + r0 = CPyList_GetItem(a, i) + r1 = cast(__main__.C, r0) + r2 = r1.n + dec_ref r1 + return r2 +def attr_after_index_literal(a): + a :: list + r0 :: object + r1 :: __main__.C + r2 :: int +L0: + r0 = CPyList_GetItemShort(a, 0) + r1 = cast(__main__.C, r0) + r2 = r1.n + dec_ref r1 + return r2 + [case testCannotBorrowListGetItem] from typing import List @@ -1257,7 +1331,7 @@ def f(): L0: return 0 -[case testBorrowListGetItemKeepAlive] +[case testBorrowListGetItemKeepAlive_withgil] from typing import List def f() -> str: diff --git a/mypyc/test/test_exceptions.py b/mypyc/test/test_exceptions.py index e9d4348eb64c5..d842ea1a85eda 100644 --- a/mypyc/test/test_exceptions.py +++ b/mypyc/test/test_exceptions.py @@ -11,7 +11,7 @@ from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase from mypyc.analysis.blockfreq import frequently_executed_blocks -from mypyc.common import TOP_LEVEL_NAME +from mypyc.common import IS_FREE_THREADED, TOP_LEVEL_NAME from mypyc.ir.pprint import format_func from mypyc.test.testutil import ( ICODE_GEN_BUILTINS, @@ -34,6 +34,14 @@ class TestExceptionTransform(MypycDataSuite): def run_case(self, testcase: DataDrivenTestCase) -> None: """Perform a runtime checking transformation test case.""" + + if "_withgil" in testcase.name and IS_FREE_THREADED: + # Test case should only run on a non-free-threaded build. + return + if "_nogil" in testcase.name and not IS_FREE_THREADED: + # Test case should only run on a free-threaded build. + return + with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase): expected_output = remove_comment_lines(testcase.output) try: diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index 7e3993e267e74..b11e425e51d3d 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -86,6 +86,9 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if "_withgil" in testcase.name and IS_FREE_THREADED: # Test case should only run on a non-free-threaded build. return + if "_nogil" in testcase.name and not IS_FREE_THREADED: + # Test case should only run on a free-threaded build. + return with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase): expected_output = remove_comment_lines(testcase.output) expected_output = replace_word_size(expected_output) diff --git a/mypyc/test/test_refcount.py b/mypyc/test/test_refcount.py index 7efa861e784fb..896ba14e2e68b 100644 --- a/mypyc/test/test_refcount.py +++ b/mypyc/test/test_refcount.py @@ -11,7 +11,7 @@ from mypy.errors import CompileError from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase -from mypyc.common import TOP_LEVEL_NAME +from mypyc.common import IS_FREE_THREADED, TOP_LEVEL_NAME from mypyc.ir.pprint import format_func from mypyc.test.testutil import ( ICODE_GEN_BUILTINS, @@ -40,6 +40,12 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if options is None: # Skipped test case return + if "_withgil" in testcase.name and IS_FREE_THREADED: + # Test case should only run on a non-free-threaded build. + return + if "_nogil" in testcase.name and not IS_FREE_THREADED: + # Test case should only run on a free-threaded build. + return with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase): expected_output = remove_comment_lines(testcase.output) expected_output = replace_word_size(expected_output) From 0f956541afff9e956ac1658f0f927192b8c13c6b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 15:06:12 +0100 Subject: [PATCH 101/127] [mypyc] Make librt tests resilient to cleaning all .so files (#21682) If `.so` files have been deleted, don't use cached librt build as it won't work. --- mypyc/test/librt_cache.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mypyc/test/librt_cache.py b/mypyc/test/librt_cache.py index f80059ed64786..bf57b9f54196e 100644 --- a/mypyc/test/librt_cache.py +++ b/mypyc/test/librt_cache.py @@ -162,8 +162,15 @@ def get_librt_path(experimental: bool = True, opt_level: str = "0") -> str: os.makedirs(cache_root, exist_ok=True) + binary_suffix = ".pyd" if sys.platform == "win32" else ".so" + with filelock.FileLock(lock_file, timeout=300): # 5 min timeout - if os.path.exists(marker): + # Reuse the cache only if the build completed *and* the compiled + # binaries still exist. A repo-wide clean of .so/.pyd files can delete + # the cached binaries while leaving the marker behind. + if os.path.exists(marker) and any( + f.endswith(binary_suffix) for f in os.listdir(os.path.join(build_dir, "librt")) + ): return build_dir # Clean up any partial build From cd747e557aaf5d96983e32dc69ebf84d6dfc99de Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 17:02:09 +0100 Subject: [PATCH 102/127] [mypyc] Make list get/set item more memory safe on free-threaded builds (#21683) Use the CPython C API for list get item and set item, as direct item access is not memory safe in the presence of race conditions on a free-threaded Python. Note that these operations are not necessarily atomic -- this only fixes memory safety. Explicit synchronization is still generally expected for correctness. This still doesn't cover for loops over lists. I will fix them in a follow-up PR. Preserve existing optimized primitives on non-free-threaded builds. Work on mypyc/mypyc#1202. --- mypyc/lib-rt/CPy.h | 60 ++++++++++++- mypyc/lib-rt/list_ops.c | 149 ++++++++++++++++++++------------- mypyc/lib-rt/mypyc_util.h | 4 + mypyc/test-data/run-lists.test | 62 ++++++++++++++ 4 files changed, 213 insertions(+), 62 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index c22c4162669bd..458db90efd530 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -664,16 +664,68 @@ PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end); // List operations -PyObject *CPyList_Build(Py_ssize_t len, ...); +#ifndef Py_GIL_DISABLED + PyObject *CPyList_GetItem(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index); +PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index); +bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value); +bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value); + +#else + +PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index); +bool CPyList_SetItem_(PyObject *list, CPyTagged index, PyObject *value); + +static inline PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { + if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) { + // Inlined fast path + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + return PyList_GetItemRef(list, n); + } else { + return CPyList_GetItem_(list, index); + } +} + +static inline PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + if (n < 0) { + n += PyList_GET_SIZE(list); + } + return PyList_GetItemRef(list, n); +} + +static inline PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { + if (index < 0) { + index += PyList_GET_SIZE(list); + } + return PyList_GetItemRef(list, index); +} + +static inline bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value) { + if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) { + // Inlined fast path + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + return PyList_SetItem(list, n, value) >= 0; + } else { + return CPyList_SetItem_(list, index, value); + } +} + +static inline bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value) { + if (index < 0) { + index += PyList_GET_SIZE(list); + } + return PyList_SetItem(list, index, value) >= 0; +} + +#endif + PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index); -PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index); PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index); -bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value); void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value); -bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value); +PyObject *CPyList_Build(Py_ssize_t len, ...); PyObject *CPyList_PopLast(PyObject *obj); PyObject *CPyList_Pop(PyObject *obj, CPyTagged index); CPyTagged CPyList_Count(PyObject *obj, PyObject *value); diff --git a/mypyc/lib-rt/list_ops.c b/mypyc/lib-rt/list_ops.c index 8b7eaa1acf3f0..e8fd061ada687 100644 --- a/mypyc/lib-rt/list_ops.c +++ b/mypyc/lib-rt/list_ops.c @@ -49,6 +49,8 @@ PyObject *CPyList_Copy(PyObject *list) { return PyObject_CallMethodNoArgs(list, mypyc_interned_str.copy); } +#ifndef Py_GIL_DISABLED + PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); Py_ssize_t size = PyList_GET_SIZE(list); @@ -69,24 +71,6 @@ PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) { return result; } -PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) { - Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); - Py_ssize_t size = PyList_GET_SIZE(list); - if (n >= 0) { - if (n >= size) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - } else { - n += size; - if (n < 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - } - return PyList_GET_ITEM(list, n); -} - PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); @@ -112,29 +96,6 @@ PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { } } -PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index) { - if (CPyTagged_CheckShort(index)) { - Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); - Py_ssize_t size = PyList_GET_SIZE(list); - if (n >= 0) { - if (n >= size) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - } else { - n += size; - if (n < 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - } - return PyList_GET_ITEM(list, n); - } else { - PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); - return NULL; - } -} - PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { size_t size = PyList_GET_SIZE(list); if (likely((uint64_t)index < size)) { @@ -156,23 +117,6 @@ PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { return result; } -PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index) { - size_t size = PyList_GET_SIZE(list); - if (likely((uint64_t)index < size)) { - return PyList_GET_ITEM(list, index); - } - if (index >= 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - index += size; - if (index < 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - return PyList_GET_ITEM(list, index); -} - bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); @@ -220,6 +164,95 @@ bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value) { return true; } +#else /* Py_GIL_DISABLED */ + +PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index) { + if (CPyTagged_CheckShort(index)) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + if (n < 0) { + n += PyList_GET_SIZE(list); + } + return PyList_GetItemRef(list, n); + } else { + PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); + return NULL; + } +} + +bool CPyList_SetItem_(PyObject *list, CPyTagged index, PyObject *value) { + if (CPyTagged_CheckShort(index)) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyList_GET_SIZE(list); + if (n < 0) { + n += size; + } + return PyList_SetItem(list, n, value) >= 0; + } else { + PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); + return false; + } +} + +#endif + +PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyList_GET_SIZE(list); + if (n >= 0) { + if (n >= size) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + } else { + n += size; + if (n < 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + } + return PyList_GET_ITEM(list, n); +} + +PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index) { + if (CPyTagged_CheckShort(index)) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyList_GET_SIZE(list); + if (n >= 0) { + if (n >= size) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + } else { + n += size; + if (n < 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + } + return PyList_GET_ITEM(list, n); + } else { + PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); + return NULL; + } +} + +PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index) { + size_t size = PyList_GET_SIZE(list); + if (likely((uint64_t)index < size)) { + return PyList_GET_ITEM(list, index); + } + if (index >= 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + index += size; + if (index < 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + return PyList_GET_ITEM(list, index); +} + // This function should only be used to fill in brand new lists. void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value) { PyList_SET_ITEM(list, index, value); diff --git a/mypyc/lib-rt/mypyc_util.h b/mypyc/lib-rt/mypyc_util.h index 4a4552d059b8c..d7a3eb3214bed 100644 --- a/mypyc/lib-rt/mypyc_util.h +++ b/mypyc/lib-rt/mypyc_util.h @@ -156,6 +156,10 @@ static inline CPyTagged CPyTagged_ShortFromSsize_t(Py_ssize_t x) { return x << 1; } +static inline int CPyTagged_IsNegative(CPyTagged x) { + return ((Py_ssize_t)x) < 0; +} + // Are we targeting Python 3.X or newer? #define CPY_3_11_FEATURES (PY_VERSION_HEX >= 0x030b0000) #define CPY_3_12_FEATURES (PY_VERSION_HEX >= 0x030c0000) diff --git a/mypyc/test-data/run-lists.test b/mypyc/test-data/run-lists.test index 40ca1b6e005f9..d06fc9ce10b0b 100644 --- a/mypyc/test-data/run-lists.test +++ b/mypyc/test-data/run-lists.test @@ -171,6 +171,68 @@ def test_list_build() -> None: l3.append('a') assert l3 == ['a'] +class C: + def __init__(self, s: str) -> None: + self.s = s + +a = [C("a"), C("b"), C("c")] + +def test_get_item() -> None: + zero = int() + assert a[zero].s == "a" + assert a[zero + 1].s == "b" + assert a[zero + 2].s == "c" + assert a[zero - 1].s == "c" + assert a[zero - 2].s == "b" + assert a[zero - 3].s == "a" + with assertRaises(IndexError): + a[zero + 3] + with assertRaises(IndexError): + a[zero - 4] + # TODO: Raise IndexError? + with assertRaises(OverflowError): + a[zero + 2**90] + with assertRaises(OverflowError): + a[zero - 2**90] + +def test_get_item_literal() -> None: + assert a[0].s == "a" + assert a[1].s == "b" + assert a[2].s == "c" + assert a[-1].s == "c" + assert a[-2].s == "b" + assert a[-3].s == "a" + with assertRaises(IndexError): + a[3] + with assertRaises(IndexError): + a[-4] + # TODO: Raise IndexError? + with assertRaises(OverflowError): + a[2**90] + with assertRaises(OverflowError): + a[-2**90] + +def test_set_item() -> None: + l = a.copy() + zero = int() + zero2 = int() + l[zero] = C("x") + assert l[zero2].s == "x" + l[zero + 2] = C("y") + assert l[zero2 + 2].s == "y" + l[zero - 1] = C("t") + assert l[zero2 - 1].s == "t" + l[zero - 3] = C("u") + assert l[zero2 - 3].s == "u" + with assertRaises(IndexError): + a[zero + 3] = C("z") + with assertRaises(IndexError): + a[zero - 4] = C("z") + with assertRaises(OverflowError): + a[zero + 2**90] = C("z") + with assertRaises(OverflowError): + a[zero - 2**90] = C("z") + def test_append() -> None: l = [1, 2] l.append(10) From 2f21f985ea7444083486febbcad1102dde8294d2 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 6 Jul 2026 17:48:50 +0100 Subject: [PATCH 103/127] [mypyc] Make multiple assignment from list memory-safe with free threading (#21684) Work on mypyc/mypyc#1202. --- mypyc/irbuild/builder.py | 13 +++++++-- mypyc/primitives/list_ops.py | 2 +- mypyc/test-data/irbuild-statements.test | 39 ++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 587be873a46d9..39e2c6aa5181f 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -62,6 +62,7 @@ BITMAP_BITS, EXT_SUFFIX, GENERATOR_ATTRIBUTE_PREFIX, + IS_FREE_THREADED, MODULE_PREFIX, SELF_NAME, TEMP_ATTR_NAME, @@ -146,7 +147,12 @@ from mypyc.options import CompilerOptions from mypyc.primitives.dict_ops import dict_get_item_op, dict_set_item_op from mypyc.primitives.generic_ops import iter_op, next_op, py_setattr_op -from mypyc.primitives.list_ops import list_get_item_unsafe_op, list_pop_last, to_list +from mypyc.primitives.list_ops import ( + list_get_item_int64_op, + list_get_item_unsafe_op, + list_pop_last, + to_list, +) from mypyc.primitives.misc_ops import ( check_unpack_count_op, get_module_dict_op, @@ -893,7 +899,10 @@ def process_sequence_assignment( index: Value if is_list_rprimitive(rvalue.type): index = Integer(i, c_pyssize_t_rprimitive) - item_value = self.primitive_op(list_get_item_unsafe_op, [rvalue, index], line) + if not IS_FREE_THREADED: + item_value = self.primitive_op(list_get_item_unsafe_op, [rvalue, index], line) + else: + item_value = self.primitive_op(list_get_item_int64_op, [rvalue, index], line) elif is_tuple_rprimitive(rvalue.type): index = Integer(i, c_pyssize_t_rprimitive) item_value = self.call_c(tuple_get_item_unsafe_op, [rvalue, index], line) diff --git a/mypyc/primitives/list_ops.py b/mypyc/primitives/list_ops.py index 25c9cd4b4c319..8af7e9e71df7e 100644 --- a/mypyc/primitives/list_ops.py +++ b/mypyc/primitives/list_ops.py @@ -110,7 +110,7 @@ ) # Version with native int index -method_op( +list_get_item_int64_op = method_op( name="__getitem__", arg_types=[list_rprimitive, int64_rprimitive], return_type=object_rprimitive, diff --git a/mypyc/test-data/irbuild-statements.test b/mypyc/test-data/irbuild-statements.test index df7eb5fc7449a..8199cfdaa699e 100644 --- a/mypyc/test-data/irbuild-statements.test +++ b/mypyc/test-data/irbuild-statements.test @@ -575,7 +575,7 @@ L0: z = r6 return 1 -[case testMultipleAssignmentUnpackFromSequence] +[case testMultipleAssignmentUnpackFromSequence_withgil] from typing import List, Tuple def f(l: List[int], t: Tuple[int, ...]) -> None: @@ -612,6 +612,43 @@ L0: y = r9 return 1 +[case testMultipleAssignmentUnpackFromSequence_nogil] +from typing import List, Tuple + +def f(l: List[int], t: Tuple[int, ...]) -> None: + x: object + y: int + x, y = l + x, y = t +[out] +def f(l, t): + l :: list + t :: tuple + r0 :: i32 + r1 :: bit + r2, r3, x :: object + r4, y :: int + r5 :: i32 + r6 :: bit + r7, r8 :: object + r9 :: int +L0: + r0 = CPySequence_CheckUnpackCount(l, 2) + r1 = r0 >= 0 :: signed + r2 = CPyList_GetItemInt64(l, 0) + r3 = CPyList_GetItemInt64(l, 1) + x = r2 + r4 = unbox(int, r3) + y = r4 + r5 = CPySequence_CheckUnpackCount(t, 2) + r6 = r5 >= 0 :: signed + r7 = CPySequenceTuple_GetItemUnsafe(t, 0) + r8 = CPySequenceTuple_GetItemUnsafe(t, 1) + x = r7 + r9 = unbox(int, r8) + y = r9 + return 1 + [case testAssert] from typing import Optional From d308a8dd60d92dbc0ac6e8443d0ef1a9807a21cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Mon, 6 Jul 2026 22:23:17 +0200 Subject: [PATCH 104/127] Comment spelling cleanup (#21676) Correct 3 misspellings. I've used https://github.com/crate-ci/typos to discover them, and my robobuddy to speed up work. --- CHANGELOG.md | 2 +- mypy/inspections.py | 2 +- mypy/plugins/dataclasses.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2cb8036dc71..2a2c9191b71a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -449,7 +449,7 @@ Contributed by Marc Mueller (PR [20156](https://github.com/python/mypy/pull/2015 For best performance, mypy can be compiled to C extension modules using mypyc. This makes mypy 3-5x faster than when interpreted with pure Python. We now build and upload mypyc accelerated mypy wheels for `win_arm64` and `cp314t-...` to PyPI, making it easy for Windows -users on ARM and those using the free theading builds for Python 3.14 to realise this speedup +users on ARM and those using the free threading builds for Python 3.14 to realise this speedup -- just `pip install` the latest mypy. Contributed by Marc Mueller diff --git a/mypy/inspections.py b/mypy/inspections.py index 1a869438101fe..6e2018262944c 100644 --- a/mypy/inspections.py +++ b/mypy/inspections.py @@ -297,7 +297,7 @@ def cmp_types(x: TypeInfo, y: TypeInfo) -> int: result = {} for base in sorted_bases: if not combined_attrs[base]: - # Skip bases where everytihng was filtered out. + # Skip bases where everything was filtered out. continue result[base] = combined_attrs[base] return result diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 5b39b11a623cc..790fe618c95c7 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -914,7 +914,7 @@ def _infer_dataclass_attr_init_type( # Perform a simple-minded inference from the signature of __set__, if present. # We can't use mypy.checkmember here, since this plugin runs before type checking. - # We only support some basic scanerios here, which is hopefully sufficient for + # We only support some basic scenarios here, which is hopefully sufficient for # the vast majority of use cases. if not isinstance(t, Instance): return default From a805f692014921d840e70ad8e511e3a6a10c28cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:48:48 -0700 Subject: [PATCH 105/127] Sync typeshed (#21665) Source commit: https://github.com/python/typeshed/commit/b9090e99745ac1511d8efd828622b11a9a3623e8 --- mypy/typeshed/stdlib/VERSIONS | 2 - mypy/typeshed/stdlib/_curses.pyi | 32 +++--- mypy/typeshed/stdlib/ast.pyi | 21 +++- mypy/typeshed/stdlib/builtins.pyi | 14 +-- mypy/typeshed/stdlib/fnmatch.pyi | 7 +- mypy/typeshed/stdlib/html/__init__.pyi | 3 + mypy/typeshed/stdlib/inspect.pyi | 10 +- .../profiling/sampling/gecko_collector.pyi | 102 +++++++++++++++++- mypy/typeshed/stdlib/types.pyi | 6 +- mypy/typeshed/stdlib/typing_extensions.pyi | 84 ++++++++++++--- 10 files changed, 229 insertions(+), 52 deletions(-) diff --git a/mypy/typeshed/stdlib/VERSIONS b/mypy/typeshed/stdlib/VERSIONS index e9c8d91fdbd71..96eb98131db63 100644 --- a/mypy/typeshed/stdlib/VERSIONS +++ b/mypy/typeshed/stdlib/VERSIONS @@ -245,8 +245,6 @@ posixpath: 3.0- pprint: 3.0- profile: 3.0- profiling: 3.15- -profiling.sampling: 3.15- -profiling.tracing: 3.15- pstats: 3.0- pty: 3.0- pwd: 3.0- diff --git a/mypy/typeshed/stdlib/_curses.pyi b/mypy/typeshed/stdlib/_curses.pyi index 449cf75dad422..fcd0da4c465c7 100644 --- a/mypy/typeshed/stdlib/_curses.pyi +++ b/mypy/typeshed/stdlib/_curses.pyi @@ -423,7 +423,7 @@ class window: # undocumented def chgat(self, y: int, x: int, num: int, attr: int) -> None: ... def clear(self) -> None: ... - def clearok(self, yes: int) -> None: ... + def clearok(self, flag: bool, /) -> None: ... def clrtobot(self) -> None: ... def clrtoeol(self) -> None: ... def cursyncup(self) -> None: ... @@ -480,9 +480,9 @@ class window: # undocumented @overload def hline(self, y: int, x: int, ch: _ChType, n: int) -> None: ... - def idcok(self, flag: bool) -> None: ... - def idlok(self, yes: bool) -> None: ... - def immedok(self, flag: bool) -> None: ... + def idcok(self, flag: bool, /) -> None: ... + def idlok(self, flag: bool, /) -> None: ... + def immedok(self, flag: bool, /) -> None: ... @overload def inch(self) -> int: ... @@ -494,7 +494,7 @@ class window: # undocumented @overload def insch(self, y: int, x: int, ch: _ChType, attr: int = ...) -> None: ... - def insdelln(self, nlines: int) -> None: ... + def insdelln(self, nlines: int, /) -> None: ... def insertln(self) -> None: ... @overload @@ -514,13 +514,13 @@ class window: # undocumented def is_linetouched(self, line: int, /) -> bool: ... def is_wintouched(self) -> bool: ... - def keypad(self, yes: bool, /) -> None: ... - def leaveok(self, yes: bool) -> None: ... - def move(self, new_y: int, new_x: int) -> None: ... - def mvderwin(self, y: int, x: int) -> None: ... - def mvwin(self, new_y: int, new_x: int) -> None: ... - def nodelay(self, yes: bool) -> None: ... - def notimeout(self, yes: bool) -> None: ... + def keypad(self, flag: bool, /) -> None: ... + def leaveok(self, flag: bool, /) -> None: ... + def move(self, new_y: int, new_x: int, /) -> None: ... + def mvderwin(self, y: int, x: int, /) -> None: ... + def mvwin(self, new_y: int, new_x: int, /) -> None: ... + def nodelay(self, flag: bool, /) -> None: ... + def notimeout(self, flag: bool, /) -> None: ... @overload def noutrefresh(self) -> None: ... @@ -550,9 +550,9 @@ class window: # undocumented @overload def refresh(self, pminrow: int, pmincol: int, sminrow: int, smincol: int, smaxrow: int, smaxcol: int) -> None: ... - def resize(self, nlines: int, ncols: int) -> None: ... + def resize(self, nlines: int, ncols: int, /) -> None: ... def scroll(self, lines: int = 1) -> None: ... - def scrollok(self, flag: bool) -> None: ... + def scrollok(self, flag: bool, /) -> None: ... def setscrreg(self, top: int, bottom: int, /) -> None: ... def standend(self) -> None: ... def standout(self) -> None: ... @@ -568,9 +568,9 @@ class window: # undocumented def subwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ... def syncdown(self) -> None: ... - def syncok(self, flag: bool) -> None: ... + def syncok(self, flag: bool, /) -> None: ... def syncup(self) -> None: ... - def timeout(self, delay: int) -> None: ... + def timeout(self, delay: int, /) -> None: ... def touchline(self, start: int, count: int, changed: bool = True) -> None: ... def touchwin(self) -> None: ... def untouchwin(self) -> None: ... diff --git a/mypy/typeshed/stdlib/ast.pyi b/mypy/typeshed/stdlib/ast.pyi index 14a98b9a5fcaf..a1993773ee25f 100644 --- a/mypy/typeshed/stdlib/ast.pyi +++ b/mypy/typeshed/stdlib/ast.pyi @@ -956,14 +956,27 @@ class DictComp(expr): else: value: expr generators: list[comprehension] - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 15): + def __init__( + self, key: expr, value: expr | None = None, generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes] + ) -> None: ... + elif sys.version_info >= (3, 13): def __init__( self, key: expr, value: expr, generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes] ) -> None: ... else: def __init__(self, key: expr, value: expr, generators: list[comprehension], **kwargs: Unpack[_Attributes]) -> None: ... - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 15): + def __replace__( + self, + *, + key: expr = ..., + value: expr | None = ..., + generators: list[comprehension] = ..., + **kwargs: Unpack[_Attributes], + ) -> Self: ... + elif sys.version_info >= (3, 14): def __replace__( self, *, key: expr = ..., value: expr = ..., generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... @@ -2147,6 +2160,10 @@ class NodeVisitor: def visit_TypeVarTuple(self, node: TypeVarTuple) -> Any: ... def visit_TypeAlias(self, node: TypeAlias) -> Any: ... + if sys.version_info >= (3, 14): + def visit_TemplateStr(self, node: TemplateStr) -> Any: ... + def visit_Interpolation(self, node: Interpolation) -> Any: ... + # visit methods for deprecated nodes def visit_ExtSlice(self, node: ExtSlice) -> Any: ... def visit_Index(self, node: Index) -> Any: ... diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi index d773f98e90b6b..7b659e5476d59 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi @@ -33,7 +33,7 @@ from _typeshed import ( from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from os import PathLike -from types import CellType, CodeType, EllipsisType, GenericAlias, NotImplementedType, TracebackType +from types import CellType, CodeType, EllipsisType, GenericAlias, NotImplementedType, TracebackType, UnionType # mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} # are imported from collections.abc in builtins.pyi @@ -2080,11 +2080,13 @@ if sys.version_info >= (3, 15): class sentinel: __name__: str __module__: str - def __new__(cls, name: str, /, *, repr: str | None = None) -> Self: ... - def __copy__(self, /) -> Self: ... - def __deepcopy__(self, memo: Any, /) -> Self: ... - def __or__(self, other: Any, /) -> Any: ... - def __ror__(self, other: Any, /) -> Any: ... + def __new__(cls, name: str, /, *, repr: str | None = None) -> sentinel: ... + def __copy__(self, /) -> sentinel: ... + def __deepcopy__(self, memo: Any, /) -> sentinel: ... + # `other` can be any legal form for unions. + # `x | x` creates a `sentinel` instance if `x` is a sentinel, not a `UnionType` instance. + def __or__(self, other: Any, /) -> UnionType | sentinel: ... + def __ror__(self, other: Any, /) -> UnionType | sentinel: ... @overload def sorted( diff --git a/mypy/typeshed/stdlib/fnmatch.pyi b/mypy/typeshed/stdlib/fnmatch.pyi index 345c4576497de..018139dc482bf 100644 --- a/mypy/typeshed/stdlib/fnmatch.pyi +++ b/mypy/typeshed/stdlib/fnmatch.pyi @@ -1,15 +1,16 @@ import sys from collections.abc import Iterable +from os import PathLike from typing import AnyStr __all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] if sys.version_info >= (3, 14): __all__ += ["filterfalse"] -def fnmatch(name: AnyStr, pat: AnyStr) -> bool: ... +def fnmatch(name: AnyStr | PathLike[AnyStr], pat: AnyStr | PathLike[AnyStr]) -> bool: ... def fnmatchcase(name: AnyStr, pat: AnyStr) -> bool: ... -def filter(names: Iterable[AnyStr], pat: AnyStr) -> list[AnyStr]: ... +def filter(names: Iterable[AnyStr | PathLike[AnyStr]], pat: AnyStr | PathLike[AnyStr]) -> list[AnyStr]: ... def translate(pat: str) -> str: ... if sys.version_info >= (3, 14): - def filterfalse(names: Iterable[AnyStr], pat: AnyStr) -> list[AnyStr]: ... + def filterfalse(names: Iterable[AnyStr | PathLike[AnyStr]], pat: AnyStr | PathLike[AnyStr]) -> list[AnyStr]: ... diff --git a/mypy/typeshed/stdlib/html/__init__.pyi b/mypy/typeshed/stdlib/html/__init__.pyi index 8ad72f1265882..71e971d150449 100644 --- a/mypy/typeshed/stdlib/html/__init__.pyi +++ b/mypy/typeshed/stdlib/html/__init__.pyi @@ -1,4 +1,7 @@ +import re + __all__ = ["escape", "unescape"] def escape(s: str, quote: bool = True) -> str: ... def unescape(s: str) -> str: ... +def _replace_charref(s: re.Match[str]) -> str: ... diff --git a/mypy/typeshed/stdlib/inspect.pyi b/mypy/typeshed/stdlib/inspect.pyi index 003ecc10f072e..1bd2b9dee325b 100644 --- a/mypy/typeshed/stdlib/inspect.pyi +++ b/mypy/typeshed/stdlib/inspect.pyi @@ -38,7 +38,7 @@ from typing import ( overload, type_check_only, ) -from typing_extensions import Self, TypeIs, deprecated, disjoint_base +from typing_extensions import Never, Self, TypeIs, deprecated, disjoint_base if sys.version_info >= (3, 14): from annotationlib import Format @@ -217,7 +217,7 @@ if sys.version_info >= (3, 11): def getmodulename(path: StrPath) -> str | None: ... def ismodule(object: object) -> TypeIs[ModuleType]: ... -def isclass(object: object) -> TypeIs[type[Any]]: ... +def isclass(object: object) -> TypeIs[type[object]]: ... def ismethod(object: object) -> TypeIs[MethodType]: ... if sys.version_info >= (3, 14): @@ -245,7 +245,7 @@ def iscoroutinefunction(obj: Callable[_P, object]) -> TypeGuard[Callable[_P, Cor @overload def iscoroutinefunction(obj: object) -> TypeGuard[Callable[..., CoroutineType[Any, Any, Any]]]: ... -def isgenerator(object: object) -> TypeIs[GeneratorType[Any, Any, Any]]: ... +def isgenerator(object: object) -> TypeIs[GeneratorType[object, Never, object]]: ... def iscoroutine(object: object) -> TypeIs[CoroutineType[Any, Any, Any]]: ... def isawaitable(object: object) -> TypeIs[Awaitable[Any]]: ... @@ -264,7 +264,7 @@ class _SupportsSet(Protocol[_T_contra, _V_contra]): class _SupportsDelete(Protocol[_T_contra]): def __delete__(self, instance: _T_contra, /) -> None: ... -def isasyncgen(object: object) -> TypeIs[AsyncGeneratorType[Any, Any]]: ... +def isasyncgen(object: object) -> TypeIs[AsyncGeneratorType[object, Never]]: ... def istraceback(object: object) -> TypeIs[TracebackType]: ... def isframe(object: object) -> TypeIs[FrameType]: ... def iscode(object: object) -> TypeIs[CodeType]: ... @@ -289,7 +289,7 @@ def ismethoddescriptor(object: object) -> TypeIs[MethodDescriptorType]: ... def ismemberdescriptor(object: object) -> TypeIs[MemberDescriptorType]: ... def isabstract(object: object) -> bool: ... def isgetsetdescriptor(object: object) -> TypeIs[GetSetDescriptorType]: ... -def isdatadescriptor(object: object) -> TypeIs[_SupportsSet[Any, Any] | _SupportsDelete[Any]]: ... +def isdatadescriptor(object: object) -> TypeIs[_SupportsSet[Never, Never] | _SupportsDelete[Never]]: ... # # Retrieving source code diff --git a/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi b/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi index 6072d4f2359af..666fd42c3ea5f 100644 --- a/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi +++ b/mypy/typeshed/stdlib/profiling/sampling/gecko_collector.pyi @@ -1,11 +1,109 @@ -from _typeshed import StrOrBytesPath -from collections.abc import Sequence +from _typeshed import Incomplete, StrOrBytesPath, StrPath +from collections.abc import Generator, Sequence +from tempfile import TemporaryDirectory +from typing import Any, ClassVar, Final, TypedDict, type_check_only from _remote_debugging import AwaitedInfo, InterpreterInfo from .collector import Collector, _Timestamps +@type_check_only +class _GeckoCategory(TypedDict): + name: str + color: str + subcategories: list[str] + +THREAD_STATUS_HAS_GIL: Final[int] +THREAD_STATUS_ON_CPU: Final[int] +THREAD_STATUS_UNKNOWN: Final[int] +THREAD_STATUS_GIL_REQUESTED: Final[int] +THREAD_STATUS_HAS_EXCEPTION: Final[int] +THREAD_STATUS_MAIN_THREAD: Final[int] + +GECKO_CATEGORIES: Final[list[_GeckoCategory]] + +CATEGORY_OTHER: Final = 0 +CATEGORY_PYTHON: Final = 1 +CATEGORY_NATIVE: Final = 2 +CATEGORY_GC: Final = 3 +CATEGORY_GIL: Final = 4 +CATEGORY_CPU: Final = 5 +CATEGORY_CODE_TYPE: Final = 6 +CATEGORY_OPCODES: Final = 7 +CATEGORY_EXCEPTION: Final = 8 + +DEFAULT_SUBCATEGORY: Final = 0 + +GECKO_FORMAT_VERSION: Final = 32 +GECKO_PREPROCESSED_VERSION: Final = 57 + +RESOURCE_TYPE_LIBRARY: Final = 1 + +FRAME_ADDRESS_NONE: Final = -1 +FRAME_INLINE_DEPTH_ROOT: Final = 0 + +PROCESS_TYPE_MAIN: Final = 0 +STACKWALK_DISABLED: Final = 0 + +DEFAULT_SPILL_BUFFER_BYTES: Final[int] + +class SpillColumn: + path: str + buffer: bytearray + + def __init__(self, directory: StrPath, basename: StrPath, *, buffer_bytes: int | None = None) -> None: ... + # "value" accepts the same types as json.JSONEncoder.encode() + def append(self, value: Any) -> None: ... + def flush(self) -> None: ... + def iter_tokens(self) -> Generator[str]: ... + +class GeckoThreadSpill: + sample_count: int + marker_count: int + def __init__(self, directory: StrPath, tid: int) -> None: ... + def append_sample(self, stack_index: int, time_ms: float) -> None: ... + def append_marker( + self, name_idx: int, start_time: float, end_time: float, phase: int, category: int, data: dict[str, Any] + ) -> None: ... + def prepare_read(self) -> None: ... + class GeckoCollector(Collector): + aggregating: ClassVar[bool] + + sample_interval_usec: int + skip_idle: bool + opcodes_enabled: bool + start_time: float + + global_strings: list[str] + global_string_map: dict[str, int] + + threads: dict[int, dict[str, Any]] + spill_dir: TemporaryDirectory[str] | None + exported: bool + + libs: list[Incomplete] + + sample_count: int + last_sample_time: float + interval: float + + has_gil_start: dict[Incomplete, Incomplete] + no_gil_start: dict[Incomplete, Incomplete] + on_cpu_start: dict[Incomplete, Incomplete] + off_cpu_start: dict[Incomplete, Incomplete] + python_code_start: dict[Incomplete, Incomplete] + native_code_start: dict[Incomplete, Incomplete] + gil_wait_start: dict[Incomplete, Incomplete] + exception_start: dict[Incomplete, Incomplete] + no_exception_start: dict[Incomplete, Incomplete] + + gc_start_per_thread: dict[int, float] + + initialized_threads: set[Incomplete] + + opcode_state: dict[int, tuple[Incomplete, int, int, str, str, float]] + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False, opcodes: bool = False) -> None: ... def collect( self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None diff --git a/mypy/typeshed/stdlib/types.pyi b/mypy/typeshed/stdlib/types.pyi index b9771ffc72dad..68b6b3fbe41d7 100644 --- a/mypy/typeshed/stdlib/types.pyi +++ b/mypy/typeshed/stdlib/types.pyi @@ -703,8 +703,10 @@ class GenericAlias: @property def __typing_unpacked_tuple_args__(self) -> tuple[Any, ...] | None: ... - def __or__(self, value: Any, /) -> UnionType: ... - def __ror__(self, value: Any, /) -> UnionType: ... + # `other` can be any legal form for unions. + # `list[int] | list[int]` creates a `GenericAlias` instance, not a `UnionType` instance + def __or__(self, value: Any, /) -> UnionType | GenericAlias: ... + def __ror__(self, value: Any, /) -> UnionType | GenericAlias: ... # GenericAlias delegates attr access to `__origin__` def __getattr__(self, name: str) -> Any: ... diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index fdbba495c579f..80341175e6e14 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -59,7 +59,6 @@ from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 Tuple as Tuple, Type as Type, TypeAlias as TypeAlias, - TypedDict as TypedDict, TypeGuard as TypeGuard, TypeVar as _TypeVar, Union as Union, @@ -72,6 +71,9 @@ from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 type_check_only, ) +if sys.version_info >= (3, 14): + from _typeshed import EvaluateFunc + # Please keep order the same as at runtime. __all__ = [ # Super-special typing primitives. @@ -143,6 +145,7 @@ __all__ = [ "override", "Protocol", "Sentinel", + "sentinel", "reveal_type", "runtime", "runtime_checkable", @@ -232,6 +235,11 @@ Literal: _SpecialForm def IntVar(name: str) -> Any: ... # returns a new TypeVar +# Kept as a distinct symbol to `typing.TypedDict` so that type checkers can more easily +# distinguish between the two on Python 3.14, on which `typing_extensions.TypedDict` +# exposes `__closed__` and `__extra_items__` but `typing.TypedDict` does not +TypedDict: _SpecialForm + # Internal mypy fallback type for all typed dicts (does not exist at runtime) # N.B. Keep this mostly in sync with typing._TypedDict/mypy_extensions._TypedDict @type_check_only @@ -456,7 +464,6 @@ if sys.version_info >= (3, 13): ReadOnly as ReadOnly, TypeIs as TypeIs, TypeVar as TypeVar, - TypeVarTuple as TypeVarTuple, get_protocol_members as get_protocol_members, is_protocol as is_protocol, ) @@ -545,19 +552,58 @@ else: def has_default(self) -> bool: ... def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... + ReadOnly: _SpecialForm + TypeIs: _SpecialForm + +if sys.version_info >= (3, 15): + from typing import TypeVarTuple as TypeVarTuple +else: @final class TypeVarTuple: @property def __name__(self) -> str: ... @property + def __bound__(self) -> AnnotationForm | None: ... + @property + def __covariant__(self) -> bool: ... + @property + def __contravariant__(self) -> bool: ... + @property + def __infer_variance__(self) -> bool: ... + @property def __default__(self) -> AnnotationForm: ... - def __init__(self, name: str, *, default: AnnotationForm = ...) -> None: ... + if sys.version_info >= (3, 11): + def __new__( + cls, + name: str, + *, + bound: AnnotationForm | None = None, + covariant: bool = False, + contravariant: bool = False, + infer_variance: bool = False, + default: AnnotationForm = ..., + ) -> Self: ... + else: + def __init__( + self, + name: str, + *, + bound: AnnotationForm | None = None, + covariant: bool = False, + contravariant: bool = False, + infer_variance: bool = False, + default: AnnotationForm = ..., + ) -> None: ... + def __iter__(self) -> Any: ... # Unpack[Self] def has_default(self) -> bool: ... - def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... + if sys.version_info >= (3, 11): + def __typing_subst__(self, arg: Never, /) -> Never: ... - ReadOnly: _SpecialForm - TypeIs: _SpecialForm + def __typing_prepare_subst__(self, alias: Any, args: Any, /) -> tuple[Any, ...]: ... + if sys.version_info >= (3, 14): + @property + def evaluate_default(self) -> EvaluateFunc | None: ... # TypeAliasType was added in Python 3.12, but had significant changes in 3.14. if sys.version_info >= (3, 14): @@ -680,11 +726,21 @@ else: def type_repr(value: object) -> str: ... # PEP 661 -class Sentinel: - def __init__(self, name: str, repr: str | None = None) -> None: ... - if sys.version_info >= (3, 14): - def __or__(self, other: Any) -> UnionType: ... # other can be any type form legal for unions - def __ror__(self, other: Any) -> UnionType: ... # other can be any type form legal for unions - else: - def __or__(self, other: Any) -> _SpecialForm: ... # other can be any type form legal for unions - def __ror__(self, other: Any) -> _SpecialForm: ... # other can be any type form legal for unions +if sys.version_info >= (3, 15): + from builtins import sentinel as sentinel +else: + class sentinel: + def __init__(self, name: str, /, *, repr: str | None = None) -> None: ... + __name__: str + __module__: str + if sys.version_info >= (3, 14): + # `other`` can be any type form legal for unions. + # `x | x` creates a `sentinel` instance if `x` is a sentinel, not a `UnionType` instance + def __or__(self, other: Any) -> UnionType | sentinel: ... + def __ror__(self, other: Any) -> UnionType | sentinel: ... + else: + # other can be any type form legal for unions + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... + +Sentinel = sentinel From 523acf6d19bcf45bb4189cc3542c9a7f8fa6f3d3 Mon Sep 17 00:00:00 2001 From: esarp <11684270+esarp@users.noreply.github.com> Date: Tue, 7 Jul 2026 04:36:30 -0500 Subject: [PATCH 106/127] Update version in prep for new release (#21685) --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index a33ca938708f4..13a4418314c25 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "2.2.0+dev" +__version__ = "2.3.0+dev" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) From c07d7888e7b61983924632036a864c4d210848fe Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 10:37:11 +0100 Subject: [PATCH 107/127] [mypyc] Make for loop over list memory-safe on free-threaded builds (#21686) Don't access list items directly but use CPython C API functions for item access. Also, reading the list length should use an atomic relaxed memory order read, but changing this is lower priority and won't be fixed until later. There is no change on non-free-threaded Python builds. Work on mypyc/mypyc#1202. --- mypyc/irbuild/for_helpers.py | 13 ++++++-- mypyc/test-data/irbuild-basic.test | 6 ++-- mypyc/test-data/irbuild-lists.test | 8 ++--- mypyc/test-data/irbuild-set.test | 4 +-- mypyc/test-data/irbuild-statements.test | 43 +++++++++++++++++++++++-- mypyc/test-data/irbuild-tuple.test | 4 +-- mypyc/test-data/irbuild-vec-i64.test | 4 +-- mypyc/test-data/lowering-int.test | 2 +- mypyc/test/test_lowering.py | 8 ++++- 9 files changed, 72 insertions(+), 20 deletions(-) diff --git a/mypyc/irbuild/for_helpers.py b/mypyc/irbuild/for_helpers.py index 95894dcedaba8..fc539a1fc090b 100644 --- a/mypyc/irbuild/for_helpers.py +++ b/mypyc/irbuild/for_helpers.py @@ -28,6 +28,7 @@ Var, ) from mypy.types import LiteralType, TupleType, get_proper_type, get_proper_types +from mypyc.common import IS_FREE_THREADED from mypyc.ir.ops import ( ERR_NEVER, BasicBlock, @@ -81,7 +82,12 @@ ) from mypyc.primitives.exc_ops import no_err_occurred_op, propagate_if_error_op from mypyc.primitives.generic_ops import aiter_op, anext_op, iter_op, next_op -from mypyc.primitives.list_ops import list_append_op, list_get_item_unsafe_op, new_list_set_item_op +from mypyc.primitives.list_ops import ( + list_append_op, + list_get_item_int64_op, + list_get_item_unsafe_op, + new_list_set_item_op, +) from mypyc.primitives.misc_ops import stop_async_iteration_op from mypyc.primitives.registry import CFunctionDescription from mypyc.primitives.set_ops import set_add_op @@ -866,7 +872,10 @@ def unsafe_index(builder: IRBuilder, target: Value, index: Value, line: int) -> # since we want to use __getitem__ if we don't have an unsafe version, # so we just check manually. if is_list_rprimitive(target.type): - return builder.primitive_op(list_get_item_unsafe_op, [target, index], line) + if not IS_FREE_THREADED: + return builder.primitive_op(list_get_item_unsafe_op, [target, index], line) + else: + return builder.primitive_op(list_get_item_int64_op, [target, index], line) elif is_tuple_rprimitive(target.type): return builder.call_c(tuple_get_item_unsafe_op, [target, index], line) elif is_str_rprimitive(target.type): diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 4e015c80a71d3..e0c86e4cfb93f 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -1870,7 +1870,7 @@ L0: r5 = a.f(12, 6, r4) return 1 -[case testListComprehension] +[case testListComprehension_withgil] from typing import List def f() -> List[int]: @@ -1931,7 +1931,7 @@ L7: L8: return r0 -[case testDictComprehension] +[case testDictComprehension_withgil] from typing import Dict def f() -> Dict[int, int]: return {x: x*x for x in [1,2,3] if x != 2 if x != 3} @@ -1993,7 +1993,7 @@ L7: L8: return r0 -[case testLoopsMultipleAssign] +[case testLoopsMultipleAssign_withgil] from typing import List, Tuple def f(l: List[Tuple[int, int, int]]) -> List[int]: for x, y, z in l: diff --git a/mypyc/test-data/irbuild-lists.test b/mypyc/test-data/irbuild-lists.test index 6050931497a7b..e70d81c96bf7a 100644 --- a/mypyc/test-data/irbuild-lists.test +++ b/mypyc/test-data/irbuild-lists.test @@ -379,7 +379,7 @@ L0: r2 = r1 >= 0 :: signed return 1 -[case testListBuiltFromGenerator] +[case testListBuiltFromGenerator_withgil] from typing import List def f(source: List[int]) -> None: a = list(x + 1 for x in source) @@ -448,7 +448,7 @@ L8: b = r11 return 1 -[case testGeneratorNext] +[case testGeneratorNext_withgil] from typing import List, Optional def test(x: List[int]) -> None: @@ -489,7 +489,7 @@ L5: res = r6 return 1 -[case testSimplifyListUnion] +[case testSimplifyListUnion_withgil] from typing import List, Union, Optional def narrow(a: Union[List[str], List[bytes], int]) -> int: @@ -923,7 +923,7 @@ L10: a = r3 return 1 -[case testListBuiltFromStars] +[case testListBuiltFromStars_withgil] from typing import Final abc: Final = "abc" diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index 00bcff68d4ff5..ac01c58401118 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -53,7 +53,7 @@ L0: r0 = PySet_New(l) return r0 -[case testNewSetFromIterable2] +[case testNewSetFromIterable2_withgil] def f(x: int) -> int: return x @@ -273,7 +273,7 @@ L4: e = r0 return 1 -[case testNewSetFromIterable3] +[case testNewSetFromIterable3_withgil] def f1(x: int) -> int: return x diff --git a/mypyc/test-data/irbuild-statements.test b/mypyc/test-data/irbuild-statements.test index 8199cfdaa699e..8c5c95c8223ec 100644 --- a/mypyc/test-data/irbuild-statements.test +++ b/mypyc/test-data/irbuild-statements.test @@ -218,7 +218,7 @@ L5: L6: return 1 -[case testForList] +[case testForList_withgil] from typing import List def f(ls: List[int]) -> int: @@ -255,6 +255,43 @@ L3: L4: return y +[case testForList_nogil] +from typing import List + +def f(ls: List[int]) -> int: + y = 0 + for x in ls: + y = y + x + return y +[out] +def f(ls): + ls :: list + y :: int + r0, r1 :: native_int + r2 :: bit + r3 :: object + r4, x, r5 :: int + r6 :: native_int +L0: + y = 0 + r0 = 0 +L1: + r1 = var_object_size ls + r2 = r0 < r1 :: signed + if r2 goto L2 else goto L4 :: bool +L2: + r3 = CPyList_GetItemInt64(ls, r0) + r4 = unbox(int, r3) + x = r4 + r5 = CPyTagged_Add(y, x) + y = r5 +L3: + r6 = r0 + 1 + r0 = r6 + goto L1 +L4: + return y + [case testForDictBasic] from typing import Dict @@ -899,7 +936,7 @@ L0: r6 = r5 >= 0 :: signed return 1 -[case testForEnumerate] +[case testForEnumerate_withgil] from typing import List, Iterable def f(a: List[int]) -> None: @@ -967,7 +1004,7 @@ L4: L5: return 1 -[case testForZip] +[case testForZip_withgil] from typing import List, Iterable, Sequence def f(a: List[int], b: Sequence[bool]) -> None: diff --git a/mypyc/test-data/irbuild-tuple.test b/mypyc/test-data/irbuild-tuple.test index 9f6b0f5d07390..808b5b9e0d4ab 100644 --- a/mypyc/test-data/irbuild-tuple.test +++ b/mypyc/test-data/irbuild-tuple.test @@ -281,7 +281,7 @@ L5: L6: return r6 -[case testTupleBuiltFromList] +[case testTupleBuiltFromList_withgil] def f(val: int) -> bool: return val % 2 == 0 @@ -1018,7 +1018,7 @@ L5: a = r42 return 1 -[case testTupleBuiltFromStars] +[case testTupleBuiltFromStars_withgil] from typing import Final abc: Final = "abc" diff --git a/mypyc/test-data/irbuild-vec-i64.test b/mypyc/test-data/irbuild-vec-i64.test index af69f30924d46..176e2616a4856 100644 --- a/mypyc/test-data/irbuild-vec-i64.test +++ b/mypyc/test-data/irbuild-vec-i64.test @@ -334,7 +334,7 @@ L3: L4: return r1 -[case testVecI64FastComprehensionFromList] +[case testVecI64FastComprehensionFromList_nogil] from librt.vecs import vec from mypy_extensions import i64 from typing import List @@ -364,7 +364,7 @@ L1: r4 = r2 < r3 :: signed if r4 goto L2 else goto L4 :: bool L2: - r5 = list_get_item_unsafe l, r2 + r5 = CPyList_GetItemInt64(l, r2) r6 = unbox(i64, r5) x = r6 r7 = x + 1 diff --git a/mypyc/test-data/lowering-int.test b/mypyc/test-data/lowering-int.test index c2bcba54e444d..72c4a82dcaea3 100644 --- a/mypyc/test-data/lowering-int.test +++ b/mypyc/test-data/lowering-int.test @@ -332,7 +332,7 @@ L4: L5: return 4 -[case testLowerIntForLoop_64bit] +[case testLowerIntForLoop_withgil_64bit] from __future__ import annotations def f(l: list[int]) -> None: diff --git a/mypyc/test/test_lowering.py b/mypyc/test/test_lowering.py index 3eb4698c68793..75351cd50e4a7 100644 --- a/mypyc/test/test_lowering.py +++ b/mypyc/test/test_lowering.py @@ -7,7 +7,7 @@ from mypy.errors import CompileError from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase -from mypyc.common import TOP_LEVEL_NAME +from mypyc.common import IS_FREE_THREADED, TOP_LEVEL_NAME from mypyc.ir.pprint import format_func from mypyc.options import CompilerOptions from mypyc.test.testutil import ( @@ -36,6 +36,12 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if options is None: # Skipped test case return + if "_withgil" in testcase.name and IS_FREE_THREADED: + # Test case should only run on a non-free-threaded build. + return + if "_nogil" in testcase.name and not IS_FREE_THREADED: + # Test case should only run on a free-threaded build. + return with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase): expected_output = remove_comment_lines(testcase.output) expected_output = replace_word_size(expected_output) From cfc07346408d92ac91c18e3698c74b9c0d3a4a65 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 12:39:10 +0100 Subject: [PATCH 108/127] [mypyc] Fix unsafe borrowing of instance attributes with free-threading (#21688) Don't borrow native attributes any more, except for final native attributes (these can't be rebound, so they are safe to borrow), and attributes with `vec` types (`vec` requires explicit synchronization by design). Work on mypyc/mypyc#1203. I used coding agent assist for this, but manually reviewed changes. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypyc/irbuild/builder.py | 16 ++ mypyc/irbuild/expression.py | 11 +- mypyc/test-data/exceptions.test | 28 ++- mypyc/test-data/irbuild-basic.test | 2 +- mypyc/test-data/irbuild-classes.test | 24 ++- mypyc/test-data/irbuild-generics.test | 2 +- mypyc/test-data/irbuild-glue-methods.test | 2 +- mypyc/test-data/irbuild-i64.test | 6 +- mypyc/test-data/irbuild-isinstance.test | 2 +- mypyc/test-data/irbuild-optional.test | 2 +- mypyc/test-data/irbuild-vec-t.test | 12 +- mypyc/test-data/opt-copy-propagation.test | 2 +- mypyc/test-data/refcount.test | 211 ++++++++++++++++++++-- mypyc/test/test_optimizations.py | 8 +- 14 files changed, 294 insertions(+), 34 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 39e2c6aa5181f..56a3f944fe77f 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -1565,6 +1565,22 @@ def is_native_attr_ref(self, expr: MemberExpr) -> bool: and any(expr.name in ir.attributes for ir in obj_rtype.class_ir.mro) ) + def is_final_native_attr_ref(self, expr: MemberExpr) -> bool: + """Is expr a direct reference to a Final native (struct) attribute of an instance? + + A Final attribute is read-only at runtime (it has no setter), so it can never be + reassigned after construction. This makes it safe to borrow even on free-threaded + builds, since no concurrent store can invalidate the borrowed reference. + """ + obj_rtype = self.node_type(expr.expr) + if not (isinstance(obj_rtype, RInstance) and obj_rtype.class_ir.is_ext_class): + return False + # Find the class that defines the attribute and check whether it's Final there. + for ir in obj_rtype.class_ir.mro: + if expr.name in ir.attributes: + return expr.name in ir.final_attributes + return False + def mark_block_unreachable(self) -> None: """Mark statements in the innermost block being processed as unreachable. diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 21cb2f8df1b8f..b99e8161c08c5 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -272,9 +272,16 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value: if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports: return builder.load_module(expr.node.fullname) - can_borrow = builder.is_native_attr_ref(expr) - obj = builder.accept(expr.expr, can_borrow=can_borrow) rtype = builder.node_type(expr) + # Borrowing a native attribute read is unsafe on free-threaded builds, since another + # thread could concurrently reassign the attribute and free the old value. We still borrow + # in two cases: + # - Native Final attributes are read-only at runtime, so they can never be reassigned. + # - Vec-typed attributes require manual synchronization, so we borrow them liberally. + can_borrow = builder.is_native_attr_ref(expr) and ( + not IS_FREE_THREADED or isinstance(rtype, RVec) or builder.is_final_native_attr_ref(expr) + ) + obj = builder.accept(expr.expr, can_borrow=can_borrow) if ( is_object_rprimitive(obj.type) diff --git a/mypyc/test-data/exceptions.test b/mypyc/test-data/exceptions.test index 1fea2a1eb9203..b8d8b4727363c 100644 --- a/mypyc/test-data/exceptions.test +++ b/mypyc/test-data/exceptions.test @@ -553,7 +553,7 @@ L3: r3 = :: i64 return r3 -[case testExceptionWithNativeAttributeGetAndSet] +[case testExceptionWithNativeAttributeGetAndSet_withgil] class C: def __init__(self, x: int) -> None: self.x = x @@ -578,6 +578,32 @@ L0: c.x = r1 return 1 +[case testExceptionWithNativeAttributeGetAndSet_nogil] +class C: + def __init__(self, x: int) -> None: + self.x = x + +def foo(c: C, x: int) -> None: + c.x = x - c.x +[out] +def C.__init__(self, x): + self :: __main__.C + x :: int +L0: + inc_ref x :: int + self.x = x + return 1 +def foo(c, x): + c :: __main__.C + x, r0, r1 :: int + r2 :: bool +L0: + r0 = c.x + r1 = CPyTagged_Subtract(x, r0) + dec_ref r0 :: int + c.x = r1 + return 1 + [case testExceptionWithOverlappingFloatErrorValue] def f() -> float: return 0.0 diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index e0c86e4cfb93f..c6c231f0386be 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -2063,7 +2063,7 @@ L7: L8: return r10 -[case testProperty] +[case testProperty_withgil] class PropertyHolder: @property def value(self) -> int: diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 0310d2f69d444..66caf0772ec40 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -26,7 +26,7 @@ L0: a.x = 2; r0 = is_error return 1 -[case testUserClassInList] +[case testUserClassInList_withgil] class C: x: int @@ -103,7 +103,7 @@ L0: r0 = a.n return r0 -[case testOptionalMember] +[case testOptionalMember_withgil] from typing import Optional class Node: next: Optional[Node] @@ -1239,7 +1239,7 @@ L0: __mypyc_self__.s = r0 return 1 -[case testBorrowAttribute] +[case testBorrowAttribute_withgil] def f(d: D) -> int: return d.c.x @@ -1258,6 +1258,24 @@ L0: keep_alive d return r1 +[case testCannotBorrowAttribute_nogil] +def f(d: D) -> int: + return d.c.x + +class C: + x: int +class D: + c: C +[out] +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = d.c + r1 = r0.x + return r1 + [case testNoBorrowOverPropertyAccess] class C: d: D diff --git a/mypyc/test-data/irbuild-generics.test b/mypyc/test-data/irbuild-generics.test index 9ec29182e89b6..0d4dad4f97e23 100644 --- a/mypyc/test-data/irbuild-generics.test +++ b/mypyc/test-data/irbuild-generics.test @@ -40,7 +40,7 @@ L0: y = r3 return 1 -[case testGenericAttrAndTypeApplication] +[case testGenericAttrAndTypeApplication_withgil] from typing import TypeVar, Generic T = TypeVar('T') class C(Generic[T]): diff --git a/mypyc/test-data/irbuild-glue-methods.test b/mypyc/test-data/irbuild-glue-methods.test index 87b3bf7fee588..bdbdb7e2afa68 100644 --- a/mypyc/test-data/irbuild-glue-methods.test +++ b/mypyc/test-data/irbuild-glue-methods.test @@ -91,7 +91,7 @@ L0: r0 = x.foo(y) return r0 -[case testPropertyDerivedGen] +[case testPropertyDerivedGen_withgil] from typing import Callable class BaseProperty: @property diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index 44157f5a820a0..4cea7894052aa 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1032,7 +1032,7 @@ L4: y = r3 return y -[case testBorrowOverI64Arithmetic] +[case testBorrowOverI64Arithmetic_withgil] from mypy_extensions import i64 def add_simple(c: C) -> i64: @@ -1084,7 +1084,7 @@ L0: keep_alive d, d return r4 -[case testBorrowOverI64Bitwise] +[case testBorrowOverI64Bitwise_withgil] from mypy_extensions import i64 def bitwise_simple(c: C) -> i64: @@ -2070,7 +2070,7 @@ L4: r6 = CPyFloat_FromTagged(r3) return r6 -[case testI64IsinstanceNarrowing] +[case testI64IsinstanceNarrowing_withgil] from typing import Union from mypy_extensions import i64 diff --git a/mypyc/test-data/irbuild-isinstance.test b/mypyc/test-data/irbuild-isinstance.test index 36a9300350bd3..216a52ff2a25c 100644 --- a/mypyc/test-data/irbuild-isinstance.test +++ b/mypyc/test-data/irbuild-isinstance.test @@ -48,7 +48,7 @@ L2: L3: return r1 -[case testBorrowSpecialCaseWithIsinstance] +[case testBorrowSpecialCaseWithIsinstance_withgil] class C: s: str diff --git a/mypyc/test-data/irbuild-optional.test b/mypyc/test-data/irbuild-optional.test index fbf7cb148b089..252f9489dc57f 100644 --- a/mypyc/test-data/irbuild-optional.test +++ b/mypyc/test-data/irbuild-optional.test @@ -237,7 +237,7 @@ L3: L4: return 1 -[case testUnionType] +[case testUnionType_withgil] from typing import Union class A: diff --git a/mypyc/test-data/irbuild-vec-t.test b/mypyc/test-data/irbuild-vec-t.test index ee48d81fc29c8..ce162bd0806c7 100644 --- a/mypyc/test-data/irbuild-vec-t.test +++ b/mypyc/test-data/irbuild-vec-t.test @@ -512,15 +512,25 @@ L0: return r2 [case testVecTBorrowGetItem_64bit] +from typing import Final + from librt.vecs import vec from mypy_extensions import i64 class A: - x: str + def __init__(self, x: str) -> None: + # Final to allow borrowing on free-threaded builds + self.x: Final = x def f(v: vec[A], n: i64) -> int: return len(v[n].x) [out] +def A.__init__(self, x): + self :: __main__.A + x :: str +L0: + self.x = x + return 1 def f(v, n): v :: vec[__main__.A] n :: i64 diff --git a/mypyc/test-data/opt-copy-propagation.test b/mypyc/test-data/opt-copy-propagation.test index 49b80f4385fc4..9fa5edb579fac 100644 --- a/mypyc/test-data/opt-copy-propagation.test +++ b/mypyc/test-data/opt-copy-propagation.test @@ -185,7 +185,7 @@ L1: L2: return 2 -[case testIRTransformRegisterOps1] +[case testIRTransformRegisterOps1_withgil] from __future__ import annotations from typing import cast diff --git a/mypyc/test-data/refcount.test b/mypyc/test-data/refcount.test index 7d88cb7d11623..7c7134dcfbfaa 100644 --- a/mypyc/test-data/refcount.test +++ b/mypyc/test-data/refcount.test @@ -651,7 +651,7 @@ L0: r4 = (r2, r3) return r4 -[case testDecomposeTuple] +[case testDecomposeTuple_withgil] from typing import Tuple class C: @@ -970,7 +970,7 @@ L0: dec_ref r4 return r5 -[case testBorrowAttribute] +[case testBorrowAttribute_withgil] def g() -> int: d = D() return d.c.x @@ -1003,7 +1003,99 @@ L0: r1 = r0.x return r1 -[case testBorrowAttributeTwice] +[case testBorrowAttribute_nogil] +from typing import Final + +def g() -> int: + d = D(C(1)) + return d.c.x + +def f(d: D) -> int: + return d.c.x + +class C: + def __init__(self, x: int) -> None: + self.x: Final = x +class D: + def __init__(self, c: C) -> None: + self.c: Final = c +[out] +def g(): + r0 :: __main__.C + r1, d :: __main__.D + r2 :: __main__.C + r3 :: int +L0: + r0 = C(2) + r1 = D(r0) + dec_ref r0 + d = r1 + r2 = borrow d.c + r3 = r2.x + dec_ref d + return r3 +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = borrow d.c + r1 = r0.x + return r1 +def C.__init__(self, x): + self :: __main__.C + x :: int +L0: + inc_ref x :: int + self.x = x + return 1 +def D.__init__(self, c): + self :: __main__.D + c :: __main__.C +L0: + inc_ref c + self.c = c + return 1 + +[case testBorrowInheritedFinalAttribute_nogil] +from typing import Final + +def f(d: D) -> int: + return d.c.x + +class B: + def __init__(self, x: int) -> None: + self.x: Final = x +class C(B): + pass +class D: + def __init__(self, c: C) -> None: + self.c: Final = c +[out] +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = borrow d.c + r1 = r0.x + return r1 +def B.__init__(self, x): + self :: __main__.B + x :: int +L0: + inc_ref x :: int + self.x = x + return 1 +def D.__init__(self, c): + self :: __main__.D + c :: __main__.C +L0: + inc_ref c + self.c = c + return 1 + +[case testBorrowAttributeTwice_withgil] def f(e: E) -> int: return e.d.c.x @@ -1025,7 +1117,7 @@ L0: r2 = r1.x return r2 -[case testBorrowAttributeIsNone] +[case testBorrowAttributeIsNone_withgil] from typing import Optional def f(c: C) -> bool: @@ -1058,7 +1150,7 @@ L0: r2 = r0 == r1 return r2 -[case testBorrowAttributeNarrowOptional] +[case testBorrowAttributeNarrowOptional_withgil] from typing import Optional def f(c: C) -> bool: @@ -1093,7 +1185,7 @@ L1: L2: return 0 -[case testBorrowLenArgument] +[case testBorrowLenArgument_withgil] from typing import List def f(x: C) -> int: @@ -1113,7 +1205,7 @@ L0: r2 = r1 << 1 return r2 -[case testBorrowIsinstanceArgument] +[case testBorrowIsinstanceArgument_withgil] from typing import List def f(x: C) -> bool: @@ -1152,7 +1244,7 @@ L1: L2: return 1 -[case testBorrowListGetItem1] +[case testBorrowListGetItem1_withgil] from typing import List def literal_index(x: C) -> str: @@ -1200,6 +1292,57 @@ L0: r2 = cast(str, r1) return r2 +[case testBorrowListGetItem1_nogil] +from typing import List + +def literal_index(x: C) -> str: + return x.a[0] + +def negative_index(x: C) -> str: + return x.a[-1] + +def lvar_index(x: C, n: int) -> str: + return x.a[n] + +class C: + a: List[str] + +[out] +def literal_index(x): + x :: __main__.C + r0 :: list + r1 :: object + r2 :: str +L0: + r0 = x.a + r1 = CPyList_GetItemShort(r0, 0) + dec_ref r0 + r2 = cast(str, r1) + return r2 +def negative_index(x): + x :: __main__.C + r0 :: list + r1 :: object + r2 :: str +L0: + r0 = x.a + r1 = CPyList_GetItemShort(r0, -2) + dec_ref r0 + r2 = cast(str, r1) + return r2 +def lvar_index(x, n): + x :: __main__.C + n :: int + r0 :: list + r1 :: object + r2 :: str +L0: + r0 = x.a + r1 = CPyList_GetItem(r0, n) + dec_ref r0 + r2 = cast(str, r1) + return r2 + [case testBorrowListGetItem2_withgil] from typing import List @@ -1273,9 +1416,11 @@ def attr_before_index(x): r2 :: object r3 :: str L0: - r0 = borrow x.a - r1 = borrow x.n + r0 = x.a + r1 = x.n r2 = CPyList_GetItem(r0, r1) + dec_ref r0 + dec_ref r1 :: int r3 = cast(str, r2) return r3 def attr_after_index(a, i): @@ -1361,7 +1506,7 @@ L0: dec_ref a return r5 -[case testBorrowSetAttrObject] +[case testBorrowSetAttrObject_withgil] from typing import Optional def f(x: Optional[C]) -> None: @@ -1401,7 +1546,7 @@ L0: r0.b = 0; r1 = is_error return 1 -[case testBorrowIntEquality] +[case testBorrowIntEquality_withgil] def add(c: C) -> bool: return c.x == c.y @@ -1419,7 +1564,7 @@ L0: r2 = int_eq r0, r1 return r2 -[case testBorrowIntLessThan] +[case testBorrowIntLessThan_withgil] def add(c: C) -> bool: return c.x < c.y @@ -1437,7 +1582,7 @@ L0: r2 = int_lt r0, r1 return r2 -[case testBorrowIntCompareFinal] +[case testBorrowIntCompareFinal_withgil] from typing import Final X: Final = 10 @@ -1457,7 +1602,7 @@ L0: r1 = int_eq r0, 20 return r1 -[case testBorrowIntArithmetic] +[case testBorrowIntArithmetic_withgil] def add(c: C) -> int: return c.x + c.y @@ -1485,7 +1630,39 @@ L0: r2 = CPyTagged_Subtract(r0, r1) return r2 -[case testBorrowIntComparisonInIf] +[case testBorrowIntArithmetic_nogil] +def add(c: C) -> int: + return c.x + c.y + +def sub(c: C) -> int: + return c.x - c.y + +class C: + x: int + y: int +[out] +def add(c): + c :: __main__.C + r0, r1, r2 :: int +L0: + r0 = c.x + r1 = c.y + r2 = CPyTagged_Add(r0, r1) + dec_ref r0 :: int + dec_ref r1 :: int + return r2 +def sub(c): + c :: __main__.C + r0, r1, r2 :: int +L0: + r0 = c.x + r1 = c.y + r2 = CPyTagged_Subtract(r0, r1) + dec_ref r0 :: int + dec_ref r1 :: int + return r2 + +[case testBorrowIntComparisonInIf_withgil] def add(c: C, n: int) -> bool: if c.x == c.y: return True @@ -1509,7 +1686,7 @@ L1: L2: return 0 -[case testBorrowIntInPlaceOp] +[case testBorrowIntInPlaceOp_withgil] def add(c: C, n: int) -> None: c.x += n diff --git a/mypyc/test/test_optimizations.py b/mypyc/test/test_optimizations.py index 6ca53b134ba91..c6a40d08e2e67 100644 --- a/mypyc/test/test_optimizations.py +++ b/mypyc/test/test_optimizations.py @@ -7,7 +7,7 @@ from mypy.errors import CompileError from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase -from mypyc.common import TOP_LEVEL_NAME +from mypyc.common import IS_FREE_THREADED, TOP_LEVEL_NAME from mypyc.ir.func_ir import FuncIR from mypyc.ir.pprint import format_func from mypyc.options import CompilerOptions @@ -33,6 +33,12 @@ class OptimizationSuite(MypycDataSuite): base_path = test_temp_dir def run_case(self, testcase: DataDrivenTestCase) -> None: + if "_withgil" in testcase.name and IS_FREE_THREADED: + # Test case should only run on a non-free-threaded build. + return + if "_nogil" in testcase.name and not IS_FREE_THREADED: + # Test case should only run on a free-threaded build. + return with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase): expected_output = remove_comment_lines(testcase.output) try: From f3294a092bd2f43979bc1d83e3f27e1c2e0a1453 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 18:41:19 +0100 Subject: [PATCH 109/127] [mypyc] Add librt.threading.Lock class (#21690) The new native `Lock` class is a partial replacement for `threading.Lock`. It's much faster than `threading.Lock` in compiled code. In a microbenchmark I saw ~2.5x to ~4x performance improvement over compiled code using stdlib. The Lock type has four platform-specific backends: - PyMutex on Python 3.14+ (all platforms); uses CPython's internal 1-byte atomic lock - SRWLOCK + condition variable on Windows < 3.14 - POSIX semaphore on Linux < 3.14 with GIL - pthread mutex + condvar fallback (macOS, free-threaded builds) All backends preserve threading.Lock semantics (cross-thread release allowed) and drop the GIL while blocking. Added optimized handling of `with lock:` statements (for `librt.threading` only). There is one significant known regression compared to CPython (beyond missing features): if two threads race to release the same lock, this triggers undefined behavior. The PyMutex C API doesn't provide a way to avoid this without a massive performance cost, so I decided to keep this limitation instead of sacrificing performance, as performance is the main goal of the new type. This is just a lock -- using it with `threading.Condition` is not supported. We may later decide to add a native `Condition` (and possibly other synchronization primitives). I used heavy coding agent assist with small, incremental changes, and multiple review iterations. I left the rather verbose comments generated by coding agents, since the code is legitimately tricky. --------- Co-authored-by: Piotr Sawicki --- mypy/typeshed/stubs/librt/librt/threading.pyi | 16 + mypyc/build.py | 6 + mypyc/codegen/emitmodule.py | 5 + mypyc/ir/deps.py | 1 + mypyc/ir/rtypes.py | 11 +- mypyc/irbuild/statement.py | 32 + mypyc/lib-rt/setup.py | 6 + mypyc/lib-rt/threading/librt_threading.c | 714 ++++++++++++++++++ mypyc/lib-rt/threading/librt_threading.h | 10 + mypyc/lib-rt/threading/librt_threading_api.c | 43 ++ mypyc/lib-rt/threading/librt_threading_api.h | 20 + mypyc/primitives/librt_threading_ops.py | 54 ++ mypyc/primitives/registry.py | 1 + mypyc/test-data/irbuild-threading.test | 115 +++ mypyc/test-data/run-threading.test | 156 ++++ mypyc/test/test_irbuild.py | 1 + mypyc/test/test_run.py | 1 + 17 files changed, 1190 insertions(+), 2 deletions(-) create mode 100644 mypy/typeshed/stubs/librt/librt/threading.pyi create mode 100644 mypyc/lib-rt/threading/librt_threading.c create mode 100644 mypyc/lib-rt/threading/librt_threading.h create mode 100644 mypyc/lib-rt/threading/librt_threading_api.c create mode 100644 mypyc/lib-rt/threading/librt_threading_api.h create mode 100644 mypyc/primitives/librt_threading_ops.py create mode 100644 mypyc/test-data/irbuild-threading.test create mode 100644 mypyc/test-data/run-threading.test diff --git a/mypy/typeshed/stubs/librt/librt/threading.pyi b/mypy/typeshed/stubs/librt/librt/threading.pyi new file mode 100644 index 0000000000000..a3571ac7f4745 --- /dev/null +++ b/mypy/typeshed/stubs/librt/librt/threading.pyi @@ -0,0 +1,16 @@ +from types import TracebackType +from typing import final + +@final +class Lock: + def acquire(self, blocking: bool = True) -> bool: ... + def release(self) -> None: ... + def locked(self) -> bool: ... + def __enter__(self) -> bool: ... + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + /, + ) -> None: ... diff --git a/mypyc/build.py b/mypyc/build.py index b46365d263e6a..8c6eabead17c9 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -127,6 +127,12 @@ class ModDesc(NamedTuple): ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), ModDesc("librt.random", ["random/librt_random.c"], ["random/librt_random.h"], ["random"]), + ModDesc( + "librt.threading", + ["threading/librt_threading.c"], + ["threading/librt_threading.h"], + ["threading"], + ), ] try: diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 6e3c122aa13f6..3b320b5321232 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -62,6 +62,7 @@ LIBRT_BASE64, LIBRT_RANDOM, LIBRT_STRINGS, + LIBRT_THREADING, LIBRT_TIME, LIBRT_VECS, Capsule, @@ -1261,6 +1262,10 @@ def emit_module_exec_func( emitter.emit_line("if (import_librt_time() < 0) {") emitter.emit_line("return -1;") emitter.emit_line("}") + if LIBRT_THREADING in module.dependencies: + emitter.emit_line("if (import_librt_threading() < 0) {") + emitter.emit_line("return -1;") + emitter.emit_line("}") if LIBRT_VECS in module.dependencies: emitter.emit_line("if (import_librt_vecs() < 0) {") emitter.emit_line("return -1;") diff --git a/mypyc/ir/deps.py b/mypyc/ir/deps.py index 751845d3a324c..b1a27f85fcfe6 100644 --- a/mypyc/ir/deps.py +++ b/mypyc/ir/deps.py @@ -110,6 +110,7 @@ def get_header(self) -> str: LIBRT_VECS: Final = Capsule("librt.vecs") LIBRT_TIME: Final = Capsule("librt.time") LIBRT_RANDOM: Final = Capsule("librt.random") +LIBRT_THREADING: Final = Capsule("librt.threading") BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c") BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c") diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 9d13ecc83175d..1a5515d5621a8 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -41,7 +41,7 @@ class to enable the new behavior. In rare cases, adding a new from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeGuard, TypeVar, Union, final from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name -from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_VECS, Dependency +from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_THREADING, LIBRT_VECS, Dependency from mypyc.namegen import NameGenerator if TYPE_CHECKING: @@ -550,12 +550,19 @@ def __hash__(self) -> int: } | { "librt.random.Random": RPrimitive( "librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,) - ) + ), + "librt.threading.Lock": RPrimitive( + "librt.threading.Lock", + is_unboxed=False, + is_refcounted=True, + dependencies=(LIBRT_THREADING,), + ), } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] string_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.StringWriter"] random_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.random.Random"] +lock_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.threading.Lock"] def is_native_rprimitive(rtype: RType) -> bool: diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index 428650a810dca..e90aa089305f2 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -80,6 +80,7 @@ c_pyssize_t_rprimitive, exc_rtuple, is_tagged, + lock_rprimitive, none_rprimitive, object_pointer_rprimitive, object_rprimitive, @@ -115,6 +116,7 @@ restore_exc_info_op, ) from mypyc.primitives.generic_ops import iter_op, next_raw_op, py_delattr_op +from mypyc.primitives.librt_threading_ops import lock_acquire_op, lock_release_op from mypyc.primitives.misc_ops import ( check_stop_op, coro_op, @@ -1108,6 +1110,10 @@ def transform_with( al = "a" if is_async else "" mgr_v = builder.accept(expr) + + if not is_async and mgr_v.type == lock_rprimitive: + transform_with_lock(builder, mgr_v, target, body, line) + return is_native = isinstance(mgr_v.type, RInstance) if is_native: value = builder.add(MethodCall(mgr_v, f"__{al}enter__", args=[], line=line)) @@ -1179,6 +1185,32 @@ def finally_body() -> None: ) +def transform_with_lock( + builder: IRBuilder, mgr_v: Value, target: Lvalue | None, body: GenFunc, line: int +) -> None: + """Optimized 'with' for librt.threading.Lock. + + Generate a simple try/finally with direct acquire/release calls. + Lock.__exit__ never suppresses exceptions, so we don't need the + full PEP 343 try/except/finally machinery. + """ + # __enter__: acquire the lock + value = builder.primitive_op(lock_acquire_op, [mgr_v], line) + + mgr = builder.maybe_spill(mgr_v) + + def try_body() -> None: + if target: + builder.assign(builder.get_assignment_target(target), value, line) + body() + + def finally_body() -> None: + # __exit__: release the lock (ignoring exception info) + builder.primitive_op(lock_release_op, [builder.read(mgr, line)], line) + + transform_try_finally_stmt(builder, try_body, finally_body, line) + + def transform_with_stmt(builder: IRBuilder, o: WithStmt) -> None: # Generate separate logic for each expr in it, left to right def generate(i: int) -> None: diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index 371b322ca18b2..15d31a443ff86 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -164,5 +164,11 @@ def run(self) -> None: include_dirs=["."], extra_compile_args=cflags, ), + Extension( + "librt.threading", + ["threading/librt_threading.c"], + include_dirs=[".", "threading"], + extra_compile_args=cflags, + ), ] ) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c new file mode 100644 index 0000000000000..84d7346a75872 --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -0,0 +1,714 @@ +#include "pythoncapi_compat.h" + +#define PY_SSIZE_T_CLEAN +#include +#include "librt_threading.h" +#include "mypyc_util.h" + +#if !defined(_WIN32) +#include +#include +#endif + +#if CPY_3_14_FEATURES + +// Python 3.14+ (all platforms, with or without the GIL): Use PyMutex (1-byte +// atomic lock with parking lot). PyMutex gives better interruptibility than the +// pthread fallback, and its fast release path is competitive with sem_t. +// PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal +// CPython APIs that might change across minor releases. +#define LOCK_BACKEND_PYMUTEX +#ifndef Py_BUILD_CORE +#define Py_BUILD_CORE +#endif +#include "internal/pycore_lock.h" + +#elif defined(_WIN32) + +// Python <3.14 on Windows: Use Slim Reader/Writer Lock +#define LOCK_BACKEND_SRWLOCK +#include + +#else + +// Python <3.14 on POSIX. +// +// Prefer a POSIX unnamed semaphore when the platform supports it well and the +// GIL is enabled, and fall back to a pthread mutex + condition variable +// otherwise. We use the same test CPython uses to pick its semaphore-based lock +// (see Python/thread_pthread.h): an unnamed semaphore is only usable when +// sem_init() actually works AND a timed wait is available. Notably this is true +// on Linux but false on macOS (whose sem_init() is a non-functional stub), so +// macOS uses the mutex+condvar fallback. Free-threaded builds also use the +// mutex+condvar fallback because the semaphore backend's `locked` bookkeeping +// relies on GIL serialization. +#if !defined(Py_GIL_DISABLED) && \ + defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \ + (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT)) +#define LOCK_BACKEND_SEM +#include +#else +#define LOCK_BACKEND_PTHREAD +#include +#endif + +#endif + +// +// Lock +// +// A fast mutex lock for use from mypyc-compiled code. +// +// On Python 3.14+ (all platforms), this uses CPython's PyMutex, a 1-byte atomic +// lock backed by a parking lot for contended waits. PyMutex automatically +// releases the GIL when blocking. +// +// On Python 3.13 and earlier with Windows, this uses an SRWLOCK (Slim +// Reader/Writer Lock) plus a CONDITION_VARIABLE guarding a `locked` flag. The SRWLOCK only +// protects the flag and is never held across the user's critical section, so +// release() may be called from a thread other than the acquirer (matching +// threading.Lock semantics). This mirrors CPython's Windows lock (NRMUTEX in +// Python/thread_nt.h) and is the Windows twin of the POSIX pthread+condvar +// backend below. +// +// On Python 3.13 and earlier with POSIX systems, there are two backends, both +// of which allow release() from a thread other than the one that acquired the +// lock (matching threading.Lock semantics): +// +// - Where unnamed POSIX semaphores work well (e.g. Linux) and the GIL is +// enabled, this uses a sem_t initialized to 1: acquire is sem_wait, release +// is sem_post. Semaphores have no ownership concept, so cross-thread release +// is directly well-defined. +// +// - Otherwise (e.g. macOS, whose sem_init() is a non-functional stub, and +// free-threaded builds), this uses a pthread mutex + condition variable +// guarding a `locked` flag. The mutex only protects the flag and is never +// held across the user's critical section, so the OS mutex is always +// unlocked on the same thread that locked it. +// + +// ---------- Platform-specific lock state ---------- + +#if defined(LOCK_BACKEND_PYMUTEX) + +typedef struct { + PyObject_HEAD + PyMutex mutex; +} LockObject; + +#elif defined(LOCK_BACKEND_SRWLOCK) + +typedef struct { + PyObject_HEAD + // The SRWLOCK below does NOT represent the Python lock; like the pthread + // fallback's `mut`, it only guards `locked` and the condition variable, + // and is held just long enough to inspect/flip the flag -- never across + // the user's critical section. That is what allows release() from a + // thread other than the acquirer: SRWLOCK's same-thread-release rule is + // never violated, and clearing `locked` is just a guarded store. This + // mirrors CPython's Windows lock (NRMUTEX in Python/thread_nt.h). + SRWLOCK srw; + CONDITION_VARIABLE lock_released; + int locked; // 0=unlocked, 1=locked; protected by `srw` +} LockObject; + +#elif defined(LOCK_BACKEND_SEM) + +typedef struct { + PyObject_HEAD + sem_t sem; // counting semaphore, initialized to 1 + // Tracks the locked state for locked() and release-unlocked detection. + // The semaphore itself is the source of truth for mutual exclusion; this + // flag is advisory bookkeeping. It is set after a successful acquire and + // cleared before sem_post in release. + // + // This backend is only selected when the GIL is enabled, so this flag is a + // plain int relying on the GIL for serialization: it is only ever touched + // with the GIL held (the blocking sem_wait drops the GIL, but the flag + // store happens after the GIL is reacquired). This mirrors the old + // PyThread_type_lock wrapper bookkeeping used by CPython 3.12 and earlier, + // where _thread lock kept a plain `char locked` for sanity checks and + // locked(). + int locked; +} LockObject; + +#else // pthread mutex + condvar fallback + +typedef struct { + PyObject_HEAD + // The pthread mutex below does NOT represent the Python lock; it only + // guards the `locked` flag and the condition variable. The Python lock + // state is `locked` itself. This indirection (matching CPython's POSIX + // lock) is what allows release() from a thread other than the acquirer: + // `mut` is always locked and unlocked within a single call on a single + // thread, so pthread's same-thread-unlock rule is never violated. + pthread_mutex_t mut; + pthread_cond_t lock_released; + // Always accessed while holding `mut` (including the locked() reader), + // so a plain int is sufficient -- the mutex provides the ordering. + // Matches CPython's pthread_lock.locked (a plain char). + int locked; // 0=unlocked, 1=locked; protected by `mut` +} LockObject; + +#endif + +// ---------- Platform-specific init/acquire/release ---------- + +static inline int +Lock_init_internal(LockObject *self) +{ +#if defined(LOCK_BACKEND_PYMUTEX) + self->mutex = (PyMutex){0}; +#elif defined(LOCK_BACKEND_SRWLOCK) + InitializeSRWLock(&self->srw); + InitializeConditionVariable(&self->lock_released); + self->locked = 0; +#elif defined(LOCK_BACKEND_SEM) + if (sem_init(&self->sem, 0, 1) != 0) { + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + self->locked = 0; +#else + int status = pthread_mutex_init(&self->mut, NULL); + if (status != 0) { + errno = status; + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + + status = pthread_cond_init(&self->lock_released, NULL); + if (status != 0) { + pthread_mutex_destroy(&self->mut); + errno = status; + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + + self->locked = 0; +#endif + return 0; +} + +// Try to acquire the lock. Returns 1 (true) on success, 0 (false) if +// non-blocking and the lock is held, or -1 if interrupted by an error-raising +// signal handler. +static int +Lock_acquire_impl(LockObject *self, int blocking) +{ +#if defined(LOCK_BACKEND_PYMUTEX) + if (!blocking) { + PyLockStatus r = _PyMutex_LockTimed(&self->mutex, 0, _Py_LOCK_DONT_DETACH); + return r == PY_LOCK_ACQUIRED; + } + if (PyMutex_LockFast(&self->mutex)) { + return 1; + } + PyLockStatus r = _PyMutex_LockTimed(&self->mutex, -1, + _PY_LOCK_DETACH | _PY_LOCK_HANDLE_SIGNALS); + if (r == PY_LOCK_INTR) { + return -1; + } + return 1; + +#elif defined(LOCK_BACKEND_SRWLOCK) + // `srw` only guards `locked` and the condition variable; it is held just + // long enough to inspect/flip the flag, never across the user's critical + // section. This is what lets a different thread call release(). + // + // Fast path: grab the lock without releasing the GIL if it is free. This is + // also the whole story in the non-blocking case. + AcquireSRWLockExclusive(&self->srw); + if (!self->locked) { + self->locked = 1; + ReleaseSRWLockExclusive(&self->srw); + return 1; + } + ReleaseSRWLockExclusive(&self->srw); + if (!blocking) { + return 0; + } + + // Slow path: wait for the lock to be released, with the GIL dropped so + // other Python threads can run (and release the lock). + // SleepConditionVariableSRW atomically releases the SRWLOCK while sleeping + // and reacquires it on wake, exactly like pthread_cond_wait. + Py_BEGIN_ALLOW_THREADS + AcquireSRWLockExclusive(&self->srw); + while (self->locked) { + SleepConditionVariableSRW(&self->lock_released, &self->srw, INFINITE, 0); + } + self->locked = 1; + ReleaseSRWLockExclusive(&self->srw); + Py_END_ALLOW_THREADS + return 1; + +#elif defined(LOCK_BACKEND_SEM) + // A semaphore has no ownership: any thread may sem_post a token that + // another thread consumed via sem_wait, so cross-thread release is + // directly well-defined. `locked` is advisory bookkeeping for locked() + // and for guarding against releasing an unheld lock. + // + // Fast path: try a non-blocking acquire first to avoid GIL release/reacquire + // overhead in the common uncontended case. This is also the whole story in + // the non-blocking case. + { + int status; + do { + status = sem_trywait(&self->sem); + } while (status == -1 && errno == EINTR); + if (status == 0) { + self->locked = 1; + return 1; + } + } + if (!blocking) { + return 0; // EAGAIN: already held + } + + // Slow path: block with the GIL dropped so other Python threads can run + // (and release the lock). If a signal interrupts sem_wait(), run pending + // Python signal handlers with the GIL held; retry unless a handler raises. + for (;;) { + int status; + int err = 0; + + Py_BEGIN_ALLOW_THREADS + status = sem_wait(&self->sem); + if (status == -1) { + err = errno; + } + Py_END_ALLOW_THREADS + + if (status == 0) { + self->locked = 1; + return 1; + } + + if (err != EINTR) { + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + + if (Py_MakePendingCalls() < 0) { + return -1; + } + } + +#else // pthread mutex + condvar fallback + // `mut` only guards `locked` and the condition variable; it is held just + // long enough to inspect/flip the flag, never across the user's critical + // section. This is what lets a different thread call release(). + // + // Fast path: grab the lock without releasing the GIL if it is free. This is + // also the whole story in the non-blocking case. + pthread_mutex_lock(&self->mut); + if (!self->locked) { + self->locked = 1; + pthread_mutex_unlock(&self->mut); + return 1; + } + pthread_mutex_unlock(&self->mut); + if (!blocking) { + return 0; + } + + // Slow path: wait for the lock to be released, with the GIL dropped so + // other Python threads can run (and release the lock). If we wake but do + // not get the lock, give pending Python signal handlers a chance to run, + // matching CPython's pthread fallback. + for (;;) { + int acquired = 0; + int interrupted = 0; + int status; + + Py_BEGIN_ALLOW_THREADS + status = pthread_mutex_lock(&self->mut); + if (status == 0) { + while (self->locked) { + status = pthread_cond_wait(&self->lock_released, &self->mut); + if (status != 0) { + break; + } + if (self->locked) { + interrupted = 1; + break; + } + } + if (status == 0 && !interrupted) { + self->locked = 1; + acquired = 1; + } + int unlock_status = pthread_mutex_unlock(&self->mut); + if (status == 0) { + status = unlock_status; + } + } + Py_END_ALLOW_THREADS + + if (status != 0) { + errno = status; + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + if (acquired) { + return 1; + } + if (interrupted && Py_MakePendingCalls() < 0) { + return -1; + } + } +#endif +} + +// Release the lock. Returns 0 on success, -1 if the lock was not held. +static int +Lock_release_impl(LockObject *self) +{ +#if defined(LOCK_BACKEND_PYMUTEX) + // threading.Lock is unowned, so release() may be called from a different + // thread than acquire(). CPython's atomic _PyMutex_TryUnlock() is not part + // of the public API and is not exported by all CPython builds. This fast + // partial-replacement path deliberately avoids a second guard mutex: + // ordinary release() of an unlocked lock raises RuntimeError, but racy + // erroneous release() calls are undefined behavior and may make + // PyMutex_Unlock() abort. + if (!PyMutex_IsLocked(&self->mutex)) { + return -1; + } + PyMutex_Unlock(&self->mutex); + return 0; + +#elif defined(LOCK_BACKEND_SRWLOCK) + AcquireSRWLockExclusive(&self->srw); + if (!self->locked) { + ReleaseSRWLockExclusive(&self->srw); + return -1; + } + self->locked = 0; + // Wake one waiter (if any). Signalling under `srw` is fine and avoids a + // lost-wakeup race. + WakeConditionVariable(&self->lock_released); + ReleaseSRWLockExclusive(&self->srw); + return 0; + +#elif defined(LOCK_BACKEND_SEM) + // Check-then-clear the flag, then post. This backend is only selected when + // the GIL is enabled, so the check and clear are serialized without an + // atomic. + if (!self->locked) { + return -1; + } + self->locked = 0; + sem_post(&self->sem); + return 0; + +#else // pthread mutex + condvar fallback + pthread_mutex_lock(&self->mut); + if (!self->locked) { + pthread_mutex_unlock(&self->mut); + return -1; + } + self->locked = 0; + // Wake one waiter (if any). Signalling under `mut` is fine and avoids a + // lost-wakeup race. + pthread_cond_signal(&self->lock_released); + pthread_mutex_unlock(&self->mut); + return 0; +#endif +} + +static inline int +Lock_is_locked(LockObject *self) +{ +#if defined(LOCK_BACKEND_PYMUTEX) + return PyMutex_IsLocked(&self->mutex); +#elif defined(LOCK_BACKEND_SRWLOCK) + // locked() is not expected to be on a perf-critical path, so take the + // SRWLOCK (shared) for a clean read of the guarded flag rather than + // relying on an atomic/volatile field. + AcquireSRWLockShared(&self->srw); + int result = self->locked; + ReleaseSRWLockShared(&self->srw); + return result; +#elif defined(LOCK_BACKEND_SEM) + // The flag is GIL-serialized (see the struct comment); locked() is a + // plain read, matching the old CPython _thread lock bookkeeping described + // above. + return self->locked != 0; +#else // pthread mutex + condvar fallback + // `locked` is only ever accessed under `mut`; take it for a clean read. + // locked() is not expected to be on a perf-critical path. + pthread_mutex_lock(&self->mut); + int result = self->locked; + pthread_mutex_unlock(&self->mut); + return result; +#endif +} + +// ---------- Python type methods (shared across platforms) ---------- + +static PyTypeObject LockType; + +static PyObject * +Lock_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + if (type != &LockType) { + PyErr_SetString(PyExc_TypeError, "Lock cannot be subclassed"); + return NULL; + } + + LockObject *self = (LockObject *)type->tp_alloc(type, 0); + if (self != NULL && Lock_init_internal(self) < 0) { + type->tp_free((PyObject *)self); + return NULL; + } + return (PyObject *)self; +} + +static int +Lock_init(LockObject *self, PyObject *args, PyObject *kwds) +{ + if (!PyArg_ParseTuple(args, "")) { + return -1; + } + + if (kwds != NULL && PyDict_Size(kwds) > 0) { + PyErr_SetString(PyExc_TypeError, + "Lock() takes no keyword arguments"); + return -1; + } + + return 0; +} + +static void +Lock_dealloc(LockObject *self) +{ +#if defined(LOCK_BACKEND_SEM) + sem_destroy(&self->sem); +#elif defined(LOCK_BACKEND_PTHREAD) + // `mut` is only ever held transiently within a single call, so it is + // always unlocked here even if the Python lock is still "locked". + // Some pthread implementations require the cond to be destroyed first. + pthread_cond_destroy(&self->lock_released); + pthread_mutex_destroy(&self->mut); +#endif + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static PyObject * +Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs, + PyObject *kwnames) +{ + int blocking = 1; + + Py_ssize_t nkw = kwnames ? PyTuple_GET_SIZE(kwnames) : 0; + if (nargs + nkw > 1) { + PyErr_SetString(PyExc_TypeError, "acquire() takes at most 1 argument"); + return NULL; + } + + if (nargs == 1) { + blocking = PyObject_IsTrue(args[0]); + if (blocking < 0) + return NULL; + } else if (nkw == 1) { + PyObject *key = PyTuple_GET_ITEM(kwnames, 0); + if (PyUnicode_CompareWithASCIIString(key, "blocking") != 0) { + PyErr_Format(PyExc_TypeError, + "acquire() got an unexpected keyword argument '%U'", + key); + return NULL; + } + blocking = PyObject_IsTrue(args[0]); + if (blocking < 0) + return NULL; + } + + int result = Lock_acquire_impl(self, blocking); + if (result < 0) { + return NULL; + } + return PyBool_FromLong(result); +} + +static PyObject * +Lock_release(LockObject *self, PyObject *Py_UNUSED(ignored)) +{ + if (Lock_release_impl(self) < 0) { + PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock"); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +Lock_locked(LockObject *self, PyObject *Py_UNUSED(ignored)) +{ + return PyBool_FromLong(Lock_is_locked(self)); +} + +static PyObject * +Lock_enter(LockObject *self, PyObject *Py_UNUSED(ignored)) +{ + int result = Lock_acquire_impl(self, 1); + if (result < 0) + return NULL; + return PyBool_FromLong(result); +} + +static PyObject * +Lock_exit(LockObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + return Lock_release(self, NULL); +} + +static PyMethodDef Lock_methods[] = { + {"acquire", (PyCFunction)(void(*)(void))Lock_acquire, METH_FASTCALL | METH_KEYWORDS, + PyDoc_STR("Acquire the lock, blocking or non-blocking.\n" + "Returns True if the lock was acquired, False otherwise.")}, + {"release", (PyCFunction)Lock_release, METH_NOARGS, + PyDoc_STR("Release the lock.")}, + {"locked", (PyCFunction)Lock_locked, METH_NOARGS, + PyDoc_STR("Return True if the lock is currently held.")}, + {"__enter__", (PyCFunction)Lock_enter, METH_NOARGS, + PyDoc_STR("Acquire the lock.")}, + {"__exit__", (PyCFunction)Lock_exit, METH_FASTCALL, + PyDoc_STR("Release the lock.")}, + {NULL} +}; + +static PyTypeObject LockType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "Lock", + .tp_doc = PyDoc_STR("A fast mutual exclusion lock"), + .tp_basicsize = sizeof(LockObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = Lock_new, + .tp_init = (initproc)Lock_init, + .tp_dealloc = (destructor)Lock_dealloc, + .tp_methods = Lock_methods, +}; + +static PyTypeObject * +Lock_type_internal(void) { + return &LockType; +} + +// Create a new Lock object (for use from compiled code) +static PyObject * +Lock_new_internal(void) { + LockObject *self = (LockObject *)LockType.tp_alloc(&LockType, 0); + if (self != NULL && Lock_init_internal(self) < 0) { + LockType.tp_free((PyObject *)self); + return NULL; + } + return (PyObject *)self; +} + +// Acquire the lock (blocking), for use from compiled code. +// Returns true on success, sets error and returns 2 (ERR_MAGIC) on failure. +static char +Lock_acquire_internal(PyObject *self) { + int result = Lock_acquire_impl((LockObject *)self, 1); + if (result < 0) { + return 2; + } + return (char)result; +} + +// Acquire the lock with explicit blocking arg, for use from compiled code. +// Returns true if acquired, false otherwise. Sets error and returns 2 +// (ERR_MAGIC) on failure. +static char +Lock_acquire_blocking_internal(PyObject *self, char blocking) { + int result = Lock_acquire_impl((LockObject *)self, blocking); + if (result < 0) { + return 2; + } + return (char)result; +} + +// Release the lock, for use from compiled code. +// Returns 0 (None) on success, sets error and returns 2 (ERR_MAGIC) on failure. +static char +Lock_release_internal(PyObject *self) { + if (Lock_release_impl((LockObject *)self) < 0) { + PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock"); + return 2; + } + return 0; +} + +// Check if the lock is held, for use from compiled code. +static char +Lock_locked_internal(PyObject *self) { + return (char)Lock_is_locked((LockObject *)self); +} + +static PyMethodDef librt_threading_module_methods[] = { + {NULL, NULL, 0, NULL} +}; + +static int +threading_abi_version(void) { + return LIBRT_THREADING_ABI_VERSION; +} + +static int +threading_api_version(void) { + return LIBRT_THREADING_API_VERSION; +} + +static int +librt_threading_module_exec(PyObject *m) +{ + if (PyType_Ready(&LockType) < 0) { + return -1; + } + if (PyModule_AddObjectRef(m, "Lock", (PyObject *)&LockType) < 0) { + return -1; + } + + // Export mypyc internal C API via capsule + static void *threading_api[LIBRT_THREADING_API_LEN] = { + (void *)threading_abi_version, + (void *)threading_api_version, + (void *)Lock_type_internal, + (void *)Lock_new_internal, + (void *)Lock_acquire_internal, + (void *)Lock_release_internal, + (void *)Lock_locked_internal, + (void *)Lock_acquire_blocking_internal, + }; + PyObject *c_api_object = PyCapsule_New((void *)threading_api, "librt.threading._C_API", NULL); + if (PyModule_Add(m, "_C_API", c_api_object) < 0) { + return -1; + } + return 0; +} + +static PyModuleDef_Slot librt_threading_module_slots[] = { + {Py_mod_exec, librt_threading_module_exec}, +#ifdef Py_MOD_GIL_NOT_USED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + +static PyModuleDef librt_threading_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "threading", + .m_doc = "Fast threading primitives optimized for mypyc", + .m_size = 0, + .m_methods = librt_threading_module_methods, + .m_slots = librt_threading_module_slots, +}; + +PyMODINIT_FUNC +PyInit_threading(void) +{ + return PyModuleDef_Init(&librt_threading_module); +} diff --git a/mypyc/lib-rt/threading/librt_threading.h b/mypyc/lib-rt/threading/librt_threading.h new file mode 100644 index 0000000000000..a22284f3fdbcd --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading.h @@ -0,0 +1,10 @@ +#ifndef LIBRT_THREADING_H +#define LIBRT_THREADING_H + +#include + +#define LIBRT_THREADING_ABI_VERSION 1 +#define LIBRT_THREADING_API_VERSION 1 +#define LIBRT_THREADING_API_LEN 8 + +#endif // LIBRT_THREADING_H diff --git a/mypyc/lib-rt/threading/librt_threading_api.c b/mypyc/lib-rt/threading/librt_threading_api.c new file mode 100644 index 0000000000000..156975d243638 --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading_api.c @@ -0,0 +1,43 @@ +#include "librt_threading_api.h" + +void *LibRTThreading_API[LIBRT_THREADING_API_LEN] = {0}; + +int +import_librt_threading(void) +{ + PyObject *mod = PyImport_ImportModule("librt.threading"); + if (mod == NULL) + return -1; + Py_DECREF(mod); // we import just for the side effect of making the below work. + void **capsule = (void **)PyCapsule_Import("librt.threading._C_API", 0); + if (capsule == NULL) + return -1; + + // Only after version validation succeeds can we safely copy the full table. + int (*abi_version)(void) = (int (*)(void))capsule[0]; + int (*api_version)(void) = (int (*)(void))capsule[1]; + if (abi_version() != LIBRT_THREADING_ABI_VERSION) { + char err[128]; + snprintf(err, sizeof(err), "ABI version conflict for librt.threading, expected %d, found %d", + LIBRT_THREADING_ABI_VERSION, + abi_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + if (api_version() < LIBRT_THREADING_API_VERSION) { + char err[128]; + snprintf(err, sizeof(err), + "API version conflict for librt.threading, expected %d or newer, found %d (hint: upgrade librt)", + LIBRT_THREADING_API_VERSION, + api_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + // Provider API version is >= our expected version, which (by the API + // compatibility contract) means it has at least LIBRT_THREADING_API_LEN + // entries, so this copy is safe. + memcpy(LibRTThreading_API, capsule, sizeof(LibRTThreading_API)); + return 0; +} diff --git a/mypyc/lib-rt/threading/librt_threading_api.h b/mypyc/lib-rt/threading/librt_threading_api.h new file mode 100644 index 0000000000000..acb3414d79561 --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading_api.h @@ -0,0 +1,20 @@ +#ifndef LIBRT_THREADING_API_H +#define LIBRT_THREADING_API_H + +#include "librt_threading.h" + +int +import_librt_threading(void); + +extern void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; + +#define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0]) +#define LibRTThreading_APIVersion (*(int (*)(void)) LibRTThreading_API[1]) +#define LibRTThreading_Lock_type_internal (*(PyTypeObject* (*)(void)) LibRTThreading_API[2]) +#define LibRTThreading_Lock_new_internal (*(PyObject* (*)(void)) LibRTThreading_API[3]) +#define LibRTThreading_Lock_acquire_internal (*(char (*)(PyObject *self)) LibRTThreading_API[4]) +#define LibRTThreading_Lock_release_internal (*(char (*)(PyObject *self)) LibRTThreading_API[5]) +#define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) +#define LibRTThreading_Lock_acquire_blocking_internal (*(char (*)(PyObject *self, char blocking)) LibRTThreading_API[7]) + +#endif // LIBRT_THREADING_API_H diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py new file mode 100644 index 0000000000000..c060742dc2050 --- /dev/null +++ b/mypyc/primitives/librt_threading_ops.py @@ -0,0 +1,54 @@ +from mypyc.ir.deps import LIBRT_THREADING +from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER +from mypyc.ir.rtypes import bool_rprimitive, lock_rprimitive, none_rprimitive +from mypyc.primitives.registry import function_op, method_op + +# Lock() +function_op( + name="librt.threading.Lock", + arg_types=[], + return_type=lock_rprimitive, + c_function_name="LibRTThreading_Lock_new_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_THREADING], +) + +# Lock.acquire() -- blocking acquire, returns True unless it raises +lock_acquire_op = method_op( + name="acquire", + arg_types=[lock_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTThreading_Lock_acquire_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_THREADING], +) + +# Lock.acquire(blocking) -- acquire with explicit blocking argument +method_op( + name="acquire", + arg_types=[lock_rprimitive, bool_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTThreading_Lock_acquire_blocking_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_THREADING], +) + +# Lock.release() +lock_release_op = method_op( + name="release", + arg_types=[lock_rprimitive], + return_type=none_rprimitive, + c_function_name="LibRTThreading_Lock_release_internal", + error_kind=ERR_MAGIC, + dependencies=[LIBRT_THREADING], +) + +# Lock.locked() +method_op( + name="locked", + arg_types=[lock_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTThreading_Lock_locked_internal", + error_kind=ERR_NEVER, + dependencies=[LIBRT_THREADING], +) diff --git a/mypyc/primitives/registry.py b/mypyc/primitives/registry.py index 22422987b4277..060af0f88020c 100644 --- a/mypyc/primitives/registry.py +++ b/mypyc/primitives/registry.py @@ -411,6 +411,7 @@ def load_global_op(name: str, type: RType, src: str) -> LoadAddressDescription: import mypyc.primitives.int_ops import mypyc.primitives.librt_random_ops import mypyc.primitives.librt_strings_ops +import mypyc.primitives.librt_threading_ops import mypyc.primitives.librt_time_ops import mypyc.primitives.librt_vecs_ops import mypyc.primitives.list_ops diff --git a/mypyc/test-data/irbuild-threading.test b/mypyc/test-data/irbuild-threading.test new file mode 100644 index 0000000000000..652ccedcb9d75 --- /dev/null +++ b/mypyc/test-data/irbuild-threading.test @@ -0,0 +1,115 @@ +[case testLockBasics] +from librt.threading import Lock + +def lock_create() -> Lock: + return Lock() + +def lock_acquire(lk: Lock) -> bool: + return lk.acquire() + +def lock_release(lk: Lock) -> None: + lk.release() + +def lock_locked(lk: Lock) -> bool: + return lk.locked() +[out] +def lock_create(): + r0 :: librt.threading.Lock +L0: + r0 = LibRTThreading_Lock_new_internal() + return r0 +def lock_acquire(lk): + lk :: librt.threading.Lock + r0 :: bool +L0: + r0 = LibRTThreading_Lock_acquire_internal(lk) + return r0 +def lock_release(lk): + lk :: librt.threading.Lock + r0 :: None +L0: + r0 = LibRTThreading_Lock_release_internal(lk) + return 1 +def lock_locked(lk): + lk :: librt.threading.Lock + r0 :: bool +L0: + r0 = LibRTThreading_Lock_locked_internal(lk) + return r0 + +[case testLockAcquireRelease] +from librt.threading import Lock + +def acquire_release() -> None: + lk = Lock() + lk.acquire() + lk.release() +[out] +def acquire_release(): + r0, lk :: librt.threading.Lock + r1 :: bool + r2 :: None +L0: + r0 = LibRTThreading_Lock_new_internal() + lk = r0 + r1 = LibRTThreading_Lock_acquire_internal(lk) + r2 = LibRTThreading_Lock_release_internal(lk) + return 1 + +[case testLockAcquireBlocking] +from librt.threading import Lock + +def lock_acquire_blocking(lk: Lock, b: bool) -> bool: + return lk.acquire(b) +[out] +def lock_acquire_blocking(lk, b): + lk :: librt.threading.Lock + b, r0 :: bool +L0: + r0 = LibRTThreading_Lock_acquire_blocking_internal(lk, b) + return r0 + +[case testLockWith] +from librt.threading import Lock + +def with_lock(lk: Lock) -> None: + with lk: + a: list[int] = [] +[out] +def with_lock(lk): + lk :: librt.threading.Lock + r0 :: bool + r1, a :: list + r2, r3, r4 :: tuple[object, object, object] + r5 :: None + r6 :: bit +L0: + r0 = LibRTThreading_Lock_acquire_internal(lk) +L1: + r1 = PyList_New(0) + a = r1 +L2: +L3: + r2 = :: tuple[object, object, object] + r3 = r2 + goto L5 +L4: (handler for L1) + r4 = CPy_CatchError() + r3 = r4 +L5: + r5 = LibRTThreading_Lock_release_internal(lk) + if is_error(r3) goto L7 else goto L6 +L6: + CPy_Reraise() + unreachable +L7: + goto L11 +L8: (handler for L5, L6) + if is_error(r3) goto L10 else goto L9 +L9: + CPy_RestoreExcInfo(r3) +L10: + r6 = CPy_KeepPropagating() + unreachable +L11: + return 1 diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test new file mode 100644 index 0000000000000..8af8755b76c44 --- /dev/null +++ b/mypyc/test-data/run-threading.test @@ -0,0 +1,156 @@ +# Test cases for librt.threading (compile and run) + +[case testLockBasics_librt] +from typing import Any +from testutil import assertRaises +from librt.threading import Lock + +class BadBool: + def __bool__(self) -> bool: + raise RuntimeError("bad bool") + +def test_lock_basic() -> None: + lock = Lock() + assert not lock.locked() + assert lock.acquire() + assert lock.locked() + lock.release() + assert not lock.locked() + +def test_lock_context_manager() -> None: + lock = Lock() + with lock as acquired: + assert acquired is True + assert lock.locked() + assert not lock.locked() + +def test_lock_non_blocking() -> None: + lock = Lock() + assert lock.acquire() + assert not lock.acquire(False) + lock.release() + assert lock.acquire(False) + lock.release() + +def test_contention() -> None: + import threading + lock = Lock() + counter = [0] + n_threads = 4 + n_increments = 10000 + + def worker() -> None: + for _ in range(n_increments): + lock.acquire() + counter[0] += 1 + lock.release() + + threads = [threading.Thread(target=worker) for _ in range(n_threads)] + for t in threads: + t.start() + for t in threads: + t.join() + assert counter[0] == n_threads * n_increments + +def test_cross_thread_release() -> None: + # threading.Lock is unowned: a lock acquired on one thread may be + # released from another, after which another thread can acquire it. + import threading + lock = Lock() + assert lock.acquire() + + started = threading.Event() + released = threading.Event() + + def releaser() -> None: + started.set() + # Hand the lock off from a different thread than the acquirer. + lock.release() + released.set() + + t = threading.Thread(target=releaser) + t.start() + started.wait() + # This may block until releaser releases the lock on the other thread. + assert lock.acquire() + t.join() + assert released.is_set() + lock.release() + assert not lock.locked() + +def test_context_manager_exception() -> None: + lock = Lock() + try: + with lock: + assert lock.locked() + raise ValueError("test") + except ValueError: + pass + assert not lock.locked() + +def test_acquire_blocking_true() -> None: + lock = Lock() + assert lock.acquire(True) + assert lock.locked() + lock.release() + +def test_lock_constructor_errors() -> None: + lock_type: Any = Lock + make_type: Any = type + with assertRaises(TypeError): + lock_type(1) + with assertRaises(TypeError): + lock_type(foo=1) + with assertRaises(TypeError): + make_type("LockSubclass", (lock_type,), {}) + +def test_lock_acquire_argument_errors() -> None: + lock: Any = Lock() + with assertRaises(TypeError): + lock.acquire(True, False) + with assertRaises(TypeError): + lock.acquire(foo=True) + +def test_lock_acquire_blocking_truthiness() -> None: + lock: Any = Lock() + assert lock.acquire(blocking=True) + assert lock.locked() + assert not lock.acquire(blocking=False) + lock.release() + + assert lock.acquire(None) + assert lock.locked() + assert not lock.acquire(None) + lock.release() + + assert lock.acquire(1) + lock.release() + assert lock.acquire(0) + lock.release() + +def test_lock_acquire_blocking_bool_error() -> None: + lock: Any = Lock() + with assertRaises(RuntimeError, "bad bool"): + lock.acquire(BadBool()) + with assertRaises(RuntimeError, "bad bool"): + lock.acquire(blocking=BadBool()) + +def test_lock_exit_manual_call() -> None: + lock: Any = Lock() + lock.acquire() + assert lock.__exit__(None, None, None) is None + assert not lock.locked() + + lock.acquire() + assert lock.__exit__() is None + assert not lock.locked() + +def test_release_unlocked() -> None: + lock = Lock() + with assertRaises(RuntimeError): + lock.release() + # Also after acquire + release + lock.acquire() + lock.release() + with assertRaises(RuntimeError): + lock.release() diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index b11e425e51d3d..50ffc9004743e 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -61,6 +61,7 @@ "irbuild-librt-strings.test", "irbuild-librt-random.test", "irbuild-base64.test", + "irbuild-threading.test", "irbuild-time.test", "irbuild-match.test", ] diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index 9004a28ebf598..868f83654b47c 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -82,6 +82,7 @@ "run-base64.test", "run-librt-time.test", "run-librt-random.test", + "run-threading.test", "run-match.test", "run-vecs-i64-interp.test", "run-vecs-misc-interp.test", From e986558a7ce0c32a5115137e9a6cfa24984459ee Mon Sep 17 00:00:00 2001 From: esarp <11684270+esarp@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:40:02 -0500 Subject: [PATCH 110/127] [Chore] Update changelog for 2.2 (#21691) --- CHANGELOG.md | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2c9191b71a1..02739ef6591fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,283 @@ ## Next Release +## Mypy 2.2 + +We've just uploaded mypy 2.2.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). +Mypy is a static type checker for Python. This release includes new features, performance +improvements and bug fixes. You can install it as follows: + + python3 -m pip install -U mypy + +You can read the full documentation for this release on [Read the Docs](http://mypy.readthedocs.io). + +### Support for Closed TypedDicts (PEP 728) + +Mypy now supports closed TypedDicts as specified in PEP 728. A closed TypedDict cannot have extra +keys beyond those explicitly defined. This allows the type checker to determine that certain +operations are safe when they otherwise wouldn't be due to the potential presence of unknown keys. + +You can use the `closed` keyword argument with `TypedDict`: + +```python +HasName = TypedDict("HasName", {"name": str}) +HasOnlyName = TypedDict("HasOnlyName", {"name": str}, closed=True) +Movie = TypedDict("Movie", {"name": str, "year": int}) + +movie: Movie = {"name": "Nimona", "year": 2023} +has_name: HasName = movie # OK: HasName is open (default) +has_only_name: HasOnlyName = movie # Error: HasOnlyName is closed and Movie has extra "year" key +``` + +Closed TypedDicts enable more precise type checking because the type checker knows exactly which +keys are present. This is particularly useful when working with TypedDict unions or when you want +to ensure that a TypedDict conforms to an exact shape. + +The `closed` keyword also enables safe type narrowing with `in` checks: + +```python +Book = TypedDict('Book', {'book': str}, closed=True) +DVD = TypedDict('DVD', {'dvd': str}, closed=True) +type Inventory = Book | DVD + +def print_type(inventory: Inventory) -> None: + if "book" in inventory: + # Type is narrowed to Book here - safe because DVD is closed + print(inventory["book"]) + else: + # Type is narrowed to DVD here + print(inventory["dvd"]) +``` + +The `closed` keyword is also supported in class-based syntax: + +```python +class HasOnlyName(TypedDict, closed=True): + name: str +``` + +Note that closed TypedDicts are structural types, so a closed TypedDict is assignable to an open +TypedDict with the same keys, but not vice versa. + +Contributed by Alice (PR [21382](https://github.com/python/mypy/pull/21382)). + +### Complete Support for Type Variable Defaults (PEP 696) + +Mypy now has complete support for type variable defaults as specified in PEP 696. This allows you to +specify default values for type parameters in generic classes, functions, and type aliases. + +Traditional syntax (Python 3.11 and earlier): + +```python +T = TypeVar("T", default=int) # This means that if no type is specified T = int + +@dataclass +class Box(Generic[T]): + value: T | None = None + +reveal_type(Box()) # type is Box[int] +reveal_type(Box(value="Hello World!")) # type is Box[str] +``` + +New syntax (Python 3.12+): + +```python +class Box[T = int]: + def __init__(self, value: T) -> None: + self.value = value + +reveal_type(Box()) # type is Box[int] +reveal_type(Box(value="Hello World!")) # type is Box[str] +``` + +Type variable defaults work with all forms of generics, including classes, functions, and type aliases. +This release completes the implementation by fixing various edge cases involving recursive defaults, +dependencies between type variables, and interactions with variadic generics. + +Contributed by Ivan Levkivskyi (PRs [21491](https://github.com/python/mypy/pull/21491), +[21526](https://github.com/python/mypy/pull/21526), [21544](https://github.com/python/mypy/pull/21544)). + +### Respect Explicit Return Type of `__new__()` + +Mypy now respects explicitly annotated return types in `__new__()` methods. Previously, mypy would +always assume that `__new__()` returns an instance of the current class, ignoring explicit annotations. + +With this change, if you explicitly annotate a return type that differs from the implicit type, mypy +will use the explicit annotation: + +```python +class Factory: + def __new__(cls) -> Product: + return Product() + +reveal_type(Factory()) # type is Product, not Factory +``` + +Note that mypy still gives an error at the definition site if the explicit annotation is not a +subtype of the current class, since this is technically not type-safe. + +For backwards compatibility, there are two exceptions: +- If the return type is `Any`, mypy will still use the current class as the return type. +- If the explicit return type comes from a superclass and is a supertype of the implicit return type, + mypy will use the implicit (more specific) type: + +```python +class A: + def __new__(cls) -> A: ... + +reveal_type(A()) # type is A + +class B: + def __new__(cls) -> B: + return cls() + +class C(B): ... +reveal_type(C()) # type is C +``` + +This fixes several long-standing issues where explicit `__new__()` return types were ignored. + +Contributed by Ivan Levkivskyi (PR [21441](https://github.com/python/mypy/pull/21441)). + +### TypeForm Support No Longer Experimental + +Support for `TypeForm` is no longer experimental. `TypeForm` (introduced in Python 3.14) allows you +to annotate parameters that accept type expressions, providing better type checking for functions +that work with types as values. + +```python +from typing import TypeForm + +def make_list(tp: TypeForm[T]) -> list[T]: + ... + +# Correctly typed as list[int] +int_list = make_list(int) +``` + +`TypeForm` support was previously reverted from mypy 2.1 due to a performance regression, but this +has now been mitigated. + +Contributed by Ivan Levkivskyi and Jelle Zijlstra (PRs [21262](https://github.com/python/mypy/pull/21262), [21591](https://github.com/python/mypy/pull/21591), +[21459](https://github.com/python/mypy/pull/21459)). + +### Experimental WASM Wheel for Python 3.14 + +Mypy now ships an experimental WebAssembly (WASM) wheel for Python 3.14. This allows mypy to run +in WASM environments such as Pyodide and browser-based Python implementations. + +The WASM wheel is considered experimental and may have limitations compared to native builds. Please +report any issues you encounter when using mypy in WASM environments. + +Contributed by Ivan Levkivskyi (PR [21671](https://github.com/python/mypy/pull/21671)). + +### Mypyc Free-threading Improvements + +- Make function wrappers thread-safe on free-threaded builds (Jukka Lehtosalo, PR [21620](https://github.com/python/mypy/pull/21620)) +- Make list remove and index thread-safe on free-threaded builds (Jukka Lehtosalo, PR [21614](https://github.com/python/mypy/pull/21614)) +- Fix dict iteration memory safety on free-threaded builds (Jukka Lehtosalo, PR [21617](https://github.com/python/mypy/pull/21617)) +- Make some dict primitives thread-safe on free-threading builds (Jukka Lehtosalo, PR [21616](https://github.com/python/mypy/pull/21616)) +- Fix free-threading race condition in argument parsing (Jukka Lehtosalo, PR [21613](https://github.com/python/mypy/pull/21613)) +- Document free threading and other doc updates (Jukka Lehtosalo, PR [21494](https://github.com/python/mypy/pull/21494)) + +### `librt.strings` Updates + +- Add `librt.strings.toupper` and `librt.strings.tolower` codepoint primitives (Vaggelis Danias, PR [21553](https://github.com/python/mypy/pull/21553)) +- Add `librt.strings.isidentifier` codepoint primitive (Vaggelis Danias, PR [21522](https://github.com/python/mypy/pull/21522)) +- Add `librt.strings.isalpha` codepoint primitive (Vaggelis Danias, PR [21521](https://github.com/python/mypy/pull/21521)) +- Add `librt.strings.isalnum` codepoint primitive (Vaggelis Danias, PR [21509](https://github.com/python/mypy/pull/21509)) +- Add `librt.strings.isdigit` codepoint primitive (Vaggelis Danias, PR [21504](https://github.com/python/mypy/pull/21504)) +- Add `librt.strings.isspace` char primitive (Vaggelis Danias, PR [21462](https://github.com/python/mypy/pull/21462)) + +### Mypyc Improvements + +- Fix name lookup when class var and module var have the same name (Jukka Lehtosalo, PR [21594](https://github.com/python/mypy/pull/21594)) +- Report file and line number on uncaught exceptions (Jukka Lehtosalo, PR [21584](https://github.com/python/mypy/pull/21584)) +- Use `other` arg instead of `self` for RHS type (Ryan Heard, PR [21569](https://github.com/python/mypy/pull/21569)) +- Use `method_sig` to get the method signature (Ryan Heard, PR [21567](https://github.com/python/mypy/pull/21567)) +- Preserve inherited attribute defaults under `separate=True` (Jo, PR [21547](https://github.com/python/mypy/pull/21547)) +- Fix missing cross-group header deps in incremental builds (Jo, PR [21490](https://github.com/python/mypy/pull/21490)) +- Fix cross-group call to inherited `__mypyc_defaults_setup` (Jo, PR [21481](https://github.com/python/mypy/pull/21481)) +- Fix non-deterministic class struct layout under `separate=True` (Vaggelis Danias, PR [21530](https://github.com/python/mypy/pull/21530)) +- Specialize `s[i] == 'x'` to a codepoint int compare (Vaggelis Danias, PR [21579](https://github.com/python/mypy/pull/21579)) +- Fix reference leak in mypyc bytes concatenation (Colinxu2020, PR [21469](https://github.com/python/mypy/pull/21469)) + +### Fixes to Crashes + +- Fix crash on invalid recursive variadic alias (Ivan Levkivskyi, PR [21572](https://github.com/python/mypy/pull/21572)) +- Fix crashes on variadic unpacking in synthetic types (Ivan Levkivskyi, PR [21555](https://github.com/python/mypy/pull/21555)) +- Fix crash on unhandled meet variadic tuple vs instance (Ivan Levkivskyi, PR [21558](https://github.com/python/mypy/pull/21558)) +- Fix crash on deferred generic class nested in function (Ivan Levkivskyi, PR [21557](https://github.com/python/mypy/pull/21557)) +- Fix crash in new-style type alias with variadic unpack (Ivan Levkivskyi, PR [21551](https://github.com/python/mypy/pull/21551)) +- Fix various crashes on recursive type variable defaults (Ivan Levkivskyi, PR [21491](https://github.com/python/mypy/pull/21491)) +- Fix crash for empty `Annotated` type application (Rayan Salhab, PR [21503](https://github.com/python/mypy/pull/21503)) +- Fix crash on `Unpack` used without arguments in class bases (Sai Asish Y, PR [21470](https://github.com/python/mypy/pull/21470)) + +### Performance Improvements + +- Memoize the options snapshot (Kevin Kannammalil, PR [21354](https://github.com/python/mypy/pull/21354)) +- Don't include `not_ready_deps` tracking as relating to mypy internals (Kevin Kannammalil, PR [21389](https://github.com/python/mypy/pull/21389)) +- Speed up transitive dependency hash for singleton SCCs (Kevin Kannammalil, PR [21390](https://github.com/python/mypy/pull/21390)) +- Optimize typeform checks (Jelle Zijlstra, PR [21459](https://github.com/python/mypy/pull/21459)) + +### Improvements to the Native Parser + +- Support `--shadow-file` with `--native-parser` (Jukka Lehtosalo, PR [21623](https://github.com/python/mypy/pull/21623)) +- Add Python version checks to native parser (Kevin Kannammalil, PR [21539](https://github.com/python/mypy/pull/21539)) +- Allow nativeparse to parse source code directly (bzoracler, PR [21260](https://github.com/python/mypy/pull/21260)) + +### Other Notable Fixes and Improvements + +- Add function definition notes for `too many positional arguments` errors (Kevin Kannammalil, PR [21410](https://github.com/python/mypy/pull/21410)) +- Fix the exportjson tool (.ff cache to .json conversion) (Jukka Lehtosalo, PR [21628](https://github.com/python/mypy/pull/21628)) +- Support floats in JSON in fixed-format cache (Ivan Levkivskyi, PR [21603](https://github.com/python/mypy/pull/21603)) +- Update `TypedDictType.__init__` signature to preserve backward compat (Jukka Lehtosalo, PR [21590](https://github.com/python/mypy/pull/21590)) +- Fix constructor calls for union-bounded `TypeVar`s (Jingchen Ye, PR [21571](https://github.com/python/mypy/pull/21571)) +- Fix `TypedDict` indexing with literal keys in comprehensions (Jingchen Ye, PR [21556](https://github.com/python/mypy/pull/21556)) +- Correctly handle empty tuple index when unpacked (Ivan Levkivskyi, PR [21545](https://github.com/python/mypy/pull/21545)) +- Support protocol checks for self-types in tuple types (Ivan Levkivskyi, PR [21535](https://github.com/python/mypy/pull/21535)) +- Fix edge cases in variadic tuple subclasses (Ivan Levkivskyi, PR [21518](https://github.com/python/mypy/pull/21518)) +- Special-case constructor for tuple types (Ivan Levkivskyi, PR [21502](https://github.com/python/mypy/pull/21502)) +- Fix false positive "Expected TypedDict key to be string literal" for `Union[TypedDict, dict[K, V]]` (Zakir Jiwani, PR [21511](https://github.com/python/mypy/pull/21511)) +- Use explicit `Never` for type inference (Ivan Levkivskyi, PR [21497](https://github.com/python/mypy/pull/21497)) +- Narrow membership in statically known containers (Shantanu, PR [21461](https://github.com/python/mypy/pull/21461)) +- Improve negative narrowing for membership checks on tuples (Shantanu, PR [21456](https://github.com/python/mypy/pull/21456)) +- Analyze `TypedDict` decorators (Pranav Manglik, PR [21267](https://github.com/python/mypy/pull/21267)) +- Start testing Python 3.15 (Marc Mueller, PR [21439](https://github.com/python/mypy/pull/21439)) +- Improved handling of `NamedTuple`, `TypedDict`, `Enum`, and regular classes nested in functions (Ivan Levkivskyi, PR [21478](https://github.com/python/mypy/pull/21478)) + +### Typeshed Updates + +Please see [git log](https://github.com/python/typeshed/commits/main?after=616424285beccaa76f90e87e1e922b1dc68710ca+0&branch=main&path=stdlib) for full list of standard library typeshed stub changes. + +### Acknowledgements + +Thanks to all mypy contributors who contributed to this release: + +- Adam Turner +- alicederyn +- bzoracler +- Colinxu2020 +- georgesittas +- Ivan Levkivskyi +- Jelle Zijlstra +- Jingchen Ye +- Jukka Lehtosalo +- Kevin Kannammalil +- lphuc2250gma +- Marc Mueller +- Pranav Manglik +- Rayan Salhab +- Ryan Heard +- Sai Asish Y +- Shantanu +- sobolevn +- Vaggelis Danias +- Victor Letichevsky +- Zakir Jiwani + +I'd also like to thank my employer, Dropbox, for supporting mypy development. + ## Mypy 2.1 We’ve just uploaded mypy 2.1.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). From b6d93f5a26abcb8554d9938b162ce3eda74b4ea8 Mon Sep 17 00:00:00 2001 From: ygale Date: Tue, 7 Jul 2026 22:54:01 +0300 Subject: [PATCH 111/127] Fix regression in dataclass narrowing for Python >= 3.13 (#21675) Fixes #21635 On Python >= 3.13, `@dataclass` synthesizes a `__replace__(self, ...) -> Self` method to support `copy.replace()`. When mypy tries to narrow a `type[A]` expression via `issubclass(cls, M)` (or `isinstance`) against a second, unrelated dataclass `M`, it builds an ad-hoc `` type to check whether that narrowing is sound (`intersect_instances` in `checker.py`). Building that ad-hoc type runs `check_multiple_inheritance`, which sees `A`'s synthesized `__replace__` returning `A` and `M`'s returning `M`, and flags them as incompatible. That's a false positive: a real subclass of both (e.g. `class C(M, A)`, itself decorated with `@dataclass`) gets its *own* freshly synthesized, mutually compatible `__replace__`. --- mypy/checker.py | 2 +- test-data/unit/check-dataclasses.test | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index d13b927b28f2f..212ddd65d9122 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3133,7 +3133,7 @@ class C(B, A[int]): ... # this is unsafe because... x: A[int] = C() x.foo # ...runtime type is (str) -> None, while static type is (int) -> None """ - if name in ("__init__", "__new__", "__init_subclass__"): + if name in {"__init__", "__new__", "__init_subclass__", "__replace__"}: # __init__ and friends can be incompatible -- it's a special case. return first = base1.names[name] diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 54b3afadc8b32..5ac0318421167 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2609,6 +2609,27 @@ class Y(X): [builtins fixtures/tuple.pyi] +[case testDunderReplaceDoesNotBlockPlainIssubclassNarrowing] +# https://github.com/python/mypy/issues/21635 +# flags: --python-version 3.13 +from dataclasses import dataclass + +@dataclass +class A: ... +@dataclass +class M: ... +@dataclass +class B(A): ... +@dataclass +class C(M, A): ... + +cls: type[A] = C +if issubclass(cls, M): + reveal_type(cls) # N: Revealed type is "type[__main__.]" + n: int = 'foo' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/isinstancelist.pyi] + + [case testFrozenWithFinal] from dataclasses import dataclass from typing import Final From f5163c011078ef66753cdf706b7b2dd14da401ab Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:52:31 -0700 Subject: [PATCH 112/127] Fix variance inference issues caused by dataclass replace (#21694) Fixes #17623. We already special case `__init__` and `__new__`, so adding `__replace__` here feels alright Along with #21675 should hopefully get rid of this surprising edge when moving to Python 3.13 I didn't add tests due to fixture issues --- mypy/subtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 259bb3791deaf..2d97dd534d9f5 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2278,7 +2278,7 @@ def infer_variance(info: TypeInfo, i: int) -> bool: self_type = fill_typevars(info) for member in all_non_object_members(info): # __mypy-replace is an implementation detail of the dataclass plugin - if member in ("__init__", "__new__", "__mypy-replace"): + if member in {"__init__", "__new__", "__replace__", "__mypy-replace"}: continue if isinstance(self_type, TupleType): From 89deb7db255fb414aab70d3fb77b4f2209f2b326 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 8 Jul 2026 11:20:53 +0100 Subject: [PATCH 113/127] [mypyc] Document recent additions to `librt.strings`, such as `ispace` (#21696) Document `is*` functions, `toupper` and `tolower`. --- mypyc/doc/librt_strings.rst | 62 ++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/mypyc/doc/librt_strings.rst b/mypyc/doc/librt_strings.rst index aece015f4f752..6bb4beef6f25e 100644 --- a/mypyc/doc/librt_strings.rst +++ b/mypyc/doc/librt_strings.rst @@ -15,7 +15,7 @@ Thread safety ``BytesWriter`` and ``StringWriter`` objects are unsafe to access from another thread if they are concurrently modified (on free-threaded Python builds). They are optimized for maximal performance, and they aren't fully synchronized. Read-only access from multiple -threads is safe. +threads is safe, as always. BytesWriter ^^^^^^^^^^^ @@ -101,6 +101,9 @@ StringWriter Functions --------- +Reading and writing binary data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + The ``write_*`` and ``read_*`` functions allow interpreting bytes as packed binary data. They can be used as (much) more efficient but lower-level alternatives to the stdlib :mod:`struct` module in compiled code. @@ -210,3 +213,60 @@ This example writes two binary values and reads them afterwards:: Read a 64-bit floating-point value starting at the given index as a big-endian binary value (8 bytes). + +Code point classification +^^^^^^^^^^^^^^^^^^^^^^^^^ + +These functions classify a single Unicode code point, passed as an ``i32`` integer. They are +faster alternatives to calling the corresponding :py:class:`str` methods on a one-character +string in compiled code. A code point is often obtained via ``ord(s[i])``, which is a +fast operation in compiled code when ``s`` has type :py:class:`str`. + +Each function agrees with the matching :py:class:`str` method applied to the one-character +string ``chr(c)``. Out-of-range inputs (negative values, or values past the maximum Unicode +code point ``0x10FFFF``) return ``False``. + +.. function:: isspace(c: i32, /) -> bool + + Return whether the code point is whitespace. Equivalent to ``chr(c).isspace()``. + +.. function:: isalpha(c: i32, /) -> bool + + Return whether the code point is alphabetic. Equivalent to ``chr(c).isalpha()``. + +.. function:: isdigit(c: i32, /) -> bool + + Return whether the code point is a digit. Equivalent to ``chr(c).isdigit()``. + +.. function:: isalnum(c: i32, /) -> bool + + Return whether the code point is alphanumeric. Equivalent to ``chr(c).isalnum()``. + +.. function:: isidentifier(c: i32, /) -> bool + + Return whether the code point is valid as the first character of a Python identifier. + Equivalent to ``chr(c).isidentifier()``. + +Code point case conversion +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These functions convert the case of a single Unicode code point, passed as an ``i32`` integer, +and return the converted code point as an ``i32``. They are faster alternatives to +:py:meth:`str.upper` / :py:meth:`str.lower` on a one-character string in compiled code. + +For the rare code points whose Unicode uppercase or lowercase form has multiple code points +(e.g. U+00DF ``ß`` has the upper case form ``"SS"``, and U+FB01 ``fi`` maps to ``"FI"``), the +input is returned unchanged, so the signature can stay ``i32 -> i32``. Use :py:meth:`str.upper` +/ :py:meth:`str.lower` when full Unicode case conversion matters, or implement the logic to +handle the special cases explicitly. Out-of-range inputs (negative values, or values past the +maximum Unicode code point ``0x10FFFF``) are returned unchanged. + +.. function:: toupper(c: i32, /) -> i32 + + Return the uppercase of the code point, or the input unchanged if the uppercase does not + consist of exactly one code point. + +.. function:: tolower(c: i32, /) -> i32 + + Return the lowercase of the code point, or the input unchanged if the lowercase does not + consist of exactly one code point. From 732802c8b6a3859b8d2160404e6410690c214409 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 8 Jul 2026 13:44:37 +0100 Subject: [PATCH 114/127] [mypyc] Document librt.threading (#21697) --- mypyc/doc/index.rst | 1 + mypyc/doc/librt.rst | 2 ++ mypyc/doc/librt_threading.rst | 54 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 mypyc/doc/librt_threading.rst diff --git a/mypyc/doc/index.rst b/mypyc/doc/index.rst index aacf275de9885..3f74595dd47fb 100644 --- a/mypyc/doc/index.rst +++ b/mypyc/doc/index.rst @@ -35,6 +35,7 @@ generate fast code. librt_base64 librt_random librt_strings + librt_threading librt_time librt_vecs diff --git a/mypyc/doc/librt.rst b/mypyc/doc/librt.rst index 23206f8cfe806..681efe47e7603 100644 --- a/mypyc/doc/librt.rst +++ b/mypyc/doc/librt.rst @@ -30,6 +30,8 @@ Follow submodule links in the table to a detailed description of each submodule. - Pseudorandom number generation * - :doc:`librt.strings ` - String and bytes utilities + * - :doc:`librt.threading ` + - Threading primitives * - :doc:`librt.time ` - Time utilities * - :doc:`librt.vecs ` diff --git a/mypyc/doc/librt_threading.rst b/mypyc/doc/librt_threading.rst new file mode 100644 index 0000000000000..c6cb5c9e84e72 --- /dev/null +++ b/mypyc/doc/librt_threading.rst @@ -0,0 +1,54 @@ +.. _librt-threading: + +librt.threading +=============== + +The ``librt.threading`` module is part of the ``librt`` package on PyPI, and it includes +threading primitives. + +Classes +------- + +Lock +^^^^ + +.. class:: Lock + + A fast mutual exclusion lock. This can be used as a faster replacement for + :py:class:`threading.Lock` in compiled code. + + Like :py:class:`threading.Lock`, a ``Lock`` is *unowned*: it may be released by a thread + other than the one that acquired it, and it doesn't support reentrant (recursive) locking. + A newly created lock is unlocked. + + ``Lock`` can be used as a context manager. The lock is acquired (blocking) on entry and + released on exit, including when the body raises an exception:: + + def example(lock: Lock) -> None: + with lock: + ... # Critical section; the lock is held here. + + ``Lock`` cannot be subclassed. ``Lock`` cannot be used with :py:class:`threading.Condition`. + + .. method:: acquire(blocking: bool = True) -> bool + + Acquire the lock. + + When *blocking* is true (the default), block (if needed) until the lock is available, + acquire it, and return ``True``. When *blocking* is false, acquire the lock only if it + can be done without blocking: return ``True`` if the lock could be acquired, or + ``False`` otherwise (it was already locked by some thread). + + Unlike :py:meth:`threading.Lock.acquire`, there is no *timeout* argument. + + .. method:: release() -> None + + Release the lock, allowing another thread (if any) that is blocked on :meth:`acquire` + to proceed. Since the lock is unowned, it may be released from a thread other than the + one that acquired it. + + Raise :py:exc:`RuntimeError` if the lock is not currently held. + + .. method:: locked() -> bool + + Return ``True`` if the lock is currently held (by any thread). From 7a45d255818e972b293825d701393732b971c4b3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 8 Jul 2026 14:27:46 +0100 Subject: [PATCH 115/127] Bump librt to 0.13.0 (#21698) This includes `librt.threading.Lock`. --- mypy-requirements.txt | 2 +- pyproject.toml | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy-requirements.txt b/mypy-requirements.txt index d2991309e391e..87663f2adde40 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -5,5 +5,5 @@ typing_extensions>=4.14.0; python_version>='3.15' mypy_extensions>=1.0.0 pathspec>=1.0.0 tomli>=1.1.0; python_version<'3.11' -librt>=0.12.0; platform_python_implementation != 'PyPy' +librt>=0.13.0; platform_python_implementation != 'PyPy' ast-serialize>=0.6.0,<1.0.0 diff --git a/pyproject.toml b/pyproject.toml index f166a58086821..aa9432c06e48b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires = [ "mypy_extensions>=1.0.0", "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.12.0; platform_python_implementation != 'PyPy'", + "librt>=0.13.0; platform_python_implementation != 'PyPy'", # the following is from build-requirements.txt "types-psutil", "types-setuptools", @@ -58,7 +58,7 @@ dependencies = [ "mypy_extensions>=1.0.0", "pathspec>=1.0.0", "tomli>=1.1.0; python_version<'3.11'", - "librt>=0.12.0; platform_python_implementation != 'PyPy'", + "librt>=0.13.0; platform_python_implementation != 'PyPy'", "ast-serialize>=0.6.0,<1.0.0", ] dynamic = ["version"] diff --git a/test-requirements.txt b/test-requirements.txt index e79319fdbeece..73d5966040c4a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -25,7 +25,7 @@ identify==2.6.19 # via pre-commit iniconfig==2.3.0 # via pytest -librt==0.12.0 ; platform_python_implementation != "PyPy" +librt==0.13.0 ; platform_python_implementation != "PyPy" # via -r mypy-requirements.txt lxml==6.1.0 ; python_version < "3.15" # via -r test-requirements.in From e68ecefd1884f7e53e8ac45f0373e60c717e4753 Mon Sep 17 00:00:00 2001 From: Jingchen Ye <11172084+97littleleaf11@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:43:16 +0800 Subject: [PATCH 116/127] Infer Coroutine for unannotated async defs (#21651) Fixes #12776 Add an extra check for unannotated async defs when checking function type. `async def foo()` is now treated like `async def foo() -> Any` --- mypy/checker.py | 15 +++++++++++++- mypy/checker_shared.py | 6 ++++++ mypy/checkexpr.py | 3 +-- mypy/checkmember.py | 7 +++---- test-data/unit/check-async-await.test | 28 +++++++++++++++++++++++++++ test-data/unit/check-flags.test | 2 +- 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 212ddd65d9122..5190e25844029 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8129,7 +8129,20 @@ def iterable_item_type(self, it: ProperType, context: Context) -> Type: return self.analyze_iterable_item_type_without_expression(it, context)[1] def function_type(self, func: FuncBase) -> FunctionLike: - return function_type(func, self.named_type("builtins.function")) + typ = function_type(func, self.named_type("builtins.function")) + if ( + isinstance(func, FuncItem) + and func.is_coroutine + and not func.is_async_generator + and func.type is None + and isinstance(typ, CallableType) + ): + any_type = AnyType(TypeOfAny.special_form) + ret_type = self.named_generic_type( + "typing.Coroutine", [any_type, any_type, typ.ret_type] + ) + return typ.copy_modified(ret_type=ret_type) + return typ def push_type_map(self, type_map: TypeMap, *, from_assignment: bool = True) -> None: if is_unreachable_map(type_map): diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 2b25d3e73a6fe..c1c4cee748b32 100644 --- a/mypy/checker_shared.py +++ b/mypy/checker_shared.py @@ -16,6 +16,7 @@ ArgKind, Context, Expression, + FuncBase, FuncItem, LambdaExpr, MypyFile, @@ -28,6 +29,7 @@ from mypy.plugin import CheckerPluginInterface, Plugin from mypy.types import ( CallableType, + FunctionLike, Instance, LiteralValue, Overloaded, @@ -149,6 +151,10 @@ def expr_checker(self) -> ExpressionCheckerSharedApi: def named_type(self, name: str) -> Instance: raise NotImplementedError + @abstractmethod + def function_type(self, func: FuncBase) -> FunctionLike: + raise NotImplementedError + @abstractmethod def lookup_typeinfo(self, fullname: str) -> TypeInfo: raise NotImplementedError diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 44855f49afaf9..172d44555b946 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -150,7 +150,6 @@ false_only, fixup_partial_type, freeze_all_type_vars, - function_type, get_all_type_vars, get_type_vars, is_literal_type_like, @@ -415,7 +414,7 @@ def analyze_static_reference( if isinstance(node, (Var, Decorator, OverloadedFuncDef)): return node.type or AnyType(TypeOfAny.special_form) elif isinstance(node, FuncDef): - return function_type(node, self.named_type("builtins.function")) + return self.chk.function_type(node) elif isinstance(node, TypeInfo): # Reference to a type object. if node.typeddict_type: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index e75a8ed7a5b03..3ba99d8e8c6b2 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -47,7 +47,6 @@ bind_self, erase_to_bound, freeze_all_type_vars, - function_type, get_all_type_vars, make_simplified_union, supported_self_type, @@ -360,7 +359,7 @@ def analyze_instance_member_access( if mx.is_lvalue and not mx.suppress_errors: mx.msg.cant_assign_to_method(mx.context) if not isinstance(method, OverloadedFuncDef): - signature = function_type(method, mx.named_type("builtins.function")) + signature = mx.chk.function_type(method) else: if method.type is None: # Overloads may be not ready if they are decorated. Handle this in same @@ -1325,7 +1324,7 @@ def analyze_class_attribute_access( return AnyType(TypeOfAny.from_error) else: assert isinstance(node.node, SYMBOL_FUNCBASE_TYPES) - typ = function_type(node.node, mx.named_type("builtins.function")) + typ = mx.chk.function_type(node.node) # Note: if we are accessing class method on class object, the cls argument is bound. # Annotated and/or explicit class methods go through other code paths above, for # unannotated implicit class methods we do this here. @@ -1490,7 +1489,7 @@ def analyze_decorator_or_funcbase_access( """ if isinstance(defn, Decorator): return analyze_var(name, defn.var, itype, mx) - typ = function_type(defn, mx.chk.named_type("builtins.function")) + typ = mx.chk.function_type(defn) if isinstance(defn, (FuncDef, OverloadedFuncDef)) and defn.is_trivial_self: return bind_self_fast(typ, mx.self_type) typ = check_self_arg(typ, mx.self_type, defn.is_class, mx.context, name, mx.msg) diff --git a/test-data/unit/check-async-await.test b/test-data/unit/check-async-await.test index e887b4c575528..cd40f2cf29a65 100644 --- a/test-data/unit/check-async-await.test +++ b/test-data/unit/check-async-await.test @@ -839,6 +839,34 @@ def bar() -> None: [builtins fixtures/async_await.pyi] [typing fixtures/typing-async.pyi] +[case testUntypedAsyncFunctionAndMethodReturnCoroutine] +# flags: --show-error-codes +async def foo(): + pass + +class C: + async def method(self): + pass + +def f(c: C) -> None: + a = foo() + b = c.method() + reveal_type(foo) # N: Revealed type is "def () -> typing.Coroutine[Any, Any, Any]" + reveal_type(a) # E: Value of type "Coroutine[Any, Any, Any]" must be used [unused-coroutine] \ + # N: Are you missing an await? \ + # N: Revealed type is "typing.Coroutine[Any, Any, Any]" + foo() # E: Value of type "Coroutine[Any, Any, Any]" must be used [unused-coroutine] \ + # N: Are you missing an await? + reveal_type(c.method) # N: Revealed type is "def () -> typing.Coroutine[Any, Any, Any]" + reveal_type(b) # E: Value of type "Coroutine[Any, Any, Any]" must be used [unused-coroutine] \ + # N: Are you missing an await? \ + # N: Revealed type is "typing.Coroutine[Any, Any, Any]" + c.method() # E: Value of type "Coroutine[Any, Any, Any]" must be used [unused-coroutine] \ + # N: Are you missing an await? + +[builtins fixtures/async_await.pyi] +[typing fixtures/typing-async.pyi] + [case testAsyncForOutsideCoroutine] async def g(): yield 0 diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index dd4687181ca41..acdee2a10430a 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -59,7 +59,7 @@ async def f(): # E: Function is missing a return type annotation \ # N: Use "-> None" if function does not return a value pass [builtins fixtures/async_await.pyi] -[typing fixtures/typing-medium.pyi] +[typing fixtures/typing-async.pyi] [case testAsyncUnannotatedArgument] # flags: --disallow-untyped-defs From 77ccc7a4d0e15aa0b7f905e348e1c66e37c5d646 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:48:33 -0700 Subject: [PATCH 117/127] Fix custom equality handling for membership narrowing in static containers (#21706) This was an issue in the new feature #21461 In narrow_type_by_identity_equality we check for custom equality before coercing to literals, and this was done the wrong way round here Fixes #21703 --- mypy/checker.py | 10 +++++++--- test-data/unit/check-narrowing.test | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5190e25844029..3a98505cc87ec 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6858,9 +6858,13 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa for known_item in container_item_types: # Match the should_coerce_literals logic from narrow_type_by_identity_equality p_known_item = get_proper_type(known_item) - if is_literal_type_like(p_known_item) or ( - isinstance(p_known_item, Instance) and p_known_item.type.is_enum - ): + if ( + is_literal_type_like(p_known_item) + or ( + isinstance(p_known_item, Instance) + and p_known_item.type.is_enum + ) + ) and not has_custom_eq_checks(p_known_item): known_item = coerce_to_literal(known_item) if_map, else_map = self.narrow_type_by_identity_equality( "==", diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 7bab7baa6cebd..29f4cd47929d9 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3335,6 +3335,24 @@ def narrow_dict(x: Literal['a', 'b', 'c'], t: dict[Literal['a', 'b'], int]): [builtins fixtures/primitives.pyi] +[case testNarrowCustomEqEnumInLiteralContainer] +# flags: --strict-equality --warn-unreachable +# https://github.com/python/mypy/issues/21703 +from enum import Enum + +class E(Enum): + foo = 1 + bar = 2 + + def __eq__(self, other: object) -> bool: + return True + +def f(x: int) -> None: + if x in [E.foo, E.bar]: + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/list.pyi] + + [case testNarrowingLiteralInLiteralContainer] # flags: --strict-equality --warn-unreachable from typing import Literal From af2bc0f3cc7f2f129f0c11294158d0c292692c3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:46:12 -0700 Subject: [PATCH 118/127] Sync typeshed (#21707) Source commit: https://github.com/python/typeshed/commit/f76037a1eb3923c67a8bc0e302ee9c016ffb3431 --- mypy/typeshed/stdlib/builtins.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi index 7b659e5476d59..8497f9e2e1037 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi @@ -1277,7 +1277,6 @@ if sys.version_info >= (3, 15): cls: type[frozendict[str, _VT]], iterable: Iterable[tuple[str, _VT]], /, **kwargs: _VT ) -> frozendict[str, _VT]: ... - def __init__(self) -> None: ... def copy(self) -> frozendict[_KT, _VT]: ... @overload From cbcb51add3094ec91b29cdd4c624943bf251b63f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:55:18 -0700 Subject: [PATCH 119/127] Narrow for frozendict membership check (#21709) --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index 3a98505cc87ec..612d2face5b8a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8892,6 +8892,7 @@ def builtin_item_type(tp: Type) -> Type | None: "builtins.list", "builtins.tuple", "builtins.dict", + "builtins.frozendict", "builtins.set", "builtins.frozenset", "_collections_abc.dict_keys", From b5be217392b9b2771d1764066b9d600bf93ce7a8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 9 Jul 2026 15:35:40 +0100 Subject: [PATCH 120/127] [mypyc] Update free threading Python compatibility docs (#21711) We've made a bunch of compatibility improvements on free threaded builds recently. --- mypyc/doc/differences_from_python.rst | 44 +++++++++++++++------------ mypyc/doc/librt_threading.rst | 2 ++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/mypyc/doc/differences_from_python.rst b/mypyc/doc/differences_from_python.rst index 414476723125b..a139fab3aa2fb 100644 --- a/mypyc/doc/differences_from_python.rst +++ b/mypyc/doc/differences_from_python.rst @@ -265,25 +265,31 @@ also be modified. Free threading -------------- -Mypyc has basic support for free threading, but it doesn't provide the -same memory safety guarantees as Python in compiled modules, since -in current Python versions this would cause an unacceptable performance -impact. - -The exact details of the memory safety in the presence of data races -are likely to evolve in the future. Currently, compiled code must -ensure that proper synchronization is used to prevent data races. In -particular, these operations require explicit synchronization, such as -via ``threading.Lock``, if there is a possibility of data races -(the list is not exhaustive): - -* Reads or writes of non-final instance data attributes of native - classes. -* List item access or iteration using a ``list`` static type (using - ``Sequence`` or ``MutableSequence`` as the type ensure correct - implicit synchronization). -* Dict item access using a static ``dict`` type (using ``Mapping`` - or ``MutableMapping`` ensures correct implicit synchronization). +Mypyc supports free threading, but it doesn't provide the exact +memory safety guarantees as Python in compiled modules under +free threading when there are race conditions. + +Additionally, optimized primitive operations in compiled code may have +different atomicity properties compared to CPython. Use explicit +synchronization if code depends on operations being atomic. This is +already the recommended approach for normal Python code. + +Currently, compiled code must ensure that proper synchronization is +used to prevent data races involving non-final attributes in native +classes, unless the attribute has a value type such as ``bool``, +``float`` or ``i64``. You can use explicit +synchronization, such as via +:ref:`librt.threading.Lock ` (or +:py:class:`threading.Lock`, which is less efficient than +``librt.threading.Lock``) if there is a possibility of such a data +race. + +.. note:: + + We are working on improving memory safety in free-threading + builds of Python, and hope to make all normal Python features + memory safe, while providing more efficient but less safe + opt-in, non-standard features. As libraries often won't be able to control the concurrent access by user code, we recommend that modules document that multi-threaded diff --git a/mypyc/doc/librt_threading.rst b/mypyc/doc/librt_threading.rst index c6cb5c9e84e72..0af795bc0daab 100644 --- a/mypyc/doc/librt_threading.rst +++ b/mypyc/doc/librt_threading.rst @@ -9,6 +9,8 @@ threading primitives. Classes ------- +.. _librt-threading-lock: + Lock ^^^^ From 24c237d85b48f618e655ffff1dc0f19089d9b599 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 9 Jul 2026 17:29:52 +0100 Subject: [PATCH 121/127] [mypyc] Improve documentation of Final (#21713) The semantics of Final were changed recently: mention that final instance attributes can't be rebound. Also add more detail and a new example. --- mypyc/doc/differences_from_python.rst | 50 +++++++++++++++++++++------ 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/mypyc/doc/differences_from_python.rst b/mypyc/doc/differences_from_python.rst index a139fab3aa2fb..c65c330edbef3 100644 --- a/mypyc/doc/differences_from_python.rst +++ b/mypyc/doc/differences_from_python.rst @@ -237,28 +237,56 @@ nested function calls, typically due to out-of-control recursion. Final values ------------ -Compiled code replaces a reference to an attribute declared ``Final`` with -the value of the attribute computed at compile time. This is an example of -:ref:`early binding `. Example:: +Mypy treats variables and attributes defined as ``Final`` specially. +For instance attributes of native classes, mypyc prevents reassignment +of these attributes. + +Mypyc replaces references to a variable or attribute declared ``Final`` +with the value of the attribute computed at compile time, when it can +be determined during compilation. Example:: MAX: Final = 100 def limit_to_max(x: int) -> int: - if x > MAX: - return MAX - return x + if x > MAX: + return MAX + return x The two references to ``MAX`` don't involve any module namespace lookups, and are equivalent to this code:: def limit_to_max(x: int) -> int: - if x > 100: - return 100 - return x + if x > 100: + return 100 + return x When run as interpreted, the first example will execute slower due to -the extra namespace lookups. In interpreted code final attributes can -also be modified. +the extra namespace lookups. + +For a final class attribute or global variable whose value can't be +determined during compilation, mypyc defines a hidden native variable +that stores the value when the initialization assignment is evaluated. +Redefining the attribute using ``setattr`` or direct namespace access +has no effect for mypyc-compiled code (in the same compilation unit). + +Final global variables and class attributes are faster to read in +compiled code, since they don't have to be looked up from a namespace +dictionary. Thus using a ``Final`` object that holds mutable state in an +attribute tends to be faster than a plain global variable in compiled code:: + + class Counter: + def __init__(self) -> None: + self.value = 0 + + x: Final = Counter() + y = 0 + + def inc() -> None: + # Faster: no Python namespace lookup + x.value += 1 + # Slower: access through globals() namespace dictionary + global y + y += 1 .. _free-threading: From 3d75cdb09f0928fa8b83e5ef03572ed878ac8d09 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 9 Jul 2026 19:24:21 +0100 Subject: [PATCH 122/127] [mypyc] Borrow final attributes more aggressively (#21702) Final instance attributes of native classes can't be rebound after initialization, so we can borrow them more aggressively than regular attributes, as long as the base object is kept alive for the duration of the borrowing. This allows skipping some incref/decref operations. Many workloads spend a significant fraction of CPU on incref/decref, and these operations are quite a bit more expensive on free-threaded builds, so the potential performance impact is significant especially on free-threaded builds. For example, the attribute `x` can only be safely borrowed if it's Final, since otherwise `foo` could assign to the attribute and free the old value: ```py def func(o: C) -> None: foo(o.x) ``` There are some subtleties in the implementation. Here are the main things that required extra care: * We don't allow borrowed values to escape from conditionally executed code paths (e.g. conditional expressions, comprehensions). * If a local variable can be modified with an assignment expression, we restrict borrowing based on that variable. * We can only borrow for a longer duration if the attribute value doesn't depend on a subexpression with a smaller borrow scope. * Lambda expressions generate a complete separate expression and borrowing scope. I used coding agent assist, especially for tests, but created the implementation is short, individually reviewed increments. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- mypyc/common.py | 8 + mypyc/ir/ops.py | 5 +- mypyc/irbuild/builder.py | 173 ++++++- mypyc/irbuild/expression.py | 65 ++- mypyc/irbuild/for_helpers.py | 11 +- mypyc/irbuild/ll_builder.py | 116 ++++- mypyc/irbuild/statement.py | 23 +- mypyc/irbuild/vec.py | 4 +- mypyc/test-data/irbuild-final.test | 252 ++++++++++ mypyc/test-data/refcount.test | 714 ++++++++++++++++++++++++++++ mypyc/test-data/run-async.test | 93 ++++ mypyc/test-data/run-classes.test | 73 +++ mypyc/test-data/run-generators.test | 43 ++ mypyc/test/test_irbuild.py | 1 + 15 files changed, 1536 insertions(+), 47 deletions(-) create mode 100644 mypyc/test-data/irbuild-final.test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 361290e55a1d1..548b568621a70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=HAX,Nam,ccompiler,ot,statics,whet,zar + - --ignore-words-list=HAX,Nam,ccompiler,keep-alives,ot,statics,whet,zar exclude: ^(mypy/test/|mypy/typeshed/|mypyc/test-data/|test-data/).+$ - repo: https://github.com/rhysd/actionlint rev: v1.7.7 diff --git a/mypyc/common.py b/mypyc/common.py index 382d640a84083..fa34647c5c729 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -68,6 +68,14 @@ BITMAP_TYPE: Final = "uint32_t" BITMAP_BITS: Final = 32 +# Constant for keeping a (often borrowed) op alive for a short time (a few subexpressions, +# no arbitrary computation or memory allocations), until flush_keep_alives() is called. +KEEP_ALIVE_SHORT_LIVED: Final = 0 +# Keep value alive longer, up to until the current top-level expression has been fully +# evaluated. This allows keeping alive values across function calls and arbitrary +# computation. Note that some expressions (e.g. lambdas), restrict the scope of borrowing. +KEEP_ALIVE_WHOLE_EXPRESSION: Final = 1 + # Runtime C library files that are always included (some ops may bring # extra dependencies via mypyc.ir.deps.SourceDep or mypyc.ir.deps.HeaderDep) RUNTIME_C_FILES: Final = [ diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 4bc7671b82082..14e12559be1eb 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -31,7 +31,7 @@ class to enable the new behavior. Sometimes adding a new abstract from mypy_extensions import trait -from mypyc.common import PROPSET_PREFIX +from mypyc.common import KEEP_ALIVE_SHORT_LIVED, PROPSET_PREFIX from mypyc.ir.deps import Dependency from mypyc.ir.rtypes import ( RArray, @@ -898,6 +898,7 @@ def __init__( *, borrow: bool = False, allow_error_value: bool = False, + borrow_scope: int = KEEP_ALIVE_SHORT_LIVED, ) -> None: super().__init__(line) self.obj = obj @@ -912,6 +913,8 @@ def __init__( elif attr_type.error_overlap: self.error_kind = ERR_MAGIC_OVERLAPPING self.is_borrowed = borrow and attr_type.is_refcounted + # How long a borrowed result of this op stays valid (a KEEP_ALIVE_* constant). + self.borrow_scope = borrow_scope def sources(self) -> list[Value]: return [self.obj] diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 56a3f944fe77f..d8ae71e33bf59 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -20,12 +20,17 @@ TYPE_VAR_KIND, TYPE_VAR_TUPLE_KIND, ArgKind, + AssignmentExpr, + AwaitExpr, CallExpr, Decorator, + DictionaryComprehension, Expression, FuncDef, + GeneratorExpr, IndexExpr, IntExpr, + LambdaExpr, Lvalue, MemberExpr, MypyFile, @@ -41,7 +46,10 @@ TypeInfo, TypeParam, Var, + YieldExpr, + YieldFromExpr, ) +from mypy.traverser import TraverserVisitor from mypy.types import ( AnyType, DeletedType, @@ -63,6 +71,8 @@ EXT_SUFFIX, GENERATOR_ATTRIBUTE_PREFIX, IS_FREE_THREADED, + KEEP_ALIVE_SHORT_LIVED, + KEEP_ALIVE_WHOLE_EXPRESSION, MODULE_PREFIX, SELF_NAME, TEMP_ATTR_NAME, @@ -80,6 +90,7 @@ BasicBlock, Branch, Call, + Cast, ComparisonOp, GetAttr, InitStatic, @@ -284,6 +295,20 @@ def __init__( self.imports: dict[str, None] = {} self.can_borrow = False + self.expression_depth = 0 + # Symbols (local vars) reassigned via a walrus expression within the current + # top-level expression. Used to avoid borrowing an attribute over the whole + # expression when the borrow root could be rebound (and thus freed) partway. + self.reassigned_in_expr: set[SymbolNode] = set() + # Whether the current top-level expression contains a suspension point + # (await, yield or yield from). A whole-expression borrow can't span such a + # point, since the borrowed value (and its root) live in registers that are + # not spilled into the generator environment across the suspend. + self.expr_has_suspend = False + # Saved expression state for enclosing functions (see enter()/leave()). + self.expression_depth_stack: list[int] = [] + self.reassigned_in_expr_stack: list[set[SymbolNode]] = [] + self.expr_has_suspend_stack: list[bool] = [] # When set, load_globals_dict uses this module instead of self.module_name. # Used by generate_attr_defaults_init for cross-module inherited defaults. @@ -315,6 +340,10 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V """ with self.catch_errors(node.line): if isinstance(node, Expression): + self.expression_depth += 1 + if self.expression_depth == 1: + self.reassigned_in_expr = find_walrus_targets(node) + self.expr_has_suspend = expr_has_suspend(node) old_can_borrow = self.can_borrow self.can_borrow = can_borrow try: @@ -329,6 +358,11 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V self.can_borrow = old_can_borrow if not can_borrow: self.flush_keep_alives(node.line) + self.expression_depth -= 1 + if self.expression_depth == 0: + self.flush_keep_alives(node.line, scope=KEEP_ALIVE_WHOLE_EXPRESSION) + self.reassigned_in_expr = set() + self.expr_has_suspend = False return res else: try: @@ -337,8 +371,8 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V pass return None - def flush_keep_alives(self, line: int) -> None: - self.builder.flush_keep_alives(line) + def flush_keep_alives(self, line: int, *, scope: int = KEEP_ALIVE_SHORT_LIVED) -> None: + self.builder.flush_keep_alives(line, scope=scope) # Pass through methods for the most common low-level builder ops, for convenience. @@ -1192,7 +1226,9 @@ def is_synthetic_type(self, typ: TypeInfo) -> bool: return typ.is_named_tuple or typ.is_newtype or typ.typeddict_type is not None def get_final_ref(self, expr: MemberExpr) -> tuple[str, Var, bool] | None: - """Check if `expr` is a final attribute. + """Check if `expr` is a final class or module attribute. + + Return False for instance attributes. This needs to be done differently for class and module attributes to correctly determine fully qualified name. Return a tuple that consists of @@ -1341,6 +1377,15 @@ def enter(self, fn_info: FuncInfo | str = "", *, ret_type: RType = none_rprimiti self.fn_info = fn_info self.fn_infos.append(self.fn_info) self.ret_types.append(ret_type) + # A function body is its own top-level expression context, even when the + # function (e.g. a lambda) is being generated in the middle of an outer + # expression. Save the outer expression state and start fresh. + self.expression_depth_stack.append(self.expression_depth) + self.reassigned_in_expr_stack.append(self.reassigned_in_expr) + self.expr_has_suspend_stack.append(self.expr_has_suspend) + self.expression_depth = 0 + self.reassigned_in_expr = set() + self.expr_has_suspend = False if fn_info.is_generator: self.nonlocal_control.append(GeneratorNonlocalControl()) else: @@ -1354,6 +1399,9 @@ def leave(self) -> tuple[list[Register], list[RuntimeArg], list[BasicBlock], RTy ret_type = self.ret_types.pop() fn_info = self.fn_infos.pop() self.nonlocal_control.pop() + self.expression_depth = self.expression_depth_stack.pop() + self.reassigned_in_expr = self.reassigned_in_expr_stack.pop() + self.expr_has_suspend = self.expr_has_suspend_stack.pop() self.builder = self.builders[-1] self.fn_info = self.fn_infos[-1] return builder.args, runtime_args, builder.blocks, ret_type, fn_info @@ -1389,6 +1437,27 @@ def enter_scope(self, fn_info: FuncInfo) -> Iterator[None]: self.builder = self.builders[-1] self.fn_info = self.fn_infos[-1] + @contextmanager + def enter_borrow_scope(self, line: int) -> Iterator[None]: + """Enter new borrow scope from which borrows can't leak to outer expressions. + + This is a borrow region (see LowLevelIRBuilder.borrow_region) that also + resets the per-expression borrowing heuristic state, since the body forms + its own top-level expression context (e.g. a comprehension iteration or a + lambda body). + """ + old_expression_depth = self.expression_depth + old_reassigned_in_expr = self.reassigned_in_expr + old_expr_has_suspend = self.expr_has_suspend + self.expression_depth = 0 + try: + with self.builder.borrow_region(line): + yield + finally: + self.expression_depth = old_expression_depth + self.reassigned_in_expr = old_reassigned_in_expr + self.expr_has_suspend = old_expr_has_suspend + @contextmanager def enter_method( self, @@ -1581,6 +1650,32 @@ def is_final_native_attr_ref(self, expr: MemberExpr) -> bool: return expr.name in ir.final_attributes return False + def root_is_reassigned(self, v: Value) -> bool: + """Is the root local variable a borrow chain 'v' reads from reassigned this expression? + + A whole-expression borrow of an attribute keeps the borrow root alive only + via the register holding it. If that register belongs to a local variable + that is rebound (via a walrus assignment) during the same top-level + expression, the old value may be freed while the borrow is still live. + """ + if not self.reassigned_in_expr: + return False + # Peel borrowed links back to the root value the chain reads from. + while True: + if isinstance(v, GetAttr) and v.is_borrowed: + v = v.obj + elif isinstance(v, Cast) and v.is_borrowed: + v = v.src + else: + break + if not isinstance(v, Register): + return False + for symbol in self.reassigned_in_expr: + target = self.symtables[-1].get(symbol) + if isinstance(target, AssignmentTargetRegister) and target.register is v: + return True + return False + def mark_block_unreachable(self) -> None: """Mark statements in the innermost block being processed as unreachable. @@ -1695,6 +1790,78 @@ def get_call_target_fullname(ref: RefExpr) -> str: return ref.fullname +class WalrusTargetCollector(TraverserVisitor): + """Collect the symbols assigned to by walrus expressions in a subtree.""" + + def __init__(self) -> None: + self.targets: set[SymbolNode] = set() + + def visit_assignment_expr(self, o: AssignmentExpr) -> None: + if o.target.node is not None: + self.targets.add(o.target.node) + super().visit_assignment_expr(o) + + def visit_lambda_expr(self, o: LambdaExpr) -> None: + # A lambda body forms its own expression context, so don't descend into it. + pass + + +def find_walrus_targets(expr: Expression) -> set[SymbolNode]: + """Return the symbols reassigned via a walrus expression within 'expr'. + + Walrus (':=') is the only way to rebind a variable in the middle of evaluating + an expression, so this is the complete set of in-expression reassignments. + """ + collector = WalrusTargetCollector() + expr.accept(collector) + return collector.targets + + +class SuspendDetector(TraverserVisitor): + """Detect await/yield/yield from expressions in a subtree.""" + + def __init__(self) -> None: + self.found = False + + def visit_await_expr(self, o: AwaitExpr) -> None: + self.found = True + + def visit_yield_expr(self, o: YieldExpr) -> None: + self.found = True + + def visit_yield_from_expr(self, o: YieldFromExpr) -> None: + self.found = True + + def visit_generator_expr(self, o: GeneratorExpr) -> None: + # An 'async for' clause suspends via an implicit await on __anext__ that + # isn't represented as an AwaitExpr node in the AST (list/set comprehensions + # delegate to a GeneratorExpr, so they are covered here too). + if any(o.is_async): + self.found = True + super().visit_generator_expr(o) + + def visit_dictionary_comprehension(self, o: DictionaryComprehension) -> None: + if any(o.is_async): + self.found = True + super().visit_dictionary_comprehension(o) + + def visit_lambda_expr(self, o: LambdaExpr) -> None: + # A lambda body forms its own function (and suspension) context. + pass + + +def expr_has_suspend(expr: Expression) -> bool: + """Does evaluating 'expr' involve a suspension point (await/yield/yield from)? + + A whole-expression borrow can't safely span a suspension point, since the + borrowed value and its borrow root are held in registers that aren't spilled + into the generator environment across the suspend. + """ + detector = SuspendDetector() + expr.accept(detector) + return detector.found + + def create_type_params( builder: IRBuilder, typing_mod: Value, type_args: list[TypeParam], line: int ) -> list[Value]: diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index b99e8161c08c5..91e1e55d8da26 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -58,7 +58,12 @@ TypeType, get_proper_type, ) -from mypyc.common import IS_FREE_THREADED, MAX_SHORT_INT +from mypyc.common import ( + IS_FREE_THREADED, + KEEP_ALIVE_SHORT_LIVED, + KEEP_ALIVE_WHOLE_EXPRESSION, + MAX_SHORT_INT, +) from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD from mypyc.ir.ops import ( @@ -66,11 +71,14 @@ Assign, BasicBlock, CallC, + Cast, ComparisonOp, + GetAttr, Integer, LoadAddress, LoadLiteral, PrimitiveDescription, + PrimitiveOp, RaiseStandardError, Register, TupleGet, @@ -252,7 +260,7 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value: if expr.fullname in ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING"): return builder.false(expr.line) - # First check if this is maybe a final attribute. + # First check if this is maybe a final class/module attribute. final = builder.get_final_ref(expr) if final is not None: fullname, final_var, native = final @@ -305,8 +313,43 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value: check_instance_attribute_access_through_class(builder, expr, typ) - borrow = can_borrow and builder.can_borrow - return builder.builder.get_attr(obj, expr.name, rtype, expr.line, borrow=borrow) + is_final = builder.is_final_native_attr_ref(expr) + scope = KEEP_ALIVE_SHORT_LIVED + if ( + is_final + and builder.expression_depth > 1 + and value_borrow_scope(builder, obj) >= KEEP_ALIVE_WHOLE_EXPRESSION + # Don't borrow across the whole expression if the borrow root can be + # rebound via a walrus assignment + and not builder.root_is_reassigned(obj) + # Don't borrow across a suspension point (await/yield/yield from), since + # the borrow would not survive the suspend (registers aren't spilled). + and not builder.expr_has_suspend + ): + scope = KEEP_ALIVE_WHOLE_EXPRESSION + borrow = (can_borrow and builder.can_borrow) or scope == KEEP_ALIVE_WHOLE_EXPRESSION + return builder.builder.get_attr( + obj, expr.name, rtype, expr.line, borrow=borrow, borrow_scope=scope + ) + + +def value_borrow_scope(builder: IRBuilder, v: Value) -> int: + """Compute how long an existing borrowed value can safely be kept alive. + + Returns a KEEP_ALIVE_* constant (or a large value if 'v' is not borrowed and + thus has no borrowing constraint). + """ + if isinstance(v, GetAttr) and v.is_borrowed: + return min(v.borrow_scope, value_borrow_scope(builder, v.obj)) + elif isinstance(v, Cast) and v.is_borrowed: + # A cast just propagates the value (when successful), so the scope is + # determined by the source. + return value_borrow_scope(builder, v.src) + elif isinstance(v, (CallC, PrimitiveOp)) and v.is_borrowed: + # Values borrowed from a C function (e.g. a borrowed list/vec item) may be + # invalidated by arbitrary computation, so they are only short-lived. + return KEEP_ALIVE_SHORT_LIVED + return 999 def check_instance_attribute_access_through_class( @@ -875,15 +918,17 @@ def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Val target = Register(expr_type) builder.activate_block(if_body) - true_value = builder.accept(expr.if_expr) - true_value = builder.coerce(true_value, expr_type, expr.line) - builder.add(Assign(target, true_value, expr.line)) + with builder.builder.borrow_region(expr.line): + true_value = builder.accept(expr.if_expr) + true_value = builder.coerce(true_value, expr_type, expr.line) + builder.add(Assign(target, true_value, expr.line)) builder.goto(next_block) builder.activate_block(else_body) - false_value = builder.accept(expr.else_expr) - false_value = builder.coerce(false_value, expr_type, expr.line) - builder.add(Assign(target, false_value, expr.line)) + with builder.builder.borrow_region(expr.line): + false_value = builder.accept(expr.else_expr) + false_value = builder.coerce(false_value, expr_type, expr.line) + builder.add(Assign(target, false_value, expr.line)) builder.goto(next_block) builder.activate_block(next_block) diff --git a/mypyc/irbuild/for_helpers.py b/mypyc/irbuild/for_helpers.py index fc539a1fc090b..5a36dcb80ff7d 100644 --- a/mypyc/irbuild/for_helpers.py +++ b/mypyc/irbuild/for_helpers.py @@ -293,7 +293,8 @@ def sequence_from_generator_preallocate_helper( target_op = empty_op_llbuilder(length, line) def set_item(item_index: Value) -> None: - e = builder.accept(gen.left_expr) + with builder.enter_borrow_scope(line): + e = builder.accept(gen.left_expr) set_item_op(target_op, item_index, e, line) for_loop_helper_with_index( @@ -446,9 +447,10 @@ def loop_contents( remaining_loop_params: the parameters for any further nested loops; if it's empty we'll instead evaluate the "gen_inner_stmts" function """ - # Check conditions, in order, short circuiting them. + # Check conditions, in order, short-circuiting them. for cond in conds: - cond_val = builder.accept(cond) + with builder.enter_borrow_scope(line): + cond_val = builder.accept(cond) cont_block, rest_block = BasicBlock(), BasicBlock() # If the condition is true we'll skip the continue. builder.add_bool_branch(cond_val, rest_block, cont_block) @@ -462,7 +464,8 @@ def loop_contents( else: # We finally reached the actual body of the generator. # Generate the IR for the inner loop body. - gen_inner_stmts() + with builder.enter_borrow_scope(line): + gen_inner_stmts() handle_loop(loop_params) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index c0ad1cf1f8264..aafdb73fed84e 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -7,7 +7,8 @@ from __future__ import annotations import sys -from collections.abc import Callable, Sequence +from collections.abc import Callable, Iterator, Sequence +from contextlib import contextmanager from typing import Final, TypeGuard, cast from mypy.argmap import map_actuals_to_formals @@ -19,6 +20,7 @@ FAST_ISINSTANCE_MAX_SUBCLASSES, FAST_PREFIX, IS_FREE_THREADED, + KEEP_ALIVE_SHORT_LIVED, MAX_LITERAL_SHORT_INT, MAX_SHORT_INT, MIN_LITERAL_SHORT_INT, @@ -250,6 +252,15 @@ BOOL_BINARY_OPS: Final = {"&", "&=", "|", "|=", "^", "^=", "==", "!=", "<", "<=", ">", ">="} +class PendingKeepAlive: + __slots__ = ("value", "scope", "seq") + + def __init__(self, value: Value, scope: int, seq: int) -> None: + self.value = value + self.scope = scope + self.seq = seq + + class LowLevelIRBuilder: """A "low-level" IR builder class. @@ -276,7 +287,10 @@ def __init__(self, errors: Errors | None, options: CompilerOptions) -> None: self.error_handlers: list[BasicBlock | None] = [None] # Values that we need to keep alive as long as we have borrowed # temporaries. Use flush_keep_alives() to mark the end of the live range. - self.keep_alives: list[Value] = [] + # The second value is the scope/duration of keep alive (KEEP_ALIVE_* constant). + # Different values must be kept alive for different durations. + self.keep_alives: list[PendingKeepAlive] = [] + self.next_keep_alive_seq = 0 def set_module(self, module_name: str, module_path: str) -> None: """Set the name and path of the current module.""" @@ -367,10 +381,55 @@ def self(self) -> Register: """ return self.args[0] - def flush_keep_alives(self, line: int) -> None: - if self.keep_alives: - self.add(KeepAlive(self.keep_alives.copy(), line)) - self.keep_alives = [] + def flush_keep_alives(self, line: int, *, scope: int = KEEP_ALIVE_SHORT_LIVED) -> None: + if any(entry.scope == scope for entry in self.keep_alives): + self.add( + KeepAlive( + [entry.value for entry in self.keep_alives if entry.scope == scope], line + ) + ) + self.keep_alives = [entry for entry in self.keep_alives if entry.scope != scope] + + def keep_alive_checkpoint(self) -> int: + return self.next_keep_alive_seq + + def flush_keep_alives_since(self, line: int, checkpoint: int) -> None: + """Flush keep-alives added after 'checkpoint' without touching earlier ones.""" + new_keep_alives = [entry for entry in self.keep_alives if entry.seq >= checkpoint] + if new_keep_alives: + self.add(KeepAlive([entry.value for entry in new_keep_alives], line)) + self.keep_alives = [entry for entry in self.keep_alives if entry.seq < checkpoint] + + @contextmanager + def borrow_region(self, line: int) -> Iterator[None]: + """Confine borrows created in this region: flush them when leaving it. + + Keep-alives added inside the region (regardless of their duration scope) + are flushed on exit, so borrows can't leak past a lexical boundary such as + a conditional branch, an 'and'/'or' branch or a comprehension iteration. + """ + checkpoint = self.keep_alive_checkpoint() + yield + self.flush_keep_alives_since(line, checkpoint) + + def add_keep_alive(self, value: Value, scope: int) -> None: + self.keep_alives.append(PendingKeepAlive(value, scope, self.next_keep_alive_seq)) + self.next_keep_alive_seq += 1 + + def keep_borrow_source_alive(self, value: Value, borrow_scope: int) -> None: + """Ensure the source of borrowed value will be alive for given borrow scope. + + A borrow keeps reading from 'value', so 'value' must stay valid for the + borrow's whole scope. When 'value' is itself a borrowed cast it holds no + reference of its own (and coerce() only kept its source alive short-term), + so we follow the cast to the underlying owned value and keep *that* alive + for the requested scope too. Otherwise, a longer-lived borrow through a cast + (e.g. a Final attribute read via cast(T, make())) could read freed memory. + """ + self.add_keep_alive(value, borrow_scope) + while isinstance(value, Cast) and value.is_borrowed: + value = value.src + self.add_keep_alive(value, borrow_scope) def debug_print(self, toprint: str | Value) -> None: if isinstance(toprint, str): @@ -400,7 +459,7 @@ def unbox_or_cast( return self.add(Unbox(src, target_type, line)) else: if can_borrow: - self.keep_alives.append(src) + self.add_keep_alive(src, KEEP_ALIVE_SHORT_LIVED) return self.add(Cast(src, target_type, line, borrow=can_borrow, unchecked=unchecked)) def coerce( @@ -805,19 +864,32 @@ def coerce_nullable(self, src: Value, target_type: RType, line: int) -> Value: # Attribute access def get_attr( - self, obj: Value, attr: str, result_type: RType, line: int, *, borrow: bool = False + self, + obj: Value, + attr: str, + result_type: RType, + line: int, + *, + borrow: bool = False, + borrow_scope: int = KEEP_ALIVE_SHORT_LIVED, ) -> Value: - """Get a native or Python attribute of an object.""" + """Get a native or Python attribute of an object. + + If the result is borrowed, borrow_scope controls how long it stays valid + (a KEEP_ALIVE_* constant). The caller is responsible for choosing a scope + that is actually safe (e.g. downgrading to short-lived if the borrow root + may be rebound during the borrow's live range). + """ if ( isinstance(obj.type, RInstance) and obj.type.class_ir.is_ext_class and obj.type.class_ir.has_attr(attr) ): - op = GetAttr(obj, attr, line, borrow=borrow) + op = GetAttr(obj, attr, line, borrow=borrow, borrow_scope=borrow_scope) # For non-refcounted attribute types, the borrow might be # disabled even if requested, so don't check 'borrow'. if op.is_borrowed: - self.keep_alives.append(obj) + self.keep_borrow_source_alive(obj, borrow_scope) return self.add(op) elif isinstance(obj.type, RUnion): return self.union_get_attr(obj, obj.type, attr, result_type, line) @@ -2112,18 +2184,20 @@ def shortcircuit_helper( # it is the right side if the left is false. true_body, false_body = (right_body, left_body) if op == "and" else (left_body, right_body) - left_value = left() - self.add_bool_branch(left_value, true_body, false_body) + with self.borrow_region(line): + left_value = left() + self.add_bool_branch(left_value, true_body, false_body) - self.activate_block(left_body) - left_coerced = self.coerce(left_value, expr_type, line) - self.add(Assign(target, left_coerced, line)) + self.activate_block(left_body) + left_coerced = self.coerce(left_value, expr_type, line) + self.add(Assign(target, left_coerced, line)) self.goto(next_block) self.activate_block(right_body) - right_value = right() - right_coerced = self.coerce(right_value, expr_type, line) - self.add(Assign(target, right_coerced, line)) + with self.borrow_region(line): + right_value = right() + right_coerced = self.coerce(right_value, expr_type, line) + self.add(Assign(target, right_coerced, line)) self.goto(next_block) self.activate_block(next_block) @@ -2281,7 +2355,7 @@ def call_c( # immediately freed, at the risk of a dangling pointer. for arg in coerced: if not isinstance(arg, (Integer, LoadLiteral)): - self.keep_alives.append(arg) + self.add_keep_alive(arg, KEEP_ALIVE_SHORT_LIVED) if desc.error_kind == ERR_NEG_INT: comp = ComparisonOp(target, Integer(0, desc.return_type, line), ComparisonOp.SGE, line) comp.error_kind = ERR_FALSE @@ -2402,7 +2476,7 @@ def primitive_op( # immediately freed, at the risk of a dangling pointer. for arg in coerced: if not isinstance(arg, (Integer, LoadLiteral)): - self.keep_alives.append(arg) + self.add_keep_alive(arg, KEEP_ALIVE_SHORT_LIVED) if desc.error_kind == ERR_NEG_INT: comp = ComparisonOp(target, Integer(0, desc.return_type, line), ComparisonOp.SGE, line) comp.error_kind = ERR_FALSE diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index e90aa089305f2..21f47b190c302 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -49,7 +49,7 @@ YieldExpr, YieldFromExpr, ) -from mypyc.common import TEMP_ATTR_NAME +from mypyc.common import KEEP_ALIVE_SHORT_LIVED, KEEP_ALIVE_WHOLE_EXPRESSION, TEMP_ATTR_NAME from mypyc.ir.ops import ( ERR_NEVER, NAMESPACE_MODULE, @@ -86,7 +86,13 @@ object_rprimitive, ) from mypyc.irbuild.ast_helpers import is_borrow_friendly_expr, process_conditional -from mypyc.irbuild.builder import IRBuilder, create_type_params, int_borrow_friendly_op +from mypyc.irbuild.builder import ( + IRBuilder, + create_type_params, + expr_has_suspend, + find_walrus_targets, + int_borrow_friendly_op, +) from mypyc.irbuild.for_helpers import for_loop_helper from mypyc.irbuild.generator import add_raise_exception_blocks_to_generator_class from mypyc.irbuild.nonlocalcontrol import ( @@ -161,10 +167,17 @@ def transform_expression_stmt(builder: IRBuilder, stmt: ExpressionStmt) -> None: if isinstance(stmt.expr, StrExpr): # Docstring. Ignore return - # ExpressionStmts do not need to be coerced like other Expressions, so we shouldn't - # call builder.accept here. + # ExpressionStmts do not need to be coerced like other Expressions, so + # we shouldn't call builder.accept here. + builder.expression_depth += 1 + builder.reassigned_in_expr = find_walrus_targets(stmt.expr) + builder.expr_has_suspend = expr_has_suspend(stmt.expr) stmt.expr.accept(builder.visitor) - builder.flush_keep_alives(stmt.line) + builder.expression_depth -= 1 + builder.reassigned_in_expr = set() + builder.expr_has_suspend = False + builder.flush_keep_alives(stmt.line, scope=KEEP_ALIVE_SHORT_LIVED) + builder.flush_keep_alives(stmt.line, scope=KEEP_ALIVE_WHOLE_EXPRESSION) def transform_return_stmt(builder: IRBuilder, stmt: ReturnStmt) -> None: diff --git a/mypyc/irbuild/vec.py b/mypyc/irbuild/vec.py index 38615ebe16026..8dffc12a22130 100644 --- a/mypyc/irbuild/vec.py +++ b/mypyc/irbuild/vec.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from mypyc.common import IS_32_BIT_PLATFORM, PLATFORM_SIZE +from mypyc.common import IS_32_BIT_PLATFORM, KEEP_ALIVE_SHORT_LIVED, PLATFORM_SIZE from mypyc.ir.ops import ( ERR_MAGIC, Assign, @@ -351,7 +351,7 @@ def vec_get_item_unsafe_lower( vtype = base.type item_addr = vec_item_ptr(builder, base, index) result = vec_load_mem_item(builder, item_addr, vtype.item_type, can_borrow=can_borrow) - builder.keep_alives.append(base) + builder.add_keep_alive(base, KEEP_ALIVE_SHORT_LIVED) return result diff --git a/mypyc/test-data/irbuild-final.test b/mypyc/test-data/irbuild-final.test new file mode 100644 index 0000000000000..0ede988a43206 --- /dev/null +++ b/mypyc/test-data/irbuild-final.test @@ -0,0 +1,252 @@ +[case testFinalAttrBorrowed] +from typing import Final + +class C: + def __init__(self, x: str) -> None: + self.f: Final = x + self.x = x + +def f(s: str) -> None: pass + +def calls(c: C) -> None: + f(c.f) + f(c.x) + c.f.startswith("a") + +def ret(c: C) -> str: + return c.f +[out] +def C.__init__(self, x): + self :: __main__.C + x :: str +L0: + self.f = x + self.x = x + return 1 +def f(s): + s :: str +L0: + return 1 +def calls(c): + c :: __main__.C + r0 :: str + r1 :: None + r2 :: str + r3 :: None + r4, r5 :: str + r6 :: i32 + r7 :: bool +L0: + r0 = borrow c.f + r1 = f(r0) + keep_alive c + r2 = c.x + r3 = f(r2) + r4 = borrow c.f + r5 = 'a' + r6 = CPyStr_Startswith(r4, r5) + r7 = truncate r6: i32 to builtins.bool + keep_alive c + return 1 +def ret(c): + c :: __main__.C + r0 :: str +L0: + r0 = c.f + return r0 + +[case testInheritedFinalAttrBorrowed] +from typing import Final + +class Base: + x: str + + def __init__(self, s: str) -> None: + self.s: Final = s + +class Deriv(Base): + def __init__(self, n: int) -> None: + self.n: Final = n + +def f(b: Base) -> str: + return b.s + "a" + +def g(d: Deriv) -> str: + return d.s * d.n + +def h(d: Deriv) -> str: + return d.x + "a" +[out] +def Base.__init__(self, s): + self :: __main__.Base + s :: str +L0: + self.s = s + return 1 +def Deriv.__init__(self, n): + self :: __main__.Deriv + n :: int +L0: + self.n = n + return 1 +def f(b): + b :: __main__.Base + r0, r1, r2 :: str +L0: + r0 = borrow b.s + r1 = 'a' + r2 = PyUnicode_Concat(r0, r1) + keep_alive b + return r2 +def g(d): + d :: __main__.Deriv + r0 :: str + r1 :: int + r2 :: str +L0: + r0 = borrow d.s + r1 = borrow d.n + r2 = CPyStr_Multiply(r0, r1) + keep_alive d, d + return r2 +def h(d): + d :: __main__.Deriv + r0, r1, r2 :: str +L0: + r0 = d.x + r1 = 'a' + r2 = PyUnicode_Concat(r0, r1) + return r2 + +[case testFinalAttrExpressionKinds] +from typing import Final + +class C: + def __init__(self, s: str, n: int, l: list[str]) -> None: + self.f: Final = s + self.n: Final = n + self.l: Final = l + self.s = s + +def f() -> C: + return C("", 0, []) + +def assign(c: C) -> None: + a = c.f + c.s = c.f + +def indexing(c: C) -> str: + return c.l[c.n] + +def call_eq(c: C) -> bool: + return f().f == c.f +[out] +def C.__init__(self, s, n, l): + self :: __main__.C + s :: str + n :: int + l :: list +L0: + self.f = s + self.n = n + self.l = l + self.s = s + return 1 +def f(): + r0 :: str + r1 :: list + r2 :: __main__.C +L0: + r0 = '' + r1 = PyList_New(0) + r2 = C(r0, 0, r1) + return r2 +def assign(c): + c :: __main__.C + r0, a, r1 :: str + r2 :: bool +L0: + r0 = c.f + a = r0 + r1 = c.f + c.s = r1; r2 = is_error + return 1 +def indexing(c): + c :: __main__.C + r0 :: list + r1 :: int + r2 :: object + r3 :: str +L0: + r0 = borrow c.l + r1 = borrow c.n + r2 = CPyList_GetItem(r0, r1) + r3 = cast(str, r2) + keep_alive c, c + return r3 +def call_eq(c): + c, r0 :: __main__.C + r1, r2 :: str + r3 :: bool +L0: + r0 = f() + r1 = borrow r0.f + r2 = borrow c.f + r3 = CPyStr_Equal(r1, r2) + keep_alive r0, c + return r3 + +[case testFinalAttrExpressionKinds2_withgil] +from typing import Final + +class C: + def __init__(self, d: D) -> None: + self.d: Final = d + +class D: + def __init__(self, s: str) -> None: + self.f: Final = s + +def f(c: C) -> str: + return c.d.f + "a" + +def g(a: list[D], n: int) -> str: + return a[n].f + "a" +[out] +def C.__init__(self, d): + self :: __main__.C + d :: __main__.D +L0: + self.d = d + return 1 +def D.__init__(self, s): + self :: __main__.D + s :: str +L0: + self.f = s + return 1 +def f(c): + c :: __main__.C + r0 :: __main__.D + r1, r2, r3 :: str +L0: + r0 = borrow c.d + r1 = borrow r0.f + r2 = 'a' + r3 = PyUnicode_Concat(r1, r2) + keep_alive c, r0 + return r3 +def g(a, n): + a :: list + n :: int + r0 :: object + r1 :: __main__.D + r2, r3, r4 :: str +L0: + r0 = CPyList_GetItemBorrow(a, n) + r1 = borrow cast(__main__.D, r0) + r2 = r1.f + keep_alive a, n, r0 + r3 = 'a' + r4 = PyUnicode_Concat(r2, r3) + return r4 diff --git a/mypyc/test-data/refcount.test b/mypyc/test-data/refcount.test index 7c7134dcfbfaa..bedfabac791d4 100644 --- a/mypyc/test-data/refcount.test +++ b/mypyc/test-data/refcount.test @@ -2366,3 +2366,717 @@ L0: r3 = CPyBytes_Concat(r2, c) dec_ref r2 return r3 + +[case testBorrowedFinalAttribute] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(c: C, b: bool) -> str: + a = c.s if b else "x" + return a + +def g(c: C) -> C: + return C(c.s) +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(c, b): + c :: __main__.C + b :: bool + r0, r1, r2, a :: str +L0: + if b goto L1 else goto L2 :: bool +L1: + r0 = borrow c.s + inc_ref r0 + r1 = r0 + goto L3 +L2: + r2 = 'x' + inc_ref r2 + r1 = r2 +L3: + a = r1 + return a +def g(c): + c :: __main__.C + r0 :: str + r1 :: __main__.C +L0: + r0 = borrow c.s + r1 = C(r0) + return r1 + +[case testBorrowedFinalAttribute2_withgil] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def g(c: C, n: int) -> str: + a = [c] + return a[n].s + "a" +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def g(c, n): + c :: __main__.C + n :: int + r0 :: list + r1 :: ptr + a :: list + r2 :: object + r3 :: __main__.C + r4, r5, r6 :: str +L0: + r0 = PyList_New(1) + r1 = list_items r0 + inc_ref c + buf_init_item r1, 0, c + a = r0 + r2 = CPyList_GetItemBorrow(a, n) + r3 = borrow cast(__main__.C, r2) + r4 = r3.s + dec_ref a + r5 = 'a' + r6 = PyUnicode_Concat(r4, r5) + dec_ref r4 + return r6 + +[case testBorrowedFinalAttribute3_withgil] +from typing import Final + +class C: + def __init__(self, d: D) -> None: + self.d = d + +class CC: + def __init__(self, d: D) -> None: + self.d: Final = d + +class D: + def __init__(self, s: str) -> None: + self.f: Final = s + +def f() -> str: + c = C(D("x")) + return c.d.f + "a" + +def g(d: D) -> str: + c = CC(d) + return c.d.f + "a" +[out] +def C.__init__(self, d): + self :: __main__.C + d :: __main__.D +L0: + inc_ref d + self.d = d + return 1 +def CC.__init__(self, d): + self :: __main__.CC + d :: __main__.D +L0: + inc_ref d + self.d = d + return 1 +def D.__init__(self, s): + self :: __main__.D + s :: str +L0: + inc_ref s + self.f = s + return 1 +def f(): + r0 :: str + r1 :: __main__.D + r2, c :: __main__.C + r3 :: __main__.D + r4, r5, r6 :: str +L0: + r0 = 'x' + r1 = D(r0) + r2 = C(r1) + dec_ref r1 + c = r2 + r3 = borrow c.d + r4 = r3.f + dec_ref c + r5 = 'a' + r6 = PyUnicode_Concat(r4, r5) + dec_ref r4 + return r6 +def g(d): + d :: __main__.D + r0, c :: __main__.CC + r1 :: __main__.D + r2, r3, r4 :: str +L0: + r0 = CC(d) + c = r0 + r1 = borrow c.d + r2 = borrow r1.f + r3 = 'a' + r4 = PyUnicode_Concat(r2, r3) + dec_ref c + return r4 + +[case testBorrowedFinalAttribute4] +from typing import Final + +class C: + def __init__(self, d: D) -> None: + self.d: Final = d + + def method(self) -> int: + return self.d.foo() + +class D: + def foo(self) -> int: + return 1 +[out] +def C.__init__(self, d): + self :: __main__.C + d :: __main__.D +L0: + inc_ref d + self.d = d + return 1 +def C.method(self): + self :: __main__.C + r0 :: __main__.D + r1 :: int +L0: + r0 = borrow self.d + r1 = r0.foo() + return r1 +def D.foo(self): + self :: __main__.D +L0: + return 2 + +[case testBorrowedFinalAttribute5_64bit] +from typing import Final +from librt.vecs import vec +from mypy_extensions import i64 + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def g(c: C, n: i64) -> str: + a = vec[C]([c]) + return a[n].s + "a" +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def g(c, n): + c :: __main__.C + n :: i64 + r0 :: object + r1 :: ptr + r2 :: vec[__main__.C] + r3, r4 :: ptr + a :: vec[__main__.C] + r5 :: native_int + r6 :: bit + r7 :: i64 + r8 :: bit + r9 :: bool + r10 :: i64 + r11 :: __main__.C + r12, r13, r14 :: str +L0: + r0 = __main__.C :: type + r1 = r0 + r2 = VecTApi.alloc(1, 1, r1) + r3 = r2.items + inc_ref c + set_mem r3, c :: __main__.C* + r4 = r3 + 8 + a = r2 + r5 = a.len + r6 = n < r5 :: unsigned + if r6 goto L4 else goto L1 :: bool +L1: + r7 = n + r5 + r8 = r7 < r5 :: unsigned + if r8 goto L3 else goto L6 :: bool +L2: + r9 = raise IndexError + unreachable +L3: + r10 = r7 + goto L5 +L4: + r10 = n +L5: + r11 = vec_get_item_unsafe_borrow[__main__.C] a, r10 + r12 = r11.s + dec_ref a + r13 = 'a' + r14 = PyUnicode_Concat(r12, r13) + dec_ref r12 + return r14 +L6: + dec_ref a + goto L2 + +[case testBorrowedFinalAttributeReassignViaWalrus] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(c: C, d: C) -> str: + return c.s + (c := d).s +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(c, d): + c, d :: __main__.C + r0, r1, r2 :: str +L0: + r0 = c.s + inc_ref d + c = d + dec_ref c + r1 = borrow d.s + r2 = PyUnicode_Concat(r0, r1) + dec_ref r0 + return r2 + +[case testBorrowedFinalAttributeConditionalMerge] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(b: bool) -> str: + return (C("x").s if b else "y") + "z" +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(b): + b :: bool + r0 :: str + r1 :: __main__.C + r2, r3, r4, r5, r6 :: str +L0: + if b goto L1 else goto L2 :: bool +L1: + r0 = 'x' + r1 = C(r0) + r2 = borrow r1.s + inc_ref r2 + r3 = r2 + dec_ref r1 + goto L3 +L2: + r4 = 'y' + inc_ref r4 + r3 = r4 +L3: + r5 = 'z' + r6 = PyUnicode_Concat(r3, r5) + dec_ref r3 + return r6 + +[case testBorrowedFinalAttributeAndMerge] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(s: str) -> str: + return (s and C("x").s) + "z" +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(s): + s :: str + r0 :: bit + r1, r2 :: str + r3 :: __main__.C + r4, r5, r6 :: str +L0: + r0 = CPyStr_IsTrue(s) + if r0 goto L2 else goto L1 :: bool +L1: + inc_ref s + r1 = s + goto L3 +L2: + r2 = 'x' + r3 = C(r2) + r4 = borrow r3.s + inc_ref r4 + r1 = r4 + dec_ref r3 +L3: + r5 = 'z' + r6 = PyUnicode_Concat(r1, r5) + dec_ref r1 + return r6 + +[case testBorrowedFinalAttributeOrMerge] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(s: str) -> str: + return (s or C("x").s) + "z" +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(s): + s :: str + r0 :: bit + r1, r2 :: str + r3 :: __main__.C + r4, r5, r6 :: str +L0: + r0 = CPyStr_IsTrue(s) + if r0 goto L1 else goto L2 :: bool +L1: + inc_ref s + r1 = s + goto L3 +L2: + r2 = 'x' + r3 = C(r2) + r4 = borrow r3.s + inc_ref r4 + r1 = r4 + dec_ref r3 +L3: + r5 = 'z' + r6 = PyUnicode_Concat(r1, r5) + dec_ref r1 + return r6 + +[case testBorrowedFinalAttributeChainedComparisonMerge] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(s: str) -> bool: + return s == "x" == C("x").s +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(s): + s, r0 :: str + r1, r2 :: bool + r3 :: str + r4 :: __main__.C + r5 :: str + r6 :: bool +L0: + r0 = 'x' + r1 = CPyStr_EqualLiteral(s, r0, 1) + if r1 goto L2 else goto L1 :: bool +L1: + r2 = r1 + goto L3 +L2: + r3 = 'x' + r4 = C(r3) + r5 = borrow r4.s + r6 = CPyStr_EqualLiteral(r5, r0, 1) + r2 = r6 + dec_ref r4 +L3: + return r2 + +[case testBorrowedFinalInListComprehension_withgil] +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(a: list[C]) -> list[str]: + return [x.s for x in a] +[out] +def C.__init__(self, s): + self :: __main__.C + s :: str +L0: + inc_ref s + self.s = s + return 1 +def f(a): + a :: list + r0 :: native_int + r1 :: list + r2, r3 :: native_int + r4 :: bit + r5 :: object + r6, x :: __main__.C + r7 :: str + r8 :: native_int +L0: + r0 = var_object_size a + r1 = PyList_New(r0) + r2 = 0 +L1: + r3 = var_object_size a + r4 = r2 < r3 :: signed + if r4 goto L2 else goto L4 :: bool +L2: + r5 = list_get_item_unsafe a, r2 + r6 = cast(__main__.C, r5) + x = r6 + r7 = x.s + dec_ref x + CPyList_SetItemUnsafe(r1, r2, r7) +L3: + r8 = r2 + 1 + r2 = r8 + goto L1 +L4: + return r1 + +[case testBorrowedFinalInSetComprehension_withgil] +from typing import Final + +class C: + def __init__(self, d: D) -> None: + self.d: Final = d + +class D: + def __init__(self, s: str) -> None: + self.s: Final = s + +def use(s: str) -> bool: + return True + +def f(a: list[C]) -> set[C]: + return {x for x in a if use(x.d.s)} +[out] +def C.__init__(self, d): + self :: __main__.C + d :: __main__.D +L0: + inc_ref d + self.d = d + return 1 +def D.__init__(self, s): + self :: __main__.D + s :: str +L0: + inc_ref s + self.s = s + return 1 +def use(s): + s :: str +L0: + return 1 +def f(a): + a :: list + r0 :: set + r1, r2 :: native_int + r3 :: bit + r4 :: object + r5, x :: __main__.C + r6 :: __main__.D + r7 :: str + r8 :: bool + r9 :: i32 + r10 :: bit + r11 :: native_int +L0: + r0 = PySet_New(0) + r1 = 0 +L1: + r2 = var_object_size a + r3 = r1 < r2 :: signed + if r3 goto L2 else goto L5 :: bool +L2: + r4 = list_get_item_unsafe a, r1 + r5 = cast(__main__.C, r4) + x = r5 + r6 = borrow x.d + r7 = borrow r6.s + r8 = use(r7) + if r8 goto L3 else goto L6 :: bool +L3: + r9 = PySet_Add(r0, x) + dec_ref x + r10 = r9 >= 0 :: signed +L4: + r11 = r1 + 1 + r1 = r11 + goto L1 +L5: + return r0 +L6: + dec_ref x + goto L4 + +[case testBorrowedFinalAttributeThroughCast] +from typing import Final, cast + +class C: + def __init__(self, x: object) -> None: + self.x: Final = x + +def make() -> object: + return C(1) + +def g(a: object) -> object: + return a + +def h() -> object: + return g(cast(C, make()).x) +[out] +def C.__init__(self, x): + self :: __main__.C + x :: object +L0: + inc_ref x + self.x = x + return 1 +def make(): + r0 :: object + r1 :: __main__.C +L0: + r0 = object 1 + r1 = C(r0) + return r1 +def g(a): + a :: object +L0: + inc_ref a + return a +def h(): + r0 :: object + r1 :: __main__.C + r2, r3 :: object +L0: + r0 = make() + r1 = borrow cast(__main__.C, r0) + r2 = borrow r1.x + r3 = g(r2) + dec_ref r0 + return r3 + +[case testBorrowedFinalAttributeCastChain] +from typing import Final, cast + +class Inner: + def __init__(self, x: object) -> None: + self.x: Final = x + +class Outer: + def __init__(self, inner: object) -> None: + self.inner: Final = inner + +def g(a: object) -> object: + return a + +def make() -> object: + return Outer(Inner(1)) + +def h() -> object: + # cast( Inner, ).x + o = cast(Outer, make()) + return g(cast(Inner, o.inner).x) +[out] +def Inner.__init__(self, x): + self :: __main__.Inner + x :: object +L0: + inc_ref x + self.x = x + return 1 +def Outer.__init__(self, inner): + self :: __main__.Outer + inner :: object +L0: + inc_ref inner + self.inner = inner + return 1 +def g(a): + a :: object +L0: + inc_ref a + return a +def make(): + r0 :: object + r1 :: __main__.Inner + r2 :: __main__.Outer +L0: + r0 = object 1 + r1 = Inner(r0) + r2 = Outer(r1) + dec_ref r1 + return r2 +def h(): + r0 :: object + r1, o :: __main__.Outer + r2 :: object + r3 :: __main__.Inner + r4, r5 :: object +L0: + r0 = make() + r1 = cast(__main__.Outer, r0) + o = r1 + r2 = borrow o.inner + r3 = borrow cast(__main__.Inner, r2) + r4 = borrow r3.x + r5 = g(r4) + dec_ref o + return r5 diff --git a/mypyc/test-data/run-async.test b/mypyc/test-data/run-async.test index cf9b54368c277..a7a08e7ced5f2 100644 --- a/mypyc/test-data/run-async.test +++ b/mypyc/test-data/run-async.test @@ -1974,3 +1974,96 @@ async def async_iter(vals: list[int]) -> AsyncIterator[int]: yield v [typing fixtures/typing-full.pyi] + +[case testBorrowedFinalAttrAcrossAwait] +from typing import Final +from testutil import async_val + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +async def f(c: C) -> str: + # c.s is a final attr; it must not be borrowed across the await, which is a + # suspension point in the middle of the expression. + return c.s + await async_val("hello") + +[file driver.py] +from native import f, C +from testutil import run_generator + +for i in range(50): + c = C("value" + str(i)) + yields, val = run_generator(f(c), inputs=["world"]) + assert yields == ("hello",), yields + assert val == "value" + str(i) + "world", repr(val) + +[case testBorrowedFinalAttrAcrossAsyncComprehension] +import asyncio +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +class AsyncRange: + def __init__(self, n: int) -> None: + self.n = n + self.i = 0 + + def __aiter__(self) -> "AsyncRange": + return self + + async def __anext__(self) -> str: + if self.i >= self.n: + raise StopAsyncIteration + i = self.i + self.i += 1 + # Suspend in the middle of iteration. + await asyncio.sleep(0) + return "item" + str(i) + +async def f(c: C) -> str: + # c.s is a final attr; it must not be borrowed across the async comprehension, + # which suspends via an implicit await on __anext__ that isn't represented as + # an AwaitExpr node in the AST. + return c.s + "".join([x async for x in AsyncRange(3)]) + +async def test_async_comprehension_borrow() -> None: + for i in range(50): + c = C("value" + str(i)) + assert await f(c) == "value" + str(i) + "item0item1item2" + +[file asyncio/__init__.pyi] +async def sleep(t: float) -> None: ... + +[typing fixtures/typing-full.pyi] + +[case testBorrowedFinalAttrAcrossAwaitAfterComprehension] +import asyncio +from typing import Final + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +async def sink() -> str: + await asyncio.sleep(0) + return "z" + +async def f(c: C, xs: list[int]) -> str: + # A (sync) comprehension enters a nested borrow scope earlier in the same + # top-level expression. It must not clear the enclosing expression's suspend + # flag, or c.s (a final attr) gets borrowed across the await below, which + # produces invalid C (the borrow does not survive the suspension point). + return str([x for x in xs]) + (c.s + await sink()) + +async def test_borrow_final_attr_across_await_after_comprehension() -> None: + for i in range(50): + c = C("v" + str(i)) + assert await f(c, [1, 2, 3]) == "[1, 2, 3]" + "v" + str(i) + "z" + +[file asyncio/__init__.pyi] +async def sleep(t: float) -> None: ... + +[typing fixtures/typing-full.pyi] diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 73927bb037a71..4e8b67a0061a8 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -6104,3 +6104,76 @@ base = sys.getrefcount(BIG) o.v = BIG o.v = BIG assert sys.getrefcount(BIG) == base, "reassignment leaked refs" + +[case testBorrowedFinalAttributeInLambdaAndNestedFunction] +from typing import Final, Callable + +class Box: + def __init__(self, n: int) -> None: + self.n = n + +class C: + def __init__(self, b: Box) -> None: + self.b: Final = b + +def use(x: Box, y: Box) -> int: + return x.n + y.n + +def spend(x: Box) -> Box: + # Allocate a lot so a freed Box slot is likely to be reused, turning any + # use-after-free of a borrowed attribute into an observable wrong result. + junk = [Box(i) for i in range(100)] + return Box(x.n + junk[50].n - 50) + +def make_lambda() -> Callable[[], int]: + # The lambda body creates a C, borrows its final attribute, then calls + # spend() (which allocates heavily) before reading the borrow. + return lambda: use(C(Box(111)).b, spend(Box(0))) + +def make_nested() -> Callable[[], int]: + def inner() -> int: + return use(C(Box(111)).b, spend(Box(0))) + return inner + +def test_borrowed_final_attribute_in_lambda() -> None: + f = make_lambda() + for _ in range(1000): + assert f() == 111 + +def test_borrowed_final_attribute_in_nested_function() -> None: + g = make_nested() + for _ in range(1000): + assert g() == 111 + +[case testBorrowedFinalAttributeInComprehension] +from typing import Final + +class Box: + def __init__(self, n: int) -> None: + self.n = n + +class C: + def __init__(self, b: Box) -> None: + self.b: Final = b + +def use(x: Box, y: Box) -> int: + return x.n + y.n + +def spend(x: Box) -> Box: + # Allocate a lot so a freed Box slot is likely to be reused, turning any + # use-after-free of a borrowed attribute into an observable wrong result. + junk = [Box(i) for i in range(100)] + return Box(x.n + junk[50].n - 50) + +def comp(ns: list[int]) -> list[int]: + # Each iteration allocates a fresh C, borrows its final attribute .b, then + # calls spend() (which allocates heavily) before reading the borrow. The + # borrow container must be kept alive for the current iteration only; if it + # is scoped to the whole (comprehension) expression, the refcount pass + # dec_refs an uninitialized temporary on the loop back-edge and leaks/uses + # freed memory. + return [use(C(Box(111)).b, spend(Box(i))) for i in ns] + +def test_borrowed_final_attribute_in_comprehension() -> None: + for _ in range(1000): + assert comp([1, 2, 3, 4, 5]) == [112, 113, 114, 115, 116] diff --git a/mypyc/test-data/run-generators.test b/mypyc/test-data/run-generators.test index cf1dac7c57333..a23af2ed6eea7 100644 --- a/mypyc/test-data/run-generators.test +++ b/mypyc/test-data/run-generators.test @@ -944,3 +944,46 @@ from typing import Optional, Union def test_compiledGeneratorEmptyTuple() -> None: jobs: Generator[Optional[str], None, None] = (_ for _ in ()) assert list(jobs) == [] + +[case testBorrowedFinalAttrAcrossYield] +from typing import Final, Generator + +class C: + def __init__(self, s: str) -> None: + self.s: Final = s + +def f(c: C) -> Generator[str, str, None]: + # c.s is a final attr; it must not be borrowed across the yield, which is + # a suspension point in the middle of the expression. + x = c.s + (yield "first") + yield x + +def test_borrow_across_yield() -> None: + for i in range(50): + c = C("value" + str(i)) + g = f(c) + assert next(g) == "first" + assert g.send("world") == "value" + str(i) + "world" + +def yield_from_gen(c: C) -> Generator[str, str, str]: + r = yield "inner" + return c.s + r + +def g2(c: C) -> Generator[str, str, str]: + # Borrow a final attr across a yield from expression. + x = c.s + (yield from yield_from_gen(c)) + return x + +def test_borrow_across_yield_from() -> None: + for i in range(50): + c = C("v" + str(i)) + gen = g2(c) + assert next(gen) == "inner" + try: + gen.send("!") + except StopIteration as e: + assert e.value == "v" + str(i) + "v" + str(i) + "!", e.value + else: + assert False + +[typing fixtures/typing-full.pyi] diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index 50ffc9004743e..6608e6db8e7b4 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -36,6 +36,7 @@ "irbuild-statements.test", "irbuild-nested.test", "irbuild-classes.test", + "irbuild-final.test", "irbuild-optional.test", "irbuild-any.test", "irbuild-generics.test", From 0faa413ebf7c924a864ef5dabd70303d898e7766 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Fri, 10 Jul 2026 05:47:43 +0530 Subject: [PATCH 123/127] Use `PYODIDE` environment variable for Emscripten cross-compilation detection (#21714) I noticed that y'all were carrying this workaround ever since #14888. A better way to detect this is through [the `PYODIDE` environment variable, which we've documented over the years](https://pyodide-build.readthedocs.io/en/latest/how-to/migrate.html#detecting-pyodide-at-build-time), and is what most projects use now. The MACHDEP generality would mostly apply to non-Pyodide-specific WASM build targets, which are growing in relevance but are a bit less mature as Pyodide at this time. --- mypy/options.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/options.py b/mypy/options.py index 2f03cd3eab5b7..92c9ea3b1701d 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -1,9 +1,9 @@ from __future__ import annotations +import os import pprint import re import sys -import sysconfig from collections.abc import Callable from re import Pattern from typing import Any, Final @@ -112,11 +112,11 @@ def __init__(self) -> None: # then mypy does not search for PEP 561 packages. self.python_executable: str | None = sys.executable - # When cross compiling to emscripten, we need to rely on MACHDEP because - # sys.platform is the host build platform, not emscripten. - MACHDEP = sysconfig.get_config_var("MACHDEP") - if MACHDEP == "emscripten": - self.platform = MACHDEP + # When cross compiling to emscripten, sys.platform is the host build + # platform, not emscripten. Pyodide sets the PYODIDE environment variable + # at build time, so we use it to detect the emscripten target. + if "PYODIDE" in os.environ: + self.platform = "emscripten" else: self.platform = sys.platform From a9f62a3cf98a58a7a2607b7c81695802b39f5edc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 10 Jul 2026 12:29:39 +0100 Subject: [PATCH 124/127] [mypyc] Make attribute access memory safe on free-threaded builds (#21705) This fixes memory unsafety when there is a race condition related to attribute get/set operations on a free-threaded build, for simple reference-based attributes only (`PyObject *`). This includes attributes with primitive types like `list`, `set` and `str` and `dict`, and native class types. The main idea is to use delayed decref for the old attribute value when assigning to an attribute. There are detailed comments explaining the approach in detail. This causes a ~5% slowdown in self check when using 3.14t. Currently it looks like a significant perf cost is unavoidable if we want to fix memory unsafety, but we can further optimize mypy to reduce the impact by introducing final attributes, for example. Remaining work includes fixing attributes with fixed-length tuple types and tagged integer types (in follow-up PRs). I used coding agent assist heavily. Work on mypyc/mypyc#1203. --- mypyc/codegen/emitclass.py | 53 ++++++++++- mypyc/codegen/emitfunc.py | 83 ++++++++++++++--- mypyc/ir/class_ir.py | 16 ++++ mypyc/ir/rtypes.py | 13 +++ mypyc/irbuild/builder.py | 12 +-- mypyc/lib-rt/pythonsupport.c | 21 +++++ mypyc/lib-rt/pythonsupport.h | 134 +++++++++++++++++++++++++++ mypyc/test-data/irbuild-classes.test | 40 ++++++++ mypyc/test-data/run-classes.test | 52 +++++++++++ 9 files changed, 404 insertions(+), 20 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 9baaec06a4611..a1127b15ad9d1 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -29,6 +29,7 @@ BITMAP_BITS, BITMAP_TYPE, CPYFUNCTION_NAME, + IS_FREE_THREADED, MYPYC_DEFAULTS_SETUP, NATIVE_PREFIX, PREFIX, @@ -43,7 +44,7 @@ FuncIR, get_text_signature, ) -from mypyc.ir.rtypes import RTuple, RType, object_rprimitive +from mypyc.ir.rtypes import RTuple, RType, is_simple_refcounted_pointer, object_rprimitive from mypyc.namegen import NameGenerator from mypyc.sametype import is_same_type @@ -1165,6 +1166,32 @@ def generate_getter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N emitter.emit_line("{") attr_expr = f"self->{attr_field}" + if IS_FREE_THREADED and is_simple_refcounted_pointer(rtype): + # In free-threaded builds, load the attribute and take a new reference + # atomically to avoid a use-after-free race with a concurrent setter. + # CPy_GetAttrRef returns NULL if the attribute is undefined (NULL field), + # which is exactly the error/undefined value for a 'PyObject *' field. + # + # Final attributes are never rebound (no setter), so there is no concurrent + # writer to race with: a plain load + incref is safe. Use the cheaper + # CPy_GetAttrRefFinal, which skips the try-incref and _Py_NewRefWithLock + # slow path entirely (an unconditional Py_INCREF needs no maybe-weakref). + # This getter is generated per defining class, so a direct membership test + # matches the read-only getset table above (no need to walk the MRO). + if attr in cl.final_attributes: + getattr_ref = f"CPy_GetAttrRefFinal((PyObject **)&{attr_expr})" + else: + getattr_ref = f"CPy_GetAttrRef((PyObject **)&{attr_expr})" + emitter.emit_line(f"PyObject *retval = {getattr_ref};") + emitter.emit_line("if (unlikely(retval == NULL)) {") + emitter.emit_line("PyErr_SetString(PyExc_AttributeError,") + emitter.emit_line(f' "attribute {repr(attr)} of {repr(cl.name)} undefined");') + emitter.emit_line("return NULL;") + emitter.emit_line("}") + emitter.emit_line("return retval;") + emitter.emit_line("}") + return + # HACK: Don't consider refcounted values as always defined, since it's possible to # access uninitialized values via 'gc.get_objects()'. Accessing non-refcounted # values is benign. @@ -1202,6 +1229,30 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N emitter.emit_line("return -1;") emitter.emit_line("}") + if IS_FREE_THREADED and is_simple_refcounted_pointer(rtype): + # In free-threaded builds, publish the new value atomically via + # CPy_SetAttrRef so a concurrent reader (see CPy_GetAttrRef) never sees a + # torn pointer or a freed old value. CPy_SetAttrRef steals its value and + # reclaims the old one, so we cast/type-check the incoming value, take a + # new reference (the setter only borrows 'value'), then hand it over. + # A NULL value deletes the attribute (reclaims the old value, stores NULL). + if deletable: + emitter.emit_line("if (value != NULL) {") + if is_same_type(rtype, object_rprimitive): + emitter.emit_line("PyObject *tmp = value;") + else: + emitter.emit_cast("value", "tmp", rtype, declare_dest=True) + emitter.emit_lines("if (!tmp)", " return -1;") + emitter.emit_inc_ref("tmp", rtype) + emitter.emit_line(f"CPy_SetAttrRef((PyObject **)&self->{attr_field}, tmp);") + if deletable: + emitter.emit_line("} else {") + emitter.emit_line(f"CPy_SetAttrRef((PyObject **)&self->{attr_field}, NULL);") + emitter.emit_line("}") + emitter.emit_line("return 0;") + emitter.emit_line("}") + return + # HACK: Don't consider refcounted values as always defined, since it's possible to # access uninitialized values via 'gc.get_objects()'. Accessing non-refcounted # values is benign. diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index dcb606f6ab51b..4c3c17d047c8a 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -12,7 +12,13 @@ TracebackAndGotoHandler, c_array_initializer, ) -from mypyc.common import GENERATOR_ATTRIBUTE_PREFIX, HAVE_IMMORTAL, NATIVE_PREFIX, REG_PREFIX +from mypyc.common import ( + GENERATOR_ATTRIBUTE_PREFIX, + HAVE_IMMORTAL, + IS_FREE_THREADED, + NATIVE_PREFIX, + REG_PREFIX, +) from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR, all_values from mypyc.ir.ops import ( @@ -83,6 +89,7 @@ is_int_rprimitive, is_none_rprimitive, is_pointer_rprimitive, + is_simple_refcounted_pointer, is_tagged, ) @@ -394,6 +401,35 @@ def get_attr_expr(self, obj: str, op: GetAttr | SetAttr, decl_cl: ClassIR) -> st cast = f"({decl_cl.struct_name(self.emitter.names)} *)" return f"({cast}{obj})->{self.emitter.attr(op.attr)}" + def emit_load_attr_take_ref( + self, dest: str, obj: str, op: GetAttr, cl: ClassIR, attr_rtype: RType, attr_expr: str + ) -> bool: + """Emit the load of a native attribute into 'dest', taking a new reference. + + On free-threaded builds, reading a single reference-counted 'PyObject *' field + and taking a new reference must be done atomically to avoid a use-after-free + race with a concurrent setter. CPy_GetAttrRef performs the load and incref + atomically and returns a new reference (or NULL if undefined), so callers must + NOT emit a separate inc_ref. Return True in that case so the caller can skip it. + + Final attributes are never rebound (no setter), so there is no concurrent writer + and no use-after-free window; an owned read uses the cheaper CPy_GetAttrRefFinal + (a plain load + incref). Borrowed reads keep the plain load: they are only emitted + for attributes safe to borrow on free-threaded builds (Final and vec attrs -- see + transform_member_expr in irbuild), whose values live as long as their container. + The default (GIL) build always takes the plain-load path and increfs separately. + """ + use_get_attr_ref = ( + IS_FREE_THREADED and is_simple_refcounted_pointer(attr_rtype) and not op.is_borrowed + ) + if use_get_attr_ref and cl.is_final_attr(op.attr): + self.emitter.emit_line(f"{dest} = CPy_GetAttrRefFinal((PyObject **)&{attr_expr});") + elif use_get_attr_ref: + self.emitter.emit_line(f"{dest} = CPy_GetAttrRef((PyObject **)&{attr_expr});") + else: + self.emitter.emit_line(f"{dest} = {attr_expr};") + return use_get_attr_ref + def visit_get_attr(self, op: GetAttr) -> None: if op.allow_error_value: self.get_attr_with_allow_error_value(op) @@ -427,7 +463,9 @@ def visit_get_attr(self, op: GetAttr) -> None: else: # Otherwise, use direct or offset struct access. attr_expr = self.get_attr_expr(obj, op, decl_cl) - self.emitter.emit_line(f"{dest} = {attr_expr};") + use_get_attr_ref = self.emit_load_attr_take_ref( + dest, obj, op, cl, attr_rtype, attr_expr + ) always_defined = cl.is_always_defined(op.attr) merged_branch = None if not always_defined: @@ -458,7 +496,7 @@ def visit_get_attr(self, op: GetAttr) -> None: ) ) - if attr_rtype.is_refcounted and not op.is_borrowed: + if attr_rtype.is_refcounted and not op.is_borrowed and not use_get_attr_ref: if not merged_branch and not always_defined: self.emitter.emit_line("} else {") self.emitter.emit_inc_ref(dest, attr_rtype) @@ -480,16 +518,20 @@ def get_attr_with_allow_error_value(self, op: GetAttr) -> None: cl = rtype.class_ir attr_rtype, decl_cl = cl.attr_details(op.attr) - # Direct struct access without NULL check attr_expr = self.get_attr_expr(obj, op, decl_cl) - self.emitter.emit_line(f"{dest} = {attr_expr};") - - # Only emit inc_ref if not NULL - if attr_rtype.is_refcounted and not op.is_borrowed: - check = self.error_value_check(op, "!=") - self.emitter.emit_line(f"if ({check}) {{") - self.emitter.emit_inc_ref(dest, attr_rtype) - self.emitter.emit_line("}") + # On free-threaded builds this takes a new reference atomically (see + # emit_load_attr_take_ref). CPy_GetAttrRef returns NULL when the field is + # undefined, which is precisely the error value here, so the "NULL without + # AttributeError" behavior is preserved (this op has error_kind ERR_NEVER). + # is_simple_refcounted_pointer excludes tuples/vecs, so error_value_check's + # special cases only matter on the plain-load path (where no ref was taken). + if not self.emit_load_attr_take_ref(dest, obj, op, cl, attr_rtype, attr_expr): + # Only emit inc_ref if not NULL + if attr_rtype.is_refcounted and not op.is_borrowed: + check = self.error_value_check(op, "!=") + self.emitter.emit_line(f"if ({check}) {{") + self.emitter.emit_inc_ref(dest, attr_rtype) + self.emitter.emit_line("}") def next_branch(self) -> Branch | None: if self.op_index + 1 < len(self.ops): @@ -529,6 +571,23 @@ def visit_set_attr(self, op: SetAttr) -> None: op.attr, ) ) + elif IS_FREE_THREADED and is_simple_refcounted_pointer(attr_rtype): + # In free-threaded builds, publishing a single reference-counted + # 'PyObject *' field must be atomic so a concurrent reader (see + # CPy_GetAttrRef) never observes a torn pointer or a freed value. + # Both helpers steal the reference to src. + attr_expr = self.get_attr_expr(obj, op, decl_cl) + if op.is_init: + # The attribute is known to be previously undefined (NULL), so + # there is no old value to reclaim; a relaxed store suffices + # (self's later publication provides the release barrier -- see + # CPy_InitAttrRef). + self.emitter.emit_line(f"CPy_InitAttrRef((PyObject **)&{attr_expr}, {src});") + else: + # Atomically swap in the new value and reclaim the old one. + self.emitter.emit_line(f"CPy_SetAttrRef((PyObject **)&{attr_expr}, {src});") + if op.error_kind == ERR_FALSE: + self.emitter.emit_line(f"{dest} = 1;") else: # ...and struct access for normal attributes. attr_expr = self.get_attr_expr(obj, op, decl_cl) diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index 028df5898d920..6ea1eb072999d 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -269,6 +269,22 @@ def attr_details(self, name: str) -> tuple[RType, ClassIR]: def attr_type(self, name: str) -> RType: return self.attr_details(name)[0] + def is_final_attr(self, name: str) -> bool: + """Is the (possibly inherited) attribute Final, i.e. never rebound? + + A Final attribute is read-only at runtime (it has no setter) and is assigned + exactly once during construction, so it can never be reassigned afterwards. + This makes it safe to borrow on free-threaded builds (no concurrent store can + invalidate a borrowed reference) and lets reads skip the concurrent-writer + guard. Returns False for properties and for attributes this class doesn't have. + """ + for ir in self.mro: + if name in ir.attributes: + return name in ir.final_attributes + if name in ir.property_types: + return False + return False + def method_decl(self, name: str) -> FuncDecl: for ir in self.mro: if name in ir.method_decls: diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 1a5515d5621a8..afe3d3e9df39b 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -569,6 +569,19 @@ def is_native_rprimitive(rtype: RType) -> bool: return isinstance(rtype, RPrimitive) and rtype.name in KNOWN_NATIVE_TYPES +def is_simple_refcounted_pointer(rtype: RType) -> bool: + """Is rtype represented at runtime as a single, reference-counted 'PyObject *'? + + This covers 'object', 'str', containers, instances of native classes and + optional/union types -- everything whose C representation is exactly one + 'PyObject *' field that owns a reference. It excludes unboxed types (tagged + 'int', fixed-width ints, floats, bools), inline tuples ('RTuple'), vectors + ('RVec') and C structs ('RStruct'), which need different treatment for + free-threaded memory safety. + """ + return rtype.is_refcounted and not rtype.is_unboxed and not isinstance(rtype, RStruct) + + def is_tagged(rtype: RType) -> TypeGuard[RPrimitive]: return rtype is int_rprimitive or rtype is short_int_rprimitive diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index d8ae71e33bf59..0b598a00889f5 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -1642,13 +1642,11 @@ def is_final_native_attr_ref(self, expr: MemberExpr) -> bool: builds, since no concurrent store can invalidate the borrowed reference. """ obj_rtype = self.node_type(expr.expr) - if not (isinstance(obj_rtype, RInstance) and obj_rtype.class_ir.is_ext_class): - return False - # Find the class that defines the attribute and check whether it's Final there. - for ir in obj_rtype.class_ir.mro: - if expr.name in ir.attributes: - return expr.name in ir.final_attributes - return False + return ( + isinstance(obj_rtype, RInstance) + and obj_rtype.class_ir.is_ext_class + and obj_rtype.class_ir.is_final_attr(expr.name) + ) def root_is_reassigned(self, v: Value) -> bool: """Is the root local variable a borrow chain 'v' reads from reassigned this expression? diff --git a/mypyc/lib-rt/pythonsupport.c b/mypyc/lib-rt/pythonsupport.c index 0a99f0ae2e29b..a8f5a4f4ad4ea 100644 --- a/mypyc/lib-rt/pythonsupport.c +++ b/mypyc/lib-rt/pythonsupport.c @@ -5,6 +5,27 @@ #include "pythonsupport.h" +#ifdef Py_GIL_DISABLED +// Cold slow path of CPy_GetAttrRef (declared in pythonsupport.h). Reached only +// when the inline fast-path try-incref fails: the value is owned by another +// thread, so taking a reference requires an atomic shared-refcount operation. +// Kept out-of-line so the inline fast path stays small. +// +// First try the lock-free shared-refcount CAS. If the value has not had +// maybe-weakref set yet (for example, it was published by CPy_InitAttrRef), force +// a cross-thread reference via _Py_NewRefWithLock, which cannot fail and sets +// maybe-weakref so subsequent reads take the fast path. The value was already +// observed in the field by CPy_GetAttrRef; CPy_SetAttrRef's QSBR-delayed decref +// keeps any replaced value alive long enough for this reader. +CPy_NOINLINE +PyObject *CPy_GetAttrRefSlow(PyObject *v) { + if (_Py_TryIncRefShared(v)) { + return v; + } + return _Py_NewRefWithLock(v); // sets maybe-weakref; cannot fail +} +#endif + ///////////////////////////////////////// // Adapted from bltinmodule.c in Python 3.7.0 PyObject* diff --git a/mypyc/lib-rt/pythonsupport.h b/mypyc/lib-rt/pythonsupport.h index 35f1e78df3915..33c5a596d1747 100644 --- a/mypyc/lib-rt/pythonsupport.h +++ b/mypyc/lib-rt/pythonsupport.h @@ -23,6 +23,10 @@ #include "internal/pycore_setobject.h" // _PySet_Update #endif +#ifdef Py_GIL_DISABLED +#include "internal/pycore_object.h" // _Py_TryIncrefFast, _Py_TryIncRefShared +#endif + #if CPY_3_12_FEATURES #include "internal/pycore_frame.h" #endif @@ -34,6 +38,136 @@ extern "C" { } // why isn't emacs smart enough to not indent this #endif +#ifdef Py_GIL_DISABLED +// Read a native attribute that is a single reference-counted 'PyObject *' field, +// returning a new reference (or NULL if the field is NULL/undefined). +// +// On free-threaded builds a plain load followed by an incref races with a +// concurrent setter that may decref the old value to zero and free it before the +// incref runs (use-after-free). CPy_SetAttrRef avoids that by reclaiming old +// values through QSBR-delayed decref, so a value observed in the field remains +// safe to touch while this reader is running. +// +// Only the hot case is inlined here: an incref of a value owned by this thread or +// immortal, via '_Py_TryIncrefFast' (no CAS, no loop). Everything colder -- the +// cross-thread shared-refcount CAS and the _Py_NewRefWithLock fallback -- lives +// out-of-line in 'CPy_GetAttrRefSlow'. Splitting it this way keeps each call +// site's fast path small enough to inline, which is measurably faster than +// letting the compiler auto-out-line the whole helper (that merges every read +// site's branch history into one shared copy and mispredicts). It is only used in +// free-threaded builds; the default (GIL) build keeps the plain load + incref +// generated inline by mypyc. +PyObject *CPy_GetAttrRefSlow(PyObject *v); + +static inline PyObject *CPy_GetAttrRef(PyObject **field) { + PyObject *v = (PyObject *)_Py_atomic_load_ptr_acquire(field); + if (v == NULL) { + return NULL; + } + if (_Py_TryIncrefFast(v)) { + return v; + } + return CPy_GetAttrRefSlow(v); +} + +// Read a native attribute that is a single reference-counted 'PyObject *' field +// AND is Final (assigned once during construction, never rebound -- mypyc emits no +// setter for it), returning a new reference (or NULL if undefined). +// +// A Final attribute has no concurrent writer after 'self' is published, so the +// use-after-free race that CPy_GetAttrRef guards against cannot happen: the field +// holds a strong reference for the object's whole lifetime, and any thread reading +// it necessarily holds 'self', which keeps the value alive. So the try-incref + +// _Py_NewRefWithLock fallback are unnecessary here -- a plain load + Py_INCREF is +// safe. A cross-thread Py_INCREF is an unconditional +// atomic add on ob_ref_shared, so (unlike CPy_GetAttrRef's try-incref) it needs no +// maybe-weakref and has no slow path. The load is relaxed rather than acquire: the +// reader reached 'self' through a synchronization edge (self's own publication) +// that already ordered the construction stores before it, exactly as with +// CPy_InitAttrRef's relaxed store. Relaxed keeps it TSan-clean at zero cost (plain +// mov/ldr). +static inline PyObject *CPy_GetAttrRefFinal(PyObject **field) { + PyObject *v = (PyObject *)_Py_atomic_load_ptr_relaxed(field); + if (v != NULL) { + Py_INCREF(v); + } + return v; +} + +// Reclaim the previous value of a native attribute after it has been replaced. +// +// CPy_GetAttrRef reads the field optimistically without holding any lock the +// writer also takes, so a reader can load the old pointer and then try to take a +// reference after this store. The old value must therefore stay alive until every +// thread has passed a quiescent point, which is exactly what a QSBR-deferred +// decref guarantees. So all mortal old values are +// reclaimed via _PyObject_XDecRefDelayed, matching CPython's own replace-a-slot +// paths (e.g. _PyObject_SetDict / _PyObject_SetManagedDict). +// +// We deliberately do NOT take a "local refcount > 1, owned by this thread" fast +// path: dropping the field's reference is not the only decref of the object, so a +// non-freeing local decrement here does not prevent an unrelated reference holder +// from driving the object to zero (a plain, non-deferred Py_DECREF -> _Py_Dealloc) +// while an in-flight reader still holds the stale pointer -- a use-after-free that +// only QSBR deferral closes. Immortal objects are never freed, so skipping their +// decref entirely is safe and avoids queuing a no-op onto the delayed-free list. +static inline void CPy_DecRefAttrOld(PyObject *op) { + if (op == NULL) { + return; + } + if (_Py_IsImmortal(op)) { + return; + } + _PyObject_XDecRefDelayed(op); +} + +// Set a native attribute that is a single reference-counted 'PyObject *' field, +// stealing the reference to 'value' (which may be NULL to delete the attribute) +// and safely reclaiming the previous value. +// +// Memory safety does NOT depend on SetMaybeWeakref here, so (unlike an earlier +// version) we do not call it. Two things keep this safe: +// - The old value is reclaimed via CPy_DecRefAttrOld, a QSBR-deferred decref, so +// it cannot be freed while an in-flight CPy_GetAttrRef still holds the stale +// pointer. +// - A concurrent cross-thread reader whose inline fast-path try-incref fails on +// an unflagged 'value' still cannot fail and never blocks on this writer: +// CPy_GetAttrRef falls into CPy_GetAttrRefSlow, which is fully lock-free. It +// retries with a lock-free shared-refcount CAS (_Py_TryIncRefShared) and, only +// if that also fails, forces a reference via _Py_NewRefWithLock, which cannot +// fail and lazily sets maybe-weakref so later cross-thread reads take the CAS. +// This mirrors CPy_InitAttrRef, which already omits SetMaybeWeakref for the same +// reason. Setting the flag here would only be a possible performance tuning knob +// (it would let that first cross-thread reader succeed on the cheaper +// _Py_TryIncRefShared CAS instead of falling through to _Py_NewRefWithLock); it is +// not needed for correctness. The atomic exchange publishes the new pointer and +// hands back the old one without a writer/writer race. +static inline void CPy_SetAttrRef(PyObject **field, PyObject *value) { + PyObject *old = (PyObject *)_Py_atomic_exchange_ptr(field, value); + CPy_DecRefAttrOld(old); +} + +// Initialize a native attribute that is known to be previously undefined (NULL), +// stealing the reference to 'value'. +// +// Initializer stores only happen while 'self' is still thread-local (the +// attribute-definedness analysis marks a SetAttr as an initializer only before +// 'self' can leak -- see mypyc/analysis/attrdefined.py). So there is no old value +// to reclaim, no competing writer, and the field store is not itself the +// publication point: 'self' is published later (when it escapes __init__ or is +// returned), and that publication carries the release barrier making all the +// construction stores visible. A relaxed store therefore suffices. +// +// Unlike CPy_SetAttrRef, this deliberately does NOT call SetMaybeWeakref (its CAS +// is pure overhead here, ~+2.6ns per fresh store, and construction-heavy code +// pays it on every attribute of every new object). The cost is moved off this hot +// path onto CPy_GetAttrRef's cold slow path, which sets maybe-weakref lazily on +// the first cross-thread read that needs it. +static inline void CPy_InitAttrRef(PyObject **field, PyObject *value) { + _Py_atomic_store_ptr_relaxed(field, value); +} +#endif + PyObject* update_bases(PyObject *bases); int init_subclass(PyTypeObject *type, PyObject *kwds); diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 66caf0772ec40..8eaa63e4583b5 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1276,6 +1276,46 @@ L0: r1 = r0.x return r1 +[case testCanBorrowFinalAttribute_nogil] +from typing import Final + +# On free-threaded builds native attribute reads are not borrowed (a concurrent +# store could free the old value), EXCEPT Final attributes, which can't be rebound. +# Here the intermediate 'd.c' is borrowed because 'c' is Final; contrast with +# testCannotBorrowAttribute_nogil, where the non-Final 'c' is read owned. The +# borrow decision uses the same ClassIR.is_final_attr predicate as codegen. +def f(d: D) -> int: + return d.c.xf + +class C: + def __init__(self, xf: int) -> None: + self.xf: Final = xf +class D: + def __init__(self, c: C) -> None: + self.c: Final = c +[out] +def f(d): + d :: __main__.D + r0 :: __main__.C + r1 :: int +L0: + r0 = borrow d.c + r1 = r0.xf + keep_alive d + return r1 +def C.__init__(self, xf): + self :: __main__.C + xf :: int +L0: + self.xf = xf + return 1 +def D.__init__(self, c): + self :: __main__.D + c :: __main__.C +L0: + self.c = c + return 1 + [case testNoBorrowOverPropertyAccess] class C: d: D diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 4e8b67a0061a8..56ad3673e2896 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2865,6 +2865,58 @@ def test_rebind_inherited_via_setattr() -> None: assert d.x == 1 assert d.y == 2 +[case testFinalRefcountedAttributeRead] +# Exercises reading Final reference-counted ('PyObject *') attributes, which on +# free-threaded builds use a faster read path (CPy_GetAttrRefFinal) that skips the +# concurrent-writer guard, since a Final attribute is never rebound. +from typing import Final + +class C: + def __init__(self, s: str, items: list[int]) -> None: + self.s: Final = s + self.items: Final = items + + def read_s(self) -> str: + return self.s + + def read_items(self) -> list[int]: + return self.items + + def chained_len(self) -> int: + # borrowed intermediate read of a Final attr, then a call on it + return len(self.items) + +class D(C): + def __init__(self, s: str, items: list[int], t: str) -> None: + super().__init__(s, items) + self.t: Final = t + + def read_inherited(self) -> str: + return self.s + +def test_read_final_object_attrs() -> None: + c = C("hello", [1, 2, 3]) + # Read via getter (property access) and via native method bodies. + assert c.s == "hello" + assert c.read_s() == "hello" + assert c.items == [1, 2, 3] + assert c.read_items() == [1, 2, 3] + assert c.chained_len() == 3 + # Reading repeatedly must not corrupt the refcount / free the value. + for _ in range(1000): + assert c.read_s() == "hello" + assert c.read_items() == [1, 2, 3] + assert c.s == "hello" + assert c.read_items() is c.items + +def test_read_inherited_final_attr() -> None: + d = D("a", [4, 5], "b") + assert d.s == "a" + assert d.t == "b" + assert d.read_s() == "a" + assert d.read_inherited() == "a" + assert d.read_items() == [4, 5] + [case testClassDerivedFromIntEnum] from enum import IntEnum, auto From 2c2154672040c52e481f423854d104e6cf172585 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jul 2026 09:07:04 +0100 Subject: [PATCH 125/127] [mypyc] Update documentation of race conditions under free threading (#21726) Mypyc now gives additional thread safety guarantees. --- mypyc/doc/differences_from_python.rst | 48 ++++++++++----------------- mypyc/doc/librt_vecs.rst | 16 ++++++--- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/mypyc/doc/differences_from_python.rst b/mypyc/doc/differences_from_python.rst index c65c330edbef3..7e6cf37154d3e 100644 --- a/mypyc/doc/differences_from_python.rst +++ b/mypyc/doc/differences_from_python.rst @@ -293,40 +293,26 @@ attribute tends to be faster than a plain global variable in compiled code:: Free threading -------------- -Mypyc supports free threading, but it doesn't provide the exact -memory safety guarantees as Python in compiled modules under -free threading when there are race conditions. - -Additionally, optimized primitive operations in compiled code may have -different atomicity properties compared to CPython. Use explicit -synchronization if code depends on operations being atomic. This is -already the recommended approach for normal Python code. - -Currently, compiled code must ensure that proper synchronization is -used to prevent data races involving non-final attributes in native -classes, unless the attribute has a value type such as ``bool``, -``float`` or ``i64``. You can use explicit -synchronization, such as via -:ref:`librt.threading.Lock ` (or -:py:class:`threading.Lock`, which is less efficient than -``librt.threading.Lock``) if there is a possibility of such a data -race. +Mypyc supports free threading. However, optimized primitive operations in +compiled code may have different atomicity properties compared to CPython. +Use explicit synchronization if code depends on operations being atomic and +race conditions are possible. This is already the recommended approach for +normal Python code. You can often use :ref:`librt.threading.Lock ` +(or :py:class:`threading.Lock`, which is less efficient than +``librt.threading.Lock``) to fix data races. + +Since mypyc 2.3, the vast majority of operations are memory safe even if +there are race conditions (unlike earlier mypyc releases). This includes +list operations and access to native instance attributes (except for +a few less common use cases that will be fixed in future releases). .. note:: - We are working on improving memory safety in free-threading - builds of Python, and hope to make all normal Python features - memory safe, while providing more efficient but less safe - opt-in, non-standard features. - -As libraries often won't be able to control the concurrent access by -user code, we recommend that modules document that multi-threaded -access is only supported via public interfaces that ensure correct -synchronization. Marking attributes as internal using an underscore -attribute prefix is another possibility, but this is not enforced at -runtime. Another option is to document that multithreaded access is -not supported, or that particular objects should not be used from -multiple threads concurrently. + Operations that aren't safe under race conditions in interpreted CPython + are not expected to be memory safe in compiled code either. + Some :ref:`librt ` features are heavily optimized for performance and + don't guarantee memory safety when there are race conditions + (notably the :ref:`vec ` type). It's always safe to perform read-only operations concurrently. Using objects with final attributes and tuple objects can help prevent diff --git a/mypyc/doc/librt_vecs.rst b/mypyc/doc/librt_vecs.rst index dad3be621ff4c..a7f5bc3ffbd96 100644 --- a/mypyc/doc/librt_vecs.rst +++ b/mypyc/doc/librt_vecs.rst @@ -1,3 +1,5 @@ +.. _librt-vecs: + librt.vecs ========== @@ -198,15 +200,21 @@ with no unnecessary temporary objects. Thread safety ------------- +The ``vec`` type is heavily optimized for performance, and this means that ``vec`` +gives fewer thread safety guarantees than built-in types such as ``list``. +``vec`` is a lower-level type where implicit synchronization would have a very +significant performance cost. However, since vec lengths are immutable, some race +conditions that lists can be susceptible to are not possible with vecs. + In free-threaded Python builds, it's unsafe to write or modify an item if other threads might be concurrently accessing *the same item*. For example, writing ``v[4]`` is not safe to do if another thread might be reading ``v[4]``. Similarly, two threads concurrently calling ``append`` or ``remove`` on the same vec object is not safe. -This is different from list objects, since vec is a lower-level type where implicit -synchronization would have a significant performance cost. However, since vec lengths -are immutable, some race conditions that lists can be susceptible to are not possible -with vecs. +Similarly, assigning to an instance attribute of a native class with a ``vec`` type +is unsafe if another thread might be accessing the same attribute concurrently +(in a free-threaded Python build only). Using ``Final`` attributes is a way +to prevent this race condition, since ``Final`` attributes cannot be rebound. Implementation details ---------------------- From 4d8ad2ab5e86c99581b73775f2c00b9b8265b589 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jul 2026 10:49:03 +0100 Subject: [PATCH 126/127] Update changelog for 2.3 release (#21728) Release tracking issue: #21717 --- CHANGELOG.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02739ef6591fc..efe63effbfb4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,122 @@ ## Next Release +## Mypy 2.3 + +We've just uploaded mypy 2.3.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). +Mypy is a static type checker for Python. This release includes new features, performance +improvements and bug fixes. You can install it as follows: + + python3 -m pip install -U mypy + +You can read the full documentation for this release on [Read the Docs](http://mypy.readthedocs.io). + +### The Upcoming Switch to the New Native Parser + +We are planning to enable the new native parser (`--native-parser`) by +default soon. We recommend that you test the native parser in your projects and report +any issues in the [mypy issue tracker](https://github.com/python/mypy/issues). + +### Mypyc Free-threading Memory Safety + +Free-threaded Python builds that don't have the GIL require additional synchronization +primitives or lock-free algorithms to ensure memory safety when there are race conditions +(for example, when a thread reads a list item while another thread writes the same list +item concurrently). This release greatly improves memory safety of free threading. + +List operations are now memory-safe on free threaded Python builds, even in the presence of +race conditions. This has some performance cost. For list-heavy workloads, using +`librt.vecs.vec` instead of list is often significantly faster, but note that `vec` is not +(and likely won't be) fully memory safe, and the user is expected to avoid race conditions. +The newly introduced `librt.threading.Lock` helps with this. Using variable-length tuples +can also be more efficient than lists, since tuples are immutable and don't require +expensive synchronization to ensure memory safety. + +Instance attribute access is also (mostly) memory safe now on free-threaded builds in +the presence of race conditions. We are planning to fix the remaining unsafe cases in a +future release. + +Full list of changes: + +- Make attribute access memory safe on free-threaded builds (Jukka Lehtosalo, PR [21705](https://github.com/python/mypy/pull/21705)) +- Fix unsafe borrowing of instance attributes with free-threading (Jukka Lehtosalo, PR [21688](https://github.com/python/mypy/pull/21688)) +- Make list get/set item more memory safe on free-threaded builds (Jukka Lehtosalo, PR [21683](https://github.com/python/mypy/pull/21683)) +- Don't borrow list items on free-threaded builds (Jukka Lehtosalo, PR [21679](https://github.com/python/mypy/pull/21679)) +- Make multiple assignment from list memory-safe on free-threaded builds (Jukka Lehtosalo, PR [21684](https://github.com/python/mypy/pull/21684)) +- Make `for` loop over list memory-safe on free-threaded builds (Jukka Lehtosalo, PR [21686](https://github.com/python/mypy/pull/21686)) +- Fix memory safety of `list.count` on free-threaded builds (Jukka Lehtosalo, PR [21680](https://github.com/python/mypy/pull/21680)) +- Make `vec` creation from list memory safe on free-threaded builds (Jukka Lehtosalo, PR [21681](https://github.com/python/mypy/pull/21681)) + +### librt.threading: Fast Native Lock Type + +Mypyc now supports `librt.threading.Lock`, which is a lock type optimized for use +in compiled code. It can be 2x to 4x faster than `threading.Lock`. + +This feature was contributed by Jukka Lehtosalo (PR [21690](https://github.com/python/mypy/pull/21690), PR [21697](https://github.com/python/mypy/pull/21697)). + +### Mypyc: Read-only Final Instance Attributes + +Instance attributes of native classes declared as `Final` are now read-only at runtime. +This enables additional optimizations, and it's now recommended to use `Final` for +all performance-sensitive attributes when feasible. + +Related changes: + +- Make instance attribute read-only at runtime if `Final` (Jukka Lehtosalo, PR [21666](https://github.com/python/mypy/pull/21666)) +- Borrow final attributes more aggressively (Jukka Lehtosalo, PR [21702](https://github.com/python/mypy/pull/21702)) +- Improve documentation of `Final` in mypyc (Jukka Lehtosalo, PR [21713](https://github.com/python/mypy/pull/21713)) + +### Mypyc Documentation Updates + +- Update documentation of race conditions under free threading (Jukka Lehtosalo, PR [21726](https://github.com/python/mypy/pull/21726)) +- Update mypyc free threading Python compatibility docs (Jukka Lehtosalo, PR [21711](https://github.com/python/mypy/pull/21711)) +- Document recent additions to `librt.strings`, such as `ispace` (Jukka Lehtosalo, PR [21696](https://github.com/python/mypy/pull/21696)) + +### Miscellaneous Mypyc Improvements + +- Fix reference leak when setting unboxed refcounted attributes (Tom Bannink, PR [21657](https://github.com/python/mypy/pull/21657)) +- Fix function wrapper memory leak (Piotr Sawicki, PR [21654](https://github.com/python/mypy/pull/21654)) +- Fix handling of invalid codepoint values in `librt.strings` (Jukka Lehtosalo, PR [21634](https://github.com/python/mypy/pull/21634)) +- Fix non-deterministic ordering of spilled registers (Jukka Lehtosalo, PR [21632](https://github.com/python/mypy/pull/21632)) +- Fix non-deterministic compiler output due to frozensets (Jukka Lehtosalo, PR [21631](https://github.com/python/mypy/pull/21631)) + +### Changes to Messages + +- Fix error code of note about unbound type variable (Jukka Lehtosalo, PR [21668](https://github.com/python/mypy/pull/21668)) + +### Other Notable Fixes and Improvements + +- Use `PYODIDE` environment variable for Emscripten cross-compilation detection (Agriya Khetarpal, PR [21714](https://github.com/python/mypy/pull/21714)) +- Narrow for frozendict membership check (Shantanu, PR [21709](https://github.com/python/mypy/pull/21709)) +- Fix custom equality handling for membership narrowing in static containers (Shantanu, PR [21706](https://github.com/python/mypy/pull/21706)) +- Infer `Coroutine` for unannotated async functions (Jingchen Ye, PR [21651](https://github.com/python/mypy/pull/21651)) +- Fix variance inference issues caused by dataclass replace (Shantanu, PR [21694](https://github.com/python/mypy/pull/21694)) +- Fix regression in dataclass narrowing for Python >= 3.13 (ygale, PR [21675](https://github.com/python/mypy/pull/21675)) +- Fix star import dependencies in mypy daemon (Jukka Lehtosalo, PR [21673](https://github.com/python/mypy/pull/21673)) +- Fix skipped imports considered stale (Piotr Sawicki, PR [21639](https://github.com/python/mypy/pull/21639)) +- Support `.ff` files with `--cache-map` (Jukka Lehtosalo, PR [21633](https://github.com/python/mypy/pull/21633)) + +### Typeshed Updates + +Please see [git log](https://github.com/python/typeshed/commits/main?after=f76037a1eb3923c67a8bc0e302ee9c016ffb3431+0&branch=main&path=stdlib) for full list of standard library typeshed stub changes. + +### Acknowledgements + +Thanks to all mypy contributors who contributed to this release: + +- Agriya Khetarpal +- Ethan Sarp +- Ivan Levkivskyi +- Jingchen Ye +- Jukka Lehtosalo +- Piotr Sawicki +- Shantanu +- Tom Bannink +- Viktor Szépe +- ygale + +I'd also like to thank my employer, Dropbox, for supporting mypy development. + ## Mypy 2.2 We've just uploaded mypy 2.2.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). From 8aabf8435357eaffceca7237f371e293b8168e54 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jul 2026 10:50:09 +0100 Subject: [PATCH 127/127] Drop +dev from version --- mypy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/version.py b/mypy/version.py index 13a4418314c25..dd5800e0daced 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -8,7 +8,7 @@ # - Release versions have the form "1.2.3". # - Dev versions have the form "1.2.3+dev" (PLUS sign to conform to PEP 440). # - Before 1.0 we had the form "0.NNN". -__version__ = "2.3.0+dev" +__version__ = "2.3.0" base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))