1
0
mirror of https://github.com/wting/autojump synced 2024-10-27 20:34:07 +00:00

Merge branch 'add_match_tests'

This commit is contained in:
William Ting 2016-07-13 20:29:33 -07:00
commit b9e70a0eea
14 changed files with 312 additions and 155 deletions

View File

@ -1,11 +1,12 @@
- repo: git@github.com:pre-commit/pre-commit-hooks.git - repo: git@github.com:pre-commit/pre-commit-hooks.git
sha: 6f2b0a27e5b9047c6c067fb3d575ba323d572793 sha: 35548254adb636ce52b5574eb1904b8c795b673e
hooks: hooks:
- id: autopep8-wrapper - id: autopep8-wrapper
args: args:
- --in-place - --in-place
- --aggressive - --aggressive
- --aggressive - --aggressive
- --max-line-length=131
- id: check-added-large-files - id: check-added-large-files
- id: check-ast - id: check-ast
- id: check-case-conflict - id: check-case-conflict
@ -17,7 +18,7 @@
- id: flake8 - id: flake8
args: args:
- --max-complexity=10 - --max-complexity=10
- --max-line-length=130 - --max-line-length=131
- --ignore=E126,E128,E731 - --ignore=E126,E128,E731
- --exclude=bin/autojump_argparse.py - --exclude=bin/autojump_argparse.py
- id: requirements-txt-fixer - id: requirements-txt-fixer

View File

@ -1,7 +1,7 @@
VERSION = $(shell grep -oE "[0-9]+\.[0-9]+\.[0-9]+" bin/autojump) VERSION = $(shell grep -oE "[0-9]+\.[0-9]+\.[0-9]+" bin/autojump)
TAGNAME = release-v$(VERSION) TAGNAME = release-v$(VERSION)
.PHONY: docs install uninstall lint tar test .PHONY: clean docs install uninstall pre-commit lint tar test
install: install:
./install.py ./install.py
@ -39,9 +39,15 @@ tar:
sha1sum autojump_v$(VERSION).tar.gz sha1sum autojump_v$(VERSION).tar.gz
test: pre-commit test: pre-commit
@find . -type f -iname '*.py[co]' -delete @tox
tox
test-xfail: pre-commit
@tox -- --runxfail
test-fast: pre-commit test-fast: pre-commit
@tox -e py27
clean:
@find . -type f -iname '*.py[co]' -delete @find . -type f -iname '*.py[co]' -delete
tox -e py27 @find . -type d -iname '__pycache__' -delete
@rm -fr .tox

View File

