mypyc silently drops class keyword arguments

Đang mở
#21,938 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
70/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python
Lĩnh vực
compilers

Hướng nghiên cứu

Chạy ví dụ repro.py được cung cấp bằng Python thông dịch và mypyc, sau đó đọc ClassBuilder.finalize trong mypyc/irbuild/classdef.py và py_init_subclass_op trong mypyc/primitives/misc_ops.py. So sánh các đường dẫn đó với đường dẫn builder.py_call không native và kiểm tra ClassDef.keywords trong mypy/nodes.py và mypy/fastparse.py. Được xem là hoàn tất khi các từ khóa của lớp được giữ nguyên trong cả hai trường hợp, hoặc các trường hợp không được hỗ trợ bị từ chối thay vì bị biên dịch âm thầm.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

bug topic-mypyc topic-runtime-semantics

Bug Report

Keyword arguments given in a class definition line are not forwarded when the
module is compiled. __init_subclass__ is called with no keywords, and a
metaclass is called with none either. Nothing is reported — neither by mypyc
during compilation nor by mypy when type checking.

This is not a case of an unsupported construct being rejected: __init_subclass__
is supported (there is a dedicated py_init_subclass_op in
mypyc/primitives/misc_ops.py), and the docs state that a class using an
unsupported metaclass "is compiled into a regular Python class (non-native
class)". The class is created and the hook does run — only the keywords are
missing, so the resulting behaviour still differs from interpreted Python.

To Reproduce

# repro.py
from typing import Any


class Base:
    def __init_subclass__(cls, **kwargs: Any) -> None:
        print("__init_subclass__ received:", kwargs)


class Child(Base, marker=1):
    pass
$ python -c "import repro"
__init_subclass__ received: {'marker': 1}

$ python -m mypyc repro.py
$ python -c "import repro"
__init_subclass__ received: {}

$ mypy --strict repro.py
Success: no issues found in 1 source file

Expected Behavior

__init_subclass__ (and a metaclass) receives {'marker': 1} in both cases,
as it does in interpreted Python. If forwarding class keywords is out of scope,
mypyc should reject the class rather than compile it and drop them silently.

Actual Behavior

The keywords are dropped. Compilation produces no diagnostic, and the resulting
extension module behaves differently from the same source under the interpreter.

Where this comes from

ClassDef.keywords is populated by the parser (mypy/nodes.py,
mypy/fastparse.py) and used by the checker, but mypyc never reads it —
searching the mypyc package for .keywords returns nothing. The two paths
that create the class both pass positional arguments only:

  • native classes — ClassBuilder.finalize in mypyc/irbuild/classdef.py calls
    py_init_subclass_op with [self.type_obj], and the primitive itself is
    declared with arg_types=[object_rprimitive], so there is no place for
    keywords to go;
  • non-native classes — the metaclass is invoked as
    builder.py_call(non_ext.metaclass, [cls_name, non_ext.bases, non_ext.dict]).

I could not find this limitation mentioned in the mypyc documentation. The
"Native classes" page documents which metaclasses are supported and what
happens to classes that use others, but says nothing about class keywords.

Real-world impact

Libraries that configure classes through class keywords lose that
configuration. With msgspec, whose Struct takes its options this way:

# tags.py
import msgspec


class Fact(msgspec.Struct, frozen=True, tag=True):
    call_id: str
    seq: int
$ python -c "import tags; c = tags.Fact.__struct_config__; print(c.tag, c.frozen)"
Fact True

$ python -m mypyc tags.py
$ python -c "import tags; c = tags.Fact.__struct_config__; print(c.tag, c.frozen)"
None False

$ python -c "
import tags
f = tags.Fact(call_id='c', seq=1)
f.call_id = 'mutated'
print(f)"
Fact(call_id='mutated', seq=1)

frozen=True becomes frozen=False, so instances meant to be immutable can be
mutated. tag=True is lost, so tagged unions stop decoding:

TypeError: If a type union contains multiple Struct types, all Struct types
must be tagged (via `tag` or `tag_field` kwarg)

The tagged-union failure is at least loud. The loss of frozen is not: the code
compiles, passes mypy --strict, and the immutability guarantee simply
disappears at runtime.

@mypy_extensions.mypyc_attr(native_class=False) does not help — the keywords
are dropped either way. Building the struct through a runtime call
(msgspec.defstruct(..., frozen=True, tag=True)) does preserve them, but gives
up static field types.

Your Environment

  • Mypy version used: 2.3.1, and 2.4.0+dev.543dbceea27fc9cca25d8adaaa0b36a77c3e1de8 (same behaviour on both)
  • Mypy command-line flags: python -m mypyc repro.py; type checking with mypy --strict repro.py
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.14.7, reproduced on both the default build and the free-threaded build (3.14.7t)
  • Operating system and version: Linux x86_64, GCC 16.2.1
  • msgspec version (for the second example): 0.21.1
Ngôn ngữ chính
Python
Star
20.6k
Fork
3.3k
Merge trung bình
1 ngày 9 giờ
Pull request đã merge (30 ngày)
56

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của python/mypy

Tất cả issue của python/mypy

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.