Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ jobs:
strategy:
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
Expand All @@ -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
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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",
)
47 changes: 14 additions & 33 deletions src/sphinxcontrib/programoutput/tests/test_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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")'
""")
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -183,21 +179,18 @@ 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:""")
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:""")
Expand All @@ -207,15 +200,13 @@ 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, """\
$ echo "spam with eggs"
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):
Expand Down Expand Up @@ -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""")
Expand All @@ -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""")
Expand All @@ -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""")
Expand All @@ -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)
Expand All @@ -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:""",
Expand All @@ -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""")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down