diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bab5533..8523933 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,8 +13,6 @@ jobs: strategy: matrix: python-version: - - "3.8" - - "3.9" - "3.10" - "3.11" - "3.12" @@ -38,7 +36,7 @@ jobs: run: | tox -v -e py - name: Lint - if: matrix.python-version == '3.12' + if: matrix.python-version == '3.14' run: | python -m pip install -U pylint ".[test]" pylint src diff --git a/CHANGES.rst b/CHANGES.rst index 4899120..bcc64e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,10 @@ 0.20 (unreleased) ================= -- Nothing changed yet. +- Stop testing Python 3.8 and 3.9. Only 3.10 and above are supported. +- Add an ``ansi`` extra to install ``erbsland-sphinx-ansi``. Version + 1.2.4 or later is required. +- Explicitly list ``docutils`` as a dependency. 0.19 (2026-02-20) diff --git a/setup.py b/setup.py index 9693c2d..894d932 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def read_version_number(): # method is invoked. So we now have to test side effects. # That's OK, and the same side effect test works on older # versions as well. - "erbsland-sphinx-ansi; python_version >= '3.10'", + "erbsland-sphinx-ansi >= 1.2.4", ] setup( @@ -84,8 +84,6 @@ def read_version_number(): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3 :: Only', - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -103,13 +101,17 @@ def read_version_number(): package_dir={'': 'src'}, include_package_data=True, install_requires=[ - 'Sphinx>=5.0.0', + 'Sphinx >= 5.0.0', + 'docutils', ], extras_require={ 'test': tests_require, 'docs': [ 'furo', ], + 'ansi': [ + "erbsland-sphinx-ansi >= 1.2.4", + ], }, - python_requires=">=3.8", + python_requires=">=3.10", ) diff --git a/src/sphinxcontrib/programoutput/tests/test_directive.py b/src/sphinxcontrib/programoutput/tests/test_directive.py index cd6aa48..4d5da3c 100644 --- a/src/sphinxcontrib/programoutput/tests/test_directive.py +++ b/src/sphinxcontrib/programoutput/tests/test_directive.py @@ -71,9 +71,19 @@ class TestDirective(AppMixin, # pylint:disable=too-many-public-methods def assert_output(self, doctree, output, **kwargs): __tracebackhide__ = True - literal = doctree.next_node(literal_block) - self.assertTrue(literal) - self.assertEqual(literal.astext(), output) + # Sometime around 1.2.4, erpsland-sphinx-ansi changed from being a + # ``literal_block`` to being a ``container``, and its astext() + # stopped being the + # See + # https://github.com/erbsland-dev/erbsland-sphinx-ansi + # /commit/f9b2481b65ac4df208bc0dc47b433ecefbbb0701 + # #diff-501301f0162ce64234424c0fa1e1b56724e0d526c3d364d9db3ddc19931f2a27 + using_ansi = kwargs.get('ansi') + literal = doctree.next_node(literal_block + if not using_ansi + else container) + self.assertTrue(literal, (literal, output)) + self.assertEqual(literal.astext() if not using_ansi else literal.rawsource, output) if 'caption' in kwargs: caption_node = doctree.next_node(caption) @@ -106,7 +116,6 @@ def test_simple(self): self.assert_output(self.doctree, 'eggs') self.assert_cache(self.app, 'echo eggs', 'eggs') - @with_content("""\ .. program-output:: python -c 'print("spam with eggs")'""") def test_with_spaces(self): @@ -120,7 +129,6 @@ def test_with_spaces(self): self.assert_cache(self.app, sys.executable + ' -c \'print("spam with eggs")\'', 'spam with eggs') - @with_content("""\ .. program-output:: python -c 'import sys; sys.stderr.write("spam with eggs")' """) @@ -130,17 +138,6 @@ def test_standard_error(self): cmd = sys.executable + ' -c \'import sys; sys.stderr.write("spam with eggs")\'' self.assert_cache(self.app, cmd, output) - - @with_content("""\ - .. program-output:: python -V - :nostderr:""") - @unittest.skipIf(sys.version_info[0] > 2, - reason="Python 3 prints version to stdout, not stderr") - def test_standard_error_disabled(self): - self.assert_output(self.doctree, '') - self.assert_cache(self.app, sys.executable + ' -V', '', hide_standard_error=True) - - @with_content("""\ .. program-output:: python -c 'import os; print(os.getcwd())'""") def test_working_directory_defaults_to_srcdir(self): @@ -170,7 +167,6 @@ def test_working_directory_relative_to_document(self): self.assert_cache(self.app, sys.executable + " -c 'import os; print(os.getcwd())'", output, working_directory=str(contentdir)) - @with_content("""\ .. program-output:: echo "${PWD}" :shell: @@ -183,13 +179,11 @@ def test_working_directory_with_shell(self): self.assert_cache(self.app, 'echo "${PWD}"', output, use_shell=True, working_directory=str(contentdir)) - @with_content('.. program-output:: echo "${HOME}"') def test_no_expansion_without_shell(self): self.assert_output(self.doctree, '${HOME}') self.assert_cache(self.app, 'echo "${HOME}"', '${HOME}') - @with_content("""\ .. program-output:: echo "${HOME}" :shell:""") @@ -197,7 +191,6 @@ def test_expansion_with_shell(self): self.assert_output(self.doctree, os.environ['HOME']) self.assert_cache(self.app, 'echo "${HOME}"', os.environ['HOME'], use_shell=True) - @with_content("""\ .. program-output:: echo "spam with eggs" :prompt:""") @@ -207,7 +200,6 @@ def test_prompt(self): spam with eggs""") self.assert_cache(self.app, 'echo "spam with eggs"', 'spam with eggs') - @with_content('.. command-output:: echo "spam with eggs"') def test_command(self): self.assert_output(self.doctree, """\ @@ -215,7 +207,6 @@ def test_command(self): spam with eggs""") self.assert_cache(self.app, 'echo "spam with eggs"', 'spam with eggs') - @with_content('.. command-output:: echo spam', programoutput_prompt_template='>> {command}\n<< {output}') def test_command_non_default_prompt(self): @@ -256,7 +247,6 @@ def test_ellipsis_stop_only(self): self.assert_cache(self.app, sys.executable + ' -c \'print("spam\\nwith\\neggs")\'', 'spam\nwith\neggs') - @with_content("""\ .. program-output:: python -c 'print("spam\\nwith\\neggs")' :ellipsis: -2""") @@ -266,7 +256,6 @@ def test_ellipsis_negative_stop(self): sys.executable + """ -c 'print("spam\\nwith\\neggs")'""", 'spam\nwith\neggs') - @with_content("""\ .. program-output:: python -c 'print("spam\\nwith\\neggs")' :ellipsis: 1, 2""") @@ -276,7 +265,6 @@ def test_ellipsis_start_and_stop(self): sys.executable + """ -c 'print("spam\\nwith\\neggs")'""", 'spam\nwith\neggs') - @with_content("""\ .. program-output:: python -c 'print("spam\\nwith\\neggs")' :ellipsis: 1, -1""") @@ -286,7 +274,6 @@ def test_ellipsis_start_and_negative_stop(self): sys.executable + """ -c 'print("spam\\nwith\\neggs")'""", 'spam\nwith\neggs') - @with_content("""\ .. program-output:: python -c 'import sys; sys.exit(1)'""", ignore_warnings=False) @@ -301,8 +288,6 @@ def test_unexpected_return_code(self): parsed_command = (sys.executable, '-c', 'import sys; sys.exit(1)') self.assertIn(repr(parsed_command), repr(command)) - - @with_content("""\ .. program-output:: python -c 'import sys; sys.exit("some output")' :shell:""", @@ -318,7 +303,6 @@ def test_shell_with_unexpected_return_code(self): # Python 2 include the u'' prefix on the output string. self.assertEqual('some output', output) - @with_content("""\ .. program-output:: python -c 'import sys; print("foo"); sys.exit(1)' :returncode: 1""") @@ -397,7 +381,6 @@ def test_bytes_prompt_with_unicode_output(self): 'echo "U+2264 ≤ LESS-THAN OR EQUAL TO"', 'U+2264 ≤ LESS-THAN OR EQUAL TO') - @with_content("""\ .. program-output:: python -c 'print("U+2264 ≤ LESS-THAN OR EQUAL TO\\n≤ line2\\n≤ line3")' :ellipsis: 2 @@ -487,13 +470,11 @@ def test_use_ansi_missing_extension(self): .. program-output:: python -c 'print("\\x1b[31mspam\\x1b[0m")'""", programoutput_use_ansi=True, extensions=['sphinxcontrib.programoutput', 'erbsland.sphinx.ansi']) - @unittest.skipIf(sys.version_info[:2] < (3, 10), - "The extension is only available on 3.10+") def test_use_ansi_enabled_extension(self): with Patch('sphinxcontrib.programoutput.logger.warning') as patch_warning: doctree = self.doctree - self.assert_output(doctree, '\x1b[31mspam\x1b[0m') + self.assert_output(doctree, '\x1b[31mspam\x1b[0m', ansi=True) patch_warning.assert_not_called() self.assert_cache( self.app, diff --git a/tox.ini b/tox.ini index 6ac6556..76767fc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py38,py39,py310,py311,py312,py313,py314,pypy3,docs +envlist=py310,py311,py312,py313,py314,pypy3,docs [testenv] usedevelop = false