From 0f6a9d7e333a45f253abf7dbf156c6911ceef30d Mon Sep 17 00:00:00 2001 From: Chip Kent <5250374+chipkent@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:15:49 -0600 Subject: [PATCH 1/2] feat: let type checkers see model constructors @modelclass builds an __init__ that accepts every field positionally or by keyword, but type checkers could not see it: a call such as CryptoQuote(pair="BTC-USD", bid_price=1.5) was reported as "Unexpected keyword argument" for every field. Mark modelclass with dataclass_transform (PEP 681) so checkers derive the constructor from the field annotations. The decorator is imported only under TYPE_CHECKING, from typing on 3.11+ and from typing_extensions on 3.10, and is a no-op at runtime, so there is no new runtime dependency and Python 3.10 keeps working. --- massive/modelclass.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/massive/modelclass.py b/massive/modelclass.py index 117f7910..0605180a 100644 --- a/massive/modelclass.py +++ b/massive/modelclass.py @@ -1,10 +1,23 @@ import inspect +import sys import typing from dataclasses import dataclass +if typing.TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import dataclass_transform + else: + from typing_extensions import dataclass_transform +else: + + def dataclass_transform(**kwargs): + return lambda f: f + + _T = typing.TypeVar("_T") +@dataclass_transform() def modelclass(cls: typing.Type[_T]) -> typing.Type[_T]: cls = dataclass(cls) type_hints = typing.get_type_hints(cls) From 8da79e199baa5611fef7907c29105f5bf5c3ae2b Mon Sep 17 00:00:00 2001 From: Chip Kent <5250374+chipkent@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:02:31 -0600 Subject: [PATCH 2/2] feat: ship py.typed so the package is checked as typed Without the PEP 561 marker, mypy skips the installed package and reports import-untyped, and users who silence that get Any for every massive type. - Add massive/py.typed. The existing packages entry in pyproject.toml already includes it in the wheel and sdist. - Re-export RESTClient and WebSocketClient explicitly ("X as X"), so "from massive import RESTClient" passes mypy --strict, which does not treat plain imports in a typed package as re-exports. No __all__ is added, so "from massive import *" exports the same names as before. - Add a test that "from massive import *" still provides the main names. --- massive/__init__.py | 4 ++-- massive/py.typed | 0 test_rest/test_exports.py | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 massive/py.typed create mode 100644 test_rest/test_exports.py diff --git a/massive/__init__.py b/massive/__init__.py index da6c5136..04f41281 100644 --- a/massive/__init__.py +++ b/massive/__init__.py @@ -1,6 +1,6 @@ -from .rest import RESTClient +from .rest import RESTClient as RESTClient from .rest.base import version -from .websocket import WebSocketClient +from .websocket import WebSocketClient as WebSocketClient from .exceptions import * __version__ = version diff --git a/massive/py.typed b/massive/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/test_rest/test_exports.py b/test_rest/test_exports.py new file mode 100644 index 00000000..3c3c40bc --- /dev/null +++ b/test_rest/test_exports.py @@ -0,0 +1,9 @@ +import unittest + + +class ExportsTest(unittest.TestCase): + def test_star_import(self): + ns = {} + exec("from massive import *", ns) + for name in ("RESTClient", "WebSocketClient", "AuthError", "BadResponse"): + self.assertIn(name, ns)