diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aac1b7f..3c0992e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,10 +50,11 @@ jobs: run: | python -m pip install -U pip python -m pip install -U setuptools wheel - python -m pip install -U coverage coveralls + python -m pip install -U coverage coveralls pytest + python -m pip install -e . - name: Run tests - run: coverage run testsuite.py + run: coverage run -m pytest - name: Check test coverage run: | diff --git a/MANIFEST.in b/MANIFEST.in index 9ab8716..b7fa563 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,17 +1,14 @@ +include *.mk include *.rst +include *.yml +include LICENSE include Makefile include .coveragerc include .gitignore include tox.ini -include testsuite.py +include pytest.ini +include conftest.py include tests.py include tests/*.txt include tests/sample-tree/snake.egg-info recursive-include tests/sample-tree *.py *.zip - -# added by check_manifest.py -include *.yml -include LICENSE - -# added by check_manifest.py -include *.mk diff --git a/README.rst b/README.rst index 15e0b08..a25c987 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Licence: MIT (https://mit-license.org/) |buildstatus|_ |appveyor|_ |coverage|_ -.. |buildstatus| image:: https://github.com/mgedmin/findimports/workflows/build/badge.svg?branch=master +.. |buildstatus| image:: https://github.com/mgedmin/findimports/actions/workflows/build.yml/badge.svg?branch=master .. _buildstatus: https://github.com/mgedmin/findimports/actions .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/mgedmin/findimports?branch=master&svg=true diff --git a/appveyor.yml b/appveyor.yml index 101b83f..5f3982a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,8 +24,9 @@ install: - ps: if (-not (Test-Path $env:PYTHON)) { throw "No $env:PYTHON" } - "set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - python --version + - pip install tox build: off test_script: - - python testsuite.py + - tox -e py diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..17a6422 --- /dev/null +++ b/conftest.py @@ -0,0 +1,69 @@ +import linecache +import os +import pathlib +import sys + +import pytest + + +here = pathlib.Path(__file__).resolve().parent +sample_tree = pathlib.Path(here, 'tests/sample-tree') + + +class RedirectToStdout(object): + """A file-like object that prints to sys.stdout + + A reason to use sys.stderr = RedirectToStdout() instead of assigning + sys.stderr = sys.stdout is when sys.stdout is later reassigned to a + different object (e.g. the StringIO that doctests use) and you want + sys.stderr to always refer to whatever sys.stdout is printing to. + + Not all file methods are implemented, just the ones that were actually + needed. + """ + + def write(self, msg): + sys.stdout.write(msg) + + +class RewriteBackslashes(object): + """A file-like object that normalizes path separators. + + pytest doesn't allow custom doctest checkers, so I have to do terrible + crimes like this class. + """ + + def __init__(self): + self.real_stdout = sys.stdout + + def write(self, msg): + self.real_stdout.write(msg.replace(os.path.sep, '/')) + + +def create_tree(files): + f = None + try: + for line in files.splitlines(): + if line.startswith('-- ') and line.endswith(' --'): + pathname = pathlib.Path(line.strip('- ')) + pathname.parent.mkdir(parents=True, exist_ok=True) + if f is not None: + f.close() + f = pathname.open('w') + elif f is not None: + print(line, file=f) + finally: + if f is not None: + f.close() + + +@pytest.fixture(autouse=True) +def doctest_setup(doctest_namespace, tmp_path, monkeypatch): + doctest_namespace['create_tree'] = create_tree + doctest_namespace['sample_tree'] = str(sample_tree) + monkeypatch.syspath_prepend(str(sample_tree.joinpath('zippedmodules.zip'))) + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(sys, 'stderr', RedirectToStdout()) + monkeypatch.setattr(sys, 'stdout', RewriteBackslashes()) + yield + linecache.clearcache() diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b25e019 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests.py tests/*.txt +addopts = -ra -s diff --git a/tests.py b/tests.py index de925bc..8bfe7f3 100644 --- a/tests.py +++ b/tests.py @@ -1,15 +1,10 @@ import os import unittest +from io import StringIO import findimports -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - - here = os.path.dirname(__file__) diff --git a/tests/cyclic-imports.txt b/tests/cyclic-imports.txt index ccdd6fb..2ed5e8c 100644 --- a/tests/cyclic-imports.txt +++ b/tests/cyclic-imports.txt @@ -1,7 +1,5 @@ Test cyclic imports - >>> from testsuite import create_tree - >>> create_tree(''' ... -- foo/__init__.py -- ... diff --git a/tests/import-statements.txt b/tests/import-statements.txt index 81e6786..e2ddea5 100644 --- a/tests/import-statements.txt +++ b/tests/import-statements.txt @@ -8,10 +8,10 @@ All kinds of import statements are handled >>> with open('marmalade.py', 'w') as f: _ = f.write(''' ... import sys ... import os.path - ... import email.Message as EM + ... import email.message as EM ... from . import foobar - ... from cStringIO import StringIO - ... from cPickle import dumps as D + ... from io import StringIO + ... from pickle import dumps as D ... from sys import (argv, ... exc_info as EI, # complexity is fun ... exit) @@ -23,10 +23,10 @@ All kinds of import statements are handled ... print(imp) ImportInfo('sys', 'marmalade.py', 2, None) ImportInfo('os.path', 'marmalade.py', 3, None) - ImportInfo('email.Message', 'marmalade.py', 4, None) + ImportInfo('email.message', 'marmalade.py', 4, None) ImportInfo('foobar', 'marmalade.py', 5, 1) - ImportInfo('cStringIO.StringIO', 'marmalade.py', 6, 0) - ImportInfo('cPickle.dumps', 'marmalade.py', 7, 0) + ImportInfo('io.StringIO', 'marmalade.py', 6, 0) + ImportInfo('pickle.dumps', 'marmalade.py', 7, 0) ImportInfo('sys.argv', 'marmalade.py', 8, 0) ImportInfo('sys.exc_info', 'marmalade.py', 9, 0) ImportInfo('sys.exit', 'marmalade.py', 10, 0) diff --git a/tests/package-graph.txt b/tests/package-graph.txt index 9128575..66dc775 100644 --- a/tests/package-graph.txt +++ b/tests/package-graph.txt @@ -1,8 +1,6 @@ Test collapsing to packages =========================== - >>> from testsuite import create_tree - >>> create_tree(''' ... -- foo/__init__.py -- ... diff --git a/tests/relative-imports.txt b/tests/relative-imports.txt index e6726ac..7b63df9 100644 --- a/tests/relative-imports.txt +++ b/tests/relative-imports.txt @@ -1,8 +1,6 @@ Test handling of relative imports ================================= - >>> from testsuite import create_tree - >>> create_tree(''' ... -- foo/__init__.py -- ... diff --git a/tests/test-packages.txt b/tests/test-packages.txt index a2bad2c..7c3a3e0 100644 --- a/tests/test-packages.txt +++ b/tests/test-packages.txt @@ -12,8 +12,6 @@ Test handling of test packages >>> g.removeTestPackage('tests') 'tests' - >>> from testsuite import create_tree - >>> create_tree(''' ... -- foo/__init__.py -- ... diff --git a/tests/unknown-modules.txt b/tests/unknown-modules.txt index 3ee0858..81232a4 100644 --- a/tests/unknown-modules.txt +++ b/tests/unknown-modules.txt @@ -8,7 +8,7 @@ We print warnings when we cannot find a module/package >>> with open('marmalade.py', 'w') as f: _ = f.write(''' ... import sys ... import os.path - ... import email.Message as EM + ... import email.message as EM ... from . import foobar ... from io import StringIO ... from pickle import dumps as D @@ -17,6 +17,7 @@ We print warnings when we cannot find a module/package ... exit) ... from email import * ... import imaginary.package + ... import curses.panel ... ''') >>> with open('foo.py', 'w') as f: _ = f.write(''' diff --git a/testsuite.py b/testsuite.py deleted file mode 100755 index 474f2e4..0000000 --- a/testsuite.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/python - -import doctest -import glob -import linecache -import os -import re -import shutil -import sys -import tempfile -import unittest - - -class RedirectToStdout(object): - """A file-like object that prints to sys.stdout - - A reason to use sys.stderr = RedirectToStdout() instead of assigning - sys.stderr = sys.stdout is when sys.stdout is later reassigned to a - different object (e.g. the StringIO that doctests use) and you want - sys.stderr to always refer to whatever sys.stdout is printing to. - - Not all file methods are implemented, just the ones that were actually - needed. - """ - - def write(self, msg): - sys.stdout.write(msg) - - -class Checker(doctest.OutputChecker): - """Doctest output checker for normalizing Windows pathname differences.""" - - def check_output(self, want, got, optionflags): - want = re.sub("sample-tree/[^:]*", - lambda m: m.group(0).replace("/", os.path.sep), - want) - return doctest.OutputChecker.check_output(self, want, got, optionflags) - - -def setUp(test): - test.old_path = list(sys.path) - sample_tree = os.path.abspath(os.path.join('tests', 'sample-tree')) - sys.path.append(os.path.join(sample_tree, 'zippedmodules.zip')) - test.old_stderr = sys.stderr - sys.stderr = RedirectToStdout() - test.old_cwd = os.getcwd() - test.tempdir = tempfile.mkdtemp(prefix='test-findimports-') - os.chdir(test.tempdir) - - -def tearDown(test): - sys.path[:] = test.old_path - sys.stderr = test.old_stderr - os.chdir(test.old_cwd) - shutil.rmtree(test.tempdir) - linecache.clearcache() - - -def create_tree(files): - f = None - for line in files.splitlines(): - if line.startswith('-- ') and line.endswith(' --'): - filename = line.strip('- ') - if not os.path.isdir(os.path.dirname(filename)): - os.makedirs(os.path.dirname(filename)) - if f is not None: - f.close() - f = open(filename, 'w') - elif f is not None: - f.write(line + '\n') - if f is not None: - f.close() - - -def additional_tests(): # hook for setuptools setup.py test - # paths relative to __file__ don't work if you run 'figleaf testsuite.py' - # so we have to use paths relative to os.getcwd() - sample_tree = os.path.abspath(os.path.join('tests', 'sample-tree')) - globs = dict(sample_tree=sample_tree) - doctests = sorted(glob.glob('tests/*.txt')) - return unittest.TestSuite([ - unittest.defaultTestLoader.loadTestsFromName('tests'), - doctest.DocFileSuite(setUp=setUp, tearDown=tearDown, - module_relative=False, globs=globs, - checker=Checker(), - optionflags=doctest.REPORT_NDIFF, - *doctests), - ]) - - -def main(): - unittest.main(defaultTest='additional_tests') - - -if __name__ == '__main__': - main() diff --git a/tox.ini b/tox.ini index f06bbe4..2988ef8 100644 --- a/tox.ini +++ b/tox.ini @@ -9,30 +9,38 @@ envlist = py313 pypy3 flake8 + ruff isort check-python-versions [testenv] deps = + pytest commands = - python testsuite.py {posargs} + pytest {posargs} [testenv:coverage] deps = + pytest coverage commands = - coverage run testsuite.py + coverage run -m pytest coverage report -m --fail-under=100 [testenv:flake8] deps = flake8 skip_install = true -commands = flake8 findimports.py setup.py tests.py testsuite.py +commands = flake8 findimports.py setup.py tests.py conftest.py + +[testenv:ruff] +deps = ruff +skip_install = true +commands = ruff check findimports.py setup.py tests.py conftest.py [testenv:isort] deps = isort skip_install = true -commands = isort {posargs: -c --diff findimports.py setup.py tests.py testsuite.py} +commands = isort {posargs: -c --diff findimports.py setup.py tests.py conftest.py} [testenv:check-manifest] deps = check-manifest