From a9a5e18b65e29c5e4d0b932f14560c9baec07872 Mon Sep 17 00:00:00 2001 From: amin Date: Tue, 30 Jun 2020 14:23:22 +0430 Subject: [PATCH 1/4] git add remove functionality --- cli_client/client.py | 18 ++++++++- cli_client/config.py | 5 ++- manager/abstract.py | 4 ++ manager/manager.py | 5 +++ requirements.txt | 1 + tasks/tasks.py | 6 +++ tests/test_cli_client.py | 80 +++++++++++++++++++++++++++++-------- tests/test_tasks.py | 31 ++++++++++++++ tests/test_tasks_manager.py | 11 +++++ 9 files changed, 143 insertions(+), 18 deletions(-) diff --git a/cli_client/client.py b/cli_client/client.py index 2dc0760..b43e1fd 100644 --- a/cli_client/client.py +++ b/cli_client/client.py @@ -39,7 +39,23 @@ def edit(group, entry): except LookupError: click.secho(config.FAILED_LOOKUP.format(entry=entry, group=group), fg='red') except ConflictError: - click.secho(config.EDIT_FAILED_CONFLICT.format(entry=entry, group=group), fg='red') + click.secho(config.CONFLICTING_ENTRIES.format(entry=entry, group=group), fg='red') + + +@task.command(help=config.REMOVE_HELP) +@click.argument('group', nargs=1) +@click.argument('entry', nargs=-1, required=True) +def remove(group, entry): + entry = ' '.join(word for word in entry) + manager = ClientManagerFactory.create(group) + try: + full_name = manager.get_entry_full_name(partial_name=entry) + task_name = manager.delete_entry(full_name) + click.echo(config.REMOVE_SUCCESS.format(entry=full_name, group=group, new_entry=task_name)) + except LookupError: + click.secho(config.FAILED_LOOKUP.format(entry=entry, group=group), fg='red') + except ConflictError: + click.secho(config.CONFLICTING_ENTRIES.format(entry=entry, group=group), fg='red') @task.command(name='list', help=config.LIST_HELP) diff --git a/cli_client/config.py b/cli_client/config.py index 2087c06..28eb112 100644 --- a/cli_client/config.py +++ b/cli_client/config.py @@ -7,8 +7,11 @@ EDIT_HELP = 'Edit an entry in a specific group (prefix matches are valid).' EDIT_NEW_NAME_PROMPT = 'Enter the new name for {entry}' EDIT_SUCCESS = 'Entry {entry} in {group} changed to {new_entry}.' -EDIT_FAILED_CONFLICT = 'More than one entry match for {entry} in {group}!' +REMOVE_HELP = 'Remove an entry from a specific group (prefix matches are valid).' +REMOVE_SUCCESS = 'Entry {entry} in {group} removed.' + +CONFLICTING_ENTRIES = 'More than one entry match for {entry} in {group}!' FAILED_LOOKUP = 'Entry {entry} not found in {group}!' FINISH_HELP = 'Finish an entry in a specific group (accepts non-existing entries).' diff --git a/manager/abstract.py b/manager/abstract.py index e2a55aa..818fdea 100644 --- a/manager/abstract.py +++ b/manager/abstract.py @@ -16,6 +16,10 @@ def get_entry_full_name(self, partial_name): def add_entry(self, entry: str) -> str: pass + @abstractmethod + def delete_entry(self, entry: str) -> str: + pass + @abstractmethod def edit_entry(self, entry: str, new_entry: str) -> str: pass diff --git a/manager/manager.py b/manager/manager.py index c1f57c0..47f84e7 100644 --- a/manager/manager.py +++ b/manager/manager.py @@ -25,6 +25,11 @@ def edit_entry(self, entry: str, new_entry: str) -> str: self.storage.put(tasks) return new_entry + def delete_entry(self, entry: str) -> None: + tasks = self.retrieve() + tasks.delete(entry) + self.storage.put(tasks) + def finish_entry(self, entry: str) -> str: tasks = self.retrieve() if tasks.has(entry): diff --git a/requirements.txt b/requirements.txt index 3e0e127..8586972 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ click==7.1.1 fpdf==1.7.2 +coverage==5.1 \ No newline at end of file diff --git a/tasks/tasks.py b/tasks/tasks.py index 61d9228..3cb89b8 100644 --- a/tasks/tasks.py +++ b/tasks/tasks.py @@ -28,6 +28,12 @@ def add(self, task_name: str): self._task_names.add(task_name) return task + def delete(self, task_name: str): + index = self._find_task_index_based_on_full_match_or_prefix_match_on_name(task_name) + full_name = self._tasks[index].name + self._task_names.remove(full_name) + del self._tasks[index] + def _find_task_index_based_on_full_match_or_prefix_match_on_name(self, task_name): matched_indices = [] for index, task in enumerate(self._tasks): diff --git a/tests/test_cli_client.py b/tests/test_cli_client.py index 563308a..3c1740d 100644 --- a/tests/test_cli_client.py +++ b/tests/test_cli_client.py @@ -4,7 +4,7 @@ from cli_client import client from cli_client import config -from cli_client.client import task, add, edit, finish, list_entries, export, undo +from cli_client.client import task, add, edit, finish, list_entries, export, undo, remove from cli_client.factory import ClientManagerFactory from manager.abstract import TasksManager from tasks.errors import UniqueViolationError, ConflictError @@ -13,6 +13,12 @@ class ConcreteTasksManager(TasksManager): + def delete_entry(self, entry: str) -> str: + global storage + if self.name not in storage: + raise LookupError + storage[self.name].remove(entry) + def get_entry_full_name(self, partial_name): return partial_name @@ -158,7 +164,7 @@ def patched_edit(*args, **kwargs): original_edit_entry = ConcreteTasksManager.edit_entry ConcreteTasksManager.edit_entry = patched_edit result = runner.invoke(edit, ['work', 'task'], input='another task') - msg = config.EDIT_FAILED_CONFLICT.format(entry='task', group='work') + msg = config.CONFLICTING_ENTRIES.format(entry='task', group='work') self.assertEqual(0, result.exit_code) self.assertTrue(msg in result.output) self.assertEqual({}, storage) @@ -169,41 +175,83 @@ def test_edit_outputs_correct_success_text(self): with runner.isolated_filesystem(): result = runner.invoke(add, ['work', 'task_1']) self.assertEqual(0, result.exit_code) - self.assertTrue(config.ADD_SUCCESS.format(group='work', entry='task_1') in result.output) + result = runner.invoke(edit, ['work', 'task_1'], input='task_2') + self.assertEqual(0, result.exit_code) + self.assertTrue( + config.EDIT_SUCCESS.format(group='work', entry='task_1', new_entry='task_2') in result.output) - def test_edit_creates_group_if_not_existing(self): + def test_edit_accepts_entry_with_spaces(self): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(add, ['work', 'task_1']) + result = runner.invoke(add, ['work', 'this', 'is', 'an', 'entry', 'with', 'spaces']) self.assertEqual(0, result.exit_code) - self.assertEqual('task_1', storage['work'][0]) + result = runner.invoke(edit, ['work', 'this', 'is', 'an', 'entry', 'with', 'spaces'], input='new_task') + self.assertEqual(0, result.exit_code) + self.assertEqual('new_task', storage['work']) - def test_edit_accepts_entry_with_spaces(self): + def test_remove_helps_outputs_the_help(self): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(add, ['work', 'this', 'is', 'an', 'entry', 'with', 'spaces']) + result = runner.invoke(remove, ['--help']) self.assertEqual(0, result.exit_code) - self.assertEqual('this is an entry with spaces', storage['work'][0]) + self.assertTrue(config.REMOVE_HELP in result.output) - def test_edit_correctly_adds_to_an_already_existing_group(self): + def test_remove_removes_the_task_correctly(self): runner = CliRunner() with runner.isolated_filesystem(): result = runner.invoke(add, ['work', 'task 1']) self.assertEqual(0, result.exit_code) - self.assertEqual('task 1', storage['work'][0]) result = runner.invoke(add, ['work', 'task 2']) self.assertEqual(0, result.exit_code) + self.assertEqual('task 1', storage['work'][0]) self.assertEqual('task 2', storage['work'][1]) + result = runner.invoke(remove, ['work', 'task 2']) + self.assertEqual(0, result.exit_code) + self.assertEqual('task 1', storage['work'][0]) + self.assertEqual(1, len(storage['work'])) - def test_edit_does_not_print_error_when_entry_already_exists(self): + def test_remove_prints_error_if_entry_does_not_exists(self): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(add, ['work', 'task 1']) + result = runner.invoke(remove, ['work', 'task']) + msg = config.FAILED_LOOKUP.format(entry='task', group='work') + print(result.output) self.assertEqual(0, result.exit_code) - self.assertEqual('task 1', storage['work'][0]) - result = runner.invoke(add, ['work', 'task 1']) + self.assertTrue(msg in result.output) + self.assertEqual({}, storage) + + def test_remove_prints_error_if_entry_matches_more_than_one_tasks(self): + runner = CliRunner() + with runner.isolated_filesystem(): + def patched_remove(*args, **kwargs): + raise ConflictError + + original_remove_entry = ConcreteTasksManager.delete_entry + ConcreteTasksManager.delete_entry = patched_remove + result = runner.invoke(remove, ['work', 'task']) + msg = config.CONFLICTING_ENTRIES.format(entry='task', group='work') self.assertEqual(0, result.exit_code) - self.assertTrue(config.ADD_FAILED.format(group='work', entry='task 1') in result.output) + self.assertTrue(msg in result.output) + self.assertEqual({}, storage) + ConcreteTasksManager.delete_entry = original_remove_entry + + def test_remove_outputs_correct_success_text(self): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(add, ['work', 'task_1']) + self.assertEqual(0, result.exit_code) + result = runner.invoke(remove, ['work', 'task_1']) + self.assertEqual(0, result.exit_code) + self.assertTrue(config.REMOVE_SUCCESS.format(group='work', entry='task_1') in result.output) + + def test_remove_accepts_entry_with_spaces(self): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(add, ['work', 'this', 'is', 'an', 'entry', 'with', 'spaces']) + self.assertEqual(0, result.exit_code) + result = runner.invoke(remove, ['work', 'this', 'is', 'an', 'entry', 'with', 'spaces']) + self.assertEqual(0, result.exit_code) + self.assertEqual(storage['work'], []) def test_list_helps_outputs_the_help(self): runner = CliRunner() diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 50621f3..5eb0eea 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -125,3 +125,34 @@ def test_has_looks_up_correctly(self): self.assertFalse(tasks.has('tasks 3')) self.assertTrue(tasks.has('tasks 2')) self.assertTrue(tasks.has('tasks 1')) + + def test_delete_correctly_deletes_a_task(self): + tasks = SimpleTasks('work') + tasks.add('tasks 1') + tasks.add('tasks 2') + tasks.add('tasks 3') + self.assertTrue(tasks.has('tasks 1')) + self.assertTrue(tasks.has('tasks 2')) + self.assertTrue(tasks.has('tasks 3')) + tasks.delete('tasks 2') + self.assertTrue(tasks.has('tasks 1')) + self.assertTrue(not tasks.has('tasks 2')) + self.assertTrue(tasks.has('tasks 3')) + + def test_delete_correctly_deletes_a_task_with_prefix_match(self): + tasks = SimpleTasks('work') + tasks.add('first task') + tasks.add('second task') + tasks.add('third task') + self.assertTrue(tasks.has('first task')) + self.assertTrue(tasks.has('second task')) + self.assertTrue(tasks.has('third task')) + tasks.delete('sec') + self.assertTrue(tasks.has('first task')) + self.assertTrue(not tasks.has('second task')) + self.assertTrue(tasks.has('third task')) + + def test_delete_raises_lookup_error_when_task_not_present(self): + tasks = SimpleTasks('work') + tasks.add('tasks 1') + self.assertRaises(LookupError, tasks.delete, 'tasks 2') diff --git a/tests/test_tasks_manager.py b/tests/test_tasks_manager.py index c5c31a9..713a31f 100644 --- a/tests/test_tasks_manager.py +++ b/tests/test_tasks_manager.py @@ -96,6 +96,17 @@ def test_manager_get_full_name_get_the_complete_name_of_the_entry_correctly(self tasks_manager.add_entry('three job') self.assertEqual('one job', tasks_manager.get_entry_full_name('one')) + def test_manager_delete_entry_deletes_the_entry_correctly(self): + tasks_manager = SimpleTasksManager('work', self.storage) + jobs = ['one job', 'two job', 'three job'] + for job in jobs: + tasks_manager.add_entry(job) + saved_jobs = [file['./work.foo']['tasks'][i]['name'] for i in range(len(file['./work.foo']['tasks']))] + self.assertEqual(saved_jobs, jobs) + tasks_manager.delete_entry('two job') + saved_jobs = [file['./work.foo']['tasks'][i]['name'] for i in range(len(file['./work.foo']['tasks']))] + self.assertEqual(['one job', 'three job'], saved_jobs) + def tearDown(self) -> None: global file file = {} From 0f29f313412582bef3c335c6fbd57060e35f5e28 Mon Sep 17 00:00:00 2001 From: amin Date: Tue, 30 Jun 2020 14:23:32 +0430 Subject: [PATCH 2/4] increase patch version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a09bc3..f8266b9 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='cli-task', - version='0.1', + version='0.1.1', description='A simple cli to-do list.', long_description=README, long_description_content_type="text/markdown", From 49e23a69371f6771f43ca2fd97e528fc4140b331 Mon Sep 17 00:00:00 2001 From: amin Date: Tue, 30 Jun 2020 14:34:45 +0430 Subject: [PATCH 3/4] fix version to 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f8266b9..56b879c 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='cli-task', - version='0.1.1', + version='0.2.0', description='A simple cli to-do list.', long_description=README, long_description_content_type="text/markdown", From 2ceb94e6fdf5308dfccb1cdb68d7dfb93dd95431 Mon Sep 17 00:00:00 2001 From: amin Date: Tue, 30 Jun 2020 14:35:00 +0430 Subject: [PATCH 4/4] update readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b91f304..867743b 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,15 @@ Install using python 3.8 and pip3: ```bash pip3 install cli-task ``` + +## Versions +### 0.2.0 +* Added remove functionality: + + ```bash + ~ task remove Movies John + ``` + ## Future I try to add sorting feature for list and exports, also I might try to add attributes to each task (due date, urgency, ...).