Skip to content

Commit d9bbaaf

Browse files
authored
Docstrings and type annotations (#51)
1 parent 19a2afd commit d9bbaaf

9 files changed

Lines changed: 281 additions & 127 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ select = [
108108
"PLE",
109109
"PLR",
110110
"PLW",
111-
# "PT",
111+
"PT",
112112
"PYI",
113113
"Q",
114114
"RET",

src/uwhoisd/__init__.py

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
A 'universal' WHOIS proxy server.
3-
"""
1+
"""A 'universal' WHOIS proxy server."""
42

53
import asyncio
64
import configparser
@@ -11,20 +9,19 @@
119
import re
1210
import socket
1311
import sys
12+
import typing as t
1413

1514
from . import caching, client, server, utils
1615

1716
USAGE = "Usage: %s <config>"
1817

1918
PORT = socket.getservbyname("whois", "tcp")
2019

21-
logger = logging.getLogger("uwhoisd")
20+
logger = logging.getLogger(__name__)
2221

2322

2423
class UWhois:
25-
"""
26-
Universal WHOIS proxy.
27-
"""
24+
"""Universal WHOIS proxy."""
2825

2926
__slots__ = (
3027
"conservative",
@@ -36,22 +33,21 @@ class UWhois:
3633
"suffix",
3734
)
3835

39-
def __init__(self):
40-
"""
41-
Initialise the proxy.
42-
"""
36+
def __init__(self) -> None:
4337
super().__init__()
44-
self.suffix = None
45-
self.overrides = {}
46-
self.prefixes = {}
47-
self.recursion_patterns = {}
48-
self.registry_whois = False
49-
self.page_feed = True
50-
self.conservative = ()
51-
52-
def read_config(self, parser):
53-
"""
54-
Read the configuration for this object from a config file.
38+
self.suffix: t.Optional[str] = None
39+
self.overrides: dict[str, str] = {}
40+
self.prefixes: dict[str, str] = {}
41+
self.recursion_patterns: dict[str, re.Pattern] = {}
42+
self.registry_whois: bool = False
43+
self.page_feed: bool = True
44+
self.conservative: t.Sequence[str] = ()
45+
46+
def read_config(self, parser: utils.ConfigParser) -> None:
47+
"""Read the configuration for this object from a config file.
48+
49+
Args:
50+
parser: The config parser to read from.
5551
"""
5652
self.registry_whois = parser.get_bool("uwhoisd", "registry_whois")
5753
self.page_feed = parser.get_bool("uwhoisd", "page_feed")
@@ -64,34 +60,53 @@ def read_config(self, parser):
6460
for zone, pattern in parser.items("recursion_patterns"):
6561
self.recursion_patterns[zone] = re.compile(utils.decode_value(pattern), re.IGNORECASE)
6662

67-
def get_whois_server(self, zone):
68-
"""
69-
Get the WHOIS server for the given zone.
63+
def get_whois_server(self, zone: str) -> tuple[str, int]:
64+
"""Get the WHOIS server for the given zone.
65+
66+
Args:
67+
zone: The zone to get the WHOIS server for.
68+
69+
Returns:
70+
A tuple of the WHOIS server and port.
7071
"""
7172
server = self.overrides.get(zone, f"{zone}.{self.suffix}")
7273
if ":" in server:
7374
server, port = server.split(":", 1)
74-
port = int(port)
75-
else:
76-
port = PORT
77-
return server, port
75+
return server, int(port)
76+
return server, PORT
7877

79-
def get_registrar_whois_server(self, zone, response):
80-
"""
81-
Extract the registrar's WHOIS server from the registry response.
78+
def get_registrar_whois_server(self, zone: str, response: str) -> t.Optional[str]:
79+
"""Extract the registrar's WHOIS server from the registry response.
80+
81+
Args:
82+
zone: The zone being queried.
83+
response: The response from the registry's WHOIS server.
84+
85+
Returns:
86+
The registrar's WHOIS server, or None if not found.
8287
"""
8388
matches = self.recursion_patterns[zone].search(response)
8489
return None if matches is None else matches.group("server")
8590

86-
def get_prefix(self, zone):
87-
"""
88-
Get the prefix required when querying the servers for the given zone.
91+
def get_prefix(self, zone: str) -> str:
92+
"""Get the prefix required when querying the servers for the given zone.
93+
94+
Args:
95+
zone: The zone to get the prefix for.
96+
97+
Returns:
98+
The prefix string.
8999
"""
90100
return self.prefixes.get(zone, "")
91101

92102
async def whois(self, query: str) -> str:
93-
"""
94-
Query the appropriate WHOIS server.
103+
"""Query the appropriate WHOIS server.
104+
105+
Args:
106+
query: The WHOIS query.
107+
108+
Returns:
109+
The WHOIS response.
95110
"""
96111
# Figure out the zone whose WHOIS server we're meant to be querying.
97112
for zone in self.conservative:
@@ -107,23 +122,21 @@ async def whois(self, query: str) -> str:
107122

108123
# Thin registry? Query the registrar's WHOIS server.
109124
if zone in self.recursion_patterns:
110-
server = self.get_registrar_whois_server(zone, response)
111-
if server is not None:
125+
registrar_server = self.get_registrar_whois_server(zone, response)
126+
if registrar_server is not None:
112127
if not self.registry_whois:
113128
response = ""
114129
elif self.page_feed:
115130
# A form feed character so it's possible to find the split.
116131
response += "\f"
117-
logger.info("Recursive query to %s about %s", server, query)
118-
response += await client.query_whois(server, port, query)
132+
logger.info("Recursive query to %s about %s", registrar_server, query)
133+
response += await client.query_whois(registrar_server, port, query)
119134

120135
return response
121136

122137

123-
def main():
124-
"""
125-
Execute the daemon.
126-
"""
138+
def main() -> int:
139+
"""Execute the daemon."""
127140
if len(sys.argv) != 2:
128141
print(USAGE % os.path.basename(sys.argv[0]), file=sys.stderr)
129142
return 1

src/uwhoisd/caching.py

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,49 @@
1-
"""
2-
Caching support.
3-
"""
1+
"""Caching support."""
42

53
import collections
64
from importlib import metadata
75
import logging
86
import time
7+
import typing as t
98

10-
logger = logging.getLogger("uwhoisd")
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class Cache(t.Protocol):
13+
"""A WHOIS cache protocol."""
14+
15+
def get(self, key: str) -> t.Optional[str]:
16+
"""Retrieve a value from the cache.
17+
18+
Args:
19+
key: The cache key to look up.
20+
21+
Returns:
22+
The cached value, or `None` if not found.
23+
"""
24+
25+
def set(self, key: str, value: str) -> None:
26+
"""Store a value in the cache.
27+
28+
Args:
29+
key: The cache key to store the value under.
30+
value: The value to store.
31+
"""
1132

1233

1334
class UnknownCacheError(Exception):
14-
"""
15-
The supplied cache type name cannot be found.
16-
"""
35+
"""The supplied cache type name cannot be found."""
1736

1837

19-
def get_cache(cfg):
20-
"""
21-
Attempt to load the configured cache.
38+
def get_cache(cfg: dict[str, str]) -> t.Optional[Cache]:
39+
"""Attempt to load the configured cache.
40+
41+
Args:
42+
cfg: The cache configuration. Must contain a "type" key giving the
43+
cache type name. Any other keys are passed as parameters to the
44+
cache constructor.
45+
46+
Returns: The cache object, or `None` if caching is disabled.
2247
"""
2348
cache_name = cfg.pop("type", "null")
2449
if cache_name == "null":
@@ -33,14 +58,23 @@ def get_cache(cfg):
3358
raise UnknownCacheError(cache_name)
3459

3560

36-
def wrap_whois(cache, whois_func):
37-
"""
38-
Wrap a WHOIS query function with a cache.
61+
def wrap_whois(
62+
cache: t.Optional[Cache],
63+
whois_func: t.Callable[[str], t.Awaitable[str]],
64+
) -> t.Callable[[str], t.Awaitable[str]]:
65+
"""Wrap a WHOIS query function with a cache.
66+
67+
Args:
68+
cache: The cache to use, or `None` to disable caching.
69+
whois_func: The WHOIS query function to wrap.
70+
71+
Returns:
72+
The wrapped WHOIS query function.
3973
"""
4074
if cache is None:
4175
return whois_func
4276

43-
async def wrapped(query):
77+
async def wrapped(query: str) -> str:
4478
response = cache.get(query)
4579
if response is None:
4680
response = await whois_func(query)
@@ -60,40 +94,41 @@ class LFU:
6094
2-tuples consisting of a counter giving the number of times this item
6195
occurs on the eviction queue and the value.
6296
97+
Args:
98+
max_size: Maximum number of entries the cache can contain.
99+
max_age: Maximum number of seconds to consider an entry live.
63100
"""
64101

65102
# I may end up reimplementing an LRU cache if it turns out that's more apt,
66103
# but I haven't went that route as an LRU cache is somewhat more awkward
67104
# and involved to implement correctly.
68105

69-
__slots__ = ("cache", "max_age", "max_size", "queue")
106+
__slots__ = (
107+
"cache",
108+
"max_age",
109+
"max_size",
110+
"queue",
111+
)
70112

71113
clock = staticmethod(time.time)
72114

73-
def __init__(self, max_size=256, max_age=300):
74-
"""
75-
Create a new LFU cache.
76-
77-
:param max_size int: Maximum number of entries the cache can contain.
78-
:param max_age int: Maximum number of seconds to consider an entry
79-
live.
80-
"""
115+
def __init__(self, max_size: int = 256, max_age: int = 300) -> None:
81116
super().__init__()
82-
self.cache = {}
83-
self.queue = collections.deque()
117+
self.cache: dict[str, tuple[int, str]] = {}
118+
self.queue: t.Deque[tuple[int, str]] = collections.deque()
84119
self.max_size = int(max_size)
85120
self.max_age = int(max_age)
86121

87-
def evict_one(self):
88-
"""
89-
Remove the item at the head of the eviction cache.
90-
"""
122+
def evict_one(self) -> None:
123+
"""Remove the item at the head of the eviction cache."""
91124
_, key = self.queue.popleft()
92125
self.attempt_eviction(key)
93126

94-
def attempt_eviction(self, key):
95-
"""
96-
Attempt to remove the named item from the cache.
127+
def attempt_eviction(self, key: str) -> None:
128+
"""Attempt to remove the named item from the cache.
129+
130+
Args:
131+
key: The cache key to evict.
97132
"""
98133
counter, value = self.cache[key]
99134
counter -= 1
@@ -102,10 +137,8 @@ def attempt_eviction(self, key):
102137
else:
103138
self.cache[key] = (counter, value)
104139

105-
def evict_expired(self):
106-
"""
107-
Evict any items older than the maximum age from the cache.
108-
"""
140+
def evict_expired(self) -> None:
141+
"""Evict any items older than the maximum age from the cache."""
109142
cutoff = self.clock() - self.max_age
110143
while len(self.queue) > 0:
111144
ts, key = self.queue.popleft()
@@ -114,11 +147,14 @@ def evict_expired(self):
114147
break
115148
self.attempt_eviction(key)
116149

117-
def get(self, key):
118-
"""
119-
Pull a value from the cache corresponding to the key.
150+
def get(self, key: str) -> t.Optional[str]:
151+
"""Pull a value from the cache corresponding to the key.
120152
121-
If no value exists, `None` is returned.
153+
Args:
154+
key: The cache key to look up.
155+
156+
Returns:
157+
The cached value, or `None` if not found.
122158
"""
123159
self.evict_expired()
124160
if key not in self.cache:
@@ -128,9 +164,12 @@ def get(self, key):
128164
self.set(key, value)
129165
return value
130166

131-
def set(self, key, value):
132-
"""
133-
Add `value` to the cache, to be referenced by `key`.
167+
def set(self, key: str, value: str) -> None:
168+
"""Add `value` to the cache, to be referenced by `key`.
169+
170+
Args:
171+
key: The cache key to store the value under.
172+
value: The value to store.
134173
"""
135174
if len(self.queue) == self.max_size:
136175
self.evict_one()
@@ -139,4 +178,4 @@ def set(self, key, value):
139178
else:
140179
counter = 0
141180
self.cache[key] = (counter + 1, value)
142-
self.queue.append((self.clock(), key))
181+
self.queue.append((int(self.clock()), key))

src/uwhoisd/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44

55

66
async def query_whois(host: str, port: int, query: str) -> str:
7+
"""Query a WHOIS server.
8+
9+
Args:
10+
host: The WHOIS server hostname.
11+
port: The WHOIS server port.
12+
query: The WHOIS query.
13+
14+
Returns:
15+
The WHOIS response.
16+
"""
717
reader, writer = await asyncio.open_connection(host, port)
818

919
writer.write(f"{query}\r\n".encode())

0 commit comments

Comments
 (0)