-
Notifications
You must be signed in to change notification settings - Fork 0
Type Checking
One of the fork's biggest wins over upstream is first-class static type checking under Pylance / pyright. Every generated protobuf module ships with a full .pyi companion — attribute accesses on message fields resolve to their real types, and IDEs can autocomplete and refactor them.
-
Full
.pyistubs for every_pb2file viamypy-protobuf. Emitted alongside the runtime.pybyprotoc --mypy_out. EveryCMsgX.fieldreads as its real type, every method resolves throughMessage's base signatures. -
Modernised codegen output — protobuf 5.x/6.x
_descriptor_pool.AddSerializedFile+_builder, ~20× smaller than the old_reflection.GeneratedProtocolMessageTypepattern, and static-analyser-friendly (no dynamic type mutation at import time). -
py.typedmarker atsteam/py.typed— signals to downstream projects that this package ships inline types (PEP 561). - Local stub overrides for typeshed bugs the fork hit — see below.
-
[tool.pyright]config inpyproject.toml— turns onstubPath = "typings"so the local overrides are picked up automatically.
The runtime protobuf Message base class (ParseFromString, SerializeToString, MergeFrom, HasField, WhichOneof, …) is typed via types-protobuf, a typeshed-first-party package. Pulled in as a dev dep:
[tool.poetry.group.dev.dependencies]
types-protobuf = ">=4.24"Dev-only because it ships only .pyi files — zero runtime impact. Bumped past the 4.24 line so the emitted .pyi stubs from mypy-protobuf line up with what Pylance's typeshed already expects.
Generates the domain .pyi files (one per .proto). Every message class becomes a real Message subclass with each field's type declared:
# steam/protobufs/steammessages_clientserver_pb2.pyi (auto-generated excerpt)
class CMsgClientLogonResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class OutOfGameEmoticonSlots: ...
EMSG_FIELD_NUMBER: builtins.int
ERESULT_FIELD_NUMBER: builtins.int
@property
def eresult(self) -> builtins.int: ...
@property
def out_of_game_heartbeat_seconds(self) -> builtins.int: ...
# ...Downstream:
from steam.core.msg import MsgProto
from steam.enums.emsg import EMsg
msg = MsgProto(EMsg.ClientLogOnResponse)
result = msg.body.eresult # Pylance knows this is intDev dep:
mypy-protobuf = ">=3.6"[tool.pyright] stubPath = "typings" in pyproject.toml points pyright at local stub overrides. Currently one file lives there.
Fixes a bug in the upstream typeshed stub for google.protobuf.internal.builder.BuildServices. Upstream declares:
def BuildServices(file_des: FileDescriptor, module_name: str, module: ModuleType) -> None: ...But the runtime implementation does module[name] = ... — item assignment, which needs a MutableMapping, not a real ModuleType. The other three Build* functions in the same file ship the correct dict[str, Any] annotation; this override just aligns BuildServices with them.
Without the override, every steam/protobufs/*_pb2.py with a service declaration reports reportArgumentType on the _globals argument, even though the runtime code is correct. Once typeshed lands the fix upstream, this override can be deleted.
scripts/pb_compile.py post-processes the raw protoc output. Two rules matter for type checking:
Sibling protobuf imports get the steam.protobufs. prefix so runtime import works from any working directory:
# before
import steammessages_base_pb2
# after
import steam.protobufs.steammessages_base_pb2Doesn't affect type checking directly, but fixes runtime ModuleNotFoundError under normal imports.
Per-message DESCRIPTOR: _descriptor.Descriptor overrides inside message classes are stripped. Under types-protobuf 7.34+, the base Message.DESCRIPTOR is typed as Descriptor | _upb_Descriptor; mypy-protobuf emits the narrower single-type override, which trips reportIncompatibleVariableOverride (mutable-attribute variance forbids narrowing).
The fix: delete the override, let each Message subclass inherit the base's correctly-typed union. Module-level FileDescriptor and enum-wrapper EnumDescriptor overrides are left alone (they don't collide).
Regex is ^\s+DESCRIPTOR: _descriptor\.Descriptor\n — matches any leading whitespace, so deeply-nested (16-space) message classes are covered too.
Both the .py and .pyi are emitted together by pb-compile:
poetry run pb-compileThat runs protoc --python_out=... --mypy_out=... .... Full details on Regenerating Protobufs.
Some parts of the codebase — mostly the mixin classes in steam/client/builtins/* — need to reach for attributes their host class provides at runtime (via multiple inheritance in BuiltinBase). They use TYPE_CHECKING-only _HostBase protocols to declare those shapes without dragging concrete SteamClient / CMClient into a runtime import (which would circular-import at load time). Look at any steam/client/builtins/*.py — the class _XxxMixinHost: block explains the pattern.
The dynamic WebAPI interface catalogue is another intentional gap. Interfaces are populated at construction time from GetSupportedAPIList, so their names aren't knowable at design time. WebAPI declares __getattr__(...) -> Any under TYPE_CHECKING so api.ISteamUser.ResolveVanityURL(...) doesn't red-underline in your IDE — trading precise typing for existence of the attribute.
VS Code + Pylance / pyright picks up pyproject.toml automatically. If you want stricter checking, add to your workspace settings:
{
"python.analysis.typeCheckingMode": "basic",
"python.analysis.stubPath": "typings"
}basic is what CI runs; strict catches more but flags the mixin _HostBase protocols as pointlessly indirect.
- Regenerating stubs after upstream changes: Regenerating Protobufs.
- Sibling package
pyproject.tomlsettings and layout: check the fork's actual[tool.pyright]block for the source of truth.
H47R15/steam — maintained fork of ValvePython/steam. MIT licensed.