├── setup.cfg ├── .gitignore ├── setup.py ├── README.md └── dewildcard /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | env 3 | venv 4 | .venv 5 | dist 6 | dewildcard.egg-info 7 | .vscode 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name="dewildcard", 10 | # packages = ['dewildcard'], 11 | scripts=['dewildcard'], 12 | version="0.2.2", 13 | description="Expand wildcard imports in Python code", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | author="Quentin Stafford-Fraser", 17 | author_email="quentin@pobox.com", 18 | url="http://github.com/quentinsf/dewildcard", 19 | download_url='https://github.com/quentinsf/dewildcard/tarball/0.2', 20 | keywords='pylint', 21 | classifiers=( 22 | "Programming Language :: Python :: 2", 23 | ) 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dewildcard 2 | 3 | A not-very-sophisticated script to replace Python wildcard import statements. 4 | 5 | ## Background 6 | 7 | In Python, wildcard import statements, such as: 8 | 9 | from foo import * 10 | from bar import * 11 | 12 | can be very convenient, but are now usually considered bad practice. If, later in your code, you encounter a symbol you don't recognise, how do you know whether it came from *foo* or *bar*? And if you install an updated version of *bar*, it may define a new name that clashes with one in *foo* that you were using in your code. 13 | 14 | It's much better, therefore, to say: 15 | 16 | from foo import item1, item2 17 | from bar import item3, item4, item5 18 | 19 | even though it's more verbose. 20 | 21 | Tools like *pylint* of *pyflakes* can also let you know if you can safely delete, say, 'item5', because it isn't used in your code. If you have a good text editor, it may have a plugin which can highlight this fact. 22 | 23 | This little script reads some python code on stdin and, when it finds a wildcard import statement, does the import and replaces the line with a full multi-line import statement: 24 | 25 | from bar import (item3, 26 | item4, 27 | item5, 28 | item6) 29 | 30 | You can then easily go through and delete any items that pylint tells you aren't needed. 31 | 32 | If you prefer a single (possibly long) import line, you can use the `--single-line` option. 33 | One advantage is that some tools such as autoflake and autopep8 handle this format better. 34 | 35 | The parentheses are there to allow it to span multiple lines, but it shouldn't be too difficult to change the code to make it a single line or to use backslashes for line continuation if you prefer that. 36 | 37 | NOTE: This has many limitations, the main one being that dewildcard must actually *perform* the imports in order to extract the symbol names, so you must run this in an environment where the appropriate modules exist, are on the Python path, and can be imported without unfortunate side-effects. 38 | 39 | NOTE 2: Another increasingly-important limitation is that it won't currently cope with _relative_ imports. If your code says: 40 | 41 | from .foo import * 42 | 43 | then Python needs to know the starting point from which to calculate the relative locations. 44 | I've made some progress in this direction: you can try running this as a module and specifying a base package, but it's not quite right yet: 45 | 46 | mv dewildcard dewildcard.py 47 | python -m dewildcard --relative_to dir1.foo dir1/foo.py 48 | 49 | 50 | 51 | ## Installation 52 | 53 | pip install dewildcard 54 | 55 | Note that dewildcard makes use of importlib, so Python 2.7 or later will be needed. 56 | 57 | ## Example usage 58 | 59 | dewildcard my_code.py 60 | 61 | This outputs the modified file to stdout. If you wish to modify it in place, add a `-w` option: 62 | 63 | dewildcard -w my_code.py 64 | 65 | ## Acknowledgements 66 | 67 | Dewildcard is based on an initial idea from Alexandre Fayolle - thanks, Alexandre! 68 | 69 | Other contributors include Jan Bieroń, Jakub Wilk and Benedek Racz - thanks! 70 | 71 | ## To Do 72 | 73 | Lots of room for improvements here, including: 74 | 75 | * Options to change the output format 76 | 77 | ## Licence 78 | 79 | Such a simple script is barely worth a licence, but, for what it's worth, it's released under GNU General Public Licence v2. Use at your own risk, etc. 80 | 81 | (c) 2015 Quentin Stafford-Fraser 82 | Occasional updates since then. 83 | -------------------------------------------------------------------------------- /dewildcard: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # Remove wildcard imports from Python code. 4 | # 5 | # Takes Python code on stdin and copies it to stdout, 6 | # replacing any 'from foo import *' with a multi-line 7 | # import of all the symbols in foo. 8 | # 9 | # You can then use pylint or similar to identify and 10 | # delete the unneeded symbols. 11 | # 12 | # See http://github.com/quentinsf/dewildcard for info. 13 | # 14 | # Quentin Stafford-Fraser, 2015 15 | 16 | import argparse 17 | import importlib 18 | import os 19 | import re 20 | import sys 21 | 22 | 23 | def import_all_string(module_name, single_line=False, base_pkg_name=None): 24 | """ 25 | Return the import statement needed to import all the local 26 | symbols in module_name that don't start with '_'. 27 | 28 | If single_line, don't split result over multiple lines. 29 | 30 | base_pkg_name needs to be specified if the imports are relative. 31 | """ 32 | importlib.import_module(module_name, base_pkg_name) 33 | if single_line: 34 | import_line = "from %s import %%s\n" % module_name 35 | length = 0 36 | separator = ", " 37 | else: 38 | import_line = "from %s import ( %%s )\n" % module_name 39 | length = len(import_line) - 5 40 | separator = ",\n" 41 | 42 | return import_line % (separator + length * " ").join( 43 | [a for a in dir(sys.modules[module_name]) if not a.startswith("_")] 44 | ) 45 | 46 | 47 | def dewildcard(python_file, do_write=False, single_line=False, relative_to=False): 48 | """ 49 | Does the parsing and replacement and returns a list of lines. 50 | """ 51 | try: 52 | open(python_file) 53 | if do_write: 54 | open(python_file, "a") 55 | except IOError as e: 56 | sys.stderr.write(str(e) + "\n") 57 | sys.exit(1) 58 | 59 | # There may be relative imports in the code we're looking at. 60 | # We need to know the starting point for those relative links. 61 | # This probably needs refining to get sys.path right. 62 | if relative_to: 63 | sys.path.insert(0, ".") 64 | base_pkg = relative_to # importlib.import_module(relative_to).__name__ 65 | else: 66 | base_pkg = None 67 | 68 | import_all_re = re.compile(r"^\s*from\s*([\w.]*)\s*import\s*[*]") 69 | parsed_lines = [] 70 | with open(python_file) as f: 71 | for i, line in enumerate(f): 72 | match = import_all_re.match(line) 73 | if match: 74 | try: 75 | line = import_all_string(match.group(1), single_line, base_pkg) 76 | except: 77 | print( 78 | "ERROR occured while parsing: {}, line {}".format(python_file, i) 79 | ) 80 | print(" {}".format(line.splitlines()[0])) 81 | print(" matching group: >>{}<<".format(match.group(1))) 82 | print("") 83 | raise 84 | parsed_lines.append(line) 85 | return parsed_lines 86 | 87 | 88 | def main(): 89 | parser = argparse.ArgumentParser() 90 | parser.add_argument("file", help="input file") 91 | parser.add_argument( 92 | "-w", "--write", action="store_true", help="write in place instead of stdout" 93 | ) 94 | parser.add_argument( 95 | "--single-line", action="store_true", help="write imports on a single line" 96 | ) 97 | parser.add_argument( 98 | "--relative_to", 99 | help="import the given package and use it as the base for relative imports" 100 | ) 101 | args = parser.parse_args() 102 | 103 | parsed_lines = dewildcard(args.file, args.write, args.single_line, args.relative_to) 104 | 105 | dest = open(args.file, "w") if args.write else sys.stdout 106 | dest.writelines(parsed_lines) 107 | dest.close() 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | --------------------------------------------------------------------------------