@ -63,7 +63,7 @@ INSTALLATION
### REQUIREMENTS ### REQUIREMENTS
- Python v2.6+ - Python v2.6+ except v3.2
- Supported shells: - Supported shells:
- bash v4.0+ - bash v4.0+
- zsh - zsh
@ -148,7 +148,7 @@ maintained by William Ting. More contributors can be found in `AUTHORS`.
COPYRIGHT COPYRIGHT
--------- ---------
Copyright © 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL Copyright © 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL
version 3 or later <http://gnu.org/licenses/gpl.html>. This is free version 3 or later <http://gnu.org/licenses/gpl.html>. This is free
software: you are free to change and redistribute it. There is NO software: you are free to change and redistribute it. There is NO
WARRANTY, to the extent permitted by law. WARRANTY, to the extent permitted by law.

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright © 2008-2012 Joel Schaerer Copyright © 2008-2012 Joel Schaerer
Copyright © 2012-2014 William Ting Copyright © 2012-2016 William Ting
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -18,16 +18,13 @@
along with this program; if not, write to the Free Software Foundation, Inc., along with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
""" """
from __future__ import print_function from __future__ import print_function
from difflib import SequenceMatcher
from itertools import chain from itertools import chain
from math import sqrt from math import sqrt
from operator import attrgetter from operator import attrgetter
from operator import itemgetter from operator import itemgetter
import os import os
import re
import sys import sys
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
@ -38,13 +35,21 @@ else:
from itertools import ifilter from itertools import ifilter
from itertools import imap from itertools import imap
# Vendorized argparse for Python 2.6 support
from autojump_argparse import ArgumentParser from autojump_argparse import ArgumentParser
# autojump is not a standard python package but rather installed as a mixed
# shell + Python app with no outside dependencies (except Python). As a
# consequence we use relative imports and depend on file prefixes to prevent
# module conflicts.
from autojump_data import dictify from autojump_data import dictify
from autojump_data import entriefy from autojump_data import entriefy
from autojump_data import Entry from autojump_data import Entry
from autojump_data import load from autojump_data import load
from autojump_data import save from autojump_data import save
from autojump_match import match_anywhere
from autojump_match import match_consecutive
from autojump_match import match_fuzzy
from autojump_utils import first from autojump_utils import first
from autojump_utils import get_pwd from autojump_utils import get_pwd
from autojump_utils import get_tab_entry_info from autojump_utils import get_tab_entry_info
@ -60,7 +65,7 @@ from autojump_utils import sanitize
from autojump_utils import take from autojump_utils import take
from autojump_utils import unico from autojump_utils import unico
VERSION = '22.3.2' VERSION = '22.3.3'
FUZZY_MATCH_THRESHOLD = 0.6 FUZZY_MATCH_THRESHOLD = 0.6
TAB_ENTRIES_COUNT = 9 TAB_ENTRIES_COUNT = 9
TAB_SEPARATOR = '__' TAB_SEPARATOR = '__'
@ -221,111 +226,6 @@ def handle_tab_completion(needle, entries):
TAB_SEPARATOR) TAB_SEPARATOR)
def match_anywhere(needles, haystack, ignore_case=False):
"""
Matches needles anywhere in the path as long as they're in the same (but
not necessary consecutive) order.
For example:
needles = ['foo', 'baz']
regex needle = r'.*foo.*baz.*'
haystack = [
(path="/foo/bar/baz", weight=10),
(path="/baz/foo/bar", weight=10),
(path="/foo/baz", weight=10)]
result = [
(path="/moo/foo/baz", weight=10),
(path="/foo/baz", weight=10)]
"""
regex_needle = '.*' + '.*'.join(needles).replace('\\', '\\\\') + '.*'
regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE
found = lambda haystack: re.search(
regex_needle,
haystack.path,
flags=regex_flags)
return ifilter(found, haystack)
def match_consecutive(needles, haystack, ignore_case=False):
"""
Matches consecutive needles at the end of a path.
For example:
needles = ['foo', 'baz']
haystack = [
(path="/foo/bar/baz", weight=10),
(path="/foo/baz/moo", weight=10),
(path="/moo/foo/baz", weight=10),
(path="/foo/baz", weight=10)]
regex_needle = re.compile(r'''
foo # needle #1
[^/]* # all characters except os.sep zero or more times
/ # os.sep
[^/]* # all characters except os.sep zero or more times
baz # needle #2
[^/]* # all characters except os.sep zero or more times
$ # end of string
''')
result = [
(path="/moo/foo/baz", weight=10),
(path="/foo/baz", weight=10)]
"""
# The normal \\ separator needs to be escaped again for use in regex.
sep = '\\\\' if is_windows() else os.sep
regex_no_sep = '[^' + sep + ']*'
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 # noqa
regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE
found = lambda entry: re.search(
regex_needle,
entry.path,
flags=regex_flags)
return ifilter(found, haystack)
def match_fuzzy(needles, haystack, ignore_case=False):
"""
Performs an approximate match with the last needle against the end of
every path past an acceptable threshold (FUZZY_MATCH_THRESHOLD).
For example:
needles = ['foo', 'bar']
haystack = [
(path="/foo/bar/baz", weight=11),
(path="/foo/baz/moo", weight=10),
(path="/moo/foo/baz", weight=10),
(path="/foo/baz", weight=10),
(path="/foo/bar", weight=10)]
result = [
(path="/foo/bar/baz", weight=11),
(path="/moo/foo/baz", weight=10),
(path="/foo/baz", weight=10),
(path="/foo/bar", weight=10)]
This is a weak heuristic and used as a last resort to find matches.
"""
end_dir = lambda path: last(os.path.split(path))
if ignore_case:
needle = last(needles).lower()
match_percent = lambda entry: SequenceMatcher(
a=needle,
b=end_dir(entry.path.lower())).ratio()
else:
needle = last(needles)
match_percent = lambda entry: SequenceMatcher(
a=needle,
b=end_dir(entry.path)).ratio()
meets_threshold = lambda entry: match_percent(entry) >= \
FUZZY_MATCH_THRESHOLD
return ifilter(meets_threshold, haystack)
def purge_missing_paths(entries): def purge_missing_paths(entries):
"""Remove non-existent paths from a list of entries.""" """Remove non-existent paths from a list of entries."""
exists = lambda entry: os.path.exists(entry.path) exists = lambda entry: os.path.exists(entry.path)

