Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions crates/monty-typeshed/custom/collections/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import sys
from _collections_abc import dict_items, dict_keys, dict_values
from types import GenericAlias
from typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only

from _typeshed import SupportsItems, SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT
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 *

__all__ = [
'Counter',
'defaultdict',
'deque',
'namedtuple',
]

_S = TypeVar('_S')
_T = TypeVar('_T')
_T1 = TypeVar('_T1')
_T2 = TypeVar('_T2')
_KT = TypeVar('_KT')
_VT = TypeVar('_VT')
_KT_co = TypeVar('_KT_co', covariant=True)
_VT_co = TypeVar('_VT_co', covariant=True)

# namedtuple is special-cased in the type checker; the initializer is ignored.
def namedtuple(
typename: str,
field_names: str | Iterable[str],
*,
rename: bool = False,
module: str | None = None,
defaults: Iterable[Any] | None = None,
) -> type[tuple[Any, ...]]: ...


# NOTE: This is a Monty-narrowed copy of the upstream typeshed stub. Only the
# members implemented by Monty's runtime are exposed (deque, defaultdict,
# Counter, namedtuple); OrderedDict, ChainMap, UserDict, UserList and
# UserString are intentionally omitted so type-checking matches the runtime.
# The source of truth is custom/collections/__init__.pyi (see custom/README.md).

@disjoint_base
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: ...
def count(self, x: _T, /) -> int: ...
def extend(self, iterable: Iterable[_T], /) -> None: ...
def extendleft(self, iterable: Iterable[_T], /) -> None: ...
def insert(self, i: int, x: _T, /) -> None: ...
def index(self, x: _T, start: int = 0, stop: int = ..., /) -> int: ...
def pop(self) -> _T: ... # type: ignore[override]
def popleft(self) -> _T: ...
def remove(self, value: _T, /) -> None: ...
def rotate(self, n: int = 1, /) -> None: ...
def __copy__(self) -> Self: ...
def __len__(self) -> int: ...
__hash__: ClassVar[None] # type: ignore[assignment]
# These methods of deque don't take slices, unlike MutableSequence, hence the type: ignores
def __getitem__(self, key: SupportsIndex, /) -> _T: ... # type: ignore[override]
def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ... # type: ignore[override]
def __delitem__(self, key: SupportsIndex, /) -> None: ... # type: ignore[override]
def __contains__(self, key: object, /) -> bool: ...
def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The deque type stub includes __reduce__, but the runtime does not implement it (the limitations doc explicitly says "No __copy__ / pickling / __reduce__"). Type checkers will consider it valid, leading to false positives. Consider removing it from the stub.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/monty-typeshed/custom/collections/__init__.pyi, line 86:

<comment>The `deque` type stub includes `__reduce__`, but the runtime does not implement it (the limitations doc explicitly says "No `__copy__` / pickling / `__reduce__`"). Type checkers will consider it valid, leading to false positives. Consider removing it from the stub.</comment>

<file context>
@@ -0,0 +1,201 @@
+    def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ...  # type: ignore[override]
+    def __delitem__(self, key: SupportsIndex, /) -> None: ...  # type: ignore[override]
+    def __contains__(self, key: object, /) -> bool: ...
+    def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ...
+    def __iadd__(self, value: Iterable[_T], /) -> Self: ...
+    def __add__(self, value: Self, /) -> Self: ...
</file context>

def __iadd__(self, value: Iterable[_T], /) -> Self: ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The deque type stub advertises __add__, __mul__, __iadd__, and __imul__ methods, but the runtime raises TypeError for all four operations. The stub's own comment states it only exposes what the runtime implements, so these should be removed from the stub to keep type-checking in sync with runtime behavior (or added to the runtime if that's the intent).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/monty-typeshed/custom/collections/__init__.pyi, line 87:

<comment>The `deque` type stub advertises `__add__`, `__mul__`, `__iadd__`, and `__imul__` methods, but the runtime raises `TypeError` for all four operations. The stub's own comment states it only exposes what the runtime implements, so these should be removed from the stub to keep type-checking in sync with runtime behavior (or added to the runtime if that's the intent).</comment>

<file context>
@@ -0,0 +1,201 @@
+    def __delitem__(self, key: SupportsIndex, /) -> None: ...  # type: ignore[override]
+    def __contains__(self, key: object, /) -> bool: ...
+    def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ...
+    def __iadd__(self, value: Iterable[_T], /) -> Self: ...
+    def __add__(self, value: Self, /) -> Self: ...
+    def __mul__(self, value: int, /) -> Self: ...
</file context>

def __add__(self, value: Self, /) -> Self: ...
def __mul__(self, value: int, /) -> Self: ...
def __imul__(self, value: int, /) -> Self: ...
def __lt__(self, value: deque[_T], /) -> bool: ...
def __le__(self, value: deque[_T], /) -> bool: ...
def __gt__(self, value: deque[_T], /) -> bool: ...
def __ge__(self, value: deque[_T], /) -> bool: ...
def __eq__(self, value: object, /) -> bool: ...
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...

