1#! /usr/bin/env python 2''' Tool for sorting imports alphabetically, and automatically separated into sections. 3 4Copyright (C) 2013 Timothy Edmund Crosley 5 6Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 7documentation files (the "Software"), to deal in the Software without restriction, including without limitation 8the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 9to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 11The above copyright notice and this permission notice shall be included in all copies or 12substantial portions of the Software. 13 14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 15TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 18OTHER DEALINGS IN THE SOFTWARE. 19 20''' 21from __future__ import absolute_import, division, print_function, unicode_literals 22 23import argparse 24import glob 25import os 26import sys 27 28import setuptools 29 30from isort import SortImports, __version__ 31from isort.settings import DEFAULT_SECTIONS, default, from_path, should_skip 32 33from .pie_slice import itemsview 34 35 36INTRO = r""" 37/#######################################################################\ 38 39 `sMMy` 40 .yyyy- ` 41 ##soos## ./o. 42 ` ``..-..` ``...`.`` ` ```` ``-ssso``` 43 .s:-y- .+osssssso/. ./ossss+:so+:` :+o-`/osso:+sssssssso/ 44 .s::y- osss+.``.`` -ssss+-.`-ossso` ssssso/::..::+ssss:::. 45 .s::y- /ssss+//:-.` `ssss+ `ssss+ sssso` :ssss` 46 .s::y- `-/+oossssso/ `ssss/ sssso ssss/ :ssss` 47 .y-/y- ````:ssss` ossso. :ssss: ssss/ :ssss. 48 `/so:` `-//::/osss+ `+ssss+-/ossso: /sso- `osssso/. 49 \/ `-/oooo++/- .:/++:/++/-` .. `://++/. 50 51 52 isort your Python imports for you so you don't have to 53 54 VERSION {0} 55 56\########################################################################/ 57""".format(__version__) 58 59 60def iter_source_code(paths, config, skipped): 61 """Iterate over all Python source files defined in paths.""" 62 for path in paths: 63 if os.path.isdir(path): 64 if should_skip(path, config, os.getcwd()): 65 skipped.append(path) 66 continue 67 68 for dirpath, dirnames, filenames in os.walk(path, topdown=True): 69 for dirname in list(dirnames): 70 if should_skip(dirname, config, dirpath): 71 skipped.append(dirname) 72 dirnames.remove(dirname) 73 for filename in filenames: 74 if filename.endswith('.py'): 75 if should_skip(filename, config, dirpath): 76 skipped.append(filename) 77 else: 78 yield os.path.join(dirpath, filename) 79 else: 80 yield path 81 82 83class ISortCommand(setuptools.Command): 84 """The :class:`ISortCommand` class is used by setuptools to perform 85 imports checks on registered modules. 86 """ 87 88 description = "Run isort on modules registered in setuptools" 89 user_options = [] 90 91 def initialize_options(self): 92 default_settings = default.copy() 93 for (key, value) in itemsview(default_settings): 94 setattr(self, key, value) 95 96 def finalize_options(self): 97 "Get options from config files." 98 self.arguments = {} 99 computed_settings = from_path(os.getcwd()) 100 for (key, value) in itemsview(computed_settings): 101 self.arguments[key] = value 102 103 def distribution_files(self): 104 """Find distribution packages.""" 105 # This is verbatim from flake8 106 if self.distribution.packages: 107 package_dirs = self.distribution.package_dir or {} 108 for package in self.distribution.packages: 109 pkg_dir = package 110 if package in package_dirs: 111 pkg_dir = package_dirs[package] 112 elif '' in package_dirs: 113 pkg_dir = package_dirs[''] + os.path.sep + pkg_dir 114 yield pkg_dir.replace('.', os.path.sep) 115 116 if self.distribution.py_modules: 117 for filename in self.distribution.py_modules: 118 yield "%s.py" % filename 119 # Don't miss the setup.py file itself 120 yield "setup.py" 121 122 def run(self): 123 arguments = self.arguments 124 wrong_sorted_files = False 125 arguments['check'] = True 126 for path in self.distribution_files(): 127 for python_file in glob.iglob(os.path.join(path, '*.py')): 128 try: 129 incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted 130 if incorrectly_sorted: 131 wrong_sorted_files = True 132 except IOError as e: 133 print("WARNING: Unable to parse file {0} due to {1}".format(python_file, e)) 134 if wrong_sorted_files: 135 exit(1) 136 137 138def create_parser(): 139 parser = argparse.ArgumentParser(description='Sort Python import definitions alphabetically ' 140 'within logical sections.') 141 parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.') 142 parser.add_argument('-y', '--apply', dest='apply', action='store_true', 143 help='Tells isort to apply changes recursively without asking') 144 parser.add_argument('-l', '--lines', help='[Deprecated] The max length of an import line (used for wrapping ' 145 'long imports).', 146 dest='line_length', type=int) 147 parser.add_argument('-w', '--line-width', help='The max length of an import line (used for wrapping long imports).', 148 dest='line_length', type=int) 149 parser.add_argument('-s', '--skip', help='Files that sort imports should skip over. If you want to skip multiple ' 150 'files you should specify twice: --skip file1 --skip file2.', dest='skip', action='append') 151 parser.add_argument('-ns', '--dont-skip', help='Files that sort imports should never skip over.', 152 dest='not_skip', action='append') 153 parser.add_argument('-sg', '--skip-glob', help='Files that sort imports should skip over.', dest='skip_glob', 154 action='append') 155 parser.add_argument('-t', '--top', help='Force specific imports to the top of their appropriate section.', 156 dest='force_to_top', action='append') 157 parser.add_argument('-f', '--future', dest='known_future_library', action='append', 158 help='Force sortImports to recognize a module as part of the future compatibility libraries.') 159 parser.add_argument('-b', '--builtin', dest='known_standard_library', action='append', 160 help='Force sortImports to recognize a module as part of the python standard library.') 161 parser.add_argument('-o', '--thirdparty', dest='known_third_party', action='append', 162 help='Force sortImports to recognize a module as being part of a third party library.') 163 parser.add_argument('-p', '--project', dest='known_first_party', action='append', 164 help='Force sortImports to recognize a module as being part of the current python project.') 165 parser.add_argument('--virtual-env', dest='virtual_env', 166 help='Virtual environment to use for determining whether a package is third-party') 167 parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], 168 help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' 169 '5-vert-grid-grouped).') 170 parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', 171 dest='indent', type=str) 172 parser.add_argument('-a', '--add-import', dest='add_imports', action='append', 173 help='Adds the specified import line to all files, ' 174 'automatically determining correct placement.') 175 parser.add_argument('-af', '--force-adds', dest='force_adds', action='store_true', 176 help='Forces import adds even if the original file is empty.') 177 parser.add_argument('-r', '--remove-import', dest='remove_imports', action='append', 178 help='Removes the specified import from all files.') 179 parser.add_argument('-ls', '--length-sort', help='Sort imports by their string length.', 180 dest='length_sort', action='store_true') 181 parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', 182 dest='write_to_stdout', action='store_true') 183 parser.add_argument('-c', '--check-only', action='store_true', dest="check", 184 help='Checks the file for unsorted / unformatted imports and prints them to the ' 185 'command line without modifying the file.') 186 parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", 187 help='Tells isort to ignore whitespace differences when --check-only is being used.') 188 parser.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', 189 help='Forces all from imports to appear on their own line') 190 parser.add_argument('-ds', '--no-sections', help='Put all imports into the same section bucket', dest='no_sections', 191 action='store_true') 192 parser.add_argument('-sd', '--section-default', dest='default_section', 193 help='Sets the default section for imports (by default FIRSTPARTY) options: ' + 194 str(DEFAULT_SECTIONS)) 195 parser.add_argument('-df', '--diff', dest='show_diff', action='store_true', 196 help="Prints a diff of all the changes isort would make to a file, instead of " 197 "changing it in place") 198 parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', 199 help='Balances wrapping to produce the most consistent line length possible') 200 parser.add_argument('-rc', '--recursive', dest='recursive', action='store_true', 201 help='Recursively look for Python files of which to sort imports') 202 parser.add_argument('-ot', '--order-by-type', dest='order_by_type', 203 action='store_true', help='Order imports by type in addition to alphabetically') 204 parser.add_argument('-dt', '--dont-order-by-type', dest='dont_order_by_type', 205 action='store_true', help='Only order imports alphabetically, do not attempt type ordering') 206 parser.add_argument('-ac', '--atomic', dest='atomic', action='store_true', 207 help="Ensures the output doesn't save if the resulting file contains syntax errors.") 208 parser.add_argument('-cs', '--combine-star', dest='combine_star', action='store_true', 209 help="Ensures that if a star import is present, nothing else is imported from that namespace.") 210 parser.add_argument('-ca', '--combine-as', dest='combine_as_imports', action='store_true', 211 help="Combines as imports on the same line.") 212 parser.add_argument('-tc', '--trailing-comma', dest='include_trailing_comma', action='store_true', 213 help='Includes a trailing comma on multi line imports that include parentheses.') 214 parser.add_argument('-v', '--version', action='store_true', dest='show_version') 215 parser.add_argument('-vb', '--verbose', action='store_true', dest="verbose", 216 help='Shows verbose output, such as when files are skipped or when a check is successful.') 217 parser.add_argument('-q', '--quiet', action='store_true', dest="quiet", 218 help='Shows extra quiet output, only errors are outputted.') 219 parser.add_argument('-sp', '--settings-path', dest="settings_path", 220 help='Explicitly set the settings path instead of auto determining based on file location.') 221 parser.add_argument('-ff', '--from-first', dest='from_first', 222 help="Switches the typical ordering preference, showing from imports first then straight ones.") 223 parser.add_argument('-wl', '--wrap-length', dest='wrap_length', 224 help="Specifies how long lines that are wrapped should be, if not set line_length is used.") 225 parser.add_argument('-fgw', '--force-grid-wrap', nargs='?', const=2, type=int, dest="force_grid_wrap", 226 help='Force number of from imports (defaults to 2) to be grid wrapped regardless of line ' 227 'length') 228 parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', 229 dest="force_alphabetical_sort", help='Force all imports to be sorted alphabetically within a ' 230 'section') 231 parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", 232 help='Force all imports to be sorted as a single section') 233 parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", 234 help='Force imports to be sorted by module, independent of import_type') 235 parser.add_argument('-lbt', '--lines-between-types', dest='lines_between_types', type=int) 236 parser.add_argument('-up', '--use-parentheses', dest='use_parentheses', action='store_true', 237 help='Use parenthesis for line continuation on lenght limit instead of slashes.') 238 239 arguments = dict((key, value) for (key, value) in itemsview(vars(parser.parse_args())) if value) 240 if 'dont_order_by_type' in arguments: 241 arguments['order_by_type'] = False 242 return arguments 243 244 245def main(): 246 arguments = create_parser() 247 if arguments.get('show_version'): 248 print(INTRO) 249 return 250 251 if 'settings_path' in arguments: 252 sp = arguments['settings_path'] 253 arguments['settings_path'] = os.path.abspath(sp) if os.path.isdir(sp) else os.path.dirname(os.path.abspath(sp)) 254 255 file_names = arguments.pop('files', []) 256 if file_names == ['-']: 257 SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) 258 else: 259 if not file_names: 260 file_names = ['.'] 261 arguments['recursive'] = True 262 if not arguments.get('apply', False): 263 arguments['ask_to_apply'] = True 264 config = from_path(os.path.abspath(file_names[0]) or os.getcwd()).copy() 265 config.update(arguments) 266 wrong_sorted_files = False 267 skipped = [] 268 if arguments.get('recursive', False): 269 file_names = iter_source_code(file_names, config, skipped) 270 num_skipped = 0 271 if config['verbose'] or config.get('show_logo', False): 272 print(INTRO) 273 for file_name in file_names: 274 try: 275 sort_attempt = SortImports(file_name, **arguments) 276 incorrectly_sorted = sort_attempt.incorrectly_sorted 277 if arguments.get('check', False) and incorrectly_sorted: 278 wrong_sorted_files = True 279 if sort_attempt.skipped: 280 num_skipped += 1 281 except IOError as e: 282 print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) 283 if wrong_sorted_files: 284 exit(1) 285 286 num_skipped += len(skipped) 287 if num_skipped and not arguments.get('quiet', False): 288 if config['verbose']: 289 for was_skipped in skipped: 290 print("WARNING: {0} was skipped as it's listed in 'skip' setting" 291 " or matches a glob in 'skip_glob' setting".format(was_skipped)) 292 print("Skipped {0} files".format(num_skipped)) 293 294 295if __name__ == "__main__": 296 main() 297