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