class Counter(dict[_T, int], Generic[_T]):
@overload
def __init__(self, iterable: None = None, /) -> None: ...
@overload
def __init__(self: Counter[str], iterable: None = None, /, **kwargs: int) -> None: ...
@overload
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)
#
# The second overload is also deliberately different to dict.update()
# (if it were `Iterable[_T] | Iterable[tuple[_T, int]]`,
# the tuples would be added as keys, breaking type safety)
@overload # type: ignore[override]
def update(self, m: Mapping[_T, int], /, **kwargs: int) -> None: ...
@overload
def update(self, iterable: Iterable[_T], /, **kwargs: int) -> None: ...
@overload
def update(self, iterable: None = None, /, **kwargs: int) -> None: ...
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 __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]
def __pos__(self) -> Counter[_T]: ...
def __neg__(self) -> Counter[_T]: ...
# several type: ignores because __iadd__ is supposedly incompatible with __add__, etc.
def __iadd__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[misc]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Counter type stub includes __iadd__, __isub__, __iand__, and __ior__ methods, but the runtime does not support in-place operators between Counters (as documented in limitations). Type checkers will consider these valid, but using them will produce a runtime error. Consider removing them from the stub.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/monty-typeshed/custom/collections/__init__.pyi, line 143:

<comment>The `Counter` type stub includes `__iadd__`, `__isub__`, `__iand__`, and `__ior__` methods, but the runtime does not support in-place operators between Counters (as documented in limitations). Type checkers will consider these valid, but using them will produce a runtime error. Consider removing them from the stub.</comment>

<file context>
@@ -0,0 +1,201 @@
+    def __pos__(self) -> Counter[_T]: ...
+    def __neg__(self) -> Counter[_T]: ...
+    # several type: ignores because __iadd__ is supposedly incompatible with __add__, etc.
+    def __iadd__(self, other: SupportsItems[_T, int]) -> Self: ...  # type: ignore[misc]
+    def __isub__(self, other: SupportsItems[_T, int]) -> Self: ...
+    def __iand__(self, other: SupportsItems[_T, int]) -> Self: ...
</file context>

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: ...

@disjoint_base
class defaultdict(dict[_KT, _VT]):
default_factory: Callable[[], _VT] | None
@overload
def __init__(self) -> None: ...
@overload
def __init__(self: defaultdict[str, _VT], **kwargs: _VT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
@overload
def __init__(self, default_factory: Callable[[], _VT] | None, /) -> None: ...
@overload
def __init__(
self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780
default_factory: Callable[[], _VT] | None,
/,
**kwargs: _VT,
) -> None: ...
@overload
def __init__(self, default_factory: Callable[[], _VT] | None, map: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ...
@overload
def __init__(
self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780
default_factory: Callable[[], _VT] | None,
map: SupportsKeysAndGetItem[str, _VT],
/,
**kwargs: _VT,
) -> None: ...
@overload
def __init__(self, default_factory: Callable[[], _VT] | None, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ...
@overload
def __init__(
self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780
default_factory: Callable[[], _VT] | None,
iterable: Iterable[tuple[str, _VT]],
/,
**kwargs: _VT,
) -> None: ...
def __missing__(self, key: _KT, /) -> _VT: ...
def __copy__(self) -> Self: ...
def copy(self) -> Self: ...
@overload
def __or__(self, value: dict[_KT, _VT], /) -> Self: ...
@overload
def __or__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ...
@overload
def __ror__(self, value: dict[_KT, _VT], /) -> Self: ...
@overload
def __ror__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc]

11 changes: 11 additions & 0 deletions crates/monty-typeshed/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,17 @@ def main() -> int:
for file in CUSTOM_DIR.glob('*.pyi'):
shutil.copy2(file, STDLIB_DIR)
custom_count += 1
# Package overrides: a `custom/<pkg>/` directory replaces the matching
# module file within the vendored `stdlib/<pkg>/` package (e.g.
# `custom/collections/__init__.pyi` narrows `collections` to Monty's
# implemented members while leaving `collections/abc.pyi` untouched).
for pkg_dir in CUSTOM_DIR.iterdir():
if not pkg_dir.is_dir():
continue
dest_pkg = STDLIB_DIR / pkg_dir.name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Package overrides can fail at runtime — shutil.copy2(file, dest_pkg / file.name) raises FileNotFoundError if dest_pkg doesn't already exist, because copy2 does not create parent directories. Add dest_pkg.mkdir(parents=True, exist_ok=True) before the inner copy loop so the override works for any package name, not just those pre-created by COPY_FILES.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/monty-typeshed/update.py, line 344:

<comment>Package overrides can fail at runtime — `shutil.copy2(file, dest_pkg / file.name)` raises `FileNotFoundError` if `dest_pkg` doesn't already exist, because `copy2` does not create parent directories. Add `dest_pkg.mkdir(parents=True, exist_ok=True)` before the inner copy loop so the override works for any package name, not just those pre-created by COPY_FILES.</comment>

<file context>
@@ -334,6 +334,17 @@ def main() -> int:
+    for pkg_dir in CUSTOM_DIR.iterdir():
+        if not pkg_dir.is_dir():
+            continue
+        dest_pkg = STDLIB_DIR / pkg_dir.name
+        for file in pkg_dir.glob('*.pyi'):
+            shutil.copy2(file, dest_pkg / file.name)
</file context>

for file in pkg_dir.glob('*.pyi'):
shutil.copy2(file, dest_pkg / file.name)
custom_count += 1
print(f'Copied {custom_count} custom typeshed files')

return 0
Expand Down
Loading
Loading