Skip to content

Commit dc81ac5

Browse files
mikethemancbornet
andauthored
chore: add type hints (#465)
Co-authored-by: cbornet <cbornet@hotmail.com>
1 parent 0b3639c commit dc81ac5

5 files changed

Lines changed: 217 additions & 29 deletions

File tree

.github/workflows/python.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ jobs:
4141
run: uv python install '${{ matrix.python-version }}' # zizmor: ignore[template-injection]
4242
- name: Prepare project for development
4343
run: uv sync
44+
- name: Check types
45+
run: uv run mypy src
4446
- name: Test with pytest
4547
run: |
4648
uv run coverage run -m pytest

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ classifiers = [
2020
"Programming Language :: Python :: Implementation :: CPython",
2121
"Operating System :: OS Independent",
2222
"License :: OSI Approved :: MIT License",
23+
"Typing :: Typed",
2324
]
2425
requires-python = ">= 3.10"
2526
dependencies = ["pytest>=7.0.0"]
@@ -32,6 +33,7 @@ dev = [
3233
"requests >= 2.32.4",
3334
"starlette >= 0.47.1",
3435
"httpx >= 0.28.1",
36+
"mypy >= 1.20",
3537
]
3638

3739
[project.urls]
@@ -55,6 +57,9 @@ known_first_party = ['pytest_socket', 'conftest']
5557
# https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#profilegcm
5658
profile = "black"
5759

60+
[tool.mypy]
61+
strict = true
62+
5863
[tool.vulture]
5964
ignore_decorators = ["@pytest.fixture"]
6065
ignore_names = ["pytest_*"]

src/pytest_socket/__init__.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from __future__ import annotations
2+
13
import ipaddress
24
import itertools
35
import socket
46
from collections import defaultdict
7+
from collections.abc import Iterator
58
from dataclasses import dataclass, field
9+
from typing import Any
610

711
import pytest
812

@@ -11,21 +15,26 @@
1115

1216

1317
class SocketBlockedError(RuntimeError):
14-
def __init__(self, *_args, **_kwargs):
18+
def __init__(self, *_args: Any, **_kwargs: Any) -> None:
1519
super().__init__("A test tried to use socket.socket.")
1620

1721

1822
class SocketConnectBlockedError(RuntimeError):
19-
def __init__(self, allowed, host, *_args, **_kwargs):
20-
if allowed:
21-
allowed = ",".join(allowed)
23+
def __init__(
24+
self,
25+
allowed: list[str],
26+
host: str | None,
27+
*_args: Any,
28+
**_kwargs: Any,
29+
) -> None:
30+
allowed_str = ",".join(allowed)
2231
super().__init__(
2332
"A test tried to use socket.socket.connect() "
24-
f'with host "{host}" (allowed: "{allowed}").'
33+
f'with host "{host}" (allowed: "{allowed_str}").'
2534
)
2635

2736

28-
def pytest_addoption(parser):
37+
def pytest_addoption(parser: pytest.Parser) -> None:
2938
group = parser.getgroup("socket")
3039
group.addoption(
3140
"--disable-socket",
@@ -54,15 +63,15 @@ def pytest_addoption(parser):
5463

5564

5665
@pytest.fixture
57-
def socket_disabled(pytestconfig):
66+
def socket_disabled(pytestconfig: pytest.Config) -> Iterator[None]:
5867
"""disable socket.socket for duration of this test function"""
5968
socket_config = pytestconfig.stash[_STASH_KEY]
6069
disable_socket(allow_unix_socket=socket_config.allow_unix_socket)
6170
yield
6271

6372

6473
@pytest.fixture
65-
def socket_enabled(pytestconfig):
74+
def socket_enabled(pytestconfig: pytest.Config) -> Iterator[None]:
6675
"""enable socket.socket for duration of this test function"""
6776
enable_socket()
6877
yield
@@ -80,31 +89,37 @@ class _PytestSocketConfig:
8089
_STASH_KEY = pytest.StashKey[_PytestSocketConfig]()
8190

8291

83-
def _is_unix_socket(family) -> bool:
92+
def _is_unix_socket(family: int) -> bool:
8493
return hasattr(socket, "AF_UNIX") and family == socket.AF_UNIX
8594

8695

87-
def disable_socket(allow_unix_socket=False):
96+
def disable_socket(allow_unix_socket: bool = False) -> None:
8897
"""disable socket.socket to disable the Internet. useful in testing."""
8998

9099
class GuardedSocket(socket.socket):
91100
"""socket guard to disable socket creation (from pytest-socket)"""
92101

93-
def __new__(cls, family=-1, type=-1, proto=-1, fileno=None):
102+
def __new__(
103+
cls,
104+
family: socket.AddressFamily | int = -1,
105+
type: socket.SocketKind | int = -1,
106+
proto: int = -1,
107+
fileno: int | None = None,
108+
) -> GuardedSocket:
94109
if _is_unix_socket(family) and allow_unix_socket:
95-
return super().__new__(cls, family, type, proto, fileno)
110+
return super().__new__(cls, family, type, proto, fileno) # type: ignore[call-arg] # noqa E501
96111

97112
raise SocketBlockedError()
98113

99-
socket.socket = GuardedSocket
114+
socket.socket = GuardedSocket # type: ignore[misc]
100115

101116

102-
def enable_socket():
117+
def enable_socket() -> None:
103118
"""re-enable socket.socket to enable the Internet. useful in testing."""
104-
socket.socket = _true_socket
119+
socket.socket = _true_socket # type: ignore[misc]
105120

106121

107-
def pytest_configure(config):
122+
def pytest_configure(config: pytest.Config) -> None:
108123
config.addinivalue_line(
109124
"markers", "disable_socket(): Disable socket connections for a specific test"
110125
)
@@ -125,7 +140,7 @@ def pytest_configure(config):
125140
)
126141

127142

128-
def pytest_runtest_setup(item) -> None:
143+
def pytest_runtest_setup(item: pytest.Item) -> None:
129144
"""During each test item's setup phase,
130145
choose the behavior based on the configurations supplied.
131146
@@ -166,7 +181,7 @@ def pytest_runtest_setup(item) -> None:
166181
disable_socket(socket_config.allow_unix_socket)
167182

168183

169-
def _resolve_allow_hosts(item):
184+
def _resolve_allow_hosts(item: pytest.Item) -> str | list[str] | None:
170185
"""Resolve `allow_hosts` behaviors."""
171186
socket_config = item.config.stash[_STASH_KEY]
172187

@@ -186,21 +201,23 @@ def _resolve_allow_hosts(item):
186201
return hosts
187202

188203

189-
def pytest_runtest_teardown():
204+
def pytest_runtest_teardown() -> None:
190205
_remove_restrictions()
191206

192207

193-
def host_from_address(address):
208+
def host_from_address(address: tuple[Any, ...]) -> str | None:
194209
host = address[0]
195210
if isinstance(host, str):
196211
return host
212+
return None
197213

198214

199-
def host_from_connect_args(args):
215+
def host_from_connect_args(args: tuple[Any, ...]) -> str | None:
200216
address = args[0]
201217

202218
if isinstance(address, tuple):
203219
return host_from_address(address)
220+
return None
204221

205222

206223
def is_ipaddress(address: str) -> bool:
@@ -217,15 +234,16 @@ def is_ipaddress(address: str) -> bool:
217234
def resolve_hostnames(hostname: str) -> set[str]:
218235
try:
219236
return {
220-
addr_struct[0] for *_, addr_struct in socket.getaddrinfo(hostname, None)
237+
addr_struct[0] # type: ignore[misc]
238+
for *_, addr_struct in socket.getaddrinfo(hostname, None)
221239
}
222240
except socket.gaierror:
223241
return set()
224242

225243

226244
def normalize_allowed_hosts(
227245
allowed_hosts: list[str],
228-
resolution_cache: dict[str, list[str]] | None = None,
246+
resolution_cache: dict[str, set[str]] | None = None,
229247
) -> dict[str, set[str]]:
230248
"""Map all items in `allowed_hosts` to IP addresses."""
231249
if resolution_cache is None:
@@ -246,7 +264,7 @@ def normalize_allowed_hosts(
246264
def socket_allow_hosts(
247265
allowed: str | list[str] | None = None,
248266
allow_unix_socket: bool = False,
249-
resolution_cache: dict[str, list[str]] | None = None,
267+
resolution_cache: dict[str, set[str]] | None = None,
250268
) -> None:
251269
"""disable socket.socket.connect() to disable the Internet. useful in testing."""
252270
if isinstance(allowed, str):
@@ -270,7 +288,7 @@ def socket_allow_hosts(
270288
]
271289
)
272290

273-
def guarded_connect(inst, *args):
291+
def guarded_connect(inst: socket.socket, *args: Any) -> None:
274292
host = host_from_connect_args(args)
275293
if host in allowed_ip_hosts_and_hostnames or (
276294
_is_unix_socket(inst.family) and allow_unix_socket
@@ -279,10 +297,10 @@ def guarded_connect(inst, *args):
279297

280298
raise SocketConnectBlockedError(allowed_list, host)
281299

282-
socket.socket.connect = guarded_connect
300+
socket.socket.connect = guarded_connect # type: ignore[assignment,method-assign]
283301

284302

285-
def _remove_restrictions():
303+
def _remove_restrictions() -> None:
286304
"""restore socket.socket.* to allow access to the Internet. useful in testing."""
287-
socket.socket = _true_socket
288-
socket.socket.connect = _true_connect
305+
socket.socket = _true_socket # type: ignore[misc]
306+
socket.socket.connect = _true_connect # type: ignore[method-assign]

src/pytest_socket/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)