diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0b84a24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: monthly diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8b2b2f..e335df1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,41 +5,45 @@ on: [push, pull_request] env: PYTHONHASHSEED: 1042466059 ZOPE_INTERFACE_STRICT_IRO: 1 - PYTHONWARNINGS: "ignore:'U' mode is deprecated:DeprecationWarning::" + jobs: test: strategy: matrix: - python-version: [2.7, pypy2, pypy3, 3.6, 3.7, 3.8, 3.9] + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Pip cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('setup.*') }} - restore-keys: | - ${{ runner.os }}-pip- + cache: 'pip' + cache-dependency-path: 'setup.py' - name: Install dependencies run: | python -m pip install -U pip setuptools wheel - python -m pip install -U coverage pylint + python -m pip install -U coverage zope.testrunner python -m pip install -U -e ".[test]" - name: Test run: | - pylint -r no src/sphinxcontrib - coverage run -m unittest discover -s src + coverage run -m zope.testrunner --test-path=src + - name: Lint + if: matrix.python-version == '3.12' + run: | + python -m pip install -U pylint + pylint src - name: Submit to Coveralls - # This is a container action, which only runs on Linux. - uses: AndreMiras/coveralls-python-action@develop + uses: coverallsapp/github-action@v2 with: parallel: true @@ -48,6 +52,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true + uses: coverallsapp/github-action@v2 diff --git a/.pylintrc b/.pylintrc index 190b732..814d8a3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,72 @@ +[MASTER] +load-plugins=pylint.extensions.bad_builtin, + pylint.extensions.check_elif, + pylint.extensions.code_style, + pylint.extensions.dict_init_mutate, + pylint.extensions.docstyle, + pylint.extensions.dunder, + pylint.extensions.comparison_placement, + pylint.extensions.confusing_elif, + pylint.extensions.for_any_all, + pylint.extensions.consider_refactoring_into_while_condition, + pylint.extensions.mccabe, + pylint.extensions.eq_without_hash, + pylint.extensions.redefined_variable_type, + pylint.extensions.overlapping_exceptions, + pylint.extensions.docparams, + pylint.extensions.private_import, + pylint.extensions.set_membership, + pylint.extensions.typing, + +# magic_value wants you to not use arbitrary strings and numbers +# inline in the code. But it's overzealous and has way too many false +# positives. Trust people to do the most readable thing. +# pylint.extensions.magic_value + +# Empty comment would be good, except it detects blank lines within +# a single comment block. +# +# Those are often used to separate paragraphs, like here. +# pylint.extensions.empty_comment, + +# consider_ternary_expression is a nice check, but is also overzealous. +# Trust the human to do the readable thing. +# pylint.extensions.consider_ternary_expression, + +# redefined_loop_name tends to catch us with things like +# for name in (a, b, c): name = name + '_column' ... +# pylint.extensions.redefined_loop_name, + +# This wants you to turn ``x in (1, 2)`` into ``x in {1, 2}``. +# They both result in the LOAD_CONST bytecode, one a tuple one a +# frozenset. In theory a set lookup using hashing is faster than +# a linear scan of a tuple; but if the tuple is small, it can often +# actually be faster to scan the tuple. +# pylint.extensions.set_membership, + +# Fix zope.cachedescriptors.property.Lazy; the property-classes doesn't seem to +# do anything. +# https://stackoverflow.com/questions/51160955/pylint-how-to-specify-a-self-defined-property-decorator-with-property-classes +# For releases prior to 2.14.2, this needs to be a one-line, quoted string. After that, +# a multi-line string. +# - Make zope.cachedescriptors.property.Lazy look like a property; +# fixes pylint thinking it is a method. +# - Run in Pure Python mode (ignore C extensions that respect this); +# fixes some issues with zope.interface, like IFoo.providedby(ob) +# claiming not to have the right number of parameters...except no, it does not. +init-hook = + import astroid.bases + astroid.bases.POSSIBLE_PROPERTIES.add('Lazy') + astroid.bases.POSSIBLE_PROPERTIES.add('LazyOnClass') + astroid.bases.POSSIBLE_PROPERTIES.add('readproperty') + astroid.bases.POSSIBLE_PROPERTIES.add('non_overridable') + import os + os.environ['PURE_PYTHON'] = ("1") + # Ending on a quoted string + # breaks pylint 2.14.5 (it strips the trailing quote. This is + # probably because it tries to handle one-line quoted strings as well as multi-blocks). + # The parens around it fix the issue. + [MESSAGES CONTROL] @@ -10,7 +79,8 @@ # comments at the end of the line does the same thing (though Py3 supports # mixing) -# invalid-name, ; Things like loadBlob get flagged + +# invalid-name, ; We get lots of these, especially in scripts. should fix many of them # protected-access, ; We have many cases of this; legit ones need to be examinid and commented, then this removed # no-self-use, ; common in superclasses with extension points # too-few-public-methods, ; Exception and marker classes get tagged with this @@ -27,58 +97,124 @@ # see https://github.com/PyCQA/pylint/issues/846 # useless-suppression: the only way to avoid repeating it for specific statements everywhere that we # do Py2/Py3 stuff is to put it here. Sadly this means that we might get better but not realize it. -# bad-option-value: Pylint on Python 3 understands and checks for somethings Pylint on Python 2 -# does not. -disable=missing-docstring, - invalid-name, +# duplicate-code: Yeah, the compatibility ssl modules are much the same +# In pylint 1.8.0, inconsistent-return-statements are created for the wrong reasons. +# This code raises it, even though there is only one return (the implicit ``return None`` is presumably +# what triggers it): +# def foo(): +# if baz: +# return 1 +# In Pylint 2dev1, needed for Python 3.7, we get spurious "useless return" errors: +# @property +# def foo(self): +# return None # generates useless-return +# Pylint 2.4 adds import-outside-toplevel. But we do that a lot to defer imports because of patching. +# Pylint 2.4 adds self-assigning-variable. But we do *that* to avoid unused-import when we +# "export" the variable and dont have a __all__. +# Pylint 2.6+ adds some python-3-only things that dont apply: raise-missing-from, super-with-arguments, consider-using-f-string, redundant-u-string-prefix +# cyclic import is added because it pylint is spuriously detecting that +# consider-using-assignment-expr wants you to transform things like: +# foo = get_foo() +# if foo: ... +# +# Into ``if (foo := get_foo()):`` +# But there are a *lot* of those. Trust people to do the right, most +# readable, thing +# +# docstring-first-line-empty: That's actually our standard, based on Django. +# XXX: unclear on the docstring warnings, missing-type-doc, missing-param-doc, +# differing-param-doc, differing-type-doc (are the last two replacements for the first two?) +# +# They should be addressed, in general they are a good thing, but sometimes they are +# unnecessary. +disable=wrong-import-position, wrong-import-order, + missing-docstring, + ungrouped-imports, + invalid-name, + too-few-public-methods, + global-statement, locally-disabled, - no-self-use, - bad-option-value, + too-many-arguments, + useless-suppression, + duplicate-code, + useless-object-inheritance, + import-outside-toplevel, + self-assigning-variable, + consider-using-f-string, + consider-using-assignment-expr, + use-dict-literal, + missing-type-doc, + missing-param-doc, + differing-param-doc, + differing-type-doc, + compare-to-zero, + docstring-first-line-empty, +enable=consider-using-augmented-assign [FORMAT] -# duplicated from setup.cfg max-line-length=100 +max-module-lines=1100 [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. #notes=FIXME,XXX,TODO -# Disable that, we don't want them in the report (???) +# Disable that, we don't want them to fail the lint CI job. notes= [VARIABLES] dummy-variables-rgx=_.* +init-import=true [TYPECHECK] # List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular +# system, and so shouldnt trigger E1101 when accessed. Python regular # expressions are accepted. -# gevent: this is helpful for py3/py2 code. -generated-members=env,build +generated-members=REQUEST,acl_users,aq_parent,providedBy + + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +# XXX: deprecated in 2.14; replaced with ignored-checks-for-mixins. +# The defaults for that value seem to be what we want +#ignore-mixin-members=yes # List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work +# (useful for classes with attributes dynamically set). This can work # with qualified names. +#ignored-classes=SSLContext, SSLSocket, greenlet, Greenlet, parent, dead -ignored-classes=Sphinx,doctree # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. -#ignored-modules=gevent._corecffi +#ignored-modules=gevent._corecffi,gevent.os,os,greenlet,threading,gevent.libev.corecffi,gevent.socket,gevent.core,gevent.testing.support +ignored-modules=psycopg2.errors [DESIGN] max-attributes=12 -max-locals=16 -max-public-methods=40 +max-parents=10 +# Bump complexity up one. +max-complexity=11 [BASIC] -bad-functions=input # Prospector turns ot unsafe-load-any-extension by default, but # pylint leaves it off. This is the proximal cause of the # undefined-all-variable crash. -#unsafe-load-any-extension = no +unsafe-load-any-extension = yes +# This does not seem to work, hence the init-hook +property-classes=zope.cachedescriptors.property.Lazy,zope.cachedescriptors.property.Cached + +[CLASSES] +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. + + + +# Local Variables: +# mode: conf +# End: diff --git a/CHANGES.rst b/CHANGES.rst index e8e3be1..86afa9d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,12 @@ 0.18 (unreleased) ================= -- Nothing changed yet. +- Drop support for Python < 3.8. This means Python 2 is no longer + supported. +- Drop setuptools dependency. Instead, use a native ``pkgutil`` + namespace package. +- Now requires docutils >= 0.18.1. This effectively requires Sphinx + versions newer than 5.0. 0.17 (2021-03-31) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..96625b5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "wheel", + "setuptools", +] diff --git a/setup.cfg b/setup.cfg index 9ca2d42..05b2903 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,2 @@ -[aliases] -release = egg_info -RDb '' - -[upload_docs] -upload_dir = build/sphinx/html - -[bdist_wheel] -universal = 1 - [zest.releaser] python-file-with-version = src/sphinxcontrib/programoutput/__init__.py diff --git a/setup.py b/setup.py index bf2fec8..15f4d6e 100644 --- a/setup.py +++ b/setup.py @@ -25,19 +25,21 @@ import os import re -from setuptools import setup, find_packages +from setuptools import setup +from setuptools import find_namespace_packages def read_desc(): - with open('README.rst') as stream: + with open('README.rst', encoding='utf-8') as stream: readme = stream.read() - with open('CHANGES.rst') as stream: + with open('CHANGES.rst', encoding='utf-8') as stream: changes = stream.read() return readme + '\n\n' + changes def read_version_number(): VERSION_PATTERN = re.compile(r"__version__ = '([^']+)'") - with open(os.path.join('src', 'sphinxcontrib', 'programoutput', '__init__.py')) as stream: + with open(os.path.join('src', 'sphinxcontrib', 'programoutput', '__init__.py'), + encoding='utf-8') as stream: for line in stream: match = VERSION_PATTERN.search(line) if match: @@ -46,6 +48,10 @@ def read_version_number(): raise ValueError('Could not extract version number') tests_require = [ + # Sphinx 8.1 stopped raising SphinxWarning when the ``logger.warning`` + # 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. ] setup( @@ -56,7 +62,7 @@ def read_version_number(): author='Sebastian Wiesner', author_email='lunaryorn@gmail.com', maintainer="Jason Madden", - maintainer_email='jason@nextthought.com', + maintainer_email='jason@seecoresoftware.com', description='Sphinx extension to include program output', long_description=read_desc(), keywords="sphinx cli command output program example", @@ -67,13 +73,13 @@ def read_version_number(): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", + '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", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", 'Topic :: Documentation', @@ -82,17 +88,14 @@ def read_version_number(): 'Framework :: Sphinx :: Extension', ], platforms='any', - packages=find_packages('src'), + packages=find_namespace_packages('src'), package_dir={'': 'src'}, - namespace_packages=['sphinxcontrib'], include_package_data=True, install_requires=[ - 'Sphinx>=1.7.0', + 'Sphinx>=5.0.0', ], - tests_require=tests_require, extras_require={ 'test': tests_require, }, - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", - test_suite='sphinxcontrib.programoutput.tests', + python_requires=">=3.8", ) diff --git a/src/sphinxcontrib/__init__.py b/src/sphinxcontrib/__init__.py index 2cdb0e4..69e3be5 100644 --- a/src/sphinxcontrib/__init__.py +++ b/src/sphinxcontrib/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/src/sphinxcontrib/programoutput/__init__.py b/src/sphinxcontrib/programoutput/__init__.py index deafb22..e60046c 100644 --- a/src/sphinxcontrib/programoutput/__init__.py +++ b/src/sphinxcontrib/programoutput/__init__.py @@ -266,7 +266,7 @@ def run_programs(app, doctree): cache = app.env.programoutput_cache - for node in doctree.traverse(program_output): + for node in doctree.findall(program_output): command = Command.from_program_output_node(node) try: returncode, output = cache[command] @@ -293,8 +293,8 @@ def run_programs(app, doctree): if 'strip_lines' in node: start, stop = node['strip_lines'] lines = output.splitlines() - lines[start:stop] = [u'...'] - output = u'\n'.join(lines) + lines[start:stop] = ['...'] + output = '\n'.join(lines) if node['show_prompt']: # The command in the node is also guaranteed to be diff --git a/src/sphinxcontrib/programoutput/tests/test_directive.py b/src/sphinxcontrib/programoutput/tests/test_directive.py index 7acacd0..4933728 100644 --- a/src/sphinxcontrib/programoutput/tests/test_directive.py +++ b/src/sphinxcontrib/programoutput/tests/test_directive.py @@ -29,8 +29,9 @@ import os import sys import unittest +from unittest.mock import patch as Patch + -from sphinx.errors import SphinxWarning from docutils.nodes import caption, container, literal_block, system_message from sphinxcontrib.programoutput import Command @@ -288,13 +289,16 @@ def test_ellipsis_start_and_negative_stop(self): .. program-output:: python -c 'import sys; sys.exit(1)'""", ignore_warnings=False) def test_unexpected_return_code(self): - with self.assertRaises(SphinxWarning) as excinfo: + with Patch('sphinxcontrib.programoutput.logger.warning') as patch_warning: self.app.build() - self.assertIn('Unexpected return code 1 from command', - excinfo.exception.args[0]) + patch_warning.assert_called_once() + msg, returncode, command, _output = patch_warning.call_args.args + self.assertEqual(returncode, 1) + self.assertIn('Unexpected return code %s from command', + msg) parsed_command = (sys.executable, '-c', 'import sys; sys.exit(1)') - self.assertIn(repr(parsed_command), - excinfo.exception.args[0]) + self.assertIn(repr(parsed_command), repr(command)) + @with_content("""\ @@ -302,18 +306,16 @@ def test_unexpected_return_code(self): :shell:""", ignore_warnings=False) def test_shell_with_unexpected_return_code(self): - with self.assertRaises(SphinxWarning) as excinfo: + with Patch('sphinxcontrib.programoutput.logger.warning') as patch_warning: self.app.build() - msg = excinfo.exception.args[0] - self.assertIn('Unexpected return code 1 from command', - msg) - self.assertIn("import sys; sys.exit", + patch_warning.assert_called_once() + msg, returncode, command, output = patch_warning.call_args.args + self.assertEqual(returncode, 1) + self.assertIn('Unexpected return code %s from command', msg) # Python 2 include the u'' prefix on the output string. - self.assertIn('(output=', msg) - self.assertIn('\'some output\')', msg) - self.assertIn('hide_standard_error=', msg) - self.assertIn('working_directory=', msg) + self.assertEqual('some output', output) + @with_content("""\ .. program-output:: python -c 'import sys; print("foo"); sys.exit(1)' @@ -343,7 +345,7 @@ def test_non_existing_executable(self): # check that a proper error message appears in the document message = self.doctree.next_node(system_message) self.assertTrue(message) - srcfile = os.path.join(self.srcdir, 'content', 'doc.rst') + srcfile = os.path.realpath(os.path.join(self.srcdir, 'content', 'doc.rst')) self.assertEqual(message['source'], srcfile) self.assertEqual(message['line'], 5) @@ -361,7 +363,7 @@ def test_non_existing_working_directory(self): srcdir = self.srcdir message = doctree.next_node(system_message) self.assertTrue(message) - srcfile = os.path.join(srcdir, 'content', 'doc.rst') + srcfile = os.path.realpath(os.path.join(srcdir, 'content', 'doc.rst')) self.assertEqual(message['source'], srcfile) self.assertEqual(message['line'], 5) diff --git a/tox.ini b/tox.ini index b047476..05f3b75 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,17 @@ [tox] -envlist=py27,py27-old,py36,py37,py38,py39,pypy,doc,coverage +envlist=py38,py39,310,311,312,313,pypy3,doc [testenv] -usedevelop = true +usedevelop = false extras = test deps = - pylint coverage - old: Sphinx == 1.7.0 + zope.testrunner commands = - coverage run -p -m unittest discover -s src - pylint -r no src/sphinxcontrib + coverage run -p -m zope.testrunner --test-path=src passenv = HOME -[testenv:coverage] -commands = - coverage combine - coverage html -i - coverage report -i --fail-under=100 -depends = py27, py27-old, py36, py37, py38, py39, pypy -parallel_show_output = true - [testenv:doc] commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees doc {envtmpdir}/linkcheck