diff --git a/.gitignore b/.gitignore index f1713db..adbb218 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *~ *.tar.gz *.patch +.tox tags diff --git a/.travis.yml b/.travis.yml index 8940265..f04fa15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,17 @@ language: python -python: - - 2.6 - - 2.7 +python: 2.7 + +env: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py32 + - TOX_ENV=py33 + - TOX_ENV=py34 + - TOX_ENV=flake8 install: - - pip install --use-mirrors testify + - pip install tox script: - - make test + - tox -e $TOX_ENV diff --git a/Makefile b/Makefile index 82a111b..a357f26 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ docs: pandoc -s -w markdown docs/header.md docs/install.md docs/body.md -o README.md lint: - @flake8 ./ --config=setup.cfg + @flake8 ./ --config=tox.ini release: docs # Check for tag existence @@ -38,6 +38,6 @@ tar: git archive --format=tar --prefix autojump_v$(VERSION)/ $(TAGNAME) | gzip > autojump_v$(VERSION).tar.gz sha1sum autojump_v$(VERSION).tar.gz -test: +test: lint @find . -type f -iname "*.pyc" -delete - testify -v tests -x disabled + tox diff --git a/README.md b/README.md index cd5643c..2e31378 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ can be used with `autojump` can be used with `j` and vice versa. jo music -- Opening a file manager to a child directory is also supported: + Opening a file manager to a child directory is also supported: jco images @@ -54,7 +54,7 @@ can be used with `autojump` can be used with `j` and vice versa. a different entry. In the above example, `j w in` would then change directory to /home/user/work/inbox. -For more options refer to the help: +For more options refer to help: autojump --help @@ -64,7 +64,12 @@ INSTALLATION ### REQUIREMENTS - Python v2.6+ -- Bash v4.0+, zsh, fish, or clink (Windows) +- Supported shells: + - bash v4.0+ + - zsh + - fish + - tcsh (experimental) + - clink on Windows (experimental) ### AUTOMATIC @@ -98,7 +103,7 @@ MacPorts also available: Windows ------- -Windows support is enabled by [clink](https://code.google.com/p/clink/), +Windows support is enabled by [clink](https://code.google.com/p/clink/) which should be installed prior to installing autojump. ### MANUAL @@ -107,7 +112,7 @@ Grab a copy of autojump: git clone git://github.com/joelthelion/autojump.git -Run the installation script and follow the on screen instructions. +Run the installation script and follow on screen instructions. cd autojump ./install.py or ./uninstall.py diff --git a/bin/autojump b/bin/autojump index 0ad7ab9..a44e86b 100755 --- a/bin/autojump +++ b/bin/autojump @@ -193,9 +193,6 @@ def find_matches(entries, needles, check_entries=True): def handle_tab_completion(needle, entries): - if not needle: - sys.exit(0) - tab_needle, tab_index, tab_path = get_tab_entry_info(needle, TAB_SEPARATOR) if tab_path: @@ -285,7 +282,7 @@ def match_consecutive(needles, haystack, ignore_case=False): regex_no_sep_end = regex_no_sep + '$' regex_one_sep = regex_no_sep + sep + regex_no_sep # can't use compiled regex because of flags - regex_needle = regex_one_sep.join(needles).replace('\\', '\\\\') + regex_no_sep_end + regex_needle = regex_one_sep.join(needles).replace('\\', '\\\\') + regex_no_sep_end # noqa regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE found = lambda entry: re.search( regex_needle, @@ -364,7 +361,7 @@ def main(args): # noqa save(config, first(add_path(load(config), args.add))) elif args.complete: handle_tab_completion( - needle=first(sanitize(args.directory)), + needle=first(chain(sanitize(args.directory), [''])), entries=entriefy(load(config))) elif args.decrease: data, entry = decrease_path(load(config), get_pwd(), args.decrease) @@ -382,8 +379,12 @@ def main(args): # noqa elif args.stat: print_stats(load(config), config['data_path']) elif not args.directory: - # always return a path to calling shell functions - print_local('.') + # Return best match. + entries = entriefy(load(config)) + print_local(first(chain( + imap(attrgetter('path'), find_matches(entries, [''])), + # always return a path to calling shell functions + ['.']))) else: entries = entriefy(load(config)) needles = sanitize(args.directory) diff --git a/bin/autojump.tcsh b/bin/autojump.tcsh new file mode 100644 index 0000000..a520be0 --- /dev/null +++ b/bin/autojump.tcsh @@ -0,0 +1,12 @@ +# set user installation paths +if (-d ~/.autojump/bin) then + set path = (~/.autojump/bin path) +endif + +# prepend autojump to cwdcmd (run after every change of working directory) +if (`alias cwdcmd` !~ *autojump*) then + alias cwdcmd 'autojump --add $cwd >/dev/null;' `alias cwdcmd` +endif + +#default autojump command +alias j 'cd `autojump -- \!:1`' diff --git a/bin/autojump_data.py b/bin/autojump_data.py index f367a59..da86c90 100644 --- a/bin/autojump_data.py +++ b/bin/autojump_data.py @@ -137,5 +137,5 @@ def save(config, data): # create backup file if it doesn't exist or is older than BACKUP_THRESHOLD if not os.path.exists(config['backup_path']) or \ - (time() - os.path.getmtime(config['backup_path']) > BACKUP_THRESHOLD): #noqa + (time() - os.path.getmtime(config['backup_path']) > BACKUP_THRESHOLD): # noqa shutil.copy(config['data_path'], config['backup_path']) diff --git a/bin/autojump_utils.py b/bin/autojump_utils.py index 48f2185..949b4fd 100644 --- a/bin/autojump_utils.py +++ b/bin/autojump_utils.py @@ -30,6 +30,8 @@ def create_dir(path): def encode_local(string): """Converts string into user's preferred encoding.""" + if is_python3(): + return string return string.encode(sys.getfilesystemencoding() or 'utf-8') @@ -170,8 +172,12 @@ def sanitize(directories): def second(xs): it = iter(xs) try: - it.next() - return it.next() + if is_python2(): + it.next() + return it.next() + elif is_python3(): + next(it) + return next(it) except StopIteration: return None diff --git a/dev-requirements.txt b/dev-requirements.txt index a8430c6..2c84e24 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,4 @@ -flake8>=2.0.0 -testify +flake8 +mock +pytest +tox diff --git a/docs/autojump.1 b/docs/autojump.1 index d3c8d25..188194f 100644 --- a/docs/autojump.1 +++ b/docs/autojump.1 @@ -25,7 +25,7 @@ j\ foo .fi .RE .IP \[bu] 2 -Jump To A Child Directory +Jump To A Child Directory: .RS 2 .PP Sometimes it\[aq]s convenient to jump to a child directory @@ -52,7 +52,7 @@ jo\ music \f[] .fi .PP -Opening a file manager to a child directory is also supported. +Opening a file manager to a child directory is also supported: .IP .nf \f[C] diff --git a/docs/header.md b/docs/header.md index 09fc42c..379af3b 100644 --- a/docs/header.md +++ b/docs/header.md @@ -21,7 +21,7 @@ be used with `autojump` can be used with `j` and vice versa. j foo -- Jump To A Child Directory +- Jump To A Child Directory: Sometimes it's convenient to jump to a child directory (sub-directory of current directory) rather than typing out the full name. @@ -35,7 +35,7 @@ be used with `autojump` can be used with `j` and vice versa. jo music - Opening a file manager to a child directory is also supported. + Opening a file manager to a child directory is also supported: jco images diff --git a/docs/install.md b/docs/install.md index a617b3c..3ad6a98 100644 --- a/docs/install.md +++ b/docs/install.md @@ -3,7 +3,12 @@ ### REQUIREMENTS - Python v2.6+ -- Bash v4.0+, zsh, fish, or clink (Windows) +- Supported shells: + - bash v4.0+ + - zsh + - fish + - tcsh (experimental) + - clink (Windows, experimental) ### AUTOMATIC @@ -47,4 +52,4 @@ Grab a copy of autojump: Run the installation script and follow on screen instructions. cd autojump - ./install.py or ./uinstall.py + ./install.py or ./uninstall.py diff --git a/install.py b/install.py index 0e03d59..9a09193 100755 --- a/install.py +++ b/install.py @@ -10,7 +10,7 @@ import sys sys.path.append('bin') from autojump_argparse import ArgumentParser -SUPPORTED_SHELLS = ('bash', 'zsh', 'fish') +SUPPORTED_SHELLS = ('bash', 'zsh', 'fish', 'tcsh') def cp(src, dest, dryrun=False): @@ -157,6 +157,7 @@ def print_post_installation_message(share_dir, bin_dir): print('\n\t' + source_msg) if get_shell() == 'zsh': print("\n\tautoload -U compinit && compinit -u") + print("\nPlease restart terminal(s) before running autojump.\n") diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index f917c4a..0000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -filename = *.py,autojump -ignore = E126,E128 -max-line-length = 79 -max-complexity = 10 diff --git a/tests/autojump_utils_test.py b/tests/autojump_utils_test.py index 815347a..147feb5 100644 --- a/tests/autojump_utils_test.py +++ b/tests/autojump_utils_test.py @@ -1,39 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from random import randrange -from shutil import rmtree -from tempfile import gettempdir -from tempfile import mkdtemp import os import sys import mock -from testify import TestCase -from testify import assert_equal -from testify import assert_false -from testify import assert_raises -from testify import assert_true -from testify import class_setup -from testify import class_teardown -from testify import run -from testify import setup -from testify import suite -from testify import teardown - -if sys.version_info[0] == 3: - os.getcwdu = os.getcwd -sys.path.append(os.path.join(os.getcwd(), 'bin')) +import pytest +sys.path.append(os.path.join(os.getcwd(), 'bin')) import autojump_utils -from autojump_utils import create_dir from autojump_utils import encode_local from autojump_utils import first -from autojump_utils import get_pwd from autojump_utils import get_tab_entry_info from autojump_utils import has_uppercase from autojump_utils import in_bash +from autojump_utils import is_python3 from autojump_utils import last -from autojump_utils import move_file from autojump_utils import sanitize from autojump_utils import second from autojump_utils import surround_quotes @@ -41,170 +22,124 @@ from autojump_utils import take from autojump_utils import unico -class StringUnitTests(TestCase): - @mock.patch.object(sys, 'getfilesystemencoding', return_value='ascii') - def test_encode_local_ascii(self, _): - assert_equal(encode_local(u'foo'), b'foo') - - @suite('disabled', reason='#246') - def test_encode_local_ascii_fails(self): - with assert_raises(UnicodeDecodeError): - with mock.patch.object( - sys, - 'getfilesystemencoding', - return_value='ascii'): - encode_local(u'日本語') - - @mock.patch.object(sys, 'getfilesystemencoding', return_value=None) - def test_encode_local_empty(self, _): - assert_equal(encode_local(b'foo'), u'foo') - - @mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8') - def test_encode_local_unicode(self, _): - assert_equal(encode_local(b'foo'), u'foo') - assert_equal(encode_local(u'foo'), u'foo') - - def test_has_uppercase(self): - assert_true(has_uppercase('Foo')) - assert_true(has_uppercase('foO')) - assert_false(has_uppercase('foo')) - assert_false(has_uppercase('')) - - @mock.patch.object(autojump_utils, 'in_bash', return_value=True) - def test_surround_quotes_in_bash(self, _): - assert_equal(surround_quotes('foo'), '"foo"') - - @mock.patch.object(autojump_utils, 'in_bash', return_value=False) - def test_dont_surround_quotes_not_in_bash(self, _): - assert_equal(surround_quotes('foo'), 'foo') - - def test_sanitize(self): - assert_equal(sanitize([]), []) - assert_equal(sanitize([r'/foo/bar/', r'/']), [u'/foo/bar', u'/']) - - def test_unico(self): - assert_equal(unico(b'blah'), u'blah') - assert_equal(unico(b'日本語'), u'日本語') - assert_equal(unico(u'でもおれは中国人だ。'), u'でもおれは中国人だ。') - - -class IterationUnitTests(TestCase): - def test_first(self): - assert_equal(first(xrange(5)), 0) - assert_equal(first([]), None) - - def test_second(self): - assert_equal(second(xrange(5)), 1) - assert_equal(second([]), None) - - def test_last(self): - assert_equal(last(xrange(4)), 3) - assert_equal(last([]), None) - - def test_take(self): - assert_equal(list(take(1, xrange(3))), [0]) - assert_equal(list(take(2, xrange(3))), [0, 1]) - assert_equal(list(take(4, xrange(3))), [0, 1, 2]) - assert_equal(list(take(10, [])), []) - - -class EnvironmentalVariableIntegrationTests(TestCase): - @setup - def create_tmp_dir(self): - self.tmp_dir = mkdtemp() - - @teardown - def delete_tmp_dir(self): - try: - rmtree(self.tmp_dir) - except OSError: - pass - - def test_in_bash(self): - os.environ['SHELL'] = '/bin/bash' - assert_true(in_bash()) +if is_python3(): + os.getcwdu = os.getcwd + xrange = range + + +def u(string): + """ + This is a unicode() wrapper since u'string' is a Python3 compiler error. + """ + if is_python3(): + return string + return unicode(string, encoding='utf-8', errors='strict') + + +# strings +@pytest.mark.skipif(is_python3(), reason="Unicode sucks.") +@mock.patch.object(sys, 'getfilesystemencoding', return_value='ascii') +def test_encode_local_ascii(_): + assert encode_local(u('foo')) == b'foo' + + +@pytest.mark.skipif(is_python3(), reason="Unicode sucks.") +@pytest.mark.xfail(reason="disabled due to pytest bug: https://bitbucket.org/hpk42/pytest/issue/534/pytest-fails-to-catch-unicodedecodeerrors") # noqa +@mock.patch.object(sys, 'getfilesystemencoding', return_value='ascii') +def test_encode_local_ascii_fails(_): + with pytest.raises(UnicodeDecodeError): + encode_local(u('日本語')) + + +@pytest.mark.skipif(is_python3(), reason="Unicode sucks.") +@mock.patch.object(sys, 'getfilesystemencoding', return_value=None) +def test_encode_local_empty(_): + assert encode_local(b'foo') == u('foo') + + +@pytest.mark.skipif(is_python3(), reason="Unicode sucks.") +@mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8') +def test_encode_local_unicode(_): + assert encode_local(b'foo') == u('foo') + assert encode_local(u('foo')) == u('foo') + + +def test_has_uppercase(): + assert has_uppercase('Foo') + assert has_uppercase('foO') + assert not has_uppercase('foo') + assert not has_uppercase('') + + +@mock.patch.object(autojump_utils, 'in_bash', return_value=True) +def test_surround_quotes_in_bash(_): + assert surround_quotes('foo') == '"foo"' + + +@mock.patch.object(autojump_utils, 'in_bash', return_value=False) +def test_dont_surround_quotes_not_in_bash(_): + assert surround_quotes('foo') == 'foo' + + +def test_sanitize(): + assert sanitize([]) == [] + assert sanitize([r'/foo/bar/', r'/']) == [u('/foo/bar'), u('/')] + + +@pytest.mark.skipif(is_python3(), reason="Unicode sucks.") +def test_unico(): + assert unico(str('blah')) == u('blah') + assert unico(str('日本語')) == u('日本語') + assert unico(u('でもおれは中国人だ。')) == u('でもおれは中国人だ。') + + +# iteration +def test_first(): + assert first(xrange(5)) == 0 + assert first([]) is None + + +def test_second(): + assert second(xrange(5)) == 1 + assert second([]) is None + + +def test_last(): + assert last(xrange(4)) == 3 + assert last([]) is None + + +def test_take(): + assert list(take(1, xrange(3))) == [0] + assert list(take(2, xrange(3))) == [0, 1] + assert list(take(4, xrange(3))) == [0, 1, 2] + assert list(take(10, [])) == [] + + +# environment variables +def test_in_bash(): + for path in ['/bin/bash', '/usr/bin/bash']: + os.environ['SHELL'] = path + assert in_bash() + + for path in ['/bin/zsh', '/usr/bin/zsh']: os.environ['SHELL'] = '/usr/bin/zsh' - assert_false(in_bash()) - - def test_good_get_pwd(self): - os.chdir(self.tmp_dir) - assert_equal(get_pwd(), self.tmp_dir) - - def test_bad_get_pwd(self): - os.chdir(self.tmp_dir) - rmtree(self.tmp_dir) - assert_raises(OSError, get_pwd) - - -class FileSystemIntegrationTests(TestCase): - @class_setup - def init(self): - self.tmp_dir = os.path.join(gettempdir(), 'autojump') - os.makedirs(self.tmp_dir) - - @class_teardown - def cleanup(self): - try: - rmtree(self.tmp_dir) - except OSError: - pass - - def get_random_path(self): - path = gettempdir() - - while os.path.exists(path): - random_string = '%30x' % randrange(16 ** 30) - path = os.path.join(self.tmp_dir, random_string) - - return path - - def get_random_file(self): - path = self.get_random_path() - with open(path, 'w+') as f: - f.write('filler\n') - - return path - - def test_create_dir(self): - path = self.get_random_path() - create_dir(path) - assert_true(os.path.exists(path)) - - # should not raise OSError if directory already exists - create_dir(path) - assert_true(os.path.exists(path)) - - def test_move_file(self): - src = self.get_random_file() - dst = self.get_random_path() - assert_true(os.path.exists(src)) - assert_false(os.path.exists(dst)) - move_file(src, dst) - assert_false(os.path.exists(src)) - assert_true(os.path.exists(dst)) - - -class HelperFunctionsUnitTests(TestCase): - def test_get_needle(self): - assert_equal( - get_tab_entry_info('foo__', '__'), - ('foo', None, None)) - - def test_get_index(self): - assert_equal( - get_tab_entry_info('foo__2', '__'), - ('foo', 2, None)) - - def test_get_path(self): - assert_equal( - get_tab_entry_info('foo__3__/foo/bar', '__'), - ('foo', 3, '/foo/bar')) - - def test_get_none(self): - assert_equal( - get_tab_entry_info('gibberish content', '__'), - (None, None, None)) - - -if __name__ == "__main__": - run() + assert not in_bash() + + +# helper functions +def test_get_needle(): + assert get_tab_entry_info('foo__', '__') == ('foo', None, None) + + +def test_get_index(): + assert get_tab_entry_info('foo__2', '__') == ('foo', 2, None) + + +def test_get_path(): + assert get_tab_entry_info('foo__3__/foo/bar', '__') \ + == ('foo', 3, '/foo/bar') + + +def test_get_none(): + assert get_tab_entry_info('gibberish content', '__') == (None, None, None) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..971d67a --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +envlist = + py26, + py27, + py32, + py33, + py34 +# ignore missing setup.py +skipsdist = True + +[testenv] +deps = -rdev-requirements.txt +commands = py.test -rsxX -q + +[testenv:flake8] +deps = flake8 +commands = flake8 . + +[flake8] +filename = + *.py, + autojump +ignore = + E126, + E128 +max-line-length = 79 +max-complexity = 10 +show-pep8 = True + +[pytest] +addopts = -rsxX -q +norecursedirs = .git .tox docs diff --git a/uninstall.py b/uninstall.py index 3e41fc6..ab76317 100755 --- a/uninstall.py +++ b/uninstall.py @@ -78,6 +78,7 @@ def remove_custom_installation(args, dryrun=False): rm(os.path.join(etc_dir, 'autojump.sh'), dryrun) rm(os.path.join(etc_dir, 'autojump.bash'), dryrun) rm(os.path.join(etc_dir, 'autojump.fish'), dryrun) + rm(os.path.join(etc_dir, 'autojump.tcsh'), dryrun) rm(os.path.join(etc_dir, 'autojump.zsh'), dryrun) rm(os.path.join(zshshare_dir, '_j'), dryrun) rmdir(icon_dir, dryrun) @@ -114,6 +115,7 @@ def remove_system_installation(dryrun=False): rm(os.path.join(etc_dir, 'autojump.sh'), dryrun) rm(os.path.join(etc_dir, 'autojump.bash'), dryrun) rm(os.path.join(etc_dir, 'autojump.fish'), dryrun) + rm(os.path.join(etc_dir, 'autojump.tcsh'), dryrun) rm(os.path.join(etc_dir, 'autojump.zsh'), dryrun) rm(os.path.join(zshshare_dir, '_j'), dryrun) rmdir(icon_dir, dryrun)