From 729bd881462f536746248188ca138fd758e910b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 07:08:32 +0000 Subject: [PATCH 1/2] Check that documentation examples run and produce the expected output The test suite now runs the doctests in doc/tutorial.rst and executes the shell session in doc/commandline.rst, comparing the actual output of the jsonpointer command with the documented one. Both run as part of the regular unittest suite, so CI covers them on every supported Python. This uncovered four outdated outputs in the tutorial, where the documented dict key order did not match what set_pointer returns; these are fixed. The doc sources are added to the sdist so the tests also run from it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PewzpkEmq7YeZd56gmKnqr --- MANIFEST.in | 1 + doc/tutorial.rst | 8 +++---- tests.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0f9b9f6..6e57b2d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include AUTHORS include LICENSE.txt include README.md include tests.py +include doc/*.rst diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 267f05c..d1926e7 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -38,10 +38,10 @@ JSON pointer notation: >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} >>> set_pointer(obj, '/foo/anArray/0/prop', 55) - {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} + {'foo': {'anArray': [{'prop': 55}], 'another prop': {'baz': 'A string'}}} >>> obj - {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} + {'foo': {'anArray': [{'prop': 55}], 'another prop': {'baz': 'A string'}}} By default ``set_pointer`` modifies the original object. Pass ``inplace=False`` to create a copy and modify the copy instead: @@ -50,10 +50,10 @@ to create a copy and modify the copy instead: >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} >>> set_pointer(obj, '/foo/anArray/0/prop', 55, inplace=False) - {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} + {'foo': {'anArray': [{'prop': 55}], 'another prop': {'baz': 'A string'}}} >>> obj - {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 44}]}} + {'foo': {'anArray': [{'prop': 44}], 'another prop': {'baz': 'A string'}}} By default, setting a value below a member that does not exist raises a ``JsonPointerException``. Pass ``create=True`` to create missing members as diff --git a/tests.py b/tests.py index 23301bb..9b508d2 100755 --- a/tests.py +++ b/tests.py @@ -6,9 +6,11 @@ import json import os import runpy +import shlex import subprocess import sys import tempfile +import textwrap import unittest from contextlib import redirect_stderr, redirect_stdout from unittest import mock @@ -886,6 +888,65 @@ def test_non_verbose_exception_excludes_doc(self): self.assertIn('bar', str(e)) +class DocExampleTests(unittest.TestCase): + """ Runs the shell examples from the documentation """ + + ROOT = os.path.dirname(os.path.abspath(__file__)) + SCRIPT = os.path.join(ROOT, 'bin', 'jsonpointer') + + def _examples(self, name): + """ Yields (command, expected output) pairs of the shell sessions in + the ``code-block:: bash`` directives of the given doc file """ + with open(os.path.join(self.ROOT, 'doc', name), encoding='utf-8') as f: + blocks = f.read().split('.. code-block:: bash\n')[1:] + self.assertTrue(blocks, 'no bash examples found in ' + name) + + for block in blocks: + # The block ends at the first non-indented line + lines = [] + for line in block.splitlines()[1:]: + if line and not line[0].isspace(): + break + lines.append(line) + lines = textwrap.dedent('\n'.join(lines)).splitlines() + + cmd, output = None, [] + for line in lines + ['']: + if line.startswith('$ '): + cmd, output = line[2:], [] + elif cmd is not None and line.strip() and not line.startswith('#'): + output.append(line) + elif cmd is not None: + yield cmd, ''.join(out + '\n' for out in output) + cmd = None + + def test_commandline_examples(self): + tmpdir = tempfile.TemporaryDirectory() + self.addCleanup(tmpdir.cleanup) + env = dict(os.environ, PYTHONPATH=self.ROOT, PYTHONUTF8='1') + + examples = list(self._examples('commandline.rst')) + self.assertTrue(any(cmd.startswith('jsonpointer ') + for cmd, _ in examples)) + for cmd, expected in examples: + with self.subTest(cmd=cmd): + args = shlex.split(cmd) + if args[0] == 'cat': + # "cat" shows the input files used by later examples + path = os.path.join(tmpdir.name, args[1]) + with open(path, 'w', encoding='utf-8') as f: + f.write(expected) + continue + + self.assertEqual(args[0], 'jsonpointer') + result = subprocess.run([sys.executable, self.SCRIPT] + args[1:], + cwd=tmpdir.name, env=env, + capture_output=True, encoding='utf-8') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, expected) + + def load_tests(loader, tests, ignore): tests.addTests(doctest.DocTestSuite(jsonpointer)) + tests.addTests(doctest.DocFileSuite('doc/tutorial.rst')) return tests From 72c3291b709a25f35b2e6b5e351483ac2d6a3318 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:40:30 +0000 Subject: [PATCH 2/2] Don't drop doc examples followed directly by another command A "$ " line right after a command's output started the next command without checking the previous one, so its output was never verified. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PewzpkEmq7YeZd56gmKnqr --- tests.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests.py b/tests.py index 17982f0..446b5cf 100755 --- a/tests.py +++ b/tests.py @@ -949,13 +949,15 @@ def _examples(self, name): cmd, output = None, [] for line in lines + ['']: - if line.startswith('$ '): - cmd, output = line[2:], [] - elif cmd is not None and line.strip() and not line.startswith('#'): + is_output = line.strip() and not line.startswith(('$ ', '#')) + if cmd is not None and is_output: output.append(line) - elif cmd is not None: + continue + if cmd is not None: yield cmd, ''.join(out + '\n' for out in output) - cmd = None + cmd, output = None, [] + if line.startswith('$ '): + cmd = line[2:] def test_commandline_examples(self): tmpdir = tempfile.TemporaryDirectory()