diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | isort/__init__.py | 2 | ||||
| -rw-r--r-- | isort/isort.py | 63 | ||||
| -rwxr-xr-x | isort/main.py | 8 | ||||
| -rw-r--r-- | isort/natural.py | 46 | ||||
| -rw-r--r-- | isort/pie_slice.py | 528 | ||||
| -rw-r--r-- | isort/settings.py | 8 | ||||
| -rw-r--r-- | runtests.py | 2 | ||||
| -rw-r--r-- | setup.cfg | 4 | ||||
| -rwxr-xr-x | setup.py | 3 | ||||
| -rw-r--r-- | test_isort.py | 102 |
12 files changed, 745 insertions, 43 deletions
@@ -17,6 +17,7 @@ develop-eggs lib lib64 MANIFEST +.eggs # Installer logs pip-log.txt @@ -143,6 +143,7 @@ and puts them all at the top of the file grouped together by the type of import: - Current Python Project - Explicitly Local (. before import, as in: from . import x) - Custom Separate Sections (Defined by forced_separate list in configuration file) +- Custom Sections (Defined by sections list in configuration file) Inside of each section the imports are sorted alphabetically. isort automatically removes duplicate python imports, and wraps long from imports to the specified line length (defaults to 80). @@ -286,6 +287,26 @@ Will be produced instead of: To enable this set 'balanced_wrapping' to True in your config or pass the -e option into the command line utility. +Custom Sections and Ordering +============================ + +You can change the section order with `sections` option from the default of: + + FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER + +to your preference: + + sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER + +You also can define your own sections and thier order. + +Example: + + known_django=django + known_pandas=pandas,numpy + sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER + +would create two new sections with the specified known modules. Auto-comment import sections ====================== diff --git a/isort/__init__.py b/isort/__init__.py index 4a445fa7..2c399d8e 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -23,6 +23,6 @@ OTHER DEALINGS IN THE SOFTWARE. from __future__ import absolute_import, division, print_function, unicode_literals from . import settings -from .isort import SECTION_NAMES, SECTIONS, SortImports +from .isort import SortImports __version__ = "3.9.6" diff --git a/isort/isort.py b/isort/isort.py index 0ec78206..274be1cf 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -38,13 +38,17 @@ from difflib import unified_diff from sys import path as PYTHONPATH from sys import stderr, stdout -from natsort import natsorted -from pies.overrides import * +from .natural import nsorted +from .pie_slice import * from . import settings -SECTION_NAMES = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") -SECTIONS = namedtuple('Sections', SECTION_NAMES)(*range(len(SECTION_NAMES))) +KNOWN_SECTION_MAPPING = { + 'STDLIB': 'STANDARD_LIBRARY', + 'FUTURE': 'FUTURE_LIBRARY', + 'FIRSTPARTY': 'FIRST_PARTY', + 'THIRDPARTY': 'THIRD_PARTY', +} class SortImports(object): @@ -61,7 +65,8 @@ class SortImports(object): self.config = settings.from_path(settings_path).copy() for key, value in itemsview(setting_overrides): access_key = key.replace('not_', '').lower() - if type(self.config.get(access_key)) in (list, tuple): + # The sections config needs to retain order and can't be converted to a set. + if access_key != 'sections' and type(self.config.get(access_key)) in (list, tuple): if key.startswith('not_'): self.config[access_key] = list(set(self.config[access_key]).difference(value)) else: @@ -115,7 +120,10 @@ class SortImports(object): self.comments = {'from': {}, 'straight': {}, 'nested': {}, 'above': {'straight': {}, 'from': {}}} self.imports = {} self.as_map = {} - for section in itertools.chain(SECTIONS, self.config['forced_separate']): + + section_names = self.config.get('sections') + self.sections = namedtuple('Sections', section_names)(*[n for n in section_names]) + for section in itertools.chain(self.sections, self.config['forced_separate']): self.imports[section] = {'straight': set(), 'from': {}} self.index = 0 @@ -206,19 +214,16 @@ class SortImports(object): return forced_separate if moduleName.startswith("."): - return SECTIONS.LOCALFOLDER + return self.sections.LOCALFOLDER # Try to find most specific placement instruction match (if any) parts = moduleName.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, config_key in ( - (SECTIONS.FUTURE, 'known_future_library'), - (SECTIONS.STDLIB, 'known_standard_library'), - (SECTIONS.THIRDPARTY, 'known_third_party'), - (SECTIONS.FIRSTPARTY, 'known_first_party'), - ): - if module_name_to_check in self.config[config_key]: + 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, []): return placement paths = PYTHONPATH @@ -234,13 +239,13 @@ class SortImports(object): 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))): if "site-packages" in prefix or "dist-packages" in prefix: - return SECTIONS.THIRDPARTY + return self.sections.THIRDPARTY elif "python2" in prefix.lower() or "python3" in prefix.lower(): - return SECTIONS.STDLIB + return self.sections.STDLIB else: - return SECTIONS.FIRSTPARTY + return self.sections.FIRSTPARTY - return SECTION_NAMES.index(self.config['default_section']) + return self.config['default_section'] def _get_line(self): """Returns the current line from the file while incrementing the index.""" @@ -290,16 +295,20 @@ class SortImports(object): """ if len(line) > self.config['line_length']: for splitter in ("import", "."): - if splitter in line and not line.strip().startswith(splitter): - line_parts = line.split(splitter) + 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) next_line = [] while (len(line) + 2) > (self.config['wrap_length'] or self.config['line_length']) and line_parts: next_line.append(line_parts.pop()) line = splitter.join(line_parts) if not line: line = next_line.pop() - return "{0}{1} \\\n{2}".format(line, splitter, - self._wrap(self.config['indent'] + splitter.join(next_line).lstrip())) + + 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) + return "{0}{1} \\\n{2}".format(line, splitter, cont_line) return line @@ -325,7 +334,7 @@ class SortImports(object): import_start = "from {0} import ".format(module) from_imports = list(self.imports[section]['from'][module]) - from_imports = natsorted(from_imports, key=lambda key: self._module_key(key, self.config, True)) + from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True)) if self.remove_imports: from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in self.remove_imports] @@ -428,11 +437,11 @@ class SortImports(object): """ output = [] - for section in itertools.chain(SECTIONS, self.config['forced_separate']): + for section in itertools.chain(self.sections, self.config['forced_separate']): straight_modules = list(self.imports[section]['straight']) - straight_modules = natsorted(straight_modules, key=lambda key: self._module_key(key, self.config)) + 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 = natsorted(from_modules, key=lambda key: self._module_key(key, self.config, )) + from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config, )) section_output = [] if self.config.get('from_first', False): @@ -444,8 +453,6 @@ class SortImports(object): if section_output: section_name = section - if section in SECTIONS: - section_name = SECTION_NAMES[section] if section_name in self.place_imports: self.place_imports[section_name] = section_output continue diff --git a/isort/main.py b/isort/main.py index c2b42cf2..86270675 100755 --- a/isort/main.py +++ b/isort/main.py @@ -26,10 +26,10 @@ import os import sys import setuptools -from pies.overrides import * +from .pie_slice import * -from isort import SECTION_NAMES, SortImports, __version__ -from isort.settings import default, from_path +from isort import SortImports, __version__ +from isort.settings import DEFAULT_SECTIONS, default, from_path def iter_source_code(paths): @@ -143,7 +143,7 @@ def create_parser(): help='Forces all from imports to appear on their own line') parser.add_argument('-sd', '--section-default', dest='default_section', help='Sets the default section for imports (by default FIRSTPARTY) options: ' + - str(SECTION_NAMES)) + str(DEFAULT_SECTIONS)) parser.add_argument('-df', '--diff', dest='show_diff', default=False, action='store_true', help="Prints a diff of all the changes isort would make to a file, instead of " "changing it in place") diff --git a/isort/natural.py b/isort/natural.py new file mode 100644 index 00000000..a20ba1d4 --- /dev/null +++ b/isort/natural.py @@ -0,0 +1,46 @@ +"""isort/natural.py. + +Enables sorting strings that contain numbers naturally + +usage: + natural.nsorted(list) + +Copyright (C) 2013 Timothy Edmund Crosley + +Implementation originally from @HappyLeapSecond stack overflow user in response to: + http://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +""" +import re + + +def _atoi(text): + return int(text) if text.isdigit() else text + + +def _natural_keys(text): + return [_atoi(c) for c in re.split('(\d+)', text)] + + +def nsorted(to_sort, key=None): + """Returns a naturally sorted list""" + if not key: + key_callback = _natural_keys + else: + key_callback = lambda item: _natural_keys(key(item)) + + return sorted(to_sort, key=key_callback) diff --git a/isort/pie_slice.py b/isort/pie_slice.py new file mode 100644 index 00000000..5cef39b1 --- /dev/null +++ b/isort/pie_slice.py @@ -0,0 +1,528 @@ +"""pie_slice/overrides.py. + +Overrides Python syntax to conform to the Python3 version as much as possible using a '*' import + +Copyright (C) 2013 Timothy Edmund Crosley + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copie_slice of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copie_slice or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +""" +from __future__ import absolute_import + +import abc +import functools +import sys +from numbers import Integral + +__version__ = "1.1.0" + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +VERSION = sys.version_info + +native_dict = dict +native_round = round +native_filter = filter +native_map = map +native_zip = zip +native_range = range +native_str = str +native_chr = chr +native_input = input +native_next = next +native_object = object + +common = ['native_dict', 'native_round', 'native_filter', 'native_map', 'native_range', 'native_str', 'native_chr', + 'native_input', 'PY2', 'PY3', 'u', 'itemsview', 'valuesview', 'keysview', 'execute', 'integer_types', + 'native_next', 'native_object', 'with_metaclass', 'OrderedDict', 'lru_cache'] + + +def with_metaclass(meta, *bases): + """Enables use of meta classes across Python Versions. taken from jinja2/_compat.py. + + Use it like this:: + + class BaseForm(object): + pass + + class FormType(type): + pass + + class Form(with_metaclass(FormType, BaseForm)): + pass + + """ + 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) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + + +def unmodified_isinstance(*bases): + """When called in the form + + MyOverrideClass(unmodified_isinstance(BuiltInClass)) + + it allows calls against passed in built in instances to pass even if there not a subclass + + """ + class UnmodifiedIsInstance(type): + if sys.version_info[0] == 2 and sys.version_info[1] <= 6: + + @classmethod + def __instancecheck__(cls, instance): + if cls.__name__ in (str(base.__name__) for base in bases): + return isinstance(instance, bases) + + subclass = getattr(instance, '__class__', None) + subtype = type(instance) + instance_type = getattr(abc, '_InstanceType', None) + if not instance_type: + class test_object: + pass + instance_type = type(test_object) + if subtype is instance_type: + subtype = subclass + if subtype is subclass or subclass is None: + return cls.__subclasscheck__(subtype) + return (cls.__subclasscheck__(subclass) or cls.__subclasscheck__(subtype)) + else: + @classmethod + def __instancecheck__(cls, instance): + if cls.__name__ in (str(base.__name__) for base in bases): + return isinstance(instance, bases) + + return type.__instancecheck__(cls, instance) + + return with_metaclass(UnmodifiedIsInstance, *bases) + + +if PY3: + import urllib + import builtins + from urllib import parse + + integer_types = (int, ) + + def u(string): + return string + + def itemsview(collection): + return collection.items() + + def valuesview(collection): + return collection.values() + + def keysview(collection): + return collection.keys() + + urllib.quote = parse.quote + urllib.quote_plus = parse.quote_plus + urllib.unquote = parse.unquote + urllib.unquote_plus = parse.unquote_plus + urllib.urlencode = parse.urlencode + execute = getattr(builtins, 'exec') + if VERSION[1] < 2: + def callable(entity): + return hasattr(entity, '__call__') + common.append('callable') + + __all__ = common + ['urllib'] +else: + from itertools import ifilter as filter + from itertools import imap as map + from itertools import izip as zip + from decimal import Decimal, ROUND_HALF_EVEN + + import codecs + str = unicode + chr = unichr + input = raw_input + range = xrange + integer_types = (int, long) + + import sys + stdout = sys.stdout + stderr = sys.stderr + reload(sys) + sys.stdout = stdout + sys.stderr = stderr + sys.setdefaultencoding('utf-8') + + def _create_not_allowed(name): + def _not_allow(*args, **kwargs): + raise NameError("name '{0}' is not defined".format(name)) + _not_allow.__name__ = name + return _not_allow + + for removed in ('apply', 'cmp', 'coerce', 'execfile', 'raw_input', 'unpacks'): + globals()[removed] = _create_not_allowed(removed) + + def u(s): + if isinstance(s, unicode): + return s + else: + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + + def execute(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + class _dict_view_base(object): + __slots__ = ('_dictionary', ) + + def __init__(self, dictionary): + self._dictionary = dictionary + + def __repr__(self): + return "{0}({1})".format(self.__class__.__name__, str(list(self.__iter__()))) + + def __unicode__(self): + return str(self.__repr__()) + + def __str__(self): + return str(self.__unicode__()) + + class dict_keys(_dict_view_base): + __slots__ = () + + def __iter__(self): + return self._dictionary.iterkeys() + + class dict_values(_dict_view_base): + __slots__ = () + + def __iter__(self): + return self._dictionary.itervalues() + + class dict_items(_dict_view_base): + __slots__ = () + + def __iter__(self): + return self._dictionary.iteritems() + + def itemsview(collection): + return dict_items(collection) + + def valuesview(collection): + return dict_values(collection) + + def keysview(collection): + return dict_keys(collection) + + class dict(unmodified_isinstance(native_dict)): + def has_key(self, *args, **kwargs): + return AttributeError("'dict' object has no attribute 'has_key'") + + def items(self): + return dict_items(self) + + def keys(self): + return dict_keys(self) + + def values(self): + return dict_values(self) + + def round(number, ndigits=None): + return_int = False + if ndigits is None: + return_int = True + ndigits = 0 + if hasattr(number, '__round__'): + return number.__round__(ndigits) + + if ndigits < 0: + raise NotImplementedError('negative ndigits not supported yet') + exponent = Decimal('10') ** (-ndigits) + d = Decimal.from_float(number).quantize(exponent, + rounding=ROUND_HALF_EVEN) + if return_int: + return int(d) + else: + return float(d) + + def next(iterator): + try: + iterator.__next__() + except Exception: + native_next(iterator) + + class FixStr(type): + def __new__(cls, name, bases, dct): + if '__str__' in dct: + dct['__unicode__'] = dct['__str__'] + dct['__str__'] = lambda self: self.__unicode__().encode('utf-8') + return type.__new__(cls, name, bases, dct) + + if sys.version_info[1] <= 6: + def __instancecheck__(cls, instance): + if cls.__name__ == "object": + return isinstance(instance, native_object) + + subclass = getattr(instance, '__class__', None) + subtype = type(instance) + instance_type = getattr(abc, '_InstanceType', None) + if not instance_type: + class test_object: + pass + instance_type = type(test_object) + if subtype is instance_type: + subtype = subclass + if subtype is subclass or subclass is None: + return cls.__subclasscheck__(subtype) + return (cls.__subclasscheck__(subclass) or cls.__subclasscheck__(subtype)) + else: + def __instancecheck__(cls, instance): + if cls.__name__ == "object": + return isinstance(instance, native_object) + return type.__instancecheck__(cls, instance) + + class object(with_metaclass(FixStr, object)): + pass + + __all__ = common + ['round', 'dict', 'apply', 'cmp', 'coerce', 'execfile', 'raw_input', 'unpacks', 'str', 'chr', + 'input', 'range', 'filter', 'map', 'zip', 'object'] + +if sys.version_info[0] == 2 and sys.version_info[1] < 7: + # OrderedDict + # Copyright (c) 2009 Raymond Hettinger + # + # Permission is hereby granted, free of charge, to any person + # obtaining a copy of this software and associated documentation files + # (the "Software"), to deal in the Software without restriction, + # including without limitation the rights to use, copy, modify, merge, + # publish, distribute, sublicense, and/or sell copies of the Software, + # and to permit persons to whom the Software is furnished to do so, + # subject to the following conditions: + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + # OTHER DEALINGS IN THE SOFTWARE. + + from UserDict import DictMixin + + class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + 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 popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other +else: + from collections import OrderedDict + + +if sys.version_info < (3, 2): + try: + from threading import Lock + except ImportError: + from dummy_threading import Lock + + from functools import wraps + + def lru_cache(maxsize=100): + """Least-recently-used cache decorator. + Taking from: https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py + with slight modifications. + If *maxsize* is set to None, the LRU features are disabled and the cache + can grow without bound. + Arguments to the cached function must be hashable. + View the cache statistics named tuple (hits, misses, maxsize, currsize) with + f.cache_info(). Clear the cache and statistics with f.cache_clear(). + Access the underlying function with f.__wrapped__. + See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used + + """ + def decorating_function(user_function, tuple=tuple, sorted=sorted, len=len, KeyError=KeyError): + hits, misses = [0], [0] + kwd_mark = (object(),) # separates positional and keyword args + lock = Lock() + + if maxsize is None: + CACHE = dict() + + @wraps(user_function) + def wrapper(*args, **kwds): + key = args + if kwds: + key += kwd_mark + tuple(sorted(kwds.items())) + try: + result = CACHE[key] + hits[0] += 1 + return result + except KeyError: + pass + result = user_function(*args, **kwds) + CACHE[key] = result + misses[0] += 1 + return result + else: + CACHE = OrderedDict() + + @wraps(user_function) + def wrapper(*args, **kwds): + key = args + if kwds: + key += kwd_mark + tuple(sorted(kwds.items())) + with lock: + cached = CACHE.get(key, None) + if cached: + del CACHE[key] + CACHE[key] = cached + hits[0] += 1 + return cached + result = user_function(*args, **kwds) + with lock: + CACHE[key] = result # record recent use of this key + misses[0] += 1 + while len(CACHE) > maxsize: + CACHE.popitem(last=False) + return result + + def cache_info(): + """Report CACHE statistics.""" + with lock: + return _CacheInfo(hits[0], misses[0], maxsize, len(CACHE)) + + def cache_clear(): + """Clear the CACHE and CACHE statistics.""" + with lock: + CACHE.clear() + hits[0] = misses[0] = 0 + + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + return wrapper + + return decorating_function + +else: + from functools import lru_cache diff --git a/isort/settings.py b/isort/settings.py index 8e44611a..a14fa115 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -27,8 +27,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera import os from collections import namedtuple -from pies.functools import lru_cache -from pies.overrides import * +from .pie_slice import * try: import configparser @@ -36,6 +35,7 @@ except ImportError: 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") WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED') WrapModes = namedtuple('WrapModes', WrapModes)(*range(len(WrapModes))) @@ -45,6 +45,7 @@ default = {'force_to_top': [], 'skip': ['__init__.py', ], 'line_length': 79, 'wrap_length': 0, + 'sections': DEFAULT_SECTIONS, 'known_future_library': ['__future__'], 'known_standard_library': ["abc", "anydbm", "argparse", "array", "asynchat", "asyncore", "atexit", "base64", "BaseHTTPServer", "bisect", "bz2", "calendar", "cgitb", "cmd", "codecs", @@ -53,7 +54,7 @@ default = {'force_to_top': [], "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", "itertools", "json", + "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", @@ -82,6 +83,7 @@ default = {'force_to_top': [], 'import_heading_firstparty': '', 'import_heading_localfolder': '', 'balanced_wrapping': False, + 'use_parentheses': False, 'order_by_type': True, 'atomic': False, 'lines_after_imports': -1, diff --git a/runtests.py b/runtests.py index ad902463..a024d3d5 100644 --- a/runtests.py +++ b/runtests.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#!/usr/bin/env python sources = """ eNrcvWmXG1l2INYzY1tjeGYk2ePtg32iQVGIKCKDSXZpyylUqVRFqqmuYvFwUVPOSoGRQGRmNJER @@ -1,2 +1,6 @@ [wheel] universal = 1 + +[flake8] +ignore = F401,F403,E502,E123,E127,E128,E303,E713,E111,E241,E302,E121,E261,W391 +max-line-length = 160 @@ -57,8 +57,6 @@ setup(name='isort', 'distutils.commands': ['isort = isort.main:ISortCommand'], }, packages=['isort'], - requires=['pies', 'natsort'], - install_requires=['pies>=2.6.2', 'natsort>=3.0.0'], cmdclass={'test': PyTest}, keywords='Refactor, Python, Python2, Python3, Refactoring, Imports, Sort, Clean', classifiers=['Development Status :: 6 - Mature', @@ -76,6 +74,7 @@ setup(name='isort', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Topic :: Software Development :: Libraries', 'Topic :: Utilities'], **PyTest.extra_kwargs) diff --git a/test_isort.py b/test_isort.py index b985d87f..9769263e 100644 --- a/test_isort.py +++ b/test_isort.py @@ -23,13 +23,12 @@ OTHER DEALINGS IN THE SOFTWARE. """ from __future__ import absolute_import, division, print_function, unicode_literals +from isort.pie_slice import * import codecs import os import shutil import tempfile -from pies.overrides import * - from isort.isort import SortImports from isort.settings import WrapModes @@ -419,6 +418,18 @@ def test_custom_indent(): " lib20, lib21, lib22\n") +def test_use_parentheses(): + test_input = ( + "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import \\" + " my_custom_function as my_special_function" + ) + test_output = SortImports( + file_contents=test_input, known_third_party=['django'], + line_length=79, use_parentheses=True, + ).output + assert '(' in test_output + + def test_skip(): """Ensure skipping a single import will work as expected.""" test_input = ("import myproject\n" @@ -671,6 +682,27 @@ def test_default_section(): "\n" "import django.settings\n") +def test_first_party_overrides_standard_section(): + """Test to ensure changing the default section works as expected.""" + test_input = ("import sys\n" + "import os\n" + "import profile.test\n") + test_output = SortImports(file_contents=test_input, known_first_party=['profile']).output + assert test_output == ("import os\n" + "import sys\n" + "\n" + "import profile.test\n") + +def test_thirdy_party_overrides_standard_section(): + """Test to ensure changing the default section works as expected.""" + test_input = ("import sys\n" + "import os\n" + "import profile.test\n") + test_output = SortImports(file_contents=test_input, known_third_party=['profile']).output + assert test_output == ("import os\n" + "import sys\n" + "\n" + "import profile.test\n") def test_force_single_line_imports(): """Test to ensure forcing imports to each have their own line works as expected.""" @@ -1178,7 +1210,6 @@ def test_place_comments(): "import os\n" "import sys\n") - def test_placement_control(): """Ensure that most specific placement control match wins""" test_input = ("import os\n" @@ -1193,6 +1224,7 @@ def test_placement_control(): known_standard_library=['p24.imports'], known_third_party=['bottle'], default_section="THIRDPARTY").output + assert test_output == ("import os\n" "import p24.imports._argparse as argparse\n" "import p24.imports._subprocess as subprocess\n" @@ -1204,6 +1236,54 @@ def test_placement_control(): "import p24.shared.media_wiki_syntax as syntax\n") +def test_custom_sections(): + """Ensure that most specific placement control match wins""" + test_input = ("import os\n" + "import sys\n" + "from django.conf import settings\n" + "from bottle import Bottle, redirect, response, run\n" + "import p24.imports._argparse as argparse\n" + "from django.db import models\n" + "import p24.imports._subprocess as subprocess\n" + "import pandas as pd\n" + "import p24.imports._VERSION as VERSION\n" + "import numpy as np\n" + "import p24.shared.media_wiki_syntax as syntax\n") + test_output = SortImports(file_contents=test_input, + known_first_party=['p24', 'p24.imports._VERSION'], + import_heading_stdlib='Standard Library', + import_heading_thirdparty='Third Party', + import_heading_firstparty='First Party', + import_heading_django='Django', + import_heading_pandas='Pandas', + known_standard_library=['p24.imports'], + known_third_party=['bottle'], + known_django=['django'], + known_pandas=['pandas', 'numpy'], + default_section="THIRDPARTY", + sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "PANDAS", "FIRSTPARTY", "LOCALFOLDER"]).output + assert test_output == ("# Standard Library\n" + "import os\n" + "import p24.imports._argparse as argparse\n" + "import p24.imports._subprocess as subprocess\n" + "import sys\n" + "\n" + "# Django\n" + "from django.conf import settings\n" + "from django.db import models\n" + "\n" + "# Third Party\n" + "from bottle import Bottle, redirect, response, run\n" + "\n" + "# Pandas\n" + "import numpy as np\n" + "import pandas as pd\n" + "\n" + "# First Party\n" + "import p24.imports._VERSION as VERSION\n" + "import p24.shared.media_wiki_syntax as syntax\n") + + def test_sticky_comments(): """Test to ensure it is possible to make comments 'stick' above imports""" test_input = ("import os\n" @@ -1336,13 +1416,27 @@ def test_fcntl(): assert SortImports(file_contents=test_input).output == test_input +def test_import_split_is_word_boundary_aware(): + """Test to ensure that isort splits words in a boundry aware mannor""" + test_input = ("from mycompany.model.size_value_array_import_func import (" + " get_size_value_array_import_func_jobs," + ")") + test_output = SortImports(file_contents=test_input, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=79).output + + assert test_output == ("from mycompany.model.size_value_array_import_func import \\\n" + " get_size_value_array_import_func_jobs\n") + + def test_other_file_encodings(): + """Test to ensure file encoding is respected""" try: tmp_dir = tempfile.mkdtemp() for encoding in ('latin1', 'utf8'): tmp_fname = os.path.join(tmp_dir, 'test_{}.py'.format(encoding)) with codecs.open(tmp_fname, mode='w', encoding=encoding) as f: - file_contents = "# coding: {}\n\ns = u'ã'\n".format(encoding) + file_contents = "# coding: {0}\n\ns = u'ã'\n".format(encoding) f.write(file_contents) assert SortImports(file_path=tmp_fname).output == file_contents finally: |
