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

2
.gitignore vendored
View file

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

View file

@ -5,6 +5,9 @@ unreleased
----------
* 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
------------------

View file

@ -11,7 +11,7 @@ Running test
To install all dependencies and run the tests use::
pipenv install --dev
pipenv run pytest
pipenv run tox
Release a new version
---------------------
@ -31,7 +31,7 @@ Start by deciding the new release number and perform the following steps:
shortlog <previous_tag>..HEAD`` could help to create a good release tag
message) ::
git tag -s -a 0.5.1
git tag -s -a <version>
* build the release artifacts ::

View file

@ -6,12 +6,11 @@ verify_ssl = true
[dev-packages]
coverage = "*"
twine = "*"
path = "*"
pytest = "*"
tox = "*"
black = "*"
[packages]
jandd-sphinxext-ip = { path = ".", editable = true }
Sphinx = ">=4"
Sphinx = ">=5"
docutils = "*"
six = "*"
ipcalc = ">=1.99"

1027
Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

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

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 {

View file

@ -15,7 +15,7 @@ author = Jan Dittberner
author_email = jan@dittberner.info
keywords = sphinx, extension, IP
license = GPLv3+
license_file = COPYING
license_files = COPYING
platforms = any
version = 0.5.1
classifiers =
@ -31,8 +31,4 @@ zip_safe = False
include_package_data = True
packages = find:
install_requires =
Sphinx >= 4
ipcalc >= 1.99
namespace_packages =
jandd
jandd.sphinxext
Sphinx >= 5

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 ------------------------------------------------
@ -47,7 +47,7 @@ master_doc = "index"
# General information about the project.
project = "Sphinxext IP Tests"
copyright = "2016-2021, Jan Dittberner"
copyright = "2016-2023, Jan Dittberner"
author = "Jan Dittberner"
# The version info for the project you're documenting, acts as replacement for
@ -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,7 +206,7 @@ htmlhelp_basename = "SphinxextIPTestsdoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
latex_elements: Dict[str, str] = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import shutil
import unittest
from io import StringIO
@ -23,7 +23,7 @@ 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)

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}