Skip to content

Instantly share code, notes, and snippets.



Last active Feb 13, 2020
What would you like to do?
Quick script to find Python files which may need porting to run on Python 3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""A simple helper to search for Python scripts which still don't declare
python3 in their shebang lines as a proxy for finding un-migrated scripts.
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__appname__ = "Python 2.x Auditor"
__version__ = "0.1"
__license__ = "MIT"
import logging, os
log = logging.getLogger(__name__)
# Extensions to treat as Python source files even without a Python shebang
PY_SRC_EXTS = ('.py', '.pyw')
def process_file(path):
"""Check whether a file is potentially Python 2.x-only code"""
has_py_ext = os.path.splitext(path)[1].lower() in PY_SRC_EXTS"Analyzing %s", path)
with open(path, 'r') as fobj:
has_shebang = == '#!'
if has_py_ext and not has_shebang:
log.warning(" NO #!: %s", path)
elif not has_shebang:
log.debug("Not Python: %s", path)
shebang_cmd = fobj.readline().rstrip('\n')
if 'python' in shebang_cmd and 'python3' not in shebang_cmd:
log.error(" NO PY3: %s", path)
except Exception as err: # pylint: disable=broad-except
if isinstance(err, UnicodeDecodeError):
if has_py_ext:
log.error("NOT UTF8: %s", path)
log.debug("Probably binary: %s", path)
log.error("READ ERR: %s (%s)", err, path)
def process_arg(path, ignored=None):
"""Walk the given path, calling ``process_file`` on each file"""
ignored = ignored or []
if os.path.isfile(path) and os.path.basename(path) not in ignored:
elif os.path.isdir(path):
for pardir, dirs, files in os.walk(path):
log.debug("Entering %s", pardir)
# Skip ignored folders
for name in ignored:
while name in dirs:
log.debug("Skipping ignored folder: %s", name)
# Ignore Python 2.x stuff in tox virtual environments
while '.tox' in dirs:
# Skip virtualenvs
for dirname in dirs[:]:
if os.path.exists(os.path.join(pardir, dirname,
'bin', 'activate')):
dirs.sort() # Traverse in consistent, ASCIIbetical order
for fname in sorted(files):
if fname in ignored:
log.debug("Skipping ignored file: %s", fname)
process_file(os.path.join(pardir, fname))
def main():
"""The main entry point, compatible with setuptools entry points."""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
parser.add_argument('--version', action='version',
version="%%(prog)s v%s" % __version__)
parser.add_argument('-v', '--verbose', action="count",
default=2, help="Increase the verbosity. Use twice for extra effect.")
parser.add_argument('-q', '--quiet', action="count",
default=0, help="Decrease the verbosity. Use twice for extra effect.")
parser.add_argument('-i', '--ignore', action="append",
help="Ignore files or don't descend into folders with the given name.")
parser.add_argument('path', action="store", nargs="+",
help="Path to search for possible Python 2.x files")
args = parser.parse_args()
# Set up clean logging to stderr
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
args.verbose = max(args.verbose, 0)
format='%(levelname)s: %(message)s')
for path in args.path:
process_arg(path, args.ignore)
if __name__ == '__main__':
# vim: set sw=4 sts=4 expandtab :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment