diff --git a/pythonFiles/isort/__init__.py b/pythonFiles/isort/__init__.py index 082128cdedc1..3063d1ed92d8 100644 --- a/pythonFiles/isort/__init__.py +++ b/pythonFiles/isort/__init__.py @@ -25,4 +25,4 @@ from . import settings from .isort import SortImports -__version__ = "4.2.5" +__version__ = "4.2.15" diff --git a/pythonFiles/isort/__main__ b/pythonFiles/isort/__main__.py similarity index 100% rename from pythonFiles/isort/__main__ rename to pythonFiles/isort/__main__.py diff --git a/pythonFiles/isort/isort.py b/pythonFiles/isort/isort.py index 01d0f9cc7ed9..cecd5af991a1 100644 --- a/pythonFiles/isort/isort.py +++ b/pythonFiles/isort/isort.py @@ -37,12 +37,10 @@ from difflib import unified_diff from fnmatch import fnmatch from glob import glob -from sys import path as PYTHONPATH -from sys import stdout from . import settings from .natural import nsorted -from .pie_slice import * +from .pie_slice import OrderedDict, OrderedSet, input, itemsview KNOWN_SECTION_MAPPING = { 'STDLIB': 'STANDARD_LIBRARY', @@ -57,7 +55,7 @@ class SortImports(object): skipped = False def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, check=False, - show_diff=False, settings_path=None, ask_to_apply=False, **setting_overrides): + show_diff=False, settings_path=None, ask_to_apply=False, **setting_overrides): if not settings_path and file_path: settings_path = os.path.dirname(os.path.abspath(file_path)) settings_path = settings_path or os.getcwd() @@ -74,7 +72,7 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch else: self.config[key] = value - if self.config.get('force_alphabetical_sort', False): + if self.config['force_alphabetical_sort']: self.config.update({'force_alphabetical_sort_within_sections': True, 'no_sections': True, 'lines_between_types': 1, @@ -91,8 +89,8 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self.place_imports = {} self.import_placements = {} - self.remove_imports = [self._format_simplified(removal) for removal in self.config.get('remove_imports', [])] - self.add_imports = [self._format_natural(addition) for addition in self.config.get('add_imports', [])] + self.remove_imports = [self._format_simplified(removal) for removal in self.config['remove_imports']] + self.add_imports = [self._format_natural(addition) for addition in self.config['add_imports']] self._section_comments = ["# " + value for key, value in itemsview(self.config) if key.startswith('import_heading') and value] @@ -118,20 +116,29 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self.in_lines = file_contents.split("\n") self.original_length = len(self.in_lines) - if (self.original_length > 1 or self.in_lines[:1] not in ([], [""])) or self.config.get('force_adds', False): + if (self.original_length > 1 or self.in_lines[:1] not in ([], [""])) or self.config['force_adds']: for add_import in self.add_imports: self.in_lines.append(add_import) self.number_of_lines = len(self.in_lines) self.out_lines = [] self.comments = {'from': {}, 'straight': {}, 'nested': {}, 'above': {'straight': {}, 'from': {}}} - self.imports = {} + self.imports = OrderedDict() self.as_map = {} - section_names = self.config.get('sections') + section_names = self.config['sections'] self.sections = namedtuple('Sections', section_names)(*[name for name in section_names]) for section in itertools.chain(self.sections, self.config['forced_separate']): - self.imports[section] = {'straight': set(), 'from': {}} + self.imports[section] = {'straight': OrderedSet(), 'from': OrderedDict()} + + self.known_patterns = [] + for placement in reversed(self.sections): + known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) + config_key = 'known_{0}'.format(known_placement.lower()) + known_patterns = self.config.get(config_key, []) + for known_pattern in known_patterns: + self.known_patterns.append((re.compile('^' + known_pattern.replace('*', '.*').replace('?', '.?') + '$'), + placement)) self.index = 0 self.import_index = -1 @@ -147,7 +154,7 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch self.out_lines.append("") self.output = "\n".join(self.out_lines) - if self.config.get('atomic', False): + if self.config['atomic']: try: compile(self._strip_top_comments(self.out_lines), self.file_path, 'exec', 0, 1) except SyntaxError: @@ -164,7 +171,7 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch if check: check_output = self.output check_against = file_contents - if not self.config.get('enforce_white_space', False): + if self.config['ignore_whitespace']: check_output = check_output.replace("\n", "").replace(" ", "") check_against = check_against.replace("\n", "").replace(" ", "") @@ -175,14 +182,11 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch print("ERROR: {0} Imports are incorrectly sorted.".format(self.file_path)) self.incorrectly_sorted = True - if show_diff or self.config.get('show_diff', False) is True: - self._show_diff(file_contents) - - if show_diff or self.config.get('show_diff', False) is True: + if show_diff or self.config['show_diff']: self._show_diff(file_contents) elif write_to_stdout: - stdout.write(self.output) - elif file_name: + sys.stdout.write(self.output) + elif file_name and not check: if ask_to_apply: if self.output == file_contents: return @@ -207,7 +211,7 @@ def _show_diff(self, file_contents): if self.file_path else datetime.now()), tofiledate=str(datetime.now()) ): - stdout.write(line) + sys.stdout.write(line) @staticmethod def _strip_top_comments(lines): @@ -239,13 +243,15 @@ def place_module(self, module_name): parts = module_name.split('.') module_names_to_check = ['.'.join(parts[:first_k]) for first_k in range(len(parts), 0, -1)] for module_name_to_check in module_names_to_check: - for placement in reversed(self.sections): - known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = 'known_{0}'.format(known_placement.lower()) - if module_name_to_check in self.config.get(config_key, []): + for pattern, placement in self.known_patterns: + if pattern.match(module_name_to_check): return placement - paths = PYTHONPATH + # Use a copy of sys.path to avoid any unintended modifications + # to it - e.g. `+=` used below will change paths in place and + # if not copied, consequently sys.path, which will grow unbounded + # with duplicates on every call to this method. + paths = list(sys.path) virtual_env = self.config.get('virtual_env') or os.environ.get('VIRTUAL_ENV') virtual_env_src = False if virtual_env: @@ -254,15 +260,20 @@ def place_module(self, module_name): paths += [path for path in glob('{0}/src/*'.format(virtual_env)) if os.path.isdir(path)] virtual_env_src = '{0}/src/'.format(virtual_env) + # handle case-insensitive paths on windows + stdlib_lib_prefix = os.path.normcase(get_stdlib_path()) + for prefix in paths: module_path = "/".join((prefix, module_name.replace(".", "/"))) package_path = "/".join((prefix, module_name.split(".")[0])) - if (os.path.exists(module_path + ".py") or os.path.exists(module_path + ".so") or - (os.path.exists(package_path) and os.path.isdir(package_path))): + is_module = (exists_case_sensitive(module_path + ".py") or + exists_case_sensitive(module_path + ".so")) + is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) + if is_module or is_package: if ('site-packages' in prefix or 'dist-packages' in prefix or - (virtual_env and virtual_env_src in prefix)): + (virtual_env and virtual_env_src in prefix)): return self.sections.THIRDPARTY - elif 'python2' in prefix.lower() or 'python3' in prefix.lower(): + elif os.path.normcase(prefix).startswith(stdlib_lib_prefix): return self.sections.STDLIB else: return self.config['default_section'] @@ -319,9 +330,9 @@ def _wrap(self, line): """ Returns an import wrapped to the specified line-length, if possible. """ - wrap_mode = self.config.get('multi_line_output', 0) + wrap_mode = self.config['multi_line_output'] if len(line) > self.config['line_length'] and wrap_mode != settings.WrapModes.NOQA: - for splitter in ("import", "."): + for splitter in ("import", ".", "as"): exp = r"\b" + re.escape(splitter) + r"\b" if re.search(exp, line) and not line.strip().startswith(splitter): line_parts = re.split(exp, line) @@ -334,7 +345,18 @@ def _wrap(self, line): cont_line = self._wrap(self.config['indent'] + splitter.join(next_line).lstrip()) if self.config['use_parentheses']: - return "{0}{1} (\n{2})".format(line, splitter, cont_line) + output = "{0}{1} (\n{2}{3}{4})".format( + line, splitter, cont_line, + "," if self.config['include_trailing_comma'] else "", + "\n" if wrap_mode in ( + settings.WrapModes.VERTICAL_HANGING_INDENT, + settings.WrapModes.VERTICAL_GRID_GROUPED, + ) else "") + lines = output.split('\n') + if ' #' in lines[-1] and lines[-1].endswith(')'): + line, comment = lines[-1].split(' #', 1) + lines[-1] = line + ') #' + comment[:-1] + return '\n'.join(lines) return "{0}{1} \\\n{2}".format(line, splitter, cont_line) elif len(line) > self.config['line_length'] and wrap_mode == settings.WrapModes.NOQA: if "# NOQA" not in line: @@ -363,7 +385,7 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): continue import_start = "from {0} import ".format(module) - from_imports = list(self.imports[section]['from'][module]) + from_imports = self.imports[section]['from'][module] from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True, ignore_case)) if self.remove_imports: from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in @@ -378,11 +400,13 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): self.config['combine_star']): from_imports[from_imports.index(from_import)] = import_definition else: - import_statement = self._wrap(import_start + import_definition) + import_statement = import_start + import_definition + force_grid_wrap = self.config['force_grid_wrap'] comments = self.comments['straight'].get(submodule) - import_statement = self._add_comments(comments, import_statement) - section_output.append(import_statement) + import_statement = self._add_comments(comments, self._wrap(import_statement)) from_imports.remove(from_import) + section_output.append(import_statement) + if from_imports: comments = self.comments['from'].pop(module, ()) @@ -427,42 +451,20 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): do_multiline_reformat = False - if self.config.get('force_grid_wrap') and len(from_imports) > 1: + force_grid_wrap = self.config['force_grid_wrap'] + if force_grid_wrap and len(from_imports) >= force_grid_wrap: do_multiline_reformat = True if len(import_statement) > self.config['line_length'] and len(from_imports) > 1: do_multiline_reformat = True # If line too long AND have imports AND we are NOT using GRID or VERTICAL wrap modes - if (len(import_statement) > self.config['line_length'] and len(from_imports) > 0 - and self.config.get('multi_line_output', 0) not in (1, 0)): + if (len(import_statement) > self.config['line_length'] and len(from_imports) > 0 and + self.config['multi_line_output'] not in (1, 0)): do_multiline_reformat = True if do_multiline_reformat: - output_mode = settings.WrapModes._fields[self.config.get('multi_line_output', - 0)].lower() - formatter = getattr(self, "_output_" + output_mode, self._output_grid) - dynamic_indent = " " * (len(import_start) + 1) - indent = self.config['indent'] - line_length = self.config['wrap_length'] or self.config['line_length'] - import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - if self.config['balanced_wrapping']: - lines = import_statement.split("\n") - line_count = len(lines) - if len(lines) > 1: - minimum_length = min([len(line) for line in lines[:-1]]) - else: - minimum_length = 0 - new_import_statement = import_statement - while (len(lines[-1]) < minimum_length and - len(lines) == line_count and line_length > 10): - import_statement = new_import_statement - line_length -= 1 - new_import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - lines = new_import_statement.split("\n") - + import_statement = self._multi_line_reformat(import_start, from_imports, comments) if not do_multiline_reformat and len(import_statement) > self.config['line_length']: import_statement = self._wrap(import_statement) @@ -472,16 +474,43 @@ def _add_from_imports(self, from_modules, section, section_output, ignore_case): section_output.extend(above_comments) section_output.append(import_statement) + def _multi_line_reformat(self, import_start, from_imports, comments): + output_mode = settings.WrapModes._fields[self.config['multi_line_output']].lower() + formatter = getattr(self, "_output_" + output_mode, self._output_grid) + dynamic_indent = " " * (len(import_start) + 1) + indent = self.config['indent'] + line_length = self.config['wrap_length'] or self.config['line_length'] + import_statement = formatter(import_start, copy.copy(from_imports), + dynamic_indent, indent, line_length, comments) + if self.config['balanced_wrapping']: + lines = import_statement.split("\n") + line_count = len(lines) + if len(lines) > 1: + minimum_length = min([len(line) for line in lines[:-1]]) + else: + minimum_length = 0 + new_import_statement = import_statement + while (len(lines[-1]) < minimum_length and + len(lines) == line_count and line_length > 10): + import_statement = new_import_statement + line_length -= 1 + new_import_statement = formatter(import_start, copy.copy(from_imports), + dynamic_indent, indent, line_length, comments) + lines = new_import_statement.split("\n") + if import_statement.count('\n') == 0: + return self._wrap(import_statement) + return import_statement + 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 """ - sort_ignore_case = self.config.get('force_alphabetical_sort_within_sections', False) + sort_ignore_case = self.config['force_alphabetical_sort_within_sections'] sections = itertools.chain(self.sections, self.config['forced_separate']) - if self.config.get('no_sections', False): + if self.config['no_sections']: self.imports['no_sections'] = {'straight': [], 'from': {}} for section in sections: self.imports['no_sections']['straight'].extend(self.imports[section].get('straight', [])) @@ -490,30 +519,36 @@ def _add_formatted_imports(self): output = [] for section in sections: - straight_modules = list(self.imports[section]['straight']) + straight_modules = self.imports[section]['straight'] straight_modules = nsorted(straight_modules, key=lambda key: self._module_key(key, self.config)) - from_modules = sorted(list(self.imports[section]['from'].keys())) - from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config, )) + from_modules = self.imports[section]['from'] + from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config)) section_output = [] - if self.config.get('from_first', False): + if self.config['from_first']: self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if self.config.get('lines_between_types', 0) and from_modules and straight_modules: + if self.config['lines_between_types'] and from_modules and straight_modules: section_output.extend([''] * self.config['lines_between_types']) self._add_straight_imports(straight_modules, section, section_output) else: self._add_straight_imports(straight_modules, section, section_output) - if self.config.get('lines_between_types', 0) and from_modules and straight_modules: + if self.config['lines_between_types'] and from_modules and straight_modules: section_output.extend([''] * self.config['lines_between_types']) self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if self.config.get('force_sort_within_sections', False): + if self.config['force_sort_within_sections']: def by_module(line): + section = 'B' + if line.startswith('#'): + return 'AA' + line = re.sub('^from ', '', line) line = re.sub('^import ', '', line) + if line.split(' ')[0] in self.config['force_to_top']: + section = 'A' if not self.config['order_by_type']: line = line.lower() - return line + return '{0}{1}'.format(section, line) section_output = nsorted(section_output, key=by_module) if section_output: @@ -525,7 +560,7 @@ def by_module(line): section_title = self.config.get('import_heading_' + str(section_name).lower(), '') if section_title: section_comment = "# {0}".format(section_title) - if not section_comment in self.out_lines[0:1]: + if not section_comment in self.out_lines[0:1] and not section_comment in self.in_lines[0:1]: section_output.insert(0, section_comment) output += section_output + ([''] * self.config['lines_between_sections']) @@ -546,15 +581,18 @@ def by_module(line): if len(self.out_lines) > imports_tail: next_construct = "" self._in_quote = False - for line in self.out_lines[imports_tail:]: - if not self._skip_line(line) and not line.strip().startswith("#") and line.strip(): + tail = self.out_lines[imports_tail:] + for index, line in enumerate(tail): + if not self._skip_line(line) and line.strip(): + if line.strip().startswith("#") and len(tail) > (index + 1) and tail[index + 1].strip(): + continue next_construct = line break if self.config['lines_after_imports'] != -1: self.out_lines[imports_tail:0] = ["" for line in range(self.config['lines_after_imports'])] elif next_construct.startswith("def") or next_construct.startswith("class") or \ - next_construct.startswith("@"): + next_construct.startswith("@") or next_construct.startswith("async def"): self.out_lines[imports_tail:0] = ["", ""] else: self.out_lines[imports_tail:0] = [""] @@ -575,8 +613,16 @@ def _output_grid(self, statement, imports, white_space, indent, line_length, com next_import = imports.pop(0) next_statement = self._add_comments(comments, statement + ", " + next_import) if len(next_statement.split("\n")[-1]) + 1 > line_length: + lines = ['{0}{1}'.format(white_space, next_import.split(" ")[0])] + for part in next_import.split(" ")[1:]: + new_line = '{0} {1}'.format(lines[-1], part) + if len(new_line) + 1 > line_length: + lines.append('{0}{1}'.format(white_space, part)) + else: + lines[-1] = new_line + next_import = '\n'.join(lines) statement = (self._add_comments(comments, "{0},".format(statement)) + - "\n{0}{1}".format(white_space, next_import)) + "\n{0}".format(next_import)) comments = None else: statement += ", " + next_import @@ -693,7 +739,7 @@ def _skip_line(self, line): elif self._in_top_comment: if not line.startswith("#"): self._in_top_comment = False - self._first_comment_index_end = self.index + self._first_comment_index_end = self.index - 1 if '"' in line or "'" in line: index = 0 @@ -767,7 +813,7 @@ def _parse(self): self.out_lines.append(line) continue - line = line.replace("\t", " ") + line = line.replace("\t", " ").replace('import*', 'import *') if self.import_index == -1: self.import_index = self.index - 1 @@ -778,7 +824,7 @@ def _parse(self): if import_type == "from" and len(stripped_line) == 2 and stripped_line[1] != "*" and new_comments: nested_comments[stripped_line[-1]] = comments[0] - if "(" in line and not self._at_end(): + if "(" in line.split("#")[0] and not self._at_end(): while not line.strip().endswith(")") and not self._at_end(): line, comments, new_comments = self._strip_comments(self._get_line(), comments) stripped_line = self._strip_syntax(line).strip() @@ -834,12 +880,12 @@ def _parse(self): if comments: self.comments['from'].setdefault(import_from, []).extend(comments) - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end, 1) - 1: + if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: last = self.out_lines and self.out_lines[-1].rstrip() or "" while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and not 'isort:imports-' in last): self.comments['above']['from'].setdefault(import_from, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, 1) - 1: + if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - 1: last = self.out_lines[-1].rstrip() else: last = "" @@ -849,21 +895,21 @@ def _parse(self): if root.get(import_from, False): root[import_from].update(imports) else: - root[import_from] = set(imports) + root[import_from] = OrderedSet(imports) else: for module in imports: if comments: self.comments['straight'][module] = comments comments = None - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end, 1) - 1: + if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: + last = self.out_lines and self.out_lines[-1].rstrip() or "" - while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") - and not 'isort:imports-' in last): + while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and + not 'isort:imports-' in last): self.comments['above']['straight'].setdefault(module, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, - 1) - 1: + if len(self.out_lines) > 0: last = self.out_lines[-1].rstrip() else: last = "" @@ -894,3 +940,30 @@ def coding_check(fname, default='utf-8'): break return coding + + +def get_stdlib_path(): + """Returns the path to the standard lib for the current path installation. + + This function can be dropped and "sysconfig.get_paths()" used directly once Python 2.6 support is dropped. + """ + if sys.version_info >= (2, 7): + import sysconfig + return sysconfig.get_paths()['stdlib'] + else: + return os.path.join(sys.prefix, 'lib') + + +def exists_case_sensitive(path): + """ + Returns if the given path exists and also matches the case on Windows. + + When finding files that can be imported, it is important for the cases to match because while + file os.path.exists("module.py") and os.path.exists("MODULE.py") both return True on Windows, Python + can only import using the case of the real file. + """ + result = os.path.exists(path) + if sys.platform.startswith('win') and result: + directory, basename = os.path.split(path) + result = basename in os.listdir(directory) + return result diff --git a/pythonFiles/isort/main.py b/pythonFiles/isort/main.py index b5ca93d9207f..eae7afa53409 100755 --- a/pythonFiles/isort/main.py +++ b/pythonFiles/isort/main.py @@ -30,11 +30,11 @@ from isort import SortImports, __version__ from isort.settings import DEFAULT_SECTIONS, default, from_path, should_skip -from .pie_slice import * +from .pie_slice import itemsview -INTRO = """ -/#######################################################################\\ +INTRO = r""" +/#######################################################################\ `sMMy` .yyyy- ` @@ -130,7 +130,7 @@ def run(self): if incorrectly_sorted: wrong_sorted_files = True except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) + print("WARNING: Unable to parse file {0} due to {1}".format(python_file, e)) if wrong_sorted_files: exit(1) @@ -164,37 +164,35 @@ def create_parser(): help='Force sortImports to recognize a module as being part of the current python project.') parser.add_argument('--virtual-env', dest='virtual_env', help='Virtual environment to use for determining whether a package is third-party') - parser.add_argument('-m', '--multi_line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], + parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' '5-vert-grid-grouped).') 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', + 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('-af', '--force_adds', dest='force_adds', action='store_true', + parser.add_argument('-af', '--force-adds', dest='force_adds', action='store_true', help='Forces import adds even if the original file is empty.') - parser.add_argument('-r', '--remove_import', dest='remove_imports', action='append', + 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('-ls', '--length-sort', help='Sort imports by their string length.', + dest='length_sort', action='store_true') parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', dest='write_to_stdout', action='store_true') - parser.add_argument('-c', '--check-only', action='store_true', default=False, dest="check", + parser.add_argument('-c', '--check-only', action='store_true', dest="check", help='Checks the file for unsorted / unformatted imports and prints them to the ' 'command line without modifying the file.') - parser.add_argument('-ws', '--enforce-white-space', action='store_true', default=False, dest="enforce_white_space", - help='Tells isort to enforce white space difference when --check-only is being used.') + parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", + help='Tells isort to ignore whitespace differences when --check-only is being used.') parser.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', help='Forces all from imports to appear on their own line') - parser.add_argument('--force_single_line_imports', dest='force_single_line', action='store_true', - help=argparse.SUPPRESS) parser.add_argument('-ds', '--no-sections', help='Put all imports into the same section bucket', dest='no_sections', action='store_true') parser.add_argument('-sd', '--section-default', dest='default_section', help='Sets the default section for imports (by default FIRSTPARTY) options: ' + str(DEFAULT_SECTIONS)) - parser.add_argument('-df', '--diff', dest='show_diff', default=False, action='store_true', + parser.add_argument('-df', '--diff', dest='show_diff', action='store_true', help="Prints a diff of all the changes isort would make to a file, instead of " "changing it in place") parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', @@ -218,22 +216,25 @@ def create_parser(): help='Shows verbose output, such as when files are skipped or when a check is successful.') parser.add_argument('-q', '--quiet', action='store_true', dest="quiet", help='Shows extra quiet output, only errors are outputted.') - parser.add_argument('-sp', '--settings-path', dest="settings_path", + parser.add_argument('-sp', '--settings-path', dest="settings_path", help='Explicitly set the settings path instead of auto determining based on file location.') parser.add_argument('-ff', '--from-first', dest='from_first', help="Switches the typical ordering preference, showing from imports first then straight ones.") parser.add_argument('-wl', '--wrap-length', dest='wrap_length', help="Specifies how long lines that are wrapped should be, if not set line_length is used.") - parser.add_argument('-fgw', '--force-grid-wrap', action='store_true', dest="force_grid_wrap", - help='Force from imports to be grid wrapped regardless of line length') - parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', + parser.add_argument('-fgw', '--force-grid-wrap', nargs='?', const=2, type=int, dest="force_grid_wrap", + help='Force number of from imports (defaults to 2) to be grid wrapped regardless of line ' + 'length') + parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', dest="force_alphabetical_sort", help='Force all imports to be sorted alphabetically within a ' 'section') - parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", + parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", help='Force all imports to be sorted as a single section') - parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", + parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", help='Force imports to be sorted by module, independent of import_type') parser.add_argument('-lbt', '--lines-between-types', dest='lines_between_types', type=int) + parser.add_argument('-up', '--use-parentheses', dest='use_parentheses', action='store_true', + help='Use parenthesis for line continuation on lenght limit instead of slashes.') arguments = dict((key, value) for (key, value) in itemsview(vars(parser.parse_args())) if value) if 'dont_order_by_type' in arguments: @@ -267,7 +268,7 @@ def main(): if arguments.get('recursive', False): file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 - if config.get('verbose', False) or config.get('show_logo', False): + if config['verbose'] or config.get('show_logo', False): print(INTRO) for file_name in file_names: try: diff --git a/pythonFiles/isort/natural.py b/pythonFiles/isort/natural.py index 0529fa60bb95..aac8c4a36157 100644 --- a/pythonFiles/isort/natural.py +++ b/pythonFiles/isort/natural.py @@ -33,7 +33,7 @@ def _atoi(text): def _natural_keys(text): - return [_atoi(c) for c in re.split('(\d+)', text)] + return [_atoi(c) for c in re.split(r'(\d+)', text)] def nsorted(to_sort, key=None): diff --git a/pythonFiles/isort/pie_slice.py b/pythonFiles/isort/pie_slice.py index d8a35769ece9..bfb341a7f7ec 100644 --- a/pythonFiles/isort/pie_slice.py +++ b/pythonFiles/isort/pie_slice.py @@ -22,6 +22,7 @@ from __future__ import absolute_import import abc +import collections import functools import sys from numbers import Integral @@ -67,6 +68,7 @@ class Form(with_metaclass(FormType, BaseForm)): class metaclass(meta): __call__ = type.__call__ __init__ = type.__init__ + def __new__(cls, name, this_bases, d): if this_bases is None: return type.__new__(cls, name, (), d) @@ -118,6 +120,7 @@ def __instancecheck__(cls, instance): import builtins from urllib import parse + input = input integer_types = (int, ) def u(string): @@ -428,7 +431,7 @@ def __eq__(self, other): if isinstance(other, OrderedDict): if len(self) != len(other): return False - for p, q in zip(self.items(), other.items()): + for p, q in zip(self.items(), other.items()): if p != q: return False return True @@ -526,3 +529,66 @@ def cache_clear(): else: from functools import lru_cache + + +class OrderedSet(collections.MutableSet): + + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] + self.map = {} + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): + if not self: + raise KeyError('set is empty') + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) + + def update(self, other): + for item in other: + self.add(item) diff --git a/pythonFiles/isort/settings.py b/pythonFiles/isort/settings.py index d17ba58aa8b4..15cdb21094ac 100644 --- a/pythonFiles/isort/settings.py +++ b/pythonFiles/isort/settings.py @@ -26,9 +26,10 @@ import fnmatch import os +import posixpath from collections import namedtuple -from .pie_slice import * +from .pie_slice import itemsview, lru_cache, native_str try: import configparser @@ -36,7 +37,7 @@ import ConfigParser as configparser MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories isort will look for a config file within -DEFAULT_SECTIONS = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") +DEFAULT_SECTIONS = ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED', 'NOQA') WrapModes = namedtuple('WrapModes', WrapModes)(*range(len(WrapModes))) @@ -50,26 +51,51 @@ 'sections': DEFAULT_SECTIONS, 'no_sections': False, 'known_future_library': ['__future__'], - '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", - "errno", "exceptions", "filecmp", "fileinput", "fnmatch", "fractions", - "functools", "gc", "gdbm", "getopt", "getpass", "gettext", "glob", "grp", "gzip", - "hashlib", "heapq", "hmac", "imaplib", "imp", "inspect", "io", "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", 'builtins', '__builtin__', 'thread', - "binascii", "statistics", "unicodedata", "fcntl", 'pathlib'], + 'known_standard_library': ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', + 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', + 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', + 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', + 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', + 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', + 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', + 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', + 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', + 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', + 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', + 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', + 'contextlib', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', + 'ctypes', 'curses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', + 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', + 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', + 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', + 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', + 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', + 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', + 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', + 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', + 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', + 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', + 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', + 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', + 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', + 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', + 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', + 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', + 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', + 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', + 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', + 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', + 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', + 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', + 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', + 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', + 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', + 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', + 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', + 'urlparse', 'user', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', + 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', + 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', + 'zipimport', 'zlib'], 'known_third_party': ['google.appengine.api'], 'known_first_party': [], 'multi_line_output': WrapModes.GRID, @@ -101,10 +127,10 @@ 'force_adds': False, 'force_alphabetical_sort_within_sections': False, 'force_alphabetical_sort': False, - 'force_grid_wrap': False, + 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'show_diff': False, - 'enforce_white_space': False} + 'ignore_whitespace': False} @lru_cache() @@ -113,6 +139,7 @@ def from_path(path): _update_settings_with_config(path, '.editorconfig', '~/.editorconfig', ('*', '*.py', '**.py'), computed_settings) _update_settings_with_config(path, '.isort.cfg', '~/.isort.cfg', ('settings', 'isort'), computed_settings) _update_settings_with_config(path, 'setup.cfg', None, ('isort', ), computed_settings) + _update_settings_with_config(path, 'tox.ini', None, ('isort', ), computed_settings) return computed_settings @@ -141,17 +168,17 @@ def _update_with_config_file(file_path, sections, computed_settings): if not settings: return - if file_path.endswith(".editorconfig"): - indent_style = settings.pop('indent_style', "").strip() - indent_size = settings.pop('indent_size', "").strip() - if indent_style == "space": - computed_settings['indent'] = " " * (indent_size and int(indent_size) or 4) - elif indent_style == "tab": - computed_settings['indent'] = "\t" * (indent_size and int(indent_size) or 1) + if file_path.endswith('.editorconfig'): + indent_style = settings.pop('indent_style', '').strip() + indent_size = settings.pop('indent_size', '').strip() + if indent_style == 'space': + computed_settings['indent'] = ' ' * (indent_size and int(indent_size) or 4) + elif indent_style == 'tab': + computed_settings['indent'] = '\t' * (indent_size and int(indent_size) or 1) - max_line_length = settings.pop('max_line_length', "").strip() + max_line_length = settings.pop('max_line_length', '').strip() if max_line_length: - computed_settings['line_length'] = int(max_line_length) + computed_settings['line_length'] = float('inf') if max_line_length == 'off' else int(max_line_length) for key, value in itemsview(settings): access_key = key.replace('not_', '').lower() @@ -166,27 +193,34 @@ def _update_with_config_file(file_path, sections, computed_settings): computed_settings[access_key] = list(existing_data.difference(_as_list(value))) else: computed_settings[access_key] = list(existing_data.union(_as_list(value))) - elif existing_value_type == bool and value.lower().strip() == "false": + elif existing_value_type == bool and value.lower().strip() == 'false': computed_settings[access_key] = False elif key.startswith('known_'): computed_settings[access_key] = list(_as_list(value)) + elif key == 'force_grid_wrap': + try: + result = existing_value_type(value) + except ValueError: + # backwards compat + result = default.get(access_key) if value.lower().strip() == 'false' else 2 + computed_settings[access_key] = result else: computed_settings[access_key] = existing_value_type(value) def _as_list(value): - return filter(bool, [item.strip() for item in value.replace('\n', ',').split(",")]) + return filter(bool, [item.strip() for item in value.replace('\n', ',').split(',')]) @lru_cache() def _get_config_data(file_path, sections): with open(file_path, 'rU') as config_file: - if file_path.endswith(".editorconfig"): - line = "\n" + if file_path.endswith('.editorconfig'): + line = '\n' last_position = config_file.tell() while line: line = config_file.readline() - if "[" in line: + if '[' in line: config_file.seek(last_position) break last_position = config_file.tell() @@ -206,7 +240,7 @@ def _get_config_data(file_path, sections): def should_skip(filename, config, path='/'): """Returns True if the file should be skipped based on the passed in settings.""" for skip_path in config['skip']: - if os.path.join(path, filename).endswith('/' + skip_path.lstrip('/')): + if posixpath.abspath(posixpath.join(path, filename)) == posixpath.abspath(skip_path.replace('\\', '/')): return True position = os.path.split(filename)