Repository: urllib3/urllib3

Issue:
Fix type checking when Zstandard is installed
### Subject

When I run type checking locally outside of a nox environment I get multiple errors because I have Zstandard installed.
`nox -s lint` succeeds because Zstandard is not listed in `mypy-requirements.txt`.

### Steps to Reproduce

1. Add `zstandard` to `mypy-requirements.txt`.
2. Run `nox -s lint`.

### Expected Behavior

Success regardless of Zstandard presence.
```sh
$ mypy -p dummyserver -m noxfile -p urllib3 -p test
Success: no issues found in 84 source files
```

### Actual Behavior

```sh
$ mypy -p dummyserver -m noxfile -p urllib3 -p test
src/urllib3/util/request.py:32: error: Unused "type: ignore" comment  [unused-ignore]
src/urllib3/response.py:29: error: Unused "type: ignore" comment  [unused-ignore]
src/urllib3/response.py:39: error: Incompatible types in assignment (expression has type "None", variable has type Module)  [assignment]
src/urllib3/response.py:42: error: Incompatible types in assignment (expression has type "None", variable has type Module)  [assignment]
src/urllib3/response.py:186: error: Unused "type: ignore" comment  [unused-ignore]
test/__init__.py:29: error: Unused "type: ignore" comment  [unused-ignore]
test/__init__.py:31: error: Incompatible types in assignment (expression has type "None", variable has type Module)  [assignment]
```



Discussion:

Resolving patch:
diff --git a/src/urllib3/response.py b/src/urllib3/response.py
--- a/src/urllib3/response.py
+++ b/src/urllib3/response.py
@@ -26,20 +26,21 @@
     brotli = None
 
 try:
-    import zstandard as zstd  # type: ignore[import-not-found]
-
+    import zstandard as zstd
+except (AttributeError, ImportError, ValueError):  # Defensive:
+    HAS_ZSTD = False
+else:
     # The package 'zstandard' added the 'eof' property starting
     # in v0.18.0 which we require to ensure a complete and
     # valid zstd stream was fed into the ZstdDecoder.
     # See: https://github.com/urllib3/urllib3/pull/2624
