diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..7df0e2a3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +*.py[cod] +.DS_Store +# C extensions +*.so + +# Packages +*.egg +*.egg-info +build +eggs +parts +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +MANIFEST + +# Installer logs +pip-log.txt +npm-debug.log + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml +htmlcov + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# SQLite +test_exp_framework + +# npm +node_modules/ + +# dolphin +.directory +libpeerconnection.log + +# IDE Files +atlassian-ide-plugin.xml +.idea/ +*.swp +*.kate-swp +.ropeproject/ diff --git a/README.md b/README.md index abafdbb5e..12b7e3ac2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,160 @@ isort ===== -A Python utility / library to sort imports. +isort your python imports you so you don't have to. + +isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections. +It provides a command line utility, Python library, and Kate plugin to quickly sort all your imports. + +Before isort: + + from my_lib import Object + + print("Hey") + + import os + + from my_lib import Object2 + + import sys + + from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14, lib15 + + import sys + + from __future__ import absolute_import + + from third_party import lib3 + +After isort: + + from __future__ import absolute_import + + import os + import sys + + from third_party import (lib1, + lib10, + lib11, + lib12, + lib13, + lib14, + lib15, + lib2, + lib3, + lib4, + lib5, + lib6, + lib7, + lib8, + lib9) + + from my_lib import Object, Object2 + + print("Hey") + +Installing isort +=================== +Installing isort is as simple as: + + pip install isort + +or if you prefer + + easy_install isort + +Using isort +=================== + +from the command line: + + isort mypythonfile.py mypythonfile2.py + +from within Python: + + from isort import SortImports + + SortImports("pythonfile.py") + +or + + from isort import SortImports + + new_contents = SortImports(file_contents=old_contents).output + +from within Kate: + + ctrl+[ + +or + + menu > Python > Sort Imports + +Installing isort's Kate plugin +=================== +To install the kate plugin you must either have pate installed or the very latest version of Kate: + + wget https://raw.github.com/timothycrosley/isort/master/kate_plugin.py --output-document ~/.kde/share/apps/kate/pate/isort.py + +You will then need to restart kate and enable Python Plugins as well as the isort plugin itself. + +Plugins for other text editors +=================== + +I use Kate, and Kate provides a very nice Python plugin API so I wrote a Kate plugin. +That said I will enthusiastically accept pull requests that include plugins for other text editors +and add documentation for them as I am notified. + +How does isort work? +==================== + +isort parses specified files for global level import lines (imports outside of try / excepts blocks, functions, etc..) +and puts them all at the top of the file grouped together by the type of import: + +- Future +- Python Standard Library +- Third Party +- Current Python Project + +Inside of each section the imports are sorted alphabetically. isort automatically removes duplicate python imports, +and wraps long from imports to the specified line length (defaults to 80). + +When will isort not work? +====================== + +If you ever have the situation where you need to have a try / except block in the middle of top-level imports or if +your import order is directly linked to precedence. + +For example: a common practice in Django settings files is importing * from various settings files to form +a new settings file. In this case if any of the imports change order you are changing the settings definition itself. + +However, you can configure isort to skip over just these files - or even to force certain imports to the top. + +Configuring isort +====================== + +If you find the default isort settings do not work well for your project, isort provides several ways to adjust +the behavior. + +To configure isort for a single user create a ~/.isort.cfg file: + [settings] + line_length=120 + force_to_top=file1.py,file2.py + skip=file3.py,file4.py + known_standard_libary=std,std2 + known_third_party=randomthirdparty + known_first_party=mylib1,mylib2 + +You can then override any of these settings by using command line arguments, or by passing in override values to the +SortImports class. + +Why isort? +====================== + +isort simply stands for import sort. It was originally called "sortImports" however I got tired of typing the extra +characters and came to the realization camelCase is not pythonic. + +I wrote isort because in an organization I used to work in the manager came in one day and decided all code must +have alphabetically sorted imports. The code base was huge - and he meant for us to do it by hand. However, being a +programmer - I'm too lazy to spend 8 hours mindlessly performing a function, but not too lazy to spend 16 +hours automating it. I was giving permission to open source sortImports and here we are :) diff --git a/dist/isort-1.0.0.tar.gz b/dist/isort-1.0.0.tar.gz new file mode 100644 index 000000000..d2917fd3e Binary files /dev/null and b/dist/isort-1.0.0.tar.gz differ diff --git a/isort/__init__.py b/isort/__init__.py new file mode 100644 index 000000000..ef12725ab --- /dev/null +++ b/isort/__init__.py @@ -0,0 +1,28 @@ +""" + __init__.py + + Defines the isort module to include the SortImports utility class as well as any defined settings. + + Copyright (C) 2013 Timothy Edmund Crosley + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from . import settings +from .isort import SortImports + +__version__ = "1.0.0" diff --git a/isort/isort.py b/isort/isort.py new file mode 100644 index 000000000..4d7129c82 --- /dev/null +++ b/isort/isort.py @@ -0,0 +1,292 @@ +""" + isort.py + + Exposes a simple library to sort through imports within Python code + + usage: + SortImports(file_name) + or: + sorted = SortImports(file_contents=file_contents).output + + Copyright (C) 2013 Timothy Edmund Crosley + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +import copy +import os +from pies import * +from sys import path as PYTHONPATH + +from . import settings + + +class Sections(object): + FUTURE = 1 + STDLIB = 2 + THIRDPARTY = 3 + FIRSTPARTY = 4 + ALL = (FUTURE, STDLIB, THIRDPARTY, FIRSTPARTY) + + +class SortImports(object): + config = settings.default + + def __init__(self, file_path=None, file_contents=None, **setting_overrides): + if setting_overrides: + self.config = settings.default.copy() + self.config.update(setting_overrides) + + if file_path: + file_name = file_path + if "/" in file_name: + file_name = file_name[file_name.rfind('/') + 1:] + if file_name in self.config['skip']: + print(file_path + ": refusing to proceed, listed in 'skip' setting.") + sys.exit(1) + self.file_path = file_path + with open(file_path) as file: + file_contents = file.read() + + if not file_contents: + return + + self.in_lines = file_contents.split("\n") + self.number_of_lines = len(self.in_lines) + if self.number_of_lines < 2: + print("File is too small!") + return + + self.out_lines = [] + self.imports = {} + self.as_map = {} + for section in Sections.ALL: + self.imports[section] = {'straight':set(), 'from':{}} + + self.index = 0 + self.import_index = -1 + self._parse() + if self.import_index != -1: + self._add_formatted_imports() + + self.output = "\n".join(self.out_lines) + if file_name: + with open(file_name, "w") as outputFile: + outputFile.write(self.output) + + def place_module(self, moduleName): + """Trys to determine if a module is a python std import, + third party import, or project code: + if it can't determine - it assumes it is project code + """ + index = moduleName.find('.') + if index: + firstPart = moduleName[:index] + else: + firstPart = None + + if moduleName == "__future__" or (firstPart == "__future__"): + return Sections.FUTURE + elif moduleName in self.config['known_standard_library'] or \ + (firstPart in self.config['known_standard_library']): + return Sections.STDLIB + elif moduleName in self.config['known_third_party'] or (firstPart in self.config['known_third_party']): + return Sections.THIRDPARTY + elif moduleName in self.config['known_first_party'] or (firstPart in self.config['known_first_party']): + return Sections.FIRSTPARTY + + for prefix in PYTHONPATH: + fixed_module_name = moduleName.replace('.', '/') + base_path = prefix + "/" + fixed_module_name + if os.path.exists(base_path + ".py") or os.path.exists(base_path) or os.path.exists(base_path + ".so"): + if "site-packages" in prefix or "dist-packages" in prefix: + return Sections.THIRDPARTY + elif "python2" in prefix.lower() or "python3" in prefix.lower(): + return Sections.STDLIB + else: + return Sections.FIRSTPARTY + + return Sections.FIRSTPARTY + + def _get_line(self): + """ Returns the current line from the file while + incrementing the index + """ + line = self.in_lines[self.index] + self.index += 1 + return line + + @staticmethod + def _import_type(line): + """ If the current line is an import line it will + return its type (from or straight) + """ + if line.startswith('import '): + return "straight" + if line.startswith('from ') and "import" in line: + return "from" + + def _at_end(self): + """ returns True if we are at the end of the file """ + return self.index == self.number_of_lines + + @staticmethod + def _module_key(module_name, config): + module_name = str(module_name).lower() + return "{0}{1}".format(module_name in config['force_to_top'] and "A" or "B", module_name) + + def _add_formatted_imports(self): + """ Adds the imports back to the file + (at the index of the first import) + sorted alphabetically and split between groups + """ + output = [] + for section in Sections.ALL: + straight_modules = list(self.imports[section]['straight']) + straight_modules.sort(key=lambda key: self._module_key(key, self.config)) + + for module in straight_modules: + if module in self.as_map: + output.append("import {0} as {1}".format(module, self.as_map[module])) + else: + output.append("import {0}".format(module)) + + from_modules = list(self.imports[section]['from'].keys()) + from_modules.sort(key=lambda key: self._module_key(key, self.config)) + for module in from_modules: + import_start = "from {0} import ".format(module) + from_imports = list(self.imports[section]['from'][module]) + from_imports.sort(key=lambda key: self._module_key(key, self.config)) + for from_import in copy.copy(from_imports): + import_as = self.as_map.get(module + "." + from_import, False) + if import_as: + output.append(import_start + "{0} as {1}".format(from_import, import_as)) + from_imports.remove(from_import) + + if from_imports: + if "*" in from_imports: + import_statement = "{0}*".format(import_start) + else: + import_statement = import_start + (", ").join(from_imports) + if len(import_statement) > self.config['line_length']: + import_statement = import_start + "(" + size = len(import_statement) + import_statement += (",\n" + " " * size).join(from_imports) + import_statement += ")" + + output.append(import_statement) + + if straight_modules or from_modules: + output.append("") + + while output[-1:] == [""]: + output.pop() + + if self.import_index + 2 < len(self.out_lines): + while self.out_lines[self.import_index + 1] == "": + self.out_lines.pop(self.import_index + 1) + + if len(self.out_lines) > self.import_index + 1: + next_construct = self.out_lines[self.import_index + 1] + if next_construct.startswith("def") or next_construct.startswith("class"): + output += ["", ""] + else: + output += [""] + + self.out_lines[self.import_index:1] = output + + @staticmethod + def _strip_comments(line): + """ + Removes comments from import line. + """ + comment_start = line.find("#") + if comment_start != -1: + print("Removing comment(%s) so imports can be sorted correctly" % line[comment_start:]) + line = line[:comment_start] + + return line + + def _parse(self): + """ + Parses a python file taking out and categorizing imports + """ + while True: + if self._at_end(): + return None + + line = self._get_line() + import_type = self._import_type(line) + if import_type: + if self.import_index == -1: + self.import_index = self.index - 1 + + import_string = self._strip_comments(line) + if "(" in line and not self._at_end(): + while not line.strip().endswith(")") and not self._at_end(): + line = self._strip_comments(self._get_line()) + import_string += "\n" + line + else: + while line.strip().endswith("\\"): + line = self._strip_comments(self._get_line()) + import_string += "\n" + line + + for remove_syntax in ['\\', '(', ')', ",", 'from ', 'import ']: + import_string = import_string.replace(remove_syntax, " ") + + imports = import_string.split() + if "as" in imports and import_type != 'from': + while True: + try: + index = imports.index('as') + except: + break + self.as_map[imports[0]] = imports[index + 1] + from_import = imports[0] + module_placment = self.place_module(from_import) + self.imports[module_placment][import_type].update([from_import]) + del imports[index -1:index + 1] + elif import_type == 'from' and "as" in imports: + while True: + try: + index = imports.index('as') + except: + break + from_import = imports[0] + self.as_map[from_import] = imports[index + 1] + module_placment = self.place_module(from_import) + imports = ["{0} as {1}".format(imports[index - 1], imports[index + 1])] + self.imports[module_placment][import_type].setdefualt(from_import, set()).update(imports) + del imports[index -1:index + 1] + if import_type == "from": + impot_from = imports.pop(0) + root = self.imports[self.place_module(impot_from)][import_type] + if root.get(impot_from, False): + root[impot_from].update(imports) + else: + root[impot_from] = set(imports) + else: + for module in imports: + self.imports[self.place_module(module)][import_type].add(module) + + if self._at_end(): + print(self.file_path + ": Either you have an import at the end of your file, or something" + " went horribly wrong!") + sys.exit(1) + + else: + self.out_lines.append(line) diff --git a/isort/settings.py b/isort/settings.py new file mode 100644 index 000000000..c4b854ceb --- /dev/null +++ b/isort/settings.py @@ -0,0 +1,52 @@ +""" + isort/settings.py + + Defines how the default settings for isort should be loaded + + (First from the default setting dictionary at the top of the file, then overridden by any settings + in ~/.isort.conf if there are any) + + Copyright (C) 2013 Timothy Edmund Crosley + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +import os +from configparser import SafeConfigParser +from pies import * + +default = {'force_to_top': [], + 'skip': ['__init__.py', ], + 'line_length': 80, + 'known_standard_library': ['os', 'sys', 'time', 'copy', 're', '__builtin__', 'thread', 'signal', 'gc', + 'exceptions', 'email'], + 'known_third_party': ['google.appengine.api'], + 'known_first_party': []} + +try: + with open(os.path.expanduser('~/.isort.cfg')) as config_file: + config = SafeConfigParser() + config.readfp(config_file) + settings = dict(config.items('settings')) + for key, value in iteritems(settings): + existing_value_type = type(default.get(key, '')) + if existing_value_type in (list, tuple): + default[key.lower()] = value.split(",") + else: + default[key.lower()] = existing_value_type(value) +except EnvironmentError: + pass diff --git a/kate_plugin.py b/kate_plugin.py new file mode 100644 index 000000000..be00baadb --- /dev/null +++ b/kate_plugin.py @@ -0,0 +1,34 @@ +""" + isort/kate_plugin.py + + Provides a simple kate plugin that enables the use of isort to sort Python imports + in the currently open kate file. + + Copyright (C) 2013 Timothy Edmund Crosley + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + +from PyKDE4.ktexteditor import KTextEditor + +import kate +from isort import SortImports + + +@kate.action(text="Sort Imports", shortcut="Ctrl+[", menu="Python") +def sortImports(): + document = kate.activeDocument() + document.setText(SortImports(file_content=document.text()).output) + document.activeView().setCursorPosition(KTextEditor.Cursor(0, 0)) diff --git a/scripts/isort b/scripts/isort new file mode 100755 index 000000000..dc2a25b11 --- /dev/null +++ b/scripts/isort @@ -0,0 +1,30 @@ +#! /usr/bin/env python +''' + Tool for sorting imports alphabetically, and automatically separated into sections. +''' +from __future__ import absolute_import, division, print_function, unicode_literals + +import argparse +from pies import * + +from isort import __version__, SortImports + +parser = argparse.ArgumentParser(description="Sort Python import definitions alphabetically within logical sections.") +parser.add_argument("files", nargs="+", help="One or more Python source files that need their imports sorted.") +parser.add_argument("-l", "--lines", help="The max length of an import line (used for wrapping long imports).", + dest="line_length", type=int) +parser.add_argument("-s", "--skip", help="Files that sort imports should skip over.", dest="skip", action="append") +parser.add_argument("-t", "--top", help="Force specific imports to the top of their appropriate section.", + dest="force_to_top", action="append") +parser.add_argument("-b", "--builtin", dest="known_standard_library", action="append", + help="Force sortImports to recognize a module as part of the python standard library.") +parser.add_argument("-o", "--thirdparty", dest="known_third_party", action="append", + help="Force sortImports to recognize a module as being part of a third party library.") +parser.add_argument("-p", "--project", dest="known_first_party", action="append", + help="Force sortImports to recognize a module as being part of the current python project.") + +parser.add_argument('--version', action='version', version='isort {0}'.format(__version__)) + +arguments = dict((key, value) for (key, value) in iteritems(vars(parser.parse_args())) if value) +for file_name in arguments.pop('files', []): + SortImports(file_name, **arguments) diff --git a/setup.py b/setup.py new file mode 100755 index 000000000..b20ee6982 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='isort', + version='1.0.0', + description='A Python utility / library to sort Python imports.', + author='Timothy Crosley', + author_email='timothy.crosley@gmail.com', + url='https://github.com/timothycrosley/isort', + download_url='https://github.com/timothycrosley/isort/blob/master' + '/dist/isort-1.0.0.tar.gz?raw=true', + license="GNU GPLv2", + scripts=['scripts/isort'], + packages=['isort'], + requires=['pies'], + install_requires=['pies>=1.0.2'])