Modernize extension

- update dependencies
- use tox for testing
- use type hints
- use pathlib and ipaddress from standard library instead of path and
  ipcalc
- fix Sphinx deprecation warnings
This commit is contained in:
Jan Dittberner 2023-01-28 17:43:04 +01:00
parent c721d1bf9c
commit 7c675a6fdb
13 changed files with 801 additions and 465 deletions

View file

@ -1 +0,0 @@
__import__("pkg_resources").declare_namespace(__name__)

View file

@ -10,13 +10,15 @@
"""
__version__ = "0.5.1"
import ipaddress
from collections import defaultdict
from typing import Iterable, List, Optional, Tuple
from typing import Iterable, List, Optional, Tuple, Any, cast
from docutils import nodes
from ipcalc import IP, Network
from docutils.nodes import Element
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.directives import ObjectDescription, T
from sphinx.domains import Domain, ObjType
@ -39,32 +41,32 @@ class IPRangeDirective(ObjectDescription):
has_content = True
required_arguments = 1
title_prefix = None
range_spec = None
title_prefix: str = ""
range_spec: str = ""
def get_title_prefix(self) -> str:
if self.title_prefix is None:
raise NotImplemented("subclasses must set title_prefix")
return self.title_prefix
def handle_signature(self, sig: str, signode: desc_signature) -> T:
signode += addnodes.desc_name(text="{} {}".format(self.get_title_prefix(), sig))
def handle_signature(self, sig: str, sig_node: desc_signature) -> str:
sig_node += addnodes.desc_name(
text="{} {}".format(self.get_title_prefix(), sig)
)
self.range_spec = sig
return sig
def transform_content(self, contentnode: addnodes.desc_content) -> None:
def transform_content(self, content_node: addnodes.desc_content) -> None:
ip_range_node = ip_range()
ip_range_node["range_spec"] = self.range_spec
contentnode += ip_range_node
content_node += ip_range_node
class IPV4RangeDirective(IPRangeDirective):
title_prefix = _("IPv4 range")
def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None:
def add_target_and_index(self, name: T, sig: str, sig_node: desc_signature) -> None:
anchor = "ip-ipv4range-{0}".format(sig)
signode["ids"].append(anchor)
ips = self.env.get_domain("ip")
sig_node["ids"].append(anchor)
ips = cast(IPDomain, self.env.get_domain("ip"))
ips.add_ip4_range(sig)
idx_text = "{}; {}".format(self.title_prefix, name)
self.indexnode["entries"] = [
@ -78,7 +80,7 @@ class IPV6RangeDirective(IPRangeDirective):
def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None:
anchor = "ip-ipv6range-{0}".format(sig)
signode["ids"].append(anchor)
ips = self.env.get_domain("ip")
ips = cast(IPDomain, self.env.get_domain("ip"))
ips.add_ip6_range(sig)
idx_text = "{}; {}".format(self.title_prefix, name)
self.indexnode["entries"] = [
@ -101,7 +103,7 @@ class IPXRefRole(XRefRole):
) -> Tuple[str, str]:
refnode.attributes.update(env.ref_context)
ips = env.get_domain("ip")
ips = cast(IPDomain, env.get_domain("ip"))
if refnode["reftype"] == "v4":
ips.add_ip4_address_reference(target)
elif refnode["reftype"] == "v6":
@ -122,7 +124,7 @@ class IPXRefRole(XRefRole):
) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
node_list, message = super().result_nodes(document, env, node, is_ref)
ip = env.get_domain("ip")
ip = cast(IPDomain, env.get_domain("ip"))
if self.reftype in ["v4", "v6"] and self.target not in ip.data["ip_dict"]:
return node_list, message
if (
@ -138,7 +140,7 @@ class IPXRefRole(XRefRole):
ip.add_ip_address_anchor(self.target, env.docname, target_id)
target_node = nodes.target("", "", ids=[target_id])
doc_title = next(d for d in document.traverse(nodes.title)).astext()
doc_title = next(d for d in document.findall(nodes.title)).astext()
node_text = node.astext()
@ -157,6 +159,25 @@ class IPXRefRole(XRefRole):
class IPDomain(Domain):
"""Custom domain for IP addresses and ranges."""
def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None:
# TODO: implement merge_domaindata
print(
f"merge_domaindata called for docnames: {docnames} with otherdata: {otherdata}"
)
def resolve_any_xref(
self,
env: BuildEnvironment,
fromdocname: str,
builder: Builder,
target: str,
node: pending_xref,
contnode: Element,
) -> list[tuple[str, Element]]:
# TODO: implement resolve_any_xref
print("resolve_any_xref called")
return []
name = "ip"
label = "IP addresses and ranges."
@ -194,23 +215,23 @@ class IPDomain(Domain):
for obj in self.data["range_nodes"]:
yield obj
def add_ip4_range(self, sig: desc_signature):
def add_ip4_range(self, sig: str):
logger.debug("add_ip4_range: %s", sig)
self._add_ip_range("v4", sig)
def add_ip6_range(self, sig: desc_signature):
def add_ip6_range(self, sig: str):
logger.debug("add_ip6_range: %s", sig)
self._add_ip_range("v6", sig)
def _add_ip_range(self, family: str, sig: desc_signature):
def _add_ip_range(self, family: str, sig: str):
name = "ip{}range.{}".format(family, sig)
anchor = "ip-ip{}range-{}".format(family, sig)
try:
ip_range = Network(sig)
new_ip_range = ipaddress.ip_network(sig)
self.data["range_nodes"].append(
(name, family, sig, self.env.docname, anchor, 0)
)
self.data["ranges"][sig].append((ip_range, self.env.docname, anchor))
self.data["ranges"][sig].append((new_ip_range, self.env.docname, anchor))
except ValueError as e:
logger.error("invalid ip range address '%s': %s", sig, e)
@ -224,13 +245,13 @@ class IPDomain(Domain):
def _add_ip_address_reference(self, family, sig):
try:
self.data["ip_dict"][sig] = IP(sig)
self.data["ip_dict"][sig] = ipaddress.ip_address(sig)
except ValueError as e:
logger.error("invalid ip address '%s': %s", sig, e)
def add_ip_address_anchor(self, sig, docname, anchor):
try:
ip = IP(sig)
ip = ipaddress.ip_address(sig)
self.data["ip_refs"][sig].append((ip, docname, anchor))
except ValueError as e:
logger.error("invalid ip address '%s': %s", sig, e)
@ -247,7 +268,7 @@ class IPDomain(Domain):
name = "iprange{}.{}".format(family, sig)
anchor = "ip-iprange{}-{}".format(family, sig)
try:
ip_range = Network(sig)
ip_range = ipaddress.ip_network(sig)
self.data["range_refs"].append(
(
name,
@ -274,15 +295,11 @@ class IPDomain(Domain):
) -> Optional[nodes.Element]:
match = []
def address_tuple(docname, anchor, ip_range) -> Tuple[str, str, str]:
def address_tuple(docname, anchor, ip_range: Any) -> Tuple[str, str, str]:
return (
docname,
anchor,
_(
"IPv{0} range {1}".format(
ip_range.version(), ip_range.to_compressed()
)
),
_("IPv{0} range {1}".format(ip_range.version, ip_range.compressed)),
)
if typ in ("v4", "v6"):
@ -318,26 +335,26 @@ class IPDomain(Domain):
return None
def process_ip_nodes(app, doctree, fromdocname):
def process_ip_nodes(app: Sphinx, doctree: nodes.Node, fromdocname: str):
env = app.builder.env
ips = env.get_domain(IPDomain.name)
header = (_("IP address"), _("Used by"))
column_widths = (2, 5)
for node in doctree.traverse(ip_range):
for node in doctree.findall(ip_range):
content = []
net = Network(node["range_spec"])
net = ipaddress.ip_network(node["range_spec"], strict=False)
addresses = defaultdict(list)
for ip_address_sig, refs in ips.data["ip_refs"].items():
for ip_address, todocname, anchor in refs:
for ip_address, to_doc_name, anchor in refs:
if ip_address in net:
addresses[ip_address_sig].append((ip_address, todocname, anchor))
addresses[ip_address_sig].append((ip_address, to_doc_name, anchor))
logger.debug(
"found %s in network %s on %s",
ip_address_sig,
net.to_compressed(),
todocname,
net.compressed,
to_doc_name,
)
if addresses:
table = nodes.table()
@ -360,30 +377,30 @@ def process_ip_nodes(app, doctree, fromdocname):
(key, addresses[key]) for key in sorted(addresses, key=sort_by_ip)
]:
para = nodes.paragraph()
para += nodes.literal("", ip_info[0][0].to_compressed())
para += nodes.literal("", ip_info[0][0].compressed)
ref_node = nodes.paragraph()
ref_nodes = []
referenced_docs = set()
for item in ip_info:
ip_address, todocname, anchor = item
if todocname in referenced_docs:
ip_address, to_doc_name, anchor = item
if to_doc_name in referenced_docs:
continue
referenced_docs.add(todocname)
referenced_docs.add(to_doc_name)
title = env.titles[todocname]
innernode = nodes.Text(title.astext())
newnode = make_refnode(
title = env.titles[to_doc_name]
inner_node = nodes.Text(title.astext())
new_node = make_refnode(
app.builder,
fromdocname,
todocname,
to_doc_name,
anchor,
innernode,
inner_node,
title.astext(),
)
ref_nodes.append(newnode)
ref_nodes.append(new_node)
for count in range(len(ref_nodes)):
ref_node.append(ref_nodes[count])
if count < len(ref_nodes) - 1:
@ -407,10 +424,10 @@ def create_table_row(rowdata):
def sort_by_ip(item):
return IP(item).ip
return ipaddress.ip_address(item)
def setup(app):
def setup(app: Sphinx):
app.add_domain(IPDomain)
app.connect("doctree-resolved", process_ip_nodes)
return {