Last active
January 8, 2022 17:45
-
-
Save ssokolow/a434d644d9cae60fb826a164bb3aef78 to your computer and use it in GitHub Desktop.
Quick script to find Python files which may need porting to run on Python 3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 | |
log.info("Analyzing %s", path) | |
try: | |
with open(path, 'r') as fobj: | |
has_shebang = fobj.read(2) == '#!' | |
if has_py_ext and not has_shebang: | |
log.warning(" NO #!: %s", path) | |
return | |
elif not has_shebang: | |
log.debug("Not Python: %s", path) | |
return | |
shebang_cmd = fobj.readline().rstrip('\n') | |
if 'python' in shebang_cmd and 'python3' not in shebang_cmd: | |
log.error(" NO PY3: %s", path) | |
return | |
except Exception as err: # pylint: disable=broad-except | |
if isinstance(err, UnicodeDecodeError): | |
if has_py_ext: | |
log.error("NOT UTF8: %s", path) | |
else: | |
log.debug("Probably binary: %s", path) | |
else: | |
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: | |
process_file(path) | |
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) | |
dirs.remove(name) | |
# Ignore Python 2.x stuff in tox virtual environments | |
while '.tox' in dirs: | |
dirs.remove('.tox') | |
# Skip virtualenvs | |
for dirname in dirs[:]: | |
if os.path.exists(os.path.join(pardir, dirname, | |
'bin', 'activate')): | |
dirs.remove(dirname) | |
dirs.sort() # Traverse in consistent, ASCIIbetical order | |
for fname in sorted(files): | |
if fname in ignored: | |
log.debug("Skipping ignored file: %s", fname) | |
continue | |
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) | |
logging.basicConfig(level=log_levels[args.verbose], | |
format='%(levelname)s: %(message)s') | |
for path in args.path: | |
process_arg(path, args.ignore) | |
if __name__ == '__main__': | |
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