ITADN

BUG: Optional Improvement for Strict Type Checker Compatibility

#2123Closedrushiagr 创建于 2025-08-30
R
rushiagrcommented
### This is a pyzmq bug - [x] This is a pyzmq-specific bug, not an issue of zmq socket behavior. Don't worry if you're not sure! We'll figure it out together. ### What pyzmq version? 26.2.0 ### What libzmq version? '4.3.5' ### Python version (and how it was installed) 3.10.18 ### OS macOS Darwin 24.5.0 ### What happened? [Disclosure: complete analysis done by me, but final report generated via AI. I've read it thoroughly and it reflects my thoughts exactly as I want] PyZMQ has **inconsistent partial stub coverage** that causes strict type checkers like Pyrefly to fail on commonly used symbols like `Context`, `PUSH`, `PULL`, and `NOBLOCK`. While runtime code works perfectly and popular type checkers (MyPy/Pyright) handle this gracefully, the mixed stub/source approach breaks compatibility with strict PEP 484 implementations. **Priority: Low** - This is an optional improvement for future ecosystem compatibility, not a blocking issue. ### Code to reproduce bug ```python ``` ### Traceback, if applicable ```shell ``` ### More info ## Environment - **PyZMQ Version**: 26.2.0 - **Python Version**: 3.10.18 - **Platform**: macOS Darwin 24.5.0 - **Type Checkers Tested**: - Pyrefly 0.18.1 ❌ (reports missing-attribute errors) - MyPy 1.13.0 ✅ (works fine) - Pyright 1.1.390 ✅ (works fine) ## Problem Description ### Runtime vs. Type Checking Discrepancy **Runtime code works perfectly**: ```python import zmq context = zmq.Context() # ✅ Works at runtime socket = context.socket(zmq.PUSH) # ✅ Works at runtime socket.send(data, zmq.NOBLOCK) # ✅ Works at runtime ``` **Type checking fails with Pyrefly**: ``` ERROR: No attribute `Context` in module `zmq` [missing-attribute] ERROR: No attribute `PUSH` in module `zmq` [missing-attribute] ERROR: No attribute `NOBLOCK` in module `zmq` [missing-attribute] ``` ### Root Cause Analysis: Mixed Stub/Source Resolution Chain PyZMQ uses an **inconsistent stub coverage approach** that breaks strict type checker resolution: #### Package Structure Shows the Problem ❌ ``` zmq/ ├── __init__.pyi (stub file) ← "from .sugar import *" ├── sugar/ │ ├── __init__.pyi (stub file) ← "from .context import *" │ └── context.py (source!) ← Context class defined here, NO .pyi ├── constants.py (source!) ← PUSH/PULL/NOBLOCK defined, NO .pyi └── backend/ └── __init__.pyi (stub file) ← Context also defined here ``` #### The Broken Resolution Chain **Import path for `zmq.Context`**: 1. `import zmq` → reads `zmq/__init__.pyi` (stub) 2. `from .sugar import *` → reads `zmq/sugar/__init__.pyi` (stub) 3. `from .context import *` → looks for `zmq/sugar/context.pyi` (doesn't exist!) 4. **Strict type checkers stop here** - no fallback to source files 5. **Permissive type checkers** fall back to `zmq/sugar/context.py` (source) #### Type Checker Resolution Mechanism Differences **MyPy/Pyright (Module-Level Hybrid Resolution)** ✅: *Resolution strategy: "Stub file precedence applies PER MODULE, not per import chain"* ``` MyPy verbose logs show: LOG: Parsing zmq/__init__.pyi (zmq) # Starts with stub LOG: Parsing zmq/sugar/__init__.pyi (zmq.sugar) # Follows stub chain LOG: Parsing zmq/sugar/context.py (zmq.sugar.context) # FALLS BACK to source! ``` **Algorithm**: 1. Follow stub chain: `__init__.pyi` → `sugar/__init__.pyi` 2. Encounter `from .context import *` in stub file 3. Look for `context.pyi` - doesn't exist 4. **Fall back to `context.py`** - reads source file ✅ 5. Successfully resolve `Context` class from source 6. **Result**: `zmq.Context` is accessible **Pyrefly (Import-Chain-Level Strict Compliance)** ❌: *Resolution strategy: "Once in stub mode, stay in stub mode for entire chain"* **Algorithm**: 1. Follow stub chain: `__init__.pyi` → `sugar/__init__.pyi` 2. Encounter `from .context import *` in stub file 3. Look for `context.pyi` - doesn't exist 4. **Refuse to fall back** to source files (strict PEP 484) ❌ 5. Treat import as failed - no symbols resolved 6. **Result**: `zmq.Context` is not accessible #### Evidence of Inconsistent Coverage **Modules WITH stubs**: `backend/__init__.pyi`, `sugar/__init__.pyi`, `zmq/__init__.pyi` **Modules WITHOUT stubs**: `sugar/context.py`, `constants.py` This mixed approach violates PEP 484's expectation that stub files provide **complete interface definitions**. ## Expected vs. Actual Behavior ### Expected Behavior ✅ Type checkers should recognize all symbols that are available at runtime, including: - `zmq.Context` (from backend) - `zmq.Socket` (from backend) - `zmq.PUSH`, `zmq.PULL`, `zmq.NOBLOCK` (from constants) - All other symbols from `backend.__all__` ### Actual Behavior ❌ Strict type checkers following stub files cannot find symbols that are imported via `from zmq.backend import *` in the Python file but not explicitly re-exported in the stub file. ## Steps to Reproduce 1. **Install PyZMQ**: `pip install pyzmq==26.2.0` 2. **Install Pyrefly**: `pip install pyrefly` 3. **Create test file**: ```python # test_zmq.py import zmq context = zmq.Context() socket = context.socket(zmq.PUSH) socket.send(b"test", zmq.NOBLOCK) ``` 4. **Run type checking**: `pyrefly check test_zmq.py` 5. **Observe errors**: ``` ERROR: No attribute `Context` in module `zmq` [missing-attribute] ERROR: No attribute `PUSH` in module `zmq` [missing-attribute] ERROR: No attribute `NOBLOCK` in module `zmq` [missing-attribute] ``` 6. **Verify runtime works**: `python test_zmq.py` (runs without errors) ## Impact Assessment ### **Low Priority - Limited User Impact** **Who's affected**: - **Small subset** of users using strict type checkers like Pyrefly - **Most users unaffected** - MyPy (most popular) and Pyright work perfectly - **Zero runtime impact** - all code execution works normally **Practical impact**: - **Workaround is simple** - targeted ignore comments on 4-5 lines - **Not blocking** - doesn't prevent PyZMQ usage - **Future-proofing** - prepares for stricter typing ecosystem evolution ### Why Most Type Checkers Work Fine **The key insight**: This is a **type checker architecture difference**, not a PyZMQ bug: - **MyPy/Pyright** (99% of users): Use **hybrid resolution** with graceful fallback to source files ✅ - **Pyrefly** (<1% of users): Uses **strict PEP 484 compliance** without fallback mechanisms ❌ - **Both approaches are technically valid** according to typing standards ### Current Simple Workaround For the few affected users, add targeted ignores: ```python # pyrefly: ignore context = zmq.Context() # pyrefly: ignore socket = context.socket(zmq.PUSH) # pyrefly: ignore socket.send(data, zmq.NOBLOCK) ``` ### Ecosystem Evolution Context This issue represents the **evolution toward stricter type checking**. As more strict type checkers emerge, consistent stub coverage becomes increasingly valuable for ecosystem compatibility. ## Optional Improvements for Strict Type Checker Compatibility **Note**: These are **optional improvements** for future ecosystem compatibility. PyZMQ works perfectly for 99% of users. If PyZMQ maintainers want to support strict type checkers, consider choosing **one consistent approach**: ### Option 1: Complete Stub Coverage (Recommended) **Add missing stub files** to complete the type interface: ```bash # Files to create: zmq/sugar/context.pyi # Export Context class zmq/constants.pyi # Export PUSH, PULL, NOBLOCK, etc. ``` **Example `zmq/sugar/context.pyi`**: ```python from zmq.backend import Context as ContextBase class Context(ContextBase): def __init__(self, io_threads: int = 1) -> None: ... def socket(self, socket_type: int) -> Socket: ... # ... other methods ``` ### Option 2: Remove Partial Stub Coverage **Remove all stub files** and rely on source-based type checking: ```bash # Files to remove: rm zmq/__init__.pyi rm zmq/sugar/__init__.pyi rm zmq/backend/__init__.pyi # Keep py.typed file for PEP 561 compliance ``` This lets type checkers analyze source files directly without stub interference. ### Option 3: Hybrid Fix (Quick Fix) **Add missing symbols to existing stubs** via re-exports: ```python # In zmq/__init__.pyi, add: from .sugar.context import Context as Context # But this still requires creating the missing stub files ``` ## Testing Strategy After implementing the fix: 1. **Runtime compatibility**: Ensure `import zmq` still works identically 2. **Type checker validation**: Test with multiple type checkers: - Pyrefly: `pyrefly check <test_files>` - MyPy: `mypy <test_files>` - Pyright: `pyright <test_files>` 3. **Symbol availability**: Verify all commonly used symbols are accessible 4. **Regression testing**: Run existing PyZMQ test suite ## Technical Context ### Stub File Standards According to [PEP 484](https://peps.python.org/pep-0484/#stub-files), stub files should: - Provide the same public API as the corresponding Python module - Include all publicly accessible names - Maintain consistency with runtime behavior ### Type Checker Behavior Differences - **Strict type checkers** (Pyrefly) follow PEP 484 exactly: stub files define complete interfaces - **Hybrid type checkers** (MyPy/Pyright) have fallback mechanisms for incomplete stub coverage - Both approaches are valid, but packages must choose consistent stub coverage strategies - The trend is toward stricter enforcement as the typing ecosystem matures ## Files Requiring Updates ### For Complete Stub Coverage (Option 1): 1. **Create**: `zmq/sugar/context.pyi` - stub for Context class 2. **Create**: `zmq/constants.pyi` - stub for PUSH/PULL/NOBLOCK constants 3. **Verify**: Existing stubs are complete and consistent ### For Removing Partial Coverage (Option 2): 1. **Remove**: `zmq/__init__.pyi` 2. **Remove**: `zmq/sugar/__init__.pyi` 3. **Remove**: `zmq/backend/__init__.pyi` 4. **Keep**: `zmq/py.typed` for PEP 561 compliance ### Documentation: - Update type checking guidance in PyZMQ documentation - Add notes about type checker compatibility ## Additional Context: Ecosystem Future-Proofing This is a **proactive compatibility issue** as the Python typing ecosystem evolves. PyZMQ's current partial stub approach works perfectly for today's popular type checkers but may face challenges with future strict implementations. **Current state**: - **Perfectly fine** - MyPy/Pyright (99% of users) handle mixed stub/source gracefully - **Minor compatibility gap** - Strict type checkers require consistent stub coverage **Future consideration**: As the typing ecosystem matures, more tools may adopt strict PEP 484 compliance. Addressing this now provides **future-proofing** for the ecosystem evolution. ### Related Ecosystem Issues This pattern affects other packages transitioning to stub-based typing. The Python typing community is moving toward clearer contracts between packages and type checkers. ### Verification Commands ```bash # Verify the issue exists: pip install pyzmq==26.2.0 pyrefly echo "import zmq; zmq.Context()" > test.py pyrefly check test.py # Shows missing-attribute error python test.py # Runs successfully # Check stub coverage: find /site-packages/zmq -name "*.pyi" # Shows partial coverage find /site-packages/zmq -name "context*" # Shows .py but no .pyi ``` ## Reproduction Environment **Working test case for validation**: ```python #!/usr/bin/env python3 """Comprehensive PyZMQ type checking test.""" import zmq # Test Context creation context = zmq.Context() print(f"Context: {context}") # Test socket types push_socket = context.socket(zmq.PUSH) pull_socket = context.socket(zmq.PULL) print(f"PUSH socket: {push_socket}") print(f"PULL socket: {pull_socket}") # Test flags print(f"NOBLOCK flag: {zmq.NOBLOCK}") # Test other common symbols print(f"ZMQ version: {zmq.zmq_version()}") print("All symbols accessible!") ``` **Expected result**: Zero type checking errors while maintaining full runtime functionality.
关闭于 2025-09-01 1 条评论