Use tox to manage virtualenvs.

Obsoletes #463.
Fixes #558.
Fixes part of #474.
Closes #479.
Closes #452.
This commit is contained in:
Florian Bruhin 2015-03-26 08:00:20 +01:00
parent 07da31e2a0
commit 1d29e3462f
5 changed files with 392 additions and 0 deletions

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
Jinja2==2.7.3
MarkupSafe==0.23
Pygments==2.0.2
pyPEG2==2.15.1
PyYAML==3.11
colorama==0.3.3
colorlog==2.6.0

142
scripts/link_pyqt.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Symlink PyQt into a given virtualenv."""
import os
import argparse
import shutil
import os.path
import sys
import glob
import subprocess
class Error(Exception):
"""Exception raised when linking fails."""
pass
def verbose_copy(src, dst, *, follow_symlinks=True):
"""Copy function for shutil.copytree which prints copied files."""
print('{} -> {}'.format(src, dst))
shutil.copy(src, dst, follow_symlinks=follow_symlinks)
def get_ignored_files(directory, files):
"""Get the files which should be ignored for link_pyqt() on Windows."""
needed_exts = ('.py', '.dll', '.pyd', '.so')
ignored_dirs = ('examples', 'qml', 'uic', 'doc')
filtered = []
for f in files:
ext = os.path.splitext(f)[1]
full_path = os.path.join(directory, f)
if os.path.isdir(full_path) and f in ignored_dirs:
filtered.append(f)
elif (ext not in needed_exts) and os.path.isfile(full_path):
filtered.append(f)
return filtered
def link_pyqt(sys_path, venv_path):
"""Symlink the systemwide PyQt/sip into the venv.
Args:
sys_path: The path to the system-wide site-packages.
venv_path: The path to the virtualenv site-packages.
"""
globbed_sip = (glob.glob(os.path.join(sys_path, 'sip*.so')) +
glob.glob(os.path.join(sys_path, 'sip*.pyd')))
if not globbed_sip:
raise Error("Did not find sip in {}!".format(sys_path))
files = ['PyQt5']
files += [os.path.basename(e) for e in globbed_sip]
for fn in files:
source = os.path.join(sys_path, fn)
dest = os.path.join(venv_path, fn)
if not os.path.exists(source):
raise FileNotFoundError(source)
if os.path.exists(dest):
if os.path.isdir(dest) and not os.path.islink(dest):
shutil.rmtree(dest)
else:
os.unlink(dest)
if os.name == 'nt':
if os.path.isdir(source):
shutil.copytree(source, dest, ignore=get_ignored_files,
copy_function=verbose_copy)
else:
print('{} -> {}'.format(source, dest))
shutil.copy(source, dest)
else:
print('{} -> {}'.format(source, dest))
os.symlink(source, dest)
def get_python_lib(executable):
"""Get the python site-packages directory for the given executable."""
return subprocess.check_output(
[executable, '-c', 'from distutils.sysconfig import get_python_lib\n'
'print(get_python_lib())'], universal_newlines=True).rstrip()
def get_tox_syspython(venv_path):
"""Get the system python based on a virtualenv created by tox."""
path = os.path.join(venv_path, '.tox-config1')
with open(path, encoding='ascii') as f:
line = f.readline()
_md5, sys_python = line.rstrip().split(' ')
return sys_python
def main():
"""Main entry point."""
parser = argparse.ArgumentParser()
parser.add_argument('path', help="Base path to the venv.")
parser.add_argument('site_path', help="Path to the venv site-packages "
"folder (for tox).", nargs='?')
parser.add_argument('--tox', help="Add when called via tox.",
action='store_true')
args = parser.parse_args()
if args.tox:
sys_path = get_python_lib(get_tox_syspython(args.path))
else:
sys_path = get_python_lib(sys.executable)
if args.site_path is None:
subdir = 'Scripts' if os.name == 'nt' else 'bin'
executable = os.path.join(args.path, subdir, 'python')
venv_path = get_python_lib(executable)
else:
venv_path = args.site_path
link_pyqt(sys_path, venv_path)
if __name__ == '__main__':
try:
main()
except Error as e:
print(str(e), file=sys.stderr)
sys.exit(1)

104
scripts/misc_checks.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Various small code checkers."""
import os
import sys
import os.path
import argparse
import subprocess
import tokenize
import traceback
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils
def check_git():
"""Check for uncommited git files.."""
if not os.path.isdir(".git"):
print("No .git dir, ignoring")
print()
return False
untracked = []
changed = []
gitst = subprocess.check_output(['git', 'status', '--porcelain'])
gitst = gitst.decode('UTF-8').strip()
for line in gitst.splitlines():
s, name = line.split(maxsplit=1)
if s == '??' and name != '.venv/':
untracked.append(name)
elif s == 'M':
changed.append(name)
status = True
if untracked:
status = False
utils.print_col("Untracked files:", 'red')
print('\n'.join(untracked))
if changed:
status = False
utils.print_col("Uncommited changes:", 'red')
print('\n'.join(changed))
print()
return status
def check_vcs_conflict(target):
"""Check VCS conflict markers."""
try:
ok = True
for (dirpath, _dirnames, filenames) in os.walk(target):
for name in (e for e in filenames if e.endswith('.py')):
fn = os.path.join(dirpath, name)
with tokenize.open(fn) as f:
for line in f:
if any(line.startswith(c * 7) for c in '<>=|'):
print("Found conflict marker in {}".format(fn))
ok = False
print()
return ok
except Exception:
traceback.print_exc()
return None
def main():
"""Main entry point."""
parser = argparse.ArgumentParser()
parser.add_argument('checker', choices=('git', 'vcs'),
help="Which checker to run.")
parser.add_argument('target', help="What to check", nargs='*')
args = parser.parse_args()
if args.checker == 'git':
ok = check_git()
return 0 if ok else 1
elif args.checker == 'vcs':
is_ok = True
for target in args.target:
ok = check_vcs_conflict(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
if __name__ == '__main__':
sys.exit(main())

61
scripts/print_coverage.py Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Print the unittest coverage based on the HTML output.
This can probably be deleted when switching to py.test
"""
import os.path
import html.parser
class Parser(html.parser.HTMLParser):
"""HTML parser to get the percentage from coverage's HTML output."""
def __init__(self):
super().__init__(convert_charrefs=True)
self._active = False
self.percentage = None
def handle_starttag(self, tag, attrs):
if tag == 'span' and dict(attrs).get('class', None) == 'pc_cov':
self._active = True
def handle_endtag(self, _tag):
self._active = False
def handle_data(self, data):
if self._active:
self.percentage = data
def main():
"""Main entry point."""
p = Parser()
with open(os.path.join('htmlcov', 'index.html'), encoding='utf-8') as f:
p.feed(f.read())
print('COVERAGE: {}'.format(p.percentage))
if __name__ == '__main__':
main()

78
tox.ini Normal file
View File

@ -0,0 +1,78 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest
[testenv]
basepython = python3
[testenv:mkvenv]
commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envsitepackagesdir}
envdir = {toxinidir}/.venv
usedevelop = true
[testenv:unittests]
deps =
coverage==3.7.1
commands =
{[testenv:mkvenv]commands}
{envpython} -m coverage erase
{envpython} -m coverage run --branch --source=qutebrowser -m unittest
{envpython} -m coverage html
{envpython} scripts/print_coverage.py
[testenv:misc]
commands =
{envpython} scripts/misc_checks.py git
{envpython} scripts/misc_checks.py vcs qutebrowser scripts
[testenv:pylint]
skip_install = true
setenv = PYTHONPATH={toxinidir}/scripts:{env:PYTHONPATH:}
deps =
-rrequirements.txt
astroid==1.3.6
beautifulsoup4==4.3.2
pylint==1.4.3
logilab-common==0.63.2
six==1.9.0
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no
[testenv:pep257]
skip_install = true
deps = pep257==0.5.0
# Disabled checks:
# D102: Docstring missing, will be handled by others
# D209: Blank line before closing """ (removed from PEP257)
# D402: First line should not be function's signature (false-positives)
commands = {envdir}/bin/pep257 scripts qutebrowser --ignore=D102,D209,D402 '--match=(?!resources|test_content_disposition).*\.py'
[testenv:flake8]
deps =
-rrequirements.txt
pyflakes==0.8.1
pep8==1.5.7 # rq.filter: <=1.5.7
flake8==2.4.0
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/flake8 scripts qutebrowser --config=.flake8
[testenv:pyroma]
deps =
pyroma==1.7
docutils==0.12
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/pyroma .
[testenv:check-manifest]
deps =
check-manifest==0.23
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'