Tool to check the completeness of MANIFEST.in for Python packages https://pypi.org/p/check-manifest
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1795 lines
66 KiB

import codecs
import locale
import os
import posixpath
import shutil
import subprocess
import sys
import tarfile
import tempfile
import textwrap
import unittest
import zipfile
from contextlib import closing
from functools import partial
from io import BytesIO, StringIO
from typing import Optional
from xml.etree import ElementTree as ET
if sys.version_info >= (3, 8):
from unittest import mock
else:
# unittest.mock in 3.7 is too old to support
# all the features used in the test suite
import mock
from check_manifest import rmtree
CAN_SKIP_TESTS = os.getenv('SKIP_NO_TESTS', '') == ''
try:
codecs.lookup('oem')
except LookupError:
HAS_OEM_CODEC = False
else:
# Python >= 3.6 on Windows
HAS_OEM_CODEC = True
class MockUI:
def __init__(self, verbosity=1):
self.verbosity = verbosity
self.warnings = []
self.errors = []
def info(self, message):
pass
def info_begin(self, message):
pass
def info_cont(self, message):
pass
def info_end(self, message):
pass
def warning(self, message):
self.warnings.append(message)
def error(self, message):
self.errors.append(message)
class Tests(unittest.TestCase):
def setUp(self):
self.ui = MockUI()
def make_temp_dir(self):
tmpdir = tempfile.mkdtemp(prefix='test-', suffix='-check-manifest')
self.addCleanup(rmtree, tmpdir)
return tmpdir
def create_file(self, filename, contents):
with open(filename, 'w') as f:
f.write(contents)
def create_zip_file(self, filename, filenames):
with closing(zipfile.ZipFile(filename, 'w')) as zf:
for fn in filenames:
zf.writestr(fn, '')
def create_tar_file(self, filename, filenames):
with closing(tarfile.TarFile(filename, 'w')) as tf:
for fn in filenames:
tf.addfile(tarfile.TarInfo(fn), BytesIO())
def test_run_success(self):
from check_manifest import run
self.assertEqual(run(["true"]), "")
def test_run_failure(self):
from check_manifest import CommandFailed, run
with self.assertRaises(CommandFailed) as cm:
run(["false"])
self.assertEqual(str(cm.exception),
"['false'] failed (status 1):\n")
def test_run_no_such_program(self):
from check_manifest import Failure, run
with self.assertRaises(Failure) as cm:
run(["there-is-really-no-such-program"])
# Linux says "[Errno 2] No such file or directory"
# Windows says "[Error 2] The system cannot find the file specified"
# but on 3.x it's "[WinErr 2] The system cannot find the file specified"
should_start_with = "could not run ['there-is-really-no-such-program']:"
self.assertTrue(
str(cm.exception).startswith(should_start_with),
'\n%r does not start with\n%r' % (str(cm.exception),
should_start_with))
def test_mkdtemp_readonly_files(self):
from check_manifest import mkdtemp
with mkdtemp(hint='-test-readonly') as d:
fn = os.path.join(d, 'file.txt')
with open(fn, 'w'):
pass
os.chmod(fn, 0o444) # readonly
assert not os.path.exists(d)
@unittest.skipIf(sys.platform == 'win32',
"No POSIX-like unreadable directories on Windows")
def test_rmtree_unreadable_directories(self):
d = self.make_temp_dir()
sd = os.path.join(d, 'subdir')
os.mkdir(sd)
os.chmod(sd, 0) # a bad mode for a directory, oops
# The onerror API of shutil.rmtree doesn't let us recover from
# os.listdir() failures.
with self.assertRaises(OSError):
rmtree(sd)
os.chmod(sd, 0o755) # so we can clean up
def test_rmtree_readonly_directories(self):
d = self.make_temp_dir()
sd = os.path.join(d, 'subdir')
fn = os.path.join(sd, 'file.txt')
os.mkdir(sd)
open(fn, 'w').close()
os.chmod(sd, 0o444) # a bad mode for a directory, oops
rmtree(sd)
assert not os.path.exists(sd)
def test_rmtree_readonly_directories_and_files(self):
d = self.make_temp_dir()
sd = os.path.join(d, 'subdir')
fn = os.path.join(sd, 'file.txt')
os.mkdir(sd)
open(fn, 'w').close()
os.chmod(fn, 0o444) # readonly
os.chmod(sd, 0o444) # a bad mode for a directory, oops
rmtree(sd)
assert not os.path.exists(sd)
def test_copy_files(self):
from check_manifest import copy_files
actions = []
n = os.path.normpath
with mock.patch('os.path.isdir', lambda d: d in ('b', n('/dest/dir'))):
with mock.patch('os.makedirs',
lambda d: actions.append('makedirs %s' % d)):
with mock.patch('os.mkdir',
lambda d: actions.append('mkdir %s' % d)):
with mock.patch('shutil.copy2',
lambda s, d: actions.append(f'cp {s} {d}')):
copy_files(['a', 'b', n('c/d/e')], n('/dest/dir'))
self.assertEqual(
actions,
[
'cp a %s' % n('/dest/dir/a'),
'mkdir %s' % n('/dest/dir/b'),
'makedirs %s' % n('/dest/dir/c/d'),
'cp %s %s' % (n('c/d/e'), n('/dest/dir/c/d/e')),
])
def test_get_one_file_in(self):
from check_manifest import get_one_file_in
with mock.patch('os.listdir', lambda dir: ['a']):
self.assertEqual(get_one_file_in(os.path.normpath('/some/dir')),
os.path.normpath('/some/dir/a'))
def test_get_one_file_in_empty_directory(self):
from check_manifest import Failure, get_one_file_in
with mock.patch('os.listdir', lambda dir: []):
with self.assertRaises(Failure) as cm:
get_one_file_in('/some/dir')
self.assertEqual(str(cm.exception),
"No files found in /some/dir")
def test_get_one_file_in_too_many(self):
from check_manifest import Failure, get_one_file_in
with mock.patch('os.listdir', lambda dir: ['b', 'a']):
with self.assertRaises(Failure) as cm:
get_one_file_in('/some/dir')
self.assertEqual(str(cm.exception),
"More than one file exists in /some/dir:\na\nb")
def test_unicodify(self):
from check_manifest import unicodify
nonascii = "\u00E9.txt"
self.assertEqual(unicodify(nonascii), nonascii)
self.assertEqual(
unicodify(nonascii.encode(locale.getpreferredencoding())),
nonascii)
def test_get_archive_file_list_unrecognized_archive(self):
from check_manifest import Failure, get_archive_file_list
with self.assertRaises(Failure) as cm:
get_archive_file_list('/path/to/archive.rar')
self.assertEqual(str(cm.exception),
'Unrecognized archive type: archive.rar')
def test_get_archive_file_list_zip(self):
from check_manifest import get_archive_file_list
filename = os.path.join(self.make_temp_dir(), 'archive.zip')
self.create_zip_file(filename, ['a', 'b/c'])
self.assertEqual(get_archive_file_list(filename),
['a', 'b/c'])
def test_get_archive_file_list_zip_nonascii(self):
from check_manifest import get_archive_file_list
filename = os.path.join(self.make_temp_dir(), 'archive.zip')
nonascii = "\u00E9.txt"
self.create_zip_file(filename, [nonascii])
self.assertEqual(get_archive_file_list(filename),
[nonascii])
def test_get_archive_file_list_tar(self):
from check_manifest import get_archive_file_list
filename = os.path.join(self.make_temp_dir(), 'archive.tar')
self.create_tar_file(filename, ['a', 'b/c'])
self.assertEqual(get_archive_file_list(filename),
['a', 'b/c'])
def test_get_archive_file_list_tar_nonascii(self):
from check_manifest import get_archive_file_list
filename = os.path.join(self.make_temp_dir(), 'archive.tar')
nonascii = "\u00E9.txt"
self.create_tar_file(filename, [nonascii])
self.assertEqual(get_archive_file_list(filename),
[nonascii])
def test_format_list(self):
from check_manifest import format_list
self.assertEqual(format_list([]), "")
self.assertEqual(format_list(['a']), " a")
self.assertEqual(format_list(['a', 'b']), " a\n b")
def test_format_missing(self):
from check_manifest import format_missing
self.assertEqual(
format_missing(set(), set(), "1st", "2nd"),
"")
self.assertEqual(
format_missing({"c"}, {"a"}, "1st", "2nd"),
"missing from 1st:\n"
" c\n"
"missing from 2nd:\n"
" a")
def test_strip_toplevel_name_empty_list(self):
from check_manifest import strip_toplevel_name
self.assertEqual(strip_toplevel_name([]), [])
def test_strip_toplevel_name_no_common_prefix(self):
from check_manifest import Failure, strip_toplevel_name
self.assertRaises(Failure, strip_toplevel_name, ["a/b", "c/d"])
def test_detect_vcs_no_vcs(self):
from check_manifest import Failure, detect_vcs
ui = MockUI()
with mock.patch('check_manifest.VCS.detect', staticmethod(lambda *a: False)):
with mock.patch('check_manifest.Git.detect', staticmethod(lambda *a: False)):
with self.assertRaises(Failure) as cm:
detect_vcs(ui)
self.assertEqual(str(cm.exception),
"Couldn't find version control data"
" (git/hg/bzr/svn supported)")
def test_normalize_names(self):
from check_manifest import normalize_names
j = os.path.join
self.assertEqual(normalize_names(["a", j("b", ""), j("c", "d"),
j("e", "f", ""),
j("g", "h", "..", "i")]),
["a", "b", "c/d", "e/f", "g/i"])
def test_canonical_file_list(self):
from check_manifest import canonical_file_list
j = os.path.join
self.assertEqual(
canonical_file_list(['b', 'a', 'c', j('c', 'd'), j('e', 'f'),
'g', j('g', 'h', 'i', 'j')]),
['a', 'b', 'c/d', 'e/f', 'g/h/i/j'])
def test_file_matches(self):
from check_manifest import file_matches
patterns = ['setup.cfg', '*.egg-info', '*.egg-info/*']
self.assertFalse(file_matches('setup.py', patterns))
self.assertTrue(file_matches('setup.cfg', patterns))
self.assertTrue(file_matches('src/zope.foo.egg-info', patterns))
self.assertTrue(file_matches('src/zope.foo.egg-info/SOURCES.txt',
patterns))
def test_strip_sdist_extras(self):
from check_manifest import (
IgnoreList,
canonical_file_list,
strip_sdist_extras,
)
filelist = canonical_file_list([
'.circleci/config.yml',
'.github',
'.github/ISSUE_TEMPLATE',
'.github/ISSUE_TEMPLATE/bug_report.md',
'.gitignore',
'.gitpod.yml',
'.travis.yml',
'setup.py',
'setup.cfg',
'README.txt',
'src',
'src/.gitignore',
'src/zope',
'src/zope/__init__.py',
'src/zope/foo',
'src/zope/foo/__init__.py',
'src/zope/foo/language.po',
'src/zope/foo/language.mo',
'src/zope.foo.egg-info',
'src/zope.foo.egg-info/SOURCES.txt',
])
expected = canonical_file_list([
'setup.py',
'README.txt',
'src',
'src/zope',
'src/zope/__init__.py',
'src/zope/foo',
'src/zope/foo/__init__.py',
'src/zope/foo/language.po',
])
ignore = IgnoreList.default()
self.assertEqual(strip_sdist_extras(ignore, filelist), expected)
def test_strip_sdist_extras_with_manifest(self):
from check_manifest import (
IgnoreList,
_get_ignore_from_manifest_lines,
canonical_file_list,
strip_sdist_extras,
)
manifest_in = textwrap.dedent("""
graft src
exclude *.cfg
global-exclude *.mo
prune src/dump
recursive-exclude src/zope *.sh
""")
filelist = canonical_file_list([
'.github/ISSUE_TEMPLATE/bug_report.md',
'.gitignore',
'setup.py',
'setup.cfg',
'MANIFEST.in',
'README.txt',
'src',
'src/helper.sh',
'src/dump',
'src/dump/__init__.py',
'src/zope',
'src/zope/__init__.py',
'src/zope/zopehelper.sh',
'src/zope/foo',
'src/zope/foo/__init__.py',
'src/zope/foo/language.po',
'src/zope/foo/language.mo',
'src/zope/foo/config.cfg',
'src/zope/foo/foohelper.sh',
'src/zope.foo.egg-info',
'src/zope.foo.egg-info/SOURCES.txt',
])
expected = canonical_file_list([
'setup.py',
'MANIFEST.in',
'README.txt',
'src',
'src/helper.sh',
'src/zope',
'src/zope/__init__.py',
'src/zope/foo',
'src/zope/foo/__init__.py',
'src/zope/foo/language.po',
'src/zope/foo/config.cfg',
])
ignore = IgnoreList.default()
ignore += _get_ignore_from_manifest_lines(manifest_in.splitlines(), self.ui)
result = strip_sdist_extras(ignore, filelist)
self.assertEqual(result, expected)
def test_find_bad_ideas(self):
from check_manifest import find_bad_ideas
filelist = [
'.gitignore',
'setup.py',
'setup.cfg',
'README.txt',
'src',
'src/zope',
'src/zope/__init__.py',
'src/zope/foo',
'src/zope/foo/__init__.py',
'src/zope/foo/language.po',
'src/zope/foo/language.mo',
'src/zope.foo.egg-info',
'src/zope.foo.egg-info/SOURCES.txt',
]
expected = [
'src/zope/foo/language.mo',
'src/zope.foo.egg-info',
]
self.assertEqual(find_bad_ideas(filelist), expected)
def test_find_suggestions(self):
from check_manifest import find_suggestions
self.assertEqual(find_suggestions(['buildout.cfg']),
(['include buildout.cfg'], []))
self.assertEqual(find_suggestions(['unknown.file~']),
([], ['unknown.file~']))
self.assertEqual(find_suggestions(['README.txt', 'CHANGES.txt']),
(['include *.txt'], []))
filelist = [
'docs/index.rst',
'docs/image.png',
'docs/Makefile',
'docs/unknown-file',
'src/etc/blah/blah/Makefile',
]
expected_rules = [
'recursive-include docs *.png',
'recursive-include docs *.rst',
'recursive-include docs Makefile',
'recursive-include src Makefile',
]
expected_unknowns = ['docs/unknown-file']
self.assertEqual(find_suggestions(filelist),
(expected_rules, expected_unknowns))
def test_find_suggestions_generic_fallback_rules(self):
from check_manifest import find_suggestions
self.assertEqual(find_suggestions(['Changelog']),
(['include Changelog'], []))
self.assertEqual(find_suggestions(['id-lang.map']),
(['include *.map'], []))
self.assertEqual(find_suggestions(['src/id-lang.map']),
(['recursive-include src *.map'], []))
def test_is_package(self):
from check_manifest import is_package
j = os.path.join
exists = {j('a', 'setup.py'), j('c', 'pyproject.toml')}
with mock.patch('os.path.exists', lambda fn: fn in exists):
self.assertTrue(is_package('a'))
self.assertFalse(is_package('b'))
self.assertTrue(is_package('c'))
def test_extract_version_from_filename(self):
from check_manifest import extract_version_from_filename as e
self.assertEqual(e('dist/foo_bar-1.2.3.dev4+g12345.zip'), '1.2.3.dev4+g12345')
self.assertEqual(e('dist/foo_bar-1.2.3.dev4+g12345.tar.gz'), '1.2.3.dev4+g12345')
self.assertEqual(e('dist/foo-bar-1.2.3.dev4+g12345.tar.gz'), '1.2.3.dev4+g12345')
def test_get_ignore_from_manifest_lines(self):
from check_manifest import IgnoreList, _get_ignore_from_manifest_lines
parse = partial(_get_ignore_from_manifest_lines, ui=self.ui)
self.assertEqual(parse([]),
IgnoreList())
self.assertEqual(parse(['', ' ']),
IgnoreList())
self.assertEqual(parse(['exclude *.cfg']),
IgnoreList().exclude('*.cfg'))
self.assertEqual(parse(['exclude *.cfg']),
IgnoreList().exclude('*.cfg'))
self.assertEqual(parse(['\texclude\t*.cfg foo.* bar.txt']),
IgnoreList().exclude('*.cfg', 'foo.*', 'bar.txt'))
self.assertEqual(parse(['exclude some/directory/*.cfg']),
IgnoreList().exclude('some/directory/*.cfg'))
self.assertEqual(parse(['include *.cfg']),
IgnoreList())
self.assertEqual(parse(['global-exclude *.pyc']),
IgnoreList().global_exclude('*.pyc'))
self.assertEqual(parse(['global-exclude *.pyc *.sh']),
IgnoreList().global_exclude('*.pyc', '*.sh'))
self.assertEqual(parse(['recursive-exclude dir *.pyc']),
IgnoreList().recursive_exclude('dir', '*.pyc'))
self.assertEqual(parse(['recursive-exclude dir *.pyc foo*.sh']),
IgnoreList().recursive_exclude('dir', '*.pyc', 'foo*.sh'))
self.assertEqual(parse(['recursive-exclude dir nopattern.xml']),
IgnoreList().recursive_exclude('dir', 'nopattern.xml'))
# We should not fail when a recursive-exclude line is wrong:
self.assertEqual(parse(['recursive-exclude dirwithoutpattern']),
IgnoreList())
self.assertEqual(parse(['prune dir']),
IgnoreList().prune('dir'))
# And a mongo test case of everything at the end
text = textwrap.dedent("""
exclude *.02
exclude *.03 04.* bar.txt
exclude *.05
exclude some/directory/*.cfg
global-exclude *.10 *.11
global-exclude *.12
include *.20
prune 30
recursive-exclude 40 *.41
recursive-exclude 42 *.43 44.*
""").splitlines()
self.assertEqual(
parse(text),
IgnoreList()
.exclude('*.02', '*.03', '04.*', 'bar.txt', '*.05', 'some/directory/*.cfg')
.global_exclude('*.10', '*.11', '*.12')
.prune('30')
.recursive_exclude('40', '*.41')
.recursive_exclude('42', '*.43', '44.*')
)
def test_get_ignore_from_manifest_lines_warns(self):
from check_manifest import IgnoreList, _get_ignore_from_manifest_lines
parse = partial(_get_ignore_from_manifest_lines, ui=self.ui)
text = textwrap.dedent("""
graft a/
recursive-include /b *.txt
""").splitlines()
self.assertEqual(parse(text), IgnoreList())
self.assertEqual(self.ui.warnings, [
'ERROR: Trailing slashes are not allowed in MANIFEST.in on Windows: a/',
'ERROR: Leading slashes are not allowed in MANIFEST.in on Windows: /b',
])
def test_get_ignore_from_manifest(self):
from check_manifest import IgnoreList, _get_ignore_from_manifest
filename = os.path.join(self.make_temp_dir(), 'MANIFEST.in')
self.create_file(filename, textwrap.dedent('''
exclude \\
# yes, this is allowed!
test.dat
# https://github.com/mgedmin/check-manifest/issues/66
# docs/ folder
'''))
ui = MockUI()
self.assertEqual(_get_ignore_from_manifest(filename, ui),
IgnoreList().exclude('test.dat'))
self.assertEqual(ui.warnings, [])
def test_get_ignore_from_manifest_warnings(self):
from check_manifest import IgnoreList, _get_ignore_from_manifest
filename = os.path.join(self.make_temp_dir(), 'MANIFEST.in')
self.create_file(filename, textwrap.dedent('''
# this is bad: a file should not end with a backslash
exclude test.dat \\
'''))
ui = MockUI()
self.assertEqual(_get_ignore_from_manifest(filename, ui),
IgnoreList().exclude('test.dat'))
self.assertEqual(ui.warnings, [
"%s, line 2: continuation line immediately precedes end-of-file" % filename,
])
def test_should_use_pep517_no_pyproject_toml(self):
from check_manifest import cd, should_use_pep_517
src_dir = self.make_temp_dir()
with cd(src_dir):
self.assertFalse(should_use_pep_517())
def test_should_use_pep517_no_build_system(self):
from check_manifest import cd, should_use_pep_517
src_dir = self.make_temp_dir()
filename = os.path.join(src_dir, 'pyproject.toml')
self.create_file(filename, textwrap.dedent('''
[tool.check-manifest]
'''))
with cd(src_dir):
self.assertFalse(should_use_pep_517())
def test_should_use_pep517_no_build_backend(self):
from check_manifest import cd, should_use_pep_517
src_dir = self.make_temp_dir()
filename = os.path.join(src_dir, 'pyproject.toml')
self.create_file(filename, textwrap.dedent('''
[build-system]
requires = [
"setuptools >= 40.6.0",
"wheel",
]
'''))
with cd(src_dir):
self.assertFalse(should_use_pep_517())
def test_should_use_pep517_yes_please(self):
from check_manifest import cd, should_use_pep_517
src_dir = self.make_temp_dir()
filename = os.path.join(src_dir, 'pyproject.toml')
self.create_file(filename, textwrap.dedent('''
[build-system]
requires = [
"setuptools >= 40.6.0",
"wheel",
]
build-backend = "setuptools.build_meta"
'''))
with cd(src_dir):
self.assertTrue(should_use_pep_517())
def _test_build_sdist_pep517(self, build_isolation):
from check_manifest import build_sdist, cd, get_one_file_in
src_dir = self.make_temp_dir()
filename = os.path.join(src_dir, 'pyproject.toml')
self.create_file(filename, textwrap.dedent('''
[build-system]
requires = [
"setuptools >= 40.6.0",
"wheel",
]
build-backend = "setuptools.build_meta"
'''))
out_dir = self.make_temp_dir()
python = os.path.abspath(sys.executable)
with cd(src_dir):
build_sdist(out_dir, python=python, build_isolation=build_isolation)
self.assertTrue(get_one_file_in(out_dir))
def test_build_sdist_pep517_isolated(self):
self._test_build_sdist_pep517(build_isolation=True)
def test_build_sdist_pep517_no_isolation(self):
self._test_build_sdist_pep517(build_isolation=False)
class TestConfiguration(unittest.TestCase):
def setUp(self):
self.oldpwd = os.getcwd()
self.tmpdir = tempfile.mkdtemp(prefix='test-', suffix='-check-manifest')
os.chdir(self.tmpdir)
self.ui = MockUI()
def tearDown(self):
os.chdir(self.oldpwd)
rmtree(self.tmpdir)
def test_read_config_no_config(self):
import check_manifest
ignore, ignore_bad_ideas = check_manifest.read_config()
self.assertEqual(ignore, check_manifest.IgnoreList.default())
def test_read_setup_config_no_section(self):
import check_manifest
with open('setup.cfg', 'w') as f:
f.write('[pep8]\nignore =\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
self.assertEqual(ignore, check_manifest.IgnoreList.default())
def test_read_pyproject_config_no_section(self):
import check_manifest
with open('pyproject.toml', 'w') as f:
f.write('[tool.pep8]\nignore = []\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
self.assertEqual(ignore, check_manifest.IgnoreList.default())
def test_read_setup_config_no_option(self):
import check_manifest
with open('setup.cfg', 'w') as f:
f.write('[check-manifest]\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
self.assertEqual(ignore, check_manifest.IgnoreList.default())
def test_read_pyproject_config_no_option(self):
import check_manifest
with open('pyproject.toml', 'w') as f:
f.write('[tool.check-manifest]\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
self.assertEqual(ignore, check_manifest.IgnoreList.default())
def test_read_setup_config_extra_ignores(self):
import check_manifest
with open('setup.cfg', 'w') as f:
f.write('[check-manifest]\nignore = foo\n bar*\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
expected = check_manifest.IgnoreList.default().global_exclude('foo', 'bar*')
self.assertEqual(ignore, expected)
def test_read_pyproject_config_extra_ignores(self):
import check_manifest
with open('pyproject.toml', 'w') as f:
f.write('[tool.check-manifest]\nignore = ["foo", "bar*"]\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
expected = check_manifest.IgnoreList.default().global_exclude('foo', 'bar*')
self.assertEqual(ignore, expected)
def test_read_setup_config_override_ignores(self):
import check_manifest
with open('setup.cfg', 'w') as f:
f.write('[check-manifest]\nignore = foo\n\n bar\n')
f.write('ignore-default-rules = yes\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
expected = check_manifest.IgnoreList().global_exclude('foo', 'bar')
self.assertEqual(ignore, expected)
def test_read_pyproject_config_override_ignores(self):
import check_manifest
with open('pyproject.toml', 'w') as f:
f.write('[tool.check-manifest]\nignore = ["foo", "bar"]\n')
f.write('ignore-default-rules = true\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
expected = check_manifest.IgnoreList().global_exclude('foo', 'bar')
self.assertEqual(ignore, expected)
def test_read_setup_config_ignore_bad_ideas(self):
import check_manifest
with open('setup.cfg', 'w') as f:
f.write('[check-manifest]\n'
'ignore-bad-ideas = \n'
' foo\n'
' bar*\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
expected = check_manifest.IgnoreList().global_exclude('foo', 'bar*')
self.assertEqual(ignore_bad_ideas, expected)
def test_read_pyproject_config_ignore_bad_ideas(self):
import check_manifest
with open('pyproject.toml', 'w') as f:
f.write('[tool.check-manifest]\n'
'ignore-bad-ideas = ["foo", "bar*"]\n')
ignore, ignore_bad_ideas = check_manifest.read_config()
expected = check_manifest.IgnoreList().global_exclude('foo', 'bar*')
self.assertEqual(ignore_bad_ideas, expected)
def test_read_manifest_no_manifest(self):
import check_manifest
ignore = check_manifest.read_manifest(self.ui)
self.assertEqual(ignore, check_manifest.IgnoreList())
def test_read_manifest(self):
import check_manifest
from check_manifest import IgnoreList
with open('MANIFEST.in', 'w') as f:
f.write('exclude *.gif\n')
f.write('global-exclude *.png\n')
ignore = check_manifest.read_manifest(self.ui)
self.assertEqual(ignore, IgnoreList().exclude('*.gif').global_exclude('*.png'))
class TestMain(unittest.TestCase):
def setUp(self):
self._cm_patcher = mock.patch('check_manifest.check_manifest')
self._check_manifest = self._cm_patcher.start()
self._se_patcher = mock.patch('sys.exit')
self._sys_exit = self._se_patcher.start()
self.ui = MockUI()
self._ui_patcher = mock.patch('check_manifest.UI', self._make_ui)
self._ui_patcher.start()
self._orig_sys_argv = sys.argv
sys.argv = ['check-manifest']
def tearDown(self):
sys.argv = self._orig_sys_argv
self._se_patcher.stop()
self._cm_patcher.stop()
self._ui_patcher.stop()
def _make_ui(self, verbosity):
self.ui.verbosity = verbosity
return self.ui
def test(self):
from check_manifest import main
sys.argv.append('-v')
main()
def test_exit_code_1_on_error(self):
from check_manifest import main
self._check_manifest.return_value = False
main()
self._sys_exit.assert_called_with(1)
def test_exit_code_2_on_failure(self):
from check_manifest import Failure, main
self._check_manifest.side_effect = Failure('msg')
main()
self.assertEqual(self.ui.errors, ['msg'])
self._sys_exit.assert_called_with(2)
def test_extra_ignore_args(self):
import check_manifest
sys.argv.append('--ignore=x,y,z*')
check_manifest.main()
ignore = check_manifest.IgnoreList().global_exclude('x', 'y', 'z*')
self.assertEqual(self._check_manifest.call_args.kwargs['extra_ignore'],
ignore)
def test_ignore_bad_ideas_args(self):
import check_manifest
sys.argv.append('--ignore-bad-ideas=x,y,z*')
check_manifest.main()
ignore = check_manifest.IgnoreList().global_exclude('x', 'y', 'z*')
self.assertEqual(self._check_manifest.call_args.kwargs['extra_ignore_bad_ideas'],
ignore)
def test_verbose_arg(self):
import check_manifest
sys.argv.append('--verbose')
check_manifest.main()
self.assertEqual(self.ui.verbosity, 2)
def test_quiet_arg(self):
import check_manifest
sys.argv.append('--quiet')
check_manifest.main()
self.assertEqual(self.ui.verbosity, 0)
def test_verbose_and_quiet_arg(self):
import check_manifest
sys.argv.append('--verbose')
sys.argv.append('--quiet')
check_manifest.main()
# the two arguments cancel each other out:
# 1 (default verbosity) + 1 - 1 = 1.
self.assertEqual(self.ui.verbosity, 1)
class TestZestIntegration(unittest.TestCase):
def setUp(self):
sys.modules['zest'] = mock.Mock()
sys.modules['zest.releaser'] = mock.Mock()
sys.modules['zest.releaser.utils'] = mock.Mock()
self.ask = sys.modules['zest.releaser.utils'].ask
self.ui = MockUI()
self._ui_patcher = mock.patch('check_manifest.UI', return_value=self.ui)
self._ui_patcher.start()
def tearDown(self):
self._ui_patcher.stop()
del sys.modules['zest.releaser.utils']
del sys.modules['zest.releaser']
del sys.modules['zest']
@mock.patch('check_manifest.is_package', lambda d: False)
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_not_a_package(self, check_manifest):
from check_manifest import zest_releaser_check
zest_releaser_check(dict(workingdir='.'))
check_manifest.assert_not_called()
@mock.patch('check_manifest.is_package', lambda d: True)
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_user_disagrees(self, check_manifest):
from check_manifest import zest_releaser_check
self.ask.return_value = False
zest_releaser_check(dict(workingdir='.'))
check_manifest.assert_not_called()
@mock.patch('check_manifest.is_package', lambda d: True)
@mock.patch('sys.exit')
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_all_okay(self, check_manifest, sys_exit):
from check_manifest import zest_releaser_check
self.ask.return_value = True
check_manifest.return_value = True
zest_releaser_check(dict(workingdir='.'))
sys_exit.assert_not_called()
@mock.patch('check_manifest.is_package', lambda d: True)
@mock.patch('sys.exit')
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_error_user_aborts(self, check_manifest,
sys_exit):
from check_manifest import zest_releaser_check
self.ask.side_effect = [True, False]
check_manifest.return_value = False
zest_releaser_check(dict(workingdir='.'))
sys_exit.assert_called_with(1)
@mock.patch('check_manifest.is_package', lambda d: True)
@mock.patch('sys.exit')
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_error_user_plods_on(self, check_manifest,
sys_exit):
from check_manifest import zest_releaser_check
self.ask.side_effect = [True, True]
check_manifest.return_value = False
zest_releaser_check(dict(workingdir='.'))
sys_exit.assert_not_called()
@mock.patch('check_manifest.is_package', lambda d: True)
@mock.patch('sys.exit')
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_failure_user_aborts(self, check_manifest,
sys_exit):
from check_manifest import Failure, zest_releaser_check
self.ask.side_effect = [True, False]
check_manifest.side_effect = Failure('msg')
zest_releaser_check(dict(workingdir='.'))
self.assertEqual(self.ui.errors, ['msg'])
sys_exit.assert_called_with(2)
@mock.patch('check_manifest.is_package', lambda d: True)
@mock.patch('sys.exit')
@mock.patch('check_manifest.check_manifest')
def test_zest_releaser_check_failure_user_plods_on(self, check_manifest,
sys_exit):
from check_manifest import Failure, zest_releaser_check
self.ask.side_effect = [True, True]
check_manifest.side_effect = Failure('msg')
zest_releaser_check(dict(workingdir='.'))
self.assertEqual(self.ui.errors, ['msg'])
sys_exit.assert_not_called()
class VCSHelper:
# override in subclasses
command = None # type: Optional[str]
def is_installed(self):
try:
p = subprocess.Popen([self.command, '--version'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout, stderr = p.communicate()
rc = p.wait()
return (rc == 0)
except OSError:
return False
def _run(self, *command):
# Windows doesn't like Unicode arguments to subprocess.Popen(), on Py2:
# https://github.com/mgedmin/check-manifest/issues/23#issuecomment-33933031
if str is bytes:
command = [s.encode(locale.getpreferredencoding()) for s in command]
print('$', ' '.join(command))
p = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
stdout, stderr = p.communicate()
rc = p.wait()
if stdout:
print(
stdout if isinstance(stdout, str) else
stdout.decode('ascii', 'backslashreplace')
)
if rc:
raise subprocess.CalledProcessError(rc, command[0], output=stdout)
class VCSMixin:
def setUp(self):
if not self.vcs.is_installed() and CAN_SKIP_TESTS:
self.skipTest("%s is not installed" % self.vcs.command)
self.tmpdir = tempfile.mkdtemp(prefix='test-', suffix='-check-manifest')
self.olddir = os.getcwd()
os.chdir(self.tmpdir)
self.ui = MockUI()
def tearDown(self):
os.chdir(self.olddir)
rmtree(self.tmpdir)
def _create_file(self, filename):
assert not os.path.isabs(filename)
basedir = os.path.dirname(filename)
if basedir and not os.path.isdir(basedir):
os.makedirs(basedir)
open(filename, 'w').close()
def _create_files(self, filenames):
for filename in filenames:
self._create_file(filename)
def _init_vcs(self):
self.vcs._init_vcs()
def _add_to_vcs(self, filenames):
self.vcs._add_to_vcs(filenames)
def _commit(self):
self.vcs._commit()
def _create_and_add_to_vcs(self, filenames):
self._create_files(filenames)
self._add_to_vcs(filenames)
def test_get_vcs_files(self):
from check_manifest import get_vcs_files
self._init_vcs()
self._create_and_add_to_vcs(['a.txt', 'b/b.txt', 'b/c/d.txt'])
self._commit()
self._create_files(['b/x.txt', 'd/d.txt', 'i.txt'])
self.assertEqual(get_vcs_files(self.ui),
['a.txt', 'b/b.txt', 'b/c/d.txt'])
def test_get_vcs_files_added_but_uncommitted(self):
from check_manifest import get_vcs_files
self._init_vcs()
self._create_and_add_to_vcs(['a.txt', 'b/b.txt', 'b/c/d.txt'])
self._create_files(['b/x.txt', 'd/d.txt', 'i.txt'])
self.assertEqual(get_vcs_files(self.ui),
['a.txt', 'b/b.txt', 'b/c/d.txt'])
def test_get_vcs_files_deleted_but_not_removed(self):
if self.vcs.command == 'bzr':
self.skipTest("this cosmetic feature is not supported with bzr")
# see the longer explanation in test_missing_source_files
from check_manifest import get_vcs_files
self._init_vcs()
self._create_and_add_to_vcs(['a.txt'])
self._commit()
os.unlink('a.txt')
self.assertEqual(get_vcs_files(self.ui), ['a.txt'])
def test_get_vcs_files_in_a_subdir(self):
from check_manifest import get_vcs_files
self._init_vcs()
self._create_and_add_to_vcs(['a.txt', 'b/b.txt', 'b/c/d.txt'])
self._commit()
self._create_files(['b/x.txt', 'd/d.txt', 'i.txt'])
os.chdir('b')
self.assertEqual(get_vcs_files(self.ui), ['b.txt', 'c/d.txt'])
def test_get_vcs_files_nonascii_filenames(self):
# This test will fail if your locale is incapable of expressing
# "eacute". UTF-8 or Latin-1 should work.
from check_manifest import get_vcs_files
self._init_vcs()
filename = "\u00E9.txt"
self._create_and_add_to_vcs([filename])
self.assertEqual(get_vcs_files(self.ui), [filename])
def test_get_vcs_files_empty(self):
from check_manifest import get_vcs_files
self._init_vcs()
self.assertEqual(get_vcs_files(self.ui), [])
class GitHelper(VCSHelper):
command = 'git'
def _init_vcs(self):
self._run('git', 'init')
self._run('git', 'config', 'user.name', 'Unit Test')
self._run('git', 'config', 'user.email', 'test@example.com')
def _add_to_vcs(self, filenames):
# Note that we use --force to prevent errors when we want to
# add foo.egg-info and the user running the tests has
# '*.egg-info' in her global .gitignore file.
self._run('git', 'add', '--force', '--', *filenames)
def _commit(self):
self._run('git', 'commit', '-m', 'Initial')
class TestGit(VCSMixin, unittest.TestCase):
vcs = GitHelper()
def _init_repo_with_files(self, dirname, filenames):
os.mkdir(dirname)
os.chdir(dirname)
self._init_vcs()
self._create_and_add_to_vcs(filenames)
self._commit()
os.chdir(self.tmpdir)
def _add_submodule(self, repo, subdir, subrepo):
os.chdir(repo)
self.vcs._run('git', 'submodule', 'add', subrepo, subdir)
self._commit()
os.chdir(self.tmpdir)
def test_detect_git_submodule(self):
from check_manifest import Failure, detect_vcs
with self.assertRaises(Failure) as cm:
detect_vcs(self.ui)
self.assertEqual(str(cm.exception),
"Couldn't find version control data"
" (git/hg/bzr/svn supported)")
# now create a .git file like in a submodule
open(os.path.join(self.tmpdir, '.git'), 'w').close()
self.assertEqual(detect_vcs(self.ui).metadata_name, '.git')
def test_get_versioned_files_with_git_submodules(self):
from check_manifest import get_vcs_files
self._init_repo_with_files('repo1', ['file1', 'file2'])
self._init_repo_with_files('repo2', ['file3'])
self._init_repo_with_files('repo3', ['file4'])
self._add_submodule('repo2', 'sub3', '../repo3')
self._init_repo_with_files('main', ['file5', 'subdir/file6'])
self._add_submodule('main', 'sub1', '../repo1')
self._add_submodule('main', 'subdir/sub2', '../repo2')
os.chdir('main')
self.vcs._run('git', 'submodule', 'update', '--init', '--recursive')
self.assertEqual(
get_vcs_files(self.ui),
[
'.gitmodules',
'file5',
'sub1/file1',
'sub1/file2',
'subdir/file6',
'subdir/sub2/.gitmodules',
'subdir/sub2/file3',
'subdir/sub2/sub3/file4',
])
def test_get_versioned_files_with_git_submodules_with_git_index_file_set(self):
with mock.patch.dict(os.environ, {"GIT_INDEX_FILE": ".git/index"}):
self.test_get_versioned_files_with_git_submodules()
class BzrHelper(VCSHelper):
command = 'bzr'
def _init_vcs(self):
self._run('bzr', 'init')
self._run('bzr', 'whoami', '--branch', 'Unit Test <test@example.com>')
def _add_to_vcs(self, filenames):
self._run('bzr', 'add', '--', *filenames)
def _commit(self):
self._run('bzr', 'commit', '-m', 'Initial')
class TestBzr(VCSMixin, unittest.TestCase):
vcs = BzrHelper()
@unittest.skipIf(HAS_OEM_CODEC,
"Python 3.6 lets us use 'oem' codec instead of guessing")
class TestBzrTerminalCharsetDetectionOnOldPythons(unittest.TestCase):
@mock.patch('sys.stdin')
@mock.patch('sys.stdout')
def test_terminal_encoding_not_known(self, mock_stdout, mock_stdin):
from check_manifest import Bazaar
mock_stdout.encoding = None
mock_stdin.encoding = None
self.assertEqual(Bazaar._get_terminal_encoding(), None)
@mock.patch('sys.stdout')
def test_terminal_encoding_stdout_known(self, mock_stdout):
from check_manifest import Bazaar
mock_stdout.encoding = 'UTF-8'
self.assertEqual(Bazaar._get_terminal_encoding(), 'UTF-8')
@mock.patch('sys.stdin')
@mock.patch('sys.stdout')
def test_terminal_encoding_stdin_known(self, mock_stdout, mock_stdin):
from check_manifest import Bazaar
mock_stdout.encoding = None
mock_stdin.encoding = 'UTF-8'
self.assertEqual(Bazaar._get_terminal_encoding(), 'UTF-8')
@mock.patch('sys.stdout')
def test_terminal_encoding_cp0(self, mock_stdout):
from check_manifest import Bazaar
mock_stdout.encoding = 'cp0'
self.assertEqual(Bazaar._get_terminal_encoding(), None)
@unittest.skipIf(not HAS_OEM_CODEC,
"'oem' codec not available on Python before 3.6")
class TestBzrTerminalCharsetDetectionOnNewPythons(unittest.TestCase):
def test_terminal_encoding_cp0(self):
from check_manifest import Bazaar
self.assertEqual(Bazaar._get_terminal_encoding(), "oem")
class HgHelper(VCSHelper):
command = 'hg'
def _init_vcs(self):
self._run('hg', 'init')
with open('.hg/hgrc', 'a') as f:
f.write('\n[ui]\nusername = Unit Test <test@example.com\n')
def _add_to_vcs(self, filenames):
self._run('hg', 'add', '--', *filenames)
def _commit(self):
self._run('hg', 'commit', '-m', 'Initial')
class TestHg(VCSMixin, unittest.TestCase):
vcs = HgHelper()
class SvnHelper(VCSHelper):
command = 'svn'
def _init_vcs(self):
self._run('svnadmin', 'create', 'repo')
self._run('svn', 'co', 'file:///' + os.path.abspath('repo').replace(os.path.sep, '/'), 'checkout')
os.chdir('checkout')
def _add_directories_and_sort(self, filelist):
from check_manifest import normalize_names
names = set(normalize_names(filelist))
names.update([posixpath.dirname(fn) for fn in names])
return sorted(names - {''})
def _add_to_vcs(self, filenames):
self._run('svn', 'add', '-N', '--', *self._add_directories_and_sort(filenames))
def _commit(self):
self._run('svn', 'commit', '-m', 'Initial')
class TestSvn(VCSMixin, unittest.TestCase):
vcs = SvnHelper()
def test_svn_externals(self):
from check_manifest import get_vcs_files
self.vcs._run('svnadmin', 'create', 'repo2')
repo2_url = 'file:///' + os.path.abspath('repo2').replace(os.path.sep, '/')
self.vcs._init_vcs()
self.vcs._run('svn', 'propset', 'svn:externals', 'ext %s' % repo2_url, '.')
self.vcs._run('svn', 'up')
self._create_files(['a.txt', 'ext/b.txt'])
self.vcs._run('svn', 'add', 'a.txt', 'ext/b.txt')
self.assertEqual(get_vcs_files(self.ui),
['a.txt', 'ext/b.txt'])
class TestSvnExtraErrors(unittest.TestCase):
def test_svn_xml_parsing_warning(self):
from check_manifest import Subversion
ui = MockUI()
svn = Subversion(ui)
entry = ET.XML('<entry path="foo/bar.txt"></entry>')
self.assertFalse(svn.is_interesting(entry))
self.assertEqual(
ui.warnings,
['svn status --xml parse error:'
' <entry path="foo/bar.txt"> without <wc-status>'])
class TestUserInterface(unittest.TestCase):
def make_ui(self, verbosity=1):
from check_manifest import UI
ui = UI(verbosity=verbosity)
ui.stdout = StringIO()
ui.stderr = StringIO()
return ui
def test_info(self):
ui = self.make_ui(verbosity=1)
ui.info("Reticulating splines")
self.assertEqual(ui.stdout.getvalue(),
"Reticulating splines\n")
def test_info_verbose(self):
ui = self.make_ui(verbosity=2)
ui.info("Reticulating splines")
self.assertEqual(ui.stdout.getvalue(),
"Reticulating splines\n")
def test_info_quiet(self):
ui = self.make_ui(verbosity=0)
ui.info("Reticulating splines")
self.assertEqual(ui.stdout.getvalue(), "")
def test_info_begin_continue_end(self):
ui = self.make_ui(verbosity=1)
ui.info_begin("Reticulating splines...")
ui.info_continue(" nearly done...")
ui.info_continue(" almost done...")
ui.info_end(" done!")
self.assertEqual(ui.stdout.getvalue(), "")
def test_info_begin_continue_end_verbose(self):
ui = self.make_ui(verbosity=2)
ui.info_begin("Reticulating splines...")
ui.info_continue(" nearly done...")
ui.info_continue(" almost done...")
ui.info_end(" done!")
self.assertEqual(
ui.stdout.getvalue(),
"Reticulating splines... nearly done... almost done... done!\n")
def test_info_emits_newline_when_needed(self):
ui = self.make_ui(verbosity=1)
ui.info_begin("Computering...")
ui.info("Forgot to turn the gas off!")
self.assertEqual(
ui.stdout.getvalue(),
"Forgot to turn the gas off!\n")
def test_info_emits_newline_when_needed_verbose(self):
ui = self.make_ui(verbosity=2)
ui.info_begin("Computering...")
ui.info("Forgot to turn the gas off!")
self.assertEqual(
ui.stdout.getvalue(),
"Computering...\n"
"Forgot to turn the gas off!\n")
def test_warning(self):
ui = self.make_ui(verbosity=1)
ui.info_begin("Computering...")
ui.warning("Forgot to turn the gas off!")
self.assertEqual(ui.stdout.getvalue(), "")
self.assertEqual(
ui.stderr.getvalue(),
"Forgot to turn the gas off!\n")
def test_warning_verbose(self):
ui = self.make_ui(verbosity=2)
ui.info_begin("Computering...")
ui.warning("Forgot to turn the gas off!")
self.assertEqual(
ui.stdout.getvalue(),
"Computering...\n")
self.assertEqual(
ui.stderr.getvalue(),
"Forgot to turn the gas off!\n")
def test_error(self):
ui = self.make_ui(verbosity=1)
ui.info_begin("Computering...")
ui.error("Forgot to turn the gas off!")
self.assertEqual(ui.stdout.getvalue(), "")
self.assertEqual(
ui.stderr.getvalue(),
"Forgot to turn the gas off!\n")
def test_error_verbose(self):
ui = self.make_ui(verbosity=2)
ui.info_begin("Computering...")
ui.error("Forgot to turn the gas off!")
self.assertEqual(
ui.stdout.getvalue(),
"Computering...\n")
self.assertEqual(
ui.stderr.getvalue(),
"Forgot to turn the gas off!\n")
class TestIgnoreList(unittest.TestCase):
def setUp(self):
from check_manifest import IgnoreList
self.ignore = IgnoreList()
def test_repr(self):
from check_manifest import IgnoreList
ignore = IgnoreList()
self.assertEqual(repr(ignore), "IgnoreList([])")
def test_exclude_pattern(self):
self.ignore.exclude('*.txt')
self.assertEqual(self.ignore.filter([
'foo.md',
'bar.txt',
'subdir/bar.txt',
]), [
'foo.md',
'subdir/bar.txt',
])
def test_exclude_file(self):
self.ignore.exclude('bar.txt')
self.assertEqual(self.ignore.filter([
'foo.md',
'bar.txt',
'subdir/bar.txt',
]), [
'foo.md',
'subdir/bar.txt',
])
def test_exclude_doest_apply_to_directories(self):
self.ignore.exclude('subdir')
self.assertEqual(self.ignore.filter([
'foo.md',
'subdir/bar.txt',
]), [
'foo.md',
'subdir/bar.txt',
])
def test_global_exclude(self):
self.ignore.global_exclude('a*.txt')
self.assertEqual(self.ignore.filter([
'bar.txt', # make sure full filenames are matched
'afile.txt',
'subdir/afile.txt',
'adir/file.txt', # make sure * doesn't match /
]), [
'bar.txt',
'adir/file.txt',
])
def test_global_exclude_does_not_apply_to_directories(self):
self.ignore.global_exclude('subdir')
self.assertEqual(self.ignore.filter([
'bar.txt',
'subdir/afile.txt',
]), [
'bar.txt',
'subdir/afile.txt',
])
def test_recursive_exclude(self):
self.ignore.recursive_exclude('subdir', 'a*.txt')
self.assertEqual(self.ignore.filter([
'afile.txt',
'subdir/afile.txt',
'subdir/extra/afile.txt',
'subdir/adir/file.txt',
'other/afile.txt',
]), [
'afile.txt',
'subdir/adir/file.txt',
'other/afile.txt',
])
def test_recursive_exclude_does_not_apply_to_directories(self):
self.ignore.recursive_exclude('subdir', 'dir')
self.assertEqual(self.ignore.filter([
'afile.txt',
'subdir/dir/afile.txt',
]), [
'afile.txt',
'subdir/dir/afile.txt',
])
def test_recursive_exclude_can_prune(self):
self.ignore.recursive_exclude('subdir', '*')
self.assertEqual(self.ignore.filter([
'afile.txt',
'subdir/afile.txt',
'subdir/dir/afile.txt',
'subdir/dir/dir/afile.txt',
]), [
'afile.txt',
])
def test_prune(self):
self.ignore.prune('subdir')
self.assertEqual(self.ignore.filter([
'foo.md',
'subdir/bar.txt',
'unrelated/subdir/baz.txt',
]), [
'foo.md',
'unrelated/subdir/baz.txt',
])
def test_prune_subdir(self):
self.ignore.prune('a/b')
self.assertEqual(self.ignore.filter([
'foo.md',
'a/b/bar.txt',
'a/c/bar.txt',
]), [
'foo.md',
'a/c/bar.txt',
])
def test_prune_glob(self):
self.ignore.prune('su*r')
self.assertEqual(self.ignore.filter([
'foo.md',
'subdir/bar.txt',
'unrelated/subdir/baz.txt',
]), [
'foo.md',
'unrelated/subdir/baz.txt',
])
def test_prune_glob_is_not_too_greedy(self):
self.ignore.prune('su*r')
self.assertEqual(self.ignore.filter([
'foo.md',
# super-unrelated/subdir matches su*r if you allow * to match /,
# which fnmatch does!
'super-unrelated/subdir/qux.txt',
]), [
'foo.md',
'super-unrelated/subdir/qux.txt',
])
def test_default_excludes_pkg_info(self):
from check_manifest import IgnoreList
ignore = IgnoreList.default()
self.assertEqual(ignore.filter([
'PKG-INFO',
'bar.txt',
]), [
'bar.txt',
])
def test_default_excludes_egg_info(self):
from check_manifest import IgnoreList
ignore = IgnoreList.default()
self.assertEqual(ignore.filter([
'mypackage.egg-info/PKG-INFO',
'mypackage.egg-info/SOURCES.txt',
'mypackage.egg-info/requires.txt',
'bar.txt',
]), [
'bar.txt',
])
def test_default_excludes_egg_info_in_a_subdirectory(self):
from check_manifest import IgnoreList
ignore = IgnoreList.default()
self.assertEqual(ignore.filter([
'src/mypackage.egg-info/PKG-INFO',
'src/mypackage.egg-info/SOURCES.txt',
'src/mypackage.egg-info/requires.txt',
'bar.txt',
]), [
'bar.txt',
])
def pick_installed_vcs():
preferred_order = [GitHelper, HgHelper, BzrHelper, SvnHelper]
force = os.getenv('FORCE_TEST_VCS')
if force:
for cls in preferred_order:
if force == cls.command:
return cls()
raise ValueError('Unsupported FORCE_TEST_VCS=%s (supported: %s)'
% (force, '/'.join(cls.command for cls in preferred_order)))
for cls in preferred_order:
vcs = cls()
if vcs.is_installed():
return vcs
return None
class TestCheckManifest(unittest.TestCase):
_vcs = pick_installed_vcs()
def setUp(self):
if self._vcs is None:
self.fail('at least one version control system should be installed')
self.oldpwd = os.getcwd()
self.tmpdir = tempfile.mkdtemp(prefix='test-', suffix='-check-manifest')
os.chdir(self.tmpdir)
self._stdout_patcher = mock.patch('sys.stdout', StringIO())
self._stdout_patcher.start()
self._stderr_patcher = mock.patch('sys.stderr', StringIO())
self._stderr_patcher.start()
def tearDown(self):
self._stderr_patcher.stop()
self._stdout_patcher.stop()
os.chdir(self.oldpwd)
rmtree(self.tmpdir)
def _create_repo_with_code(self, add_to_vcs=True):
self._vcs._init_vcs()
with open('setup.py', 'w') as f:
f.write("from setuptools import setup\n")
f.write("setup(name='sample', py_modules=['sample'])\n")
with open('sample.py', 'w') as f:
f.write("# wow. such code. so amaze\n")
if add_to_vcs:
self._vcs._add_to_vcs(['setup.py', 'sample.py'])
def _create_repo_with_code_in_subdir(self):
os.mkdir('subdir')
os.chdir('subdir')
self._create_repo_with_code()
# NB: when self._vcs is SvnHelper, we're actually in
# ./subdir/checkout rather than in ./subdir
subdir = os.path.basename(os.getcwd())
os.chdir(os.pardir)
return subdir
def _add_to_vcs(self, filename, content=''):
if os.path.sep in filename and not os.path.isdir(os.path.dirname(filename)):
os.makedirs(os.path.dirname(filename))
with open(filename, 'w') as f:
f.write(content)
self._vcs._add_to_vcs([filename])
def test_not_python_project(self):
from check_manifest import Failure, check_manifest
with self.assertRaises(Failure) as cm:
check_manifest()
self.assertEqual(
str(cm.exception),
"This is not a Python project (no setup.py/pyproject.toml).")
def test_forgot_to_git_add_anything(self):
from check_manifest import Failure, check_manifest
self._create_repo_with_code(add_to_vcs=False)
with self.assertRaises(Failure) as cm:
check_manifest()
self.assertEqual(str(cm.exception),
"There are no files added to version control!")
def test_all_is_well(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self.assertTrue(check_manifest(), sys.stderr.getvalue())
def test_relative_pathname(self):
from check_manifest import check_manifest
subdir = self._create_repo_with_code_in_subdir()
self.assertTrue(check_manifest(subdir), sys.stderr.getvalue())
def test_relative_python(self):
# https://github.com/mgedmin/check-manifest/issues/36
from check_manifest import check_manifest
subdir = self._create_repo_with_code_in_subdir()
python = os.path.relpath(sys.executable)
self.assertTrue(check_manifest(subdir, python=python),
sys.stderr.getvalue())
def test_python_from_path(self):
# https://github.com/mgedmin/check-manifest/issues/57
from check_manifest import check_manifest
# We need a Python interpeter to be in PATH.
python = 'python'
if hasattr(shutil, 'which'):
for python in 'python', 'python3', os.path.basename(sys.executable):
if shutil.which(python):
break
subdir = self._create_repo_with_code_in_subdir()
self.assertTrue(check_manifest(subdir, python=python),
sys.stderr.getvalue())
def test_extra_ignore(self):
from check_manifest import IgnoreList, check_manifest
self._create_repo_with_code()
self._add_to_vcs('unrelated.txt')
ignore = IgnoreList().global_exclude('*.txt')
self.assertTrue(check_manifest(extra_ignore=ignore),
sys.stderr.getvalue())
def test_suggestions(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('unrelated.txt')
self.assertFalse(check_manifest())
self.assertIn("missing from sdist:\n unrelated.txt",
sys.stderr.getvalue())
self.assertIn("suggested MANIFEST.in rules:\n include *.txt",
sys.stdout.getvalue())
def test_suggestions_create(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('unrelated.txt')
self.assertFalse(check_manifest(create=True))
self.assertIn("missing from sdist:\n unrelated.txt",
sys.stderr.getvalue())
self.assertIn("suggested MANIFEST.in rules:\n include *.txt",
sys.stdout.getvalue())
self.assertIn("creating MANIFEST.in",
sys.stdout.getvalue())
with open('MANIFEST.in') as f:
self.assertEqual(f.read(), "include *.txt\n")
def test_suggestions_update(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('unrelated.txt')
self._add_to_vcs('MANIFEST.in', '#tbd')
self.assertFalse(check_manifest(update=True))
self.assertIn("missing from sdist:\n unrelated.txt",
sys.stderr.getvalue())
self.assertIn("suggested MANIFEST.in rules:\n include *.txt",
sys.stdout.getvalue())
self.assertIn("updating MANIFEST.in",
sys.stdout.getvalue())
with open('MANIFEST.in') as f:
self.assertEqual(
f.read(),
"#tbd\n# added by check-manifest\ninclude *.txt\n")
def test_suggestions_all_unknown_patterns(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('.dunno-what-to-do-with-this')
self.assertFalse(check_manifest(update=True))
self.assertIn("missing from sdist:\n .dunno-what-to-do-with-this",
sys.stderr.getvalue())
self.assertIn(
"don't know how to come up with rules matching any of the files, sorry!",
sys.stdout.getvalue())
def test_suggestions_some_unknown_patterns(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('.dunno-what-to-do-with-this')
self._add_to_vcs('unrelated.txt')
self.assertFalse(check_manifest(update=True))
self.assertIn(
"don't know how to come up with rules matching\n .dunno-what-to-do-with-this",
sys.stdout.getvalue())
self.assertIn("creating MANIFEST.in",
sys.stdout.getvalue())
with open('MANIFEST.in') as f:
self.assertEqual(f.read(), "include *.txt\n")
def test_MANIFEST_in_does_not_need_to_be_added_to_be_considered(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('unrelated.txt')
with open('MANIFEST.in', 'w') as f:
f.write("include *.txt\n")
self.assertFalse(check_manifest())
self.assertIn("missing from VCS:\n MANIFEST.in", sys.stderr.getvalue())
self.assertNotIn("missing from sdist", sys.stderr.getvalue())
def test_setup_py_does_not_need_to_be_added_to_be_considered(self):
from check_manifest import check_manifest
self._create_repo_with_code(add_to_vcs=False)
self._add_to_vcs('sample.py')
self.assertFalse(check_manifest())
self.assertIn("missing from VCS:\n setup.py", sys.stderr.getvalue())
self.assertNotIn("missing from sdist", sys.stderr.getvalue())
def test_bad_ideas(self):
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('foo.egg-info')
self._add_to_vcs('moo.mo')
self.assertFalse(check_manifest())
self.assertIn("you have foo.egg-info in source control!",
sys.stderr.getvalue())
self.assertIn("this also applies to the following:\n moo.mo",
sys.stderr.getvalue())
def test_ignore_bad_ideas(self):
from check_manifest import IgnoreList, check_manifest
self._create_repo_with_code()
with open('setup.cfg', 'w') as f:
f.write('[check-manifest]\n'
'ignore =\n'
' subdir/bar.egg-info\n'
'ignore-bad-ideas =\n'
' subdir/bar.egg-info\n')
self._add_to_vcs('foo.egg-info')
self._add_to_vcs('moo.mo')
self._add_to_vcs(os.path.join('subdir', 'bar.egg-info'))
ignore = IgnoreList().global_exclude('*.mo')
self.assertFalse(check_manifest(extra_ignore_bad_ideas=ignore))
self.assertIn("you have foo.egg-info in source control!",
sys.stderr.getvalue())
self.assertNotIn("moo.mo", sys.stderr.getvalue())
self.assertNotIn("bar.egg-info", sys.stderr.getvalue())
def test_missing_source_files(self):
# https://github.com/mgedmin/check-manifest/issues/32
from check_manifest import check_manifest
self._create_repo_with_code()
self._add_to_vcs('missing.py')
os.unlink('missing.py')
check_manifest()
if self._vcs.command != 'bzr':
# 'bzr ls' doesn't list files that were deleted but not
# marked for deletion. 'bzr st' does, but it doesn't list
# unmodified files. Importing bzrlib and using the API to
# get the file list we need is (a) complicated, (b) opens
# the optional dependency can of worms, and (c) not viable
# under Python 3 unless we fork off a Python 2 subprocess.
# Manually combining 'bzr ls' and 'bzr st' outputs just to
# produce a cosmetic warning message seems like overkill.
self.assertIn(
"some files listed as being under source control are missing:\n"
" missing.py",
sys.stderr.getvalue())