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 zcmce;Wmp_Rqoxf6ch}(V4#C~s-Q5Z99tiFpEQ7nd1b26LcX#+Euk3lhz4n}cyFZ5M znV#;duD+=5zMs1)Tv1*E0Tve)1Ox;@N>WrA1O!wSxLgYj3H*!y#F-6z19euG5C*B7 z_<9Wd0B#{9CjLe&S&OoM8;$Q_-SZesGsAL2 z5zs9Y(cFUyQyWKNj7_@5Lfb4ne)jf}hLh%*76=M8d){tPb$CDHJmH*u$#v+>KiPsX zdFjHP1sW(wguKS)F7*W*EKqQYHzXGpxR8h(M1u?sF8}*DNIU5dtu*<_k;x3U5a27R zc8Tb-G?>^-@oHSeE=`K4zqI)ewgW^#jS-`H)CEcdVGG2%?KUx(s$5l?X9M@c&wVF? zNcG>Uox(vaD$CZdI-*~Kw1xCf{64;Gp}vi}R$4V!R?SP2_yLX>t{>keo;#IsjUsVX z*7XjttmdkI=)Ef3ZfB6m7gAig`BbvqJRD}ahf8q}f4Is#^{q|YK3y;_R^`oG|EsJ8 z-Pzys`n#?FEGxuUsDd|&#*0tNq5uVq*2(?q;f4NdCrNa(NVSc>f41ig9w(@YTt!?h7!H%&k{#D%rM9%N z%5IKdLL_}ph+W&Hd11jtTth4!_$yQ`z`SBwvb1(~)~mJE(nX|zH5u`^goc`VQ`EiH z5(85zUu^vx9qf6&RpUG6IdO#!DR=1z-Kdg$?Y#;Rf`2f(@>)6dc?JIjeDqDBt) z$}Z7gE42i&qA;#}j_rm``y^c(Af|#>2`HtDt*_^;UyO3K?(&m0tJU(bv98CxnNc35 zslR^nV%w2Xf51J>XQGWR?}0q|5&Bxryi!Y7$l*}$11wY4)n-(^di{Z8#IU|1@5L%# z-}b8Q1?n{xr9yWyd_>y%E@hj5N)0CDx3RG$D*2b5pXcd4JCipp=JJapk(8-bMh~PH zn2*QJN$o)pc=C?@#@p3use%Vp-q8UQ(e1)6sgn9N0SJ_pn&mu=zh$<37CJBmEx3+g zRb}C1e)pNsUHY;hT&}oakWJ-2q*WxOfeH zQ%`Bsh3^0=25b68h0PbdrZRV%+#uO`7UPFKWu;7U@z-p$aD9R9rv(P-YLhN>mA1_2 z`D7=Lai0Z(7Taw?7VXW`$@V7t)mz@u@I12Nft!st_4c+W)|=C>5LJ|^b>sR(D?&7{6C_sav&{?Qs!XF(9hwTG9zGANYvkwZgTKI$Dqj|Ysd@+g4 zgow)Oe8MHKC~YQIi*2IgraL#`7ph>l`DHwdzfF{CXlUoNuDldyX6!W2bSOKuO>DRvjJhv_uJO=!U;I^k6dbc&uU&pQKDZO_8;83x?k!_uQhdTJ)hx)ybJ~U@ca1UA1AMS@Uq18^@*bH z7S29i&=za74--U2XJycx!-!~lErSSh|7O;O|SDLNvW74I|>Bgy&Ets zCMImSF0;i{g3QY}OAWoJ+mBFjkW(b2HHXh9dfhnNgE<>0D;Wa#&sQMcD4*#u$uS%s z4T|c#5}CV-l?ZevKSRhb(>c=4Rt(&uo4YPr#Azh8c(anN5h}m*{*;krYj{`Wwwf#d z)tQEV93GI66(W&8b^UB#J|9^^CxVbCSN71i?wGc76vK-_ss zaM`xRt9z!QK@go@DhQHZ9#KqFh75Mt@*)Ykr+uL;>puN1`~vb!c=OWgLes`4Z~Cjm zzVLDXg4SBSy#?xY_5RJ~QF$Tz`=Qc4d=Dix4EDFsVIlmj6VOIYZc>o`vz5%?4KrjD z@l_NRV6DG)Q$8eSa5x$E@dl%vL@=DDa9)0y?tJm) z57#uCKEyw*b6qyCy>P&zBKWp=q>!o3T2ckKSj$;!tn*!uTMx99i&oK~C9^Y8{r37o zgpAV8IDV777M70H7Md^p^kX1&CvYs9^|7wAS$EWYHv-r>S2!U#Fsuv~$P1M}ogrH{ zWbB17y}ZjuK8Rz5%6q0yp}1jMxN>K8FC=#v{SwMk#6BR5>)Ur=ic+D1kD$gwz$&fS zWibgtfbEwQnd3aDw(VU({vm{dYBH3RU%`K$LafO=#$;n5)?u-zzifj*4EuwajAB;N z4nxE4+r4_V5VLKHk;M<{@|SX>-Vh_m{%ROVwa$s$wx{szek0i4>hx6vvy{pC6uf#= z9h$~THY#-3-d}w4g-nU-q=(oKNz2uvJKRhLMv7DeI2?P6v1R`FOk*0rx_!rke?G9d z{AGq+sE9q#pIxCbP*<}H4_n6VG*@{VdUJD{P%SGWTiuiE!LZ=CK+;b9dfK19VzT#( zPu;b2_88vG`v6;<20b*cVY#r{r#JLFY_KqMfAw`xi@uzOc@(i`9#d4*OW_8qP1VVl z(C1!s$o|M2Y=*^9yisIc=R%`jeeR`5W1R4Huw@2|4V^rng6F<&?`u0OP1AeySau;g z{BR& zX3ucqZSxHL_}b7k>I?0=tHn!gK6t`j?#UVmS3y8wtMzlfUU_38+6}c$Pj3}0_xbua zbM6+le-~_RXD^PCxBLm2i;pu26dwUR!FF(imXnV-{JBvl9$i}Uu&6bF$&R&VR%+j`4YzAkhj;- ztsYJvJO049Ea}z-giWko?6W+|OVw7ZdH^XOA5=# zMVF+8lye|*5gy-^j4R4$?Lh5S=VMM- zS#GCh(+9JUG^%pyt$U*K$NBt|{+=Gr;@S-K`ri$a`=9uL=b#hlf_=-t5tC`zG$tV@ zyFDI3iY5cRf_?4W1e56*Z%QVkUH1U^vek~F8sm1X7r3x*E&09;sVF8NaoZa9H{s zOzjReg-$DIu4Am;YE>Q=yqU#oj7Yb6WRWKp&j34l_>5aCI&u?T|0=1l3a}Ms{3n?FBB9wHZ%#~nCsejJ(O7Q(ThEJ+Dgs!gRwuRGtSj_@5 zerf{1V|&=}qme2_r|H|Aj_CdYm!h?oIse?PTD9Kqf-7z7@vX{@-YqkK#Va))| zGeF?oOffD!CPp_VYhmYrC(1A2F;3zrcaoS-4{G_?R3msY4rI76{`Ls%=9|7HSMA|9 zZf{cYb@Bv&{?_nTR}IwzviKveq{_O?1!uBFNAAlWLjrA{$D+Q|9X>X}&f!#KJ@uOI zX1#>U%bDFz8{|=w>|Vf~j6NKJl%XLJfUBSxm@D`$5F9O4-(Seb$zjHWq zJJDk&PhR~w?VRB`FIUTrCx+9%)c_Lx`@}+zS2tG#$nv{C3&(E}H;hE=mY?~N*|Hn; zse9)%>Rl}XQ-^Oy8{!#_R@ke65JEcl;jtswr#$ehv%pzM6ek0Mb8?ZKM0qba8p^8H zZJzQR=E_MXNY`SDT}STpl4Hx`{;AxQgxS^-kNaUgG7W4_fT?eevF!OzrNLpfI!*T- z26S3}lg*9TpT4(pJ(PTba~-&YiOdT3Vuh1NmzC&l1XQJ~Pu2k!`UDTF5+iaCa;&Xz zVo6}xd{Gq}bNr{+8!nH1egfaQ`)woFr93a0_&?T~K~vL!^;e*mw_eIKgP6A=`_Rvw ze2E&r$JLTh(-N`x^T#G%K>uj?#kOu0S{8Kicnr`TyFjoGQe51u{zwQ?CE?lp#nBuo1uxo=cXDBixrO3JbBaip zirj2aD%|AsTMOwAW!;2iNNY{0}?JqBy3Ns4kR?NEQBHXvkTU69mRM43N}|n*7i1jl*#!~k^E!{;r(EdSM&u*z}ohn;(nRpO7ZeSkh*9t zp<7f8R^o>4%dbE4XusNX!~AndyJT^fncN`mKQ6L|0)FK;oXM<9*zT3SNEqYxCt{H# zc__;&KbijCQ5mM8Bco-{R-4TxHC;1RCwPy&E9B-iAQC=?>6Z#oh#}!;kL>B8Q{L5J zK5s!Dkkb#pet>mjSeGQo=6wydl4Q6XJX?lcIk{bGcA>;*Fry*gPwYUqE-R*?-3vKU zGq>%Kq%DJb-0n~Hu`28OLs>OO=4OETBSX0Mq5tNj`OwpE1FtjU&=ojto) z5-(;B#Hov^2eN+uWqMyWCW4Y)A((sLtOhG_b`p~cJg?+}&qOLVZ8e>A0s*01-`%mO=%Dl2X4W;5&ZKbvWkHiB z(IDlWh^mpuQMnu?KX=gaEa}e}fq^cbSF}by;)u;tsu#9|XwNC%9ay^8I-jXiQJR$n z`S*Pxly;P*A33Z$X_fF49&57vRClD-gUX?}oG&)t5i}<#%aZO|+Twg`P-JtMvVI~^ ztMW5tH>C*3K&fzF9#ZbC>JN0WaG>CKB*V+Wq<)%ZY4kRu)%&2)Ch@<$J$j^s${7kT zDeruKu=0;!*lH7)Hqb6N>utz-o51p|rd*lreG3Xsmi04Xv{&K0-R>ihByX^K$XxBk zTI~7KCoXHza|dmVguZiv7kbjGc=$Ot$sRz=4}yP%XGC2j*oE@sIt5sCx7b340zWLo z5xsIW8TIBPAmQ?0q@>zG0Bd`b6<>2n>h6xpd@cIg}Tfg>vpJzz7PCEDC4DY3ggVwyD(dS zcHCk<6=%|10d_BYQ*WoQRZR1%dsEKZe~TLOWc6$!gG8%EdEgb)v|e`QQ*t&@HIu_UDgi)}#RoO`buj$NtszNG$m zUDx*Er`z9MJ{7FXEkqt`Xi^dys?Ttry)%t40Qa%D7qdF0xGAe8xOD>0n?!>u+#&Z4 zwbs@;U2M@15PMWQLbcf7{--Ka1SPXb)=cL$^_hhsf1_L?l{Y8;bYiRRiZ%sGk^~Po zFT?75Vcs)zbalF`o*|SaF#Z|nw-VI=bc4qn3;mOE1`1IX4DqN!Ih$M zd*<-;+@+`8=GyV>#Y~ut%LXZ7{(5hDzQBf;F~h0THa!o@T=2t3bfYpc*l+!-u44-x z)yQ6^@^GKNdw7BtXsUT7F{hr^QRwRBhDw`j~fvL*0udLQPckmgTG(g2KP|&fAn*4RjDniHA;;LHz-tXRJwB-%y z!CyzXO#ngcy66Bt^-|DUdPsV?R|7}Di1+I)RJ%l!`IrHrr6W2cxEc{M^skV#8@T@f zqQcO?>L7oEAua~e(k-(8KP~;=R{A9ezo)Vv#Dc@vYh0XLKZol8H-YZT$lY+ib`~b8)Glpl$+2^2{6_ZAyu~I>{>1V4e8Rz>Kd``J&DSvCdz5fEN`COOV z29?foWn$hHKhWijbN<$|i;IS}M#~BuUmTn@zYZ@eENZ9bz^AltnfGk)&qIaf zx*fFNqco|*7qW-H2k7Vg7E-8-Yu>~6vv@M$iEiXY$Gi*oLWpkDHEvIC_Sa_ziv>|l z6^c-WP}y|uv?Wzpt!8)e^a4^{kBb4T01j<4!nB%F;q=l+NMmILi`@GpqRC5VLSgyH z!AojoDLjy}4oq~IpUf=#`9&|xEV`&BTPS-Il0o# zJzjU%w4k|4$;IJHFyE(`APvcM2$1-O+8KXl^oF>*_PU#10|*JM zA|y5sVOt0?$Rn^N$eh8q(7IxYX{y*fShh)ScdJh5oO!5RlUiaWRAeFw+B+iJJBaPz zs|)H<>l|G@p-oF2F+G1}HhHHJzNo_vRW<#s-JZaB3k0shrsv08eq1(`kCpF*N+Usn z>K4za@mmTDznYW`O2(5P-p#B1)Sy$A7bFG?vK*KkoGCCGM%!4l+gfJ^?$VdK#-xqA z6$RbH_b}j=9Dh%^nQW#_wl0Dl*?P$*+Bp@M7k*IVMOuT3hB*L^u>RPg+aw%JSCOF2 z*J3~Hsn0D_*eFbfPnYLx1+Uc~*L{eRC$l#w)Qkr}&rJ&r_!rR;Il5wYqQE>^j9cZe zXDFs}NNOPuoxmd{m4XX*fGTSuH)p^jk(Y)j?o#e#8c%v+pot_>)zBd+e)oZ3tok+* z{o%Y+@57e0uju@sazOtP^`<=@tgaV&WqpSi)p#nn@UV=NXO8lC!>}T!W$_YaP1f77 z72yRDAKt&ko$)UubykwJrZZOA!F7cHp5h_%U2ME|Sb^DTS_2q;ILE!BGW~(5G$QMn{u z!xazYd%wiUvTPI-=LXOC^To4%392f3Pmqt9eN4bceg*ZVIQ?i0+zE6Jmysp=K5I>J z@4Bu1Ag*p(Ha^eMa>@J|EbDi{l_fW9J$E8QEXOA@6bB27&x>CFUI(Pm$kQ069h?xNw)5U@)qXK>N9J@7%K(v(>F zjQM#x*WGWi?LECbh>62!718+}r~E}3(fS>{Q-}EbjupR8Bn6Vg^cNx}d@Jy~@2gO3 zmzgrRJ6dNNY&c&Oyb`541pT-1VlrwgW!10EnC~)bVmw7YEZ<@jY>-07bOs~&y>2>x z4(6j!tgvh~(A%c*{nGziWgUNt4z6(hqn4|2CEORgjc2rU-Tp+%-kc&Gn<#_8^GDci zA$!FaR(}uUYGz)}Y0JLUGrCb}`R*Yz0uYmG!irC@)3HJLPTQh7|KViSY+H9W*(x5=Dp z_KOM6Uj0DDqN910!6dqa6a|8zgd(4(U@O=eL7yVw)`?ba6-c&;M`;y&1Y&hvV}t8| z5S;+Puif|SkEhqns_S99lK1!PoTl`5844zrDZE4%F9?` z7u!|qedps|d^V!DYd(xkutV3-h+0z2#XX^>dpui3yRXiXyt_OO zcXv26T+`l*l9{2hpwG(*FnF)ZYjk6cXeU2knSrFDx1UHSwZV1ZWQ|@dV0Bz0AZ?Z5 zNhgdK4IgA}zcnhpr%1KoSpgx3+7xpAoiAlmld^=Osv{ZgYIc_<)a;mCe@L z`k2`RN6(irvNE!=n_4pckOQ^R5QyGGdUWkIML(&`p2z`G&l2ZW1i6lk^Vn+RC0v*>aFJ)`Xa|qqNw9v+unCUxvP1 zT`~s46>%v^ox5y~yJNq%f58C~$I~Grzyt652~8iInkE-mZjc?6N@_QO+V65%f-mlRO%7E@3jlg zXgf8ZmBBloloA>a--(uRs^18V3GvrLm>_M!mQB^{`=FD=Hp$A69X;=?EBb4wwC81% zn6F$tr(X)br_}eHF1vHN>>=O2j0T`aFn}P9?bVS=e5<|I;st^1oQNkKc;nA}1_80k z@t3OE_^~nD=y)`Q(z!t(?~gT=BXyX+Pbxj>l#tmd1<7d*va-Ye zM!sPCoFF0*jgmZ%V~*qD{nVecDHZ6HzUFEhy&=CUA%q4W>pc$_M7$FlD> ztcjtR->2Wp!y&VWH4$8uMwDQrDA>>ukC_ZX0473zPL36(Rso^E>%o5)Mw!e@4TgP_ zL!JYhzyPg%eO5Wo2%D|zyKI2sc*N>e&TlQ1Af;OKX=F~Q3} z@nTmTUmjP%Ili2{I$TUtqnCr2!u;fN*g8bKFcGLm-hcJHuJC+XF9STCB6-@Kutve- ziYyJbB$NnSycO}vJuVPkzF~Q}Lf&zgp&S{QclM)Y?GJf>gbQeg@W_(hZ}A%w>=J-< z=p!LTnQZ*b1SGt^c;&P?-cqW{%XJsJ!G-h{qYy1A%x%i^JLXFQd6}RrB;rUk#h{lYX z2B`NOB+3Us$61UoEYdss+$*At%5r~9u0BAxbs@DQ$3a?X;S>c@Go&qspqB`XYUST> z3!a{D)d6*JggROEmoCE>-6Otpkq2M9uMoIZ*yV$19nYtRtNKJ;moOW$ydf`BskWL- z@ROhFacG3SMOgGI$J3{v)>O-N>0ihLbRdY75`bm_+PQX zuFp2amz{zF&$+t#6MoIyn&U2HWQdk=VO1{RZvBuj9+{rTxhuVvME*n>Tjho~;grNFq?J zO7Fc$NmG$|<9OL%M2~Q!wt_m97UgCqWi>wWDPceW$EX_Lj$CfKb_VX3v@xgqBG8+t zU-6_dKtWQ7x5zLE5lhD82oA_Wl1R2n;F`qx$>ch4HLTQEWUi!{CT9IcQ^<$a(>zSe zwGy@2#Zyqe-f;yoarPAFpQNT#Fdx^%88PhO*NZUwJgI$g8B+v5BxQWwsrt|_UR(O{ zwb}EVc4`-{laKHsvxA_HbZ3-dP03p8Sp%=Y3B>P43#DSolaVmC%&x*!l>j`k>^)*EK--4KBC#NLMmjCUg=zP3yU z%w90o%`~3EjbPY<_;iPr_Vw#4fswKc+iv+ntLcy|lI8vD>lNJQ&;ibX>l$X!4lQO! zW3aIo>wPq(W9p;nz;9*`<*}GWOrw=22h2)Pkt@)ntwCrRK8Tm-D?qS*Z0D$sxa`!GQuJS!kV+0Y>L|fp75`(`cDZqQ=1yz2RS!YOmQaC^^y7qzX@k(|@Rs0F^NnifLbqQCrPm%g~msCX6BX`Iu$g18c7L zy7i6yJEi5bbyD`cX?a#~sbJXKq>0CNl@kkpgyBmFjBFb6fVWn~b=ECI6_S(g9Paa% zS)j=?(lT90nzgb#sycXW)=kIr3{sb{V6+>ibnkeSs_g#FKoCYZrI(JNCcz)Fg4im1?;HfZoikgbvpus$hu*0vHZr(<>VwF&6u}oW!}noL~efnT*PLrm80te4=0a9-u~=1qu#KO>xaQW0LLSfVc#tH z{iEX!^hHy*Lkok0vSMZ!^4x0X=5L^BH<269uKL%&`!-U=A&jxM@e`#~aLUCG0|-sP zgFeOjk_`R+`S|x2E zk_vQztTT39DRyjTO+GXF-R7NyHIR(IUajB4@Wm(D0JA|n6U)z z_-Pz?cl`c->1m<&3widFxl)&!oZ*7+qwG^I77M&7ZL>Z1BXgxP4i0N&v0VV z)2^?75+ocr{eXkMA}BNWJE0h$rRVeAz}u=?aLV|gn>1%pr3r^N{w001u(WOEEjQK ztj_oCU>)xK0=E{3z~>p9r84FM=N83TUWxibc!hH|7?DZk4yfXFhV^E{F7>@b_&ek@ z-qbF96*I0VzCGbaY{ce5%ft`Ve~ahQU@3oNRawSNSo!YLD{O)K$==H?ZE|8n>#!?B z`eRu{fV9g(F;QO@Oh|?pe4ZfQKhMGswhl>}G(%!fkTC85>lSRpoc7MHub4gRL`zV3 zh^H_B-Morh+HOQXMGErOwsB<$-#6p(X4X7wY6pG`})4qkw@(NvDcpZZbHy}Z9o=z;82C@ ztU(!hKnq#cIN5NyF~@DWPUFut>Xyx<%7+i_lMx#!yDZ)#p}`x}&blbxC$xQ+Edrf~ zTf~u#*l!OkHOcN{0z*whuT$M?kbOEt&92Zw4a2&ScXWq0I9AQ<_+b^Mf2?lrIs#&P z4^}V*w-hyDpYbuutsL~zYDm7smm zD>|#nOVQ`?$(AS-KZ_pMIu$xq;CR*M#LbUxO1vQy7`&j*6< z;3Z|U*MXn=Mu{jzN@u0`w_U=JRois;iv|i`pL6EJ6~>D4Q#i?p29&gfJCV>%qv^(z z5?fCixUZS^pQor~y6b;^boL&omP|?2jLm-2tHh)33TEjH$d0Wt%D1xiotk(#%?oZx zazE>(sZJpS63U*~gUc(K^T``{HW9w7{ z+fQ{a5OQg6%A5Y}>1;z>LZzwVAkBzzjqaDgz${(V6-_dZR{d3Oz{k4Y?C<%8sesb~F}g}G0^9YORe2PTaGx_LWlV?2l0aC|?)w#GKKFsHavI+q z3hE8d24p5Rhv+Irq19)H?@8e+rp?#^O7k;#FbVv{xuwWh=1|_K2A;k*CbT^%u!aTu zi+i1IYiS7(-xVaNx@^61FVIPvSZa2a^jQ_&3$8Ke>_4JlcZg$ZmoFSK#Rnj!>)gP- z(U!DxqTk^7MxD7dX%~E(n*i7~uztS4{q5ni++?*;V6~`lcjS!L8_g(X-*fK`HMhXq zh&J2Dw2;Z&Fx%6m7k(hvJL3C92S4*id37N9q#FAJsGMkRLWNGuotMdqkZ`+CM8!Jp zmx+uCIhSoajpq1V>Kp2Tklh$q2=RdnC?6EJ1NX;H{W#iLDfNr}j8inO=TRJrS9` znwhLxsP{bS{+!yhc2g=CA_!9`O7Z*#QCjCv5TXg?K&kQ8Z<+#m4IbE)zEAqwPn9HQ zi6}EOP7PYAXawFeXQv#gcrz?!Fz9hRzrVQtqw9M+fVjWeAAP&8YU)m9Hqmcx5$Gf- z4y(EZ=(gSg*ffbOnf-2qUN%2ZU~*U^1>U;#aLJp4k7o*4jp{7Gx{m3=y6F!%8H+o- z?)P^3BW+i+|7>*!TQYVExk&6D91JM-oA^9<@_xQq@cX{pB~YM1Ye#LqF?6M~Ztgp| zpDyrT1vIfqJBD4ayIVZrUpaoh_*aS%SxWsYJ9!#cc>a4;)B5ka=>Lb8y#MJ+Dq07A zuUU^;aD#ywk-w#0uc!i|eOmot?l(+eK$q~G87C%kw~XlPB7&&)&=s57@dHg0@wzO8hRX9UK(W1rg1V9`3U68}4MgD;rU< z7<#I7uWcTnR5n-~AOxt07uPn7mk0Fwjo{Yfsvi0UhllBYl&#K zVuFXOg8#HSoB_zainMn|Vqq+q zPanY3vMAB(52rpa7R!+X^Ns6KEuI!m*OqlMYPd8HBN~{zjhVF7q}yT&`#f)9Hldl~ zO)7H%wNg|jj$KbxG!dzU=>3l83XjE+MeXhTJ*R~+Kk&y9zA>dvnV8kY@=g9s{)J`b zw`0{pGW1z@b=1>rHwYJ9Y5ndX1j^!bSiv%ez%heIT+l4J&dl0?Kl55o;$&Mn>|ssw z8pY>a|IUFDMU=q)CB9qlM#B>c+gWdB)cN4z^f#`-MzU2nUb3bXrwz}!mbJ2!VG6oD zaYXomu8G%BSP`77qQS_~7zu`C9BB`JujXx4My#q~fU14C^D!qL6(Q{2c)D$mwm6YS zsxyx%NWo^`N&hhl8Yc+a?4Z|=0LVO<1kQp*lWcPkOht{{yD#j2Z7z$tR>lSoa!->*@riBy`S| z%9Q%tkPZWmpjexD^%I?&pK!eJujT!}yy*=o{N@<*F(RyHuY@_>G_|m^9o32FP6pk=>KC@#Qdj2B%RF+67=y)^YkZ#$tN6=#tfc{fQJ>Q8*3`L`?%w*(c zT=lOe>RnGnt+h66cJT#ib`E{H43|Bu%u-}!NHwp!^X*T$F|{@bho+4UZ=XIekuqdf zkM{hrk$czPBSXWVP2bV=b9V|_VD>)N5X|N%P{+OIZ!MhA4sH@kb2olAnht_3Bn$Mf z0Go(YjCo|2`}mTc#CBRd!n@0OrkzUM^CR`Q9ZEU3)re5hERL=IGamddI?&|(2=w|6 zbwCRbEA|gbci}t6DgrA|@!jcHfKPSh4wlz2TH>-v1B%gwM&9XUpS2aCspzmGLo2pr zhtaEX2A7@(WyXhey-fW~y<_JIhbJTOItI$v=)^V%%N!Sp`r@zj40vX9_-PKc`vFFs zW-EV1XmzVw1iS&M8=$le+wap0YL)FayDC#w_UY6AtvB=ys4q9TEsa!N)avETcmqZ1 zuSI(L>{vHnBf*Iu?+BD`JZ?@EaF5%38oy-RtNz8rl8q$%PJQHdWUV=z5vc z2{i;BMR^v+0Fz335SE`ERb6evzijAs%cngjkq10R6M=vD+YOC!KxG_{XCk-Sr%+5M ztj|<><`n)JcvLH+*EiVbHbd@q2KPBuH{W&MtGh={Plvcd`ILQ2I%IM4u`Pb`(OTV} z)P*hqEpFo_y50#`m9*tpgB9y_af;74YuWqw{?K@!oBoYrHW%oAb4V1&Oow`ZP~k)X zGhxgvag1b;Wa|8bqrBB#UR4wQHYPpx$5{ZYE??$22FRjYJ*YHdXYh$=c=Uauw6e1E z)NFJZVQI~Tj7snOz}S$zqr@F>cuXLfM~PzDd_(F8ZQ!49nv;q&OgkfELoj`s$ok+} z=mzOiIZI)^_Uj1405x&fd^+d?%im-jBaz@bcv{qO&`G=o71@$XN7gLXXz4js!pF=3 z(6l^r94T~BDuOn66w3A7;iyt06x;aHLDXH)-anv%e$N+^LJF?1fT6W3rrZ6zBBOQz zZSd9-nl5DN4uEDq2~lhxe=JM#Fr>42zu^+5OHKw_o`{|_a+!ngf@3E>kUKnU=r#DeeEKfm<7*!u2gQjCNAUhZIU zH)+)D828>&6m}Gpmy^5?DUR#?P7BUEE+H339`E@|D~Og)y$9o=ErTStG*6V4Z{JSG zp>37j%J2X3!yVKF9}y^>?zWiIP-e!ZT98N~y#x5e&^9FrRyNjKqh&vY9tCvhL5KNT zw-7Byqw(Z9iHi{(3!-2HZgpU?m;zzc7fBG4f@JM!OjB#dGG`JW<~I}3+2f}H+2k(A zMZc(+Wlk|hqS9;FRWlaBF>zRJri>=aQsOz5nh|Zo-7UdNyRGOFskEgaWmPj5H=~wh zPLZkMB(IdU%GJ`Z5Xpu3;(ybp={d zKs9=9&$T3hM0f5k)$V}#dL}f0a!UH;aNa5j{>ovy!B}B{cOEO9KWq*?aYT)x?$ngM zwo`$5U^c0|{wuwrCtYi{f_z$a{AKmk_aaA>UHxRrbvyPHcXwV;HUXz;n&0%50~isZ zR>Gc;9jH=q~JNgcUYrE%oKO892zqINkGl1|0&JyCdaab71h6 z8aM;zXLE9!BoM@`U_|9`G@AI0#>zLS${$`%dc6iFYrp`5aV|%G74Uxu8NO`14X34>qB~nm#{o*md79~C ztVFH9%hAD-Uo8soS_c!oK_$f%aF)W3z+ZxhK`o`*tVEqK{X!Oq zypB^q;tibn4Tlh0_$#!95dC|lATAX8F93Ah`e)HWhhwHL0oOSJw)_zb#_A(bAFYP# z^2r(dIJi^t6U-v0Zf&s!63+OVHl{UeC$+Ix{~Py9yE%A zHY`_sl^#!5+yqZ1%@}O@>huNB_AgK(87rN^nmg5RX{Rnhze%wTi`u(5gjAB_ z>VKe50HlWu|MVTiOk~RE#0_5W(4?;l?5o0f@DUjMMveTJSKYYas+!Dx&ug}e{VGIF z%_A=zB04yXPo1hxtDd_(#1z4vLTJT%(j>1%VbVIXs@mxd*cYTHAZlP zU}{re=v+yGKqF@qHZQyAQEMq7P2GG~)G|-r-75nTpyJ8?05@~PwpZXANOJ=bl)RgkbwvvD-vj)pmggTR9Ki8AtP3pn^O~KK zqaul^16ZY!`q1hu_HJMUYqq=itx4Thqqg=RA%zI$C8l=sCLmm%>E~$@CjyQXB94BdDG^i}+J8YwC^Q96uwVwM{9v95|*{Sw>u6Xu7x!PpszQ zIc-*4M%)V6eTgcfV@n$<+X*MpG?=Qu4Qa|5=BmXEIL)upJ13!If@^2Y&-on@X3XA8Sa{w1V_ragTHikfQId_yuSCrAC0Dm5 z&MJ08)=tahHdR@D&T?8cz*~cp@(mU^LYtX&?abUeY{R+}os@T723ytNXAnqVj zN<)O3%h_CI;B>Z~i<()|rseEVM~tQ7t(C4Kgd8RCKT6`w!MKkSNE1XuayQrtH>Gv5 zBU0&zfuX}Dyr>`1Zyq- zIm3XB^+YWncZLFM=1T);V3NtSbkc~1Bo&ak+y8s&fv%L2f6gvoHO6;otKFI%DfR1~ zcN<1b5P~|=z_64?Bjczs@8HbsV&yY-bmodD_}>QWwuZV=&#JAo9J$dn4-l`wW{8fn z(`%ozSALR2x2zX4KM%P2(X05k_#srN%aC4phRxRAm-BS~uoLU>IPhfr_D+0h=5f6i zscvxB8cjvAeJ;1wz<*rMcHkf3{T{kCUy5BmbKjSdaoMBh^6TzqjR;-K*@$2H}Y zpu=NmfYQ<)>7w7^=ORAuzB_WuBwOQG-l3S{+{~AW7@*|b@OCK9zSb^oSqL_ zvCZnhX@QYJB|yB4n2|oYTo~_Th6>@1U1z`X)Fx%_o>q?Z+(L^G;h+SV|1m|*)wheE zR0Q6z1$O#hAg+*}3Bc&_a+A#F90=a{YnKbm&HiX-o4GkV7JffGEggBv63#1rk9;R6 ztdjc&Y5}aosJQ?N%`)C;N@&s#sJQ+_UF{^;8ivUKK`GO z#r@i1&w#ba{>NhCs21xhO5}8YOi-UACfS}!rtCs?wjbZq;vx_S)S7P;@GFBiExDGUlL*e7{loH>?zbn%OMDA~ z@m)tTPN+!R&7kxKJBj;sinmsI*-Do*eHj+nY6P4&o-V}${E$li1{XLbp=^rnM6S8U zNumkCXY?d}c$z!4xVT)DrTqIIcz&jmcEp6{Wnfw^H1$dY(4;-6e$RLD$&z-V@b#Ru zwi=i*u9kmYFtNpkm`%&P+IYrEG4ecsaDQj3RoHr+gVzH{b=XN=AJ{Rp5hcD1RCn%f58A4jaF*;TfA*%5`~u zD4fmDc^gVhb%=5r>zl@PR`#^t#k=s5a*d0k;qm)*Jcq2I&(-_*jjI;VF`A5)sB`Y1 z(r1tgiaaG<{D-3*KEF4~_4pg$u5E*sW?h#=Fkwn96O)0zOUAf2>);={Gt*%L^NObc zSJqXi^EQ)cWjYNbF(p9Rj#lkXc{QQli3d+>4-M*cu`9OQg z+ekv$NC9?3^T+=~K1G~%+g^qxcN%G^xdlI_X`7?}5&x6Vzw;J-MBeo7Q&Qf<3>q{X z>`mK9Z8P9tH0HW~b0V|o&D!o=80=FKU~@GbBVDXpX=~{06Q0Z9MOL(0b}wkf6|ZGF z*n*j)E0i%?WO#g?0PTDHyN2Cqx(-WAT)C3g`Jm}vNvvD)o0(6~`Eh;V6(SL$-#hQ0KZSb_xI=}Lin=(X8%t8IC2viF0P zgPOvUyJ(@XkgExyDoZdJfnIq|Xu1P&aoqD5KTFSX6yc0TAahhD0a`#oUY0uiC4_96 z)-c~4esCAeMKJfj4X&H@Za0%Kcp4TpdH7oLl%J%{MZ;95Ybo2+O^gu{+$+3G>pA9X zb*VZWRroExAhZlfvJ-jZs|UxAXj!WtB~I%56vyq8tzH7)Sq?=B?$@dxh}bf)Qh&(~4uS$>Fw1cf>#@_+0J(Vn&5NCkU^338X8*KKM)J<6JZc5Gehc?Zlb{_YPrn(dp) zKb5m)mH;Q@XxR8p!6l*+Qwaau2)s%J;Mg26JZiwE)ym*ZF!Ajs9R;J~&!}7OvDmi- z5#_&Y17@-_e;t(qm{$RM%=ALRHjU;Pf@7EH%5I$-e(lH%EXI8y-MNUoi5&2jvCCZy z%Nr8;PblG55+Gs5sy8(mV*Yh1U`3Kms=@*xh3+rF0y3IfEe?mfWx!_#bx2W$EEl4{ znEAe!sR?(HlqZcs`t`e$8pN7NJi`pS%yoLH^x zO@q(xX+Z*+EWNh9*Y0cPUOZj1KZF{^75H=)Hmn%F0kUTj^W8w$&{BUsB$9z~H3C?k zGCkgN7Mth`+@6gKHgu~Adla(9Cyb}zc;JQ{2Ij~EZx5>{I^wKRnLH~g(cUi?)U#1 zj$d} zGyx!HP)DIf)9)TZ@zwIXOZ^1fziS^w*e+I`*MN6e?HI(T1OoISXxn=ey|fCZ%`k)LX8Es1+MMYI8#9LvvwFO#CnuX$sP z{bxDS2g1aWECMdQ1tT`c@8h$X#e!xB);%ALue8?bcpO1~?dv#qsrOFls7^Q!%w?*0&stj-hHpXK{(`ksrOR6kZ z9AZlq*kWSTp9~QQ)dNX?-xf*I{H_A|F?gJRiI8v*O)+o5YyPAu@3*Wb3x&+fj?{bt zD3X^~V&U&1s9=z@+CWo$4~xVwj%hEH32>mY{AEn^XytbjU^?s9>fHLcg+RHU$ZSwX zdlMB(q&$c(OC>3WizUSVC?ejQ21RmIw5|5Wh>saZv)ZA2*Ek>;z#o4RaFBR5FP zFE0qAiBEP@@87qVl=P7BF^$dmehW`LFVCJ6E)%`$b=9^o>ET5gd6H`@%?7iQmaZ0- z5N1IB2PFhsrLNg;qDYDq%_o!sYH>1XL z_&-NqepjPB*L&}0!$HR@G_$Fo3rB;b+Aa<3V%@o!zaAD3V(ZHI1}9Q{UD`lwkX11A zy+60rcHMJSby#Z!Z6FSf-V^EGW9iPO`DA-V3n+5usz564RtwA27_4HUK>rmEbU<{-Uy-jX{GTe*fk z9(;TiCOi<%FW!mW&N?83Q|SFkk%M zgO2wCyfA3|+MSIdN12F^*R^s|&?bo<=bovLBkld2&a0{q=IA@wh&W(QteE`4&^R)A z*N-~BiN7hCfg0H#xsKzfCV{4Q1W;H__0U!*a+KPeV$u^R*p`0or z!3P(O6*CLom2{zqptaV093ABU2cbThdwwdpZS_T^KUOZ{vUZH>`78~6D4$7Wq-}n8 zjnlMeL|n#zn%**MLb= zL^V@WzU|V2+Xe<6!bxF*cgpNl8=%mdum+VF>a^I$m z44X|kPx-ZDn8jVgh@1}O%ELjAr>%R$D<7YcJJ=C8Ejb_XmJJ)@wS*F~!;B_K%-e%F zoZ;42T2Eeqet`6(;H#NcRD`*(8niiONPe}-ai>O)Y z#wn6FQnMo014k5s?{nC*`W8d+KjUZ9?nIhNC^y_;n& zP|u(rEK`7^$wtvQDr@zr13N)11@J9b09S};zSl+L4+tUIu%qFH8n;KH>Zuj>zT4b4 zXD^4J{i5tf0l58pckw>IN6$kMluZ}9NaPbT@=G>8X0@%$Zw=jsjZ@gx+ z|Iw8YNvtvY``{_I14OyIOc3D_;Wd$-&!8K6~n-Col}7M~zGg;Yuy%5i;9^If~KI znBJ3cl}vL^2i_BkAWb8}yYx?&tRdu{(aR$R$!9Cz*w}@R9mol;lm51x3(w01&)rF! zzCUPyE~LqbmH*ORz#_z#{%=0_l(+rtE6+|TIxwUD6|9Uqgt{n^s-l)cg`8E`{{R;X Bk3s+d literal 48977 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 St3llW0000`__) + * Fedora: ``python-sphobjinv`` (`info `__) * Gentoo: ``dev-python/sphobjinv`` (`info `__) diff --git a/doc/source/syntax.rst b/doc/source/syntax.rst index 0bbb877a..0b0f57ed 100644 --- a/doc/source/syntax.rst +++ b/doc/source/syntax.rst @@ -5,8 +5,8 @@ Sphinx objects.inv v2 Syntax After decompression, "version 2" Sphinx |objects.inv| files follow a syntax that, to the best of this author's ability to determine, is not included in the -Sphinx documentation. The below syntax is believed to be accurate as of May 2024 -(Sphinx v7.3.7). It is based on inspection of |objects.inv| files "in the +Sphinx documentation. The below syntax is believed to be accurate as of March 2026 +(Sphinx v9.1.0). It is based on inspection of |objects.inv| files "in the wild" and of the Sphinx inventory object `parsing regex`_. Based upon a quick ``git diff`` of the `Sphinx repository @@ -194,7 +194,7 @@ of cross-references from other documentation source. **For illustration**, the following is the entry for the :meth:`join() ` method of the :class:`str` class in the -Python 3.12 |objects.inv|, broken out field-by-field: +Python 3.14 |objects.inv|, broken out field-by-field: .. code-block:: none From 86ca3214c389a047e61b6eb63790da0b08e4f543 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 00:57:17 -0400 Subject: [PATCH 087/122] Bump package to 2.4.dev0 and update README example --- README.md | 14 ++++++-------- src/sphobjinv/version.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3bf36e72..14bf93f4 100644 --- a/README.md +++ b/README.md @@ -42,26 +42,24 @@ For internal cross-references, locate `objects.inv` within `build/html`: ```none $ sphobjinv suggest doc/build/html/objects.inv as_rst -st 58 ------------------------------------------------- +----------------------------------------------------------------------------------------------------------- Cannot infer intersphinx_mapping from a local objects.inv. ------------------------------------------------- +----------------------------------------------------------------------------------------------------------- Project: sphobjinv -Version: 2.3 - -220 objects in inventory. +Version: 2.4 ------------------------------------------------- +151 objects in inventory. -11 results found at/above current threshold of 58. +----------------------------------------------------------------------------------------------------------- +10 results found at/above current threshold of 58. Name Score --------------------------------------------------- ------- :py:property:`sphobjinv.data.SuperDataObj.as_rst` 60 -:py:class:`sphobjinv.cli.parser.PrsConst` 59 :py:class:`sphobjinv.data.DataFields` 59 :py:class:`sphobjinv.data.DataObjBytes` 59 :py:class:`sphobjinv.data.DataObjStr` 59 diff --git a/src/sphobjinv/version.py b/src/sphobjinv/version.py index 3840d8c1..ac087af2 100644 --- a/src/sphobjinv/version.py +++ b/src/sphobjinv/version.py @@ -29,4 +29,4 @@ """ -__version__ = "2.3.2.dev0" +__version__ = "2.4.dev0" From 40109b0ac963509a13c4a5717761d728cf475c83 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 17:25:54 -0400 Subject: [PATCH 088/122] Specify Python 3.13 for black, and blacken --- pyproject.toml | 1 + src/sphobjinv/re.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ad2588e..adeec1b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ namespaces = false version = {attr = "sphobjinv.version.__version__"} [tool.black] +target-version = ["py313"] line-length = 88 include = ''' ( diff --git a/src/sphobjinv/re.py b/src/sphobjinv/re.py index dfe4c9e6..017a181a 100644 --- a/src/sphobjinv/re.py +++ b/src/sphobjinv/re.py @@ -45,9 +45,7 @@ [#][ ]Project:[ ] # Preamble (?P<{HF.Project.value}>.*?) # Lazy rest of line is project name \r?$ # Ignore possible CR at EOL - """.encode( - encoding="utf-8" - ), + """.encode(encoding="utf-8"), re.M | re.X, ) @@ -58,9 +56,7 @@ [#][ ]Version:[ ] # Preamble (?P<{HF.Version.value}>.*?) # Lazy rest of line is version \r?$ # Ignore possible CR at EOL - """.encode( - encoding="utf-8" - ), + """.encode(encoding="utf-8"), re.M | re.X, ) From 2a0ebba683d81068a636395f5b80422fd16fa928 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 17:26:05 -0400 Subject: [PATCH 089/122] Bump minimum jsonschema to v3.1.1 --- pyproject.toml | 2 +- tox.ini | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index adeec1b8..2f37d8ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ requires-python = ">=3.10" dependencies = [ "attrs>=19.2", "certifi", - "jsonschema>=3.0", + "jsonschema>=3.1.1", ] dynamic = ["version", "readme"] diff --git a/tox.ini b/tox.ini index b0366da9..8ada0578 100644 --- a/tox.ini +++ b/tox.ini @@ -11,11 +11,13 @@ envlist= # 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} + py313-sphx_latest-attrs_latest-jsch_{3_1_1,3_x,4_0,4_8,4_14,4_20,dev} # Earliest supported Python and lib versions all together - py310-sphx_1_6_x-attrs_19_2-jsch_3_0 + py310-sphx_1_6_x-attrs_19_2-jsch_3_1_1 # Spot matrix of early Python, Sphinx, attrs versions py3{10,11}-sphx_{1,2}_x-attrs_{19,20}_2-jsch_latest + # Test the new-earliest jsonschema version that started failing in 3.13 + py3{10,11,12,13,14}-sphx_latest-attrs_latest-jsch_3_1_1 # Test the specific Sphinx threshold cases where behavior changed 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 @@ -63,7 +65,7 @@ deps= attrs_latest: attrs attrs_dev: git+https://github.com/python-attrs/attrs - jsch_3_0: jsonschema==3.0 + jsch_3_1_1: jsonschema==3.1.1 jsch_3_x: jsonschema<4 jsch_4_0: jsonschema<4.1 jsch_4_8: jsonschema<4.9 From ade2a2069e8cfea160ccbb055a91ca063dcf3c37 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 20:25:22 -0400 Subject: [PATCH 090/122] Update sphinx and attrs invs --- tests/resource/objects_attrs.inv | Bin 1434 -> 1838 bytes tests/resource/objects_attrs_22_1.inv | Bin 0 -> 1434 bytes tests/resource/objects_sphinx.inv | Bin 14192 -> 17616 bytes tests/resource/objects_sphinx_6_0b.inv | Bin 0 -> 14192 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resource/objects_attrs_22_1.inv create mode 100644 tests/resource/objects_sphinx_6_0b.inv diff --git a/tests/resource/objects_attrs.inv b/tests/resource/objects_attrs.inv index 85189bdf1858248a4f6b3eead8acdc496c072967..7c5a9b0a8c39d9e1c978f5e68bce2409a0dbb340 100644 GIT binary patch delta 1746 zcmV;@1}*uT3$6~3Is-K>G?6_zf2CN>a@#f#zVj(K(_HCkueo#-MUFj9Y)|YY(-Q_F zAqis$U;)Usa_ZCc!TKa!{E*@wB!IodA@qH__*v}k2duTs>*M>qs1A(ezvj561%|<^ zo!+xw+;H(OFETs0fPd)vpo!s>3@erYHlVcFyZfh`+q+K&Sz(RC!0$vVe=7Jb--rma zXpf)C)IK?T@XH(2fPnlh@7ah7_FkkvoP@m0v`p0il1 zT}I!k;4egKBF`FXK|Ss@F13(nr_ewe-itd(MByZ-Pqi2x>nH%cEvzb9YD3%{c&Xvn za72J%QN}{`p=Kl!Y!(>@e=YYlIw;Dwk?>~ARl?n?$cFOBsgoDMUShFtC8-T0Wils{ zy=AJsYIj0kivzD#QVJV(Epj4&BCU*lek2=wV0ytM!PW7R=u~$B17lCuQv4*momi@$ zNqWf3JmcqW-g2ceFK14?&mQ}Lb6VKdDVvT`olMdvy(}?fc+JQRe-C}?*!sg!?*xAm z;+`c(?U*~Z|BYkLg~pmxv%@HC!~5G%!m7CYZlNj#i{f+fhO)%8<{4(S>`;*;H&>LW zkZefDVYfshdEn>;GYZ{{h%E#=90{cBIGm})$Q92x{6U*wj{{4hOr1dbSd&Qmv9Ou@ z2fW1i@g5>q+@DXJe~S@x8Wn7g`%~!PI%RrKAdytl_^E#N0&u!DO(0#}EFNzbA3m=P z8@CIaV*xD}%bV55`G!8P7T;HB@%jGlf6V6F-Mke|cH;@8tLyu(%lSS*Ma}2_xq3_^ zOz32GHxn#B&UJtmY?iBjrAcntYxafxB(l>32gog?)xc#hKV zmuy^C=5ZEg6VFKNO9n3M<_HUCH1Q?&1UL(M|>59cdZ1NEsk zdM43l(ct4UeuECH)FC4oDWbRWM7YAdLMa}g43Qd9SW8?u44Hde*HKl!OQgy;tCC1t zk_gvmo*$hAryM2XU2Q>Plg3l1^kpH5mkLi?z14U`foe*my&)EJC%YWj=~NDsvb58Q zZxOpBf0^b)#fD~0bET{Il_m{FS{f6E=_Tf)S)yFu-s+o5IinU!K+V@Va9 zMi<_WdVKqxb;L|>P3F~PupYDg@}RWX7XvpfYv9OmErI^TW&<)Wf6n;w4Ydzm15vE#VZlB^$mf;-z>hGV-|GAa zjQ^euk-a%hSqa$ijak$zZuhofwU14ff9&6LiV{Nss!%NjEK46_tc~eKOJCaW{K0)w zm@-_^NB?%TbT=eBovvSX=U{DUzi*)V4+^*9pz{xcG9qgR{aALen&f{nQyJ0USsVHQQ(8cKAf57%$ zKsP!O?)9-~esG??xHi@WJ~wrzJ1INhfcHFqg8H-+&{UYxWouL>AcMY8^4cXeW1i=~ zvRcv?*yo}x6KD4B!C~{o32U}u!6-H--C4zjt#UrbasEhI2ir+?|-uY0LB^4h%0zy@Bjb+ delta 1339 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 literal 0 HcmV?d00001 diff --git a/tests/resource/objects_sphinx.inv b/tests/resource/objects_sphinx.inv index ed2fe5b49dea079c6de5789bf7e6d5695944f23b..7a4f085be3d3b5c8ebc6785883e7cba494a58a76 100644 GIT binary patch literal 17616 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 literal 14192 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 G(=kkTBdzKH diff --git a/tests/resource/objects_sphinx_6_0b.inv b/tests/resource/objects_sphinx_6_0b.inv new file mode 100644 index 0000000000000000000000000000000000000000..ed2fe5b49dea079c6de5789bf7e6d5695944f23b GIT binary patch literal 14192 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 G(=kkTBdzKH literal 0 HcmV?d00001 From 2014ccf6ef60a73fe28643a6d4d332bb6e784f65 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 20:50:47 -0400 Subject: [PATCH 091/122] Update JSON and TXT attrs invs, too --- tests/resource/objects_attrs.json | 2 +- tests/resource/objects_attrs.txt | 121 +++++++++++++++++++++--------- 2 files changed, 87 insertions(+), 36 deletions(-) diff --git a/tests/resource/objects_attrs.json b/tests/resource/objects_attrs.json index b1144317..d0ad36c1 100644 --- a/tests/resource/objects_attrs.json +++ b/tests/resource/objects_attrs.json @@ -1 +1 @@ -{"project": "attrs", "version": "22.1", "count": 129, "0": {"name": "attr", "domain": "py", "role": "module", "priority": "0", "uri": "index.html#module-$", "dispname": "-"}, "1": {"name": "attr.VersionInfo", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "2": {"name": "attr._make.Attribute", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Attribute", "dispname": "-"}, "3": {"name": "attr._make.Factory", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Factory", "dispname": "-"}, "4": {"name": "attr._version_info.VersionInfo", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attr.VersionInfo", "dispname": "-"}, "5": {"name": "attr.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "6": {"name": "attr.assoc", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "7": {"name": "attr.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "8": {"name": "attr.attr.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "9": {"name": "attr.attr.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "10": {"name": "attr.attr.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "11": {"name": "attr.attr.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "12": {"name": "attr.attr.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "13": {"name": "attr.attr.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "14": {"name": "attr.attr.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "15": {"name": "attr.attr.has", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "16": {"name": "attr.attr.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "17": {"name": "attr.attr.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "18": {"name": "attr.attrs.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "19": {"name": "attr.attrs.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "20": {"name": "attr.attrs.setters.NO_OP", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "21": {"name": "attr.define", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "22": {"name": "attr.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.AttrsAttributeNotFoundError", "dispname": "-"}, "23": {"name": "attr.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.DefaultAlreadySetError", "dispname": "-"}, "24": {"name": "attr.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenAttributeError", "dispname": "-"}, "25": {"name": "attr.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenError", "dispname": "-"}, "26": {"name": "attr.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenInstanceError", "dispname": "-"}, "27": {"name": "attr.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotAnAttrsClassError", "dispname": "-"}, "28": {"name": "attr.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotCallableError", "dispname": "-"}, "29": {"name": "attr.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.PythonTooOldError", "dispname": "-"}, "30": {"name": "attr.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.UnannotatedAttributeError", "dispname": "-"}, "31": {"name": "attr.field", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "32": {"name": "attr.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "33": {"name": "attr.get_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "34": {"name": "attr.ib", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "35": {"name": "attr.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "36": {"name": "attr.s", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "37": {"name": "attr.set_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "38": {"name": "attrs", "domain": "py", "role": "module", "priority": "0", "uri": "index.html#module-$", "dispname": "-"}, "39": {"name": "attrs.Attribute", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "40": {"name": "attrs.Attribute.evolve", "domain": "py", "role": "method", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "41": {"name": "attrs.Factory", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "42": {"name": "attrs.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "43": {"name": "attrs.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "44": {"name": "attrs.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "45": {"name": "attrs.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "46": {"name": "attrs.converters.default_if_none", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "47": {"name": "attrs.converters.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "48": {"name": "attrs.converters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "49": {"name": "attrs.converters.to_bool", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "50": {"name": "attrs.define", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "51": {"name": "attrs.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "52": {"name": "attrs.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "53": {"name": "attrs.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "54": {"name": "attrs.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "55": {"name": "attrs.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "56": {"name": "attrs.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "57": {"name": "attrs.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "58": {"name": "attrs.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "59": {"name": "attrs.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "60": {"name": "attrs.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "61": {"name": "attrs.field", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "62": {"name": "attrs.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "63": {"name": "attrs.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "64": {"name": "attrs.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "65": {"name": "attrs.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "66": {"name": "attrs.has", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "67": {"name": "attrs.make_class", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "68": {"name": "attrs.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "69": {"name": "attrs.setters.convert", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "70": {"name": "attrs.setters.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "71": {"name": "attrs.setters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "72": {"name": "attrs.setters.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "73": {"name": "attrs.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "74": {"name": "attrs.validators.and_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "75": {"name": "attrs.validators.deep_iterable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "76": {"name": "attrs.validators.deep_mapping", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "77": {"name": "attrs.validators.disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "78": {"name": "attrs.validators.ge", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "79": {"name": "attrs.validators.get_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "80": {"name": "attrs.validators.gt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "81": {"name": "attrs.validators.in_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "82": {"name": "attrs.validators.instance_of", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "83": {"name": "attrs.validators.is_callable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "84": {"name": "attrs.validators.le", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "85": {"name": "attrs.validators.lt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "86": {"name": "attrs.validators.matches_re", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "87": {"name": "attrs.validators.max_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "88": {"name": "attrs.validators.min_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "89": {"name": "attrs.validators.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "90": {"name": "attrs.validators.provides", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "91": {"name": "attrs.validators.set_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "92": {"name": "api", "domain": "std", "role": "doc", "priority": "-1", "uri": "api.html", "dispname": "API Reference"}, "93": {"name": "api_setters", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#api-setters", "dispname": "Setters"}, "94": {"name": "api_validators", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#api-validators", "dispname": "Validators"}, "95": {"name": "asdict", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Converting to Collections Types"}, "96": {"name": "changelog", "domain": "std", "role": "doc", "priority": "-1", "uri": "changelog.html", "dispname": "Changelog"}, "97": {"name": "comparison", "domain": "std", "role": "doc", "priority": "-1", "uri": "comparison.html", "dispname": "Comparison"}, "98": {"name": "converters", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Converters"}, "99": {"name": "custom-comparison", "domain": "std", "role": "label", "priority": "-1", "uri": "comparison.html#$", "dispname": "Customization"}, "100": {"name": "dict classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dict-classes", "dispname": "-"}, "101": {"name": "dunder methods", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dunder-methods", "dispname": "-"}, "102": {"name": "examples", "domain": "std", "role": "doc", "priority": "-1", "uri": "examples.html", "dispname": "attrs by Example"}, "103": {"name": "examples_validators", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#examples-validators", "dispname": "Validators"}, "104": {"name": "extending", "domain": "std", "role": "doc", "priority": "-1", "uri": "extending.html", "dispname": "Extending"}, "105": {"name": "extending_metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#extending-metadata", "dispname": "Metadata"}, "106": {"name": "genindex", "domain": "std", "role": "label", "priority": "-1", "uri": "genindex.html", "dispname": "Index"}, "107": {"name": "glossary", "domain": "std", "role": "doc", "priority": "-1", "uri": "glossary.html", "dispname": "Glossary"}, "108": {"name": "hashing", "domain": "std", "role": "doc", "priority": "-1", "uri": "hashing.html", "dispname": "Hashing"}, "109": {"name": "helpers", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "Helpers"}, "110": {"name": "how", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "How Does It Work?"}, "111": {"name": "how-does-it-work", "domain": "std", "role": "doc", "priority": "-1", "uri": "how-does-it-work.html", "dispname": "How Does It Work?"}, "112": {"name": "how-frozen", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "Immutability"}, "113": {"name": "index", "domain": "std", "role": "doc", "priority": "-1", "uri": "index.html", "dispname": "attrs: Classes Without Boilerplate"}, "114": {"name": "init", "domain": "std", "role": "doc", "priority": "-1", "uri": "init.html", "dispname": "Initialization"}, "115": {"name": "license", "domain": "std", "role": "doc", "priority": "-1", "uri": "license.html", "dispname": "License and Credits"}, "116": {"name": "metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Metadata"}, "117": {"name": "modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Module Index"}, "118": {"name": "names", "domain": "std", "role": "doc", "priority": "-1", "uri": "names.html", "dispname": "On The Core API Names"}, "119": {"name": "overview", "domain": "std", "role": "doc", "priority": "-1", "uri": "overview.html", "dispname": "Overview"}, "120": {"name": "philosophy", "domain": "std", "role": "label", "priority": "-1", "uri": "overview.html#$", "dispname": "Philosophy"}, "121": {"name": "py-modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Python Module Index"}, "122": {"name": "search", "domain": "std", "role": "label", "priority": "-1", "uri": "search.html", "dispname": "Search Page"}, "123": {"name": "slotted classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-slotted-classes", "dispname": "-"}, "124": {"name": "transform-fields", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#$", "dispname": "Automatic Field Transformation and Modification"}, "125": {"name": "types", "domain": "std", "role": "doc", "priority": "-1", "uri": "types.html", "dispname": "Type Annotations"}, "126": {"name": "validators", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Validators"}, "127": {"name": "version-info", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "-"}, "128": {"name": "why", "domain": "std", "role": "doc", "priority": "-1", "uri": "why.html", "dispname": "Why not\u2026"}} \ No newline at end of file +{"project": "attrs", "version": "25.4", "count": 180, "0": {"name": "attr", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "1": {"name": "attr.Attribute", "domain": "py", "role": "class", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "2": {"name": "attr.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "3": {"name": "attr.VersionInfo", "domain": "py", "role": "class", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "4": {"name": "attr._make.Attribute", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Attribute", "dispname": "-"}, "5": {"name": "attr._make.ClassProps", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.ClassProps", "dispname": "-"}, "6": {"name": "attr._make.ClassProps.Hashability", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.ClassProps.Hashability", "dispname": "-"}, "7": {"name": "attr._make.ClassProps.KeywordOnly", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.ClassProps.KeywordOnly", "dispname": "-"}, "8": {"name": "attr._make.Converter", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Converter", "dispname": "-"}, "9": {"name": "attr._make.Factory", "domain": "py", "role": "class", "priority": "-1", "uri": "api.html#attrs.Factory", "dispname": "-"}, "10": {"name": "attr._version_info.VersionInfo", "domain": "py", "role": "class", "priority": "-1", "uri": "api-attr.html#attr.VersionInfo", "dispname": "-"}, "11": {"name": "attr.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "12": {"name": "attr.assoc", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "13": {"name": "attr.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "14": {"name": "attr.attrs", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "15": {"name": "attr.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "16": {"name": "attr.converters", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "17": {"name": "attr.define", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "18": {"name": "attr.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "19": {"name": "attr.exceptions", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "20": {"name": "attr.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.AttrsAttributeNotFoundError", "dispname": "-"}, "21": {"name": "attr.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.DefaultAlreadySetError", "dispname": "-"}, "22": {"name": "attr.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenAttributeError", "dispname": "-"}, "23": {"name": "attr.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenError", "dispname": "-"}, "24": {"name": "attr.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.FrozenInstanceError", "dispname": "-"}, "25": {"name": "attr.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotAnAttrsClassError", "dispname": "-"}, "26": {"name": "attr.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.NotCallableError", "dispname": "-"}, "27": {"name": "attr.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.PythonTooOldError", "dispname": "-"}, "28": {"name": "attr.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "-1", "uri": "api.html#attrs.exceptions.UnannotatedAttributeError", "dispname": "-"}, "29": {"name": "attr.field", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "30": {"name": "attr.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "31": {"name": "attr.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "32": {"name": "attr.filters", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "33": {"name": "attr.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "34": {"name": "attr.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "35": {"name": "attr.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "36": {"name": "attr.get_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "37": {"name": "attr.has", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "38": {"name": "attr.ib", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "39": {"name": "attr.make_class", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "40": {"name": "attr.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "41": {"name": "attr.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "42": {"name": "attr.s", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "43": {"name": "attr.set_run_validators", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "44": {"name": "attr.setters", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "45": {"name": "attr.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api-attr.html#$", "dispname": "-"}, "46": {"name": "attr.validators", "domain": "py", "role": "module", "priority": "0", "uri": "api-attr.html#module-$", "dispname": "-"}, "47": {"name": "attrs", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "48": {"name": "attrs.Attribute", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "49": {"name": "attrs.Attribute.evolve", "domain": "py", "role": "method", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "50": {"name": "attrs.ClassProps", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "51": {"name": "attrs.ClassProps.Hashability", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "52": {"name": "attrs.ClassProps.Hashability.HASHABLE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "53": {"name": "attrs.ClassProps.Hashability.HASHABLE_CACHED", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "54": {"name": "attrs.ClassProps.Hashability.LEAVE_ALONE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "55": {"name": "attrs.ClassProps.Hashability.UNHASHABLE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "56": {"name": "attrs.ClassProps.KeywordOnly", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "57": {"name": "attrs.ClassProps.KeywordOnly.FORCE", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "58": {"name": "attrs.ClassProps.KeywordOnly.NO", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "59": {"name": "attrs.ClassProps.KeywordOnly.YES", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "60": {"name": "attrs.ClassProps.added_eq", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "61": {"name": "attrs.ClassProps.added_init", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "62": {"name": "attrs.ClassProps.added_match_args", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "63": {"name": "attrs.ClassProps.added_ordering", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "64": {"name": "attrs.ClassProps.added_pickling", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "65": {"name": "attrs.ClassProps.added_repr", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "66": {"name": "attrs.ClassProps.added_str", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "67": {"name": "attrs.ClassProps.collected_fields_by_mro", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "68": {"name": "attrs.ClassProps.field_transformer", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "69": {"name": "attrs.ClassProps.has_weakref_slot", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "70": {"name": "attrs.ClassProps.hashability", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "71": {"name": "attrs.ClassProps.is_exception", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "72": {"name": "attrs.ClassProps.is_frozen", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "73": {"name": "attrs.ClassProps.is_slotted", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "74": {"name": "attrs.ClassProps.kw_only", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "75": {"name": "attrs.ClassProps.on_setattr_hook", "domain": "py", "role": "attribute", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "76": {"name": "attrs.Converter", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "77": {"name": "attrs.Factory", "domain": "py", "role": "class", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "78": {"name": "attrs.NOTHING", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "79": {"name": "attrs.asdict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "80": {"name": "attrs.astuple", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "81": {"name": "attrs.cmp_using", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "82": {"name": "attrs.converters", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "83": {"name": "attrs.converters.default_if_none", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "84": {"name": "attrs.converters.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "85": {"name": "attrs.converters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "86": {"name": "attrs.converters.to_bool", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "87": {"name": "attrs.define", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "88": {"name": "attrs.evolve", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "89": {"name": "attrs.exceptions", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "90": {"name": "attrs.exceptions.AttrsAttributeNotFoundError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "91": {"name": "attrs.exceptions.DefaultAlreadySetError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "92": {"name": "attrs.exceptions.FrozenAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "93": {"name": "attrs.exceptions.FrozenError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "94": {"name": "attrs.exceptions.FrozenInstanceError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "95": {"name": "attrs.exceptions.NotAnAttrsClassError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "96": {"name": "attrs.exceptions.NotCallableError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "97": {"name": "attrs.exceptions.PythonTooOldError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "98": {"name": "attrs.exceptions.UnannotatedAttributeError", "domain": "py", "role": "exception", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "99": {"name": "attrs.field", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "100": {"name": "attrs.fields", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "101": {"name": "attrs.fields_dict", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "102": {"name": "attrs.filters", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "103": {"name": "attrs.filters.exclude", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "104": {"name": "attrs.filters.include", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "105": {"name": "attrs.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "106": {"name": "attrs.has", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "107": {"name": "attrs.inspect", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "108": {"name": "attrs.make_class", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "109": {"name": "attrs.mutable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "110": {"name": "attrs.resolve_types", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "111": {"name": "attrs.setters", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "112": {"name": "attrs.setters.NO_OP", "domain": "py", "role": "data", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "113": {"name": "attrs.setters.convert", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "114": {"name": "attrs.setters.frozen", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "115": {"name": "attrs.setters.pipe", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "116": {"name": "attrs.setters.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "117": {"name": "attrs.validate", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "118": {"name": "attrs.validators", "domain": "py", "role": "module", "priority": "0", "uri": "api.html#module-$", "dispname": "-"}, "119": {"name": "attrs.validators.and_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "120": {"name": "attrs.validators.deep_iterable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "121": {"name": "attrs.validators.deep_mapping", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "122": {"name": "attrs.validators.disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "123": {"name": "attrs.validators.ge", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "124": {"name": "attrs.validators.get_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "125": {"name": "attrs.validators.gt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "126": {"name": "attrs.validators.in_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "127": {"name": "attrs.validators.instance_of", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "128": {"name": "attrs.validators.is_callable", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "129": {"name": "attrs.validators.le", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "130": {"name": "attrs.validators.lt", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "131": {"name": "attrs.validators.matches_re", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "132": {"name": "attrs.validators.max_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "133": {"name": "attrs.validators.min_len", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "134": {"name": "attrs.validators.not_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "135": {"name": "attrs.validators.optional", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "136": {"name": "attrs.validators.or_", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "137": {"name": "attrs.validators.set_disabled", "domain": "py", "role": "function", "priority": "1", "uri": "api.html#$", "dispname": "-"}, "138": {"name": "api", "domain": "std", "role": "doc", "priority": "-1", "uri": "api.html", "dispname": "API Reference"}, "139": {"name": "api-attr", "domain": "std", "role": "doc", "priority": "-1", "uri": "api-attr.html", "dispname": "API Reference for the attr Namespace"}, "140": {"name": "api-validators", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "Validators"}, "141": {"name": "api_setters", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#api-setters", "dispname": "Setters"}, "142": {"name": "asdict", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Converting to Collections Types"}, "143": {"name": "attribute", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-$", "dispname": "-"}, "144": {"name": "changelog", "domain": "std", "role": "doc", "priority": "-1", "uri": "changelog.html", "dispname": "Changelog"}, "145": {"name": "comparison", "domain": "std", "role": "doc", "priority": "-1", "uri": "comparison.html", "dispname": "Comparison"}, "146": {"name": "converters", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Converters"}, "147": {"name": "custom-comparison", "domain": "std", "role": "label", "priority": "-1", "uri": "comparison.html#$", "dispname": "Customization"}, "148": {"name": "defaults", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Defaults"}, "149": {"name": "dict classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dict-classes", "dispname": "-"}, "150": {"name": "dunder methods", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-dunder-methods", "dispname": "-"}, "151": {"name": "examples", "domain": "std", "role": "doc", "priority": "-1", "uri": "examples.html", "dispname": "attrs by Example"}, "152": {"name": "examples-validators", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Validators"}, "153": {"name": "extending", "domain": "std", "role": "doc", "priority": "-1", "uri": "extending.html", "dispname": "Extending"}, "154": {"name": "extending-metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#$", "dispname": "Metadata"}, "155": {"name": "field", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-$", "dispname": "-"}, "156": {"name": "genindex", "domain": "std", "role": "label", "priority": "-1", "uri": "genindex.html", "dispname": "Index"}, "157": {"name": "glossary", "domain": "std", "role": "doc", "priority": "-1", "uri": "glossary.html", "dispname": "Glossary"}, "158": {"name": "hashing", "domain": "std", "role": "doc", "priority": "-1", "uri": "hashing.html", "dispname": "Hashing"}, "159": {"name": "helpers", "domain": "std", "role": "label", "priority": "-1", "uri": "api.html#$", "dispname": "Helpers"}, "160": {"name": "how", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "How Does It Work?"}, "161": {"name": "how-does-it-work", "domain": "std", "role": "doc", "priority": "-1", "uri": "how-does-it-work.html", "dispname": "How Does It Work?"}, "162": {"name": "how-frozen", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#$", "dispname": "Immutability"}, "163": {"name": "how-slotted-cached_property", "domain": "std", "role": "label", "priority": "-1", "uri": "how-does-it-work.html#how-slotted-cached-property", "dispname": "Cached Properties on Slotted Classes"}, "164": {"name": "index", "domain": "std", "role": "doc", "priority": "-1", "uri": "index.html", "dispname": "attrs: Classes Without Boilerplate"}, "165": {"name": "init", "domain": "std", "role": "doc", "priority": "-1", "uri": "init.html", "dispname": "Initialization"}, "166": {"name": "init-subclass", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "attrs and __init_subclass__"}, "167": {"name": "license", "domain": "std", "role": "doc", "priority": "-1", "uri": "license.html", "dispname": "License and Credits"}, "168": {"name": "metadata", "domain": "std", "role": "label", "priority": "-1", "uri": "examples.html#$", "dispname": "Metadata"}, "169": {"name": "modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Module Index"}, "170": {"name": "names", "domain": "std", "role": "doc", "priority": "-1", "uri": "names.html", "dispname": "On The Core API Names"}, "171": {"name": "overview", "domain": "std", "role": "doc", "priority": "-1", "uri": "overview.html", "dispname": "Overview"}, "172": {"name": "private-attributes", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Private Attributes and Aliases"}, "173": {"name": "py-modindex", "domain": "std", "role": "label", "priority": "-1", "uri": "py-modindex.html", "dispname": "Python Module Index"}, "174": {"name": "search", "domain": "std", "role": "label", "priority": "-1", "uri": "search.html", "dispname": "Search Page"}, "175": {"name": "slotted classes", "domain": "std", "role": "term", "priority": "-1", "uri": "glossary.html#term-slotted-classes", "dispname": "-"}, "176": {"name": "transform-fields", "domain": "std", "role": "label", "priority": "-1", "uri": "extending.html#$", "dispname": "Automatic Field Transformation and Modification"}, "177": {"name": "types", "domain": "std", "role": "doc", "priority": "-1", "uri": "types.html", "dispname": "Type Annotations"}, "178": {"name": "validators", "domain": "std", "role": "label", "priority": "-1", "uri": "init.html#$", "dispname": "Validators"}, "179": {"name": "why", "domain": "std", "role": "doc", "priority": "-1", "uri": "why.html", "dispname": "Why not\u2026"}} \ No newline at end of file diff --git a/tests/resource/objects_attrs.txt b/tests/resource/objects_attrs.txt index 50cf9464..a81d2965 100644 --- a/tests/resource/objects_attrs.txt +++ b/tests/resource/objects_attrs.txt @@ -1,29 +1,27 @@ # Sphinx inventory version 2 # Project: attrs -# Version: 22.1 +# Version: 25.4 # The remainder of this file is compressed using zlib. -attr py:module 0 index.html#module-$ - -attr.VersionInfo py:class 1 api.html#$ - +attr py:module 0 api-attr.html#module-$ - +attr.Attribute py:class 1 api-attr.html#$ - +attr.NOTHING py:data 1 api-attr.html#$ - +attr.VersionInfo py:class 1 api-attr.html#$ - attr._make.Attribute py:class -1 api.html#attrs.Attribute - +attr._make.ClassProps py:class -1 api.html#attrs.ClassProps - +attr._make.ClassProps.Hashability py:class -1 api.html#attrs.ClassProps.Hashability - +attr._make.ClassProps.KeywordOnly py:class -1 api.html#attrs.ClassProps.KeywordOnly - +attr._make.Converter py:class -1 api.html#attrs.Converter - attr._make.Factory py:class -1 api.html#attrs.Factory - -attr._version_info.VersionInfo py:class -1 api.html#attr.VersionInfo - -attr.asdict py:function 1 api.html#$ - -attr.assoc py:function 1 api.html#$ - -attr.astuple py:function 1 api.html#$ - -attr.attr.NOTHING py:data 1 api.html#$ - -attr.attr.cmp_using py:function 1 api.html#$ - -attr.attr.evolve py:function 1 api.html#$ - -attr.attr.fields py:function 1 api.html#$ - -attr.attr.fields_dict py:function 1 api.html#$ - -attr.attr.filters.exclude py:function 1 api.html#$ - -attr.attr.filters.include py:function 1 api.html#$ - -attr.attr.has py:function 1 api.html#$ - -attr.attr.resolve_types py:function 1 api.html#$ - -attr.attr.validate py:function 1 api.html#$ - -attr.attrs.frozen py:function 1 api.html#$ - -attr.attrs.mutable py:function 1 api.html#$ - -attr.attrs.setters.NO_OP py:data 1 api.html#$ - -attr.define py:function 1 api.html#$ - +attr._version_info.VersionInfo py:class -1 api-attr.html#attr.VersionInfo - +attr.asdict py:function 1 api-attr.html#$ - +attr.assoc py:function 1 api-attr.html#$ - +attr.astuple py:function 1 api-attr.html#$ - +attr.attrs py:function 1 api-attr.html#$ - +attr.cmp_using py:function 1 api-attr.html#$ - +attr.converters py:module 0 api-attr.html#module-$ - +attr.define py:function 1 api-attr.html#$ - +attr.evolve py:function 1 api-attr.html#$ - +attr.exceptions py:module 0 api-attr.html#module-$ - attr.exceptions.AttrsAttributeNotFoundError py:exception -1 api.html#attrs.exceptions.AttrsAttributeNotFoundError - attr.exceptions.DefaultAlreadySetError py:exception -1 api.html#attrs.exceptions.DefaultAlreadySetError - attr.exceptions.FrozenAttributeError py:exception -1 api.html#attrs.exceptions.FrozenAttributeError - @@ -33,27 +31,67 @@ attr.exceptions.NotAnAttrsClassError py:exception -1 api.html#attrs.exceptions.N attr.exceptions.NotCallableError py:exception -1 api.html#attrs.exceptions.NotCallableError - attr.exceptions.PythonTooOldError py:exception -1 api.html#attrs.exceptions.PythonTooOldError - attr.exceptions.UnannotatedAttributeError py:exception -1 api.html#attrs.exceptions.UnannotatedAttributeError - -attr.field py:function 1 api.html#$ - -attr.frozen py:function 1 api.html#$ - -attr.get_run_validators py:function 1 api.html#$ - -attr.ib py:function 1 api.html#$ - -attr.mutable py:function 1 api.html#$ - -attr.s py:function 1 api.html#$ - -attr.set_run_validators py:function 1 api.html#$ - -attrs py:module 0 index.html#module-$ - +attr.field py:function 1 api-attr.html#$ - +attr.fields py:function 1 api-attr.html#$ - +attr.fields_dict py:function 1 api-attr.html#$ - +attr.filters py:module 0 api-attr.html#module-$ - +attr.filters.exclude py:function 1 api-attr.html#$ - +attr.filters.include py:function 1 api-attr.html#$ - +attr.frozen py:function 1 api-attr.html#$ - +attr.get_run_validators py:function 1 api-attr.html#$ - +attr.has py:function 1 api-attr.html#$ - +attr.ib py:function 1 api-attr.html#$ - +attr.make_class py:function 1 api-attr.html#$ - +attr.mutable py:function 1 api-attr.html#$ - +attr.resolve_types py:function 1 api-attr.html#$ - +attr.s py:function 1 api-attr.html#$ - +attr.set_run_validators py:function 1 api-attr.html#$ - +attr.setters py:module 0 api-attr.html#module-$ - +attr.validate py:function 1 api-attr.html#$ - +attr.validators py:module 0 api-attr.html#module-$ - +attrs py:module 0 api.html#module-$ - attrs.Attribute py:class 1 api.html#$ - attrs.Attribute.evolve py:method 1 api.html#$ - +attrs.ClassProps py:class 1 api.html#$ - +attrs.ClassProps.Hashability py:class 1 api.html#$ - +attrs.ClassProps.Hashability.HASHABLE py:attribute 1 api.html#$ - +attrs.ClassProps.Hashability.HASHABLE_CACHED py:attribute 1 api.html#$ - +attrs.ClassProps.Hashability.LEAVE_ALONE py:attribute 1 api.html#$ - +attrs.ClassProps.Hashability.UNHASHABLE py:attribute 1 api.html#$ - +attrs.ClassProps.KeywordOnly py:class 1 api.html#$ - +attrs.ClassProps.KeywordOnly.FORCE py:attribute 1 api.html#$ - +attrs.ClassProps.KeywordOnly.NO py:attribute 1 api.html#$ - +attrs.ClassProps.KeywordOnly.YES py:attribute 1 api.html#$ - +attrs.ClassProps.added_eq py:attribute 1 api.html#$ - +attrs.ClassProps.added_init py:attribute 1 api.html#$ - +attrs.ClassProps.added_match_args py:attribute 1 api.html#$ - +attrs.ClassProps.added_ordering py:attribute 1 api.html#$ - +attrs.ClassProps.added_pickling py:attribute 1 api.html#$ - +attrs.ClassProps.added_repr py:attribute 1 api.html#$ - +attrs.ClassProps.added_str py:attribute 1 api.html#$ - +attrs.ClassProps.collected_fields_by_mro py:attribute 1 api.html#$ - +attrs.ClassProps.field_transformer py:attribute 1 api.html#$ - +attrs.ClassProps.has_weakref_slot py:attribute 1 api.html#$ - +attrs.ClassProps.hashability py:attribute 1 api.html#$ - +attrs.ClassProps.is_exception py:attribute 1 api.html#$ - +attrs.ClassProps.is_frozen py:attribute 1 api.html#$ - +attrs.ClassProps.is_slotted py:attribute 1 api.html#$ - +attrs.ClassProps.kw_only py:attribute 1 api.html#$ - +attrs.ClassProps.on_setattr_hook py:attribute 1 api.html#$ - +attrs.Converter py:class 1 api.html#$ - attrs.Factory py:class 1 api.html#$ - attrs.NOTHING py:data 1 api.html#$ - attrs.asdict py:function 1 api.html#$ - attrs.astuple py:function 1 api.html#$ - attrs.cmp_using py:function 1 api.html#$ - +attrs.converters py:module 0 api.html#module-$ - attrs.converters.default_if_none py:function 1 api.html#$ - attrs.converters.optional py:function 1 api.html#$ - attrs.converters.pipe py:function 1 api.html#$ - attrs.converters.to_bool py:function 1 api.html#$ - attrs.define py:function 1 api.html#$ - attrs.evolve py:function 1 api.html#$ - +attrs.exceptions py:module 0 api.html#module-$ - attrs.exceptions.AttrsAttributeNotFoundError py:exception 1 api.html#$ - attrs.exceptions.DefaultAlreadySetError py:exception 1 api.html#$ - attrs.exceptions.FrozenAttributeError py:exception 1 api.html#$ - @@ -66,16 +104,23 @@ attrs.exceptions.UnannotatedAttributeError py:exception 1 api.html#$ - attrs.field py:function 1 api.html#$ - attrs.fields py:function 1 api.html#$ - attrs.fields_dict py:function 1 api.html#$ - +attrs.filters py:module 0 api.html#module-$ - attrs.filters.exclude py:function 1 api.html#$ - attrs.filters.include py:function 1 api.html#$ - +attrs.frozen py:function 1 api.html#$ - attrs.has py:function 1 api.html#$ - +attrs.inspect py:function 1 api.html#$ - attrs.make_class py:function 1 api.html#$ - +attrs.mutable py:function 1 api.html#$ - attrs.resolve_types py:function 1 api.html#$ - +attrs.setters py:module 0 api.html#module-$ - +attrs.setters.NO_OP py:data 1 api.html#$ - attrs.setters.convert py:function 1 api.html#$ - attrs.setters.frozen py:function 1 api.html#$ - attrs.setters.pipe py:function 1 api.html#$ - attrs.setters.validate py:function 1 api.html#$ - attrs.validate py:function 1 api.html#$ - +attrs.validators py:module 0 api.html#module-$ - attrs.validators.and_ py:function 1 api.html#$ - attrs.validators.deep_iterable py:function 1 api.html#$ - attrs.validators.deep_mapping py:function 1 api.html#$ - @@ -91,23 +136,28 @@ attrs.validators.lt py:function 1 api.html#$ - attrs.validators.matches_re py:function 1 api.html#$ - attrs.validators.max_len py:function 1 api.html#$ - attrs.validators.min_len py:function 1 api.html#$ - +attrs.validators.not_ py:function 1 api.html#$ - attrs.validators.optional py:function 1 api.html#$ - -attrs.validators.provides py:function 1 api.html#$ - +attrs.validators.or_ py:function 1 api.html#$ - attrs.validators.set_disabled py:function 1 api.html#$ - api std:doc -1 api.html API Reference +api-attr std:doc -1 api-attr.html API Reference for the attr Namespace +api-validators std:label -1 api.html#$ Validators api_setters std:label -1 api.html#api-setters Setters -api_validators std:label -1 api.html#api-validators Validators asdict std:label -1 examples.html#$ Converting to Collections Types +attribute std:term -1 glossary.html#term-$ - changelog std:doc -1 changelog.html Changelog comparison std:doc -1 comparison.html Comparison converters std:label -1 init.html#$ Converters custom-comparison std:label -1 comparison.html#$ Customization +defaults std:label -1 init.html#$ Defaults dict classes std:term -1 glossary.html#term-dict-classes - dunder methods std:term -1 glossary.html#term-dunder-methods - examples std:doc -1 examples.html attrs by Example -examples_validators std:label -1 examples.html#examples-validators Validators +examples-validators std:label -1 examples.html#$ Validators extending std:doc -1 extending.html Extending -extending_metadata std:label -1 extending.html#extending-metadata Metadata +extending-metadata std:label -1 extending.html#$ Metadata +field std:term -1 glossary.html#term-$ - genindex std:label -1 genindex.html Index glossary std:doc -1 glossary.html Glossary hashing std:doc -1 hashing.html Hashing @@ -115,19 +165,20 @@ helpers std:label -1 api.html#$ Helpers how std:label -1 how-does-it-work.html#$ How Does It Work? how-does-it-work std:doc -1 how-does-it-work.html How Does It Work? how-frozen std:label -1 how-does-it-work.html#$ Immutability +how-slotted-cached_property std:label -1 how-does-it-work.html#how-slotted-cached-property Cached Properties on Slotted Classes index std:doc -1 index.html attrs: Classes Without Boilerplate init std:doc -1 init.html Initialization +init-subclass std:label -1 init.html#$ attrs and __init_subclass__ license std:doc -1 license.html License and Credits metadata std:label -1 examples.html#$ Metadata modindex std:label -1 py-modindex.html Module Index names std:doc -1 names.html On The Core API Names overview std:doc -1 overview.html Overview -philosophy std:label -1 overview.html#$ Philosophy +private-attributes std:label -1 init.html#$ Private Attributes and Aliases py-modindex std:label -1 py-modindex.html Python Module Index search std:label -1 search.html Search Page slotted classes std:term -1 glossary.html#term-slotted-classes - transform-fields std:label -1 extending.html#$ Automatic Field Transformation and Modification types std:doc -1 types.html Type Annotations validators std:label -1 init.html#$ Validators -version-info std:label -1 api.html#$ - why std:doc -1 why.html Why not… From 77613b239e8b8c02b988e84adbf5e2063200911d Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 21:03:54 -0400 Subject: [PATCH 092/122] Update test checks to match new attrs inventory --- tests/conftest.py | 4 ++-- tests/test_api_good.py | 4 ++-- tests/test_cli.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 19db6815..7402f2fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -310,8 +310,8 @@ def func(inv, source_type): """ assert inv.project == "attrs" - assert inv.version == "22.1" - assert inv.count == 129 + assert inv.version == "25.4" + assert inv.count == 180 assert inv.source_type return func diff --git a/tests/test_api_good.py b/tests/test_api_good.py index 8193306b..9e9d57c7 100644 --- a/tests/test_api_good.py +++ b/tests/test_api_good.py @@ -452,8 +452,8 @@ def test_api_inventory_toosmallflatdict_importbutignore(self, res_dec): def test_api_inventory_namesuggest(self, res_cmp, check): """Confirm object name suggestion is nominally working on a specific object.""" - rst = ":py:function:`attr.attr.evolve`" - idx = 10 + rst = ":py:function:`attr.evolve`" + idx = 18 inv = soi.Inventory(str(res_cmp)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8dd367e7..adffa6b2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -355,7 +355,7 @@ def test_cli_suggest_withindex(self, run_cmdline_test, res_cmp): """Confirm with_index suggest works.""" with stdio_mgr() as (in_, out_, err_): run_cmdline_test(["suggest", res_cmp, "instance", "-it", "50"]) - assert re.search("^.*instance_of\\S*\\s+82\\s*$", out_.getvalue(), re.M) + assert re.search("^.*instance_of\\S*\\s+127\\s*$", out_.getvalue(), re.M) @pytest.mark.timeout(CLI_TEST_TIMEOUT) def test_cli_suggest_withscore(self, run_cmdline_test, res_cmp): @@ -373,7 +373,7 @@ def test_cli_suggest_withscoreandindex(self, run_cmdline_test, res_cmp): @pytest.mark.parametrize( ["inp", "flags", "nlines"], - [("", "-at", 129), ("y\n", "-t", 130), ("n\n", "-t", 1)], + [("", "-at", 180), ("y\n", "-t", 181), ("n\n", "-t", 1)], ) # Extra line for input() query in the "y\n" case @pytest.mark.timeout(CLI_TEST_TIMEOUT) def test_cli_suggest_long_list(self, inp, flags, nlines, run_cmdline_test, res_cmp): From 7a763584178f7f84c8af2cc3a6a864e81c81a244 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 21:10:52 -0400 Subject: [PATCH 093/122] Update README doctest examples --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 14bf93f4..5ae5a014 100644 --- a/README.md +++ b/README.md @@ -152,13 +152,13 @@ inventory creation/modification: >>> import sphobjinv as soi >>> inv = soi.Inventory('doc/build/html/objects.inv') >>> print(inv) - + >>> inv.project 'sphobjinv' >>> inv.version -'2.3' +'2.4' >>> inv.objects[0] -DataObjStr(name='sphobjinv.cli.convert', domain='py', role='module', priority='0', uri='cli/implementation/convert.html#module-$', dispname='-') +DataObjStr(name='sphobjinv.data', domain='py', role='module', priority='0', uri='api/data.html#module-$', dispname='-') ``` From 573c2555e8051efb4f2a49bc9151d826f608ae83 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 21:38:17 -0400 Subject: [PATCH 094/122] Update Pepy badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ae5a014..b5313ea7 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ under a [Creative Commons Attribution 4.0 International License][cc-by 4.0] [mit license]: https://opensource.org/licenses/MIT [numpy linspace]: https://numpy.org/doc/1.26/reference/generated/numpy.linspace.html [pepy badge]: https://pepy.tech/badge/sphobjinv/month -[pepy link target]: https://pepy.tech/projects/sphobjinv?timeRange=threeMonths&category=version&includeCIDownloads=true&granularity=daily&viewType=chart&versions=2.0.*%2C2.1.*%2C2.2.*%2C2.3.* +[pepy link target]: https://pepy.tech/projects/sphobjinv?timeRange=threeMonths&category=version&includeCIDownloads=true&granularity=daily&versions=2.3.*%2C2.4* [pypi badge]: https://img.shields.io/pypi/v/sphobjinv.svg?logo=pypi] [pypi link target]: https://pypi.org/project/sphobjinv [python versions badge]: https://img.shields.io/pypi/pyversions/sphobjinv.svg?logo=python From 1297e6c2249b00b92e992d4efbc8ca640d4b1471 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 21:38:44 -0400 Subject: [PATCH 095/122] Reset and refactor version override in setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f172d296..00615320 100644 --- a/setup.py +++ b/setup.py @@ -9,13 +9,13 @@ exec(Path("src", "sphobjinv", "version.py").read_text(encoding="utf-8"), exec_ns) __version__ = exec_ns["__version__"] -version_override = "2.3.1.2" +version_override = None def readme(): content = Path("README.md").read_text(encoding="utf-8") - new_ver = version_override if version_override else __version__ + new_ver = version_override or __version__ # Helper function def content_update(content, pattern, sub): From 7ae75a1cbd585f3cf7e72202afc1a23ecad92360 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 15 Mar 2026 21:39:07 -0400 Subject: [PATCH 096/122] Update copyright statements and year ranges --- LICENSE.txt | 2 +- README.md | 2 +- doc/source/conf.py | 2 +- src/sphobjinv/__init__.py | 2 +- src/sphobjinv/__main__.py | 2 +- src/sphobjinv/_vendored/__init__.py | 2 +- src/sphobjinv/_vendored/fuzzywuzzy/__init__.py | 2 +- src/sphobjinv/cli/__init__.py | 2 +- src/sphobjinv/cli/convert.py | 2 +- src/sphobjinv/cli/core.py | 2 +- src/sphobjinv/cli/load.py | 2 +- src/sphobjinv/cli/parser.py | 4 ++-- src/sphobjinv/cli/paths.py | 2 +- src/sphobjinv/cli/suggest.py | 2 +- src/sphobjinv/cli/ui.py | 2 +- src/sphobjinv/cli/write.py | 2 +- src/sphobjinv/data.py | 2 +- src/sphobjinv/enum.py | 2 +- src/sphobjinv/error.py | 2 +- src/sphobjinv/fileops.py | 2 +- src/sphobjinv/inventory.py | 2 +- src/sphobjinv/re.py | 2 +- src/sphobjinv/schema.py | 2 +- src/sphobjinv/version.py | 4 ++-- src/sphobjinv/zlib.py | 2 +- 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 | 2 +- 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 +- 38 files changed, 40 insertions(+), 40 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index e892de6b..8afcbd75 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016-2025 Brian Skinn and community contributors +Copyright (c) 2016-2026 Brian Skinn and community contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index b5313ea7..6a3e8858 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Available on [PyPI][pypi link target] (`pip install sphobjinv`). Source on [GitHub][github repo]. Bug reports and feature requests are welcomed at the [Issues][github issue tracker] page there. -Copyright (c) Brian Skinn 2016-2025 +Copyright (c) 2016-2026 Brian Skinn and community contributors The `sphobjinv` documentation (including docstrings and README) is licensed under a [Creative Commons Attribution 4.0 International License][cc-by 4.0] diff --git a/doc/source/conf.py b/doc/source/conf.py index 5abed10c..c9a29321 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -21,7 +21,7 @@ # -- Project information ----------------------------------------------------- project = "sphobjinv" -copyright = "2016-2025, Brian Skinn" +copyright = "2016-2026, Brian Skinn and community contributors" author = "Brian Skinn" # The full version for `release`, including alpha/beta/rc tags diff --git a/src/sphobjinv/__init__.py b/src/sphobjinv/__init__.py index 0e141f96..e46dd28a 100644 --- a/src/sphobjinv/__init__.py +++ b/src/sphobjinv/__init__.py @@ -10,7 +10,7 @@ 17 May 2016 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/__main__.py b/src/sphobjinv/__main__.py index 6e307758..d063bb24 100644 --- a/src/sphobjinv/__main__.py +++ b/src/sphobjinv/__main__.py @@ -10,7 +10,7 @@ 15 May 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/_vendored/__init__.py b/src/sphobjinv/_vendored/__init__.py index 36ba0197..b5f7e176 100644 --- a/src/sphobjinv/_vendored/__init__.py +++ b/src/sphobjinv/_vendored/__init__.py @@ -12,7 +12,7 @@ 11 Dec 2021 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py b/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py index ff3129d3..f88ee546 100644 --- a/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py +++ b/src/sphobjinv/_vendored/fuzzywuzzy/__init__.py @@ -25,7 +25,7 @@ 11 Dec 2021 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/__init__.py b/src/sphobjinv/cli/__init__.py index 357e6e8c..d7b267b8 100644 --- a/src/sphobjinv/cli/__init__.py +++ b/src/sphobjinv/cli/__init__.py @@ -10,7 +10,7 @@ 15 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/convert.py b/src/sphobjinv/cli/convert.py index 1a342d63..829f1c79 100644 --- a/src/sphobjinv/cli/convert.py +++ b/src/sphobjinv/cli/convert.py @@ -10,7 +10,7 @@ 20 Oct 2022 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py index 9130f992..3fbc6d1c 100644 --- a/src/sphobjinv/cli/core.py +++ b/src/sphobjinv/cli/core.py @@ -10,7 +10,7 @@ 15 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/load.py b/src/sphobjinv/cli/load.py index 32405758..07032c95 100644 --- a/src/sphobjinv/cli/load.py +++ b/src/sphobjinv/cli/load.py @@ -10,7 +10,7 @@ 17 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py index 19727e18..224bd59d 100644 --- a/src/sphobjinv/cli/parser.py +++ b/src/sphobjinv/cli/parser.py @@ -10,7 +10,7 @@ 15 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv @@ -44,7 +44,7 @@ class PrsConst: #: Version &c. output blurb VER_TXT = ( - f"\nsphobjinv v{__version__}\n\nCopyright (c) Brian Skinn 2016-2025\n" + f"\nsphobjinv v{__version__}\n\nCopyright (c) 2016-2026 Brian Skinn and community contributors\n" "License: The MIT License\n\n" "Bug reports & feature requests:" " https://github.com/bskinn/sphobjinv\n" diff --git a/src/sphobjinv/cli/paths.py b/src/sphobjinv/cli/paths.py index de9bfc23..1ce9581e 100644 --- a/src/sphobjinv/cli/paths.py +++ b/src/sphobjinv/cli/paths.py @@ -10,7 +10,7 @@ 19 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/suggest.py b/src/sphobjinv/cli/suggest.py index 1fb7dd56..865dd4a6 100644 --- a/src/sphobjinv/cli/suggest.py +++ b/src/sphobjinv/cli/suggest.py @@ -10,7 +10,7 @@ 20 Oct 2022 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/ui.py b/src/sphobjinv/cli/ui.py index 2ab2290b..a886880b 100644 --- a/src/sphobjinv/cli/ui.py +++ b/src/sphobjinv/cli/ui.py @@ -10,7 +10,7 @@ 19 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/cli/write.py b/src/sphobjinv/cli/write.py index f7f0816e..b9a9930d 100644 --- a/src/sphobjinv/cli/write.py +++ b/src/sphobjinv/cli/write.py @@ -10,7 +10,7 @@ 19 Nov 2020 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/data.py b/src/sphobjinv/data.py index 10a6c7d7..76cade55 100644 --- a/src/sphobjinv/data.py +++ b/src/sphobjinv/data.py @@ -10,7 +10,7 @@ 7 Nov 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/enum.py b/src/sphobjinv/enum.py index d046dc95..3fa4fcd0 100644 --- a/src/sphobjinv/enum.py +++ b/src/sphobjinv/enum.py @@ -10,7 +10,7 @@ 4 May 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/error.py b/src/sphobjinv/error.py index 37422303..ba07b200 100644 --- a/src/sphobjinv/error.py +++ b/src/sphobjinv/error.py @@ -10,7 +10,7 @@ 5 Nov 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/fileops.py b/src/sphobjinv/fileops.py index feba9dad..e30c4b42 100644 --- a/src/sphobjinv/fileops.py +++ b/src/sphobjinv/fileops.py @@ -10,7 +10,7 @@ 5 Nov 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/inventory.py b/src/sphobjinv/inventory.py index 33c685e5..d56c7124 100644 --- a/src/sphobjinv/inventory.py +++ b/src/sphobjinv/inventory.py @@ -10,7 +10,7 @@ 7 Dec 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/re.py b/src/sphobjinv/re.py index 017a181a..e42ab29d 100644 --- a/src/sphobjinv/re.py +++ b/src/sphobjinv/re.py @@ -10,7 +10,7 @@ 5 Nov 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/schema.py b/src/sphobjinv/schema.py index b609b042..2cc54d2b 100644 --- a/src/sphobjinv/schema.py +++ b/src/sphobjinv/schema.py @@ -11,7 +11,7 @@ 7 Dec 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/src/sphobjinv/version.py b/src/sphobjinv/version.py index ac087af2..00369de0 100644 --- a/src/sphobjinv/version.py +++ b/src/sphobjinv/version.py @@ -10,7 +10,7 @@ 18 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv @@ -29,4 +29,4 @@ """ -__version__ = "2.4.dev0" +__version__ = "2.4" diff --git a/src/sphobjinv/zlib.py b/src/sphobjinv/zlib.py index 649d54c6..34dfe6af 100644 --- a/src/sphobjinv/zlib.py +++ b/src/sphobjinv/zlib.py @@ -10,7 +10,7 @@ 5 Nov 2017 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/conftest.py b/tests/conftest.py index 7402f2fb..937fa434 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ 20 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/enum.py b/tests/enum.py index 685d2406..0c1b9757 100644 --- a/tests/enum.py +++ b/tests/enum.py @@ -10,7 +10,7 @@ 22 Dec 2025 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py index 27abe554..694d5234 100644 --- a/tests/fixtures_http.py +++ b/tests/fixtures_http.py @@ -10,7 +10,7 @@ 24 Dec 2025 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_api_fail.py b/tests/test_api_fail.py index d77088f4..786ca49b 100644 --- a/tests/test_api_fail.py +++ b/tests/test_api_fail.py @@ -10,7 +10,7 @@ 20 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_api_good.py b/tests/test_api_good.py index 9e9d57c7..7ce59187 100644 --- a/tests/test_api_good.py +++ b/tests/test_api_good.py @@ -10,7 +10,7 @@ 20 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_api_good_nonlocal.py b/tests/test_api_good_nonlocal.py index eec40738..5d267f93 100644 --- a/tests/test_api_good_nonlocal.py +++ b/tests/test_api_good_nonlocal.py @@ -10,7 +10,7 @@ 21 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_cli.py b/tests/test_cli.py index adffa6b2..06ade7ed 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -10,7 +10,7 @@ 20 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_cli_nonlocal.py b/tests/test_cli_nonlocal.py index f074e86b..064733ff 100644 --- a/tests/test_cli_nonlocal.py +++ b/tests/test_cli_nonlocal.py @@ -10,7 +10,7 @@ 20 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_cli_textconv.py b/tests/test_cli_textconv.py index 3ad39cc8..f08356f5 100644 --- a/tests/test_cli_textconv.py +++ b/tests/test_cli_textconv.py @@ -10,7 +10,7 @@ 22 Dec 2025 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_fixture.py b/tests/test_fixture.py index a48cb98d..0760ddff 100644 --- a/tests/test_fixture.py +++ b/tests/test_fixture.py @@ -10,7 +10,7 @@ 20 Mar 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_flake8_ext.py b/tests/test_flake8_ext.py index 8604d12c..ddc5933e 100644 --- a/tests/test_flake8_ext.py +++ b/tests/test_flake8_ext.py @@ -10,7 +10,7 @@ 27 Apr 2019 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_intersphinx.py b/tests/test_intersphinx.py index 4096c0c4..7d72739f 100644 --- a/tests/test_intersphinx.py +++ b/tests/test_intersphinx.py @@ -10,7 +10,7 @@ 21 Jun 2022 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv diff --git a/tests/test_valid_objects.py b/tests/test_valid_objects.py index 6367dde8..b934394b 100644 --- a/tests/test_valid_objects.py +++ b/tests/test_valid_objects.py @@ -10,7 +10,7 @@ 13 Feb 2021 **Copyright** - \(c) Brian Skinn 2016-2025 + \(c) 2016-2026 Brian Skinn and community contributors **Source Repository** https://github.com/bskinn/sphobjinv From f9582a94cc0b48f4297d15efb699b307cb08e08c Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Thu, 19 Mar 2026 22:26:49 -0400 Subject: [PATCH 097/122] Modernize package license metadata --- pyproject.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2f37d8ef..1ed6313f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,10 @@ requires = [ [project] name = "sphobjinv" description = "Sphinx objects.inv Inspection/Manipulation Tool" -license = {text = "MIT License"} +license = "MIT" +license-files = ["LICENSE.txt"] authors = [{name = "Brian Skinn", email = "brian.skinn@gmail.com"}] classifiers = [ - "License :: OSI Approved", - "License :: OSI Approved :: MIT License", "Natural Language :: English", "Environment :: Console", "Framework :: Sphinx", @@ -57,7 +56,6 @@ sphobjinv-textconv = "sphobjinv.cli.core:main_textconv" [tool.setuptools] package-dir = {"" = "src"} platforms = ["any"] -license-files = ["LICENSE.txt"] include-package-data = false [tool.setuptools.packages.find] From 454328ae31a4642ed1544fc2143dd774fcb4f505 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Thu, 19 Mar 2026 23:04:56 -0400 Subject: [PATCH 098/122] Substitute in 2.4 for ##VER## tags --- doc/source/cli/index.rst | 2 +- doc/source/cli/textconv.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 2d26cc07..952f11d7 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -12,7 +12,7 @@ The primary CLI for |soi| is implemented using two subcommands of the - ``sphobjinv suggest`` (:doc:`docs page `), which provides suggestions for objects in an inventory matching a desired search term. -As of v##VER##, |soi| also provides an auxiliary entrypoint, +As of v2.4, |soi| also provides an auxiliary entrypoint, ``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 diff --git a/doc/source/cli/textconv.rst b/doc/source/cli/textconv.rst index fd13a371..2baa429d 100644 --- a/doc/source/cli/textconv.rst +++ b/doc/source/cli/textconv.rst @@ -82,4 +82,4 @@ will render correctly in ReadTheDocs builds): Display brief package version information and exit. -.. versionadded:: ##VER## +.. versionadded:: 2.4 From cdf1ee43ccf2e27041069c6d450fa5e355cbee8f Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Thu, 19 Mar 2026 23:10:31 -0400 Subject: [PATCH 099/122] Apply version and date to CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e793362..b46b88bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project follows an extension of fourth number represents an administrative maintenance release with no code changes. -### *Unreleased* +### [2.4] - 2026-03-19 #### Added From 19bcd72dd7651de22270a8c26a96f84cb94c3006 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Fri, 20 Mar 2026 00:01:49 -0400 Subject: [PATCH 100/122] Update CONTRIBUTING.md --- CONTRIBUTING.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c51c3a5e..e73bd72a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,6 +122,10 @@ flag: $ pytest --nonloc ``` +Most of these nonlocal tests now use an ephemeral local web server instead of +reaching out to the web, and so should run even without network access and +should just run faster in general. + When putting together a PR, at minimum, please add/augment the test suite as necessary to maintain 100% test coverage. To the extent possible, please go beyond this and add tests that check potential edge cases, bad/malformed/invalid @@ -141,15 +145,15 @@ 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.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.: +through 3.14 installed and on `PATH` as `python3.10`, `python3.11`, etc., as +well as free-threaded versions for Python 3.13 onward as `python3.13t`, etc. The +test matrix can be accelerated by using `tox`'s parallel execution mode; e.g.: ```bash $ tox -rp2 ``` + ## Code Autoformatting The project is set up with a `tox` environment to blacken the codebase; run with: @@ -244,20 +248,17 @@ with `make linkcheck`. ## Continuous Integration -Both Github Actions and Azure Pipelines are set up for the project, and should -run on any forks of the repository. +Github Actions workflows are set up for the project, and should run on any forks +of the repository. Note that the CI runs differently on draft versus non-draft +PRs: on draft PRs, the only workflow that runs runs tests on Windows and Linux +with one Python version; whereas on non-draft PRs, a complete test matrix of +platforms and Python versions is run, as well as doctests and linting checks. 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. -The Azure Pipelines CI runs an extensive matrix of cross-platform and -cross-Python-version tests, as well as numerous other checks. Due to its length, -it is configured to run only on release branches and PRs to `main` or `stable`. -The Azure Pipelines workflows now [also obey `[skip ci]` -directives](https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#skipping-ci-for-individual-pushes). - ## CHANGELOG From 1f852e81506d49bee9ed1d5cd5771999bc38f56f Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Fri, 20 Mar 2026 00:08:24 -0400 Subject: [PATCH 101/122] Bump release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b46b88bb..320098ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project follows an extension of fourth number represents an administrative maintenance release with no code changes. -### [2.4] - 2026-03-19 +### [2.4] - 2026-03-20 #### Added From 13376fc55341498bd105ea95af92400822f09c26 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Fri, 20 Mar 2026 00:12:50 -0400 Subject: [PATCH 102/122] Fix line length in parser.py --- src/sphobjinv/cli/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sphobjinv/cli/parser.py b/src/sphobjinv/cli/parser.py index 224bd59d..44e1a1ab 100644 --- a/src/sphobjinv/cli/parser.py +++ b/src/sphobjinv/cli/parser.py @@ -44,7 +44,8 @@ class PrsConst: #: Version &c. output blurb VER_TXT = ( - f"\nsphobjinv v{__version__}\n\nCopyright (c) 2016-2026 Brian Skinn and community contributors\n" + f"\nsphobjinv v{__version__}\n\n" + "Copyright (c) 2016-2026 Brian Skinn and community contributors\n" "License: The MIT License\n\n" "Bug reports & feature requests:" " https://github.com/bskinn/sphobjinv\n" From 085356e31ca2979b5b6ea333b75e08fe636b0250 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Fri, 20 Mar 2026 00:16:13 -0400 Subject: [PATCH 103/122] Update attrs versions in doctests --- doc/source/api_usage.rst | 16 ++++++++-------- doc/source/cli/convert.rst | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/source/api_usage.rst b/doc/source/api_usage.rst index 8e6e097c..bef6fc6e 100644 --- a/doc/source/api_usage.rst +++ b/doc/source/api_usage.rst @@ -17,9 +17,9 @@ Inspecting the contents of an existing inventory is handled entirely by the >>> inv = soi.Inventory('objects_attrs.inv') >>> print(inv) - + >>> inv.version - '22.1' + '25.4' >>> inv.count 129 @@ -56,10 +56,10 @@ inventories, as |bytes|: >>> inv2 = soi.Inventory(inv.data_file()) >>> print(inv2) - + >>> inv3 = soi.Inventory(soi.compress(inv.data_file())) >>> print(inv3) - + Remote |objects.inv| files can also be retrieved via URL, with the *url* keyword argument: @@ -67,7 +67,7 @@ Remote |objects.inv| files can also be retrieved via URL, with the *url* keyword >>> inv4 = soi.Inventory(url='https://github.com/bskinn/sphobjinv/raw/main/tests/resource/objects_attrs.inv') >>> print(inv4) - + Comparing Inventories --------------------- @@ -156,7 +156,7 @@ the plaintext |objects.inv| format **as** |bytes| via :meth:`~sphobjinv.inventor >>> print(*inv.data_file().splitlines()[:6], sep='\n') b'# Sphinx inventory version 2' b'# Project: attrs' - b'# Version: 22.1' + b'# Version: 25.4' b'# The remainder of this file is compressed using zlib.' b'attr py:module 0 index.html#module-$ -' b'attr.VersionInfo py:class 1 api.html#$ -' @@ -202,7 +202,7 @@ To export plaintext: >>> print(*Path('objects_attrs.txt').read_text().splitlines()[:6], sep='\n') # Sphinx inventory version 2 # Project: attrs - # Version: 22.1 + # Version: 25.4 # The remainder of this file is compressed using zlib. attr py:module 0 index.html#module-$ - attr.VersionInfo py:class 1 api.html#$ - @@ -216,7 +216,7 @@ For zlib-compressed: >>> print(*Path('objects_attrs_new.inv').read_bytes().splitlines()[:4], sep='\n') b'# Sphinx inventory version 2' b'# Project: attrs' - b'# Version: 22.1' + b'# Version: 25.4' b'# The remainder of this file is compressed using zlib.' >>> print(Path('objects_attrs_new.inv').read_bytes().splitlines()[6][:10]) b'\xbf\x86\x8fL49\xc4\x91\xb8\x8c' diff --git a/doc/source/cli/convert.rst b/doc/source/cli/convert.rst index f3871938..2f49e38e 100644 --- a/doc/source/cli/convert.rst +++ b/doc/source/cli/convert.rst @@ -32,7 +32,7 @@ Basic file conversion to the default output filename is straightforward: >>> print(file_head('objects_attrs.txt', head=6)) # Sphinx inventory version 2 # Project: attrs - # Version: 22.1 + # Version: 25.4 # The remainder of this file is compressed using zlib. attr py:module 0 index.html#module-$ - attr.VersionInfo py:class 1 api.html#$ - @@ -76,7 +76,7 @@ indicated URL): >>> print(file_head('objects.txt', head=6)) # Sphinx inventory version 2 # Project: attrs - # Version: 22.1 + # Version: 25.4 # The remainder of this file is compressed using zlib. attr py:module 0 index.html#module-$ - attr.VersionInfo py:class 1 api.html#$ - @@ -135,7 +135,7 @@ If processing of JSON files by API URL is desirable, please >>> cli_run('sphobjinv co plain objects_attrs.inv -') # Sphinx inventory version 2 # Project: attrs - # Version: 22.1 + # Version: 25.4 # The remainder of this file is compressed using zlib. attr py:module 0 index.html#module-$ - attr.VersionInfo py:class 1 api.html#$ - From 83acf02d83f975f0d2dee60ff6020b38b0cef4cd Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Fri, 20 Mar 2026 00:20:04 -0400 Subject: [PATCH 104/122] Bump checkout/setup-python actions versions and de-persist creds Closes #322 --- .github/workflows/all_core_tests.yml | 6 ++++-- .github/workflows/ready_doctest.yml | 6 ++++-- .github/workflows/ready_linting.yml | 12 ++++++++---- .github/workflows/ready_test_matrix.yml | 6 ++++-- .github/workflows/ready_test_nonloc.yml | 6 ++++-- .github/workflows/release_check_sdist.yml | 6 ++++-- .github/workflows/release_doc_warnings.yml | 6 ++++-- .github/workflows/release_enusre_no_ver_markers.yml | 2 ++ .github/workflows/release_flake8_noqa_nofail.yml | 6 ++++-- .github/workflows/release_readme_doctest.yml | 6 ++++-- .github/workflows/release_test_file_coverage.yml | 6 ++++-- 11 files changed, 46 insertions(+), 22 deletions(-) diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml index 96935961..33e07497 100644 --- a/.github/workflows/all_core_tests.yml +++ b/.github/workflows/all_core_tests.yml @@ -19,10 +19,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_doctest.yml b/.github/workflows/ready_doctest.yml index a5fc4dda..cb3b304b 100644 --- a/.github/workflows/ready_doctest.yml +++ b/.github/workflows/ready_doctest.yml @@ -22,10 +22,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_linting.yml b/.github/workflows/ready_linting.yml index 0e1cb685..08e9fa96 100644 --- a/.github/workflows/ready_linting.yml +++ b/.github/workflows/ready_linting.yml @@ -22,10 +22,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' @@ -50,10 +52,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_test_matrix.yml b/.github/workflows/ready_test_matrix.yml index be3136be..f013c860 100644 --- a/.github/workflows/ready_test_matrix.yml +++ b/.github/workflows/ready_test_matrix.yml @@ -29,10 +29,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.py }} cache: 'pip' diff --git a/.github/workflows/ready_test_nonloc.yml b/.github/workflows/ready_test_nonloc.yml index 9a7d0d7b..8bfd830e 100644 --- a/.github/workflows/ready_test_nonloc.yml +++ b/.github/workflows/ready_test_nonloc.yml @@ -25,10 +25,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_check_sdist.yml b/.github/workflows/release_check_sdist.yml index bc2ac334..e8e7598c 100644 --- a/.github/workflows/release_check_sdist.yml +++ b/.github/workflows/release_check_sdist.yml @@ -21,10 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_doc_warnings.yml b/.github/workflows/release_doc_warnings.yml index 2cb09b18..82addcd6 100644 --- a/.github/workflows/release_doc_warnings.yml +++ b/.github/workflows/release_doc_warnings.yml @@ -21,10 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml index 446bd552..24f67444 100644 --- a/.github/workflows/release_enusre_no_ver_markers.yml +++ b/.github/workflows/release_enusre_no_ver_markers.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@v6 + with: + persist-credentials: false - name: Error if any markers found run: | diff --git a/.github/workflows/release_flake8_noqa_nofail.yml b/.github/workflows/release_flake8_noqa_nofail.yml index 249ed3bb..0fdcde5f 100644 --- a/.github/workflows/release_flake8_noqa_nofail.yml +++ b/.github/workflows/release_flake8_noqa_nofail.yml @@ -21,10 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_readme_doctest.yml b/.github/workflows/release_readme_doctest.yml index 9327fce3..056a97ad 100644 --- a/.github/workflows/release_readme_doctest.yml +++ b/.github/workflows/release_readme_doctest.yml @@ -21,10 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_test_file_coverage.yml b/.github/workflows/release_test_file_coverage.yml index d3bd5452..5dd7beba 100644 --- a/.github/workflows/release_test_file_coverage.yml +++ b/.github/workflows/release_test_file_coverage.yml @@ -21,10 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' cache: 'pip' From 12076095aa01c29ddb2cbc2d9d6aeee99b7a5e95 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 21:46:18 -0400 Subject: [PATCH 105/122] Pin actions to SHAs --- .github/workflows/all_core_tests.yml | 4 ++-- .github/workflows/ready_doctest.yml | 4 ++-- .github/workflows/ready_linting.yml | 8 ++++---- .github/workflows/ready_test_matrix.yml | 4 ++-- .github/workflows/ready_test_nonloc.yml | 4 ++-- .github/workflows/release_check_sdist.yml | 4 ++-- .github/workflows/release_doc_warnings.yml | 4 ++-- .github/workflows/release_enusre_no_ver_markers.yml | 2 +- .github/workflows/release_flake8_noqa_nofail.yml | 4 ++-- .github/workflows/release_readme_doctest.yml | 4 ++-- .github/workflows/release_test_file_coverage.yml | 4 ++-- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml index 33e07497..a521cdb0 100644 --- a/.github/workflows/all_core_tests.yml +++ b/.github/workflows/all_core_tests.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_doctest.yml b/.github/workflows/ready_doctest.yml index cb3b304b..f2ae5c25 100644 --- a/.github/workflows/ready_doctest.yml +++ b/.github/workflows/ready_doctest.yml @@ -22,12 +22,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_linting.yml b/.github/workflows/ready_linting.yml index 08e9fa96..1896ba75 100644 --- a/.github/workflows/ready_linting.yml +++ b/.github/workflows/ready_linting.yml @@ -22,12 +22,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' @@ -52,12 +52,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_test_matrix.yml b/.github/workflows/ready_test_matrix.yml index f013c860..b8d34101 100644 --- a/.github/workflows/ready_test_matrix.yml +++ b/.github/workflows/ready_test_matrix.yml @@ -29,12 +29,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: ${{ matrix.py }} cache: 'pip' diff --git a/.github/workflows/ready_test_nonloc.yml b/.github/workflows/ready_test_nonloc.yml index 8bfd830e..31173549 100644 --- a/.github/workflows/ready_test_nonloc.yml +++ b/.github/workflows/ready_test_nonloc.yml @@ -25,12 +25,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_check_sdist.yml b/.github/workflows/release_check_sdist.yml index e8e7598c..2d4fe8dc 100644 --- a/.github/workflows/release_check_sdist.yml +++ b/.github/workflows/release_check_sdist.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_doc_warnings.yml b/.github/workflows/release_doc_warnings.yml index 82addcd6..fe4105eb 100644 --- a/.github/workflows/release_doc_warnings.yml +++ b/.github/workflows/release_doc_warnings.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml index 24f67444..e7efbd75 100644 --- a/.github/workflows/release_enusre_no_ver_markers.yml +++ b/.github/workflows/release_enusre_no_ver_markers.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false diff --git a/.github/workflows/release_flake8_noqa_nofail.yml b/.github/workflows/release_flake8_noqa_nofail.yml index 0fdcde5f..731f5029 100644 --- a/.github/workflows/release_flake8_noqa_nofail.yml +++ b/.github/workflows/release_flake8_noqa_nofail.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_readme_doctest.yml b/.github/workflows/release_readme_doctest.yml index 056a97ad..c195cf17 100644 --- a/.github/workflows/release_readme_doctest.yml +++ b/.github/workflows/release_readme_doctest.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_test_file_coverage.yml b/.github/workflows/release_test_file_coverage.yml index 5dd7beba..727ebdf7 100644 --- a/.github/workflows/release_test_file_coverage.yml +++ b/.github/workflows/release_test_file_coverage.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@0c366fd6a with: persist-credentials: false - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@28f2168f4d9 with: python-version: '3.13' cache: 'pip' From 432603e7913082fec649f4ac5f47b1c8b0c7df52 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 21:48:19 -0400 Subject: [PATCH 106/122] Update actions to full SHA refs --- .github/workflows/all_core_tests.yml | 4 ++-- .github/workflows/ready_doctest.yml | 4 ++-- .github/workflows/ready_linting.yml | 8 ++++---- .github/workflows/ready_test_matrix.yml | 4 ++-- .github/workflows/ready_test_nonloc.yml | 4 ++-- .github/workflows/release_check_sdist.yml | 4 ++-- .github/workflows/release_doc_warnings.yml | 4 ++-- .github/workflows/release_enusre_no_ver_markers.yml | 2 +- .github/workflows/release_flake8_noqa_nofail.yml | 4 ++-- .github/workflows/release_readme_doctest.yml | 4 ++-- .github/workflows/release_test_file_coverage.yml | 4 ++-- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml index a521cdb0..686676ad 100644 --- a/.github/workflows/all_core_tests.yml +++ b/.github/workflows/all_core_tests.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_doctest.yml b/.github/workflows/ready_doctest.yml index f2ae5c25..73519dde 100644 --- a/.github/workflows/ready_doctest.yml +++ b/.github/workflows/ready_doctest.yml @@ -22,12 +22,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_linting.yml b/.github/workflows/ready_linting.yml index 1896ba75..427d6847 100644 --- a/.github/workflows/ready_linting.yml +++ b/.github/workflows/ready_linting.yml @@ -22,12 +22,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' @@ -52,12 +52,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/ready_test_matrix.yml b/.github/workflows/ready_test_matrix.yml index b8d34101..dda2f278 100644 --- a/.github/workflows/ready_test_matrix.yml +++ b/.github/workflows/ready_test_matrix.yml @@ -29,12 +29,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: ${{ matrix.py }} cache: 'pip' diff --git a/.github/workflows/ready_test_nonloc.yml b/.github/workflows/ready_test_nonloc.yml index 31173549..e434d728 100644 --- a/.github/workflows/ready_test_nonloc.yml +++ b/.github/workflows/ready_test_nonloc.yml @@ -25,12 +25,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_check_sdist.yml b/.github/workflows/release_check_sdist.yml index 2d4fe8dc..35e2505d 100644 --- a/.github/workflows/release_check_sdist.yml +++ b/.github/workflows/release_check_sdist.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_doc_warnings.yml b/.github/workflows/release_doc_warnings.yml index fe4105eb..f3e33c09 100644 --- a/.github/workflows/release_doc_warnings.yml +++ b/.github/workflows/release_doc_warnings.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_enusre_no_ver_markers.yml b/.github/workflows/release_enusre_no_ver_markers.yml index e7efbd75..da49997e 100644 --- a/.github/workflows/release_enusre_no_ver_markers.yml +++ b/.github/workflows/release_enusre_no_ver_markers.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false diff --git a/.github/workflows/release_flake8_noqa_nofail.yml b/.github/workflows/release_flake8_noqa_nofail.yml index 731f5029..16d9993d 100644 --- a/.github/workflows/release_flake8_noqa_nofail.yml +++ b/.github/workflows/release_flake8_noqa_nofail.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_readme_doctest.yml b/.github/workflows/release_readme_doctest.yml index c195cf17..4a690a07 100644 --- a/.github/workflows/release_readme_doctest.yml +++ b/.github/workflows/release_readme_doctest.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/release_test_file_coverage.yml b/.github/workflows/release_test_file_coverage.yml index 727ebdf7..52c3dad7 100644 --- a/.github/workflows/release_test_file_coverage.yml +++ b/.github/workflows/release_test_file_coverage.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@0c366fd6a + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install Python - uses: actions/setup-python@28f2168f4d9 + uses: actions/setup-python@28f2168f4d98ee0445e3c6321f6e6616c83dd5ec with: python-version: '3.13' cache: 'pip' From a5db0c1c55e0e9e51fcf90820b285591e9482b75 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 22:26:30 -0400 Subject: [PATCH 107/122] Update doctests to new attrs version --- doc/source/api_usage.rst | 40 +++++++++++++++++++------------------- doc/source/cli/convert.rst | 13 +++++++------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/doc/source/api_usage.rst b/doc/source/api_usage.rst index bef6fc6e..b8f236a5 100644 --- a/doc/source/api_usage.rst +++ b/doc/source/api_usage.rst @@ -17,11 +17,11 @@ Inspecting the contents of an existing inventory is handled entirely by the >>> inv = soi.Inventory('objects_attrs.inv') >>> print(inv) - + >>> inv.version '25.4' >>> inv.count - 129 + 180 The location of the inventory file to import can also be provided as a :class:`pathlib.Path`, instead of as a string: @@ -38,16 +38,16 @@ a |list| in the :attr:`~sphobjinv.inventory.Inventory.objects` attribute: .. doctest:: api_inspect >>> len(inv.objects) - 129 + 180 >>> dobj = inv.objects[0] >>> dobj - DataObjStr(name='attr', domain='py', role='module', priority='0', uri='index.html#module-$', dispname='-') + DataObjStr(name='attr', domain='py', role='module', priority='0', uri='api-attr.html#module-$', dispname='-') >>> dobj.name 'attr' >>> dobj.domain 'py' - >>> [d.name for d in inv.objects if 'validator' in d.uri] - ['api_validators', 'examples_validators'] + >>> [d.name for d in inv.objects if 'validator' in d.name][:2] + ['attr.get_run_validators', 'attr.set_run_validators'] :class:`~sphobjinv.inventory.Inventory` objects can also import from plaintext or zlib-compressed inventories, as |bytes|: @@ -56,10 +56,10 @@ inventories, as |bytes|: >>> inv2 = soi.Inventory(inv.data_file()) >>> print(inv2) - + >>> inv3 = soi.Inventory(soi.compress(inv.data_file())) >>> print(inv3) - + Remote |objects.inv| files can also be retrieved via URL, with the *url* keyword argument: @@ -67,7 +67,7 @@ Remote |objects.inv| files can also be retrieved via URL, with the *url* keyword >>> inv4 = soi.Inventory(url='https://github.com/bskinn/sphobjinv/raw/main/tests/resource/objects_attrs.inv') >>> print(inv4) - + Comparing Inventories --------------------- @@ -118,7 +118,7 @@ The :class:`~sphobjinv.data.DataObjStr` instances can be edited in place: >>> inv = soi.Inventory('objects_attrs.inv') >>> inv.objects[0] - DataObjStr(name='attr', domain='py', role='module', priority='0', uri='index.html#module-$', dispname='-') + DataObjStr(name='attr', domain='py', role='module', priority='0', uri='api-attr.html#module-$', dispname='-') >>> inv.objects[0].uri = 'attribute.html' >>> inv.objects[0] DataObjStr(name='attr', domain='py', role='module', priority='0', uri='attribute.html', dispname='-') @@ -130,7 +130,7 @@ New instances can be easily created either by direct instantiation, or by >>> inv.objects.append(inv.objects[0].evolve(name='attr.Generator', uri='generator.html')) >>> inv.count - 130 + 181 >>> inv.objects[-1] DataObjStr(name='attr.Generator', domain='py', role='module', priority='0', uri='generator.html', dispname='-') @@ -141,7 +141,7 @@ The other attributes of the :class:`~sphobjinv.inventory.Inventory` instance can >>> inv.project = 'not_attrs' >>> inv.version = '0.1' >>> print(inv) - + Formatting Inventory Contents @@ -158,8 +158,8 @@ the plaintext |objects.inv| format **as** |bytes| via :meth:`~sphobjinv.inventor b'# Project: attrs' b'# Version: 25.4' b'# The remainder of this file is compressed using zlib.' - b'attr py:module 0 index.html#module-$ -' - b'attr.VersionInfo py:class 1 api.html#$ -' + b'attr py:module 0 api-attr.html#module-$ -' + b'attr.Attribute py:class 1 api-attr.html#$ -' This method makes use of the :meth:`DataObjStr.data_line ` method to format each of the object information lines. @@ -171,11 +171,11 @@ If desired, the :ref:`shorthand ` used for the .. doctest:: api_formatting >>> print(*inv.data_file(expand=True).splitlines()[4:6], sep='\n') - b'attr py:module 0 index.html#module-attr attr' - b'attr.VersionInfo py:class 1 api.html#attr.VersionInfo attr.VersionInfo' + b'attr py:module 0 api-attr.html#module-attr attr' + b'attr.Attribute py:class 1 api-attr.html#attr.Attribute attr.Attribute' >>> do = inv.objects[0] >>> do.data_line(expand=True) - 'attr py:module 0 index.html#module-attr attr' + 'attr py:module 0 api-attr.html#module-attr attr' Exporting an Inventory @@ -204,8 +204,8 @@ To export plaintext: # Project: attrs # Version: 25.4 # The remainder of this file is compressed using zlib. - attr py:module 0 index.html#module-$ - - attr.VersionInfo py:class 1 api.html#$ - + attr py:module 0 api-attr.html#module-$ - + attr.Attribute py:class 1 api-attr.html#$ - For zlib-compressed: @@ -219,7 +219,7 @@ For zlib-compressed: b'# Version: 25.4' b'# The remainder of this file is compressed using zlib.' >>> print(Path('objects_attrs_new.inv').read_bytes().splitlines()[6][:10]) - b'\xbf\x86\x8fL49\xc4\x91\xb8\x8c' + b"$e2'\x92\xbde\xaa\xbdj" For JSON: diff --git a/doc/source/cli/convert.rst b/doc/source/cli/convert.rst index 2f49e38e..7220276c 100644 --- a/doc/source/cli/convert.rst +++ b/doc/source/cli/convert.rst @@ -34,8 +34,8 @@ Basic file conversion to the default output filename is straightforward: # Project: attrs # Version: 25.4 # The remainder of this file is compressed using zlib. - attr py:module 0 index.html#module-$ - - attr.VersionInfo py:class 1 api.html#$ - + attr py:module 0 api-attr.html#module-$ - + attr.Attribute py:class 1 api-attr.html#$ - A different target filename can be specified, to avoid overwriting an existing file: @@ -76,7 +76,7 @@ indicated URL): >>> print(file_head('objects.txt', head=6)) # Sphinx inventory version 2 # Project: attrs - # Version: 25.4 + # Version: ... # The remainder of this file is compressed using zlib. attr py:module 0 index.html#module-$ - attr.VersionInfo py:class 1 api.html#$ - @@ -137,9 +137,10 @@ If processing of JSON files by API URL is desirable, please # Project: attrs # Version: 25.4 # The remainder of this file is compressed using zlib. - attr py:module 0 index.html#module-$ - - attr.VersionInfo py:class 1 api.html#$ - - attr._make.Attribute py:class -1 api.html#attrs.Attribute - + attr py:module 0 api-attr.html#module-$ - + attr.Attribute py:class 1 api-attr.html#$ - + attr.NOTHING py:data 1 api-attr.html#$ - + attr.VersionInfo py:class 1 api-attr.html#$ - ... From 44ccac6584ba025d3e485f43c3f616630a85c04d Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 22:35:30 -0400 Subject: [PATCH 108/122] Update test-tests with new attrs content --- tests/test_api_good.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_api_good.py b/tests/test_api_good.py index 7ce59187..a6c41021 100644 --- a/tests/test_api_good.py +++ b/tests/test_api_good.py @@ -115,19 +115,19 @@ def test_api_decompress(self, path_fxn, scratch_path, misc_info, decomp_cmp_test soi.DataFields.Domain: b"py", soi.DataFields.Role: b"module", soi.DataFields.Priority: b"0", - soi.DataFields.URI: b"index.html#module-$", + soi.DataFields.URI: b"api-attr.html#module-$", soi.DataFields.DispName: b"-", }, ], [ -3, { # slots std:label -1 examples.html#$ Slots - soi.DataFields.Name: b"validators", + soi.DataFields.Name: b"types", soi.DataFields.Domain: b"std", - soi.DataFields.Role: b"label", + soi.DataFields.Role: b"doc", soi.DataFields.Priority: b"-1", - soi.DataFields.URI: b"init.html#$", - soi.DataFields.DispName: b"Validators", + soi.DataFields.URI: b"types.html", + soi.DataFields.DispName: b"Type Annotations", }, ], ), @@ -136,7 +136,7 @@ def test_api_data_regex(self, element, datadict, bytes_txt, misc_info): """Confirm the regex for loading data lines is working properly.""" # Prelim approximate check to be sure we're working with the # correct file/data. - assert len(soi.re.pb_data.findall(bytes_txt)) == 129 + assert len(soi.re.pb_data.findall(bytes_txt)) == 180 mchs = list(soi.re.pb_data.finditer(bytes_txt)) @@ -448,7 +448,7 @@ def test_api_inventory_toosmallflatdict_importbutignore(self, res_dec): inv2 = soi.Inventory(d, count_error=False) # 128 (one less than 129) b/c the loop continues past missing elements - assert inv2.count == 128 + assert inv2.count == 179 def test_api_inventory_namesuggest(self, res_cmp, check): """Confirm object name suggestion is nominally working on a specific object.""" From 24677038a5c50d0beea8836f1a8ed083d5f37e9b Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 23:29:26 -0400 Subject: [PATCH 109/122] Fix Sphinx inv objects check for multiple sphinx inventories Pattern match, not just substring --- tests/test_api_good.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_api_good.py b/tests/test_api_good.py index a6c41021..7945b57a 100644 --- a/tests/test_api_good.py +++ b/tests/test_api_good.py @@ -584,7 +584,25 @@ def test_api_inventory_matches_sphinx_ifile( original_ifile_data ), fname - elif "sphinx.inv" in fname: # pragma: no cover + elif re.search(r"sphinx.*[.]inv", fname): # pragma: no cover + soi_names = [o.name for o in inv.objects] + ifile_names = list( + itt.chain.from_iterable( + list(original_ifile_data[k].keys()) for k in original_ifile_data + ) + ) + # There is the same set of unique names in the sphobjinv Inventory + # as in the Sphinx IFile imported data ... + assert set(soi_names) == set(ifile_names), fname + + # ... but there are duplicate names in each ... + assert inv.count > len(set(soi_names)), fname + assert sphinx_ifile_data_count(original_ifile_data) > len( + set(ifile_names) + ), fname + + # ... and there are always four more items in the sphobjinv Inventory + # than in the IFile data. assert inv.count == 4 + sphinx_ifile_data_count(original_ifile_data), fname else: From 440c68588143fa990ffec5a17301cc0e5fd47f3e Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 23:39:14 -0400 Subject: [PATCH 110/122] Refine sphinx inv filter The old one doesn't have the mismatch --- tests/test_api_good.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api_good.py b/tests/test_api_good.py index 7945b57a..3d4d52c2 100644 --- a/tests/test_api_good.py +++ b/tests/test_api_good.py @@ -584,7 +584,7 @@ def test_api_inventory_matches_sphinx_ifile( original_ifile_data ), fname - elif re.search(r"sphinx.*[.]inv", fname): # pragma: no cover + elif re.search(r"sphinx(|_6_0b)[.]inv", fname): # pragma: no cover soi_names = [o.name for o in inv.objects] ifile_names = list( itt.chain.from_iterable( From d2402afe6a15a94df9b411ec10529587baca61cf Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 21 Mar 2026 23:59:17 -0400 Subject: [PATCH 111/122] Add missing newline in test_api_good.py --- tests/test_api_good.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_api_good.py b/tests/test_api_good.py index 3d4d52c2..d746bff1 100644 --- a/tests/test_api_good.py +++ b/tests/test_api_good.py @@ -591,6 +591,7 @@ def test_api_inventory_matches_sphinx_ifile( list(original_ifile_data[k].keys()) for k in original_ifile_data ) ) + # There is the same set of unique names in the sphobjinv Inventory # as in the Sphinx IFile imported data ... assert set(soi_names) == set(ifile_names), fname From 4bf131fdb4b4408cc76f8e7627dc35d3ae616663 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 22 Mar 2026 01:15:10 -0400 Subject: [PATCH 112/122] Remove flake8 extensions test Now that we're running flake8 with tox, the virtualenv can be de-synced with the linting environment, resulting in spurious test failure. Test was really always overkill anyways. --- .../workflows/release_test_file_coverage.yml | 4 +- tests/conftest.py | 3 - tests/test_flake8_ext.py | 73 ------------------- tox.ini | 1 - 4 files changed, 2 insertions(+), 79 deletions(-) delete mode 100644 tests/test_flake8_ext.py diff --git a/.github/workflows/release_test_file_coverage.yml b/.github/workflows/release_test_file_coverage.yml index 52c3dad7..f2c40258 100644 --- a/.github/workflows/release_test_file_coverage.yml +++ b/.github/workflows/release_test_file_coverage.yml @@ -35,7 +35,7 @@ jobs: requirements-flake8.txt - name: Install CI requirements - run: pip install -r requirements-ci.txt -r requirements-flake8.txt + run: pip install -r requirements-ci.txt - name: Build docs & ensure scratch run: | @@ -44,7 +44,7 @@ jobs: mkdir scratch - name: Run pytest covering entire project tree - run: pytest --cov=. --nonloc --flake8_ext + run: pytest --cov=. --nonloc - name: Check 100% test code execution run: coverage report --include="tests/*" --fail-under=100 diff --git a/tests/conftest.py b/tests/conftest.py index 937fa434..f982e265 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,9 +61,6 @@ def pytest_addoption(parser): ), ) parser.addoption("--nonloc", action="store_true", help="Include nonlocal tests") - parser.addoption( - "--flake8_ext", action="store_true", help="Include flake8 extensions test" - ) @pytest.fixture(scope="session") diff --git a/tests/test_flake8_ext.py b/tests/test_flake8_ext.py deleted file mode 100644 index ddc5933e..00000000 --- a/tests/test_flake8_ext.py +++ /dev/null @@ -1,73 +0,0 @@ -r"""*Test(s) to ensure full loading of flake8 extensions*. - -``sphobjinv`` is a toolkit for manipulation and inspection of -Sphinx |objects.inv| files. - -**Author** - Brian Skinn (brian.skinn@gmail.com) - -**File Created** - 27 Apr 2019 - -**Copyright** - \(c) 2016-2026 Brian Skinn and community contributors - -**Source Repository** - https://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 re -import subprocess as sp # noqa: S404 -import sys -from pathlib import Path - -import pytest - -pytestmark = [pytest.mark.flake8_ext] - - -@pytest.fixture(scope="module", autouse=True) -def skip_if_no_flake8_ext(pytestconfig): - """Skip test if --flake8_ext not provided. - - Auto-applied to all functions in module. - - """ - if not pytestconfig.getoption("--flake8_ext"): - pytest.skip("'--flake8_ext' not specified") # pragma: no cover - - -@pytest.mark.skipif( - sys.version_info < (3, 6), - reason="Some flake8 extensions require Python 3.6 or later", -) -def test_flake8_version_output(check): - """Confirm that all desired plugins actually report as loaded.""" - p_pkgname = re.compile("^[0-9a-z_-]+", re.I) - plugins = Path("requirements-flake8.txt").read_text().splitlines()[1:] - plugins = [p_pkgname.search(p).group(0) for p in plugins] - - # This is fragile if anything ends up not having a prefix that needs - # stripping - plugins = [p.partition("flake8-")[-1] for p in plugins] - - flake8_ver_output = sp.check_output( # noqa: S607,S603 - ["flake8", "--version"], universal_newlines=True - ) # noqa: S607,S603 - - for p in plugins: - with check(msg=p): - assert p in flake8_ver_output.replace("_", "-").replace("\n", "") diff --git a/tox.ini b/tox.ini index 8ada0578..fe6d9d68 100644 --- a/tox.ini +++ b/tox.ini @@ -162,7 +162,6 @@ markers = 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 - flake8_ext: Test checking that all desired plugins are active first: Inherited marker from `pytest-ordering` timeout: Inherited marker from `pytest-timeout` From 3c40df2aad401fab7d9ad2d6ef5114beba9f1f78 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 22 Mar 2026 01:15:55 -0400 Subject: [PATCH 113/122] Add some pragma: no cover Some for windows; some for paths that are only reached when tests fail. --- tests/conftest.py | 8 ++++---- tests/fixtures_http.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f982e265..26dae3bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -155,7 +155,7 @@ def scratch_path(tmp_path, res_path, misc_info, is_win, unix2dos): # With the conversion of resources/objects_attrs.txt to Unix EOLs in order to # provide for a Unix-testable sdist, on Windows systems this resource needs # to be converted to DOS EOLs for consistency. - if is_win: + if is_win: # pragma: no cover win_path = tmp_path / f"{scr_base}{misc_info.Extensions.DEC.value}" win_path.write_bytes(unix2dos(win_path.read_bytes())) @@ -217,7 +217,7 @@ def func(path): """Perform the 'live' inventory load test.""" try: sphinx_ifile_load(path) - except Exception as e: # noqa: PIE786 + except Exception as e: # noqa: PIE786 pragma: no cover # An exception here is a failing test, not a test error. pytest.fail(e) @@ -262,7 +262,7 @@ def func(arglist, *, command=CLICommand.Core, expect=0): # , suffix=None): except SystemExit as e: retcode = e.args[0] ok = True - else: + else: # pragma: no cover ok = False # Do all pytesty stuff outside monkeypatch context @@ -284,7 +284,7 @@ def func(path): res_bytes = Path(misc_info.res_decomp_path).read_bytes() tgt_bytes = Path(path).read_bytes() # .replace(b"\r\n", b"\n") - if is_win: + if is_win: # pragma: no cover # Have to explicitly convert these newlines, now that the # tests/resource/objects_attrs.txt file is marked 'binary' in # .gitattributes diff --git a/tests/fixtures_http.py b/tests/fixtures_http.py index 694d5234..7627db42 100644 --- a/tests/fixtures_http.py +++ b/tests/fixtures_http.py @@ -94,7 +94,7 @@ 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(): + if not resource_dir.is_dir(): # pragma: no cover raise RuntimeError( f"Expected test resource directory not found: {resource_dir}" ) @@ -119,7 +119,7 @@ def resource_url(resource_http_base_url: str) -> Callable[[str], str]: 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: + if ".." in Path(rel_path).parts: # pragma: no cover raise ValueError("Path must not contain '..'") # Ensure consistent URL path separators. From b49356bbe8cf787a1fd64cb24ec99b4e6022a380 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 22 Mar 2026 01:16:02 -0400 Subject: [PATCH 114/122] Don't cover setup.py --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index d3b3b11b..9cac3c1e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,8 @@ source = omit = # Don't worry about covering vendored libraries src/sphobjinv/_vendored/* + # Not part of the test suite + setup.py [report] exclude_lines = From 40ed2d7eeb772fa1eaf9ee4f496b4abdd47301c5 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 22 Mar 2026 01:16:24 -0400 Subject: [PATCH 115/122] Remove obsolete fixture for doc scratch No longer needed now that we've pulled out the README shell doctests --- tests/conftest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 26dae3bb..ffd9fdf4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -162,14 +162,6 @@ def scratch_path(tmp_path, res_path, misc_info, is_win, unix2dos): yield tmp_path -@pytest.fixture(scope="session") -def ensure_doc_scratch(): - """Ensure doc/scratch dir exists, for README shell examples.""" - (Path(__file__).resolve().parent.parent / "doc" / "scratch").mkdir( - parents=True, exist_ok=True - ) - - @pytest.fixture(scope="session") def bytes_txt(misc_info, res_path): """Load and return the contents of the example objects_attrs.txt as bytes.""" From d3bcb832bcc8087186cc6bced8e266f6fd72baf5 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sun, 22 Mar 2026 01:19:50 -0400 Subject: [PATCH 116/122] Update CHANGELOG --- CHANGELOG.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 320098ee..693a5e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project follows an extension of fourth number represents an administrative maintenance release with no code changes. -### [2.4] - 2026-03-20 +### [2.4] - 2026-03-22 #### Added @@ -21,6 +21,18 @@ changes. #### Tests + * Remove flake8_ext test file and machinery ([#336]). + * pytest environment now can easily de-sync from the flake8 environment + since flake8 is running in tox now. + * It was really always over-cautious, too. + + * Exclude `setup.py` from coverage ([#336]). + * Necessary due to a change in coverage.py behavior, maybe? + * Definitely is not expected to run during execution of the test suite. + + * Remove unused `ensure_doc_scratch` fixture from `conftest.py` ([#336]). + * Obsolete now that the README shell examples aren't doctested. + * Add 3.13t and 3.14t to `tox` test matrix ([#333]). * Also add report of the current GIL status to the `tox` env output. @@ -55,6 +67,9 @@ changes. #### Internal + * Pin Actions versions to SHAs and de-persist credentials ([#)336]. + * Closes [#322]. + * Add Actions workflow to error on a non-draft release branch if any `#VER#` markers remain in docs source ([#331]). @@ -81,6 +96,9 @@ changes. #### Documentation + * Update Sphinx, attrs, Python, etc. content to freshen and to match the new + inventories in the test resources ([#336]). + * Dynamically retrieve the current values of `PrsConst.SUGGEST_CONFIRM_LENGTH` and `PrsConst.DEF_THRESH` to define their replaces in `conf.py` ([#331]). @@ -785,7 +803,9 @@ 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 +[#322]: https://github.com/bskinn/sphobjinv/issues/322 [#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 +[#336]: https://github.com/bskinn/sphobjinv/pull/336 From ae31b9d6e3ae75dd2738f3030301024499454c56 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Mon, 23 Mar 2026 00:25:11 -0400 Subject: [PATCH 117/122] Add 'build' tox env --- tox.ini | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tox.ini b/tox.ini index fe6d9d68..47857a09 100644 --- a/tox.ini +++ b/tox.ini @@ -152,6 +152,16 @@ commands= python -Werror -c "import sphobjinv" deps= +[testenv:build] +skip_install=True +description=Build sdist and wheel +deps= + build + twine +commands= + python -m build + twine check dist/* + [pytest] markers = local: Tests not requiring Internet access From 77ae00bf170a28573776d12f77d4ede695d323fe Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Mon, 23 Mar 2026 00:29:09 -0400 Subject: [PATCH 118/122] Remove superfluous packages from req'ts files They'll be pulled in by the -e . --- requirements-ci.txt | 3 --- requirements-dev.txt | 4 ---- 2 files changed, 7 deletions(-) diff --git a/requirements-ci.txt b/requirements-ci.txt index 33429641..68da2580 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -1,8 +1,5 @@ -attrs>=19.2 -certifi coverage dictdiffer -jsonschema md-toc pytest>=4.4.0 pytest-check>=1.1.2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 5070a9a5..df2e9b95 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,5 @@ -attrs>=19.2 -build -certifi coverage dictdiffer -jsonschema md-toc pytest>=4.4.0 pytest-check>=1.1.2 From 823f452a9cf834af7088231f97fa262fc9e826d9 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Mon, 23 Mar 2026 00:37:48 -0400 Subject: [PATCH 119/122] Add tests/resource/objects_pdfminer* to MANIFEST.in Otherwise the sdist-install-and-test job warns during the docs build, since it won't have that inventory available. < 1kB, so should negligibly affect sdist size. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0acd4658..51cd741a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ include doc/make.bat doc/Makefile graft tests prune tests/resource -include tests/resource/objects_attrs* tests/resource/objects_sarge* +include tests/resource/objects_attrs* tests/resource/objects_sarge* tests/resource/objects_pdfminer* global-exclude __pycache__/* prune **/*.egg-info From c841682551ad22b3450a9d8d250a0cf63ae04e36 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Mon, 23 Mar 2026 00:46:24 -0400 Subject: [PATCH 120/122] Fix to correct reST italics in textconv.rst --- doc/source/cli/textconv.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/textconv.rst b/doc/source/cli/textconv.rst index 2baa429d..b01c7b95 100644 --- a/doc/source/cli/textconv.rst +++ b/doc/source/cli/textconv.rst @@ -38,7 +38,7 @@ Ultimately, a textconv requires three things: *.inv diff=objects_inv With |sphobjinv-textconv| configured in this fashion as a textconv for Sphinx -inventory files, the following should all yield _nearly_ the same output. +inventory files, the following should all yield *nearly* the same output. Using ``sphobjinv convert``: From d42583c2335fde0771c4754cc25cc79b3b990ca6 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Mon, 23 Mar 2026 00:47:06 -0400 Subject: [PATCH 121/122] Update CHANGELOG & fix typo --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 693a5e65..e46e8358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,19 @@ changes. #### Internal - * Pin Actions versions to SHAs and de-persist credentials ([#)336]. + * Convert `build` call into a `tox` env and remove `build` from + `requirements-dev.txt` ([#336]). + + * Remove redundant packages from `requirements-dev.txt` and + `requirements-ci.txt` that are pulled in by the `-e .` line ([#336]). + + * Add `tests/resource/objects_pdfminer*` to `MANIFEST.in`, to make that + inventory available to the docs build in the sdist unpack-and-test workflow + job ([#336]). + * Otherwise the docs job emits a warning. Not fatal, but better to have a + clean build. + + * Pin Actions versions to SHAs and de-persist credentials ([#336]). * Closes [#322]. * Add Actions workflow to error on a non-draft release branch if any `#VER#` From f4a2a2815d6225ac34817b96668305ee739ba9f8 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Mon, 23 Mar 2026 00:51:13 -0400 Subject: [PATCH 122/122] Bump v2.4 date in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e46e8358..a90a49a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project follows an extension of fourth number represents an administrative maintenance release with no code changes. -### [2.4] - 2026-03-22 +### [2.4] - 2026-03-23 #### Added