Skip to content

Commit 6407df7

Browse files
authored
Adding type hints and docstrings (#317)
* Adding type hints and docstrings. * Add coverage.xml to .gitignore and remove from tracking * Fix setsockopt signature to match standard socket API * Add tests for setsockopt with and without optlen
1 parent 3a184d5 commit 6407df7

22 files changed

+1276
-172
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ shippable
2828
.vscode/
2929
Pipfile.lock
3030
requirements.txt
31+
coverage.xml

mocket/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Mocket - socket mocking library for Python."""
2+
13
import importlib
24
import sys
35

mocket/compat.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,57 @@
1111

1212

1313
def encode_to_bytes(s: str | bytes, encoding: str = ENCODING) -> bytes:
14+
"""Encode a string or bytes to bytes.
15+
16+
Args:
17+
s: String or bytes to encode
18+
encoding: Encoding to use (default: utf-8 or MOCKET_ENCODING env var)
19+
20+
Returns:
21+
Encoded bytes
22+
"""
1423
if isinstance(s, str):
1524
s = s.encode(encoding)
1625
return bytes(s)
1726

1827

1928
def decode_from_bytes(s: str | bytes, encoding: str = ENCODING) -> str:
29+
"""Decode bytes or string to string.
30+
31+
Args:
32+
s: String or bytes to decode
33+
encoding: Encoding to use (default: utf-8 or MOCKET_ENCODING env var)
34+
35+
Returns:
36+
Decoded string
37+
"""
2038
if isinstance(s, bytes):
2139
s = codecs.decode(s, encoding, "ignore")
2240
return str(s)
2341

2442

2543
def shsplit(s: str | bytes) -> list[str]:
44+
"""Split a shell command string into arguments.
45+
46+
Args:
47+
s: Shell command string or bytes
48+
49+
Returns:
50+
List of shell command arguments
51+
"""
2652
s = decode_from_bytes(s)
2753
return shlex.split(s)
2854

2955

30-
def do_the_magic(body):
56+
def do_the_magic(body: bytes) -> str:
57+
"""Detect MIME type of binary data using puremagic.
58+
59+
Args:
60+
body: Binary data to analyze
61+
62+
Returns:
63+
MIME type string
64+
"""
3165
try:
3266
magic = puremagic.magic_string(body)
3367
except puremagic.PureError:

mocket/decorators/async_mocket.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1+
"""Async version of Mocket decorator."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, Callable
6+
17
from mocket.decorators.mocketizer import Mocketizer
28
from mocket.utils import get_mocketize
39

410

511
async def wrapper(
6-
test,
7-
truesocket_recording_dir=None,
8-
strict_mode=False,
9-
strict_mode_allowed=None,
10-
*args,
11-
**kwargs,
12-
):
12+
test: Callable,
13+
truesocket_recording_dir: str | None = None,
14+
strict_mode: bool = False,
15+
strict_mode_allowed: list | None = None,
16+
*args: Any,
17+
**kwargs: Any,
18+
) -> Any:
19+
"""Async wrapper function for @async_mocketize decorator.
20+
21+
Args:
22+
test: Async test function to wrap
23+
truesocket_recording_dir: Directory for recording true socket calls
24+
strict_mode: Enable STRICT mode to forbid real socket calls
25+
strict_mode_allowed: List of allowed hosts in STRICT mode
26+
*args: Test arguments
27+
**kwargs: Test keyword arguments
28+
29+
Returns:
30+
Result of the test function
31+
"""
1332
async with Mocketizer.factory(
1433
test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args
1534
):

mocket/decorators/mocketizer.py

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
1+
"""Mocketizer decorator for managing Mocket lifecycle in tests."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, Callable
6+
17
from mocket.mocket import Mocket
28
from mocket.mode import MocketMode
39
from mocket.utils import get_mocketize
410

511

612
class Mocketizer:
13+
"""Context manager and decorator for managing Mocket lifecycle in tests."""
14+
715
def __init__(
816
self,
9-
instance=None,
10-
namespace=None,
11-
truesocket_recording_dir=None,
12-
strict_mode=False,
13-
strict_mode_allowed=None,
14-
):
17+
instance: Any | None = None,
18+
namespace: str | None = None,
19+
truesocket_recording_dir: str | None = None,
20+
strict_mode: bool = False,
21+
strict_mode_allowed: list | None = None,
22+
) -> None:
23+
"""Initialize the Mocketizer.
24+
25+
Args:
26+
instance: Test instance (optional)
27+
namespace: Namespace for recordings
28+
truesocket_recording_dir: Directory for recording true socket calls
29+
strict_mode: Enable STRICT mode to forbid real socket calls
30+
strict_mode_allowed: List of allowed hosts in STRICT mode
31+
"""
1532
self.instance = instance
1633
self.truesocket_recording_dir = truesocket_recording_dir
1734
self.namespace = namespace or str(id(self))
@@ -23,41 +40,89 @@ def __init__(
2340
"Allowed locations are only accepted when STRICT mode is active."
2441
)
2542

26-
def enter(self):
43+
def enter(self) -> None:
44+
"""Enter the Mocketizer context (enable Mocket)."""
2745
Mocket.enable(
2846
namespace=self.namespace,
2947
truesocket_recording_dir=self.truesocket_recording_dir,
3048
)
3149
if self.instance:
3250
self.check_and_call("mocketize_setup")
3351

34-
def __enter__(self):
52+
def __enter__(self) -> Mocketizer:
53+
"""Enter context manager.
54+
55+
Returns:
56+
Self for use in `with` statements
57+
"""
3558
self.enter()
3659
return self
3760

38-
def exit(self):
61+
def exit(self) -> None:
62+
"""Exit the Mocketizer context (disable Mocket)."""
3963
if self.instance:
4064
self.check_and_call("mocketize_teardown")
4165

4266
Mocket.disable()
4367

44-
def __exit__(self, type, value, tb):
68+
def __exit__(self, type: Any, value: Any, tb: Any) -> None:
69+
"""Exit context manager.
70+
71+
Args:
72+
type: Exception type
73+
value: Exception value
74+
tb: Traceback
75+
"""
4576
self.exit()
4677

47-
async def __aenter__(self, *args, **kwargs):
78+
async def __aenter__(self, *args: Any, **kwargs: Any) -> Mocketizer:
79+
"""Enter async context manager.
80+
81+
Returns:
82+
Self for use in `async with` statements
83+
"""
4884
self.enter()
4985
return self
5086

51-
async def __aexit__(self, *args, **kwargs):
87+
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
88+
"""Exit async context manager.
89+
90+
Args:
91+
*args: Exception arguments
92+
**kwargs: Exception keyword arguments
93+
"""
5294
self.exit()
5395

54-
def check_and_call(self, method_name):
96+
def check_and_call(self, method_name: str) -> None:
97+
"""Check if instance has a method and call it.
98+
99+
Args:
100+
method_name: Name of method to check and call
101+
"""
55102
method = getattr(self.instance, method_name, None)
56103
if callable(method):
57104
method()
58105

59106
@staticmethod
60-
def factory(test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args):
107+
def factory(
108+
test: Callable,
109+
truesocket_recording_dir: str | None,
110+
strict_mode: bool,
111+
strict_mode_allowed: list | None,
112+
args: tuple,
113+
) -> Mocketizer:
114+
"""Create a Mocketizer instance for a test function.
115+
116+
Args:
117+
test: Test function being decorated
118+
truesocket_recording_dir: Recording directory
119+
strict_mode: Enable STRICT mode
120+
strict_mode_allowed: Allowed hosts in STRICT mode
121+
args: Positional arguments to test
122+
123+
Returns:
124+
Configured Mocketizer instance
125+
"""
61126
instance = args[0] if args else None
62127
namespace = None
63128
if truesocket_recording_dir:
@@ -79,13 +144,26 @@ def factory(test, truesocket_recording_dir, strict_mode, strict_mode_allowed, ar
79144

80145

81146
def wrapper(
82-
test,
83-
truesocket_recording_dir=None,
84-
strict_mode=False,
85-
strict_mode_allowed=None,
86-
*args,
87-
**kwargs,
88-
):
147+
test: Callable,
148+
truesocket_recording_dir: str | None = None,
149+
strict_mode: bool = False,
150+
strict_mode_allowed: list | None = None,
151+
*args: Any,
152+
**kwargs: Any,
153+
) -> Any:
154+
"""Wrapper function for @mocketize decorator.
155+
156+
Args:
157+
test: Test function to wrap
158+
truesocket_recording_dir: Recording directory
159+
strict_mode: Enable STRICT mode
160+
strict_mode_allowed: Allowed hosts in STRICT mode
161+
*args: Test arguments
162+
**kwargs: Test keyword arguments
163+
164+
Returns:
165+
Result of the test function
166+
"""
89167
with Mocketizer.factory(
90168
test, truesocket_recording_dir, strict_mode, strict_mode_allowed, args
91169
):

mocket/entry.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
1+
"""Mocket entry base class for registering mock responses."""
2+
3+
from __future__ import annotations
4+
15
import collections.abc
6+
from typing import Any
27

38
from mocket.compat import encode_to_bytes
49
from mocket.mocket import Mocket
510

611

712
class MocketEntry:
13+
"""Base class for Mocket entries that match requests and return responses."""
14+
815
class Response(bytes):
16+
"""Response wrapper class that extends bytes."""
17+
918
@property
10-
def data(self):
19+
def data(self) -> bytes:
20+
"""Get the response data."""
1121
return self
1222

13-
response_index = 0
14-
request_cls = bytes
15-
response_cls = Response
16-
responses = None
17-
_served = None
23+
response_index: int = 0
24+
request_cls: type = bytes
25+
response_cls: type = Response
26+
responses: list | None = None
27+
_served: bool | None = None
28+
29+
def __init__(self, location: tuple, responses: Any) -> None:
30+
"""Initialize a Mocket entry.
1831
19-
def __init__(self, location, responses):
32+
Args:
33+
location: Tuple of (host, port)
34+
responses: Single response or list of responses to cycle through
35+
"""
2036
self._served = False
2137
self.location = location
2238

@@ -34,18 +50,40 @@ def __init__(self, location, responses):
3450
r = self.response_cls(r)
3551
self.responses.append(r)
3652

37-
def __repr__(self):
53+
def __repr__(self) -> str:
54+
"""Return a string representation of the entry."""
3855
return f"{self.__class__.__name__}(location={self.location})"
3956

4057
@staticmethod
41-
def can_handle(data):
58+
def can_handle(data: bytes) -> bool:
59+
"""Check if this entry can handle the given request data.
60+
61+
Args:
62+
data: Request data to check
63+
64+
Returns:
65+
True if this entry can handle the request, False otherwise
66+
"""
4267
return True
4368

44-
def collect(self, data):
69+
def collect(self, data: bytes) -> None:
70+
"""Collect the request data in the Mocket singleton.
71+
72+
Args:
73+
data: Request data to collect
74+
"""
4575
req = self.request_cls(data)
4676
Mocket.collect(req)
4777

48-
def get_response(self):
78+
def get_response(self) -> bytes:
79+
"""Get the next response to send.
80+
81+
Returns:
82+
Response bytes to send to the client
83+
84+
Raises:
85+
BaseException: If a response is an exception, it will be raised
86+
"""
4987
response = self.responses[self.response_index]
5088
if self.response_index < len(self.responses) - 1:
5189
self.response_index += 1

mocket/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
"""Mocket exception classes."""
2+
3+
14
class MocketException(Exception):
5+
"""Base exception class for Mocket errors."""
6+
27
pass
38

49

510
class StrictMocketException(MocketException):
11+
"""Exception raised when a socket operation is not allowed in STRICT mode."""
12+
613
pass

0 commit comments

Comments
 (0)