Unrud 5 лет назад
Родитель
Сommit
73e42f8101

+ 1 - 0
.gitignore

@@ -15,6 +15,7 @@ coverage.xml
 .coverage
 .coverage.*
 .eggs
+.mypy_cache
 .project
 .pydevproject
 .settings

+ 1 - 1
radicale/app/__init__.py

@@ -58,7 +58,7 @@ from radicale.log import logger
 
 # WORKAROUND: https://github.com/tiran/defusedxml/issues/54
 import defusedxml.ElementTree as DefusedET  # isort: skip
-sys.modules["xml.etree"].ElementTree = ET
+sys.modules["xml.etree"].ElementTree = ET  # type: ignore[attr-defined]
 
 VERSION = pkg_resources.get_distribution("radicale").version
 

+ 2 - 1
radicale/config.py

@@ -31,6 +31,7 @@ import os
 import string
 from collections import OrderedDict
 from configparser import RawConfigParser
+from typing import Any, ClassVar
 
 from radicale import auth, rights, storage, web
 
@@ -285,7 +286,7 @@ def load(paths=()):
 
 
 class Configuration:
-    SOURCE_MISSING = {}
+    SOURCE_MISSING: ClassVar[Any] = {}
 
     def __init__(self, schema):
         """Initialize configuration.

+ 4 - 1
radicale/pathutils.py

@@ -27,6 +27,7 @@ import posixpath
 import sys
 import threading
 from tempfile import TemporaryDirectory
+from typing import Type, Union
 
 if os.name == "nt":
     import ctypes
@@ -34,6 +35,7 @@ if os.name == "nt":
     import msvcrt
 
     LOCKFILE_EXCLUSIVE_LOCK = 2
+    ULONG_PTR: Union[Type[ctypes.c_uint32], Type[ctypes.c_uint64]]
     if ctypes.sizeof(ctypes.c_void_p) == 4:
         ULONG_PTR = ctypes.c_uint32
     else:
@@ -47,7 +49,8 @@ if os.name == "nt":
             ("offset_high", ctypes.wintypes.DWORD),
             ("h_event", ctypes.wintypes.HANDLE)]
 
-    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
+    kernel32 = ctypes.WinDLL(  # type: ignore[attr-defined]
+        "kernel32", use_last_error=True)
     lock_file_ex = kernel32.LockFileEx
     lock_file_ex.argtypes = [
         ctypes.wintypes.HANDLE,

+ 6 - 2
radicale/server.py

@@ -30,21 +30,25 @@ import socketserver
 import ssl
 import sys
 import wsgiref.simple_server
+from typing import MutableMapping
 from urllib.parse import unquote
 
 from radicale import Application, config
 from radicale.log import logger
 
+COMPAT_EAI_ADDRFAMILY: int
 if hasattr(socket, "EAI_ADDRFAMILY"):
-    COMPAT_EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY
+    COMPAT_EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY  # type: ignore[attr-defined]
 elif hasattr(socket, "EAI_NONAME"):
     # Windows and BSD don't have a special error code for this
     COMPAT_EAI_ADDRFAMILY = socket.EAI_NONAME
+COMPAT_EAI_NODATA: int
 if hasattr(socket, "EAI_NODATA"):
     COMPAT_EAI_NODATA = socket.EAI_NODATA
 elif hasattr(socket, "EAI_NONAME"):
     # Windows and BSD don't have a special error code for this
     COMPAT_EAI_NODATA = socket.EAI_NONAME
+COMPAT_IPPROTO_IPV6: int
 if hasattr(socket, "IPPROTO_IPV6"):
     COMPAT_IPPROTO_IPV6 = socket.IPPROTO_IPV6
 elif os.name == "nt":
@@ -155,7 +159,7 @@ class ParallelHTTPSServer(ParallelHTTPServer):
 class ServerHandler(wsgiref.simple_server.ServerHandler):
 
     # Don't pollute WSGI environ with OS environment
-    os_environ = {}
+    os_environ: MutableMapping[str, str] = {}
 
     def log_exception(self, exc_info):
         logger.error("An exception occurred during request: %s",

+ 3 - 1
radicale/tests/test_base.py

@@ -25,6 +25,7 @@ import posixpath
 import shutil
 import sys
 import tempfile
+from typing import Any, ClassVar
 
 import defusedxml.ElementTree as DefusedET
 import pytest
@@ -1549,7 +1550,8 @@ class BaseRequestsMixIn:
 
 class BaseFileSystemTest(BaseTest):
     """Base class for filesystem backend tests."""
-    storage_type = None
+
+    storage_type: ClassVar[Any]
 
     def setup(self):
         self.configuration = config.load()

+ 8 - 1
radicale/tests/test_server.py

@@ -41,10 +41,17 @@ from radicale.tests.helpers import configuration_to_dict, get_file_path
 
 
 class DisabledRedirectHandler(request.HTTPRedirectHandler):
+    def http_error_301(self, req, fp, code, msg, headers):
+        raise HTTPError(req.full_url, code, msg, headers, fp)
+
     def http_error_302(self, req, fp, code, msg, headers):
         raise HTTPError(req.full_url, code, msg, headers, fp)
 
-    http_error_301 = http_error_303 = http_error_307 = http_error_302
+    def http_error_303(self, req, fp, code, msg, headers):
+        raise HTTPError(req.full_url, code, msg, headers, fp)
+
+    def http_error_307(self, req, fp, code, msg, headers):
+        raise HTTPError(req.full_url, code, msg, headers, fp)
 
 
 class TestBaseServerRequests(BaseTest):

+ 5 - 0
setup.cfg

@@ -5,6 +5,7 @@ test = pytest
 python-tag = py3
 
 [tool:pytest]
+# More options are set in `setup.py` via environment variable `PYTEST_ADDOPTS`
 addopts = --flake8 --isort --cov --cov-report=term --cov-report=xml -r s
 norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv
 
@@ -15,6 +16,10 @@ known_third_party = defusedxml,passlib,pkg_resources,pytest,vobject
 [flake8]
 extend-ignore = H
 
+[mypy]
+ignore_missing_imports = True
+show_error_codes = True
+
 [coverage:run]
 branch = True
 source = radicale

+ 6 - 0
setup.py

@@ -36,6 +36,7 @@ For further information, please visit the `Radicale Website
 
 """
 
+import os
 import sys
 
 from setuptools import find_packages, setup
@@ -52,6 +53,11 @@ needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv)
 pytest_runner = ["pytest-runner"] if needs_pytest else []
 tests_require = ["pytest-runner", "pytest", "pytest-cov", "pytest-flake8",
                  "pytest-isort", "waitress"]
+os.environ["PYTEST_ADDOPTS"] = os.environ.get("PYTEST_ADDOPTS", "")
+# Mypy only supports CPython
+if sys.implementation.name == "cpython":
+    tests_require.extend(["pytest-mypy", "types-setuptools"])
+    os.environ["PYTEST_ADDOPTS"] += " --mypy"
 
 setup(
     name="Radicale",