Skip to content

Instantly share code, notes, and snippets.

@ssokolow ssokolow/
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
You can’t perform that action at this time.