From ba601644d697ef5f54198ae4d041eabb8792ad8d Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 08:38:38 +0200 Subject: [PATCH] #1 Add history of last commands, allow fuck more than once --- tests/test_history.py | 44 +++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 33 +++++++++++++++++++++++++------- thefuck/history.py | 27 ++++++++++++++++++++++++++ thefuck/main.py | 26 ++++++++++++------------- 4 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 tests/test_history.py create mode 100644 thefuck/history.py diff --git a/tests/test_history.py b/tests/test_history.py new file mode 100644 index 00000000..37687608 --- /dev/null +++ b/tests/test_history.py @@ -0,0 +1,44 @@ +import pytest +from mock import patch, Mock +from thefuck.history import History + + +@pytest.fixture +def process(monkeypatch): + Process = Mock() + Process.return_value.parent.return_value.pid = 1 + monkeypatch.setattr('thefuck.history.Process', Process) + + +@pytest.fixture +def db(monkeypatch): + class DBMock(dict): + def __init__(self): + super(DBMock, self).__init__() + self.sync = Mock() + + def __call__(self, *args, **kwargs): + return self + + db = DBMock() + monkeypatch.setattr('thefuck.history.shelve.open', db) + return db + + +@pytest.mark.usefixtures('process') +class TestHistory(object): + def test_set(self, db): + history = History() + history.update(last_script='ls', + last_fixed_script=None) + assert db == {'1-last_script': 'ls', + '1-last_fixed_script': None} + + def test_get(self, db): + history = History() + db['1-last_script'] = 'cd ..' + assert history.last_script == 'cd ..' + + def test_get_without_value(self, db): + history = History() + assert history.last_script is None diff --git a/tests/test_main.py b/tests/test_main.py index 776f5c99..bb3710e2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -46,15 +46,30 @@ def test_get_command(): return_value=True): Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stderr.read.return_value = b'stderr' - assert main.get_command(Mock(), ['thefuck', 'apt-get', - 'search', 'vim']) \ + assert main.get_command(Mock(), Mock(), + ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', shell=True, stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) - assert main.get_command(Mock(), ['']) is None + assert main.get_command(Mock(), Mock(), ['']) is None + # When command is `fuck`: + assert main.get_command( + Mock(), + Mock(last_script='ls', last_fixed_script='ls -la'), + ['thefuck', 'fuck']).script == 'ls -la' + # When command equals to last command: + assert main.get_command( + Mock(), + Mock(last_script='ls', last_fixed_script='ls -la'), + ['thefuck', 'ls']).script == 'ls -la' + # When last command is `fuck` and no last fixed script: + assert main.get_command( + Mock(), + Mock(last_script='ls', last_fixed_script=''), + ['thefuck', 'ls']).script == 'ls' def test_get_matched_rule(capsys): @@ -72,20 +87,24 @@ def test_get_matched_rule(capsys): def test_run_rule(capsys): with patch('thefuck.main.confirm', return_value=True): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - None, None) + Command(), Mock(), None) assert capsys.readouterr() == ('new-command\n', '') # With side effect: side_effect = Mock() settings = Mock() - command = Mock() + command = Mock(script='ls') + history = Mock() main.run_rule(Rule(get_new_command=lambda *_: 'new-command', side_effect=side_effect), - command, settings) + command, history, settings) assert capsys.readouterr() == ('new-command\n', '') side_effect.assert_called_once_with(command, settings) + # Ensure that history updated: + history.update.assert_called_once_with(last_script='ls', + last_fixed_script='new-command') with patch('thefuck.main.confirm', return_value=False): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - None, None) + Command(), Mock(), None) assert capsys.readouterr() == ('', '') diff --git a/thefuck/history.py b/thefuck/history.py new file mode 100644 index 00000000..86a24413 --- /dev/null +++ b/thefuck/history.py @@ -0,0 +1,27 @@ +import os +import shelve +from tempfile import gettempdir +from psutil import Process + + +class History(object): + """Temporary history of commands/fixed-commands dependent on + current shell instance. + + """ + + def __init__(self): + self._path = os.path.join(gettempdir(), '.thefuck_history') + self._pid = Process(os.getpid()).parent().pid + self._db = shelve.open(self._path) + + def _prepare_key(self, key): + return '{}-{}'.format(self._pid, key) + + def update(self, **kwargs): + self._db.update({self._prepare_key(k): v for k,v in kwargs.items()}) + self._db.sync() + return self + + def __getattr__(self, item): + return self._db.get(self._prepare_key(item)) diff --git a/thefuck/main.py b/thefuck/main.py index 235784b8..d75a43ec 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -6,6 +6,7 @@ import os import sys from psutil import Process, TimeoutExpired import colorama +from .history import History from . import logs, conf, types @@ -59,16 +60,21 @@ def wait_output(settings, popen): return False -def get_command(settings, args): +def get_command(settings, history, args): """Creates command from `args` and executes it.""" if sys.version_info[0] < 3: script = ' '.join(arg.decode('utf-8') for arg in args[1:]) else: script = ' '.join(args[1:]) + if script == 'fuck' or script == history.last_script: + script = history.last_fixed_script or history.last_script + if not script: return + history.update(last_script=script, + last_fixed_script=None) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=dict(os.environ, LANG='C')) if wait_output(settings, result): @@ -101,20 +107,17 @@ def confirm(new_command, side_effect, settings): return False -def run_rule(rule, command, settings): +def run_rule(rule, command, history, settings): """Runs command from rule for passed command.""" new_command = rule.get_new_command(command, settings) if confirm(new_command, rule.side_effect, settings): if rule.side_effect: rule.side_effect(command, settings) + history.update(last_script=command.script, + last_fixed_script=new_command) print(new_command) -def is_second_run(command): - """Is it the second run of `fuck`?""" - return command.script.startswith('fuck') - - def alias(): print("\nalias fuck='eval $(thefuck $(fc -ln -1))'\n") @@ -123,17 +126,14 @@ def main(): colorama.init() user_dir = setup_user_dir() settings = conf.get_settings(user_dir) + history = History() - command = get_command(settings, sys.argv) + command = get_command(settings, history, sys.argv) if command: - if is_second_run(command): - logs.failed("Can't fuck twice", settings) - return - rules = get_rules(user_dir, settings) matched_rule = get_matched_rule(command, rules, settings) if matched_rule: - run_rule(matched_rule, command, settings) + run_rule(matched_rule, command, history, settings) return logs.failed('No fuck given', settings)