Compare commits

...

6 commits
0.5.1 ... 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
14 changed files with 863 additions and 485 deletions

2
.gitignore vendored
View file

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

View file

@ -1,6 +1,19 @@
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
------------------

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

@ -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

@ -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

@ -5,20 +5,22 @@
The IP domain.
:copyright: Copyright (c) 2016-2021 Jan Dittberner
:copyright: Copyright (c) Jan Dittberner
:license: GPLv3+, see COPYING file for details.
"""
__version__ = "0.5.1"
__version__ = "0.6.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.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.environment import BuildEnvironment
from sphinx.errors import NoUri
@ -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, 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"] = [
@ -75,10 +77,10 @@ class IPV4RangeDirective(IPRangeDirective):
class IPV6RangeDirective(IPRangeDirective):
title_prefix = _("IPv6 range")
def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None:
def add_target_and_index(self, name, 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,9 +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.5.1
version = 0.6.1
classifiers =
Development Status :: 5 - Production/Stable
Framework :: Sphinx :: Extension
@ -29,10 +29,6 @@ classifiers =
[options]
zip_safe = False
include_package_data = True
packages = find:
packages = find_namespace:
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
@ -55,7 +55,7 @@ author = "Jan Dittberner"
# built documents.
#
# The short X.Y version.
version = "0.5.1"
version = "0.6.1"
# The full version, including alpha/beta/rc tags.
release = version
@ -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,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}