summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore53
-rw-r--r--README.md158
-rw-r--r--isort/__init__.py28
-rw-r--r--isort/isort.py292
-rw-r--r--isort/settings.py52
-rw-r--r--kate_plugin.py34
-rwxr-xr-xscripts/isort30
-rwxr-xr-xsetup.py17
8 files changed, 663 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..5c2d26e3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,53 @@
+*.py[cod]
+.DS_Store
+# C extensions
+*.so
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+var
+sdist
+develop-eggs
+.installed.cfg
+lib
+lib64
+
+# 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 abafdbb5..12b7e3ac 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/isort/__init__.py b/isort/__init__.py
new file mode 100644
index 00000000..ef12725a
--- /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 00000000..4d7129c8
--- /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 00000000..c4b854ce
--- /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 00000000..be00baad
--- /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 00000000..dc2a25b1
--- /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 00000000..38366a06
--- /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/SortImports/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'])