Compare commits

...

12 commits
0.4.0 ... main

Author SHA1 Message Date
Jan Dittberner 04985650e7 Remove broken Sphinx type annotation
sphinx.directives.T has been removed upstream. This commit removes the
type annotation to avoid breaking the extension with Sphinx >= 6
2023-07-18 11:18:25 +02:00
Jan Dittberner 9d94b308ae Release 0.6.1 2023-01-29 17:53:22 +01:00
Jan Dittberner 1afe18b429 Release 0.6.0 2023-01-29 17:49:40 +01:00
Jan Dittberner 7c675a6fdb 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
2023-01-28 17:43:04 +01:00
Jan Dittberner c721d1bf9c Remove development information from README.rst 2021-09-05 12:26:05 +02:00
Jan Dittberner 6d425acaac Add release documentation in DEVELOPMENT.rst 2021-09-05 11:40:42 +02:00
Jan Dittberner 7acff11695 Release 0.5.1
- bump version
- update CHANGES.rst
2021-09-04 19:56:18 +02:00
Jan Dittberner d1a151f8bd Fix anchor links 2021-09-04 19:54:24 +02:00
Jan Dittberner 1451a5a1c0 Release 0.5.0 2021-09-04 18:53:49 +02:00
Jan Dittberner e53838acfb Use make_refnode for creating the references on the IP list 2021-09-04 17:51:04 +02:00
Jan Dittberner 49a3d89488 Reimplement using Sphinx 4 APIs
- use ObjectDescription for IPRange
- reimplement data handling in IPDomain
- store target docname and anchors in data to avoid later lookups
- remove broken entries from index
- adapt tests
2021-09-04 17:15:27 +02:00
Jan Dittberner 8bc07c611f Update to Sphinx 4
- allow local development using editable = true in Pipfile and by adding
  a setup.py
- bump dependencies for Sphinx 4.x
2021-09-04 17:14:18 +02:00
18 changed files with 1277 additions and 915 deletions

2
.gitignore vendored
View file

@ -5,6 +5,8 @@
.coverage
.idea/
.ropeproject/
/.*_cache/
/.python-version
/.tox/
__pycache__/
_build/

View file

@ -1,6 +1,30 @@
Changes
=======
0.6.1 - 2023-01-29
------------------
* reupload to include namespace package
0.6.0 - 2023-01-29
------------------
* add development documentation in development.rst
* use tox as test runner
* add type annotations
* fix Sphinx 6 deprecation warnings
0.5.1 - 2021-09-04
------------------
* fix anchors in links
0.5.0 - 2021-09-04
------------------
* fix Docutils error
* adapt to Sphinx 4
0.4.0 - 2021-01-02
------------------

47
DEVELOPMENT.rst Normal file
View file

