Merge branch 'release/2.0.0'
diff --git a/.env b/.env
new file mode 100644
index 0000000..91eaaee
--- /dev/null
+++ b/.env
@@ -0,0 +1,43 @@
+#!/bin/bash
+OPEN_PROJECT_NAME="isort"
+
+if [ "$PROJECT_NAME" = "$OPEN_PROJECT_NAME" ]; then
+ return
+fi
+
+export PROJECT_NAME=$OPEN_PROJECT_NAME
+export PROJECT_DIR="$PWD"
+
+# Let's make sure this is a hubflow enabled repo
+yes | git hf init >/dev/null 2>/dev/null
+
+# Quick directory switching
+alias root="cd $PROJECT_DIR"
+alias project="root; cd $PROJECT_NAME"
+
+# Commands
+alias test="root; py.test"
+alias install="_install_project"
+alias distribute="sudo python setup.py sdist upload"
+alias leave="_leave_project"
+
+function _install_project()
+{
+ CURRENT_DIRECTORY="$PWD"
+ root
+ sudo python setup.py install
+ cp kate_plugin.py ~/.kde/share/apps/kate/pate/isort_plugin.py >/dev/null 2>/dev/null
+ cd $CURRENT_DIRECTORY
+}
+
+function _leave_project()
+{
+ export PROJECT_NAME=""
+ export PROJECT_DIR=""
+
+ unalias root
+ unalias project
+ unalias test
+ unalias install
+ unalias leave
+}
diff --git a/.gitignore b/.gitignore
index 4b66cf3..04c6f1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@
.tox
nosetests.xml
htmlcov
+.cache
# Translations
*.mo
diff --git a/README.md b/README.md
index 37aa214..4a83309 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
isort your python imports for 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.
+It provides a command line utility, Python library, Vim plugin, Sublime plugin, and Kate plugin to quickly sort all your imports.
Before isort:
@@ -83,6 +83,16 @@
menu > Python > Sort Imports
+Installing isort's Vim plugin
+===================
+The Vim plugin for isort is maintained by @fisadev with installation directions located on the dedicated vim-isort repository
+here: https://github.com/fisadev/vim-isort#installation
+
+Installing isort's Sublime plugin
+===================
+The sublime plugin for isort is maintained by @thijsdezoete with installation directions located on the dedicated sublime-text-isort-plugin
+repository here: https://github.com/thijsdezoete/sublime-text-isort-plugin#install
+
Installing isort's Kate plugin
===================
To install the kate plugin you must either have pate installed or the very latest version of Kate:
@@ -199,14 +209,49 @@
To make isort ignore a single import simply add a comment at the end of the import line containing the text 'isort:skip'
- import module # isort:skip
+ import module # isort:skip
or
- from xyz import (abc, # isort:skip
+ from xyz import (abc, # isort:skip
yo,
hey)
+Adding an import to multiple files
+======================
+
+isort makes it easy to add an import statement across multiple files, while being assured it's correctly placed.
+
+from the command line:
+
+ isort -a "from __future__ import print_function" *.py
+
+from within Kate:
+
+ ctrl+]
+
+or:
+
+ menu > Python > Add Import
+
+Removing an import from multiple files
+======================
+
+isort makes it easy to remove an import from multiple files, without having to be concerned with how it was originally
+formatted
+
+from the command line:
+
+ isort -r "os.system" *.py
+
+from within Kate:
+
+ ctrl+shift+]
+
+or:
+
+ menu > Python > Remove Import
+
Why isort?
======================
diff --git a/isort/__init__.py b/isort/__init__.py
index a2aa0bc..9d68f91 100644
--- a/isort/__init__.py
+++ b/isort/__init__.py
@@ -25,4 +25,4 @@
from . import settings
from .isort import SortImports
-__version__ = "1.3.2"
+__version__ = "2.0.0"
diff --git a/isort/isort.py b/isort/isort.py
index 6a77b68..cfbc6e3 100644
--- a/isort/isort.py
+++ b/isort/isort.py
@@ -75,6 +75,8 @@
return
self.in_lines = file_contents.split("\n")
+ for add_import in self.config['add_imports']:
+ self.in_lines.append(add_import)
self.number_of_lines = len(self.in_lines)
self.out_lines = []
@@ -95,7 +97,7 @@
output_file.write(self.output)
def place_module(self, moduleName):
- """Trys to determine if a module is a python std import,
+ """Tries 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
"""
@@ -122,7 +124,7 @@
fixed_module_name = moduleName.replace('.', '/')
base_path = prefix + "/" + fixed_module_name
if (os.path.exists(base_path + ".py") or os.path.exists(base_path + ".so") or
- (os.path.exists(base_path) and os.path.isdir(base_path))):
+ (os.path.exists(base_path) and os.path.isdir(base_path))):
if "site-packages" in prefix or "dist-packages" in prefix:
return Sections.THIRDPARTY
elif "python2" in prefix.lower() or "python3" in prefix.lower():
@@ -160,7 +162,7 @@
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",
- config['length_sort'] and len(module_name) or module_name)
+ config['length_sort'] and (str(len(module_name)) + ":" + module_name) or module_name)
def _add_formatted_imports(self):
""" Adds the imports back to the file
@@ -170,9 +172,12 @@
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))
+ straight_modules = natsorted(straight_modules, key=lambda key: self._module_key(key, self.config))
for module in straight_modules:
+ if module in self.config['remove_imports']:
+ continue
+
if module in self.as_map:
output.append("import {0} as {1}".format(module, self.as_map[module]))
else:
@@ -181,9 +186,16 @@
from_modules = list(self.imports[section]['from'].keys())
from_modules = natsorted(from_modules, key=lambda key: self._module_key(key, self.config))
for module in from_modules:
+ if module in self.config['remove_imports']:
+ continue
+
import_start = "from {0} import ".format(module)
from_imports = list(self.imports[section]['from'][module])
from_imports = natsorted(from_imports, key=lambda key: self._module_key(key, self.config))
+ if self.config['remove_imports']:
+ from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in
+ self.config['remove_imports']]
+
for from_import in copy.copy(from_imports):
import_as = self.as_map.get(module + "." + from_import, False)
if import_as:
@@ -266,50 +278,47 @@
"""
Parses a python file taking out and categorizing imports
"""
- while True:
- if self._at_end():
- return None
-
+ while not self._at_end():
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
-
- import_string = import_string.replace("_import", "[[i]]")
- for remove_syntax in ['\\', '(', ')', ",", 'from ', 'import ']:
- import_string = import_string.replace(remove_syntax, " ")
- import_string = import_string.replace("[[i]]", "_import")
-
- imports = import_string.split()
- if "as" in imports:
- while "as" in imports:
- index = imports.index('as')
- if import_type == "from":
- self.as_map[imports[0] + "." + imports[index -1]] = imports[index + 1]
- else:
- self.as_map[imports[index -1]] = imports[index + 1]
- del imports[index:index + 2]
- 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)
-
- else:
+ if not import_type:
self.out_lines.append(line)
+ continue
+
+ 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
+
+ import_string = import_string.replace("_import", "[[i]]")
+ for remove_syntax in ['\\', '(', ')', ",", 'from ', 'import ']:
+ import_string = import_string.replace(remove_syntax, " ")
+ import_string = import_string.replace("[[i]]", "_import")
+
+ imports = import_string.split()
+ if "as" in imports:
+ while "as" in imports:
+ index = imports.index('as')
+ if import_type == "from":
+ self.as_map[imports[0] + "." + imports[index -1]] = imports[index + 1]
+ else:
+ self.as_map[imports[index -1]] = imports[index + 1]
+ del imports[index:index + 2]
+ if import_type == "from":
+ import_from = imports.pop(0)
+ root = self.imports[self.place_module(import_from)][import_type]
+ if root.get(import_from, False):
+ root[import_from].update(imports)
+ else:
+ root[import_from] = set(imports)
+ else:
+ for module in imports:
+ self.imports[self.place_module(module)][import_type].add(module)
diff --git a/isort/settings.py b/isort/settings.py
index a0e767c..e7ddcbe 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -36,17 +36,36 @@
HANGING_INDENT = 2
VERTICAL_HANGING_INDENT = 3
-
+# Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails.
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_standard_library': ["abc", "anydbm", "argparse", "array", "asynchat", "asyncore", "atexit", "base64",
+ "BaseHTTPServer", "bisect", "bz2", "calendar", "cgitb", "cmd", "codecs",
+ "collections", "commands", "compileall", "ConfigParser", "contextlib", "Cookie",
+ "copy", "cPickle", "cProfile", "cStringIO", "csv", "datetime", "dbhash", "dbm",
+ "decimal", "difflib", "dircache", "dis", "doctest", "dumbdbm", "EasyDialogs",
+ "exceptions", "filecmp", "fileinput", "fnmatch", "fractions", "functools", "gc",
+ "gdbm", "getopt", "getpass", "gettext", "glob", "grp", "gzip", "hashlib", "heapq",
+ "hmac", "imaplib", "imp", "inspect", "itertools", "json", "linecache", "locale",
+ "logging", "mailbox", "math", "mhlib", "mmap", "multiprocessing", "operator",
+ "optparse", "os", "pdb", "pickle", "pipes", "pkgutil", "platform", "plistlib",
+ "pprint", "profile", "pstats", "pwd", "pyclbr", "pydoc", "Queue", "random",
+ "re", "readline", "resource", "rlcompleter", "robotparser", "sched", "select",
+ "shelve", "shlex", "shutil", "signal", "SimpleXMLRPCServer", "site",
+ "sitecustomize", "smtpd", "smtplib", "socket", "SocketServer", "sqlite3",
+ "string", "StringIO", "struct", "subprocess", "sys", "sysconfig", "tabnanny",
+ "tarfile", "tempfile", "textwrap", "threading", "time", "timeit", "trace",
+ "traceback", "unittest", "urllib", "urllib2", "urlparse", "usercustomize", "uuid",
+ "warnings", "weakref", "webbrowser", "whichdb", "xml", "xmlrpclib", "zipfile",
+ "zipimport", "zlib"],
'known_third_party': ['google.appengine.api'],
'known_first_party': [],
'multi_line_output': MultiLineOutput.GRID,
'indent': ' ' * 4,
- 'length_sort': False}
+ 'length_sort': False,
+ 'add_imports': [],
+ 'remove_imports': []}
try:
from configparser import SafeConfigParser
diff --git a/kate_plugin.py b/kate_plugin.py
index 25efaf4..b4e841e 100644
--- a/kate_plugin.py
+++ b/kate_plugin.py
@@ -22,13 +22,45 @@
"""
from isort import SortImports
-from PyKDE4.ktexteditor import KTextEditor
import kate
+try:
+ from PySide import QtGui
+except ImportError:
+ from PyQt4 import QtGui
+
@kate.action(text="Sort Imports", shortcut="Ctrl+[", menu="Python")
-def sortImports():
+def sort_imports():
document = kate.activeDocument()
+ view = document.activeView()
+ position = view.cursorPosition()
document.setText(SortImports(file_contents=document.text()).output)
- document.activeView().setCursorPosition(KTextEditor.Cursor(0, 0))
+ view.setCursorPosition(position)
+
+
+@kate.action(text="Add Import", shortcut="Ctrl+]", menu="Python")
+def add_imports():
+ text, ok = QtGui.QInputDialog.getText(None,
+ 'Add Import',
+ 'Enter an import line to add (example: from os import path):')
+ if ok:
+ document = kate.activeDocument()
+ view = document.activeView()
+ position = view.cursorPosition()
+ document.setText(SortImports(file_contents=document.text(), add_imports=text.split(";")).output)
+ view.setCursorPosition(position)
+
+
+@kate.action(text="Remove Import", shortcut="Ctrl+Shift+]", menu="Python")
+def remove_imports():
+ text, ok = QtGui.QInputDialog.getText(None,
+ 'Remove Import',
+ 'Enter an import line to remove (example: os.path):')
+ if ok:
+ document = kate.activeDocument()
+ view = document.activeView()
+ position = view.cursorPosition()
+ document.setText(SortImports(file_contents=document.text(), remove_imports=text.split(";")).output)
+ view.setCursorPosition(position)
diff --git a/scripts/isort b/scripts/isort
index 5ddabf2..21aac59 100755
--- a/scripts/isort
+++ b/scripts/isort
@@ -26,6 +26,10 @@
dest="multi_line_output", type=int, choices=[0, 1, 2, 3])
parser.add_argument("-i", "--indent", help="String to place for indents defaults to ' ' (4 spaces).",
dest="indent", type=str)
+parser.add_argument("-a", "--add_import", dest="add_imports", action="append",
+ help="Adds the specified import line to all files, automatically determining correct placement.")
+parser.add_argument("-r", "--remove_import", dest="remove_imports", action="append",
+ help="Removes the specified import from all files.")
parser.add_argument("-ls", "--length_sort", help="Sort imports by their string length.",
dest="length_sort", action="store_true", default=False)
parser.add_argument('--version', action='version', version='isort {0}'.format(__version__))
diff --git a/setup.py b/setup.py
index d4ba1e1..90ef579 100755
--- a/setup.py
+++ b/setup.py
@@ -6,12 +6,12 @@
from distutils.core import setup
setup(name='isort',
- version='1.3.2',
+ version='2.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/archive/1.3.2.tar.gz',
+ download_url='https://github.com/timothycrosley/isort/archive/2.0.0.tar.gz',
license="MIT",
scripts=['scripts/isort'],
packages=['isort'],
diff --git a/test_isort.py b/test_isort.py
new file mode 100644
index 0000000..801c619
--- /dev/null
+++ b/test_isort.py
@@ -0,0 +1,333 @@
+'''
+ test_isort.py
+
+ Tests all major functionality of the isort library
+ Should be ran using py.test by simply running by.test in the isort project directory
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or
+ substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+'''
+
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+from isort.isort import SortImports
+from isort.settings import MultiLineOutput
+
+REALLY_LONG_IMPORT = ("from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11,"
+ "lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22")
+
+
+def test_happy_path():
+ """
+ Test the most basic use case, straight imports no code, simply not organized by category.
+ """
+ test_input = ("import sys\n"
+ "import os\n"
+ "import myproject.test\n"
+ "import django.settings")
+ test_output = SortImports(file_contents=test_input, known_third_party=['django']).output
+ assert test_output == ("import os\n"
+ "import sys\n"
+ "\n"
+ "import django.settings\n"
+ "\n"
+ "import myproject.test")
+
+
+def test_code_intermixed():
+ """
+ Defines what should happen when isort encounters imports intermixed with code
+ (it should pull them all to the top).
+ """
+ test_input = ("import sys\n"
+ "print('yo')\n"
+ "print('I like to put code between imports cause I want stuff to break')\n"
+ "import myproject.test\n")
+ test_output = SortImports(file_contents=test_input).output
+ assert test_output == ("import sys\n"
+ "\n"
+ "import myproject.test\n"
+ "\n"
+ "print('yo')\n"
+ "print('I like to put code between imports cause I want stuff to break')\n")
+
+
+def test_correct_space_between_imports():
+ """
+ Ensure after imports a correct amount of space (in newlines) is enforced
+ (2 for method, class, or decorator definitions 1 for anything else)
+ """
+ test_input_method = ("import sys\n"
+ "def my_method():\n"
+ " print('hello world')\n")
+ test_output_method = SortImports(file_contents=test_input_method).output
+ assert test_output_method == ("import sys\n"
+ "\n"
+ "\n"
+ "def my_method():\n"
+ " print('hello world')\n")
+
+ test_input_decorator = ("import sys\n"
+ "@my_decorator\n"
+ "def my_method():\n"
+ " print('hello world')\n")
+ test_output_decorator = SortImports(file_contents=test_input_decorator).output
+ assert test_output_decorator == ("import sys\n"
+ "\n"
+ "\n"
+ "@my_decorator\n"
+ "def my_method():\n"
+ " print('hello world')\n")
+
+ test_input_class = ("import sys\n"
+ "class MyClass(object):\n"
+ " pass\n")
+ test_output_class = SortImports(file_contents=test_input_class).output
+ assert test_output_class == ("import sys\n"
+ "\n"
+ "\n"
+ "class MyClass(object):\n"
+ " pass\n")
+
+ test_input_other = ("import sys\n"
+ "print('yo')\n")
+ test_output_other = SortImports(file_contents=test_input_other).output
+ assert test_output_other == ("import sys\n"
+ "\n"
+ "print('yo')\n")
+
+
+def test_sort_on_number():
+ """
+ Ensure numbers get sorted logically (10 > 9 not the other way around).
+ """
+ test_input = ("import lib10\n"
+ "import lib9\n")
+ test_output = SortImports(file_contents=test_input).output
+ assert test_output == ("import lib9\n"
+ "import lib10")
+
+
+def test_line_length():
+ """
+ Ensure isort enforces the set line_length
+ """
+ assert len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split("\n")[0]) <= 80
+ assert len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split("\n")[0]) <= 120
+
+ test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=42).output
+ assert test_output == ("from third_party import (lib1, lib2, lib3,\n"
+ " lib4, lib5, lib6,\n"
+ " lib7, lib8, lib9,\n"
+ " lib10, lib11,\n"
+ " lib12, lib13,\n"
+ " lib14, lib15,\n"
+ " lib16, lib17,\n"
+ " lib18, lib20,\n"
+ " lib21, lib22)")
+
+
+def test_output_modes():
+ """
+ Test setting isort to use various output modes works as expected
+ """
+ test_output_grid = SortImports(file_contents=REALLY_LONG_IMPORT,
+ multi_line_output=MultiLineOutput.GRID, line_length=40).output
+ assert test_output_grid == ("from third_party import (lib1, lib2,\n"
+ " lib3, lib4,\n"
+ " lib5, lib6,\n"
+ " lib7, lib8,\n"
+ " lib9, lib10,\n"
+ " lib11, lib12,\n"
+ " lib13, lib14,\n"
+ " lib15, lib16,\n"
+ " lib17, lib18,\n"
+ " lib20, lib21,\n"
+ " lib22)")
+
+ test_output_vertical = SortImports(file_contents=REALLY_LONG_IMPORT,
+ multi_line_output=MultiLineOutput.VERTICAL, line_length=40).output
+ assert test_output_vertical == ("from third_party import (lib1,\n"
+ " lib2,\n"
+ " lib3,\n"
+ " lib4,\n"
+ " lib5,\n"
+ " lib6,\n"
+ " lib7,\n"
+ " lib8,\n"
+ " lib9,\n"
+ " lib10,\n"
+ " lib11,\n"
+ " lib12,\n"
+ " lib13,\n"
+ " lib14,\n"
+ " lib15,\n"
+ " lib16,\n"
+ " lib17,\n"
+ " lib18,\n"
+ " lib20,\n"
+ " lib21,\n"
+ " lib22)")
+
+ test_output_hanging_indent = SortImports(file_contents=REALLY_LONG_IMPORT,
+ multi_line_output=MultiLineOutput.HANGING_INDENT,
+ line_length=40, indent=" ").output
+ assert test_output_hanging_indent == ("from third_party import lib1, lib2, \\\n"
+ " lib3, lib4, lib5, lib6, lib7, \\\n"
+ " lib8, lib9, lib10, lib11, lib12, \\\n"
+ " lib13, lib14, lib15, lib16, lib17, \\\n"
+ " lib18, lib20, lib21, lib22")
+
+ test_output_vertical_indent = SortImports(file_contents=REALLY_LONG_IMPORT,
+ multi_line_output=MultiLineOutput.VERTICAL_HANGING_INDENT,
+ line_length=40, indent=" ").output
+ test_output_vertical_indent == ("from third_party import (\n"
+ " lib1,\n"
+ " lib2,\n"
+ " lib3,\n"
+ " lib4,\n"
+ " lib5,\n"
+ " lib6,\n"
+ " lib7,\n"
+ " lib8,\n"
+ " lib9,\n"
+ " lib10,\n"
+ " lib11,\n"
+ " lib12,\n"
+ " lib13,\n"
+ " lib14,\n"
+ " lib15,\n"
+ " lib16,\n"
+ " lib17,\n"
+ " lib18,\n"
+ " lib20,\n"
+ " lib21,\n"
+ " lib22\n"
+ ")")
+
+
+def test_length_sort():
+ """
+ Test setting isort to sort on length instead of alphabetically.
+ """
+ test_input = ("import medium_sizeeeeeeeeeeeeee\n"
+ "import shortie\n"
+ "import looooooooooooooooooooooooooooooooooooooong\n"
+ "import medium_sizeeeeeeeeeeeeea")
+ test_output = SortImports(file_contents=test_input, length_sort=True).output
+ assert test_output == ("import shortie\n"
+ "import medium_sizeeeeeeeeeeeeea\n"
+ "import medium_sizeeeeeeeeeeeeee\n"
+ "import looooooooooooooooooooooooooooooooooooooong")
+
+
+def test_convert_hanging():
+ """
+ Ensure that isort will convert hanging indents to correct indent method.
+ """
+ test_input = ("from third_party import lib1, lib2, \\\n"
+ " lib3, lib4, lib5, lib6, lib7, \\\n"
+ " lib8, lib9, lib10, lib11, lib12, \\\n"
+ " lib13, lib14, lib15, lib16, lib17, \\\n"
+ " lib18, lib20, lib21, lib22")
+ test_output = SortImports(file_contents=test_input, multi_line_output=MultiLineOutput.GRID,
+ line_length=40).output
+ assert test_output == ("from third_party import (lib1, lib2,\n"
+ " lib3, lib4,\n"
+ " lib5, lib6,\n"
+ " lib7, lib8,\n"
+ " lib9, lib10,\n"
+ " lib11, lib12,\n"
+ " lib13, lib14,\n"
+ " lib15, lib16,\n"
+ " lib17, lib18,\n"
+ " lib20, lib21,\n"
+ " lib22)")
+
+
+def test_custom_indent():
+ """
+ Ensure setting a custom indent will work as expected.
+ """
+ test_output = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=MultiLineOutput.HANGING_INDENT,
+ line_length=40, indent=" ").output
+ assert test_output == ("from third_party import lib1, lib2, \\\n"
+ " lib3, lib4, lib5, lib6, lib7, lib8, \\\n"
+ " lib9, lib10, lib11, lib12, lib13, \\\n"
+ " lib14, lib15, lib16, lib17, lib18, \\\n"
+ " lib20, lib21, lib22")
+
+
+def test_skip():
+ """
+ Ensure skipping a single import will work as expected.
+ """
+ test_input = ("import myproject\n"
+ "import django\n"
+ "print('hey')\n"
+ "import sys # isort:skip this import needs to be placed here")
+
+ test_output = SortImports(file_contents=test_input, known_third_party=['django']).output
+ assert test_output == ("import django\n"
+ "\n"
+ "import myproject\n"
+ "\n"
+ "print('hey')\n"
+ "import sys # isort:skip this import needs to be placed here")
+
+
+def test_force_to_top():
+ """
+ Ensure forcing a single import to the top of its category works as expected.
+ """
+ test_input = ("import lib6\n"
+ "import lib2\n"
+ "import lib5\n"
+ "import lib1")
+ test_output = SortImports(file_contents=test_input, force_to_top=['lib5']).output
+ assert test_output == ("import lib5\n"
+ "import lib1\n"
+ "import lib2\n"
+ "import lib6")
+
+
+def test_add_imports():
+ """
+ Ensures adding imports works as expected.
+ """
+ test_input = ("import lib6\n"
+ "import lib2\n"
+ "import lib5\n"
+ "import lib1")
+ test_output = SortImports(file_contents=test_input, add_imports=['import lib4', 'import lib7']).output
+ assert test_output == ("import lib1\n"
+ "import lib2\n"
+ "import lib4\n"
+ "import lib5\n"
+ "import lib6\n"
+ "import lib7")
+
+
+def test_remove_imports():
+ """
+ Ensures removing imports works as expected.
+ """
+ test_input = ("import lib6\n"
+ "import lib2\n"
+ "import lib5\n"
+ "import lib1")
+ test_output = SortImports(file_contents=test_input, remove_imports=['lib2', 'lib6']).output
+ assert test_output == ("import lib1\n"
+ "import lib5")