From fe832fdd263ac477a7d690e5f04ef472517fe998 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Thu, 29 Aug 2024 00:47:08 -0400 Subject: [PATCH 01/37] 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 12518da3a3ae927c8970a649182f9bffec5ae6fc Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Wed, 24 Dec 2025 20:04:25 -0500 Subject: [PATCH 10/37] 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 11/37] 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 12/37] 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 13/37] 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 14/37] 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 15/37] 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 16/37] 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 17/37] 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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] 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 33/37] 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 34/37] 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 35/37] 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 36/37] 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 37/37] 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