127
bin/autojump_match.py Normal file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
from difflib import SequenceMatcher
from autojump_utils import is_python3
from autojump_utils import last
if is_python3(): # pragma: no cover
ifilter = filter
imap = map
os.getcwdu = os.getcwd
else:
from itertools import ifilter
from itertools import imap
def match_anywhere(needles, haystack, ignore_case=False):
"""
Matches needles anywhere in the path as long as they're in the same (but
not necessary consecutive) order.
For example:
needles = ['foo', 'baz']
regex needle = r'.*foo.*baz.*'
haystack = [
(path='/foo/bar/baz', weight=10),
(path='/baz/foo/bar', weight=10),
(path='/foo/baz', weight=10),
]
result = [
(path='/moo/foo/baz', weight=10),
(path='/foo/baz', weight=10),
]
"""
regex_needle = '.*' + '.*'.join(imap(re.escape, needles)) + '.*'
regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE
found = lambda haystack: re.search(
regex_needle,
haystack.path,
flags=regex_flags,
)
return ifilter(found, haystack)
def match_consecutive(needles, haystack, ignore_case=False):
"""
Matches consecutive needles at the end of a path.
For example:
needles = ['foo', 'baz']
haystack = [
(path='/foo/bar/baz', weight=10),
(path='/foo/baz/moo', weight=10),
(path='/moo/foo/baz', weight=10),
(path='/foo/baz', weight=10),
]
# We can't actually use re.compile because of re.UNICODE
regex_needle = re.compile(r'''
foo # needle #1
[^/]* # all characters except os.sep zero or more times
/ # os.sep
[^/]* # all characters except os.sep zero or more times
baz # needle #2
[^/]* # all characters except os.sep zero or more times
$ # end of string
''')
result = [
(path='/moo/foo/baz', weight=10),
(path='/foo/baz', weight=10),
]
"""
regex_no_sep = '[^' + os.sep + ']*'
regex_no_sep_end = regex_no_sep + '$'
regex_one_sep = regex_no_sep + os.sep + regex_no_sep
regex_needle = regex_one_sep.join(imap(re.escape, needles)) + regex_no_sep_end
regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE
found = lambda entry: re.search(
regex_needle,
entry.path,
flags=regex_flags,
)
return ifilter(found, haystack)
def match_fuzzy(needles, haystack, ignore_case=False, threshold=0.6):
"""
Performs an approximate match with the last needle against the end of
every path past an acceptable threshold.
For example:
needles = ['foo', 'bar']
haystack = [
(path='/foo/bar/baz', weight=11),
(path='/foo/baz/moo', weight=10),
(path='/moo/foo/baz', weight=10),
(path='/foo/baz', weight=10),
(path='/foo/bar', weight=10),
]
result = [
(path='/foo/bar/baz', weight=11),
(path='/moo/foo/baz', weight=10),
(path='/foo/baz', weight=10),
(path='/foo/bar', weight=10),
]
This is a weak heuristic and used as a last resort to find matches.
"""
end_dir = lambda path: last(os.path.split(path))
if ignore_case:
needle = last(needles).lower()
match_percent = lambda entry: SequenceMatcher(
a=needle,
b=end_dir(entry.path.lower())).ratio()
else:
needle = last(needles)
match_percent = lambda entry: SequenceMatcher(
a=needle,
b=end_dir(entry.path)).ratio()
meets_threshold = lambda entry: match_percent(entry) >= threshold
return ifilter(meets_threshold, haystack)

View File

@ -127,7 +127,7 @@ maintained by William Ting.
More contributors can be found in \f[C]AUTHORS\f[]. More contributors can be found in \f[C]AUTHORS\f[].
.SS COPYRIGHT .SS COPYRIGHT
.PP .PP
Copyright © 2012 Free Software Foundation, Inc. Copyright © 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later License GPLv3+: GNU GPL version 3 or later
<http://gnu.org/licenses/gpl.html>. <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it. This is free software: you are free to change and redistribute it.

View File