-    _zstd_version = _zstd_version = tuple(
+    _zstd_version = tuple(
         map(int, re.search(r"^([0-9]+)\.([0-9]+)", zstd.__version__).groups())  # type: ignore[union-attr]
     )
     if _zstd_version < (0, 18):  # Defensive:
-        zstd = None
-
-except (AttributeError, ImportError, ValueError):  # Defensive:
-    zstd = None
+        HAS_ZSTD = False
+    else:
+        HAS_ZSTD = True
 
 from . import util
 from ._base_connection import _TYPE_BODY
@@ -163,7 +164,7 @@ def flush(self) -> bytes:
             return b""
 
 
-if zstd is not None:
+if HAS_ZSTD:
 
     class ZstdDecoder(ContentDecoder):
         def __init__(self) -> None:
@@ -183,7 +184,7 @@ def flush(self) -> bytes:
             ret = self._obj.flush()  # note: this is a no-op
             if not self._obj.eof:
                 raise DecodeError("Zstandard data is incomplete")
-            return ret  # type: ignore[no-any-return]
+            return ret
 
 
 class MultiDecoder(ContentDecoder):
@@ -219,7 +220,7 @@ def _get_decoder(mode: str) -> ContentDecoder:
     if brotli is not None and mode == "br":
         return BrotliDecoder()
 
-    if zstd is not None and mode == "zstd":
+    if HAS_ZSTD and mode == "zstd":
         return ZstdDecoder()
 
     return DeflateDecoder()
@@ -302,7 +303,7 @@ class BaseHTTPResponse(io.IOBase):
     CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"]
     if brotli is not None:
         CONTENT_DECODERS += ["br"]
-    if zstd is not None:
+    if HAS_ZSTD:
         CONTENT_DECODERS += ["zstd"]
     REDIRECT_STATUSES = [301, 302, 303, 307, 308]
 
@@ -310,7 +311,7 @@ class BaseHTTPResponse(io.IOBase):
     if brotli is not None:
         DECODER_ERROR_CLASSES += (brotli.error,)
 
-    if zstd is not None:
+    if HAS_ZSTD:
         DECODER_ERROR_CLASSES += (zstd.ZstdError,)
 
     def __init__(
diff --git a/src/urllib3/util/request.py b/src/urllib3/util/request.py
--- a/src/urllib3/util/request.py
+++ b/src/urllib3/util/request.py
@@ -29,7 +29,7 @@
 else:
     ACCEPT_ENCODING += ",br"
 try:
-    import zstandard as _unused_module_zstd  # type: ignore[import-not-found] # noqa: F401
+    import zstandard as _unused_module_zstd  # noqa: F401
 except ImportError:
     pass
 else:


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -26,9 +26,11 @@
     brotli = None
 
 try:
-    import zstandard as zstd  # type: ignore[import-not-found]
+    import zstandard as _unused_module_zstd  # noqa: F401
 except ImportError:
-    zstd = None
+    HAS_ZSTD = False
+else:
+    HAS_ZSTD = True
 
 from urllib3 import util
 from urllib3.connectionpool import ConnectionPool
@@ -144,13 +146,13 @@ def notBrotli() -> typing.Callable[[_TestFuncT], _TestFuncT]:
 
 def onlyZstd() -> typing.Callable[[_TestFuncT], _TestFuncT]:
     return pytest.mark.skipif(
-        zstd is None, reason="only run if a python-zstandard library is installed"
+        not HAS_ZSTD, reason="only run if a python-zstandard library is installed"
     )
 
 
 def notZstd() -> typing.Callable[[_TestFuncT], _TestFuncT]:
     return pytest.mark.skipif(
-        zstd is not None,
+        HAS_ZSTD,
         reason="only run if a python-zstandard library is not installed",
     )
 
diff --git a/test/test_response.py b/test/test_response.py
--- a/test/test_response.py
+++ b/test/test_response.py
@@ -30,7 +30,6 @@
     BytesQueueBuffer,
     HTTPResponse,
     brotli,
-    zstd,
 )
 from urllib3.util.response import is_fp_closed
 from urllib3.util.retry import RequestHistory, Retry
@@ -389,6 +388,8 @@ def test_decode_brotli_error(self) -> None:
 
     @onlyZstd()
     def test_decode_zstd(self) -> None:
+        import zstandard as zstd
+
         data = zstd.compress(b"foo")
 
         fp = BytesIO(data)
@@ -397,6 +398,8 @@ def test_decode_zstd(self) -> None:
 
     @onlyZstd()
     def test_decode_multiframe_zstd(self) -> None:
+        import zstandard as zstd
+
         data = (
             # Zstandard frame
             zstd.compress(b"foo")
@@ -416,6 +419,8 @@ def test_decode_multiframe_zstd(self) -> None:
 
     @onlyZstd()
     def test_chunked_decoding_zstd(self) -> None:
+        import zstandard as zstd
+
         data = zstd.compress(b"foobarbaz")
 
         fp = BytesIO(data)
@@ -447,6 +452,8 @@ def test_decode_zstd_error(self, data: bytes) -> None:
     @onlyZstd()
     @pytest.mark.parametrize("data", decode_param_set)
     def test_decode_zstd_incomplete_preload_content(self, data: bytes) -> None:
+        import zstandard as zstd
+
         data = zstd.compress(data)
         fp = BytesIO(data[:-1])
 
@@ -456,6 +463,8 @@ def test_decode_zstd_incomplete_preload_content(self, data: bytes) -> None:
     @onlyZstd()
     @pytest.mark.parametrize("data", decode_param_set)
     def test_decode_zstd_incomplete_read(self, data: bytes) -> None:
+        import zstandard as zstd
+
         data = zstd.compress(data)
         fp = BytesIO(data[:-1])  # shorten the data to trigger DecodeError
 
@@ -471,6 +480,8 @@ def test_decode_zstd_incomplete_read(self, data: bytes) -> None:
     @onlyZstd()
     @pytest.mark.parametrize("data", decode_param_set)
     def test_decode_zstd_incomplete_read1(self, data: bytes) -> None:
+        import zstandard as zstd
+
         data = zstd.compress(data)
         fp = BytesIO(data[:-1])
 
@@ -489,6 +500,8 @@ def test_decode_zstd_incomplete_read1(self, data: bytes) -> None:
     @onlyZstd()
     @pytest.mark.parametrize("data", decode_param_set)
     def test_decode_zstd_read1(self, data: bytes) -> None:
+        import zstandard as zstd
+
         encoded_data = zstd.compress(data)
         fp = BytesIO(encoded_data)