├── .gitignore ├── .travis.yml ├── README.rst ├── imps.png ├── imps ├── .cache │ └── v │ │ └── cache │ │ └── lastfailed ├── __init__.py ├── core.py ├── rebuilders.py ├── shell.py ├── stdlib.py ├── strings.py └── tests │ ├── __init__.py │ ├── test_core.py │ ├── test_crypto.py │ ├── test_google.py │ ├── test_rebuilders.py │ ├── test_shell.py │ ├── test_smarkets.py │ ├── test_stdlib.py │ └── test_strings.py ├── license ├── requirements.txt ├── run ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .py-test/ 4 | .cache/ 5 | .idea/ 6 | UNKNOWN.egg-info/ 7 | build/ 8 | dist/ 9 | imps.egg-info/ 10 | .tox/ 11 | dump/ 12 | .vscode/ 13 | .pytest_cache/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | install: pip install tox-travis 8 | env: 9 | - REQUIREMENTS=tests 10 | - REQUIREMENTS=flake8 11 | script: 12 | - tox -e $REQUIREMENTS 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: https://raw.github.com/bootandy/imps/master/imps.png 3 | :alt: isort 4 | 5 | ####### 6 | 7 | .. image:: https://travis-ci.org/bootandy/imps.png?branch=master 8 | :target: https://travis-ci.org/bootandy/imps 9 | :alt: Build Status 10 | 11 | Python 2.7, 3.5, 3.6 12 | 13 | To Install: 14 | =========== 15 | pip install imps 16 | 17 | or 18 | 19 | python setup.py install 20 | 21 | 22 | Why? 23 | ==== 24 | 25 | It sorts your imports and is designed to work with this 26 | `flake8-import-order plugin `_ 27 | It differs from `Isort `_ as it is more opinionated and 28 | does not require config as it works out what to do by reading your setup.cfg 29 | 30 | 31 | Usage: 32 | ====== 33 | imps 34 | 35 | imps - s style 36 | 37 | where style is smarkets / crypto / google 38 | 39 | 40 | To Run Tests: 41 | ============= 42 | pytest 43 | 44 | Note if you run tests in Pycharm: Specify test as type: py.test 45 | 46 | Is it ready: 47 | ============ 48 | Mostly, 49 | 50 | To Run Tox locally: 51 | =================== 52 | Tests: 53 | tox -e tests 54 | Flake8: 55 | tox -e flake8 56 | -------------------------------------------------------------------------------- /imps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootandy/imps/21c84b788d44a296d8db4f655bedcef3fad12c36/imps.png -------------------------------------------------------------------------------- /imps/.cache/v/cache/lastfailed: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /imps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootandy/imps/21c84b788d44a296d8db4f655bedcef3fad12c36/imps/__init__.py -------------------------------------------------------------------------------- /imps/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import re 4 | 5 | from imps.rebuilders import does_line_have_hash_noqa, Rebuilder, sortable_key 6 | from imps.strings import get_doc_string 7 | 8 | 9 | IMPORT_LINE = r'^import\s.*' 10 | FROM_IMPORT_LINE = r'^from\s.*import\s.*' 11 | FROM_IMPORT_LINE_WITH_PARAN = r'^from\s.*import\s.*\(' 12 | 13 | 14 | # We do sorting here early for a single line with multiple imports. 15 | def split_imports(s): 16 | from_part, import_list = re.split(r'\s*import\s+', s, 1) 17 | 18 | if '#' in import_list: 19 | search_to = import_list.find('#') 20 | else: 21 | search_to = len(import_list) 22 | 23 | ending = import_list[search_to:] 24 | if ending: 25 | ending = " " + ending 26 | if from_part: 27 | from_part += " " 28 | imps = import_list[0:search_to].split(',') 29 | 30 | imps = sorted(set([i.strip() for i in imps if i.strip()]), key=sortable_key) 31 | return from_part + "import " + ', '.join(imps) + ending 32 | 33 | 34 | def is_line_an_import(l): 35 | return re.match(FROM_IMPORT_LINE, l) or re.match(IMPORT_LINE, l) 36 | 37 | 38 | def _is_there_no_close_paran(l): 39 | return ')' not in l or ('#' in l and l.find('#') < l.find(')')) 40 | 41 | 42 | class Sorter(): 43 | def __init__(self, type='s', max_line_length=80, local_imports=None, indent=" "): 44 | self.reader = ReadInput(indent) 45 | self.local_imports = local_imports or [] 46 | self.builder_object = Rebuilder(type, int(max_line_length), indent) 47 | 48 | def sort(self, lines): 49 | self.reader.clean() 50 | self.reader.process_and_split(lines) 51 | return self.builder_object.rebuild(self.local_imports, *self.reader.get_imports_as_dicts()) 52 | 53 | 54 | class ReadInput(): 55 | def __init__(self, indent): 56 | self.indent = indent 57 | 58 | def clean(self): 59 | self.lines_before_any_imports = None 60 | self.lines_before_import = [] 61 | self.pre_import = {} 62 | self.pre_from_import = {} 63 | 64 | def _store_line(self, target_map, line): 65 | # Special case keep the first comments at the very top of the file (eg utf encodings) 66 | if self.lines_before_any_imports is None: 67 | self.lines_before_any_imports = self.lines_before_import 68 | self.lines_before_import = [] 69 | target_map[line] = self.lines_before_import 70 | self.lines_before_import = [] 71 | 72 | def _process_line(self, line): 73 | if does_line_have_hash_noqa(line): 74 | self.lines_before_import.append(line) 75 | elif re.match(IMPORT_LINE, line): 76 | self._store_line(self.pre_import, split_imports(line)) 77 | elif re.match(FROM_IMPORT_LINE_WITH_PARAN, line): 78 | self._process_from_paren_block(line) 79 | elif re.match(FROM_IMPORT_LINE, line): 80 | self._store_line(self.pre_from_import, split_imports(line)) 81 | else: 82 | self.lines_before_import.append(line) 83 | 84 | def _process_from_paren_block(self, line): 85 | """ 86 | If there are no comments we squash a from X import (Y,Z) into -> from X import Y,Z 87 | by removing the parenthesis 88 | 89 | However if there are comments we must split them into comments for the import on the line below 90 | the comment and comments on the same line as the import. 91 | 92 | Imports are then sorted inside this method to preserve the position of the comments. 93 | """ 94 | 95 | if '# noqa' in line: 96 | self._store_line(self.pre_from_import, split_imports(line)) 97 | return 98 | 99 | if '#' not in line: 100 | line = line.replace('(', '').replace(')', '') 101 | self._store_line(self.pre_from_import, split_imports(line)) 102 | return 103 | 104 | base = line[0:line.find('(') + 1] 105 | 106 | line = line[line.find('(') + 1:line.rfind(')')] 107 | pre_comments = [] 108 | pre_imp_comment = {} 109 | same_line_comment = {} 110 | old_import = None 111 | while line: 112 | is_newline = line.find('\n') < line.find('#') 113 | 114 | line = line.lstrip() 115 | # If the next part of l is NOT a comment. 116 | if line.find('#') != 0: 117 | 118 | # l[0:end_marker] is the name of the next import 119 | end_marker = line.find(',') 120 | if end_marker == -1: 121 | end_marker = line.find('#') 122 | if end_marker == -1: 123 | end_marker = line.find('\n') 124 | 125 | old_import = line[0:end_marker] 126 | if not old_import: 127 | break 128 | same_line_comment[old_import] = '' 129 | pre_imp_comment[old_import] = pre_comments 130 | pre_comments = [] 131 | line = line[end_marker + 1:] 132 | else: 133 | comment = line[line.find('#'):line.find('\n')] 134 | 135 | # If the comment is on a newline mark it as a 'pre-import-comment' to go on the line 136 | # above. (or if old_import is None which means this is the first line). 137 | if is_newline or not old_import: 138 | pre_comments.append(comment) 139 | else: 140 | same_line_comment[old_import] = comment 141 | if old_import not in pre_imp_comment: 142 | pre_imp_comment[old_import] = [] 143 | line = line[line.find('\n'):] 144 | 145 | for i in sorted(same_line_comment.keys(), key=lambda s: s.lower()): 146 | if pre_imp_comment.get(i): 147 | for c in pre_imp_comment[i]: 148 | base += '\n' + self.indent + c 149 | base += '\n' + self.indent + i + ',' 150 | if same_line_comment[i]: 151 | base += " " + same_line_comment[i] 152 | 153 | # include the last pre import comments - they were at the end 154 | for c in pre_comments: 155 | base += '\n' + self.indent + c 156 | 157 | base += '\n)' 158 | self.pre_from_import[base] = self.lines_before_import 159 | self.lines_before_import = [] 160 | 161 | def process_and_split(self, text): 162 | lines = text.split('\n') 163 | i = -1 164 | while i < len(lines) - 1: 165 | i += 1 166 | data = lines[i] 167 | 168 | if is_line_an_import(lines[i]) and '\\' in lines[i] and lines[i].strip()[-1] == '\\': 169 | while '\\' in lines[i] and lines[i].strip()[-1] == '\\' and i < len(lines) - 1: 170 | data = data.strip()[0:-1] + lines[i+1] 171 | i += 1 172 | 173 | if re.match(FROM_IMPORT_LINE_WITH_PARAN, data): 174 | while _is_there_no_close_paran(lines[i]) and i < len(lines) - 1: 175 | i += 1 176 | data += '\n' + lines[i] 177 | 178 | # If a doc_strings was opened but not closed on this line: 179 | doc_string_points = get_doc_string(data) 180 | if len(doc_string_points) % 2 == 1: 181 | giant_comment = doc_string_points[-1][1] 182 | 183 | while i < len(lines) - 1: 184 | i += 1 185 | data += '\n' + lines[i] 186 | comment_point = lines[i].find(giant_comment) 187 | if comment_point != -1: 188 | after_comment = lines[i][comment_point + 3:] 189 | doc_string_points = get_doc_string(after_comment) 190 | 191 | if len(doc_string_points) % 2 == 0: 192 | break 193 | self._process_line(data) 194 | 195 | def get_imports_as_dicts(self): 196 | if self.lines_before_any_imports is None: 197 | self.lines_before_any_imports = [] 198 | return self.pre_import, self.pre_from_import, self.lines_before_any_imports, self.lines_before_import 199 | -------------------------------------------------------------------------------- /imps/rebuilders.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import re 4 | from collections import OrderedDict 5 | 6 | from imps.stdlib import FUTURE, get_paths, LOCAL, RELATIVE, STDLIB, THIRDPARTY 7 | 8 | 9 | NOQA = r'.*\s*\#\sNOQA' # wont work if NOQA is inside a triple string. 10 | PYLINT_IGNORE = r'.*\s*\#\s*pylint:\s*disable\=.*$' # wont work if pylint: disable is inside a triple string. 11 | 12 | FROM_IMPORT_LINE = r'^from\s.*import\s.*' 13 | FROM_IMPORT_PARAN_LINE = r'^from\s.*import\s\(.*' 14 | 15 | 16 | def sortable_key(s): 17 | s = s.strip() 18 | broken_s = s.split(' ') 19 | results = [] 20 | for bs in broken_s: 21 | new_s = '' 22 | for c in bs: 23 | if c.islower(): 24 | new_s += '1' 25 | else: 26 | new_s += '0' 27 | results.append(bs.lower() + new_s) 28 | return ' '.join(results) 29 | 30 | 31 | def does_line_have_hash_noqa(line): 32 | return re.match(NOQA, line, re.IGNORECASE) 33 | 34 | 35 | def does_line_end_in_pylint_ignore(line): 36 | if re.match(PYLINT_IGNORE, line, re.IGNORECASE): 37 | _, post = re.split(r'#\spylint', line, re.IGNORECASE) 38 | if 'F0401' in post or 'E0611' in post: 39 | return True 40 | return False 41 | 42 | 43 | def _classify_imports(imports, local_imports): 44 | result = OrderedDict() 45 | result[FUTURE] = [] 46 | result[STDLIB] = [] 47 | result[THIRDPARTY] = [] 48 | result[LOCAL] = [] 49 | result[RELATIVE] = [] 50 | 51 | for i in imports: 52 | result[get_paths(i, local_imports)].append(i) 53 | 54 | return result 55 | 56 | 57 | def _get_core_import(imp): 58 | imp = re.sub(r'^from\s+', '', imp) 59 | imp = re.sub(r'^import\s+', '', imp) 60 | return re.sub(r'\s+.*', '', imp) 61 | 62 | 63 | def _sorter_relative_imports(s): 64 | s = s.replace('.', chr(ord('z') + 1)) 65 | s = s.replace('_', chr(ord('A') - 1)) 66 | return s.lower() 67 | 68 | 69 | def _sorter(s): 70 | s = s.replace('.', chr(ord('A') - 2)) 71 | s = s.replace('_', chr(ord('A') - 1)) 72 | # We only alphabetically sort the from part of the imports in style: from X import Y 73 | if re.match(FROM_IMPORT_PARAN_LINE, s): 74 | s = re.sub(r'\#.*\n', '', s) 75 | s = re.sub(r'\s+', ' ', s) 76 | s = sortable_key(s[4:s.find(' import ')]) + ' import' + s[s.find('(') + 1:s.find(')')] 77 | if re.match(FROM_IMPORT_LINE, s): 78 | s = sortable_key(s[4:s.find(' import ')]) + s[s.find(' import '):] 79 | return sortable_key(s) 80 | 81 | 82 | def _sorter_unify_import_and_from(s): 83 | s = re.sub(r'^from\s+', '', s) 84 | s = re.sub(r'^import\s+', '', s) 85 | return _sorter(s) 86 | 87 | 88 | def _remove_double_newlines(lines): 89 | i = 0 90 | while i < len(lines) - 1: 91 | if lines[i+1] == lines[i] == '': 92 | lines[i:i+1] = [] 93 | else: 94 | i += 1 95 | return lines 96 | 97 | 98 | def _get_builder_func(s, max_line_length, indent): 99 | if s in ('s', 'smarkets'): 100 | return SmarketsBuilder(max_line_length, indent) 101 | elif s in ('g', 'google'): 102 | return GoogleBuilder(max_line_length, indent) 103 | elif s in ('c', 'crypto', 'cryptography'): 104 | return CryptoBuilder(max_line_length, indent) 105 | else: 106 | raise Exception('Unknown style type %s', s) 107 | 108 | 109 | class GenericBuilder(object): 110 | def __init__(self, max_line_length, indent): 111 | self.max_line_length = max_line_length 112 | self.indent = indent 113 | 114 | def do_all( 115 | self, imports_by_type, from_imports_by_type, lines_before_any_imports, pre_import, 116 | pre_from_import, after_imports 117 | ): 118 | output = '\n'.join(lines_before_any_imports) 119 | self.new_import_group = False 120 | 121 | for typ in imports_by_type.keys(): 122 | if typ == RELATIVE: 123 | continue 124 | new_import_group = self.special_sort( 125 | imports_by_type, from_imports_by_type, typ, pre_import, pre_from_import 126 | ) 127 | if new_import_group: 128 | self.new_import_group = True 129 | output += new_import_group + '\n' 130 | 131 | output += self._relative_builder_func(from_imports_by_type, pre_from_import) 132 | output = output.strip() 133 | after_imports_str = '\n'.join(after_imports).strip() 134 | 135 | result = (output + '\n\n\n' + after_imports_str).strip() 136 | 137 | if result: 138 | return result + '\n' 139 | return '' 140 | 141 | def _relative_builder_func(self, from_imports, pre_from_import): 142 | output = "" 143 | for imp in sorted(from_imports[RELATIVE], key=_sorter_relative_imports): 144 | output += self._build(imp, pre_from_import[imp]) 145 | return output 146 | 147 | def _build(self, core_import, pre_imp): 148 | pre_imp = [a for a in pre_imp if a] 149 | output = '\n'.join([''] + pre_imp + ['']) 150 | output += self._split_core_import(core_import) 151 | return output 152 | 153 | def _split_core_import(self, core_import): 154 | if len(core_import) <= self.max_line_length or does_line_have_hash_noqa(core_import) or ( 155 | '(' in core_import and ')' in core_import) or does_line_end_in_pylint_ignore(core_import): 156 | return core_import 157 | 158 | # To turn a long line of imports into a multiline import using parenthesis 159 | result = (',\n' + self.indent).join([s.strip() for s in core_import.split(',')]) 160 | result = re.sub(r'import\s+', 'import (\n' + self.indent, result) 161 | result += ",\n)" 162 | return result 163 | 164 | def special_sort(self, *args): 165 | raise NotImplementedError() 166 | 167 | 168 | class SmarketsBuilder(GenericBuilder): 169 | def special_sort(self, imports, from_imports, typ, pre_import, pre_from_import): 170 | output = "" 171 | for imp in sorted(imports[typ], key=_sorter): 172 | output += self._build(imp, pre_import[imp]) 173 | 174 | for imp in sorted(from_imports[typ], key=_sorter): 175 | output += self._build(imp, pre_from_import[imp]) 176 | 177 | return output 178 | 179 | 180 | class GoogleBuilder(GenericBuilder): 181 | def special_sort(self, imports, from_imports, typ, pre_import, pre_from_import): 182 | output = "" 183 | for imp in sorted(imports[typ] + from_imports[typ], key=_sorter_unify_import_and_from): 184 | output += self._build(imp, pre_import.get(imp, pre_from_import.get(imp))) 185 | return output 186 | 187 | 188 | class CryptoBuilder(GenericBuilder): 189 | def special_sort(self, imports, from_imports, typ, pre_import, pre_from_import): 190 | output = "" 191 | if typ in (STDLIB, FUTURE, RELATIVE): 192 | for imp in sorted(imports[typ], key=_sorter): 193 | output += self._build(imp, pre_import[imp]) 194 | 195 | for imp in sorted(from_imports[typ], key=_sorter): 196 | output += self._build(imp, pre_from_import[imp]) 197 | else: 198 | last_imp = '' 199 | for imp in sorted(imports[typ] + from_imports[typ], key=_sorter_unify_import_and_from): 200 | if not last_imp or not _get_core_import(imp).startswith(last_imp): 201 | if last_imp: 202 | if imp in pre_import: 203 | pre_import.get(imp).append('') 204 | if imp in pre_from_import: 205 | pre_from_import.get(imp).append('') 206 | last_imp = _get_core_import(imp) 207 | 208 | output += self._build(imp, pre_import.get(imp, pre_from_import.get(imp))) 209 | return output 210 | 211 | 212 | class Rebuilder(): 213 | def __init__(self, type='s', max_line_length=80, indent=" "): 214 | self.builder_object = _get_builder_func(type, int(max_line_length), indent) 215 | 216 | def rebuild( 217 | self, local_imports, pre_import, pre_from_import, lines_before_any_imports, 218 | after_imports 219 | ): 220 | imports_by_type = _classify_imports(pre_import.keys(), local_imports) 221 | from_imports_by_type = _classify_imports(pre_from_import.keys(), local_imports) 222 | return self.builder_object.do_all( 223 | imports_by_type, from_imports_by_type, lines_before_any_imports, pre_import, 224 | pre_from_import, after_imports 225 | ) 226 | -------------------------------------------------------------------------------- /imps/shell.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import argparse 4 | import difflib 5 | import os 6 | import sys 7 | 8 | try: 9 | from backports import configparser 10 | except Exception: 11 | import configparser 12 | from imps.core import Sorter 13 | 14 | 15 | def run(sorter, file_name, is_dry_run): 16 | # Why not send array of lines in if that's what we use? 17 | with open(file_name, 'r') as f: 18 | data = f.read() 19 | 20 | output = sorter.sort(data) 21 | if output: 22 | if is_dry_run: 23 | result = difflib.unified_diff(output.splitlines(), data.splitlines()) 24 | print('\n'.join(result)) 25 | else: 26 | with open(file_name, 'w') as f: 27 | f.write(output) 28 | 29 | 30 | def recurse_down_tree(args, path, sorter=None): 31 | is_dry_run = args.dry_run 32 | if os.path.isfile(path): 33 | run(get_sorter(args, path), path, is_dry_run) 34 | else: 35 | files = os.listdir(path) 36 | if 'setup.cfg' in files or sorter is None: 37 | sorter = get_sorter(args, path) 38 | for f in files: 39 | if os.path.isfile(os.path.join(path, f)) and f[-3:] == '.py': 40 | run(sorter, os.path.join(path, f), is_dry_run) 41 | elif not os.path.isfile(f) and '.' not in f: 42 | recurse_down_tree(args, os.path.join(path, f), sorter) 43 | 44 | 45 | def get_sorter(args, path): 46 | if os.path.isfile(path): 47 | path, _ = os.path.split(path) 48 | conf = read_config(os.path.abspath(path)) 49 | style, max_line_length, local_imports = setup_vars(conf, args) 50 | return Sorter(style, max_line_length, local_imports) 51 | 52 | 53 | def read_config(path): 54 | config = configparser.ConfigParser() 55 | while not config.sections() and path != '/': 56 | config.read(os.path.join(path, 'setup.cfg')) 57 | path, _ = os.path.split(path) 58 | return config 59 | 60 | 61 | def normalize_file_name(file_name): 62 | orig_file_name = file_name 63 | 64 | while not os.path.exists(file_name) and ':' in file_name: 65 | file_name = file_name[0:file_name.rfind(':')] 66 | 67 | if os.path.exists(file_name): 68 | return file_name 69 | 70 | print('File %s does not exist' % orig_file_name, file=sys.stderr) 71 | return None 72 | 73 | 74 | def normalize_file_names(file_list): 75 | return [normalize_file_name(f) for f in file_list if normalize_file_name(f)] 76 | 77 | 78 | def setup_vars(config, args): 79 | # Read from command line first. Else setup.cfg 'imps' else 'flake8'. Else assume 's' 80 | style = args.style 81 | if not style: 82 | style = config.get('imps', 'style', fallback=config.get('flake8', 'import-order-style', fallback='s')) 83 | 84 | max_line_length = args.max_line_length 85 | if not max_line_length: 86 | max_line_length = config.get('imps', 'max-line-length', fallback=config.get( 87 | 'flake8', 'max-line-length', fallback=80 88 | )) 89 | 90 | application_import_names = args.application_import_names 91 | if not application_import_names: 92 | application_import_names = config.get( 93 | 'imps', 'application-import-names', fallback=config.get( 94 | 'flake8', 'application-import-names', fallback='' 95 | ) 96 | ) 97 | 98 | return style, max_line_length, application_import_names.split(',') 99 | 100 | 101 | def main(): 102 | parser = argparse.ArgumentParser(description='Sort your python') 103 | 104 | parser.add_argument('file', nargs='*', default=[os.getcwd()]) 105 | 106 | parser.add_argument('-s', '--style', type=str, help='Import style', default='') 107 | parser.add_argument('-l', '--max-line-length', type=int, help='Line length') 108 | parser.add_argument('-n', '--application-import-names', type=str, help='Local Imports') 109 | 110 | parser.add_argument('-d', '--dry-run', dest='dry_run', action='store_true') 111 | parser.set_defaults(dry_run=False) 112 | 113 | args = parser.parse_args() 114 | file_names = normalize_file_names(args.file) 115 | for file_name in file_names: 116 | recurse_down_tree(args, file_name) 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /imps/stdlib.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import re 4 | 5 | from flake8_import_order import STDLIB_NAMES 6 | 7 | 8 | FUTURE = 0 9 | STDLIB = 1 10 | THIRDPARTY = 2 11 | LOCAL = 3 12 | RELATIVE = -1 13 | 14 | 15 | def strip_to_first_module(imp): 16 | imp = re.sub(r'^from\s+', '', imp) 17 | imp = re.sub(r'^import\s+', '', imp) 18 | if imp.find('.') == 0: 19 | return re.match(r'\.+\w*', imp).group() 20 | return re.match(r'\w+', imp).group() 21 | 22 | 23 | def get_paths(module, local_list): 24 | module = strip_to_first_module(module) 25 | if not module: 26 | raise Exception('No module') 27 | 28 | if module[0] == '.': 29 | return RELATIVE 30 | 31 | if module == '__future__': 32 | return FUTURE 33 | 34 | if module in local_list: 35 | return LOCAL 36 | 37 | if module in STDLIB_NAMES: 38 | return STDLIB 39 | 40 | return THIRDPARTY 41 | -------------------------------------------------------------------------------- /imps/strings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | 4 | IMPORT_LIB = r'^import\s+([\w\.]+)' 5 | FROM_IMPORT_LIB = r'^from\s+([\w\.]+)\s+import' 6 | 7 | TRIPLE_DOUBLE = '"""' 8 | TRIPLE_SINGLE = "'''" 9 | 10 | 11 | def _is_hash_a_comment(s): 12 | return ("'" not in s or s.index('#') < s.index("'")) and ('"' not in s or s.index('#') < s.index('"')) 13 | 14 | 15 | def _get_doc_string_by_type(s, quote_type): 16 | opposite_quote = {TRIPLE_DOUBLE: "'", TRIPLE_SINGLE: '"'}[quote_type] 17 | 18 | if '#' in s and s.index('#') < s.index(quote_type) and _is_hash_a_comment(s): 19 | return len(s), False 20 | 21 | if opposite_quote in s and s.index(opposite_quote) < s.index(quote_type): 22 | return s.index(opposite_quote, s.index(opposite_quote) + 1) + 1, False # fails on backslash '\'' 23 | 24 | return s.index(quote_type), True 25 | 26 | 27 | def _get_part(s, base_index, quote): 28 | points = [] 29 | index, in_quote = _get_doc_string_by_type(s, quote_type=quote) 30 | 31 | if in_quote: 32 | points.append((index + base_index, quote)) 33 | s = s[index + 3:] 34 | base_index += index + 3 35 | try: 36 | points.append((s.index(quote) + 3 + base_index, quote)) 37 | base_index += s.index(quote) + 3 38 | s = s[s.index(quote) + 3:] 39 | except ValueError: 40 | return "", base_index, points 41 | else: 42 | base_index += index 43 | s = s[index:] 44 | 45 | return s, base_index, points 46 | 47 | 48 | def get_doc_string(s): 49 | points = [] 50 | base_index = 0 51 | 52 | while s: 53 | double = s.find(TRIPLE_DOUBLE) 54 | single = s.find(TRIPLE_SINGLE) 55 | 56 | if double == single == -1: 57 | break 58 | elif (double < single or single == -1) and double != -1: 59 | s, base_index, p2 = _get_part(s, base_index, TRIPLE_DOUBLE) 60 | points += p2 61 | 62 | elif double > single or double == -1: 63 | s, base_index, p2 = _get_part(s, base_index, TRIPLE_SINGLE) 64 | points += p2 65 | else: 66 | raise Exception('impossible') 67 | 68 | return points 69 | -------------------------------------------------------------------------------- /imps/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | -------------------------------------------------------------------------------- /imps/tests/test_core.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.core import Sorter, split_imports 4 | 5 | 6 | def test_base_bad_order(): 7 | input = """import Z 8 | import X 9 | import Y 10 | """ 11 | output = """import X 12 | import Y 13 | import Z 14 | """ 15 | assert Sorter().sort(input) == output 16 | 17 | 18 | def test_base_more_spaces(): 19 | input = """# -*- coding: utf-8 -*- 20 | import B 21 | #A 22 | #B 23 | #C 24 | import C 25 | import A 26 | """ 27 | output = """# -*- coding: utf-8 -*- 28 | import A 29 | import B 30 | #A 31 | #B 32 | #C 33 | import C 34 | """ 35 | assert Sorter().sort(input) == output 36 | 37 | 38 | def test_base_with_func_in(): 39 | """ 40 | imports 'own' the space above them so when they are sorted the 'above space' moves with them. 41 | Therefore when import A is moved up so does the function. 42 | """ 43 | input = """import B 44 | 45 | def my_func(): 46 | return False 47 | 48 | import A 49 | """ 50 | output = """def my_func(): 51 | return False 52 | import A 53 | import B 54 | """ 55 | assert Sorter().sort(input) == output 56 | 57 | 58 | def test_stdlib_and_local(): 59 | input = """import io 60 | 61 | import a_mylib 62 | """ 63 | assert Sorter().sort(input) == input 64 | 65 | 66 | def test_stdlib_and_local_bad_order(): 67 | input = """import a_mylib 68 | import io 69 | """ 70 | output = """import io 71 | 72 | import a_mylib 73 | """ 74 | assert Sorter().sort(input) == output 75 | 76 | 77 | def test_comments_between_import_types(): 78 | input = """ 79 | import io 80 | # A comment 81 | import a_mylib 82 | """ 83 | output = """import io 84 | 85 | # A comment 86 | import a_mylib 87 | """ 88 | assert Sorter().sort(input) == output 89 | 90 | 91 | def test_comments_between_import_types3(): 92 | input = """import io 93 | 94 | # A comment 95 | import sys 96 | """ 97 | correct = """import io 98 | # A comment 99 | import sys 100 | """ 101 | assert Sorter().sort(input) == correct 102 | 103 | 104 | def test_comments_between_import_types4(): 105 | input = """import io 106 | # A comment 107 | 108 | import sys 109 | """ 110 | correct = """import io 111 | # A comment 112 | import sys 113 | """ 114 | assert Sorter().sort(input) == correct 115 | 116 | 117 | def test_triple_quote_comments(): 118 | input = """import A 119 | \"\"\"Don't break my docstring \"\"\" 120 | import B 121 | """ 122 | assert Sorter().sort(input) == input 123 | 124 | 125 | def test_triple_quote_in_a_string(): 126 | input = """import A 127 | s = '\"\"\"import C\"\"\"' 128 | import B 129 | """ 130 | assert Sorter().sort(input) == input 131 | 132 | 133 | def test_triple_quote_with_newlines_and_imports_in_it(): 134 | input = """import A 135 | \"\"\" 136 | 137 | 138 | ignore newlines and imports inside a giant comment 139 | 140 | 141 | import C 142 | \"\"\" 143 | import B 144 | """ 145 | assert Sorter().sort(input) == input 146 | 147 | 148 | def test_triple_quotes_in_comments(): 149 | input = """import A # I can put these here behind a comment \"\"\" 150 | import B 151 | """ 152 | assert Sorter().sort(input) == input 153 | 154 | 155 | def test_triple_quotes_in_string(): 156 | input = """import A 157 | str = '\"\"\"' 158 | import B 159 | """ 160 | assert Sorter().sort(input) == input 161 | 162 | 163 | def test_triple_quotes_nasty(): 164 | input = """import A 165 | str = '\"\"\" & # are used to comment' # \"\"\" and # are used for comments 166 | import B 167 | """ 168 | assert Sorter().sort(input) == input 169 | 170 | 171 | def test_multiple_triple_quotes(): 172 | input = """import A 173 | \"\"\" ''' \"\"\" ''' \\# ''' \"\"\" 174 | new line \"\"\" 175 | import B 176 | """ 177 | assert Sorter().sort(input) == input 178 | 179 | 180 | def test_from_and_regular(): 181 | input = """from __future__ import pprint 182 | 183 | import A 184 | from B import C 185 | """ 186 | assert Sorter().sort(input) == input 187 | 188 | 189 | def test_relative_imports(): 190 | input = """from imps.imps import * 191 | 192 | from . import A 193 | from . import B 194 | from .A import A 195 | """ 196 | assert Sorter().sort(input) == input 197 | 198 | 199 | def always_a_double_new_line_between_imports_and_code(): 200 | input_triple_new_line = """from imps.imps import * 201 | 202 | 203 | 204 | class Style(Enum): 205 | SMARKETS = 1 206 | GOOGLE = 2 207 | CRYPTOGRAPHY = 3 208 | """ 209 | input_double_new_line = """from imps.imps import * 210 | 211 | 212 | class Style(Enum): 213 | SMARKETS = 1 214 | GOOGLE = 2 215 | CRYPTOGRAPHY = 3 216 | """ 217 | input_single_new_line = """from imps.imps import * 218 | 219 | class Style(Enum): 220 | SMARKETS = 1 221 | GOOGLE = 2 222 | CRYPTOGRAPHY = 3 223 | """ 224 | 225 | assert Sorter().sort(input_single_new_line) == input_double_new_line 226 | assert Sorter().sort(input_double_new_line) == input_double_new_line 227 | assert Sorter().sort(input_triple_new_line) == input_double_new_line 228 | 229 | 230 | def test_reorder_from_imports(): 231 | input = """from strings import strip_to_module_name 232 | 233 | from enum import Enum 234 | """ 235 | output = """from enum import Enum 236 | 237 | from strings import strip_to_module_name 238 | """ 239 | assert Sorter().sort(input) == output 240 | 241 | 242 | def test_import_as(): 243 | input = """import enum as zenum 244 | 245 | from strings import strip_to_module_name as stripper 246 | """ 247 | assert Sorter().sort(input) == input 248 | 249 | 250 | def test_import_using_parenthesis(): 251 | input = """from string import ( 252 | upper as a_up, 253 | strip, 254 | find, 255 | ) 256 | """ 257 | output = """from string import find, strip, upper as a_up 258 | """ 259 | assert Sorter().sort(input) == output 260 | 261 | 262 | def test_noqa_import(): 263 | input = """import X 264 | import Z # noQA 265 | import Y 266 | """ 267 | assert Sorter().sort(input) == input 268 | 269 | 270 | def test_noqa2(): 271 | inp = """from brokers.smarkets.streaming_api.client import StreamingAPIClient # noqa 272 | from brokers.smarkets.streaming_api.exceptions import ( # noqa 273 | ConnectionError, DecodeError, ParseError, SocketDisconnected, InvalidCallbackError 274 | ) 275 | """ 276 | assert Sorter().sort(inp) == inp 277 | 278 | 279 | def test_pylint_disable(): 280 | inp = """import six.moves.cPickle as pickle # pylint: disable=E0611,F0401 281 | """ 282 | 283 | assert Sorter().sort(inp) == inp 284 | 285 | 286 | def test_multiline_parentheses(): 287 | input = """from imps.strings import ( 288 | get_doc_string, # We can do same line comments 289 | strip_to_module_name, # We In several # places 290 | # We can now do newline comments too 291 | # on several lines 292 | strip_to_module_name_from_import, # Yes # We # Can 293 | # Comments can be the 294 | # last line too 295 | ) 296 | """ 297 | assert Sorter(max_line_length=110).sort(input) == input 298 | 299 | 300 | def test_multiline_parentheses_with_comment_on_line_one(): 301 | input = """from imps.strings import ( # A comment 302 | get_doc_string, 303 | strip_to_module_name, 304 | ) 305 | """ 306 | output = """from imps.strings import ( 307 | # A comment 308 | get_doc_string, 309 | strip_to_module_name, 310 | ) 311 | """ 312 | assert Sorter(max_line_length=40).sort(input) == output 313 | 314 | 315 | def test_multiline_parentheses_will_sort(): 316 | input = """from imps.strings import ( 317 | get_doc_string, 318 | strip_to_module_name, # A comment 319 | ) 320 | from imps.alpha import stuff 321 | """ 322 | output = """from imps.alpha import stuff 323 | from imps.strings import ( 324 | get_doc_string, 325 | strip_to_module_name, # A comment 326 | ) 327 | """ 328 | assert Sorter(max_line_length=40).sort(input) == output 329 | 330 | 331 | def test_multiline_slash_continue_import(): 332 | input = """import Z, Y, \\ 333 | X, A, \\ 334 | B 335 | 336 | def some_func(param_a, \\ 337 | param_b): 338 | pass 339 | ''' 340 | def some_func(param_a, \\ 341 | param_b): 342 | pass 343 | ''' 344 | """ 345 | output = """import A, B, X, Y, Z 346 | 347 | 348 | def some_func(param_a, \\ 349 | param_b): 350 | pass 351 | ''' 352 | def some_func(param_a, \\ 353 | param_b): 354 | pass 355 | ''' 356 | """ 357 | assert Sorter().sort(input) == output 358 | 359 | 360 | def test_triple_quotes(): 361 | inp = '''user_tracker._request_get = lambda url, verify: Mock(text="""20793353750002077:5730728,5730727 362 | -21947406894019109:5730726,5730725""") 363 | ''' 364 | output = '''user_tracker._request_get = lambda url, verify: Mock(text="""20793353750002077:5730728,5730727 365 | -21947406894019109:5730726,5730725""") 366 | ''' 367 | assert Sorter().sort(inp) == output 368 | 369 | 370 | def test_no_state_stays_in_sorting_object(): 371 | """ 372 | Sorter kept previous state at one stage causing it to always append new 373 | files instead of creating a new file. Dont want that happening again. 374 | """ 375 | input = '''from A import B 376 | ''' 377 | s = Sorter() 378 | assert s.sort(input) == input 379 | 380 | input2 = '''from B import A 381 | ''' 382 | assert s.sort(input2) == input2 383 | 384 | 385 | def test_underscores_in_module_names(): 386 | input = '''from gateways.betting.gateway import * 387 | from gateways.betting_ng.server_module import * 388 | from gateways.bettingvery.server_module import * 389 | ''' 390 | assert Sorter().sort(input) == input 391 | 392 | 393 | def test_file_begins_with_docstring_is_ok(): 394 | input = '''"""Please don't destroy my docstring""" 395 | from __future__ import absolute_import, division 396 | ''' 397 | assert Sorter().sort(input) == input 398 | 399 | 400 | def test_split_from_import(): 401 | assert split_imports('from A import B') == 'from A import B' 402 | 403 | 404 | def test_split_from_import_complex(): 405 | assert split_imports('from A.B import Z, F, W') == 'from A.B import F, W, Z' 406 | 407 | 408 | def test_split_from_import_with_as(): 409 | assert split_imports('from A import this as that, A,Z') == 'from A import A, this as that, Z' 410 | 411 | 412 | def test_split_from_import_with_import_in_comment(): 413 | inp = """from __future__ import absolute_import 414 | 415 | import common.auto_patch # noqa # import order 416 | import argparse 417 | """ 418 | assert Sorter().sort(inp) == inp 419 | 420 | 421 | def test_order_with_capitals(): 422 | """ flake8_import_order v 0.11 behaviour changed slightly to handle capital letters more strictly""" 423 | input = '''import b 424 | import B 425 | 426 | from pytest import a, A 427 | ''' 428 | correct = '''import B 429 | import b 430 | 431 | from pytest import A, a 432 | ''' 433 | assert Sorter(local_imports=['pytest']).sort(input) == correct 434 | 435 | 436 | def test_complex_multi_quote_strings(): 437 | input = '''import a 438 | """ fasdfasdf """ """ 439 | sdf""" """dsaf s""" """dasf 440 | """ 441 | import b 442 | ''' 443 | assert Sorter().sort(input) == input 444 | 445 | 446 | def test_add_double_newline_before_func_def(): 447 | inp = '''import a 448 | def func(): 449 | pass 450 | ''' 451 | out = '''import a 452 | 453 | 454 | def func(): 455 | pass 456 | ''' 457 | assert Sorter().sort(inp) == out 458 | 459 | 460 | def test_pyximport(): 461 | inp = """from brokers.smarkets import odds as smk_odds 462 | 463 | 464 | pyximport.install(setup_args={'include_dirs': [np.get_include()]}) 465 | 466 | import cypoisson # noqa: I100 # pylint: disable=E0401 467 | """ 468 | assert Sorter().sort(inp) == inp 469 | -------------------------------------------------------------------------------- /imps/tests/test_crypto.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.core import Sorter 4 | 5 | 6 | def test_crypto_style(): 7 | input = """from __future__ import absolute_import 8 | 9 | import ast 10 | import os 11 | import sys 12 | from functools import * 13 | from os import path 14 | 15 | import flake8 16 | from flake8.defaults import * 17 | import pytest 18 | from pytest import * 19 | from pytest import capture 20 | 21 | from . import A 22 | from . import B 23 | from .A import A 24 | from .B import B 25 | from .. import A 26 | from .. import B 27 | from ..A import A 28 | from ..B import B 29 | """ 30 | 31 | assert Sorter('c').sort(input) == input 32 | 33 | 34 | def test_crypto_style_handles_newlines(): 35 | input = """from __future__ import absolute_import 36 | import pytest 37 | import enum 38 | import ast 39 | import os 40 | """ 41 | output = """from __future__ import absolute_import 42 | 43 | import ast 44 | import enum 45 | import os 46 | 47 | import pytest 48 | """ 49 | assert Sorter('c').sort(input) == output 50 | -------------------------------------------------------------------------------- /imps/tests/test_google.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.core import Sorter 4 | 5 | 6 | def test_google_style(): 7 | input = """from __future__ import absolute_import 8 | 9 | import ast 10 | from functools import * 11 | import os 12 | from os import path 13 | import StringIO 14 | import sys 15 | 16 | import flake8 17 | from flake8.defaults import * 18 | import pytest 19 | from pytest import * 20 | from pytest import capture 21 | from pytest import compat, Config 22 | 23 | from . import A 24 | from . import B 25 | from .A import A 26 | from .B import B 27 | from .. import A 28 | from .. import B 29 | from ..A import A 30 | from ..B import B 31 | """ 32 | assert Sorter('g').sort(input) == input 33 | 34 | 35 | def test_google_style_handles_newlines(): 36 | input = """from __future__ import absolute_import 37 | import pytest 38 | import enum 39 | 40 | import ast 41 | 42 | import os 43 | """ 44 | output = """from __future__ import absolute_import 45 | 46 | import ast 47 | import enum 48 | import os 49 | 50 | import pytest 51 | """ 52 | assert Sorter('g').sort(input) == output 53 | -------------------------------------------------------------------------------- /imps/tests/test_rebuilders.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.rebuilders import GenericBuilder 4 | 5 | 6 | def test_split_core_import(): 7 | s = GenericBuilder(max_line_length=40, indent=" ") 8 | ans = s._split_core_import("import alpha.alpha.alpha, beta.beta.beta, gamma.gamma.gamma") 9 | 10 | output = """import ( 11 | alpha.alpha.alpha, 12 | beta.beta.beta, 13 | gamma.gamma.gamma, 14 | )""" 15 | assert ans == output 16 | 17 | 18 | def test_split_core_import_noqa(): 19 | s = GenericBuilder(max_line_length=40, indent=" ") 20 | input = "import alpha.alpha.alpha, beta.beta.beta, gamma.gamma.gamma # NOQA" 21 | assert s._split_core_import(input) == input 22 | -------------------------------------------------------------------------------- /imps/tests/test_shell.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.shell import normalize_file_name 4 | 5 | 6 | def test_normalize_file_name(): 7 | assert normalize_file_name('tox.ini') == 'tox.ini' 8 | assert normalize_file_name('tox.ini:34:144') == 'tox.ini' 9 | assert normalize_file_name('bad_path.nothing') is None 10 | -------------------------------------------------------------------------------- /imps/tests/test_smarkets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.core import Sorter 4 | 5 | 6 | def test_smarkets_style(): 7 | input = '''from __future__ import absolute_import, division, print_function 8 | 9 | import ast 10 | import configparser 11 | import os 12 | import StringIO 13 | import sys 14 | from functools import * 15 | from os import path 16 | 17 | import flake8 18 | import pytest 19 | from flake8.defaults import NOQA_INLINE_REGEXP, STATISTIC_NAMES 20 | from flake8.exceptions import * 21 | from pytest import * 22 | from pytest import capture 23 | from pytest import compat, config 24 | 25 | from common.interfaces import Config 26 | from common.rest.decorators import jsonify 27 | from han.db import Database 28 | from winners.server.db_access import ( 29 | acknowledge_winner_exposure_for_market, 30 | get_acknowledged_winner_exposures_for_market, 31 | ) 32 | 33 | from . import A 34 | from . import B 35 | from .A import A 36 | from .B import B 37 | from .. import A 38 | from .. import B 39 | from ..A import A 40 | from ..B import B 41 | ''' 42 | 43 | assert Sorter('s', 80, ['common', 'winners', 'han']).sort(input) == input 44 | 45 | 46 | def test_smarkets_style_from_import_capitals_are_not_lowered(): 47 | input = '''from __future__ import absolute_import, division, print_function 48 | 49 | from imps.strings import AAAA 50 | from imps.strings import get_doc_string, strip_to_module_name, strip_to_module_name_from_import 51 | from imps.strings import ZZZZ 52 | ''' 53 | # Possible alternative: 54 | # output = '''from __future__ import absolute_import, division, print_function 55 | # 56 | # from imps.strings import ( 57 | # AAAA, 58 | # get_doc_string, 59 | # strip_to_module_name, 60 | # strip_to_module_name_from_import 61 | # ZZZZ, 62 | # ) 63 | # ''' 64 | 65 | assert Sorter('s', max_line_length=110).sort(input) == input 66 | 67 | 68 | def test_newlines_reduced(): 69 | s = Sorter('s', 80, ['local']) 70 | input = """import io 71 | 72 | 73 | import sys 74 | 75 | 76 | import A 77 | """ 78 | output = """import io 79 | import sys 80 | 81 | import A 82 | """ 83 | assert s.sort(input) == output 84 | 85 | 86 | def test_no_new_line_between_same_type(): 87 | s = Sorter(type='s', max_line_length=110, indent=" ") 88 | input_str = """ 89 | from __future__ import absolute_import, division, print_function 90 | 91 | import re 92 | 93 | from collections import OrderedDict 94 | """ 95 | correct = """from __future__ import absolute_import, division, print_function 96 | 97 | import re 98 | from collections import OrderedDict 99 | """ 100 | assert s.sort(input_str) == correct 101 | -------------------------------------------------------------------------------- /imps/tests/test_stdlib.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.stdlib import get_paths, LOCAL, STDLIB, strip_to_first_module, THIRDPARTY 4 | 5 | 6 | def test_strip_to_first_module(): 7 | assert strip_to_first_module('from alpha.beta import squid') == 'alpha' 8 | assert strip_to_first_module('import sys') == 'sys' 9 | assert strip_to_first_module('import sys, io') == 'sys' 10 | assert strip_to_first_module('from sys import stdin') == 'sys' 11 | assert strip_to_first_module('from . import A') == '.' 12 | assert strip_to_first_module('from ..B import A') == '..B' 13 | 14 | 15 | def test_path_std(): 16 | assert get_paths('import sys', []) == STDLIB 17 | assert get_paths('import io', []) == STDLIB 18 | assert get_paths('from contextlib import *', []) == STDLIB 19 | 20 | 21 | def test_path_local(): 22 | assert get_paths('import a_local_path', ['a_local_path']) == LOCAL 23 | assert get_paths('import a_local_path.submodule', ['a_local_path']) == LOCAL 24 | 25 | 26 | def test_path_third(): 27 | assert get_paths('import pytest', []) == THIRDPARTY 28 | assert get_paths('import flask.abort', []) == THIRDPARTY 29 | assert get_paths('fom six import sax', []) == THIRDPARTY 30 | -------------------------------------------------------------------------------- /imps/tests/test_strings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from imps.strings import get_doc_string, TRIPLE_DOUBLE, TRIPLE_SINGLE 4 | 5 | 6 | def test_doc_string_ignores_normal_line(): 7 | s = 'import A' 8 | assert not get_doc_string(s) 9 | 10 | 11 | def test_doc_string_ignores_doc_string_in_comment(): 12 | s = 'import A # triple comment \"\"\" ' 13 | assert not get_doc_string(s) 14 | 15 | 16 | def test_doc_string_ignores_strings(): 17 | s = '''s = '\"\"\"' ''' 18 | assert not get_doc_string(s) 19 | 20 | 21 | def test_doc_string_gets_data_after_a_string(): 22 | s = '''s = '\"\"\"' \"\"\" after a str \"\"\" ''' 23 | assert get_doc_string(s) == [(10, TRIPLE_DOUBLE), (29, TRIPLE_DOUBLE)] 24 | 25 | 26 | def test_doc_string_simple(): 27 | s = '''\"\"\" a doc string \"\"\"''' 28 | assert get_doc_string(s) == [(0, TRIPLE_DOUBLE), (20, TRIPLE_DOUBLE)] 29 | 30 | 31 | def test_doc_string_with_hash(): 32 | s = '''\"\"\" a doc string with hash # \"\"\"''' 33 | assert get_doc_string(s) == [(0, TRIPLE_DOUBLE), (32, TRIPLE_DOUBLE)] 34 | 35 | 36 | def test_doc_string_not_on_newline(): 37 | s = '''import A \"\"\"''' 38 | assert get_doc_string(s) == [(9, TRIPLE_DOUBLE)] 39 | 40 | 41 | def test_doc_string_with_single_quotes(): 42 | s = """\'\'\'import A \'\'\'""" 43 | 44 | assert get_doc_string(s) == [(0, TRIPLE_SINGLE), (15, TRIPLE_SINGLE)] 45 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Andy Boot 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | configparser 2 | flake8 3 | flake8-import-order 4 | pytest 5 | tox-travis 6 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from imps.shell import main 3 | 4 | if __name__ == '__main__': 5 | main() 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length=110 6 | exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,.tox,.vscode 7 | jobs=auto 8 | import-order-style=smarkets 9 | application-import-names=imps 10 | 11 | [imps] 12 | style=smarkets -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='imps', 6 | version='0.2.6', 7 | description='Python utility to sort Python imports', 8 | author='Andy Boot', 9 | author_email='bootandy@gmail.com', 10 | url='https://github.com/bootandy/imps', 11 | license="Apache", 12 | install_requires=['flake8-import-order', 'configparser'], 13 | packages=['imps'], 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'imps = imps.shell:main', 17 | ], 18 | }, 19 | keywords=['Refactoring', 'Imports'], 20 | classifiers=[ 21 | 'Intended Audience :: Developers', 22 | 'Natural Language :: English', 23 | 'Environment :: Console', 24 | 'Programming Language :: Python :: 2', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Topic :: Software Development :: Libraries', 30 | 'Topic :: Utilities', 31 | ] 32 | ) 33 | 34 | # Command to upload to pypi: 35 | # python setup.py sdist upload -r https://www.python.org/pypi 36 | # twine upload dist/* 37 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35,py36 3 | 4 | [testenv:tests] 5 | deps= -rrequirements.txt 6 | commands=pytest 7 | 8 | [testenv:flake8] 9 | deps= -rrequirements.txt 10 | commands=flake8 11 | 12 | [tox:travis] 13 | 2.7 = py27 14 | 3.5 = py35 15 | 3.6 = py36 16 | --------------------------------------------------------------------------------