@ -28,7 +28,7 @@ William Ting. More contributors can be found in `AUTHORS`.
COPYRIGHT COPYRIGHT
--------- ---------
Copyright © 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version Copyright © 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version
3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are
free to change and redistribute it. There is NO WARRANTY, to the extent free to change and redistribute it. There is NO WARRANTY, to the extent
permitted by law. permitted by law.

View File

@ -2,7 +2,7 @@
### REQUIREMENTS ### REQUIREMENTS
- Python v2.6+ - Python v2.6+ except v3.2
- Supported shells: - Supported shells:
- bash v4.0+ - bash v4.0+
- zsh - zsh
@ -41,7 +41,7 @@ MacPorts also available:
## Windows ## Windows
Windows support is enabled by [clink](https://code.google.com/p/clink/) which Windows support is enabled by [clink](https://mridgers.github.io/clink/) which
should be installed prior to installing autojump. should be installed prior to installing autojump.
### MANUAL ### MANUAL

View File

@ -1 +0,0 @@
tox

View File

@ -0,0 +1,131 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import pytest
sys.path.append(os.path.join(os.getcwd(), 'bin')) # noqa
from autojump_data import Entry
from autojump_match import match_anywhere
from autojump_match import match_consecutive
class TestMatchAnywhere(object):
entry1 = Entry('/foo/bar/baz', 10)
entry2 = Entry('/baz/foo/bar', 10)
entry3 = Entry('/foo/baz', 10)
entry4 = Entry('/中/zhong/国/guo', 10)
entry5 = Entry('/is\'t/this/a/b*tchin/edge/case?', 10)
win_entry1 = Entry('C:\\foo\\bar\\baz', 10)
win_entry2 = Entry('D:\Program Files (x86)\GIMP', 10)
win_entry3 = Entry('C:\Windows\System32', 10)
@pytest.fixture
def haystack(self):
return [
self.entry1,
self.entry2,
self.entry3,
self.entry4,
self.entry5,
]
@pytest.fixture
def windows_haystack(self):
return [self.win_entry1, self.win_entry2, self.win_entry3]
def test_single_needle(self, haystack):
assert list(match_anywhere(['bar'], haystack)) == [self.entry1, self.entry2]
def test_consecutive(self, haystack):
assert list(match_anywhere(['foo', 'bar'], haystack)) \
== [self.entry1, self.entry2]
assert list(match_anywhere(['bar', 'foo'], haystack)) == []
def test_skip(self, haystack):
assert list(match_anywhere(['baz', 'bar'], haystack)) == [self.entry2]
assert list(match_anywhere(['', ''], haystack)) == [self.entry4]
def test_ignore_case(self, haystack):
assert list(match_anywhere(['bAz', 'bAR'], haystack, ignore_case=True)) \
== [self.entry2]
def test_backslashes_for_windows_paths(self, windows_haystack):
# https://github.com/wting/autojump/issues/281
assert list(match_anywhere(['foo', 'baz'], windows_haystack)) \
== [self.win_entry1]
assert list(match_anywhere(['program', 'gimp'], windows_haystack, True)) \
== [self.win_entry2]
assert list(match_anywhere(['win', '32'], windows_haystack, True)) \
== [self.win_entry3]
def test_wildcard_in_needle(self, haystack):
# https://github.com/wting/autojump/issues/402
assert list(match_anywhere(['*', 'this'], haystack)) == []
assert list(match_anywhere(['this', '*'], haystack)) == [self.entry5]
class TestMatchConsecutive(object):
entry1 = Entry('/foo/bar/baz', 10)
entry2 = Entry('/baz/foo/bar', 10)
entry3 = Entry('/foo/baz', 10)
entry4 = Entry('/中/zhong/国/guo', 10)
entry5 = Entry('/日/本', 10)
entry6 = Entry('/is\'t/this/a/b*tchin/edge/case?', 10)
win_entry1 = Entry('C:\Foo\Bar\Baz', 10)
win_entry2 = Entry('D:\Program Files (x86)\GIMP', 10)
win_entry3 = Entry('C:\Windows\System32', 10)
@pytest.fixture
def haystack(self):
return [
self.entry1,
self.entry2,
self.entry3,
self.entry4,
self.entry5,
]
@pytest.fixture
def windows_haystack(self):
return [self.win_entry1, self.win_entry2, self.win_entry3]
def test_single_needle(self, haystack):
assert list(match_consecutive(['baz'], haystack)) == [self.entry1, self.entry3]
assert list(match_consecutive([''], haystack)) == [self.entry5]
def test_consecutive(self, haystack):
assert list(match_consecutive(['bar', 'baz'], haystack)) == [self.entry1]
assert list(match_consecutive(['foo', 'bar'], haystack)) == [self.entry2]
assert list(match_consecutive(['', 'guo'], haystack)) == [self.entry4]
assert list(match_consecutive(['bar', 'foo'], haystack)) == []
def test_ignore_case(self, haystack):
assert list(match_consecutive(['FoO', 'bAR'], haystack, ignore_case=True)) \
== [self.entry2]
def test_windows_ignore_case(self, windows_haystack):
assert list(match_consecutive(['gimp'], windows_haystack, True)) == [self.win_entry2]
@pytest.mark.xfail(reason='https://github.com/wting/autojump/issues/418')
def test_backslashes_for_windows_paths(self, windows_haystack):
assert list(match_consecutive(['program', 'gimp'], windows_haystack, True)) \
== [self.win_entry2]
@pytest.mark.xfail(reason='https://github.com/wting/autojump/issues/418')
def test_foo_bar_baz(self, windows_haystack):
assert list(match_consecutive(['bar', 'baz'], windows_haystack, ignore_case=True)) \
== [self.win_entry1]
@pytest.mark.xfail(reason='https://github.com/wting/autojump/issues/402')
def test_thing(self, windows_haystack):
assert list(match_consecutive(['win', '32'], windows_haystack, True)) \
== [self.win_entry3]
@pytest.mark.xfail(reason='https://github.com/wting/autojump/issues/402')
def test_wildcard_in_needle(self, haystack):
assert list(match_consecutive(['*', 'this'], haystack)) == []
assert list(match_consecutive(['*', 'edge', 'case'], haystack)) == [self.entry6]

View File

@ -6,20 +6,20 @@ import sys
import mock import mock
import pytest import pytest
sys.path.append(os.path.join(os.getcwd(), 'bin')) sys.path.append(os.path.join(os.getcwd(), 'bin')) # noqa
import autojump_utils # noqa import autojump_utils
from autojump_utils import encode_local # noqa from autojump_utils import encode_local
from autojump_utils import first # noqa from autojump_utils import first
from autojump_utils import get_tab_entry_info # noqa from autojump_utils import get_tab_entry_info
from autojump_utils import has_uppercase # noqa from autojump_utils import has_uppercase
from autojump_utils import in_bash # noqa from autojump_utils import in_bash
from autojump_utils import is_python3 # noqa from autojump_utils import is_python3
from autojump_utils import last # noqa from autojump_utils import last
from autojump_utils import sanitize # noqa from autojump_utils import sanitize
from autojump_utils import second # noqa from autojump_utils import second
from autojump_utils import surround_quotes # noqa from autojump_utils import surround_quotes
from autojump_utils import take # noqa from autojump_utils import take
from autojump_utils import unico # noqa from autojump_utils import unico
if is_python3(): if is_python3():

23
tox.ini
View File

@ -2,13 +2,13 @@
envlist = envlist =
py26, py26,
py27, py27,
py32,
py33, py33,
py34 py34,
py35
# ignore missing setup.py # ignore missing setup.py
skipsdist = True skipsdist = True
[testenv:py] [testenv]
setenv = setenv =
PYTHONDONTWRITEBYTECODE = 1 PYTHONDONTWRITEBYTECODE = 1
deps = deps =
@ -16,24 +16,17 @@ deps =
coverage coverage
ipdb ipdb
ipython ipython
pytest pytest >= 2.9
commands = commands =
coverage run --source=bin/ -m pytest -vv -rxs --tb native -s --durations 10 --strict {posargs:tests} coverage run --source=bin/ --omit=bin/autojump_argparse.py -m \
py.test -vv -rxs --tb native -s --strict {posargs:tests}
coverage report -m coverage report -m
[testenv:flake8]
deps =
flake8
pyflakes
pep8
mccabe
commands =
flake8 .
[testenv:pre-commit] [testenv:pre-commit]
deps = deps =
pre-commit pre-commit>=0.7.0
command = commands =
pre-commit {posargs} pre-commit {posargs}
[pytest] [pytest]