From aeb2fd948193eec3c0c2633e52e5cb1eb4beab6d Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 26 May 2025 11:19:14 -0400
Subject: [PATCH 001/122] Change version to next .dev0
---
src/sphobjinv/version.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/sphobjinv/version.py b/src/sphobjinv/version.py
index 89cb6f4f..3840d8c1 100644
--- a/src/sphobjinv/version.py
+++ b/src/sphobjinv/version.py
@@ -29,4 +29,4 @@
"""
-__version__ = "2.3.1.3"
+__version__ = "2.3.2.dev0"
From e14c8a354379aa894659189397e6105490581ee1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Thu, 29 May 2025 23:42:09 -0400
Subject: [PATCH 002/122] Fix GitHub badge image link to an existing workflow
file
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d95e84eb..f89e1025 100644
--- a/README.md
+++ b/README.md
@@ -204,5 +204,5 @@ under a [Creative Commons Attribution 4.0 International License][cc-by 4.0]
[python versions badge]: https://img.shields.io/pypi/pyversions/sphobjinv.svg?logo=python
[readthedocs badge]: https://img.shields.io/readthedocs/sphobjinv/latest.svg
[readthedocs link target]: http://sphobjinv.readthedocs.io/en/latest/
-[workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/sphobjinv/ci_tests.yml?logo=github&branch=main
+[workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/sphobjinv/all_core_tests.yml?logo=github&branch=main
[workflow link target]: https://github.com/bskinn/sphobjinv/actions
From 115cbb9270a869693af5e5ea35141b793892a908 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Thu, 29 May 2025 23:48:17 -0400
Subject: [PATCH 003/122] Run core tests on pushes to main as well as PRs
---
.github/workflows/all_core_tests.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml
index fc09a4f5..129aef0c 100644
--- a/.github/workflows/all_core_tests.yml
+++ b/.github/workflows/all_core_tests.yml
@@ -1,7 +1,10 @@
name: 'ALL: Run tests on Python 3.12'
on:
- - pull_request
+ pull_request:
+ push:
+ branches:
+ - main
jobs:
current_python_tests:
From 7d52752084b43ba8f40d5712e6feeddaa46667a2 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Fri, 30 May 2025 23:52:52 -0400
Subject: [PATCH 004/122] Update CHANGELOG
---
CHANGELOG.md | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 801bfa00..e7e3b4ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,17 @@ changes.
### *Unreleased*
-...
+#### Internal
+
+ * Add `push` trigger for `all_core_tests.yml` workflow for `main` branch
+ ([#320]).
+ * This will provide `main` branch CI results for this workflow, for the
+ GitHub badge to report.
+
+#### Administrative
+
+ * Update the GitHub badge to point to the new `all_core_tests.yml` workflow
+ ([#320]) instead of the now-removed `ci_tests.yml`.
### [2.3.1.3] - 2025-05-26
@@ -680,3 +690,4 @@ changes.
[#306]: https://github.com/bskinn/sphobjinv/pull/306
[#315]: https://github.com/bskinn/sphobjinv/pull/315
[#316]: https://github.com/bskinn/sphobjinv/pull/316
+[#320]: https://github.com/bskinn/sphobjinv/pull/320
From fe832fdd263ac477a7d690e5f04ef472517fe998 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Thu, 29 Aug 2024 00:47:08 -0400
Subject: [PATCH 005/122] Add initial code and inv
---
pyproject.toml | 6 ++++--
src/sphobjinv/cli/core.py | 28 ++++++++++++++++++++++++++-
src/sphobjinv/cli/parser.py | 29 ++++++++++++++++++++++++++++
tests/resource/objects_textconv.inv | Bin 0 -> 221 bytes
4 files changed, 60 insertions(+), 3 deletions(-)
create mode 100644 tests/resource/objects_textconv.inv
diff --git a/pyproject.toml b/pyproject.toml
index 5f35b59b..3c9c6c33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,14 +43,16 @@ dependencies = [
dynamic = ["version", "readme"]
[project.urls]
-Homepage = "https://github.com/bskinn/sphobjinv"
Changelog = "https://github.com/bskinn/sphobjinv/blob/main/CHANGELOG.md"
Docs = "https://sphobjinv.readthedocs.io/en/stable/"
-Thank = "https://fosstodon.org/@btskinn"
Donate = "https://github.com/sponsors/bskinn"
+Homepage = "https://github.com/bskinn/sphobjinv"
+Thank = "https://fosstodon.org/@btskinn"
[project.scripts]
sphobjinv = "sphobjinv.cli.core:main"
+sphobjinv-textconv = "sphobjinv.cli.core:main_textconv"
+
[tool.setuptools]
package-dir = {"" = "src"}
diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py
index 33e1c1d2..096b703b 100644
--- a/src/sphobjinv/cli/core.py
+++ b/src/sphobjinv/cli/core.py
@@ -33,7 +33,7 @@
from sphobjinv.cli.convert import do_convert
from sphobjinv.cli.load import inv_local, inv_stdin, inv_url
-from sphobjinv.cli.parser import getparser, PrsConst
+from sphobjinv.cli.parser import getparser, getparser_textconv, PrsConst
from sphobjinv.cli.suggest import do_suggest
from sphobjinv.cli.ui import print_stderr
@@ -101,3 +101,29 @@ def main():
# Clean exit
sys.exit(0)
+
+
+def main_textconv():
+ """Entrypoint for textconv operation."""
+ # If no args passed, stick in '-h'
+ if len(sys.argv) == 1:
+ sys.argv.append("-h")
+
+ prs = getparser_textconv()
+ params = vars(prs.parse_args())
+
+ # Print version &c. and exit if indicated
+ if params[PrsConst.VERSION]:
+ print(PrsConst.VER_TXT)
+ sys.exit(0)
+
+ inv, in_path = inv_local(params)
+
+ params[PrsConst.CONTRACT] = False
+ params[PrsConst.EXPAND] = False
+ params[PrsConst.MODE] = PrsConst.PLAIN
+ params[PrsConst.OUTFILE] = "-"
+
+ do_convert(inv, in_path, params)
+
+ sys.exit(0)
diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py
index edc5ed16..fc6f9fbf 100644
--- a/src/sphobjinv/cli/parser.py
+++ b/src/sphobjinv/cli/parser.py
@@ -381,3 +381,32 @@ def getparser():
)
return prs
+
+
+def getparser_textconv():
+ """Generate argument parser for textconv entrypoint.
+
+ Returns
+ -------
+ prs
+
+ :class:`~argparse.ArgumentParser` -- Parser for textconv commandline
+ usage of |soi|
+
+ """
+ prs = ap.ArgumentParser(
+ description="Text diffing of intersphinx 'objects.inv' files."
+ )
+ prs.add_argument(
+ "-" + PrsConst.VERSION[0],
+ "--" + PrsConst.VERSION,
+ help="Print package version & other info",
+ action="store_true",
+ )
+
+ prs.add_argument(
+ PrsConst.INFILE,
+ help=("Path to file to be converted."),
+ )
+
+ return prs
diff --git a/tests/resource/objects_textconv.inv b/tests/resource/objects_textconv.inv
new file mode 100644
index 0000000000000000000000000000000000000000..31805f5d784d5837d758067678668bcbc12d6e90
GIT binary patch
literal 221
zcmY#Z2rkIT%&Sny%qvUHE6FdaR47X=D$dN$Q!wIERtPA{&q_@$u~H~Wttd&(&nwd{
zNi8k`N`yfaSt%Il83VZ?8L0|Iskw=nc`2zy3i)XYB^jB;3Tc@+sR}?kIX}0cD7Cma
zHASJcI5RI@p(-acNsp`ImbTuB6aJ@8ozcGReeUEH?{nw1y>+z$HCAjnb=F_=tY@e}
z07KX7^k-sEmONYbY^ut%lxNRhI!}F>I_1eT4QG*8t2AB|i6$HS=&RbxoxHQ<(w>wF
Sid=0hzHE}53?KGcn*ab+;#vFv
literal 0
HcmV?d00001
From 3557bd3f7c6edc5b750bf5ce5737a25754c5303a Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Thu, 29 Aug 2024 22:47:05 -0400
Subject: [PATCH 006/122] Add .gitattributes line for configuring textconv
The matching revision to .git/config was:
[diff "objects_inv"]
textconv = sh -c 'sphobjinv co plain "$0" -'
---
.gitattributes | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitattributes b/.gitattributes
index c6f20dbf..2a233757 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,3 @@
tests/resource/objects_mkdoc_zlib0.inv binary
tests/resource/objects_attrs.txt binary
+*.inv diff=objects_inv
From 419f68459ac7392edf781a0c1dab914c380dca88 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Thu, 29 Aug 2024 22:48:21 -0400
Subject: [PATCH 007/122] Commit small change to objects_textconv.inv
---
tests/resource/objects_textconv.inv | Bin 221 -> 226 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/tests/resource/objects_textconv.inv b/tests/resource/objects_textconv.inv
index 31805f5d784d5837d758067678668bcbc12d6e90..19a43231c0677566353a61b926dc82b0fa2bb278 100644
GIT binary patch
delta 86
zcmV-c0IC1o0pbCWlTR@+Rw$^n%FRzH%}G@-Pyov4XXX~PbtkwRWMKhitA_Q7UZPnrskC-mSpDV=|h$2Wt8OR
nC_@zLswn7k6=O3@FC{-7$=tNkykw9e3Wfwt1{nze@V+}VfO{q!
From e7e84c3a4b0478b9a8b49e82539588140a0c40a6 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 22 Dec 2025 00:18:21 -0500
Subject: [PATCH 008/122] Tweak the -textconv CLI help text
---
src/sphobjinv/cli/parser.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py
index fc6f9fbf..eda56f3f 100644
--- a/src/sphobjinv/cli/parser.py
+++ b/src/sphobjinv/cli/parser.py
@@ -395,7 +395,9 @@ def getparser_textconv():
"""
prs = ap.ArgumentParser(
- description="Text diffing of intersphinx 'objects.inv' files."
+ description=(
+ "Emit the plaintext of the local Sphinx inventory at 'infile' to stdout."
+ )
)
prs.add_argument(
"-" + PrsConst.VERSION[0],
@@ -406,7 +408,7 @@ def getparser_textconv():
prs.add_argument(
PrsConst.INFILE,
- help=("Path to file to be converted."),
+ help=("Path to file to be converted"),
)
return prs
From 4498e10c5dda00f1f9e15233145dbf27b7f74e6c Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 22 Dec 2025 00:49:00 -0500
Subject: [PATCH 009/122] Update CLI runner to dispatch to either core or
textconv CLI
---
conftest.py | 17 ++++++++++++-----
tests/enum.py | 39 +++++++++++++++++++++++++++++++++++++++
tox.ini | 2 +-
3 files changed, 52 insertions(+), 6 deletions(-)
create mode 100644 tests/enum.py
diff --git a/conftest.py b/conftest.py
index 3aad2932..1c086931 100644
--- a/conftest.py
+++ b/conftest.py
@@ -45,6 +45,8 @@
from sphinx.util.inventory import InventoryFile as IFile
import sphobjinv as soi
+from sphobjinv.cli.core import main, main_textconv
+from tests.enum import CLICommand
def pytest_addoption(parser):
@@ -233,21 +235,26 @@ def sphinx_version():
@pytest.fixture() # Must be function scope since uses monkeypatch
def run_cmdline_test(monkeypatch):
"""Return function to perform command line exit code test."""
- from sphobjinv.cli.core import main
- def func(arglist, *, expect=0): # , suffix=None):
+ def func(arglist, *, command=CLICommand.Core, expect=0): # , suffix=None):
"""Perform the CLI exit-code test."""
-
# Assemble execution arguments
- runargs = ["sphobjinv"]
+ runargs = [command]
runargs.extend(str(a) for a in arglist)
+ # Select the command function to use
+ match command:
+ case CLICommand.Core:
+ cmd_func = main
+ case CLICommand.Textconv:
+ cmd_func = main_textconv
+
# Mock sys.argv, run main, and restore sys.argv
with monkeypatch.context() as m:
m.setattr(sys, "argv", runargs)
try:
- main()
+ cmd_func()
except SystemExit as e:
retcode = e.args[0]
ok = True
diff --git a/tests/enum.py b/tests/enum.py
new file mode 100644
index 00000000..b3469584
--- /dev/null
+++ b/tests/enum.py
@@ -0,0 +1,39 @@
+r"""*Test enums for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+ Brian Skinn (brian.skinn@gmail.com)
+
+**File Created**
+ 22 Dec 2025
+
+**Copyright**
+ \(c) Brian Skinn 2016-2025
+
+**Source Repository**
+ http://www.github.com/bskinn/sphobjinv
+
+**Documentation**
+ https://sphobjinv.readthedocs.io/en/stable
+
+**License**
+ Code: `MIT License`_
+
+ Docs & Docstrings: |CC BY 4.0|_
+
+ See |license_txt|_ for full license terms.
+
+**Members**
+
+"""
+
+from enum import Enum
+
+
+class CLICommand(str, Enum):
+ """Enumeration of CLI commands."""
+
+ Core = "sphobjinv"
+ Textconv = "sphobjinv-textconv"
diff --git a/tox.ini b/tox.ini
index e7c27a45..0349df3e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -191,4 +191,4 @@ per_file_ignores =
#flake8-import-order
import-order-style = smarkets
-application-import-names = sphobjinv
+application-import-names = sphobjinv,tests
From 19f06fff2feda0a44b8161c494e1cbae58246aa8 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 22 Dec 2025 00:50:19 -0500
Subject: [PATCH 010/122] Add initial testconv test file
Lots to still be fixed/removed
---
tests/test_cli_textconv.py | 427 +++++++++++++++++++++++++++++++++++++
1 file changed, 427 insertions(+)
create mode 100644 tests/test_cli_textconv.py
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
new file mode 100644
index 00000000..fed1c1ba
--- /dev/null
+++ b/tests/test_cli_textconv.py
@@ -0,0 +1,427 @@
+r"""*Textconv CLI tests for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+ Brian Skinn (brian.skinn@gmail.com)
+
+**File Created**
+ 22 Dec 2025
+
+**Copyright**
+ \(c) Brian Skinn 2016-2025
+
+**Source Repository**
+ http://www.github.com/bskinn/sphobjinv
+
+**Documentation**
+ https://sphobjinv.readthedocs.io/en/stable
+
+**License**
+ Code: `MIT License`_
+
+ Docs & Docstrings: |CC BY 4.0|_
+
+ See |license_txt|_ for full license terms.
+
+**Members**
+
+"""
+
+import shlex
+import subprocess as sp # noqa: S404
+from pathlib import Path
+
+import pytest
+from stdio_mgr import stdio_mgr
+
+from sphobjinv import Inventory
+from tests.enum import CLICommand
+
+
+CLI_TEST_TIMEOUT = 2
+CLI_CMDS = ["sphobjinv-textconv"]
+
+pytestmark = [pytest.mark.cli, pytest.mark.local]
+
+
+class TestMisc:
+ """Tests for miscellaneous CLI functions."""
+
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ @pytest.mark.parametrize("cmd", CLI_CMDS)
+ def test_cli_invocations(self, cmd):
+ """Confirm that actual shell invocations do not error."""
+ runargs = shlex.split(cmd)
+ runargs.append("--help")
+
+ out = sp.check_output(" ".join(runargs), shell=True).decode() # noqa: S602
+
+ assert "sphobjinv" in out
+ assert "infile" in out
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_cli_version_exits_ok(self, run_cmdline_test):
+ # """Confirm --version exits cleanly."""
+ # run_cmdline_test(["-v"])
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_cli_noargs_shows_help(self, run_cmdline_test):
+ # """Confirm help shown when invoked with no arguments."""
+ # with stdio_mgr() as (in_, out_, err_):
+ # run_cmdline_test([])
+
+ # assert "usage: sphobjinv" in out_.getvalue()
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_cli_no_subparser_prs_exit(self, run_cmdline_test):
+ # """Confirm exit code 2 if option passed but no subparser provided."""
+ # with stdio_mgr() as (in_, out_, err_):
+ # run_cmdline_test(["--foo"], expect=2)
+
+ # assert "error: No subparser selected" in err_.getvalue()
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_cli_bad_subparser_prs_exit(self, run_cmdline_test):
+ # """Confirm exit code 2 if invalid subparser provided."""
+ # with stdio_mgr() as (in_, out_, err_):
+ # run_cmdline_test(["foo"], expect=2)
+
+ # assert "invalid choice: 'foo'" in err_.getvalue()
+
+
+# class TestConvertGood:
+# """Tests for expected-good convert functionality."""
+
+# @pytest.mark.parametrize(
+# ["out_ext", "cli_arg"],
+# [(".txt", "plain"), (".inv", "zlib"), (".json", "json")],
+# ids=(lambda i: "" if i.startswith(".") else i),
+# )
+# @pytest.mark.parametrize(
+# "in_ext", [".txt", ".inv", ".json"], ids=(lambda i: i.split(".")[-1])
+# )
+# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+# def test_cli_convert_default_outname(
+# self,
+# in_ext,
+# out_ext,
+# cli_arg,
+# scratch_path,
+# run_cmdline_test,
+# decomp_cmp_test,
+# sphinx_load_test,
+# misc_info,
+# ):
+# """Confirm cmdline conversions with only input file arg."""
+# if in_ext == out_ext:
+# pytest.skip("Ignore no-change conversions")
+
+# src_path = scratch_path / (misc_info.FNames.INIT + in_ext)
+# dest_path = scratch_path / (misc_info.FNames.INIT + out_ext)
+
+# assert src_path.is_file()
+# assert dest_path.is_file()
+
+# dest_path.unlink()
+
+# cli_arglist = ["convert", cli_arg, str(src_path)]
+# run_cmdline_test(cli_arglist)
+# assert dest_path.is_file()
+
+# if cli_arg == "zlib":
+# sphinx_load_test(dest_path)
+# if cli_arg == "plain":
+# decomp_cmp_test(dest_path)
+
+# @pytest.mark.timeout(CLI_TEST_TIMEOUT * 2)
+# def test_cli_convert_expandcontract(
+# self, scratch_path, misc_info, run_cmdline_test
+# ):
+# """Confirm cmdline contract decompress of zlib with input file arg."""
+# cmp_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)
+# dec_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.DEC)
+# recmp_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
+
+# run_cmdline_test(["convert", "plain", "-e", str(cmp_path), str(dec_path)])
+# assert dec_path.is_file()
+
+# run_cmdline_test(["convert", "zlib", "-c", str(dec_path), str(recmp_path)])
+# assert recmp_path.is_file()
+
+# @pytest.mark.parametrize(
+# "dst_name", [True, False], ids=(lambda v: "dst_name" if v else "no_dst_name")
+# )
+# @pytest.mark.parametrize(
+# "dst_path", [True, False], ids=(lambda v: "dst_path" if v else "no_dst_path")
+# )
+# @pytest.mark.parametrize(
+# "src_path", [True, False], ids=(lambda v: "src_path" if v else "no_src_path")
+# )
+# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+# def test_cli_convert_various_pathargs(
+# self,
+# src_path,
+# dst_path,
+# dst_name,
+# scratch_path,
+# misc_info,
+# run_cmdline_test,
+# decomp_cmp_test,
+# monkeypatch,
+# ):
+# """Confirm the various src/dest path/file combinations work."""
+# init_dst_fname = misc_info.FNames.INIT + misc_info.Extensions.DEC
+# mod_dst_fname = misc_info.FNames.MOD + misc_info.Extensions.DEC
+
+# src_path = (scratch_path.resolve() if src_path else Path(".")) / (
+# misc_info.FNames.INIT + misc_info.Extensions.CMP
+# )
+# dst_path = (scratch_path.resolve() if dst_path else Path(".")) / (
+# mod_dst_fname if dst_name else ""
+# )
+
+# full_dst_path = scratch_path.resolve() / (
+# mod_dst_fname if dst_name else init_dst_fname
+# )
+
+# assert (scratch_path / init_dst_fname).is_file()
+# (scratch_path / init_dst_fname).unlink()
+
+# with monkeypatch.context() as m:
+# m.chdir(scratch_path)
+# run_cmdline_test(["convert", "plain", str(src_path), str(dst_path)])
+# assert full_dst_path.is_file()
+
+# decomp_cmp_test(full_dst_path)
+
+# @pytest.mark.timeout(CLI_TEST_TIMEOUT * 50 * 3)
+# @pytest.mark.testall
+# def test_cli_convert_cycle_formats(
+# self,
+# testall_inv_path,
+# res_path,
+# scratch_path,
+# run_cmdline_test,
+# misc_info,
+# pytestconfig,
+# check,
+# ):
+# """Confirm conversion in a loop, reading/writing all formats."""
+# res_src_path = res_path / testall_inv_path
+# plain_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.DEC)
+# json_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.JSON)
+# zlib_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
+
+# if (
+# not pytestconfig.getoption("--testall")
+# and testall_inv_path.name != "objects_attrs.inv"
+# ):
+# pytest.skip("'--testall' not specified")
+
+# run_cmdline_test(["convert", "plain", str(res_src_path), str(plain_path)])
+# run_cmdline_test(["convert", "json", str(plain_path), str(json_path)])
+# run_cmdline_test(["convert", "zlib", str(json_path), str(zlib_path)])
+
+# invs = {
+# "orig": Inventory(str(res_src_path)),
+# "plain": Inventory(str(plain_path)),
+# "zlib": Inventory(str(zlib_path)),
+# "json": Inventory(json.loads(json_path.read_text())),
+# }
+
+# for fmt, attrib in product(
+# ("plain", "zlib", "json"),
+# (
+# HeaderFields.Project.value,
+# HeaderFields.Version.value,
+# HeaderFields.Count.value,
+# ),
+# ):
+# check.equal(getattr(invs[fmt], attrib), getattr(invs["orig"], attrib))
+
+# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+# def test_cli_overwrite_prompt_and_behavior(
+# self, res_path, scratch_path, misc_info, run_cmdline_test
+# ):
+# """Confirm overwrite prompt works properly."""
+# src_path_1 = res_path / "objects_attrs.inv"
+# src_path_2 = res_path / "objects_sarge.inv"
+# dst_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.DEC)
+# dst_path.unlink()
+
+# args = ["convert", "plain", None, str(dst_path)]
+
+# # Initial decompress
+# args[2] = str(src_path_1)
+# with stdio_mgr() as (in_, out_, err_):
+# run_cmdline_test(args)
+
+# assert "converted" in err_.getvalue()
+# assert "(plain)" in err_.getvalue()
+
+# # First overwrite, declining clobber
+# args[2] = str(src_path_2)
+# with stdio_mgr("n\n") as (in_, out_, err_):
+# run_cmdline_test(args)
+
+# assert "(Y/N)? n" in out_.getvalue()
+
+# assert "attrs" == Inventory(str(dst_path)).project
+
+# # Second overwrite, with clobber
+# with stdio_mgr("y\n") as (in_, out_, err_):
+# run_cmdline_test(args)
+
+# assert "(Y/N)? y" in out_.getvalue()
+
+# assert "Sarge" == Inventory(str(dst_path)).project
+
+# def test_cli_stdin_clobber(
+# self, res_path, scratch_path, misc_info, run_cmdline_test
+# ):
+# """Confirm clobber with stdin data only with --overwrite."""
+# src_path_sarge = res_path / "objects_sarge.inv"
+# dst_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)
+
+# assert "attrs" == Inventory(dst_path).project
+
+# data = json.dumps(Inventory(src_path_sarge).json_dict())
+
+# args = ["convert", "plain", "-", str(dst_path)]
+# with stdio_mgr(data):
+# run_cmdline_test(args)
+# assert "attrs" == Inventory(dst_path).project
+
+# args.append("-o")
+# with stdio_mgr(data):
+# run_cmdline_test(args)
+# assert "Sarge" == Inventory(dst_path).project
+
+# def test_cli_json_no_metadata_url(
+# self, res_cmp, scratch_path, misc_info, run_cmdline_test
+# ):
+# """Confim JSON generated from local inventory has no url in metadata."""
+# json_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.JSON)
+
+# run_cmdline_test(
+# ["convert", "json", str(res_cmp.resolve()), str(json_path.resolve())]
+# )
+
+# d = json.loads(json_path.read_text())
+
+# assert "url" not in d.get("metadata", {})
+
+# def test_cli_json_export_import(
+# self, res_cmp, scratch_path, misc_info, run_cmdline_test, sphinx_load_test
+# ):
+# """Confirm JSON sent to stdout from local source imports ok."""
+# mod_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
+
+# with stdio_mgr() as (in_, out_, err_):
+# run_cmdline_test(["convert", "json", str(res_cmp.resolve()), "-"])
+
+# data = out_.getvalue()
+
+# with stdio_mgr(data) as (in_, out_, err_):
+# run_cmdline_test(["convert", "zlib", "-", str(mod_path.resolve())])
+
+# assert Inventory(json.loads(data))
+# assert Inventory(mod_path)
+# sphinx_load_test(mod_path)
+
+
+class TestFail:
+ """Tests for expected-fail behaviors."""
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_clifail_convert_wrongfiletype(
+ # self, scratch_path, run_cmdline_test, monkeypatch
+ # ):
+ # """Confirm exit code 1 with invalid file format."""
+ # monkeypatch.chdir(scratch_path)
+ # fname = "testfile"
+ # Path(fname).write_bytes(b"this is not objects.inv\n")
+
+ # with stdio_mgr() as (in_, out_, err_):
+ # run_cmdline_test(["convert", "plain", fname], expect=1)
+ # assert "Unrecognized" in err_.getvalue()
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_clifail_convert_missingfile(self, run_cmdline_test):
+ # """Confirm exit code 1 with nonexistent file specified."""
+ # run_cmdline_test(["convert", "plain", "thisfileshouldbeabsent.txt"], expect=1)
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_clifail_convert_badoutfilename(
+ # self, scratch_path, run_cmdline_test, misc_info
+ # ):
+ # """Confirm exit code 1 with invalid output file name."""
+ # run_cmdline_test(
+ # [
+ # "convert",
+ # "plain",
+ # str(scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)),
+ # misc_info.invalid_filename,
+ # ],
+ # expect=1,
+ # )
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_clifail_convert_badoutputdir(
+ # self, res_cmp, scratch_path, run_cmdline_test
+ # ):
+ # """Confirm exit code 1 when output location can't be created."""
+ # run_cmdline_test(
+ # [
+ # "convert",
+ # "plain",
+ # res_cmp,
+ # str(scratch_path / "nonexistent" / "folder" / "obj.txt"),
+ # ],
+ # expect=1,
+ # )
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_clifail_convert_pathonlysrc(self, scratch_path, run_cmdline_test):
+ # """Confirm cmdline plaintext convert with input directory arg fails."""
+ # run_cmdline_test(["convert", "plain", str(scratch_path)], expect=1)
+
+ # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ # def test_clifail_convert_localfile_as_url(
+ # self, scratch_path, misc_info, run_cmdline_test, check
+ # ):
+ # """Confirm error when using URL mode on local file."""
+ # in_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)
+
+ # (scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.DEC)).unlink()
+
+ # with check(msg="path-style"):
+ # run_cmdline_test(["convert", "plain", "-u", str(in_path)], expect=1)
+
+ # with check(msg="url-style"):
+ # file_url = "file:///" + str(in_path.resolve())
+ # run_cmdline_test(["convert", "plain", "-u", file_url], expect=1)
+
+ # def test_clifail_no_url_with_stdin(self, run_cmdline_test):
+ # """Confirm parser exit when -u passed with "-" infile."""
+ # with stdio_mgr() as (in_, out_, err_):
+ # run_cmdline_test(["convert", "plain", "-u", "-"], expect=2)
+ # assert "--url not allowed" in err_.getvalue()
+
+
+class TestStdio:
+ """Tests for the stdin/stdout functionality."""
+
+ def test_cli_stdio_output(self, res_cmp, run_cmdline_test):
+ """Confirm that inventory data can be written to stdout."""
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test([str(res_cmp.resolve())], command=CLICommand.Textconv)
+
+ result = out_.getvalue()
+
+ inv1 = Inventory(res_cmp)
+ inv2 = Inventory(result.encode("utf-8"))
+
+ assert inv1 == inv2
From d3a70fc569afb62c45fe397083c7f290418925b9 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 09:52:00 -0500
Subject: [PATCH 011/122] Add 'textconv' pytest marker
---
tests/test_cli_textconv.py | 2 +-
tox.ini | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index fed1c1ba..f4380769 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -43,7 +43,7 @@
CLI_TEST_TIMEOUT = 2
CLI_CMDS = ["sphobjinv-textconv"]
-pytestmark = [pytest.mark.cli, pytest.mark.local]
+pytestmark = [pytest.mark.cli, pytest.mark.textconv, pytest.mark.local]
class TestMisc:
diff --git a/tox.ini b/tox.ini
index 0349df3e..23b95a98 100644
--- a/tox.ini
+++ b/tox.ini
@@ -137,6 +137,7 @@ markers =
nonloc: Tests requiring Internet access
cli: Command-line interface tests
api: Direct API tests
+ textconv: Textconv CLI tests
intersphinx: Tests on intersphinx-related functionality
fixture: Trivial tests for test suite fixtures
testall: Tests that use *all* objects_xyz.inv files in tests/resource, if --testall is specified
From 71b51f610ebb8911c1c29ec7a2e484d166764614 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 09:52:21 -0500
Subject: [PATCH 012/122] Finish TestFail class for textconv CLI
---
tests/test_cli_textconv.py | 118 +++++++++++++++----------------------
1 file changed, 47 insertions(+), 71 deletions(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index f4380769..87aac3f1 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -335,80 +335,56 @@ def test_cli_invocations(self, cmd):
class TestFail:
"""Tests for expected-fail behaviors."""
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_clifail_convert_wrongfiletype(
- # self, scratch_path, run_cmdline_test, monkeypatch
- # ):
- # """Confirm exit code 1 with invalid file format."""
- # monkeypatch.chdir(scratch_path)
- # fname = "testfile"
- # Path(fname).write_bytes(b"this is not objects.inv\n")
-
- # with stdio_mgr() as (in_, out_, err_):
- # run_cmdline_test(["convert", "plain", fname], expect=1)
- # assert "Unrecognized" in err_.getvalue()
-
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_clifail_convert_missingfile(self, run_cmdline_test):
- # """Confirm exit code 1 with nonexistent file specified."""
- # run_cmdline_test(["convert", "plain", "thisfileshouldbeabsent.txt"], expect=1)
-
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_clifail_convert_badoutfilename(
- # self, scratch_path, run_cmdline_test, misc_info
- # ):
- # """Confirm exit code 1 with invalid output file name."""
- # run_cmdline_test(
- # [
- # "convert",
- # "plain",
- # str(scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)),
- # misc_info.invalid_filename,
- # ],
- # expect=1,
- # )
-
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_clifail_convert_badoutputdir(
- # self, res_cmp, scratch_path, run_cmdline_test
- # ):
- # """Confirm exit code 1 when output location can't be created."""
- # run_cmdline_test(
- # [
- # "convert",
- # "plain",
- # res_cmp,
- # str(scratch_path / "nonexistent" / "folder" / "obj.txt"),
- # ],
- # expect=1,
- # )
-
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_clifail_convert_pathonlysrc(self, scratch_path, run_cmdline_test):
- # """Confirm cmdline plaintext convert with input directory arg fails."""
- # run_cmdline_test(["convert", "plain", str(scratch_path)], expect=1)
-
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_clifail_convert_localfile_as_url(
- # self, scratch_path, misc_info, run_cmdline_test, check
- # ):
- # """Confirm error when using URL mode on local file."""
- # in_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)
-
- # (scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.DEC)).unlink()
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_wrongfiletype(
+ self, scratch_path, run_cmdline_test, monkeypatch
+ ):
+ """Confirm exit code 1 with invalid file format."""
+ monkeypatch.chdir(scratch_path)
+ fname = "testfile"
+ Path(fname).write_bytes(b"this is not objects.inv\n")
+
+ with stdio_mgr() as (in_, out_, err_):
+ run_cmdline_test([fname], command=CLICommand.Textconv, expect=1)
+ assert "Unrecognized" in err_.getvalue()
- # with check(msg="path-style"):
- # run_cmdline_test(["convert", "plain", "-u", str(in_path)], expect=1)
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_missingfile(self, run_cmdline_test):
+ """Confirm exit code 1 with nonexistent file specified."""
+ run_cmdline_test(
+ ["thisfileshouldbeabsent.txt"], command=CLICommand.Textconv, expect=1
+ )
- # with check(msg="url-style"):
- # file_url = "file:///" + str(in_path.resolve())
- # run_cmdline_test(["convert", "plain", "-u", file_url], expect=1)
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_outputdir_provided(
+ self, res_cmp, scratch_path, run_cmdline_test
+ ):
+ """Confirm exit code 2 when too many inputs are provided."""
+ run_cmdline_test(
+ [
+ res_cmp,
+ str(scratch_path / "objects.txt"),
+ ],
+ command=CLICommand.Textconv,
+ expect=2,
+ )
- # def test_clifail_no_url_with_stdin(self, run_cmdline_test):
- # """Confirm parser exit when -u passed with "-" infile."""
- # with stdio_mgr() as (in_, out_, err_):
- # run_cmdline_test(["convert", "plain", "-u", "-"], expect=2)
- # assert "--url not allowed" in err_.getvalue()
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_clifail_convert_pathonlysrc(self, scratch_path, run_cmdline_test):
+ """Confirm cmdline plaintext convert with input directory arg fails."""
+ run_cmdline_test(
+ [str(scratch_path)],
+ command=CLICommand.Textconv,
+ expect=1,
+ )
+
+ def test_clifail_no_url_arg(self, run_cmdline_test):
+ """Confirm textconv parser errors on non-existent -u flag."""
+ with stdio_mgr() as (in_, out_, err_):
+ run_cmdline_test(
+ ["-u", "nofile.inv"], command=CLICommand.Textconv, expect=2
+ )
+ assert "unrecognized argument" in err_.getvalue()
class TestStdio:
From 73636915315b61b6395a684c9f266f5346fcaaa0 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 11:03:04 -0500
Subject: [PATCH 013/122] Adjust print_stderr() to work with textconv
No subparser name in the params for it to query when we're
running textconv
---
src/sphobjinv/cli/ui.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/sphobjinv/cli/ui.py b/src/sphobjinv/cli/ui.py
index 7248ea08..62b080e2 100644
--- a/src/sphobjinv/cli/ui.py
+++ b/src/sphobjinv/cli/ui.py
@@ -58,7 +58,11 @@ def print_stderr(thing, params, *, end="\n"):
|str| -- String to append to printed content (default: ``\n``\ )
"""
- if params[PrsConst.SUBPARSER_NAME][:2] == "su" or not params[PrsConst.QUIET]:
+ if (
+ PrsConst.SUBPARSER_NAME not in params # textconv
+ or params[PrsConst.SUBPARSER_NAME][:2] == "su" # suggest is never quiet
+ or not params[PrsConst.QUIET] # non-quiet convert
+ ):
print(thing, file=sys.stderr, end=end)
From 6c47b573b14766a4c13252b20997a1c924457ce2 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 11:38:30 -0500
Subject: [PATCH 014/122] Add initial fixtures_http.py
---
tests/fixtures_http.py | 112 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 112 insertions(+)
create mode 100644 tests/fixtures_http.py
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
new file mode 100644
index 00000000..0ce5344f
--- /dev/null
+++ b/tests/fixtures_http.py
@@ -0,0 +1,112 @@
+r"""*HTTP server fixtures definitions for* ``sphobjinv``.
+
+``sphobjinv`` is a toolkit for manipulation and inspection of
+Sphinx |objects.inv| files.
+
+**Author**
+ Brian Skinn (brian.skinn@gmail.com)
+
+**File Created**
+ 24 Dec 2025
+
+**Copyright**
+ \(c) Brian Skinn 2016-2025
+
+**Source Repository**
+ http://www.github.com/bskinn/sphobjinv
+
+**Documentation**
+ https://sphobjinv.readthedocs.io/en/stable
+
+**License**
+ Code: `MIT License`_
+
+ Docs & Docstrings: |CC BY 4.0|_
+
+ See |license_txt|_ for full license terms.
+
+**Members**
+
+"""
+
+import contextlib
+import functools
+import http.server
+import threading
+from pathlib import Path
+from typing import Callable, Generator
+
+import pytest
+
+
+@contextlib.contextmanager
+def _baseurl_for_served_directory(
+ directory: Path | str, host: str = "127.0.0.1"
+) -> Generator[str, None, None]:
+ """Spin up HTTP server on a directory and yield the server base URL."""
+ directory = Path(directory).resolve()
+
+ handler_cls = functools.partial(
+ http.server.SimpleHTTPRequestHandler,
+ directory=str(directory),
+ )
+
+ # Bind to port 0 so the OS chooses a free ephemeral port (race-free).
+ httpd = http.server.HTTPServer((host, 0), handler_cls)
+ port = httpd.server_address[1]
+ base_url = f"http://{host}:{port}"
+
+ thread = threading.Thread(
+ target=httpd.serve_forever,
+ name="pytest-http-server",
+ daemon=True,
+ )
+
+ thread.start()
+
+ try:
+ yield base_url
+ finally:
+ httpd.shutdown()
+ httpd.server_close()
+ thread.join(timeout=2)
+
+
+@pytest.fixture(scope="session")
+def resource_http_base_url() -> Generator[str, None, None]:
+ """Provide base URL of HTTP server exposing tests/resource/*.""" # noqa: RST213
+ resource_dir = Path(__file__).resolve().parent / "resource"
+
+ if not resource_dir.is_dir():
+ raise RuntimeError(
+ f"Expected test resource directory not found: {resource_dir}"
+ )
+
+ with _baseurl_for_served_directory(resource_dir) as base_url:
+ yield base_url
+
+
+@pytest.fixture
+def resource_url(resource_http_base_url: str) -> Callable[[str], str]:
+ """Provide a function to calculate the full test-resource URL from a relative URL.
+
+ Forbids '..' path segments to avoid root escape.
+
+ **Examples**
+
+ resource_url("objects_bokeh.inv") -> "http://127.0.0.1:{PORT}/objects_bokeh.inv"
+ resource_url("subdir/file.ext") -> ".../subdir/file.ext"
+
+ """
+
+ def _calc_path(rel_path: str) -> str:
+ """Calculate the full test-resource URL from a relative URL."""
+ # Prevent escaping the resource directory.
+ if ".." in Path(rel_path).parts:
+ raise ValueError("Path must not contain '..'")
+
+ # Ensure consistent URL path separators.
+ url_path = "/".join(Path(rel_path).parts)
+ return f"{resource_http_base_url}/{url_path}"
+
+ return _calc_path
From 230cd6a340d8b28961cd3fd49f528155dbb33b9b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 10:16:56 -0500
Subject: [PATCH 015/122] Move conftest.py into tests/
---
MANIFEST.in | 1 -
conftest.py => tests/conftest.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
rename conftest.py => tests/conftest.py (99%)
diff --git a/MANIFEST.in b/MANIFEST.in
index e83169a2..0acd4658 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,7 +6,6 @@ graft src/sphobjinv/_vendored/fuzzywuzzy
graft doc/source
include doc/make.bat doc/Makefile
-include conftest.py
graft tests
prune tests/resource
include tests/resource/objects_attrs* tests/resource/objects_sarge*
diff --git a/conftest.py b/tests/conftest.py
similarity index 99%
rename from conftest.py
rename to tests/conftest.py
index 3aad2932..83a828f2 100644
--- a/conftest.py
+++ b/tests/conftest.py
@@ -116,7 +116,7 @@ class Extensions(str, Enum):
# For the URL mode of Inventory instantiation
remote_url = (
- "https://github.com/bskinn/sphobjinv/raw/main/"
+ "https://raw.githubusercontent.com/bskinn/sphobjinv/main/"
"tests/resource/objects_{0}.inv"
)
From 86e8cd2b1eee7968feb53b4dda6ef5cc978df819 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 10:21:15 -0500
Subject: [PATCH 016/122] Update paths inside conftest.py
---
tests/conftest.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 83a828f2..0a62211f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -66,7 +66,7 @@ def pytest_addoption(parser):
@pytest.fixture(scope="session")
def res_path():
"""Provide Path object to the test resource directory."""
- return Path("tests", "resource")
+ return Path(__file__).resolve().parent / "resource"
@pytest.fixture(scope="session")
@@ -161,7 +161,9 @@ def scratch_path(tmp_path, res_path, misc_info, is_win, unix2dos):
@pytest.fixture(scope="session")
def ensure_doc_scratch():
"""Ensure doc/scratch dir exists, for README shell examples."""
- Path("doc", "scratch").mkdir(parents=True, exist_ok=True)
+ (Path(__file__).resolve().parent.parent / "doc" / "scratch").mkdir(
+ parents=True, exist_ok=True
+ )
@pytest.fixture(scope="session")
@@ -305,7 +307,7 @@ def func(inv, source_type):
testall_inv_paths = [
p
- for p in (Path(__file__).parent / "tests" / "resource").iterdir()
+ for p in (Path(__file__).parent / "resource").iterdir()
if p.name.startswith("objects_") and p.name.endswith(".inv")
]
testall_inv_ids = [p.name[8:-4] for p in testall_inv_paths]
From 7338ba887e11b08ad74b5e7af6497707409d502e Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 10:54:42 -0500
Subject: [PATCH 017/122] Adjust tox envs for conftest.py now being inside
tests/
---
tox.ini | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tox.ini b/tox.ini
index e7c27a45..104c9eec 100644
--- a/tox.ini
+++ b/tox.ini
@@ -102,20 +102,20 @@ commands=
skip_install=True
deps=-rrequirements-flake8.txt
commands=
- flake8 ./conftest.py src tests
+ flake8 src tests
[testenv:flake8_noqa]
skip_install=True
deps=-rrequirements-flake8.txt
commands=
pip install flake8-noqa
- flake8 --color=never --exit-zero ./conftest.py tests src
+ flake8 --color=never --exit-zero tests src
[testenv:interrogate]
skip_install=True
deps=interrogate
commands=
- interrogate {posargs} conftest.py tests src
+ interrogate {posargs} tests src
[testenv:linkcheck]
skip_install=True
From 892208d5c68cd8a86a13cf7be5aa9699888ad88e Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 11:55:45 -0500
Subject: [PATCH 018/122] Add HTTP/HTTPS comments to
test_api_inventory_known_header_required()
Specifically, that the HTTP is intentional
---
tests/test_api_good_nonlocal.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index 928c223a..20e2f945 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -55,7 +55,9 @@ def skip_if_no_nonloc(pytestconfig):
@pytest.mark.parametrize(
["name", "url"],
[
+ # Intentionally HTTP to test insecure remotes
("flask", "http://flask.palletsprojects.com/en/1.1.x/objects.inv"),
+ # HTTPS as will usually be the case
("h5py", "https://docs.h5py.org/en/stable/objects.inv"),
],
ids=(lambda x: "" if "://" in x else x),
From 36d93e58d5bcbbdcc7e3dcccfc9477808ce27700 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:11:31 -0500
Subject: [PATCH 019/122] Remove extra newline
---
tests/conftest.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 0a62211f..8b128e95 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -239,7 +239,6 @@ def run_cmdline_test(monkeypatch):
def func(arglist, *, expect=0): # , suffix=None):
"""Perform the CLI exit-code test."""
-
# Assemble execution arguments
runargs = ["sphobjinv"]
runargs.extend(str(a) for a in arglist)
From c7b3a5873fd66540c210f65f47839614f3ffb67c Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:11:41 -0500
Subject: [PATCH 020/122] Replace GitHub remote URL with local-server URL
---
tests/conftest.py | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 8b128e95..b4c1f087 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -45,6 +45,7 @@
from sphinx.util.inventory import InventoryFile as IFile
import sphobjinv as soi
+from tests.fixtures_http import resource_http_base_url, resource_url # noqa: F401
def pytest_addoption(parser):
@@ -114,12 +115,6 @@ class Extensions(str, Enum):
True: b"attr.Attribute py:class 1 api.html#attr.Attribute attr.Attribute",
}
- # For the URL mode of Inventory instantiation
- remote_url = (
- "https://raw.githubusercontent.com/bskinn/sphobjinv/main/"
- "tests/resource/objects_{0}.inv"
- )
-
# Regex pattern for objects_xyz.inv files
p_inv = re.compile(r"objects_([^.]+)\.inv", re.I)
@@ -135,6 +130,16 @@ class Extensions(str, Enum):
return Info()
+@pytest.fixture(scope="session")
+def http_inv_url_template(resource_url) -> str: # noqa: F811
+ """Provide a template string for accessing files over HTTP.
+
+ Meant to be used via the URL mode of Inventory instantiation.
+
+ """
+ return resource_url("objects_{0}.inv")
+
+
@pytest.fixture()
def scratch_path(tmp_path, res_path, misc_info, is_win, unix2dos):
"""Provision pre-populated scratch directory, returned as Path."""
From 09c9640ead96d748b266fbc7c7ede7350b946afc Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:12:01 -0500
Subject: [PATCH 021/122] Switch resource_url fixture to session-scope
Since it shouldn't change, and we access it at session scope deeper in
---
tests/fixtures_http.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index 0ce5344f..4e15669e 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -86,7 +86,7 @@ def resource_http_base_url() -> Generator[str, None, None]:
yield base_url
-@pytest.fixture
+@pytest.fixture(scope="session")
def resource_url(resource_http_base_url: str) -> Callable[[str], str]:
"""Provide a function to calculate the full test-resource URL from a relative URL.
From a9e37bf77aa103581b8e68ff7460db6cfd4a46b7 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:12:28 -0500
Subject: [PATCH 022/122] Add 'tests' "package" to first-party in flake config
---
tox.ini | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tox.ini b/tox.ini
index 104c9eec..ac31e1f5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -191,4 +191,4 @@ per_file_ignores =
#flake8-import-order
import-order-style = smarkets
-application-import-names = sphobjinv
+application-import-names = sphobjinv,tests
From f2d56f67ad2cbb2800e86d81b0fe56067cd4ed0b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:12:50 -0500
Subject: [PATCH 023/122] Update external-remote URL-load check test
Add note about how it's now doing the primary lifting of checking
truly-remote URL inventory loads.
Add better in-situ reporting of which project's objects.inv failed,
if it fails.
---
tests/test_api_good_nonlocal.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index 20e2f945..c023e1a5 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -64,9 +64,14 @@ def skip_if_no_nonloc(pytestconfig):
)
@pytest.mark.timeout(30)
def test_api_inventory_known_header_required(name, url):
- """Confirm URL load works on docs pages requiring HTTP header config."""
+ """Confirm URL load works on docs pages requiring HTTP header config.
+
+ With the change to local-server HTTP testing, this test is important because
+ it directly exercises `Inventory` instantiation from a true HTTP(S) remote.
+
+ """
inv = soi.Inventory(url=url)
- assert inv.count > 0
+ assert inv.count > 0, f"'{name}' inventory imports without objects"
@pytest.mark.testall
From f784a6dad4cb00618bb7903a37d009cec341ffc8 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:13:57 -0500
Subject: [PATCH 024/122] Convert --testall URL API imports to use local server
---
tests/test_api_good_nonlocal.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index c023e1a5..e75eac37 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -81,6 +81,7 @@ def test_api_inventory_many_url_imports(
res_path,
scratch_path,
misc_info,
+ http_inv_url_template,
sphinx_load_test,
pytestconfig,
):
@@ -102,7 +103,7 @@ def test_api_inventory_many_url_imports(
mch = misc_info.p_inv.match(fname)
proj_name = mch.group(1)
inv1 = soi.Inventory(str(res_path / fname))
- inv2 = soi.Inventory(url=misc_info.remote_url.format(proj_name))
+ inv2 = soi.Inventory(url=http_inv_url_template.format(proj_name))
# Test the things
assert inv1 == inv2
From b8028999393e66b2add5a161bd3951367376abb2 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:34:57 -0500
Subject: [PATCH 025/122] Convert nonlocal CLI tests to use local server
---
tests/test_cli_nonlocal.py | 51 ++++++++++++++++++++++++++++----------
1 file changed, 38 insertions(+), 13 deletions(-)
diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py
index b6139e7d..bd730693 100644
--- a/tests/test_cli_nonlocal.py
+++ b/tests/test_cli_nonlocal.py
@@ -65,7 +65,12 @@ class TestConvert:
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_convert_from_url_with_dest(
- self, scratch_path, misc_info, run_cmdline_test, monkeypatch
+ self,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ monkeypatch,
):
"""Confirm CLI URL D/L, convert works w/outfile supplied."""
monkeypatch.chdir(scratch_path)
@@ -76,7 +81,7 @@ def test_cli_convert_from_url_with_dest(
"convert",
"plain",
"-u",
- misc_info.remote_url.format("attrs"),
+ http_inv_url_template.format("attrs"),
str(dest_path),
]
)
@@ -85,20 +90,30 @@ def test_cli_convert_from_url_with_dest(
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_convert_from_url_no_dest(
- self, scratch_path, misc_info, run_cmdline_test, monkeypatch
+ self,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ monkeypatch,
):
"""Confirm CLI URL D/L, convert works w/o outfile supplied."""
monkeypatch.chdir(scratch_path)
dest_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.DEC)
dest_path.unlink()
run_cmdline_test(
- ["convert", "plain", "-u", misc_info.remote_url.format("attrs")]
+ ["convert", "plain", "-u", http_inv_url_template.format("attrs")]
)
assert dest_path.is_file()
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_url_in_json(
- self, scratch_path, misc_info, run_cmdline_test, monkeypatch
+ self,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ monkeypatch,
):
"""Confirm URL is present when using CLI URL mode."""
monkeypatch.chdir(scratch_path)
@@ -108,7 +123,7 @@ def test_cli_url_in_json(
"convert",
"json",
"-u",
- misc_info.remote_url.format("attrs"),
+ http_inv_url_template.format("attrs"),
str(dest_path.resolve()),
]
)
@@ -118,7 +133,9 @@ def test_cli_url_in_json(
assert "objects" in d.get("metadata", {}).get("url", {})
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
- def test_clifail_bad_url(self, run_cmdline_test, misc_info, scratch_path):
+ def test_clifail_bad_url(
+ self, run_cmdline_test, misc_info, http_inv_url_template, scratch_path
+ ):
"""Confirm proper error behavior when a bad URL is passed."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(
@@ -126,12 +143,12 @@ def test_clifail_bad_url(self, run_cmdline_test, misc_info, scratch_path):
"convert",
"plain",
"-u",
- misc_info.remote_url.format("blarghers"),
+ http_inv_url_template.format("blarghers"),
str(scratch_path),
],
expect=1,
)
- assert "HTTP error: 404 Not Found." in err_.getvalue()
+ assert re.search("http error.*404.*not found", err_.getvalue(), re.I)
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_clifail_url_no_leading_http(self, run_cmdline_test, scratch_path):
@@ -150,10 +167,16 @@ def test_clifail_url_no_leading_http(self, run_cmdline_test, scratch_path):
assert "file found but inventory could not be loaded" in err_.getvalue()
def test_cli_json_export_import(
- self, res_cmp, scratch_path, misc_info, run_cmdline_test, sphinx_load_test
+ self,
+ res_cmp,
+ scratch_path,
+ misc_info,
+ http_inv_url_template,
+ run_cmdline_test,
+ sphinx_load_test,
):
"""Confirm JSON sent to stdout from local source imports ok."""
- inv_url = misc_info.remote_url.format("attrs")
+ inv_url = http_inv_url_template.format("attrs")
mod_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
with stdio_mgr() as (in_, out_, err_):
@@ -173,14 +196,16 @@ class TestSuggest:
"""Test nonlocal CLI suggest mode functionality."""
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
- def test_cli_suggest_from_url(self, misc_info, run_cmdline_test):
+ def test_cli_suggest_from_url(
+ self, misc_info, http_inv_url_template, run_cmdline_test
+ ):
"""Confirm reST-only suggest output works from URL."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(
[
"suggest",
"-u",
- misc_info.remote_url.format("attrs"),
+ http_inv_url_template.format("attrs"),
"instance",
"-t",
"50",
From 543e092f8082911b8d5bf72a1154ff9a2d346a37 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 12:47:20 -0500
Subject: [PATCH 026/122] Remove Django-specific URL test
Too flaky, breaking regularly even with retries.
---
tests/test_cli_nonlocal.py | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py
index bd730693..4e2fad06 100644
--- a/tests/test_cli_nonlocal.py
+++ b/tests/test_cli_nonlocal.py
@@ -248,13 +248,3 @@ def test_cli_suggest_from_typical_objinv_url(self, run_cmdline_test, check):
check.is_in(
"(http://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
)
-
- @pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
- def test_cli_suggest_from_django_objinv_url(self, run_cmdline_test, check):
- """Confirm reST-only suggest works for direct objects.inv URL."""
- url = "https://docs.djangoproject.com/en/4.1/_objects/"
- with stdio_mgr() as (in_, out_, err_):
- run_cmdline_test(["suggest", "-u", url, "route", "-a"])
-
- check.is_true(re.search("DATABASE_ROUTERS", out_.getvalue()))
- check.is_in("Cannot infer intersphinx_mapping", err_.getvalue())
From a07f2a52d2d9a549eabde64365f3dbd7d1f4bf16 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 13:08:01 -0500
Subject: [PATCH 027/122] Update supported Python range to 3.10-3.14
---
.github/workflows/all_core_tests.yml | 4 ++--
.github/workflows/ready_doctest.yml | 2 +-
.github/workflows/ready_linting.yml | 4 ++--
.github/workflows/ready_test_matrix.yml | 4 ++--
.github/workflows/ready_test_nonloc.yml | 4 ++--
.github/workflows/release_check_sdist.yml | 2 +-
.github/workflows/release_doc_warnings.yml | 2 +-
.github/workflows/release_flake8_noqa_nofail.yml | 2 +-
.github/workflows/release_readme_doctest.yml | 2 +-
.github/workflows/release_test_file_coverage.yml | 2 +-
.readthedocs.yaml | 2 +-
CONTRIBUTING.md | 10 +++++-----
doc/source/customfile.rst | 2 +-
pyproject.toml | 4 ++--
tox.ini | 14 +++++++-------
15 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml
index 129aef0c..96935961 100644
--- a/.github/workflows/all_core_tests.yml
+++ b/.github/workflows/all_core_tests.yml
@@ -1,4 +1,4 @@
-name: 'ALL: Run tests on Python 3.12'
+name: 'ALL: Run tests on Python 3.13'
on:
pull_request:
@@ -24,7 +24,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/ready_doctest.yml b/.github/workflows/ready_doctest.yml
index 18ed9e62..a5fc4dda 100644
--- a/.github/workflows/ready_doctest.yml
+++ b/.github/workflows/ready_doctest.yml
@@ -27,7 +27,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/ready_linting.yml b/.github/workflows/ready_linting.yml
index 95fab978..0e1cb685 100644
--- a/.github/workflows/ready_linting.yml
+++ b/.github/workflows/ready_linting.yml
@@ -27,7 +27,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-flake8.txt
@@ -55,7 +55,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-interrogate.txt
diff --git a/.github/workflows/ready_test_matrix.yml b/.github/workflows/ready_test_matrix.yml
index 8f58417b..be3136be 100644
--- a/.github/workflows/ready_test_matrix.yml
+++ b/.github/workflows/ready_test_matrix.yml
@@ -22,10 +22,10 @@ jobs:
strategy:
matrix:
os: ['windows-latest', 'ubuntu-latest', 'macos-latest']
- py: ['3.9', '3.10', '3.11', '3.13']
+ py: ['3.10', '3.11', '3.12', '3.14']
include:
- os: 'macos-latest'
- py: '3.12'
+ py: '3.13'
steps:
- name: Check out repo
diff --git a/.github/workflows/ready_test_nonloc.yml b/.github/workflows/ready_test_nonloc.yml
index 5ba2636a..9a7d0d7b 100644
--- a/.github/workflows/ready_test_nonloc.yml
+++ b/.github/workflows/ready_test_nonloc.yml
@@ -13,7 +13,7 @@ on:
jobs:
python_os_test_matrix:
- name: ${{ matrix.os }} python 3.12
+ name: ${{ matrix.os }} python 3.13
runs-on: ${{ matrix.os }}
concurrency:
group: ${{ github.workflow }}-${{ matrix.os }}-${{ github.ref }}
@@ -30,7 +30,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/release_check_sdist.yml b/.github/workflows/release_check_sdist.yml
index d5a5678b..bc2ac334 100644
--- a/.github/workflows/release_check_sdist.yml
+++ b/.github/workflows/release_check_sdist.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-dev.txt
diff --git a/.github/workflows/release_doc_warnings.yml b/.github/workflows/release_doc_warnings.yml
index 0cf4b323..2cb09b18 100644
--- a/.github/workflows/release_doc_warnings.yml
+++ b/.github/workflows/release_doc_warnings.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-rtd.txt
diff --git a/.github/workflows/release_flake8_noqa_nofail.yml b/.github/workflows/release_flake8_noqa_nofail.yml
index c2810a61..249ed3bb 100644
--- a/.github/workflows/release_flake8_noqa_nofail.yml
+++ b/.github/workflows/release_flake8_noqa_nofail.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/release_readme_doctest.yml b/.github/workflows/release_readme_doctest.yml
index 33f8649f..9327fce3 100644
--- a/.github/workflows/release_readme_doctest.yml
+++ b/.github/workflows/release_readme_doctest.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
diff --git a/.github/workflows/release_test_file_coverage.yml b/.github/workflows/release_test_file_coverage.yml
index 40cdc0d2..d3bd5452 100644
--- a/.github/workflows/release_test_file_coverage.yml
+++ b/.github/workflows/release_test_file_coverage.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: '3.13'
cache: 'pip'
cache-dependency-path: |
requirements-ci.txt
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index ab110bfa..7409274a 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -9,7 +9,7 @@ version: 2
build:
os: 'ubuntu-22.04'
tools:
- python: '3.12'
+ python: '3.13'
# Python requirements
python:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 28e29d57..c51c3a5e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -39,12 +39,12 @@ $ git clone https://github.com/{you}/sphobjinv
```
Then, create a virtual environment for the project, in whatever location you
-prefer. Any Python interpreter 3.9+ *should* work fine.
+prefer. Any Python interpreter 3.10+ *should* work fine.
I prefer to use `virtualenv` and create in `./env`:
```bash
-$ python3.12 -m virtualenv env --prompt="sphobjinv"
+$ python3.13 -m virtualenv env --prompt="sphobjinv"
```
Activate the environment:
@@ -140,8 +140,8 @@ Note that while [`tox`](https://tox.wiki/en/latest/) *is* configured for the
project, it is **not** set up to be an everyday test runner. Instead, its
purpose for testing is to execute an extensive matrix of test environments
checking for the compatibility of different Python and dependency versions. You
-can run it if you want, but you'll need working versions of all of Python 3.9
-through 3.13 installed and on `PATH` as `python3.9`, `python3.10`, etc. The
+can run it if you want, but you'll need working versions of all of Python 3.10
+through 3.14 installed and on `PATH` as `python3.10`, `python3.11`, etc. The
nonlocal test suite is run for each `tox` environment, so it's best to use at
most two parallel sub-processes to avoid oversaturating your network bandwidth;
e.g.:
@@ -247,7 +247,7 @@ with `make linkcheck`.
Both Github Actions and Azure Pipelines are set up for the project, and should
run on any forks of the repository.
-Github Actions runs the test suite on Linux for Python 3.9 through 3.13, as well
+Github Actions runs the test suite on Linux for Python 3.10 through 3.14, as well
as the `flake8` lints and the Sphinx doctests. By default, the Github Actions
will run on all commits, but the workflows can be skipped per-commit by
including `[skip ci]` in the commit message.
diff --git a/doc/source/customfile.rst b/doc/source/customfile.rst
index 3765cc57..f37b8cae 100644
--- a/doc/source/customfile.rst
+++ b/doc/source/customfile.rst
@@ -143,7 +143,7 @@ can be found at the GitHub repo
intersphinx_mapping = {
# Standard reference to web docs, with web objects.inv
- 'python': ('https://docs.python.org/3.12', None),
+ 'python': ('https://docs.python.org/3.13', None),
# Django puts its objects.inv file in a non-standard location
'django': ('https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/_objects/'),
diff --git a/pyproject.toml b/pyproject.toml
index 5f35b59b..35aac267 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,11 +21,11 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Topic :: Documentation",
"Topic :: Documentation :: Sphinx",
"Topic :: Software Development",
@@ -34,7 +34,7 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
]
keywords = ["sphinx", "sphinx-doc", "inventory", "manager", "inspector"]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
dependencies = [
"attrs>=19.2",
"certifi",
diff --git a/tox.ini b/tox.ini
index ac31e1f5..b593473e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,23 +3,23 @@ minversion=2.0
isolated_build=True
envlist=
# Test all Python versions on latest lib versions
- py3{9,10,11,12,13}-sphx_latest-attrs_latest-jsch_latest
+ py3{10,11,12,13,14}-sphx_latest-attrs_latest-jsch_latest
# Test leading Python version on current in-repo dev lib versions
py313-sphx_dev-attrs_dev-jsch_dev
# Scan across Sphinx versions
py313-sphx_{1_6_x,1_x,2_x,4_x,5_x,6_x,7_x,dev}-attrs_latest-jsch_latest
- # sphx_3_x is incompatible with py310 due to a typing import. Test on py39 instead.
- py39-sphx_3_x-attrs_latest-jsch_latest
+ # sphx_3_x is incompatible with py310 due to a typing import. Test on py311 instead.
+ py311-sphx_3_x-attrs_latest-jsch_latest
# Scan attrs versions
py313-sphx_latest-attrs_{19_2,19_3,20_3,21_3,22_2,23_2,24_3,dev}-jsch_latest
# Scan jsonschema versions
py313-sphx_latest-attrs_latest-jsch_{3_0,3_x,4_0,4_8,4_14,4_20,dev}
# Earliest supported Python and lib versions all together
- py39-sphx_1_6_x-attrs_19_2-jsch_3_0
+ py310-sphx_1_6_x-attrs_19_2-jsch_3_0
# Spot matrix of early Python, Sphinx, attrs versions
- py3{9,10}-sphx_{1,2}_x-attrs_{19,20}_2-jsch_latest
+ py3{10,11}-sphx_{1,2}_x-attrs_{19,20}_2-jsch_latest
# Test the specific Sphinx threshold cases where behavior changed
- py312-sphx_{2_3_1,2_4_0,3_2_1,3_3_0,3_4_0,8_1_3,8_2_0}-attrs_latest-jsch_latest
+ py313-sphx_{2_3_1,2_4_0,3_2_1,3_3_0,3_4_0,8_1_3,8_2_0}-attrs_latest-jsch_latest
# Simple 'does the sdist install' check
sdist_install
# Lints
@@ -86,11 +86,11 @@ deps=
[testenv:linux]
platform=linux
basepython=
+ py314: python3.14
py313: python3.13
py312: python3.12
py311: python3.11
py310: python3.10
- py39: python3.9
[testenv:black]
skip_install=True
From 551e5a02a25f5b7d74cd1ce049ea8cf5ea4114bb Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 13:08:17 -0500
Subject: [PATCH 028/122] Expand on a comment for historical info
---
src/sphobjinv/cli/parser.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py
index edc5ed16..60d2fb4e 100644
--- a/src/sphobjinv/cli/parser.py
+++ b/src/sphobjinv/cli/parser.py
@@ -227,6 +227,8 @@ def getparser():
# briefly required a/o 3.7.0b4 due to change in default behavior, per:
# https://bugs.python.org/issue33109. 3.6 behavior restored for
# 3.7 release.
+ #
+ # We retain for explicitness
sprs.required = False
spr_convert = sprs.add_parser(
From 4c91212198303b493377a04a2d74f494e632daf4 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 13:29:08 -0500
Subject: [PATCH 029/122] Bump active dev Sphinx version
---
requirements-ci.txt | 2 +-
requirements-dev.txt | 2 +-
requirements-rtd.txt | 2 +-
tox.ini | 3 ++-
4 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/requirements-ci.txt b/requirements-ci.txt
index 16e23de4..f7663e87 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -9,7 +9,7 @@ pytest-check>=1.1.2
pytest-cov
pytest-retry
pytest-timeout
-sphinx==7.4.7
+sphinx==8.2.3
sphinx-issues
sphinx-rtd-theme>=0.5.1
sphinxcontrib-programoutput
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f6258f9f..beaf5ccd 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -11,7 +11,7 @@ pytest-cov
pytest-retry
pytest-timeout
restview
-sphinx==7.4.7 # Staying <8 since 8.0 drops Python 3.9
+sphinx==8.2.3 # Staying <9 since sphinx-rtd-theme 3.0.2 is incompat w/9.0
sphinx-autobuild
sphinx-issues
sphinx-rtd-theme>=0.5.1
diff --git a/requirements-rtd.txt b/requirements-rtd.txt
index 618f3d7c..de4d63cb 100644
--- a/requirements-rtd.txt
+++ b/requirements-rtd.txt
@@ -1,5 +1,5 @@
attrs>=19.2
-sphinx==7.4.7
+sphinx==8.2.3
sphinx-issues
sphinx-rtd-theme>=0.5.1
sphinxcontrib-programoutput
diff --git a/tox.ini b/tox.ini
index b593473e..c5596519 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ envlist=
# Test leading Python version on current in-repo dev lib versions
py313-sphx_dev-attrs_dev-jsch_dev
# Scan across Sphinx versions
- py313-sphx_{1_6_x,1_x,2_x,4_x,5_x,6_x,7_x,dev}-attrs_latest-jsch_latest
+ py313-sphx_{1_6_x,1_x,2_x,4_x,5_x,6_x,7_x,8_x,dev}-attrs_latest-jsch_latest
# sphx_3_x is incompatible with py310 due to a typing import. Test on py311 instead.
py311-sphx_3_x-attrs_latest-jsch_latest
# Scan attrs versions
@@ -43,6 +43,7 @@ deps=
sphx_5_x: sphinx<6
sphx_6_x: sphinx<7
sphx_7_x: sphinx<8
+ sphx_8_x: sphinx<9
sphx_2_3_1: sphinx==2.3.1
sphx_2_4_0: sphinx==2.4.0
sphx_3_2_1: sphinx==3.2.1
From a42a01bff39b4a3d2011082fbf56f88fbdf5d4c1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 13:32:49 -0500
Subject: [PATCH 030/122] Convert docs 'suggest' call from Github to RTD
Should render more reliably.
---
doc/source/cli/suggest.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/source/cli/suggest.rst b/doc/source/cli/suggest.rst
index 21fa0a29..bf9d4cd8 100644
--- a/doc/source/cli/suggest.rst
+++ b/doc/source/cli/suggest.rst
@@ -29,7 +29,7 @@ via :option:`--thresh`:
Remote |objects.inv| files can be retrieved for inspection by passing the
:option:`--url` flag:
-.. command-output:: sphobjinv suggest https://github.com/bskinn/sphobjinv/raw/main/tests/resource/objects_attrs.inv instance -u -t 48
+.. command-output:: sphobjinv suggest https://sphobjinv.readthedocs.io/en/stable/objects_attrs.inv Inventory -u -t 85
:cwd: /../../tests/resource
The URL provided **MUST** have the leading protocol specified (here,
From f4726e805a87df98d64e10e298e24a0274234dee Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 14:17:13 -0500
Subject: [PATCH 031/122] Fix inventory filename
---
doc/source/cli/suggest.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/source/cli/suggest.rst b/doc/source/cli/suggest.rst
index bf9d4cd8..08627aa8 100644
--- a/doc/source/cli/suggest.rst
+++ b/doc/source/cli/suggest.rst
@@ -29,7 +29,7 @@ via :option:`--thresh`:
Remote |objects.inv| files can be retrieved for inspection by passing the
:option:`--url` flag:
-.. command-output:: sphobjinv suggest https://sphobjinv.readthedocs.io/en/stable/objects_attrs.inv Inventory -u -t 85
+.. command-output:: sphobjinv suggest https://sphobjinv.readthedocs.io/en/stable/objects.inv Inventory -u -t 85
:cwd: /../../tests/resource
The URL provided **MUST** have the leading protocol specified (here,
From f77ddf21705146e4b69c8dc1b240f1753b527a5d Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 14:31:47 -0500
Subject: [PATCH 032/122] Tweak a couple of tests comments
---
tests/fixtures_http.py | 2 +-
tests/test_api_good_nonlocal.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index 4e15669e..18c8e607 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -51,7 +51,7 @@ def _baseurl_for_served_directory(
directory=str(directory),
)
- # Bind to port 0 so the OS chooses a free ephemeral port (race-free).
+ # Bind to port 0 so the OS chooses a free ephemeral port
httpd = http.server.HTTPServer((host, 0), handler_cls)
port = httpd.server_address[1]
base_url = f"http://{host}:{port}"
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index e75eac37..ac1907fc 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -57,7 +57,7 @@ def skip_if_no_nonloc(pytestconfig):
[
# Intentionally HTTP to test insecure remotes
("flask", "http://flask.palletsprojects.com/en/1.1.x/objects.inv"),
- # HTTPS as will usually be the case
+ # HTTPS as will usually be the case when retrieving inventories
("h5py", "https://docs.h5py.org/en/stable/objects.inv"),
],
ids=(lambda x: "" if "://" in x else x),
From 66e52b1ecc912f92aaebbb343250fc82323fe1dc Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 14:32:32 -0500
Subject: [PATCH 033/122] Drop dev-pin Sphinx back down to 8.1.3
---
requirements-ci.txt | 2 +-
requirements-dev.txt | 2 +-
requirements-rtd.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements-ci.txt b/requirements-ci.txt
index f7663e87..33429641 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -9,7 +9,7 @@ pytest-check>=1.1.2
pytest-cov
pytest-retry
pytest-timeout
-sphinx==8.2.3
+sphinx==8.1.3
sphinx-issues
sphinx-rtd-theme>=0.5.1
sphinxcontrib-programoutput
diff --git a/requirements-dev.txt b/requirements-dev.txt
index beaf5ccd..4e081a39 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -11,7 +11,7 @@ pytest-cov
pytest-retry
pytest-timeout
restview
-sphinx==8.2.3 # Staying <9 since sphinx-rtd-theme 3.0.2 is incompat w/9.0
+sphinx==8.1.3 # Staying <9 since sphinx-rtd-theme 3.0.2 is incompat w/9.0
sphinx-autobuild
sphinx-issues
sphinx-rtd-theme>=0.5.1
diff --git a/requirements-rtd.txt b/requirements-rtd.txt
index de4d63cb..c42c329f 100644
--- a/requirements-rtd.txt
+++ b/requirements-rtd.txt
@@ -1,5 +1,5 @@
attrs>=19.2
-sphinx==8.2.3
+sphinx==8.1.3
sphinx-issues
sphinx-rtd-theme>=0.5.1
sphinxcontrib-programoutput
From d0fc65ba4e2dbf88efe8d18d6ceb6605022b21a1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 14:40:03 -0500
Subject: [PATCH 034/122] Switch fixture to 'localhost' from '127.0.0.1'
---
tests/fixtures_http.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index 18c8e607..4a3c3ba7 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -41,7 +41,7 @@
@contextlib.contextmanager
def _baseurl_for_served_directory(
- directory: Path | str, host: str = "127.0.0.1"
+ directory: Path | str, host: str = "localhost"
) -> Generator[str, None, None]:
"""Spin up HTTP server on a directory and yield the server base URL."""
directory = Path(directory).resolve()
From 201caa4cf78a0d643e5254668228b641e60e0053 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 14:55:07 -0500
Subject: [PATCH 035/122] Skip reverse DNS lookup when logging in testing HTTP
server
---
tests/fixtures_http.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index 4a3c3ba7..f0bde8e7 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -39,6 +39,14 @@
import pytest
+class NoReverseDNSRequestHandler(http.server.SimpleHTTPRequestHandler):
+ """HTTP request handler that skips reverse DNS lookup when logging."""
+
+ def address_string(self) -> str:
+ """Return the address string without reverse DNS lookup."""
+ return self.client_address[0]
+
+
@contextlib.contextmanager
def _baseurl_for_served_directory(
directory: Path | str, host: str = "localhost"
@@ -47,7 +55,7 @@ def _baseurl_for_served_directory(
directory = Path(directory).resolve()
handler_cls = functools.partial(
- http.server.SimpleHTTPRequestHandler,
+ NoReverseDNSRequestHandler,
directory=str(directory),
)
From 0508f7850655b870f76846023b534c291b3fd159 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 15:02:19 -0500
Subject: [PATCH 036/122] Revert "Skip reverse DNS lookup when logging in
testing HTTP server"
This reverts commit 201caa4cf78a0d643e5254668228b641e60e0053.
---
tests/fixtures_http.py | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index f0bde8e7..4a3c3ba7 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -39,14 +39,6 @@
import pytest
-class NoReverseDNSRequestHandler(http.server.SimpleHTTPRequestHandler):
- """HTTP request handler that skips reverse DNS lookup when logging."""
-
- def address_string(self) -> str:
- """Return the address string without reverse DNS lookup."""
- return self.client_address[0]
-
-
@contextlib.contextmanager
def _baseurl_for_served_directory(
directory: Path | str, host: str = "localhost"
@@ -55,7 +47,7 @@ def _baseurl_for_served_directory(
directory = Path(directory).resolve()
handler_cls = functools.partial(
- NoReverseDNSRequestHandler,
+ http.server.SimpleHTTPRequestHandler,
directory=str(directory),
)
From 8314969a5ce8612d688a973ce3d149dee9e13ec1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 15:09:45 -0500
Subject: [PATCH 037/122] Switch local HTTP server to not do rDNS lookup on
startup
---
tests/fixtures_http.py | 21 +++++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index 4a3c3ba7..bf3eee46 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -32,6 +32,7 @@
import contextlib
import functools
import http.server
+import socketserver
import threading
from pathlib import Path
from typing import Callable, Generator
@@ -39,9 +40,25 @@
import pytest
+class NoNameLookupHTTPServer(http.server.HTTPServer):
+ """HTTPServer subclass that doesn't do rDNS lookup when setting server name.
+
+ We use this class because the macOS GitHub runners time out when attempting
+ the rDNS lookup.
+
+ """
+
+ def server_bind(self):
+ """Override server_bind to store the server name."""
+ socketserver.TCPServer.server_bind(self)
+ host, port = self.server_address[:2]
+ self.server_name = host # Override
+ self.server_port = port
+
+
@contextlib.contextmanager
def _baseurl_for_served_directory(
- directory: Path | str, host: str = "localhost"
+ directory: Path | str, host: str = "127.0.0.1"
) -> Generator[str, None, None]:
"""Spin up HTTP server on a directory and yield the server base URL."""
directory = Path(directory).resolve()
@@ -52,7 +69,7 @@ def _baseurl_for_served_directory(
)
# Bind to port 0 so the OS chooses a free ephemeral port
- httpd = http.server.HTTPServer((host, 0), handler_cls)
+ httpd = NoNameLookupHTTPServer((host, 0), handler_cls)
port = httpd.server_address[1]
base_url = f"http://{host}:{port}"
From a47d0e3fae0e79581799519e6729e972a789ef8a Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 15:17:38 -0500
Subject: [PATCH 038/122] Generalize text search in
test_cli_noargs_shows_help()
---
tests/test_cli.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4276e732..5e9c1a22 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -76,7 +76,7 @@ def test_cli_noargs_shows_help(self, run_cmdline_test):
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test([])
- assert "usage: sphobjinv" in out_.getvalue()
+ assert re.search("usage:.*sphobjinv", out_.getvalue(), re.I)
@pytest.mark.timeout(CLI_TEST_TIMEOUT)
def test_cli_no_subparser_prs_exit(self, run_cmdline_test):
From 0413fb11455abd352987c3a65248e7e7660cfc9d Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 18:32:24 -0500
Subject: [PATCH 039/122] Update Sphinx pin note in requirements-dev.txt
---
requirements-dev.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 4e081a39..81932469 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -11,7 +11,7 @@ pytest-cov
pytest-retry
pytest-timeout
restview
-sphinx==8.1.3 # Staying <9 since sphinx-rtd-theme 3.0.2 is incompat w/9.0
+sphinx==8.1.3 # Staying <8.2 to retain Python 3.10 compat
sphinx-autobuild
sphinx-issues
sphinx-rtd-theme>=0.5.1
From c7a149177eb26b1ed366f250b14a05000f81c5ba Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 18:37:29 -0500
Subject: [PATCH 040/122] Update CHANGELOG
---
CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7e3b4ad..26079131 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,37 @@ changes.
### *Unreleased*
+#### Tests
+
+ * Update `tox` env test matrix for `py310` to `py314` ([#325]).
+
+ * Update test path calculations to always be relative to `__file__` ([#325]).
+
+ * Relocate `conftest.py` into `tests` ([#325]).
+ * Since the new HTTP server fixtures are going in their own source file, it
+ made the most sense to pull `conftest.py` into the `tests/` directory
+ also.
+ * Required some updates to paths in fixtures &c.
+
+ * Convert HTTP/URL nonloc tests to use a transient local HTTP server ([#325]).
+ * See `tests/fixtures_http.py`.
+ * With the increased caution many sites, including GitHub, are applying to
+ incoming traffic, using 'raw' GitHub assets in the `sphobjinv` repository
+ has become too flaky.
+ * So, we stand up our own HTTP server as a session-scope fixture, and point
+ (nearly) all of the URL tests at the local server.
+ * A small number of tests remain that do still reach out to an internet
+ `objects.inv`.
+ * A small number of outside-world URL tests remain, to docsets that (so far)
+ have been cooperative. Time will tell if we need to find others.
+
#### Internal
+ * Bump dev-pin Sphinx to v8.1.3 ([#325]).
+ * Two different version constraints at the moment:
+ * Sphinx v8.2 doesn't support Python 3.10 (primary constraint)
+ * Newest `sphinx-rtd-theme` only supports Sphinx `<9` (secondary).
+
* Add `push` trigger for `all_core_tests.yml` workflow for `main` branch
([#320]).
* This will provide `main` branch CI results for this workflow, for the
@@ -19,6 +48,12 @@ changes.
#### Administrative
+ * Add formal support for Python 3.14 ([#325]).
+
+ * Drop support for Python 3.9 (EOL) ([#325]).
+
+ * Bump 'core' dev and CI Python version to 3.13 ([#325]).
+
* Update the GitHub badge to point to the new `all_core_tests.yml` workflow
([#320]) instead of the now-removed `ci_tests.yml`.
@@ -691,3 +726,4 @@ changes.
[#315]: https://github.com/bskinn/sphobjinv/pull/315
[#316]: https://github.com/bskinn/sphobjinv/pull/316
[#320]: https://github.com/bskinn/sphobjinv/pull/320
+[#325]: https://github.com/bskinn/sphobjinv/pull/325
From f136f63d92070b3de2baac6bd2f096faff1a5870 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 10:55:35 -0500
Subject: [PATCH 041/122] Switch to using isort for import formatting
---
pyproject.toml | 6 ++++++
requirements-flake8.txt | 1 -
tox.ini | 8 ++++++++
3 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 35aac267..4678a8cb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -89,3 +89,9 @@ exclude = '''
exclude = ["src/sphobjinv/_vendored"]
fail-under = 100
verbose = 1
+
+[tool.isort]
+profile = "black"
+known_first_party = ["sphobjinv", "tests"]
+no_lines_before = ["LOCALFOLDER"]
+extend_skip = ["src/sphobjinv/_vendored"]
diff --git a/requirements-flake8.txt b/requirements-flake8.txt
index a2e76cd1..c0654fcf 100644
--- a/requirements-flake8.txt
+++ b/requirements-flake8.txt
@@ -10,7 +10,6 @@ flake8-comprehensions
flake8-docstrings>=1.3.1
flake8-eradicate
flake8-implicit-str-concat
-flake8-import-order
flake8-pie
flake8-raise
flake8-rst-docstrings
diff --git a/tox.ini b/tox.ini
index c5596519..a96d1bf3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -118,6 +118,14 @@ deps=interrogate
commands=
interrogate {posargs} tests src
+[testenv:isort]
+description=Sort, group, and coalesce imports
+skip_install=True
+deps=isort
+commands=
+ isort --version
+ isort {posargs} src tests
+
[testenv:linkcheck]
skip_install=True
deps=-rrequirements-dev.txt
From 469b052f9a6a6dbb9148ec63a910846f455bdb64 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 10:57:22 -0500
Subject: [PATCH 042/122] Add version reports to tox black, flake envs
---
tox.ini | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tox.ini b/tox.ini
index a96d1bf3..81cf3183 100644
--- a/tox.ini
+++ b/tox.ini
@@ -97,12 +97,14 @@ basepython=
skip_install=True
deps=black
commands=
+ black --version
black {posargs} .
[testenv:flake8]
skip_install=True
deps=-rrequirements-flake8.txt
commands=
+ flake8 --version
flake8 src tests
[testenv:flake8_noqa]
From b91c09ff7e3a533d0e2118672a32cfd9c247e81b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 11:15:44 -0500
Subject: [PATCH 043/122] Add {posargs} to flake8 tox envs
---
tox.ini | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tox.ini b/tox.ini
index 81cf3183..806b56dc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -105,14 +105,14 @@ skip_install=True
deps=-rrequirements-flake8.txt
commands=
flake8 --version
- flake8 src tests
+ flake8 {posargs} src tests
[testenv:flake8_noqa]
skip_install=True
deps=-rrequirements-flake8.txt
commands=
pip install flake8-noqa
- flake8 --color=never --exit-zero tests src
+ flake8 --color=never --exit-zero {posargs} tests src
[testenv:interrogate]
skip_install=True
From f87899c4e04a1b8f55d361dec87b4a889a750cc9 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 19:07:49 -0500
Subject: [PATCH 044/122] Remove flake8 from dev req'ts
---
requirements-dev.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 81932469..5070a9a5 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -19,5 +19,4 @@ sphinxcontrib-programoutput
stdio-mgr>=1.0.1
tox
twine
--r requirements-flake8.txt
-e .
From 018b59a98418f83d341ffbe6c3f7fd745852d0d8 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 19:11:21 -0500
Subject: [PATCH 045/122] Remove flake8-import-order config from tox.ini
---
tox.ini | 1 -
1 file changed, 1 deletion(-)
diff --git a/tox.ini b/tox.ini
index 806b56dc..70068adb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -202,4 +202,3 @@ per_file_ignores =
#flake8-import-order
import-order-style = smarkets
-application-import-names = sphobjinv,tests
From 492ca5de1a9e589b44fb82f3a41e5e71c70fa19e Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 19:11:37 -0500
Subject: [PATCH 046/122] Add flake8-isort to flake8 req'ts
---
requirements-flake8.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements-flake8.txt b/requirements-flake8.txt
index c0654fcf..93ea2c55 100644
--- a/requirements-flake8.txt
+++ b/requirements-flake8.txt
@@ -10,6 +10,7 @@ flake8-comprehensions
flake8-docstrings>=1.3.1
flake8-eradicate
flake8-implicit-str-concat
+flake8-isort
flake8-pie
flake8-raise
flake8-rst-docstrings
From 8a133e485ba7bac507a5974228e129f2c8b6b84d Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 19:11:59 -0500
Subject: [PATCH 047/122] Run isort
---
src/sphobjinv/cli/core.py | 2 +-
src/sphobjinv/cli/load.py | 2 +-
src/sphobjinv/inventory.py | 2 +-
src/sphobjinv/re.py | 1 -
src/sphobjinv/zlib.py | 1 -
tests/test_api_fail.py | 1 -
tests/test_api_good.py | 1 -
tests/test_api_good_nonlocal.py | 1 -
tests/test_cli.py | 5 +----
tests/test_intersphinx.py | 1 -
10 files changed, 4 insertions(+), 13 deletions(-)
diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py
index 33e1c1d2..05e6797b 100644
--- a/src/sphobjinv/cli/core.py
+++ b/src/sphobjinv/cli/core.py
@@ -33,7 +33,7 @@
from sphobjinv.cli.convert import do_convert
from sphobjinv.cli.load import inv_local, inv_stdin, inv_url
-from sphobjinv.cli.parser import getparser, PrsConst
+from sphobjinv.cli.parser import PrsConst, getparser
from sphobjinv.cli.suggest import do_suggest
from sphobjinv.cli.ui import print_stderr
diff --git a/src/sphobjinv/cli/load.py b/src/sphobjinv/cli/load.py
index 3df47077..469d811f 100644
--- a/src/sphobjinv/cli/load.py
+++ b/src/sphobjinv/cli/load.py
@@ -36,7 +36,7 @@
from jsonschema.exceptions import ValidationError
-from sphobjinv import Inventory, readjson, urlwalk, VersionError
+from sphobjinv import Inventory, VersionError, readjson, urlwalk
from sphobjinv.cli.parser import PrsConst
from sphobjinv.cli.paths import resolve_inpath
from sphobjinv.cli.ui import err_format, print_stderr
diff --git a/src/sphobjinv/inventory.py b/src/sphobjinv/inventory.py
index f5e15f0d..33c685e5 100644
--- a/src/sphobjinv/inventory.py
+++ b/src/sphobjinv/inventory.py
@@ -39,7 +39,7 @@
import jsonschema
from jsonschema.exceptions import ValidationError
-from sphobjinv.data import _utf8_encode, DataObjStr
+from sphobjinv.data import DataObjStr, _utf8_encode
from sphobjinv.enum import HeaderFields, SourceTypes
from sphobjinv.fileops import readbytes
from sphobjinv.re import pb_data, pb_project, pb_version
diff --git a/src/sphobjinv/re.py b/src/sphobjinv/re.py
index c51605f9..dfe4c9e6 100644
--- a/src/sphobjinv/re.py
+++ b/src/sphobjinv/re.py
@@ -34,7 +34,6 @@
from sphobjinv.data import DataFields as DF # noqa: N817
from sphobjinv.enum import HeaderFields as HF # noqa: N817
-
#: Compiled |re| |bytes| pattern for comment lines in decompressed
#: inventory files
pb_comments = re.compile(b"^#.*$", re.M)
diff --git a/src/sphobjinv/zlib.py b/src/sphobjinv/zlib.py
index eb597a69..649d54c6 100644
--- a/src/sphobjinv/zlib.py
+++ b/src/sphobjinv/zlib.py
@@ -33,7 +33,6 @@
import os
import zlib
-
BUFSIZE = 16 * 1024 # 16k chunks
diff --git a/tests/test_api_fail.py b/tests/test_api_fail.py
index 88e31d9f..7e65ce65 100644
--- a/tests/test_api_fail.py
+++ b/tests/test_api_fail.py
@@ -37,7 +37,6 @@
import sphobjinv as soi
-
pytestmark = [pytest.mark.api, pytest.mark.local]
diff --git a/tests/test_api_good.py b/tests/test_api_good.py
index 67dbe35e..a0433dec 100644
--- a/tests/test_api_good.py
+++ b/tests/test_api_good.py
@@ -39,7 +39,6 @@
import sphobjinv as soi
-
pytestmark = [pytest.mark.api, pytest.mark.local]
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index ac1907fc..2ba01490 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -33,7 +33,6 @@
import sphobjinv as soi
-
pytestmark = [
pytest.mark.api,
pytest.mark.nonloc,
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 5e9c1a22..c83ac650 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -39,10 +39,7 @@
import pytest
from stdio_mgr import stdio_mgr
-from sphobjinv import HeaderFields
-from sphobjinv import Inventory
-from sphobjinv import SourceTypes
-
+from sphobjinv import HeaderFields, Inventory, SourceTypes
CLI_TEST_TIMEOUT = 2
CLI_CMDS = ["sphobjinv", "python -m sphobjinv"]
diff --git a/tests/test_intersphinx.py b/tests/test_intersphinx.py
index 64e681c3..b2ff2de3 100644
--- a/tests/test_intersphinx.py
+++ b/tests/test_intersphinx.py
@@ -33,7 +33,6 @@
import sphobjinv.cli.suggest as soi_cli_suggest
-
pytestmark = [pytest.mark.intersphinx, pytest.mark.local]
From 3e9f9bc0cb25729210d4ff5d1fc711902c184c38 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 19:24:09 -0500
Subject: [PATCH 048/122] Update CHANGELOG
---
CHANGELOG.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26079131..7d39789b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,17 @@ changes.
#### Internal
+ * Augment `black` and `flake8` `tox` envs to run `--version` first ([#327]).
+
+ * Remove `-r requirements-flake.txt` from `requirements-dev.txt` ([#327]).
+ * `flake8` should always be run via `tox`.
+
+ * Add `tox` env to run `isort` and execute across codebase ([#327]).
+
+ * Add `flake8-isort` to `flake8` requirements and remove `flake8-import-order`
+ ([#327]).
+ * Also remove `flake8-import-order` config from `tox.ini`.
+
* Bump dev-pin Sphinx to v8.1.3 ([#325]).
* Two different version constraints at the moment:
* Sphinx v8.2 doesn't support Python 3.10 (primary constraint)
@@ -727,3 +738,4 @@ changes.
[#316]: https://github.com/bskinn/sphobjinv/pull/316
[#320]: https://github.com/bskinn/sphobjinv/pull/320
[#325]: https://github.com/bskinn/sphobjinv/pull/325
+[#327]: https://github.com/bskinn/sphobjinv/pull/327
From 12518da3a3ae927c8970a649182f9bffec5ae6fc Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 20:04:25 -0500
Subject: [PATCH 049/122] Empty textconv TestConverGood
Regular convert stuff not relevant
---
tests/test_cli_textconv.py | 242 +------------------------------------
1 file changed, 4 insertions(+), 238 deletions(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index 87aac3f1..cfb687f1 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -91,245 +91,11 @@ def test_cli_invocations(self, cmd):
# assert "invalid choice: 'foo'" in err_.getvalue()
-# class TestConvertGood:
-# """Tests for expected-good convert functionality."""
-
-# @pytest.mark.parametrize(
-# ["out_ext", "cli_arg"],
-# [(".txt", "plain"), (".inv", "zlib"), (".json", "json")],
-# ids=(lambda i: "" if i.startswith(".") else i),
-# )
-# @pytest.mark.parametrize(
-# "in_ext", [".txt", ".inv", ".json"], ids=(lambda i: i.split(".")[-1])
-# )
-# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
-# def test_cli_convert_default_outname(
-# self,
-# in_ext,
-# out_ext,
-# cli_arg,
-# scratch_path,
-# run_cmdline_test,
-# decomp_cmp_test,
-# sphinx_load_test,
-# misc_info,
-# ):
-# """Confirm cmdline conversions with only input file arg."""
-# if in_ext == out_ext:
-# pytest.skip("Ignore no-change conversions")
-
-# src_path = scratch_path / (misc_info.FNames.INIT + in_ext)
-# dest_path = scratch_path / (misc_info.FNames.INIT + out_ext)
-
-# assert src_path.is_file()
-# assert dest_path.is_file()
-
-# dest_path.unlink()
-
-# cli_arglist = ["convert", cli_arg, str(src_path)]
-# run_cmdline_test(cli_arglist)
-# assert dest_path.is_file()
-
-# if cli_arg == "zlib":
-# sphinx_load_test(dest_path)
-# if cli_arg == "plain":
-# decomp_cmp_test(dest_path)
-
-# @pytest.mark.timeout(CLI_TEST_TIMEOUT * 2)
-# def test_cli_convert_expandcontract(
-# self, scratch_path, misc_info, run_cmdline_test
-# ):
-# """Confirm cmdline contract decompress of zlib with input file arg."""
-# cmp_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)
-# dec_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.DEC)
-# recmp_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
-
-# run_cmdline_test(["convert", "plain", "-e", str(cmp_path), str(dec_path)])
-# assert dec_path.is_file()
-
-# run_cmdline_test(["convert", "zlib", "-c", str(dec_path), str(recmp_path)])
-# assert recmp_path.is_file()
-
-# @pytest.mark.parametrize(
-# "dst_name", [True, False], ids=(lambda v: "dst_name" if v else "no_dst_name")
-# )
-# @pytest.mark.parametrize(
-# "dst_path", [True, False], ids=(lambda v: "dst_path" if v else "no_dst_path")
-# )
-# @pytest.mark.parametrize(
-# "src_path", [True, False], ids=(lambda v: "src_path" if v else "no_src_path")
-# )
-# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
-# def test_cli_convert_various_pathargs(
-# self,
-# src_path,
-# dst_path,
-# dst_name,
-# scratch_path,
-# misc_info,
-# run_cmdline_test,
-# decomp_cmp_test,
-# monkeypatch,
-# ):
-# """Confirm the various src/dest path/file combinations work."""
-# init_dst_fname = misc_info.FNames.INIT + misc_info.Extensions.DEC
-# mod_dst_fname = misc_info.FNames.MOD + misc_info.Extensions.DEC
-
-# src_path = (scratch_path.resolve() if src_path else Path(".")) / (
-# misc_info.FNames.INIT + misc_info.Extensions.CMP
-# )
-# dst_path = (scratch_path.resolve() if dst_path else Path(".")) / (
-# mod_dst_fname if dst_name else ""
-# )
-
-# full_dst_path = scratch_path.resolve() / (
-# mod_dst_fname if dst_name else init_dst_fname
-# )
-
-# assert (scratch_path / init_dst_fname).is_file()
-# (scratch_path / init_dst_fname).unlink()
-
-# with monkeypatch.context() as m:
-# m.chdir(scratch_path)
-# run_cmdline_test(["convert", "plain", str(src_path), str(dst_path)])
-# assert full_dst_path.is_file()
-
-# decomp_cmp_test(full_dst_path)
-
-# @pytest.mark.timeout(CLI_TEST_TIMEOUT * 50 * 3)
-# @pytest.mark.testall
-# def test_cli_convert_cycle_formats(
-# self,
-# testall_inv_path,
-# res_path,
-# scratch_path,
-# run_cmdline_test,
-# misc_info,
-# pytestconfig,
-# check,
-# ):
-# """Confirm conversion in a loop, reading/writing all formats."""
-# res_src_path = res_path / testall_inv_path
-# plain_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.DEC)
-# json_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.JSON)
-# zlib_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
-
-# if (
-# not pytestconfig.getoption("--testall")
-# and testall_inv_path.name != "objects_attrs.inv"
-# ):
-# pytest.skip("'--testall' not specified")
-
-# run_cmdline_test(["convert", "plain", str(res_src_path), str(plain_path)])
-# run_cmdline_test(["convert", "json", str(plain_path), str(json_path)])
-# run_cmdline_test(["convert", "zlib", str(json_path), str(zlib_path)])
-
-# invs = {
-# "orig": Inventory(str(res_src_path)),
-# "plain": Inventory(str(plain_path)),
-# "zlib": Inventory(str(zlib_path)),
-# "json": Inventory(json.loads(json_path.read_text())),
-# }
-
-# for fmt, attrib in product(
-# ("plain", "zlib", "json"),
-# (
-# HeaderFields.Project.value,
-# HeaderFields.Version.value,
-# HeaderFields.Count.value,
-# ),
-# ):
-# check.equal(getattr(invs[fmt], attrib), getattr(invs["orig"], attrib))
-
-# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
-# def test_cli_overwrite_prompt_and_behavior(
-# self, res_path, scratch_path, misc_info, run_cmdline_test
-# ):
-# """Confirm overwrite prompt works properly."""
-# src_path_1 = res_path / "objects_attrs.inv"
-# src_path_2 = res_path / "objects_sarge.inv"
-# dst_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.DEC)
-# dst_path.unlink()
-
-# args = ["convert", "plain", None, str(dst_path)]
-
-# # Initial decompress
-# args[2] = str(src_path_1)
-# with stdio_mgr() as (in_, out_, err_):
-# run_cmdline_test(args)
-
-# assert "converted" in err_.getvalue()
-# assert "(plain)" in err_.getvalue()
-
-# # First overwrite, declining clobber
-# args[2] = str(src_path_2)
-# with stdio_mgr("n\n") as (in_, out_, err_):
-# run_cmdline_test(args)
-
-# assert "(Y/N)? n" in out_.getvalue()
-
-# assert "attrs" == Inventory(str(dst_path)).project
-
-# # Second overwrite, with clobber
-# with stdio_mgr("y\n") as (in_, out_, err_):
-# run_cmdline_test(args)
-
-# assert "(Y/N)? y" in out_.getvalue()
-
-# assert "Sarge" == Inventory(str(dst_path)).project
-
-# def test_cli_stdin_clobber(
-# self, res_path, scratch_path, misc_info, run_cmdline_test
-# ):
-# """Confirm clobber with stdin data only with --overwrite."""
-# src_path_sarge = res_path / "objects_sarge.inv"
-# dst_path = scratch_path / (misc_info.FNames.INIT + misc_info.Extensions.CMP)
-
-# assert "attrs" == Inventory(dst_path).project
-
-# data = json.dumps(Inventory(src_path_sarge).json_dict())
-
-# args = ["convert", "plain", "-", str(dst_path)]
-# with stdio_mgr(data):
-# run_cmdline_test(args)
-# assert "attrs" == Inventory(dst_path).project
-
-# args.append("-o")
-# with stdio_mgr(data):
-# run_cmdline_test(args)
-# assert "Sarge" == Inventory(dst_path).project
-
-# def test_cli_json_no_metadata_url(
-# self, res_cmp, scratch_path, misc_info, run_cmdline_test
-# ):
-# """Confim JSON generated from local inventory has no url in metadata."""
-# json_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.JSON)
-
-# run_cmdline_test(
-# ["convert", "json", str(res_cmp.resolve()), str(json_path.resolve())]
-# )
-
-# d = json.loads(json_path.read_text())
-
-# assert "url" not in d.get("metadata", {})
-
-# def test_cli_json_export_import(
-# self, res_cmp, scratch_path, misc_info, run_cmdline_test, sphinx_load_test
-# ):
-# """Confirm JSON sent to stdout from local source imports ok."""
-# mod_path = scratch_path / (misc_info.FNames.MOD + misc_info.Extensions.CMP)
-
-# with stdio_mgr() as (in_, out_, err_):
-# run_cmdline_test(["convert", "json", str(res_cmp.resolve()), "-"])
-
-# data = out_.getvalue()
+class TestConvertGood:
+ """Tests for expected-good convert functionality."""
+
+
-# with stdio_mgr(data) as (in_, out_, err_):
-# run_cmdline_test(["convert", "zlib", "-", str(mod_path.resolve())])
-
-# assert Inventory(json.loads(data))
-# assert Inventory(mod_path)
-# sphinx_load_test(mod_path)
class TestFail:
From 16b263502aad524aef49a68aa33ec983fefe1263 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 20:12:28 -0500
Subject: [PATCH 050/122] Add test matching core and textconv stdout conversion
---
tests/test_cli_textconv.py | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index cfb687f1..3559c5f2 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -39,7 +39,6 @@
from sphobjinv import Inventory
from tests.enum import CLICommand
-
CLI_TEST_TIMEOUT = 2
CLI_CMDS = ["sphobjinv-textconv"]
@@ -94,8 +93,19 @@ def test_cli_invocations(self, cmd):
class TestConvertGood:
"""Tests for expected-good convert functionality."""
-
+ def test_textconv_matches_main_conv(self, res_cmp, run_cmdline_test):
+ """Ensure that textconv conversion matches main CLI conversion."""
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test(
+ ["convert", "plain", res_cmp, "-"], command=CLICommand.Core
+ )
+ core_output = out_.getvalue()
+
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test([res_cmp], command=CLICommand.Textconv)
+ textconv_output = out_.getvalue()
+ assert core_output == textconv_output
class TestFail:
From ec48e5335b66e69438dac9f22d27ce518cf9d1a4 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 20:35:59 -0500
Subject: [PATCH 051/122] Convert textconv parser's 'version' report to
'version' action
Also make a short version output for textconf entrypoint.
Want 'infile' to show as required in the help, and looks like it's not
currently possible to render a multiline version string using
the 'version' action; and, treating -v as a normal flag means that
the inputs fail to parse when no 'infile' is passed.
---
src/sphobjinv/cli/core.py | 5 +----
src/sphobjinv/cli/parser.py | 6 +++++-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py
index 084ba627..4adbdf2c 100644
--- a/src/sphobjinv/cli/core.py
+++ b/src/sphobjinv/cli/core.py
@@ -112,10 +112,7 @@ def main_textconv():
prs = getparser_textconv()
params = vars(prs.parse_args())
- # Print version &c. and exit if indicated
- if params[PrsConst.VERSION]:
- print(PrsConst.VER_TXT)
- sys.exit(0)
+ # No version arg handling, using 'version' action in this parser
inv, in_path = inv_local(params)
diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py
index f831a167..19727e18 100644
--- a/src/sphobjinv/cli/parser.py
+++ b/src/sphobjinv/cli/parser.py
@@ -52,6 +52,9 @@ class PrsConst:
" https://sphobjinv.readthedocs.io\n"
)
+ #: Short version text for textconv entrypoint
+ VER_TXT_SHORT = f"sphobjinv v{__version__}"
+
# ### Subparser selectors and argparse param for storing subparser name
#: Subparser name for inventory file conversions; stored in
#: :data:`SUBPARSER_NAME` when selected
@@ -405,7 +408,8 @@ def getparser_textconv():
"-" + PrsConst.VERSION[0],
"--" + PrsConst.VERSION,
help="Print package version & other info",
- action="store_true",
+ action="version",
+ version=PrsConst.VER_TXT_SHORT,
)
prs.add_argument(
From 63c4d6e4aa8e8b3c7073b815fe01d5476f4ce82b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 20:37:27 -0500
Subject: [PATCH 052/122] Convert some basic CLI-behavior tests
---
tests/test_cli_textconv.py | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index 3559c5f2..578b46fa 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -29,6 +29,7 @@
"""
+import re
import shlex
import subprocess as sp # noqa: S404
from pathlib import Path
@@ -60,18 +61,18 @@ def test_cli_invocations(self, cmd):
assert "sphobjinv" in out
assert "infile" in out
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_cli_version_exits_ok(self, run_cmdline_test):
- # """Confirm --version exits cleanly."""
- # run_cmdline_test(["-v"])
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_cli_version_exits_ok(self, run_cmdline_test):
+ """Confirm --version exits cleanly."""
+ run_cmdline_test(["-v"], command=CLICommand.Textconv)
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_cli_noargs_shows_help(self, run_cmdline_test):
- # """Confirm help shown when invoked with no arguments."""
- # with stdio_mgr() as (in_, out_, err_):
- # run_cmdline_test([])
+ @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+ def test_cli_noargs_shows_help(self, run_cmdline_test):
+ """Confirm help shown when invoked with no arguments."""
+ with stdio_mgr() as (in_, out_, err_):
+ run_cmdline_test([], command=CLICommand.Textconv)
- # assert "usage: sphobjinv" in out_.getvalue()
+ assert re.search("usage.+sphobjinv", out_.getvalue(), re.I)
# @pytest.mark.timeout(CLI_TEST_TIMEOUT)
# def test_cli_no_subparser_prs_exit(self, run_cmdline_test):
From b7c1556bd29ab7cef66805c06137988fda278e84 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Wed, 24 Dec 2025 20:38:31 -0500
Subject: [PATCH 053/122] Remove last old tests
No subparser for textconv, so no need for subparser tests
---
tests/test_cli_textconv.py | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index 578b46fa..781f1b85 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -74,22 +74,6 @@ def test_cli_noargs_shows_help(self, run_cmdline_test):
assert re.search("usage.+sphobjinv", out_.getvalue(), re.I)
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_cli_no_subparser_prs_exit(self, run_cmdline_test):
- # """Confirm exit code 2 if option passed but no subparser provided."""
- # with stdio_mgr() as (in_, out_, err_):
- # run_cmdline_test(["--foo"], expect=2)
-
- # assert "error: No subparser selected" in err_.getvalue()
-
- # @pytest.mark.timeout(CLI_TEST_TIMEOUT)
- # def test_cli_bad_subparser_prs_exit(self, run_cmdline_test):
- # """Confirm exit code 2 if invalid subparser provided."""
- # with stdio_mgr() as (in_, out_, err_):
- # run_cmdline_test(["foo"], expect=2)
-
- # assert "invalid choice: 'foo'" in err_.getvalue()
-
class TestConvertGood:
"""Tests for expected-good convert functionality."""
From c4a8b71d74bf4be184be25b8fc8ec3e63ee9c898 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 15:11:18 -0500
Subject: [PATCH 054/122] Remove remainder of flake8-import-order config from
tox.ini
---
tox.ini | 3 ---
1 file changed, 3 deletions(-)
diff --git a/tox.ini b/tox.ini
index 5c9257a3..2429ab3b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -200,6 +200,3 @@ per_file_ignores =
src/sphobjinv/cli/__init__.py: F401, RST305,RST306
# PIE786: CLI uses 'except Exception:' as a catchall... to be changed, eventually
src/sphobjinv/cli/*: PIE786, RST305,RST306
-
-#flake8-import-order
-import-order-style = smarkets
From fbb17701792c534ae7f2e91fa864aceaf83e09da Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 15:33:09 -0500
Subject: [PATCH 055/122] Add descriptions to tox envs and make linkcheck
Linux-only
---
tox.ini | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/tox.ini b/tox.ini
index 2429ab3b..c9207995 100644
--- a/tox.ini
+++ b/tox.ini
@@ -94,6 +94,7 @@ basepython=
py310: python3.10
[testenv:black]
+description=Autoformat code and tests with black
skip_install=True
deps=black
commands=
@@ -101,6 +102,7 @@ commands=
black {posargs} .
[testenv:flake8]
+description=Lint code and tests with flake8
skip_install=True
deps=-rrequirements-flake8.txt
commands=
@@ -108,6 +110,7 @@ commands=
flake8 {posargs} src tests
[testenv:flake8_noqa]
+description=Lint noqa directives with flake8-noqa
skip_install=True
deps=-rrequirements-flake8.txt
commands=
@@ -115,6 +118,7 @@ commands=
flake8 --color=never --exit-zero {posargs} tests src
[testenv:interrogate]
+description=Lint docstrings with interrogate
skip_install=True
deps=interrogate
commands=
@@ -129,6 +133,8 @@ commands=
isort {posargs} src tests
[testenv:linkcheck]
+description=Run Sphinx linkcheck on docs (Linux only)
+platform=linux
skip_install=True
deps=-rrequirements-dev.txt
allowlist_externals=
@@ -138,6 +144,7 @@ commands=
make linkcheck
[testenv:sdist_install]
+description=Confirm that sdist installs and imports
commands=
python -Werror -c "import sphobjinv"
deps=
From d1ce02caa51b047fb702b654aea99c1fd4d763f6 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:13:46 -0500
Subject: [PATCH 056/122] Add 'sphobjinv' explicitly to convert/suggest docs
page headings
The main entrypoint is no longer the only one defined on the project.
---
doc/source/cli/convert.rst | 4 ++--
doc/source/cli/suggest.rst | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/doc/source/cli/convert.rst b/doc/source/cli/convert.rst
index b979fc78..5ad54de5 100644
--- a/doc/source/cli/convert.rst
+++ b/doc/source/cli/convert.rst
@@ -1,7 +1,7 @@
.. Description of convert commandline usage
-Command-Line Usage: "convert" Subcommand
-========================================
+Command-Line Usage: "sphobjinv convert" Subcommand
+==================================================
.. program:: sphobjinv convert
diff --git a/doc/source/cli/suggest.rst b/doc/source/cli/suggest.rst
index 08627aa8..3afca368 100644
--- a/doc/source/cli/suggest.rst
+++ b/doc/source/cli/suggest.rst
@@ -1,7 +1,7 @@
.. Description of suggest commandline usage
-Command-Line Usage: "suggest" Subcommand
-========================================
+Command-Line Usage: "sphobjinv suggest" Subcommand
+==================================================
.. program:: sphobjinv suggest
From 73f76fd7d8f39095777c6de4db5d8e45e3af8383 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:14:22 -0500
Subject: [PATCH 057/122] Add CLI entrypoint replaces in conf.py rst_epilog
---
doc/source/conf.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/doc/source/conf.py b/doc/source/conf.py
index f3afc081..1a6d3da9 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -202,6 +202,12 @@
.. |resolve_inpath| replace:: :func:`~sphobjinv.cli.paths.resolve_inpath`
+.. |sphobjinv-textconv| replace:: ``sphobjinv-textconv``
+
+.. |sphobjinv convert| replace:: ``sphobjinv convert``
+
+.. |sphobjinv suggest| replace:: ``sphobjinv suggest``
+
"""
From 9bebf6fb8ea3940ff8c3c5005fee434d5c34a0c1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:37:31 -0500
Subject: [PATCH 058/122] Add soi_root.rst
---
doc/source/cli/soi_root.rst | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 doc/source/cli/soi_root.rst
diff --git a/doc/source/cli/soi_root.rst b/doc/source/cli/soi_root.rst
new file mode 100644
index 00000000..83ca6337
--- /dev/null
+++ b/doc/source/cli/soi_root.rst
@@ -0,0 +1,26 @@
+:orphan:
+
+.. Description of root soi entrypoint commandline usage
+
+Root "sphobjinv" Entrypoint Usage
+=================================
+
+.. program:: sphobjinv
+
+The following options of the 'root' of the ``sphobjinv`` CLI entrypoint are
+documented in this 'orphan' page so that they will appear in the docset
+`objects.inv` and be available for linking, if needed, but otherwise be hidden
+from the navigation of the documentation.
+
+.. option:: -h, --help
+
+ Show help message and exit
+
+.. program-output:: sphobjinv --help
+
+
+.. option:: -v, --version
+
+ Print package version & other info
+
+.. program-output:: sphobjinv --version
From 2c50eeb051032c44fdfe0f2755199d57fe6ac2bc Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:40:32 -0500
Subject: [PATCH 059/122] Remove 'core' "sphobjinv" entrypoint options from
index.rst
---
doc/source/cli/index.rst | 19 -------------------
1 file changed, 19 deletions(-)
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst
index 1974dbdc..68e449f2 100644
--- a/doc/source/cli/index.rst
+++ b/doc/source/cli/index.rst
@@ -35,25 +35,6 @@ Some notes on these CLI docs:
* |cour|\ file_head\ |/cour| is a helper function
that retrieves the head of a specified file.
-
-.. program:: sphobjinv
-
-The options for the parent |soi| command are:
-
-.. option:: -h, --help
-
- Show help message and exit
-
-.. program-output:: sphobjinv --help
-
-
-.. option:: -v, --version
-
- Print package version & other info
-
-.. program-output:: sphobjinv --version
-
-
.. toctree::
:maxdepth: 1
:hidden:
From 9fc04f09286d5ecf5182afd0b286960309a47269 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:41:01 -0500
Subject: [PATCH 060/122] Rename page "API" -> "API Reference"
---
doc/source/api/index.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst
index 04c908fc..480baf12 100644
--- a/doc/source/api/index.rst
+++ b/doc/source/api/index.rst
@@ -1,7 +1,7 @@
.. API page
-API
-===
+API Reference
+=============
Most (all?) of the objects documented in the below submodules
are also exposed at the |soi| package root. For example,
From 28068544337b19cd565e2d729ecec7d8ad06d6e0 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:48:56 -0500
Subject: [PATCH 061/122] Revise headings of convert & suggest subcommand pages
---
doc/source/cli/convert.rst | 4 ++--
doc/source/cli/suggest.rst | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/doc/source/cli/convert.rst b/doc/source/cli/convert.rst
index 5ad54de5..f3871938 100644
--- a/doc/source/cli/convert.rst
+++ b/doc/source/cli/convert.rst
@@ -1,7 +1,7 @@
.. Description of convert commandline usage
-Command-Line Usage: "sphobjinv convert" Subcommand
-==================================================
+Command-Line Usage: ``sphobjinv convert``
+=========================================
.. program:: sphobjinv convert
diff --git a/doc/source/cli/suggest.rst b/doc/source/cli/suggest.rst
index 3afca368..b49b0325 100644
--- a/doc/source/cli/suggest.rst
+++ b/doc/source/cli/suggest.rst
@@ -1,7 +1,7 @@
.. Description of suggest commandline usage
-Command-Line Usage: "sphobjinv suggest" Subcommand
-==================================================
+Command-Line Usage: ``sphobjinv suggest``
+=========================================
.. program:: sphobjinv suggest
From e2b9ac3d081aab26de1d70bd9669aba9afbb94ff Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 18:49:08 -0500
Subject: [PATCH 062/122] Rework majority of CLI Usage index.rst
---
doc/source/cli/index.rst | 35 ++++++++++++++++++++++++-----------
1 file changed, 24 insertions(+), 11 deletions(-)
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst
index 68e449f2..ebd337ee 100644
--- a/doc/source/cli/index.rst
+++ b/doc/source/cli/index.rst
@@ -3,19 +3,31 @@
Command-Line Usage
==================
-The CLI for |soi| is implemented using two subcommands:
+The primary CLI for |soi| is implemented using two subcommands of the
+``sphobjinv`` entrypoint:
- - A :doc:`convert ` subcommand, which handles conversion of
- inventories between supported formats (currently zlib-compressed,
+ - ``sphobjinv convert`` (:doc:`docs page `), which handles conversion
+ of inventories between supported formats (currently zlib-compressed,
plaintext, and JSON).
- - A :doc:`suggest ` subcommand, which provides suggestions for
+ - ``sphobjinv suggest`` (:doc:`docs page `), which provides suggestions for
objects in an inventory matching a desired search term.
-More information about the underlying implementation of these subcommands can
-be found :doc:`here ` and in the documentation for the
-:class:`~sphobjinv.inventory.Inventory` object, in particular the
-:meth:`~sphobjinv.inventory.Inventory.data_file` and
-:meth:`~sphobjinv.inventory.Inventory.suggest` methods.
+As of v##VER##, |soi| also provides an auxiliary entrypoint,
+``sphobjinv-textconv`` (:doc:`docs page `), which takes a path
+to a file on disk as its single required argument. This entrypoint attempts
+to instantiate an |Inventory| with this file and emit its plaintext
+contents to |stdout|. The following two invocations are thus synonymous::
+
+ $ sphobjinv convert plain path/to/objects.inv -
+
+ $ sphobjinv-textconv path/to/objects.inv
+
+This alternative spelling is less awkward when configuring a Git ``textconv`` to
+allow rendering diffs of |objects.inv| files in plaintext. See the
+``sphobjinv-textconv`` :doc:`entrypoint documentation ` for more
+information.
+
+----
Some notes on these CLI docs:
@@ -39,5 +51,6 @@ Some notes on these CLI docs:
:maxdepth: 1
:hidden:
- "convert" Mode
- "suggest" Mode
+ sphobjinv convert
+ sphobjinv suggest
+ sphobjinv-textconv
From f37dabb9eed721fc48af07da34a4c64e34abfd2a Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 19:11:21 -0500
Subject: [PATCH 063/122] Start work drafting textconv.rst
---
doc/source/cli/textconv.rst | 73 +++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
create mode 100644 doc/source/cli/textconv.rst
diff --git a/doc/source/cli/textconv.rst b/doc/source/cli/textconv.rst
new file mode 100644
index 00000000..af419373
--- /dev/null
+++ b/doc/source/cli/textconv.rst
@@ -0,0 +1,73 @@
+.. Description of sphobjinv-textconv commandline usage
+
+Command-Line Usage: ``sphobjinv-textconv``
+==========================================
+
+.. program:: sphobjinv-textconv
+
+``sphobjinv-textconv`` is intentionally implemented with very narrow functionality,
+specifically to simplify configuring |soi| for use as a
+`Git "textconv" `__,
+which is a mechanism for rendering binary files in a diff-able text format. There are
+many examples of `clever`_ application of textconv in the wild.
+
+Ultimately, a textconv involves three things:
+
+1. A utility that takes in a file path as a single positional argument.
+
+2. An entry somewhere in Git config declaring a
+
+
+----
+
+Basic file conversion to the default output filename is straightforward:
+
+.. doctest:: convert_main
+
+ >>> Path('objects_attrs.txt').is_file()
+ False
+ >>> cli_run('sphobjinv convert plain objects_attrs.inv')
+
+ Conversion completed.
+ '...objects_attrs.inv' converted to '...objects_attrs.txt' (plain).
+
+
+ >>> print(file_head('objects_attrs.txt', head=6))
+ # Sphinx inventory version 2
+ # Project: attrs
+ # Version: 22.1
+ # The remainder of this file is compressed using zlib.
+ attr py:module 0 index.html#module-$ -
+ attr.VersionInfo py:class 1 api.html#$ -
+
+A different target filename can be specified, to avoid overwriting an existing
+file:
+
+
+
+
+**Usage**
+
+.. command-output:: sphobjinv-textconv --help
+ :ellipsis: 4
+
+
+**Positional Arguments**
+
+.. option:: infile
+
+ Path to file to be emitted to |stdout| in plaintext.
+
+**Flags**
+
+.. option:: -h, --help
+
+ Display help message and exit.
+
+.. option:: -v, --version
+
+ Display brief package version information and exit.
+
+.. versionadded:: ##VER##
+
+.. _clever: https://github.com/syntevosmartgit/textconv
From bc03a22a164ce6a07ba00ae9558ea6a25db3b63e Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 20:26:36 -0500
Subject: [PATCH 064/122] Remove/add some replace:: in conf.py rst_epilog
---
doc/source/conf.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 1a6d3da9..10bb972f 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -202,11 +202,11 @@
.. |resolve_inpath| replace:: :func:`~sphobjinv.cli.paths.resolve_inpath`
-.. |sphobjinv-textconv| replace:: ``sphobjinv-textconv``
+.. |textconv| replace:: |cour|\ textconv\ |/cour|
-.. |sphobjinv convert| replace:: ``sphobjinv convert``
+.. |.gitattributes| replace:: |cour|\ .gitattributes\ |/cour|
-.. |sphobjinv suggest| replace:: ``sphobjinv suggest``
+.. |sphobjinv-textconv| replace:: |cour|\ sphobjinv-textconv\ |/cour|
"""
From 508659a119ff690bdbdb264e2d85e7658fd0cbe4 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 28 Dec 2025 20:27:00 -0500
Subject: [PATCH 065/122] Further add/revise to index.rst & textconv.rst
---
doc/source/cli/index.rst | 20 ++++++-----
doc/source/cli/textconv.rst | 71 +++++++++++++++++++++----------------
2 files changed, 53 insertions(+), 38 deletions(-)
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst
index ebd337ee..2d26cc07 100644
--- a/doc/source/cli/index.rst
+++ b/doc/source/cli/index.rst
@@ -13,25 +13,29 @@ The primary CLI for |soi| is implemented using two subcommands of the
objects in an inventory matching a desired search term.
As of v##VER##, |soi| also provides an auxiliary entrypoint,
-``sphobjinv-textconv`` (:doc:`docs page `), which takes a path
-to a file on disk as its single required argument. This entrypoint attempts
-to instantiate an |Inventory| with this file and emit its plaintext
-contents to |stdout|. The following two invocations are thus synonymous::
+``sphobjinv-textconv`` (:doc:`docs page `), which takes one required
+argument: a path to a file on disk. This entrypoint attempts to instantiate an
+|Inventory| with this file and emit its plaintext contents to |stdout| with no
+cosmetic whitespace. The following two invocations are thus nearly synonymous::
$ sphobjinv convert plain path/to/objects.inv -
$ sphobjinv-textconv path/to/objects.inv
-This alternative spelling is less awkward when configuring a Git ``textconv`` to
-allow rendering diffs of |objects.inv| files in plaintext. See the
+(Be sure to note the final hyphen in the first command.) The
+``sphobjinv-textconv`` spelling is less awkward when configuring a Git
+|textconv| to allow rendering diffs of |objects.inv| files in plaintext. See the
``sphobjinv-textconv`` :doc:`entrypoint documentation ` for more
information.
----
-Some notes on these CLI docs:
+Shell examples in the CLI docs execute from within |cour|\ /tests/resource\
+|/cour| unless indicated otherwise.
- * CLI docs examples are executed in a sandboxed directory pre-loaded with
+For Python examples:
+
+ * Examples are executed in a sandboxed directory pre-loaded with
|cour|\ objects_attrs.inv\ |/cour| (from, e.g.,
`here `__).
diff --git a/doc/source/cli/textconv.rst b/doc/source/cli/textconv.rst
index af419373..47e18b12 100644
--- a/doc/source/cli/textconv.rst
+++ b/doc/source/cli/textconv.rst
@@ -5,45 +5,58 @@ Command-Line Usage: ``sphobjinv-textconv``
.. program:: sphobjinv-textconv
-``sphobjinv-textconv`` is intentionally implemented with very narrow functionality,
-specifically to simplify configuring |soi| for use as a
-`Git "textconv" `__,
-which is a mechanism for rendering binary files in a diff-able text format. There are
-many examples of `clever`_ application of textconv in the wild.
+``sphobjinv-textconv`` is intentionally implemented with very narrow
+functionality, focused on simplifying use of |soi| as a `Git "textconv"
+`__,
+which is a mechanism for rendering binary files in a diff-able text format.
+There are many `examples`__ of `clever application`__ of `textconv`__ in the
+wild.
-Ultimately, a textconv involves three things:
+.. __: https://github.com/pixelb/crudini/issues/90
+.. __: https://github.com/syntevosmartgit/textconv
+.. __: https://stackoverflow.com/questions/55601430/how-to-pass-a-filename-argument-gitconfig-diff-textconv
-1. A utility that takes in a file path as a single positional argument.
-2. An entry somewhere in Git config declaring a
+Ultimately, a textconv requires three things:
+1. A utility that takes in a file path as a single positional argument and emits
+ a plaintext representation to |stdout|, such as |sphobjinv-textconv|.
-----
+2. An entry somewhere in Git config (system, user-global, per-repo, etc.)
+ declaring a "diff driver" set up to use that utility as its |textconv|.
+ Example::
+
+ [diff "objects_inv"]
+ textconv = sphobjinv-textconv
+
+ Note that the utility must be on path in all contexts where you wish to use
+ it as a textconv.
+
+3. An entry somewhere in |.gitattributes| (system, user-global, per-repo, etc.)
+ that associates a particular file or glob pattern with the diff driver. Example::
+
+ *.inv diff=objects_inv
-Basic file conversion to the default output filename is straightforward:
+With |sphobjinv-textconv| configured in this fashion as a textconv for Sphinx
+inventory files, the following should all yield _nearly_ the same output.
-.. doctest:: convert_main
+Using ``sphobjinv convert``:
- >>> Path('objects_attrs.txt').is_file()
- False
- >>> cli_run('sphobjinv convert plain objects_attrs.inv')
-
- Conversion completed.
- '...objects_attrs.inv' converted to '...objects_attrs.txt' (plain).
-
-
- >>> print(file_head('objects_attrs.txt', head=6))
- # Sphinx inventory version 2
- # Project: attrs
- # Version: 22.1
- # The remainder of this file is compressed using zlib.
- attr py:module 0 index.html#module-$ -
- attr.VersionInfo py:class 1 api.html#$ -
+.. command-output:: sphobjinv convert plain objects_pdfminer.inv -
+ :cwd: /../../tests/resource
-A different target filename can be specified, to avoid overwriting an existing
-file:
+Using ``sphobjinv-textconv`` (note the absence of blank lines between the shell
+invocation and the inventory contents):
+.. command-output:: sphobjinv-textconv objects_pdfminer.inv
+ :cwd: /../../tests/resource
+Using ``git show --textconv``:
+
+.. command-output:: git show --textconv HEAD:tests/resource/objects_pdfminer.inv
+ :cwd: /../../tests/resource
+
+----
**Usage**
@@ -69,5 +82,3 @@ file:
Display brief package version information and exit.
.. versionadded:: ##VER##
-
-.. _clever: https://github.com/syntevosmartgit/textconv
From 0dc0b86a5ec0215d0915b6c8ea1480ac5e0a0c0b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 29 Dec 2025 01:28:58 -0500
Subject: [PATCH 066/122] Add on-the-fly Git textconv config to textconv.rst
RtD context doesn't have the Git config in place, statically, so we
have to provide it with the invocation.
---
doc/source/cli/textconv.rst | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/doc/source/cli/textconv.rst b/doc/source/cli/textconv.rst
index 47e18b12..fd13a371 100644
--- a/doc/source/cli/textconv.rst
+++ b/doc/source/cli/textconv.rst
@@ -51,9 +51,10 @@ invocation and the inventory contents):
.. command-output:: sphobjinv-textconv objects_pdfminer.inv
:cwd: /../../tests/resource
-Using ``git show --textconv``:
+Using ``git show --textconv`` (with the |textconv| set on-the-fly so that it
+will render correctly in ReadTheDocs builds):
-.. command-output:: git show --textconv HEAD:tests/resource/objects_pdfminer.inv
+.. command-output:: git -c diff.objects_inv.textconv=sphobjinv-textconv show --textconv HEAD:tests/resource/objects_pdfminer.inv
:cwd: /../../tests/resource
----
From c874091e7af6fb541ded16e55967bac4a7bc5289 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Tue, 30 Dec 2025 01:21:32 -0500
Subject: [PATCH 067/122] Handle the soi version import properly in conf.py
---
doc/source/conf.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 10bb972f..b7066837 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -14,6 +14,8 @@
# import sys
# sys.path.insert(0, os.path.abspath('.'))
+# -- Imports -----------------------------------------------------------------
+import sphobjinv as soi
# -- Project information -----------------------------------------------------
@@ -22,7 +24,8 @@
author = "Brian Skinn"
# The full version for `release`, including alpha/beta/rc tags
-from sphobjinv import __version__ as release
+release = soi.__version__
+
# Just major.minor for `version`
version = ".".join(release.split(".")[:2])
From f6604b4469427214646a0c784c3aced67e0dbc0e Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Tue, 30 Dec 2025 01:25:13 -0500
Subject: [PATCH 068/122] Remove the CLI implementation reference pages
It's an internal interface, we want to discourage people
from using it directly in code.
Replace the couple of meaningful references to CLI-related constants
with automatically-substituted-in current values of those constants
---
doc/source/cli/implementation/convert.rst | 7 ----
doc/source/cli/implementation/core.rst | 8 -----
doc/source/cli/implementation/index.rst | 19 -----------
doc/source/cli/implementation/load.rst | 8 -----
doc/source/cli/implementation/parser.rst | 8 -----
doc/source/cli/implementation/paths.rst | 8 -----
doc/source/cli/implementation/suggest.rst | 7 ----
doc/source/cli/implementation/ui.rst | 8 -----
doc/source/cli/implementation/write.rst | 8 -----
doc/source/cli/suggest.rst | 9 +++---
doc/source/conf.py | 39 ++++-------------------
doc/source/index.rst | 2 --
12 files changed, 10 insertions(+), 121 deletions(-)
delete mode 100644 doc/source/cli/implementation/convert.rst
delete mode 100644 doc/source/cli/implementation/core.rst
delete mode 100644 doc/source/cli/implementation/index.rst
delete mode 100644 doc/source/cli/implementation/load.rst
delete mode 100644 doc/source/cli/implementation/parser.rst
delete mode 100644 doc/source/cli/implementation/paths.rst
delete mode 100644 doc/source/cli/implementation/suggest.rst
delete mode 100644 doc/source/cli/implementation/ui.rst
delete mode 100644 doc/source/cli/implementation/write.rst
diff --git a/doc/source/cli/implementation/convert.rst b/doc/source/cli/implementation/convert.rst
deleted file mode 100644
index cd68000b..00000000
--- a/doc/source/cli/implementation/convert.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. Module API page for cli/convert.py
-
-sphobjinv.cli.convert
-=====================
-
-.. automodule:: sphobjinv.cli.convert
- :members:
diff --git a/doc/source/cli/implementation/core.rst b/doc/source/cli/implementation/core.rst
deleted file mode 100644
index c6fd0a53..00000000
--- a/doc/source/cli/implementation/core.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/core.py
-
-sphobjinv.cli.core
-==================
-
-.. automodule:: sphobjinv.cli.core
- :members:
-
diff --git a/doc/source/cli/implementation/index.rst b/doc/source/cli/implementation/index.rst
deleted file mode 100644
index a174e6f3..00000000
--- a/doc/source/cli/implementation/index.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. Module API page for CLI submodule code
-
-sphobjinv.cli (non-API)
-=======================
-
-.. toctree::
- :maxdepth: 1
-
- convert
- core
- load
- parser
- paths
- suggest
- ui
- write
-
-
-.. .. |argparse| replace:: :mod:`argparse`
diff --git a/doc/source/cli/implementation/load.rst b/doc/source/cli/implementation/load.rst
deleted file mode 100644
index 2cdec7ec..00000000
--- a/doc/source/cli/implementation/load.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/load.py
-
-sphobjinv.cli.load
-==================
-
-.. automodule:: sphobjinv.cli.load
- :members:
-
diff --git a/doc/source/cli/implementation/parser.rst b/doc/source/cli/implementation/parser.rst
deleted file mode 100644
index a7a18aa1..00000000
--- a/doc/source/cli/implementation/parser.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/parser.py
-
-sphobjinv.cli.parser
-====================
-
-.. automodule:: sphobjinv.cli.parser
- :members:
-
diff --git a/doc/source/cli/implementation/paths.rst b/doc/source/cli/implementation/paths.rst
deleted file mode 100644
index dad6f38b..00000000
--- a/doc/source/cli/implementation/paths.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/paths.py
-
-sphobjinv.cli.paths
-===================
-
-.. automodule:: sphobjinv.cli.paths
- :members:
-
diff --git a/doc/source/cli/implementation/suggest.rst b/doc/source/cli/implementation/suggest.rst
deleted file mode 100644
index 3424d1b5..00000000
--- a/doc/source/cli/implementation/suggest.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. Module API page for cli/suggest.py
-
-sphobjinv.cli.suggest
-=====================
-
-.. automodule:: sphobjinv.cli.suggest
- :members:
diff --git a/doc/source/cli/implementation/ui.rst b/doc/source/cli/implementation/ui.rst
deleted file mode 100644
index bd59146d..00000000
--- a/doc/source/cli/implementation/ui.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/ui.py
-
-sphobjinv.cli.ui
-================
-
-.. automodule:: sphobjinv.cli.ui
- :members:
-
diff --git a/doc/source/cli/implementation/write.rst b/doc/source/cli/implementation/write.rst
deleted file mode 100644
index f1eb55a8..00000000
--- a/doc/source/cli/implementation/write.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. Module API page for cli/write.py
-
-sphobjinv.cli.write
-===================
-
-.. automodule:: sphobjinv.cli.write
- :members:
-
diff --git a/doc/source/cli/suggest.rst b/doc/source/cli/suggest.rst
index b49b0325..4bf3b47e 100644
--- a/doc/source/cli/suggest.rst
+++ b/doc/source/cli/suggest.rst
@@ -82,9 +82,9 @@ If download of JSON files by URL is desirable, please
.. option:: -a, --all
- Display all search results without prompting, regardless of the number of hits.
- Otherwise, prompt if number of results exceeds
- :attr:`~sphobjinv.cli.parser.PrsConst.SUGGEST_CONFIRM_LENGTH`.
+ Display all search results without prompting, regardless of the number of
+ hits. Otherwise, prompt for confirmation before displaying the entire result
+ set if count exceeds |cli:SUGGEST_CONFIRM_LENGTH|.
.. option:: -i, --index
@@ -99,8 +99,7 @@ If download of JSON files by URL is desirable, please
.. option:: -t, --thresh <#>
Change the |fuzzywuzzy|_ match quality threshold (0-100; higher values
- yield fewer results). Default is specified in
- :attr:`~sphobjinv.cli.parser.PrsConst.DEF_THRESH`.
+ yield fewer results). Current default threshold is |cli:DEF_THRESH|.
.. option:: -u, --url
diff --git a/doc/source/conf.py b/doc/source/conf.py
index b7066837..5abed10c 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -16,6 +16,7 @@
# -- Imports -----------------------------------------------------------------
import sphobjinv as soi
+from sphobjinv.cli.parser import PrsConst
# -- Project information -----------------------------------------------------
@@ -73,7 +74,7 @@
# -- Common epilogue definition ------------------------------------------------
-rst_epilog = r"""
+rst_epilog = rf"""
.. |extlink| image:: /_static/extlink.svg
@@ -169,41 +170,13 @@
sphobjinv
-.. |stdin| replace:: |cour|\ stdin\ |/cour|
-
-.. |stdout| replace:: |cour|\ stdout\ |/cour|
-
-.. |cli:ALL| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.ALL`
-
-.. |cli:DEF_BASENAME| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.DEF_BASENAME`
-
-.. |cli:DEF_OUT_EXT| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.DEF_OUT_EXT`
-
-.. |cli:FOUND_URL| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.FOUND_URL`
-
-.. |cli:INDEX| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.INDEX`
-
-.. |cli:INFILE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.INFILE`
+.. |cli:DEF_THRESH| replace:: {PrsConst.DEF_THRESH}
-.. |cli:MODE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.MODE`
+.. |cli:SUGGEST_CONFIRM_LENGTH| replace:: {PrsConst.SUGGEST_CONFIRM_LENGTH}
-.. |cli:OUTFILE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.OUTFILE`
-
-.. |cli:OVERWRITE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.OVERWRITE`
-
-.. |cli:QUIET| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.QUIET`
-
-.. |cli:SCORE| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.SCORE`
-
-.. |cli:SUBPARSER_NAME| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.SUBPARSER_NAME`
-
-.. |cli:SUGGEST_CONFIRM_LENGTH| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.SUGGEST_CONFIRM_LENGTH`
-
-.. |cli:URL| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.URL`
-
-.. |cli:VERSION| replace:: :attr:`~sphobjinv.cli.parser.PrsConst.VERSION`
+.. |stdin| replace:: |cour|\ stdin\ |/cour|
-.. |resolve_inpath| replace:: :func:`~sphobjinv.cli.paths.resolve_inpath`
+.. |stdout| replace:: |cour|\ stdout\ |/cour|
.. |textconv| replace:: |cour|\ textconv\ |/cour|
diff --git a/doc/source/index.rst b/doc/source/index.rst
index ad8f24a2..d07d5be4 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -100,8 +100,6 @@ The project source repository is on GitHub: `bskinn/sphobjinv
levenshtein
syntax
api/index
- CLI Implementation (non-API)
-
Indices and Tables
From 13da13ee2c2266d0e23f0f57e0c177ecdb713bc8 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Tue, 30 Dec 2025 01:26:33 -0500
Subject: [PATCH 069/122] Remove reST replace:: markup from CLI docstrings
Meaningless at best, and actively confusing at worst. Best to
remove it.
---
src/sphobjinv/cli/convert.py | 8 ++++----
src/sphobjinv/cli/core.py | 4 ++--
src/sphobjinv/cli/load.py | 16 ++++++++--------
src/sphobjinv/cli/paths.py | 6 +++---
src/sphobjinv/cli/suggest.py | 10 +++++-----
src/sphobjinv/cli/ui.py | 2 +-
6 files changed, 23 insertions(+), 23 deletions(-)
diff --git a/src/sphobjinv/cli/convert.py b/src/sphobjinv/cli/convert.py
index d07353f3..1a342d63 100644
--- a/src/sphobjinv/cli/convert.py
+++ b/src/sphobjinv/cli/convert.py
@@ -36,13 +36,13 @@
def do_convert(inv, in_path, params):
r"""Carry out the conversion operation, including writing output.
- If |cli:OVERWRITE| is passed and the output file
- (the default location, or as passed to |cli:OUTFILE|)
+ If OVERWRITE is passed and the output file
+ (the default location, or as passed to OUTFILE)
exists, it will be overwritten without a prompt. Otherwise,
the user will be queried if it is desired to overwrite
the existing file.
- If |cli:QUIET| is passed, nothing will be
+ If QUIET is passed, nothing will be
printed to |cour|\ stdout\ |/cour|
(potentially useful for scripting),
and any existing output file will be overwritten
@@ -53,7 +53,7 @@ def do_convert(inv, in_path, params):
inv
|Inventory| -- Inventory object to be output in the format
- indicated by |cli:MODE|.
+ indicated by MODE.
in_path
diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py
index 4adbdf2c..9130f992 100644
--- a/src/sphobjinv/cli/core.py
+++ b/src/sphobjinv/cli/core.py
@@ -43,14 +43,14 @@ def main():
Parses command line arguments,
handling the no-arguments and
- |cli:VERSION| cases.
+ VERSION cases.
Creates the |Inventory| from the indicated source
and method.
Invokes :func:`~sphobjinv.cli.convert.do_convert` or
:func:`~sphobjinv.cli.suggest.do_suggest`
- per the subparser name stored in |cli:SUBPARSER_NAME|.
+ per the subparser name stored in SUBPARSER_NAME.
"""
# If no args passed, stick in '-h'
diff --git a/src/sphobjinv/cli/load.py b/src/sphobjinv/cli/load.py
index 469d811f..32405758 100644
--- a/src/sphobjinv/cli/load.py
+++ b/src/sphobjinv/cli/load.py
@@ -83,8 +83,8 @@ def import_infile(in_path):
def inv_local(params):
"""Create |Inventory| from local source.
- Uses |resolve_inpath| to sanity-check and/or convert
- |cli:INFILE|.
+ Uses ``resolve_inpath`` to sanity-check and/or convert
+ INFILE.
Calls :func:`sys.exit` internally in error-exit situations.
@@ -99,12 +99,12 @@ def inv_local(params):
inv
|Inventory| -- Object representation of the inventory
- at |cli:INFILE|
+ at INFILE
in_path
|str| -- Input file path as resolved/checked by
- |resolve_inpath|
+ ``resolve_inpath``
"""
# Resolve input file path
@@ -127,7 +127,7 @@ def inv_local(params):
def inv_url(params):
"""Create |Inventory| from file downloaded from URL.
- Initially, treats |cli:INFILE| as a download URL to be passed to
+ Initially, treats INFILE as a download URL to be passed to
the `url` initialization argument
of :class:`~sphobjinv.inventory.Inventory`.
@@ -135,7 +135,7 @@ def inv_url(params):
searches the directory tree of the URL for |objects.inv|.
Injects the URL at which an inventory was found into `params`
- under the |cli:FOUND_URL| key.
+ under the FOUND_URL key.
Calls :func:`sys.exit` internally in error-exit situations.
@@ -150,11 +150,11 @@ def inv_url(params):
inv
|Inventory| -- Object representation of the inventory
- at |cli:INFILE|
+ at INFILE
ret_path
- |str| -- URL from |cli:INFILE| used to construct `inv`.
+ |str| -- URL from INFILE used to construct `inv`.
If URL is longer than 45 characters, the central portion is elided.
"""
diff --git a/src/sphobjinv/cli/paths.py b/src/sphobjinv/cli/paths.py
index 06ea5f57..de9bfc23 100644
--- a/src/sphobjinv/cli/paths.py
+++ b/src/sphobjinv/cli/paths.py
@@ -72,11 +72,11 @@ def resolve_outpath(out_path, in_path, params):
If the output path or basename are not specified, they are
taken as the same as the input file. If the extension is
unspecified, it is taken as the appropriate mode-specific value
- from |cli:DEF_OUT_EXT|.
+ from DEF_OUT_EXT.
- If |cli:URL| is passed, the input directory
+ If URL is passed, the input directory
is taken to be :func:`os.getcwd` and the input basename
- is taken as |cli:DEF_BASENAME|.
+ is taken as DEF_BASENAME.
Parameters
----------
diff --git a/src/sphobjinv/cli/suggest.py b/src/sphobjinv/cli/suggest.py
index 31db9585..1fb7dd56 100644
--- a/src/sphobjinv/cli/suggest.py
+++ b/src/sphobjinv/cli/suggest.py
@@ -43,18 +43,18 @@ def do_suggest(inv, params):
Results are printed one per line.
- If neither |cli:INDEX| nor |cli:SCORE| is specified,
+ If neither INDEX nor SCORE is specified,
the results are output without a header.
If either or both are specified,
the results are output in a lightweight tabular format.
If the number of results exceeds
- |cli:SUGGEST_CONFIRM_LENGTH|,
+ SUGGEST_CONFIRM_LENGTH,
the user will be queried whether to display
all of the returned results
- unless |cli:ALL| is specified.
+ unless ALL is specified.
- No |cli:QUIET| option is available here, since
+ No QUIET option is available here, since
a silent mode for suggestion output is nonsensical.
Parameters
@@ -62,7 +62,7 @@ def do_suggest(inv, params):
inv
|Inventory| -- Inventory object to be output in the format
- indicated by |cli:MODE|.
+ indicated by MODE.
params
diff --git a/src/sphobjinv/cli/ui.py b/src/sphobjinv/cli/ui.py
index 62b080e2..2ab2290b 100644
--- a/src/sphobjinv/cli/ui.py
+++ b/src/sphobjinv/cli/ui.py
@@ -37,7 +37,7 @@
def print_stderr(thing, params, *, end="\n"):
r"""Print `thing` to stderr if not in quiet mode.
- Quiet mode is indicated by the value at the |cli:QUIET| key
+ Quiet mode is indicated by the value at the QUIET key
within `params`.
Quiet mode is not implemented for the ":doc:`suggest `"
From 621e6eff5e37399b24f769b0a310313a2efee779 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 5 Jan 2026 21:33:03 -0500
Subject: [PATCH 070/122] Add workflow to error at release on any ##VER## in
docs
---
.../release_enusre_no_ver_markers.yml | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 .github/workflows/release_enusre_no_ver_markers.yml
diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml
new file mode 100644
index 00000000..c08b9432
--- /dev/null
+++ b/.github/workflows/release_enusre_no_ver_markers.yml
@@ -0,0 +1,29 @@
+name: 'RELEASE: Check docs source files'
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - ready_for_review
+ branches:
+ - stable
+ - main
+
+jobs:
+ sdist_build_and_check:
+ name: 'have no ##VER## markers'
+ runs-on: 'ubuntu-latest'
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+ if: ${{ true || !github.event.pull_request.draft }}
+
+ steps:
+ - name: Check out repo
+ uses: actions/checkout@v6
+
+ - name: Error if any markers found
+ run: |
+ grep -ri '##VER##' doc/source && exit 1 || exit 0
From 94a313d5443489d958d8be0051e36a906ccc21f1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 5 Jan 2026 21:34:32 -0500
Subject: [PATCH 071/122] Switch to looking for a marker that's not there, to
test
---
.github/workflows/release_enusre_no_ver_markers.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml
index c08b9432..c275111f 100644
--- a/.github/workflows/release_enusre_no_ver_markers.yml
+++ b/.github/workflows/release_enusre_no_ver_markers.yml
@@ -26,4 +26,4 @@ jobs:
- name: Error if any markers found
run: |
- grep -ri '##VER##' doc/source && exit 1 || exit 0
+ grep -ri '##VExR##' doc/source && exit 1 || exit 0
From f3e438b2e0becedb1ca7d4158615af7bb7d57966 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 5 Jan 2026 21:36:09 -0500
Subject: [PATCH 072/122] Restore workflow to non-draft, release-only w/correct
search term
---
.github/workflows/release_enusre_no_ver_markers.yml | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml
index c275111f..e6ecbd19 100644
--- a/.github/workflows/release_enusre_no_ver_markers.yml
+++ b/.github/workflows/release_enusre_no_ver_markers.yml
@@ -9,7 +9,6 @@ on:
- ready_for_review
branches:
- stable
- - main
jobs:
sdist_build_and_check:
@@ -18,7 +17,7 @@ jobs:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
- if: ${{ true || !github.event.pull_request.draft }}
+ if: ${{ !github.event.pull_request.draft }}
steps:
- name: Check out repo
@@ -26,4 +25,4 @@ jobs:
- name: Error if any markers found
run: |
- grep -ri '##VExR##' doc/source && exit 1 || exit 0
+ grep -ri '#VER#' doc/source && exit 1 || exit 0
From a7a3ea550a6654de9f2ce1d452cc3b41e1a23497 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 5 Jan 2026 21:37:01 -0500
Subject: [PATCH 073/122] Rename workflow job
---
.github/workflows/release_enusre_no_ver_markers.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml
index e6ecbd19..446bd552 100644
--- a/.github/workflows/release_enusre_no_ver_markers.yml
+++ b/.github/workflows/release_enusre_no_ver_markers.yml
@@ -11,7 +11,7 @@ on:
- stable
jobs:
- sdist_build_and_check:
+ ver_placeholder_search:
name: 'have no ##VER## markers'
runs-on: 'ubuntu-latest'
concurrency:
From ae3100f366f140146c723c142efdf5db4176784a Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Tue, 6 Jan 2026 00:30:47 -0500
Subject: [PATCH 074/122] Rearrange & re-label some textconv entrypoint tests
---
tests/test_cli_textconv.py | 36 ++++++++++++++++--------------------
1 file changed, 16 insertions(+), 20 deletions(-)
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index 781f1b85..45156851 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -47,7 +47,7 @@
class TestMisc:
- """Tests for miscellaneous CLI functions."""
+ """Tests for miscellaneous textconv entrypoint behavior."""
@pytest.mark.timeout(CLI_TEST_TIMEOUT)
@pytest.mark.parametrize("cmd", CLI_CMDS)
@@ -75,8 +75,8 @@ def test_cli_noargs_shows_help(self, run_cmdline_test):
assert re.search("usage.+sphobjinv", out_.getvalue(), re.I)
-class TestConvertGood:
- """Tests for expected-good convert functionality."""
+class TestGood:
+ """Tests for expected-good textconv entrypoint functionality."""
def test_textconv_matches_main_conv(self, res_cmp, run_cmdline_test):
"""Ensure that textconv conversion matches main CLI conversion."""
@@ -92,9 +92,21 @@ def test_textconv_matches_main_conv(self, res_cmp, run_cmdline_test):
assert core_output == textconv_output
+ def test_textconv_matches_original(self, res_cmp, run_cmdline_test):
+ """Confirm textconv produces a consistent Inventory."""
+ with stdio_mgr() as (_, out_, _):
+ run_cmdline_test([str(res_cmp.resolve())], command=CLICommand.Textconv)
+
+ result = out_.getvalue()
+
+ inv1 = Inventory(res_cmp)
+ inv2 = Inventory(result.encode("utf-8"))
+
+ assert inv1 == inv2
+
class TestFail:
- """Tests for expected-fail behaviors."""
+ """Tests for expected-fail textconv entrypoint behaviors."""
@pytest.mark.timeout(CLI_TEST_TIMEOUT)
def test_clifail_convert_wrongfiletype(
@@ -146,19 +158,3 @@ def test_clifail_no_url_arg(self, run_cmdline_test):
["-u", "nofile.inv"], command=CLICommand.Textconv, expect=2
)
assert "unrecognized argument" in err_.getvalue()
-
-
-class TestStdio:
- """Tests for the stdin/stdout functionality."""
-
- def test_cli_stdio_output(self, res_cmp, run_cmdline_test):
- """Confirm that inventory data can be written to stdout."""
- with stdio_mgr() as (_, out_, _):
- run_cmdline_test([str(res_cmp.resolve())], command=CLICommand.Textconv)
-
- result = out_.getvalue()
-
- inv1 = Inventory(res_cmp)
- inv2 = Inventory(result.encode("utf-8"))
-
- assert inv1 == inv2
From 2807e4144cf358aafe6a8349f9e04ce0451f328b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Tue, 6 Jan 2026 00:31:59 -0500
Subject: [PATCH 075/122] Update CHANGELOG
---
CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d39789b..4119a775 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,21 @@ changes.
### *Unreleased*
+#### Added
+
+ * Add `sphobjinv-textconv` CLI entrypoint ([#331]).
+ * Takes a single required argument, the path to a local inventory file, and
+ emits the plaintext inventory to `stdout`.
+ * The target use-case is as a Git textconv, primarily intended for
+ compressed `objects.inv` files; but, it will work with any valid type of
+ input file.
+
#### Tests
+ * Add tests exercising the new `sphobjinv-textconv` CLI entrypoint ([#331]).
+ * Required generalizing the `run_cmdline_test` fixture so that tests can
+ choose between the core and textconv entrypoints.
+
* Update `tox` env test matrix for `py310` to `py314` ([#325]).
* Update test path calculations to always be relative to `__file__` ([#325]).
@@ -36,6 +49,9 @@ changes.
#### Internal
+ * Add Actions workflow to error on a non-draft release branch if any `#VER#`
+ markers remain in docs source ([#331]).
+
* Augment `black` and `flake8` `tox` envs to run `--version` first ([#327]).
* Remove `-r requirements-flake.txt` from `requirements-dev.txt` ([#327]).
@@ -57,6 +73,30 @@ changes.
* This will provide `main` branch CI results for this workflow, for the
GitHub badge to report.
+#### Documentation
+
+ * Dynamically retrieve the current values of `PrsConst.SUGGEST_CONFIRM_LENGTH`
+ and `PrsConst.DEF_THRESH` to define their replaces in `conf.py` ([#331]).
+
+ * Add `cli/textconv.rst` to document the new `sphobjinv-textconv` CLI
+ entrypoint ([#331]).
+
+ * Cull some superfluous replaces in `conf.py` ([#331]).
+
+ * Relocate the 'help' and 'version' CLI usage documentation content to a new
+ 'orphan' page ([#331]).
+ * This keeps the content in the `objects.inv`, for completeness, but keeps
+ it off of the docs nav.
+
+ * Revise 'CLI Usage' documentation to incorporate the `sphobjinv-textconv`
+ entrypoint ([#331]).
+
+ * Remove the 'CLI Implementation' "API reference" docs ([#331]).
+ * They're not part of the public API contract, and they don't actually help
+ understand how the CLI is implemented; so, why bother maintaining them?
+ * Also cull the various `replace` directives defined in `conf.py` for these
+ docs.
+
#### Administrative
* Add formal support for Python 3.14 ([#325]).
@@ -739,3 +779,4 @@ changes.
[#320]: https://github.com/bskinn/sphobjinv/pull/320
[#325]: https://github.com/bskinn/sphobjinv/pull/325
[#327]: https://github.com/bskinn/sphobjinv/pull/327
+[#331]: https://github.com/bskinn/sphobjinv/pull/331
From 58a79f51f9da9477f5cfd654c3f7c88e8ceaad00 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Tue, 6 Jan 2026 08:28:51 -0500
Subject: [PATCH 076/122] Use enum value in run_cmdline_test func
---
tests/conftest.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1fac14b0..091df5cd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -246,7 +246,7 @@ def run_cmdline_test(monkeypatch):
def func(arglist, *, command=CLICommand.Core, expect=0): # , suffix=None):
"""Perform the CLI exit-code test."""
# Assemble execution arguments
- runargs = [command]
+ runargs = [command.value]
runargs.extend(str(a) for a in arglist)
# Select the command function to use
From ca05fbc96a450c275d00a27002e55962dd29bd2b Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 11 Jan 2026 18:41:36 -0500
Subject: [PATCH 077/122] Update some CLI test URLs to https://...
---
tests/test_cli_nonlocal.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py
index 4e2fad06..3a9afef3 100644
--- a/tests/test_cli_nonlocal.py
+++ b/tests/test_cli_nonlocal.py
@@ -216,10 +216,10 @@ def test_cli_suggest_from_url(
@pytest.mark.parametrize(
"url",
[
- "http://sphobjinv.readthedocs.io/en/v2.0/modules/",
- "http://sphobjinv.readthedocs.io/en/v2.0/modules/cmdline.html",
+ "https://sphobjinv.readthedocs.io/en/v2.0/modules/",
+ "https://sphobjinv.readthedocs.io/en/v2.0/modules/cmdline.html",
(
- "http://sphobjinv.readthedocs.io/en/v2.0/modules/"
+ "https://sphobjinv.readthedocs.io/en/v2.0/modules/"
"cmdline.html#sphobjinv.cmdline.do_convert"
),
],
@@ -233,18 +233,18 @@ def test_cli_suggest_from_docset_urls(self, url, run_cmdline_test, check):
check.is_true(p_inventory.search(out_.getvalue()))
check.is_in("LIKELY", err_.getvalue())
check.is_in(
- "(http://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
+ "(https://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
)
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_cli_suggest_from_typical_objinv_url(self, run_cmdline_test, check):
"""Confirm reST-only suggest works for direct objects.inv URL."""
- url = "http://sphobjinv.readthedocs.io/en/v2.0/objects.inv"
+ url = "https://sphobjinv.readthedocs.io/en/v2.0/objects.inv"
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(["suggest", "-u", url, "inventory", "-at", "50"])
check.is_true(p_inventory.search(out_.getvalue()))
check.is_in("PROBABLY", err_.getvalue())
check.is_in(
- "(http://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
+ "(https://sphobjinv.readthedocs.io/en/v2.0/, None)", err_.getvalue()
)
From 3b9c000de210b37b8ea6ae9b06154198f83fdced Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 11 Jan 2026 18:42:06 -0500
Subject: [PATCH 078/122] Add free-threaded 3.13 and 3.14 to tox
---
tox.ini | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/tox.ini b/tox.ini
index c9207995..79ab69be 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@ minversion=2.0
isolated_build=True
envlist=
# Test all Python versions on latest lib versions
- py3{10,11,12,13,14}-sphx_latest-attrs_latest-jsch_latest
+ py3{10,11,12,13t,14,14t}-sphx_latest-attrs_latest-jsch_latest
# Test leading Python version on current in-repo dev lib versions
py313-sphx_dev-attrs_dev-jsch_dev
# Scan across Sphinx versions
@@ -30,6 +30,7 @@ envlist=
[testenv]
commands=
python --version
+ python -c 'import sys; print("GIL: ", end=""); print(getattr(sys, "_is_gil_enabled", lambda : True)())'
pip list
# Want the tox *matrix* to ignore warnings since it's primarily
# a compatibility check. The defaults for bare pytest enable -Werror
@@ -88,7 +89,9 @@ deps=
platform=linux
basepython=
py314: python3.14
+ py314t: python3.14t
py313: python3.13
+ py313t: python3.13t
py312: python3.12
py311: python3.11
py310: python3.10
From 852b89f3beb3dfbcc9317c24b67568ad36bed78c Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sun, 11 Jan 2026 19:43:39 -0500
Subject: [PATCH 079/122] Ignore implicit-cleanup ResourceWarnings in two CLI
tests
---
tests/test_cli_nonlocal.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py
index 3a9afef3..8841403b 100644
--- a/tests/test_cli_nonlocal.py
+++ b/tests/test_cli_nonlocal.py
@@ -133,6 +133,7 @@ def test_cli_url_in_json(
assert "objects" in d.get("metadata", {}).get("url", {})
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
+ @pytest.mark.filterwarnings("ignore:implicitly cleaning up.*404")
def test_clifail_bad_url(
self, run_cmdline_test, misc_info, http_inv_url_template, scratch_path
):
@@ -225,6 +226,7 @@ def test_cli_suggest_from_url(
],
)
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
+ @pytest.mark.filterwarnings("ignore:implicitly cleaning up.*404")
def test_cli_suggest_from_docset_urls(self, url, run_cmdline_test, check):
"""Confirm reST-only suggest output works from URLs within a docset."""
with stdio_mgr() as (in_, out_, err_):
From b8126083dd53cc0c756334d85c844d82875c6862 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Mon, 12 Jan 2026 00:48:47 -0500
Subject: [PATCH 080/122] Remove the problematic Sphinx 3.x test
---
tox.ini | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/tox.ini b/tox.ini
index 79ab69be..b0366da9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,10 +6,8 @@ envlist=
py3{10,11,12,13t,14,14t}-sphx_latest-attrs_latest-jsch_latest
# Test leading Python version on current in-repo dev lib versions
py313-sphx_dev-attrs_dev-jsch_dev
- # Scan across Sphinx versions
+ # Scan across Sphinx versions (skip 3_x due to a typing import error)
py313-sphx_{1_6_x,1_x,2_x,4_x,5_x,6_x,7_x,8_x,dev}-attrs_latest-jsch_latest
- # sphx_3_x is incompatible with py310 due to a typing import. Test on py311 instead.
- py311-sphx_3_x-attrs_latest-jsch_latest
# Scan attrs versions
py313-sphx_latest-attrs_{19_2,19_3,20_3,21_3,22_2,23_2,24_3,dev}-jsch_latest
# Scan jsonschema versions
From 082569e312b367e3c10f2ab187467359266ab22f Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Fri, 16 Jan 2026 21:23:53 -0500
Subject: [PATCH 081/122] Update http -> https and some docs bits
---
CHANGELOG.md | 4 ++--
CONTENT_LICENSE.txt | 2 +-
README.md | 6 +++---
doc/make.bat | 2 +-
doc/source/_templates/footer.html | 4 ++--
tests/conftest.py | 2 +-
tests/enum.py | 2 +-
tests/fixtures_http.py | 2 +-
tests/test_api_fail.py | 2 +-
tests/test_api_good.py | 2 +-
tests/test_api_good_nonlocal.py | 2 +-
tests/test_cli.py | 2 +-
tests/test_cli_nonlocal.py | 4 ++--
tests/test_cli_textconv.py | 2 +-
tests/test_fixture.py | 2 +-
tests/test_flake8_ext.py | 2 +-
tests/test_intersphinx.py | 2 +-
tests/test_valid_objects.py | 2 +-
18 files changed, 23 insertions(+), 23 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4119a775..dd9da0a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,9 @@
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project follows an extension of
-[Semantic Versioning](http://semver.org/spec/v2.0.0.html), where a bump in a
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html), where a bump in a
fourth number represents an administrative maintenance release with no code
changes.
diff --git a/CONTENT_LICENSE.txt b/CONTENT_LICENSE.txt
index d187275f..b04b0ea0 100644
--- a/CONTENT_LICENSE.txt
+++ b/CONTENT_LICENSE.txt
@@ -1,4 +1,4 @@
The sphobjinv documentation (including docstrings and README) is licensed under
a Creative Commons Attribution 4.0 International License (CC-BY).
-See http://creativecommons.org/licenses/by/4.0/.
+See https://creativecommons.org/licenses/by/4.0/.
diff --git a/README.md b/README.md
index f89e1025..8aef5f0b 100644
--- a/README.md
+++ b/README.md
@@ -187,8 +187,8 @@ under a [Creative Commons Attribution 4.0 International License][cc-by 4.0]
[black badge]: https://img.shields.io/badge/code%20style-black-000000.svg
[black link target]: https://github.com/psf/black
-[cc-by 4.0]: http://creativecommons.org/licenses/by/4.0/
-[soi docs inv export]: http://sphobjinv.readthedocs.io/en/latest/api_usage.html#exporting-an-inventory
+[cc-by 4.0]: https:/creativecommons.org/licenses/by/4.0/
+[soi docs inv export]: https://sphobjinv.readthedocs.io/en/latest/api_usage.html#exporting-an-inventory
[github issue tracker]: https://github.com/bskinn/sphobjinv/issues
[github repo]: https://github.com/bskinn/sphobjinv
[gitter badge]: https://badges.gitter.im/sphobjinv/community.svg
@@ -203,6 +203,6 @@ under a [Creative Commons Attribution 4.0 International License][cc-by 4.0]
[pypi link target]: https://pypi.org/project/sphobjinv
[python versions badge]: https://img.shields.io/pypi/pyversions/sphobjinv.svg?logo=python
[readthedocs badge]: https://img.shields.io/readthedocs/sphobjinv/latest.svg
-[readthedocs link target]: http://sphobjinv.readthedocs.io/en/latest/
+[readthedocs link target]: https://sphobjinv.readthedocs.io/en/latest/
[workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/sphobjinv/all_core_tests.yml?logo=github&branch=main
[workflow link target]: https://github.com/bskinn/sphobjinv/actions
diff --git a/doc/make.bat b/doc/make.bat
index b9c03e59..5669cb67 100644
--- a/doc/make.bat
+++ b/doc/make.bat
@@ -21,7 +21,7 @@ if errorlevel 9009 (
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
- echo.http://sphinx-doc.org/
+ echo.https://sphinx-doc.org/
exit /b 1
)
diff --git a/doc/source/_templates/footer.html b/doc/source/_templates/footer.html
index e519d56f..b231f147 100644
--- a/doc/source/_templates/footer.html
+++ b/doc/source/_templates/footer.html
@@ -13,9 +13,9 @@
{% endblock %}
diff --git a/tests/conftest.py b/tests/conftest.py
index 091df5cd..19db6815 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/enum.py b/tests/enum.py
index b3469584..685d2406 100644
--- a/tests/enum.py
+++ b/tests/enum.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py
index bf3eee46..27abe554 100644
--- a/tests/fixtures_http.py
+++ b/tests/fixtures_http.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_api_fail.py b/tests/test_api_fail.py
index 7e65ce65..d77088f4 100644
--- a/tests/test_api_fail.py
+++ b/tests/test_api_fail.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_api_good.py b/tests/test_api_good.py
index a0433dec..8193306b 100644
--- a/tests/test_api_good.py
+++ b/tests/test_api_good.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py
index 2ba01490..eec40738 100644
--- a/tests/test_api_good_nonlocal.py
+++ b/tests/test_api_good_nonlocal.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c83ac650..8dd367e7 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py
index 8841403b..f074e86b 100644
--- a/tests/test_cli_nonlocal.py
+++ b/tests/test_cli_nonlocal.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
@@ -153,7 +153,7 @@ def test_clifail_bad_url(
@pytest.mark.timeout(CLI_TEST_TIMEOUT * 4)
def test_clifail_url_no_leading_http(self, run_cmdline_test, scratch_path):
- """Confirm proper error behavior when a URL w/o leading 'http://' is passed."""
+ """Confirm proper error behavior when a URL w/o leading scheme is passed."""
with stdio_mgr() as (in_, out_, err_):
run_cmdline_test(
[
diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py
index 45156851..3ad39cc8 100644
--- a/tests/test_cli_textconv.py
+++ b/tests/test_cli_textconv.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_fixture.py b/tests/test_fixture.py
index 03531aee..a48cb98d 100644
--- a/tests/test_fixture.py
+++ b/tests/test_fixture.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_flake8_ext.py b/tests/test_flake8_ext.py
index c851fd7a..8604d12c 100644
--- a/tests/test_flake8_ext.py
+++ b/tests/test_flake8_ext.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_intersphinx.py b/tests/test_intersphinx.py
index b2ff2de3..4096c0c4 100644
--- a/tests/test_intersphinx.py
+++ b/tests/test_intersphinx.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
diff --git a/tests/test_valid_objects.py b/tests/test_valid_objects.py
index 130a9e48..6367dde8 100644
--- a/tests/test_valid_objects.py
+++ b/tests/test_valid_objects.py
@@ -13,7 +13,7 @@
\(c) Brian Skinn 2016-2025
**Source Repository**
- http://www.github.com/bskinn/sphobjinv
+ https://github.com/bskinn/sphobjinv
**Documentation**
https://sphobjinv.readthedocs.io/en/stable
From fbc1666f7dfc23d0c2982864f9b4b11643edf073 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sat, 17 Jan 2026 00:31:39 -0500
Subject: [PATCH 082/122] Update CHANGELOG
---
CHANGELOG.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd9da0a6..6e793362 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,12 @@ changes.
#### Tests
+ * Add 3.13t and 3.14t to `tox` test matrix ([#333]).
+ * Also add report of the current GIL status to the `tox` env output.
+
+ * Filter newly emerged `ResourceWarning` emitted from implicit cleanup of
+ `tempfile` resources ([#333]).
+
* Add tests exercising the new `sphobjinv-textconv` CLI entrypoint ([#331]).
* Required generalizing the `run_cmdline_test` fixture so that tests can
choose between the core and textconv entrypoints.
@@ -99,6 +105,8 @@ changes.
#### Administrative
+ * Convert several `http://` to `https://` across the project ([#333]).
+
* Add formal support for Python 3.14 ([#325]).
* Drop support for Python 3.9 (EOL) ([#325]).
@@ -780,3 +788,4 @@ changes.
[#325]: https://github.com/bskinn/sphobjinv/pull/325
[#327]: https://github.com/bskinn/sphobjinv/pull/327
[#331]: https://github.com/bskinn/sphobjinv/pull/331
+[#333]: https://github.com/bskinn/sphobjinv/pull/333
From ac33104c32cbce87301a55a63215dbe1b21c4bd7 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Fri, 13 Mar 2026 23:35:02 -0400
Subject: [PATCH 083/122] Bump to Ubuntu 24.04 for RTD builds
---
.readthedocs.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 7409274a..03c3c3f0 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -7,7 +7,7 @@ version: 2
# Build and VM configuration
build:
- os: 'ubuntu-22.04'
+ os: 'ubuntu-24.04'
tools:
python: '3.13'
From cb53f5ca1c79aa8bbb4686e3efc9722edbc7a5f1 Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Fri, 13 Mar 2026 23:35:16 -0400
Subject: [PATCH 084/122] Freshen links into Sphinx source
---
doc/source/index.rst | 2 +-
doc/source/syntax.rst | 20 ++++++++++----------
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/doc/source/index.rst b/doc/source/index.rst
index d07d5be4..114e9d77 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -11,7 +11,7 @@ Welcome to sphobjinv!
When documentation is built using, e.g., Sphinx's :obj:`~sphinx.builders.html.StandaloneHTMLBuilder`,
an inventory of the named objects in the documentation set `is dumped
-`__
+`__
to a file called |objects.inv| in the html build directory.
(One common location is, |cour|\ doc/build/html\ |/cour|, though the exact location will vary
depending on the details of how Sphinx is configured.) This file is read by |isphx| when
diff --git a/doc/source/syntax.rst b/doc/source/syntax.rst
index ab51b116..0bbb877a 100644
--- a/doc/source/syntax.rst
+++ b/doc/source/syntax.rst
@@ -27,7 +27,7 @@ data line.
----
**The first line** `must be exactly
-`__:
+`__:
.. code-block:: none
@@ -36,7 +36,7 @@ data line.
----
**The second and third lines** `must obey
-`__
+`__
the template:
.. code-block:: none
@@ -56,7 +56,7 @@ the |isphx| cross-references:
----
**The fourth line** `must contain
-`__
+`__
the string ``zlib`` somewhere within it, but for consistency it should be exactly:
.. code-block:: none
@@ -67,7 +67,7 @@ the string ``zlib`` somewhere within it, but for consistency it should be exactl
**All remaining lines** of the file are the objects data, each laid out in the
`following syntax
-`__:
+`__:
.. code-block:: none
@@ -132,7 +132,7 @@ the string ``zlib`` somewhere within it, but for consistency it should be exactl
``{priority}``
Flag for `placement in search results
- `__. Most will be ``1`` (standard priority) or
+ `__. Most will be ``1`` (standard priority) or
``-1`` (omit from results) for documentation built by Sphinx;
values of ``0`` (higher priority) or ``2`` (lower priority) may also occur.
@@ -217,11 +217,11 @@ size of the inventory file:
`__," the portion
following the ``#`` symbol) and the tail of the anchor is identical to
|{name}|_, that tail is `replaced
- `__
+ `__
with ``$``. |br| |br|
#. If |{dispname}|_ is identical to |{name}|_, it is `stored
- `__
+ `__
as ``-``.
Thus, a standard |isphx| reference to this method would take the form:
@@ -271,11 +271,11 @@ as in :obj:`This is join! `:
.. |prio_js_search| replace:: here
-.. _prio_js_search: https://github.com/sphinx-doc/sphinx/blob/ac3f74a3e0fbb326f73989a16dfa369e072064ca/sphinx/themes/basic/static/searchtools.js#L28-L46
+.. _prio_js_search: https://github.com/sphinx-doc/sphinx/blob/e552f8429c3039bc0649a7da82bdfa0df3273c3d/sphinx/themes/basic/static/searchtools.js#L21-L39
.. |prio_py_search| replace:: here
-.. _prio_py_search: https://github.com/sphinx-doc/sphinx/blob/ac3f74a3e0fbb326f73989a16dfa369e072064ca/sphinx/search/__init__.py#L344-L345
+.. _prio_py_search: https://github.com/sphinx-doc/sphinx/blob/e552f8429c3039bc0649a7da82bdfa0df3273c3d/sphinx/search/__init__.py#L377-L378
.. |sphinx_uri_issue| replace:: sphinx-doc/sphinx#7096
@@ -297,4 +297,4 @@ as in :obj:`This is join! `:
.. _rst-directive-option: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-rst-directive-option
-.. _parsing regex: https://github.com/sphinx-doc/sphinx/blob/ac3f74a3e0fbb326f73989a16dfa369e072064ca/sphinx/util/inventory.py#L134-L135
+.. _parsing regex: https://github.com/sphinx-doc/sphinx/blob/e552f8429c3039bc0649a7da82bdfa0df3273c3d/sphinx/util/inventory.py#L115-L119
From fd037642c183a79000ec1488aa934b1923e241da Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Fri, 13 Mar 2026 23:39:35 -0400
Subject: [PATCH 085/122] Update numpy example in README
---
README.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 8aef5f0b..3bf36e72 100644
--- a/README.md
+++ b/README.md
@@ -84,31 +84,31 @@ cross-reference the `linspace` function from numpy (see
[here][numpy linspace]):
```none
-$ sphobjinv suggest https://numpy.org/doc/1.26/reference/index.html linspace -su
+$ sphobjinv suggest https://numpy.org/doc/2.4/reference/index.html linspace -su
-Attempting https://numpy.org/doc/1.26/reference/index.html ...
+Attempting https://numpy.org/doc/2.4/reference/index.html ...
... no recognized inventory.
-Attempting "https://numpy.org/doc/1.26/reference/index.html/objects.inv" ...
+Attempting "https://numpy.org/doc/2.4/reference/index.html/objects.inv" ...
... HTTP error: 404 Not Found.
-Attempting "https://numpy.org/doc/1.26/reference/objects.inv" ...
+Attempting "https://numpy.org/doc/2.4/reference/objects.inv" ...
... HTTP error: 404 Not Found.
-Attempting "https://numpy.org/doc/1.26/objects.inv" ...
+Attempting "https://numpy.org/doc/2.4/objects.inv" ...
... inventory found.
-----------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
The intersphinx_mapping for this docset is LIKELY:
- (https://numpy.org/doc/1.26/, None)
+ (https://numpy.org/doc/2.4/, None)
-----------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
Project: NumPy
-Version: 1.26
+Version: 2.4
-8152 objects in inventory.
+8456 objects in inventory.
-----------------------------------------------------------------------------------
+-----------------------------------------------------------------------------------------------------------
8 results found at/above current threshold of 75.
From f188bcc436ce33ba810fc3e334f1081914ac2c0c Mon Sep 17 00:00:00 2001
From: Brian Skinn
Date: Sat, 14 Mar 2026 00:36:35 -0400
Subject: [PATCH 086/122] Freshen/update some docs bits
---
doc/source/_static/mouseover_example.png | Bin 48977 -> 21672 bytes
doc/source/customfile.rst | 2 +-
doc/source/index.rst | 2 ++
doc/source/syntax.rst | 6 +++---
4 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/doc/source/_static/mouseover_example.png b/doc/source/_static/mouseover_example.png
index eef34b44b1ea27405ffc3e8b61f4e2509e786483..7e60477449668af89e2f5e27643320d90706e1e0 100644
GIT binary patch
literal 21672
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
Bk3s+d
literal 48977
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