@ -0,0 +1,47 @@
Development
===========
The extension is developed in a git repository that can be cloned by running::
git clone https://git.dittberner.info/jan/sphinxext-ip.git
Running test
------------
To install all dependencies and run the tests use::
pipenv install --dev
pipenv run tox
Release a new version
---------------------
Start by deciding the new release number and perform the following steps:
* update CHANGES.rst
* change ``version`` in setup.cfg
* change ``__version__`` in jandd/sphinxext/ip/__init__.rst
* change ``version`` in tests/root/conf.py
* commit and push your changes ::
git commit -m "Release <version>"
git push
* create an annotated and signed tag with the new version number (``git
shortlog <previous_tag>..HEAD`` could help to create a good release tag
message) ::
git tag -s -a <version>
* build the release artifacts ::
rm -rf dist jandd.sphinxext.ip.egg-info
pipenv run python3 setup.py egg_info -b <version< bdist_wheel sdist
* upload to PyPI using twine ::
pipenv run twine upload -s dist/*
* push the tag to git ::
git push --tags

View file

@ -4,12 +4,13 @@ url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
sphinx = ">=3"
ipcalc = ">=1.99"
coverage = "*"
twine = "*"
path = "*"
pytest = "*"
tox = "*"
black = "*"
[packages]
jandd-sphinxext-ip = {path = "."}
jandd-sphinxext-ip = { path = ".", editable = true }
Sphinx = ">=5"
docutils = "*"

1149
Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,21 +8,6 @@ directives to collect information regarding IP addresses in IP ranges.
.. _Sphinx: http://www.sphinx-doc.org/
Development
===========
The extension is developed in a git repository that can be cloned by running::
git clone https://git.dittberner.info/jan/sphinxext-ip.git
Running test
------------
To install all dependencies and run the tests use::
pipenv install --dev
pipenv run pytest
Contributors
============

View file

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

View file

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

View file

@ -1,363 +0,0 @@
# -*- coding: utf-8 -*-
"""
jandd.sphinxext.ip
~~~~~~~~~~~~~~~~~~
The IP domain.
:copyright: Copyright (c) 2016 Jan Dittberner
:license: GPLv3+, see COPYING file for details.
"""
import re
from docutils import nodes
from docutils.parsers.rst import Directive
from ipcalc import Network
from sphinx import addnodes
from sphinx.domains import Domain, ObjType
from sphinx.errors import NoUri
from sphinx.locale import _
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode
__version__ = "0.4.0"
logger = logging.getLogger(__name__)
def ip_object_anchor(typ, path):
path = re.sub(r"[.:/]", "-", path)
return typ.lower() + "-" + path
class ip_node(nodes.Inline, nodes.TextElement):
pass
class ip_range(nodes.General, nodes.Element):
pass
class IPXRefRole(XRefRole):
"""
Cross referencing role for the IP domain.
"""
def __init__(self, method, index_type, **kwargs):
self.method = method
self.index_type = index_type
innernodeclass = None
if method in ("v4", "v6"):
innernodeclass = ip_node
super(IPXRefRole, self).__init__(innernodeclass=innernodeclass, **kwargs)
def __cal__(self, typ, rawtext, text, lineno, inliner, options=None, content=None):
if content is None:
content = []
if options is None:
options = {}
try:
Network(text)
except ValueError as e:
env = inliner.document.settings.env
logger.warning(
"invalid ip address/range %s" % text, location=(env.docname, lineno)
)
return [nodes.literal(text, text), []]
return super(IPXRefRole, self).__call__(
typ, rawtext, text, lineno, inliner, options, content
)
def process_link(self, env, refnode, has_explicit_title, title, target):
domaindata = env.domaindata["ip"]
domaindata[self.method][target] = (target, refnode)
return title, target
def result_nodes(self, document, env, node, is_ref):
try:
node["typ"] = self.method
indexnode = addnodes.index()
targetid = "index-%s" % env.new_serialno("index")
targetnode = nodes.target("", "", ids=[targetid])
doctitle = list(document.traverse(nodes.title))[0].astext()
idxtext = "%s; %s" % (node.astext(), doctitle)
idxtext2 = "%s; %s" % (self.index_type, node.astext())
indexnode["entries"] = [
("single", idxtext, targetid, "", None),
("single", idxtext2, targetid, "", None),
]
return [indexnode, targetnode, node], []
except KeyError as e:
return [node], [e.args[0]]
class IPRange(Directive):
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {}
def handle_rangespec(self, node):
titlenode = nodes.title()
node.append(titlenode)
titlenode.append(nodes.inline("", self.get_prefix_title()))
titlenode.append(nodes.literal("", self.rangespec))
ids = ip_object_anchor(self.typ, self.rangespec)
node["ids"].append(ids)
self.env.domaindata[self.domain][self.typ][ids] = (
self.env.docname,
self.options.get("synopsis", ""),
)
return ids
def run(self):
if ":" in self.name:
self.domain, self.objtype = self.name.split(":", 1)
else:
self.domain, self.objtype = "", self.name
self.env = self.state.document.settings.env
self.rangespec = self.arguments[0]
node = nodes.section()
name = self.handle_rangespec(node)
if self.env.docname in self.env.titles:
doctitle = self.env.titles[self.env.docname]
else:
doctitle = list(self.state.document.traverse(nodes.title))[0].astext()
idx_text = "%s; %s" % (self.rangespec, doctitle)
self.indexnode = addnodes.index(
entries=[
("single", idx_text, name, "", None),
("single", self.get_index_text(), name, "", None),
]
)
if self.content:
contentnode = nodes.paragraph("")
node.append(contentnode)
self.state.nested_parse(self.content, self.content_offset, contentnode)
iprange = ip_range()
node.append(iprange)
iprange["rangespec"] = self.rangespec
return [self.indexnode, node]
class IPv4Range(IPRange):
typ = "v4range"
def get_prefix_title(self):
return _("IPv4 address range ")
def get_index_text(self):
return "%s; %s" % (_("IPv4 range"), self.rangespec)
class IPv6Range(IPRange):
typ = "v6range"
def get_prefix_title(self):
return _("IPv6 address range ")
def get_index_text(self):
return "%s; %s" % (_("IPv6 range"), self.rangespec)
class IPDomain(Domain):
"""
IP address and range domain.
"""
name = "ip"
label = "IP addresses and ranges."
object_types = {
"v4": ObjType(_("v4"), "v4", "obj"),
"v6": ObjType(_("v6"), "v6", "obj"),
"v4range": ObjType(_("v4range"), "v4range", "obj"),
"v6range": ObjType(_("v6range"), "v6range", "obj"),
}
directives = {
"v4range": IPv4Range,
"v6range": IPv6Range,
}
roles = {
"v4": IPXRefRole("v4", _("IPv4 address")),
"v6": IPXRefRole("v6", _("IPv6 address")),
"v4range": IPXRefRole("v4range", _("IPv4 range")),
"v6range": IPXRefRole("v6range", _("IPv6 range")),
}
initial_data = {
"v4": {},
"v6": {},
"v4range": {},
"v6range": {},
"ips": [],
}
def clear_doc(self, docname):
to_remove = []
for key, value in self.data["v4range"].items():
if docname == value[0]:
to_remove.append(key)
for key in to_remove:
del self.data["v4range"][key]
to_remove = []
for key, value in self.data["v6range"].items():
if docname == value[0]:
to_remove.append(key)
for key in to_remove:
del self.data["v6range"][key]
self.data["ips"] = [
item for item in self.data["ips"] if item["docname"] != docname
]
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
key = ip_object_anchor(typ, target)
try:
info = self.data[typ][key]
except KeyError:
text = contnode.rawsource
role = self.roles.get(typ)
if role is None:
return None
resnode = role.result_nodes(env.get_doctree(fromdocname), env, node, True)[
0
][2]
if isinstance(resnode, addnodes.pending_xref):
text = node[0][0]
reporter = env.get_doctree(fromdocname).reporter
reporter.warning(
"Cannot resolve reference to %r" % text, line=node.line
)
return node.children
return resnode
else:
title = typ.upper() + " " + target
return make_refnode(builder, fromdocname, info[0], key, contnode, title)
@property
def items(self):
return dict((key, self.data[key]) for key in self.object_types)
def get_objects(self):
for typ, items in self.items.items():
for path, info in items.items():
anchor = ip_object_anchor(typ, path)
yield (path, path, typ, info[0], anchor, 1)
def process_ips(app, doctree):
env = app.builder.env
domaindata = env.domaindata[IPDomain.name]
for node in doctree.traverse(ip_node):
ip = node.astext()
domaindata["ips"].append(
{
"docname": env.docname,
"source": node.parent.source or env.doc2path(env.docname),
"lineno": node.parent.line,
"ip": ip,
"typ": node.parent["typ"],
}
)
replacement = nodes.literal(ip, ip)
node.replace_self(replacement)
def sort_ip(item):
return Network(item).ip
def create_table_row(rowdata):
row = nodes.row()
for cell in rowdata:
entry = nodes.entry()
row += entry
entry += cell
return row
def process_ip_nodes(app, doctree, fromdocname):
env = app.builder.env
domaindata = env.domaindata[IPDomain.name]
header = (_("IP address"), _("Used by"))
colwidths = (1, 3)
for node in doctree.traverse(ip_range):
content = []
net = Network(node["rangespec"])
ips = {}
for key, value in [(ip_info["ip"], ip_info) for ip_info in domaindata["ips"]]:
try:
if not key in net:
continue
except ValueError as e:
logger.info("invalid IP address info %s", e.args)
continue
addrlist = ips.get(key, [])
addrlist.append(value)
ips[key] = addrlist
if ips:
table = nodes.table()
tgroup = nodes.tgroup(cols=len(header))
table += tgroup
for colwidth in colwidths:
tgroup += nodes.colspec(colwidth=colwidth)
thead = nodes.thead()
tgroup += thead
thead += create_table_row([nodes.paragraph(text=label) for label in header])
tbody = nodes.tbody()
tgroup += tbody
for ip, ip_info in [(ip, ips[ip]) for ip in sorted(ips, key=sort_ip)]:
para = nodes.paragraph()
para += nodes.literal("", ip)
refnode = nodes.paragraph()
refuris = set()
refnodes = []
for item in ip_info:
ids = ip_object_anchor(item["typ"], item["ip"])
if ids not in para["ids"]:
para["ids"].append(ids)
domaindata[item["typ"]][ids] = (fromdocname, "")
newnode = nodes.reference("", "", internal=True)
try:
newnode["refuri"] = app.builder.get_relative_uri(
fromdocname, item["docname"]
)
if newnode["refuri"] in refuris:
continue
refuris.add(newnode["refuri"])
except NoUri:
pass
title = env.titles[item["docname"]]
innernode = nodes.Text(title.astext())
newnode.append(innernode)
refnodes.append(newnode)
for count in range(len(refnodes)):
refnode.append(refnodes[count])
if count < len(refnodes) - 1:
refnode.append(nodes.Text(", "))
tbody += create_table_row([para, refnode])
content.append(table)
else:
para = nodes.paragraph(_("No IP addresses in this range"))
content.append(para)
node.replace_self(content)
def setup(app):
app.add_domain(IPDomain)
app.connect("doctree-read", process_ips)
app.connect("doctree-resolved", process_ip_nodes)
return {"version": __version__}

View file

@ -0,0 +1,438 @@
# -*- coding: utf-8 -*-
"""
jandd.sphinxext.ip
~~~~~~~~~~~~~~~~~~
The IP domain.
:copyright: Copyright (c) Jan Dittberner
:license: GPLv3+, see COPYING file for details.
"""
__version__ = "0.6.1"
import ipaddress
from collections import defaultdict
from typing import Iterable, List, Optional, Tuple, Any, cast
from docutils import nodes
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
from sphinx.domains import Domain, ObjType
from sphinx.environment import BuildEnvironment
from sphinx.errors import NoUri
from sphinx.locale import _
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode
logger = logging.getLogger(__name__)
class ip_range(nodes.General, nodes.Element):
pass
class IPRangeDirective(ObjectDescription):
"""A custom directive that describes an IP address range."""
has_content = True
required_arguments = 1
title_prefix: str = ""
range_spec: str = ""
def get_title_prefix(self) -> str:
return self.title_prefix
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, content_node: addnodes.desc_content) -> None:
ip_range_node = ip_range()
ip_range_node["range_spec"] = self.range_spec
content_node += ip_range_node
class IPV4RangeDirective(IPRangeDirective):
title_prefix = _("IPv4 range")
def add_target_and_index(self, name, sig: str, sig_node: desc_signature) -> None:
anchor = "ip-ipv4range-{0}".format(sig)
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"] = [
("single", idx_text, anchor, "", None),
]
class IPV6RangeDirective(IPRangeDirective):
title_prefix = _("IPv6 range")
def add_target_and_index(self, name, sig: str, signode: desc_signature) -> None:
anchor = "ip-ipv6range-{0}".format(sig)
signode["ids"].append(anchor)
ips = cast(IPDomain, self.env.get_domain("ip"))
ips.add_ip6_range(sig)
idx_text = "{}; {}".format(self.title_prefix, name)
self.indexnode["entries"] = [
("single", idx_text, anchor, "", None),
]
class IPXRefRole(XRefRole):
def __init__(self, index_type):
self.index_type = index_type
super().__init__()
def process_link(
self,
env: BuildEnvironment,
refnode: nodes.Element,
has_explicit_title: bool,
title: str,
target: str,
) -> Tuple[str, str]:
refnode.attributes.update(env.ref_context)
ips = cast(IPDomain, env.get_domain("ip"))
if refnode["reftype"] == "v4":
ips.add_ip4_address_reference(target)
elif refnode["reftype"] == "v6":
ips.add_ip6_address_reference(target)
elif refnode["reftype"] == "v4range":
ips.add_ip4_range_reference(target)
elif refnode["reftype"] == "v6range":
ips.add_ip6_range_reference(target)
return title, target
def result_nodes(
self,
document: nodes.document,
env: BuildEnvironment,
node: nodes.Element,
is_ref: bool,
) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
node_list, message = super().result_nodes(document, env, node, is_ref)
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 (
self.reftype in ["v4range", "v6range"]
and self.target not in ip.data["ranges"]
):
return node_list, message
index_node = addnodes.index()
target_id = "ip-{}-{}".format(self.reftype, env.new_serialno("index"))
if self.reftype in ["v4", "v6"]:
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.findall(nodes.title)).astext()
node_text = node.astext()
idx_text = "{}; {}".format(node_text, doc_title)
idx_text_2 = "{}; {}".format(self.index_type, node.astext())
index_node["entries"] = [
("single", idx_text, target_id, "", None),
("single", idx_text_2, target_id, "", None),
]
node_list.insert(0, target_node)
node_list.insert(0, index_node)
return node_list, message
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."
object_types = {
"v4": ObjType(_("v4"), "v4", "obj"),
"v6": ObjType(_("v6"), "v6", "obj"),
"v4range": ObjType(_("v4range"), "v4range", "obj"),
"v6range": ObjType(_("v6range"), "v6range", "obj"),
}
directives = {
"v4range": IPV4RangeDirective,
"v6range": IPV6RangeDirective,
}
roles = {
"v4": IPXRefRole("IPv4 address"),
"v6": IPXRefRole("IPv6 address"),
"v4range": IPXRefRole("IPv4 range"),
"v6range": IPXRefRole("IPv6 range"),
}
initial_data = {
"range_nodes": [],
"ip_refs": defaultdict(list),
"range_refs": [],
"ranges": defaultdict(list),
"ip_dict": {},
}
def get_full_qualified_name(self, node: nodes.Element) -> Optional[str]:
return "{}.{}".format("ip", node)
def get_objects(self) -> Iterable[Tuple[str, str, str, str, str, int]]:
for obj in self.data["range_nodes"]:
yield obj
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: str):
logger.debug("add_ip6_range: %s", sig)
self._add_ip_range("v6", sig)
def _add_ip_range(self, family: str, sig: str):
name = "ip{}range.{}".format(family, sig)
anchor = "ip-ip{}range-{}".format(family, sig)
try:
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((new_ip_range, self.env.docname, anchor))
except ValueError as e:
logger.error("invalid ip range address '%s': %s", sig, e)
def add_ip4_address_reference(self, ip_address: str):
logger.debug("add_ip4_address_reference")
self._add_ip_address_reference("v4", ip_address)
def add_ip6_address_reference(self, ip_address: str):
logger.debug("add_ip4_address_reference")
self._add_ip_address_reference("v6", ip_address)
def _add_ip_address_reference(self, family, sig):
try:
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 = 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)
def add_ip4_range_reference(self, ip_range: str):
logger.debug("add_ip4_range_reference")
self._add_ip_range_reference("v4", ip_range)
def add_ip6_range_reference(self, ip_range: str):
logger.debug("add_ip6_address_reference")
self._add_ip_range_reference("v6", ip_range)
def _add_ip_range_reference(self, family, sig):
name = "iprange{}.{}".format(family, sig)
anchor = "ip-iprange{}-{}".format(family, sig)
try:
ip_range = ipaddress.ip_network(sig)
self.data["range_refs"].append(
(
name,
sig,
"IP{} range".format(family),
str(ip_range),
self.env.docname,
anchor,
0,
)
)
except ValueError as e:
logger.error("invalid ip range '%s': %s", sig, e)
def resolve_xref(
self,
env: BuildEnvironment,
fromdocname: str,
builder: Builder,
typ: str,
target: str,
node: pending_xref,
contnode: nodes.Element,
) -> Optional[nodes.Element]:
match = []
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.compressed)),
)
if typ in ("v4", "v6"):
# reference to IP range
if target not in self.data["ip_dict"]:
# invalid ip address
raise NoUri(target)
match = [
address_tuple(docname, anchor, ip_range)
for ip_range, docname, anchor in [
r
for range_nodes in self.data["ranges"].values()
for r in range_nodes
]
if self.data["ip_dict"][target] in ip_range
]
elif typ in ("v4range", "v6range"):
if target in self.data["ranges"]:
match = [
address_tuple(docname, anchor, ip_range)
for ip_range, docname, anchor in [
range_nodes for range_nodes in self.data["ranges"][target]
]
]
if len(match) > 0:
todocname = match[0][0]
targ = match[0][1]
title = match[0][2]
return make_refnode(builder, fromdocname, todocname, targ, contnode, title)
else:
logger.error("found no link target for %s", target)
return None
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.findall(ip_range):
content = []
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, to_doc_name, anchor in refs:
if ip_address in net:
addresses[ip_address_sig].append((ip_address, to_doc_name, anchor))
logger.debug(
"found %s in network %s on %s",
ip_address_sig,
net.compressed,
to_doc_name,
)
if addresses:
table = nodes.table()
table.attributes["classes"].append("indextable")
table.attributes["align"] = "left"
tgroup = nodes.tgroup(cols=len(header))
table += tgroup
for column_width in column_widths:
tgroup += nodes.colspec(colwidth=column_width)
thead = nodes.thead()
tgroup += thead
thead += create_table_row([nodes.paragraph(text=label) for label in header])
tbody = nodes.tbody()
tgroup += tbody
for ip_address_sig, ip_info in [
(key, addresses[key]) for key in sorted(addresses, key=sort_by_ip)
]:
para = nodes.paragraph()
para += nodes.literal("", ip_info[0][0].compressed)
ref_node = nodes.paragraph()
ref_nodes = []
referenced_docs = set()
for item in ip_info:
ip_address, to_doc_name, anchor = item
if to_doc_name in referenced_docs:
continue
referenced_docs.add(to_doc_name)
title = env.titles[to_doc_name]
inner_node = nodes.Text(title.astext())
new_node = make_refnode(
app.builder,
fromdocname,
to_doc_name,
anchor,
inner_node,
title.astext(),
)
ref_nodes.append(new_node)
for count in range(len(ref_nodes)):
ref_node.append(ref_nodes[count])
if count < len(ref_nodes) - 1:
ref_node.append(nodes.Text(", "))
tbody += create_table_row([para, ref_node])
content.append(table)
else:
para = nodes.paragraph(_("No IP addresses in this range"))
content.append(para)
node.replace_self(content)
def create_table_row(rowdata):
row = nodes.row()
for cell in rowdata:
entry = nodes.entry()
row += entry
entry += cell
return row
def sort_by_ip(item):
return ipaddress.ip_address(item)
def setup(app: Sphinx):
app.add_domain(IPDomain)
app.connect("doctree-resolved", process_ip_nodes)
return {
"version": __version__,
"env_version": 4,
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View file

@ -15,8 +15,9 @@ author = Jan Dittberner
author_email = jan@dittberner.info
keywords = sphinx, extension, IP
license = GPLv3+
license_file = COPYING
license_files = COPYING
platforms = any
version = 0.6.1
classifiers =
Development Status :: 5 - Production/Stable
Framework :: Sphinx :: Extension
@ -28,10 +29,6 @@ classifiers =
[options]
zip_safe = False
include_package_data = True
packages = find:
packages = find_namespace:
install_requires =
Sphinx >= 3
ipcalc >= 1.99
namespace_packages =
jandd
jandd.sphinxext
Sphinx >= 5

3
setup.py Normal file
View file

@ -0,0 +1,3 @@
from setuptools import setup
setup()

View file

@ -12,14 +12,14 @@
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# import sys
# import os
import os
import sys
from typing import Dict
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(os.path.join("..", "..")))
# -- General configuration ------------------------------------------------
@ -40,14 +40,14 @@ templates_path = ["_templates"]
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-ip_range'
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "Sphinxext IP Tests"
copyright = "2016, Jan Dittberner"
copyright = "2016-2023, Jan Dittberner"
author = "Jan Dittberner"
# The version info for the project you're documenting, acts as replacement for
@ -55,9 +55,9 @@ author = "Jan Dittberner"
# built documents.
#
# The short X.Y version.
version = "0.1.0"
version = "0.6.1"
# The full version, including alpha/beta/rc tags.
release = "0.1.0"
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -104,7 +104,6 @@ pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
@ -207,15 +206,15 @@ htmlhelp_basename = "SphinxextIPTestsdoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
latex_elements: Dict[str, str] = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# 'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples

View file

@ -1,4 +1,5 @@
Test page 3
===========
This page contains :ip:v6:`2001:dead:beef::1` like :doc:`testpage2` does.
This page contains :ip:v6:`2001:dead:beef::1` from :ip:v6range:`2001:dead:beef::/64`
like :doc:`testpage2` does.

View file

@ -31,7 +31,7 @@ def run(extra_args=[]):
"The sphinx package is needed to run the jandd.sphinxext.ip " "test suite."
)
from .test_ip import TestIPExtension
from tests.test_ip import TestIPExtension
print("Running jandd.sphinxext.ip test suite ...")

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import shutil
import unittest
from io import StringIO
@ -23,16 +23,16 @@ class TestIPExtension(unittest.TestCase):
def tearDown(self):
self.app.cleanup()
(test_root / "_build").rmtree(True)
shutil.rmtree((test_root / "_build"))
def test_ip_domaindata(self):
self.assertIn("ip", self.app.env.domaindata)
ipdomdata = self.app.env.domaindata["ip"]
self.assertIn("v4", ipdomdata)
self.assertIn("v6", ipdomdata)
self.assertIn("v4range", ipdomdata)
self.assertIn("v6range", ipdomdata)
self.assertIn("ips", ipdomdata)
self.assertIn("ip_refs", ipdomdata)
self.assertIn("range_refs", ipdomdata)
self.assertIn("range_nodes", ipdomdata)
self.assertIn("ranges", ipdomdata)
self.assertIn("ip_dict", ipdomdata)
def find_in_index(self, entry):
indexentries = self.app.env.get_domain("index").entries
@ -43,19 +43,15 @@ class TestIPExtension(unittest.TestCase):
self.fail("%s not found in index" % entry)
def test_ip4_addresses(self):
ipv4 = self.app.env.domaindata["ip"]["v4"]
ips = self.app.env.domaindata["ip"]["ips"]
ips = self.app.env.domaindata["ip"]["ip_refs"]
for ip in IP4_ADDRESSES:
self.assertIn(ip, ipv4)
self.assertIn(ip, [item["ip"] for item in ips])
self.assertIn(ip, ips)
self.find_in_index("IPv4 address; %s" % ip)
self.find_in_index("%s; Test page 2" % ip)
def test_ip6_addresses(self):
ipv6 = self.app.env.domaindata["ip"]["v6"]
ips = self.app.env.domaindata["ip"]["ips"]
ips = self.app.env.domaindata["ip"]["ip_refs"]
for ip in IP6_ADDRESSES:
self.assertIn(ip, ipv6)
self.assertIn(ip, [item["ip"] for item in ips])
self.assertIn(ip, ips)
self.find_in_index("IPv6 address; %s" % ip)
self.find_in_index("%s; Test page 2" % ip)

View file

@ -12,8 +12,8 @@ import shutil
import sys
import tempfile
from functools import wraps
from pathlib import Path
from path import Path
from sphinx import application
__all__ = [
@ -25,14 +25,13 @@ __all__ = [
"SphinxTestApplication",
"with_app",
"gen_with_app",
"Path",
"with_tempdir",
"write_file",
"sprint",
]
test_root = Path(__file__).parent.joinpath("root").abspath()
test_root = Path(__file__).parent.joinpath("root").absolute()
def _excstr(exc):
@ -96,9 +95,9 @@ class SphinxTestApplication(application.Sphinx):
def __init__(
self,
srcdir=None,
src_dir=None,
confdir=None,
outdir=None,
out_dir=None,
doctreedir=None,
buildername="html",
confoverrides=None,
@ -115,26 +114,26 @@ class SphinxTestApplication(application.Sphinx):
self.cleanup_trees = [test_root / "generated"]
if srcdir is None:
srcdir = test_root
if srcdir == "(temp)":
if src_dir is None:
src_dir = test_root
if src_dir == "(temp)":
tempdir = Path(tempfile.mkdtemp())
self.cleanup_trees.append(tempdir)
temproot = tempdir / "root"
test_root.copytree(temproot)
srcdir = temproot
temp_root = tempdir / "root"
shutil.copytree(test_root.resolve(), temp_root.resolve())
src_dir = temp_root
else:
srcdir = Path(srcdir)
self.builddir = srcdir.joinpath("_build")
src_dir = Path(src_dir)
self.builddir = src_dir.joinpath("_build")
if confdir is None:
confdir = srcdir
if outdir is None:
outdir = srcdir.joinpath(self.builddir, buildername)
if not outdir.isdir():
outdir.makedirs()
self.cleanup_trees.insert(0, outdir)
confdir = src_dir
if out_dir is None:
out_dir = src_dir.joinpath(self.builddir, buildername)
if not out_dir.is_dir():
out_dir.mkdir(parents=True)
self.cleanup_trees.insert(0, out_dir)
if doctreedir is None:
doctreedir = srcdir.joinpath(srcdir, self.builddir, "doctrees")
doctreedir = src_dir.joinpath(src_dir, self.builddir, "doctrees")
if cleanenv:
self.cleanup_trees.insert(0, doctreedir)
if confoverrides is None:
@ -150,9 +149,9 @@ class SphinxTestApplication(application.Sphinx):
application.Sphinx.__init__(
self,
srcdir,
str(src_dir),
confdir,
outdir,
out_dir,
doctreedir,
buildername,
confoverrides,
@ -211,7 +210,7 @@ def with_tempdir(func):
def new_func():
tempdir = Path(tempfile.mkdtemp())
func(tempdir)
tempdir.rmtree()
tempdir.rmdir()
new_func.__name__ = func.__name__
return new_func

26
tox.ini Normal file
View file

@ -0,0 +1,26 @@
[tox]
requires =
tox>=4
env_list = lint, type, py{39,310,311,312}
[testenv]
description = run unit tests
deps =
pytest>=7
pytest-sugar
commands =
pytest {posargs:tests}
[testenv:lint]
description = run linters
skip_install = true
deps =
black==22.12
commands = black {posargs:.}
[testenv:type]
description = run type checks
deps =
mypy>=0.991
commands =
mypy {posargs:--install-types --non-interactive jandd tests}