def test_print_open_report( self, runner: CliRunner, repo_e2e: Repository, faker: Faker, ) -> None: """Test that open returns the expected output.""" task = Task(description="Description", due=faker.date_time(), priority=3) repo_e2e.add(task) repo_e2e.commit() expected_output = [ r".*", r" +ID +│ +Description +│ +Pri.*", r".*", fr" +{task.id_} +│ +{task.description} +│ +{task.priority}.*", r".*", ] result = runner.invoke(cli, ["open"]) assert result.exit_code == 0 assert report_prints_expected(result.stdout, expected_output, result.stderr)
def test_print_report_report_allows_sorting( self, runner: CliRunner, repo_e2e: Repository, faker: Faker, ) -> None: """ Given: Three tasks When: printing the open report sorting first by ascending priority, and then by descending id. Then: The tasks are printed in the desired order """ tasks = [ Task(id_=0, description="Last", priority=3), Task(id_=1, description="Middle", priority=3), Task(id_=2, description="First", priority=1), ] for task in tasks: repo_e2e.add(task) repo_e2e.commit() expected_output = [ r".*", r" +ID +│ +Description +│ +Pri.*", r".*", fr" +{tasks[2].id_} +│ +{tasks[2].description} +│ +{tasks[2].priority}.*", fr" +{tasks[1].id_} +│ +{tasks[1].description} +│ +{tasks[1].priority}.*", fr" +{tasks[0].id_} +│ +{tasks[0].description} +│ +{tasks[0].priority}.*", r".*", ] result = runner.invoke(cli, ["report", "open", "sort:+priority,-id_"]) assert result.exit_code == 0 assert report_prints_expected(result.stdout, expected_output, result.stderr)
def test_repo_add_entity_merges_with_stored_values_before_adding( self, database: Any, repo: Repository, repo_tester: RepositoryTester[Repository], ) -> None: """ Given: A repository with an entity When: Adding that entity with updated values Then: The entities are merged before they are commited. The Genre model has the `rating` attribute in the `skip_on_merge` configuration therefore even if the added entity has a different value, it's not propagated. """ entity = GenreFactory.build() repo_tester.insert_entity(database, entity) original_entity = entity.copy() entity.rating = 3 entity.name = "new name" repo.add(entity, merge=True) repo.commit() # act stored_entity = repo_tester.get_all(database, Genre)[0] assert stored_entity.rating == original_entity.rating assert stored_entity.name == "new name"
def test_print_closed_report_can_specify_filter( self, runner: CliRunner, insert_tasks_e2e: List[Task], repo_e2e: Repository) -> None: """ Given: Two closed tasks, one deleted and another done When: the closed report is called with the filter that matches only one of them Then: only that task is shown """ task = insert_tasks_e2e[0] task.description = "description" task.area = "special" task.priority = 1 task.close(TaskState.DELETED) repo_e2e.add(task) insert_tasks_e2e[1].close() repo_e2e.add(insert_tasks_e2e[1]) repo_e2e.commit() expected_output = [ r".*", r" +ID +│ +Description +│ +Area .*", r".*", fr" +{task.id_} +│ +{task.description} +│ +{task.area}.*", r".*", ] result = runner.invoke(cli, ["closed", "area:special"]) assert result.exit_code == 0 assert report_prints_expected(result.stdout, expected_output, result.stderr)
def test_print_recurring_report(self, runner: CliRunner, repo_e2e: Repository) -> None: """Test that recurring returns the expected output.""" parent = RecurrentTaskFactory.create(description="D", priority=1, area="A") repo_e2e.add(parent) repo_e2e.commit() # ECE001: Expression is too complex. Life is tough expected_output = [ # noqa: ECE001 r".*", r" +ID +│ +Descr.* +│ +Recur +│ +RecurType +│ +Area +| +Pri +│ +Due.*", r".*", fr" +{parent.id_} +│ +{parent.description} +│ +{parent.recurrence} +│ +" fr"{parent.recurrence_type.value.title()} +│ +{parent.area} +│ +" fr"{parent.priority} +│ +{parent.due.year}.*", r".*", ] result = runner.invoke(cli, ["recurring"]) assert result.exit_code == 0 assert report_prints_expected(result.stdout, expected_output, result.stderr)
def freeze_tasks( repo: Repository, selector: TaskSelector, ) -> None: """Freeze a list of tasks based on a task filter.""" tasks = _tasks_from_selector(repo, selector) for task in tasks: if type(task) == Task: child_task = task if child_task.parent_id is None: raise ValueError( f"Task {child_task.id_}: {child_task.description} is not the child" " of any recurrent task, so it can't be frozen") parent_task = repo.get(child_task.parent_id, [RecurrentTask]) elif type(task) == RecurrentTask: parent_task = task try: child_task = repo.search( { "active": True, "parent_id": task.id_ }, [Task])[0] except EntityNotFoundError as error: raise EntityNotFoundError( f"The recurrent task {task.id_}: {task.description} has no active " "children") from error parent_task.freeze() repo.add(parent_task) repo.delete(child_task) log.info( f"Frozen recurrent task {parent_task.id_}: {parent_task.description} and " f"deleted it's last child {child_task.id_}") repo.commit()
def add_task(repo: Repository, change: TaskChanges) -> Union[RecurrentTask, Task]: """Create a new task. If it's a RecurrentTask, it returns the parent. """ task: Optional[TaskType] = None if len(change.tags_to_add) > 0: change.task_attributes["tags"] = change.tags_to_add if change.task_attributes.get("recurrence_type", None) in [ "recurring", "repeating", ]: task = repo.add(RecurrentTask(**change.task_attributes)) child_task = repo.add(task.breed_children()) log.info(f"Added {task.recurrence_type} task {task.id_}:" f" {task.description}") log.info(f"Added first child task with id {child_task.id_}") else: task = repo.add(Task(**change.task_attributes)) log.info(f"Added task {task.id_}: {task.description}") repo.commit() return task
def test_modify_task_can_remove_tag_that_starts_with_p( self, runner: CliRunner, insert_tasks_e2e: List[Task], faker: Faker, repo_e2e: Repository, caplog: LogCaptureFixture, ) -> None: """ Given: A task with a tag that starts with a p When: removing the tag Then: the tag is removed It's necessary in case we start using the `--parent` flag as `-p`, in that case when using `pydo mod 0 -python`, it interprets that the parent flag is set and that the tag is ython. """ task = insert_tasks_e2e[0] task.tags = ["python"] repo_e2e.add(task) repo_e2e.commit() result = runner.invoke(cli, ["mod", str(task.id_), "-python"]) modified_task = repo_e2e.get(task.id_, [Task]) assert result.exit_code == 0 assert re.match(f"Modified task {task.id_}", caplog.records[0].msg) assert modified_task.tags == [] assert modified_task.description == task.description
def test_task_report_can_print_tags( repo: Repository, config: Config, capsys: CaptureFixture[Any] ) -> None: """Test that the open report prints the task tags.""" # Generate the tasks task = factories.TaskFactory.create( description="Description", tags=["tag1", "tag2"] ) repo.add(task) repo.commit() # Generate the output expected_output = [ r".*", r" +ID.*│ Tags.*", r".*", r".* +│ tag1, tag2", r".*", ] out, err = run_report( "print_task_report", {"repo": repo, "config": config, "report_name": "open"}, capsys, ) # act assert report_prints_expected(out, expected_output, err)
def task_(repo: Repository) -> Task: """Insert a Task in the FakeRepository.""" task = factories.TaskFactory.create(state="backlog") repo.add(task) repo.commit() return task
def test_print_recurring_report_can_specify_filter( self, runner: CliRunner, insert_parent_tasks_e2e: Tuple[List[RecurrentTask], List[Task]], repo_e2e: Repository, ) -> None: """Test that recurring report accepts a task filter.""" parent = RecurrentTaskFactory.create(description="D", area="special", priority=1) repo_e2e.add(parent) repo_e2e.commit() # ECE001: Expression is too complex. Life is tough expected_output = [ # noqa: ECE001 r".*", r" +ID +│ +Descri.* +│ +Recur +│ +RecurType +│ +Area +| +Pri +│ +Due.*", r".*", fr" +{parent.id_} +│ +{parent.description} +│ +{parent.recurrence} +│ +" fr"{parent.recurrence_type.value.title()} +│ +{parent.area} +│ +" fr"{parent.priority} +│ +{parent.due.year}.*", r".*", ] result = runner.invoke(cli, ["recurring", "area:special"]) assert result.exit_code == 0 assert report_prints_expected(result.stdout, expected_output, result.stderr)
def test_print_frozen_report_can_specify_filter( self, runner: CliRunner, insert_frozen_parent_task_e2e: RecurrentTask, repo_e2e: Repository, ) -> None: """Test that frozen accepts a task filter.""" task = insert_frozen_parent_task_e2e task.description = "d" task.area = "special" task.priority = 1 repo_e2e.add(task) repo_e2e.commit() # ECE001: Expression is too complex. Life is tough expected_output = [ # noqa: ECE001 r".*", r" +ID +│ +Description +│ +Recur +│ +RecurType +│ +Area +| +Pri +│ +Due.*", r".*", fr" +{task.id_} +│ +{task.description} +│ +{task.recurrence} +│ +" fr"{task.recurrence_type.value.title()} +│ +{task.area} +│ +" fr"{task.priority} +│ +{task.due.year}.*", r".*", ] result = runner.invoke(cli, ["frozen", "area:special"]) assert result.exit_code == 0 assert report_prints_expected(result.stdout, expected_output, result.stderr)
def insert_multiple_tasks(repo: Repository) -> List[Task]: """Insert three Tasks in the repository.""" tasks = sorted(factories.TaskFactory.create_batch(20, state="backlog")) [repo.add(task) for task in tasks] repo.commit() return tasks
def test_tags_shows_open_tasks_without_tag( repo: Repository, config: Config, capsys: CaptureFixture[Any] ) -> None: """ Given: Two tasks with no tags, only one open When: Printing the tags report Then: Only the open task is shown in the report """ tasks = factories.TaskFactory.create_batch(2) tasks[1].close("done") for task in tasks: repo.add(task) repo.commit() expected_output = [ r".*", r" +Name +│ Open Tasks *", r".*", r" +None +│ +1", r".*", ] capsys.readouterr() out, err = run_report("tags", {"repo": repo}, capsys) # act assert report_prints_expected(out, expected_output, err)
def test_areas_prints_only_areas_with_open_tasks( repo: Repository, config: Config, capsys: CaptureFixture[Any] ) -> None: """ Given: Three tasks with different areas, where only one is open When: Printing the areas report Then: Only the area with the open task is shown """ tasks = factories.TaskFactory.create_batch(3) tasks[1].close("done") tasks[2].close("deleted") for task in tasks: repo.add(task) repo.commit() expected_output = [ r".*", r" +Name +│ Open Tasks *", r".*", fr" +{tasks[0].area} +│ +1", r".*", ] capsys.readouterr() out, err = run_report("areas", {"repo": repo}, capsys) # act assert report_prints_expected(out, expected_output, err)
def test_areas_prints_only_counts_open_tasks( repo: Repository, config: Config, capsys: CaptureFixture[Any] ) -> None: """ Given: Three tasks with the same area, one open, one completed, and other deleted When: Printing the areas report Then: Only the open task is shown """ tasks = factories.TaskFactory.create_batch(3, area="Area 1") tasks[1].close("done") tasks[2].close("deleted") for task in tasks: repo.add(task) repo.commit() expected_output = [ r".*", r" +Name +│ Open Tasks *", r".*", r" +Area 1 +│ +1", r".*", ] capsys.readouterr() out, err = run_report("areas", {"repo": repo}, capsys) # act assert report_prints_expected(out, expected_output, err)
def insert_frozen_parent_task_e2e(repo_e2e: Repository) -> RecurrentTask: """Insert a RecurrentTask in frozen state.""" parent_task = factories.RecurrentTaskFactory.create(state="backlog") parent_task.freeze() repo_e2e.add(parent_task) repo_e2e.commit() return parent_task
def tasks_(repo: Repository) -> List[Task]: """Insert three Tasks in the FakeRepository.""" tasks = sorted(factories.TaskFactory.create_batch(3, state="backlog")) for task in tasks: repo.add(task) repo.commit() return tasks
def insert_parent_task(repo: Repository, ) -> Tuple[RecurrentTask, Task]: """Insert a RecurrentTask and it's children Task in the FakeRepository.""" parent_task = factories.RecurrentTaskFactory.create(state="backlog") child_task = parent_task.breed_children() repo.add(parent_task) repo.add(child_task) repo.commit() return parent_task, child_task
def insert_frozen_parent_tasks_e2e( repo_e2e: Repository) -> List[RecurrentTask]: """Insert many RecurrentTask in frozen state.""" parent_tasks = factories.RecurrentTaskFactory.create_batch(3, state="backlog") for parent_task in parent_tasks: parent_task.freeze() repo_e2e.add(parent_task) repo_e2e.commit() return parent_tasks
def insert_tasks_e2e(repo_e2e: Repository) -> List[Task]: """Insert many tasks in the end to end repository.""" tasks = factories.TaskFactory.create_batch(3, priority=3, state="backlog") different_task = factories.TaskFactory.create(priority=2, state="backlog") tasks.append(different_task) for task in tasks: repo_e2e.add(task) repo_e2e.commit() return tasks
def test_repository_can_search_by_bool_property( self, repo: Repository, ) -> None: """Search should return the objects that have a bool property.""" expected_entity = BoolEntity(name="Name", active=True) repo.add(expected_entity) repo.commit() result = repo.search({"active": True}, BoolEntity) assert result == [expected_entity]
def test_print_tags_report(self, runner: CliRunner, repo_e2e: Repository, insert_tasks_e2e: List[Task]) -> None: """Test that tags returns the expected output.""" tasks = insert_tasks_e2e tasks[0].tags = ["tag1"] repo_e2e.add(tasks[0]) repo_e2e.commit() result = runner.invoke(cli, ["tags"]) assert result.exit_code == 0 assert re.search(r" +Name.*Open Tasks", result.output)
def test_repository_raises_error_if_get_finds_more_than_one_entity( self, repo: Repository) -> None: """ Given: Two entities of different type with the same ID When: We get the ID without specifying the model Then: a TooManyEntitiesError error is raised """ entities = AuthorFactory.batch(2, name="same name") repo.add(entities) repo.commit() with pytest.raises(TooManyEntitiesError, match=""): repo.get("same name", Author, "name") # act
def test_repo_can_search_in_list_of_str_attribute(self, repo: Repository) -> None: """ Given: A repository with an entity that contains an attribute with a list of str When: search is called with a regexp that matches one of the list elements Then: the entity is returned """ expected_entity = ListEntityFactory.build() repo.add(expected_entity) repo.commit() regexp = rf"{expected_entity.elements[0][:-1]}." result = repo.search({"elements": regexp}, ListEntity) assert result == [expected_entity]
def insert_parent_tasks_e2e( repo_e2e: Repository, ) -> Tuple[List[RecurrentTask], List[Task]]: """Insert a RecurrentTask and it's children Task in the repository.""" parent_tasks = factories.RecurrentTaskFactory.create_batch(3, state="backlog") child_tasks = [ parent_task.breed_children() for parent_task in parent_tasks ] [repo_e2e.add(parent_task) for parent_task in parent_tasks] [repo_e2e.add(child_task) for child_task in child_tasks] repo_e2e.commit() return parent_tasks, child_tasks
def modify_tasks( repo: Repository, selector: TaskSelector, change: TaskChanges, modify_parent: bool = False, is_recurrent: bool = False, ) -> None: """Modify the attributes of the tasks matching a filter.""" if not is_recurrent: selector.model = Task task_type = "task" else: selector.model = RecurrentTask task_type = "recurrent task" tasks = _tasks_from_selector(repo, selector) for task in tasks: original_task = task.copy(deep=True) for tag in change.tags_to_remove: try: task.tags.remove(tag) except ValueError: log.warning(f"Task {task.id_} doesn't have " f"the tag {tag} assigned.") for tag in change.tags_to_add: task.tags.append(tag) for attribute, value in change.task_attributes.items(): task.__setattr__(attribute, value) if task != original_task: task.modified = datetime.datetime.now() repo.add(task) log.info(f"Modified {task_type} {task.id_}.") if modify_parent: if task.parent_id is not None: parent_change = change.copy() with suppress(EntityNotFoundError): parent_selector = TaskSelector(task_ids=[task.parent_id]) modify_tasks(repo, parent_selector, parent_change, is_recurrent=True) else: log.warning(f"Task {task.id_} doesn't have a parent task.") repo.commit()
def test_repository_cant_save_an_entity_with_a_negative_id( self, repo: Repository, inserted_int_entity: Entity, merge: bool ) -> None: """ Given: A repository with an entity When: adding an entity with a negative id Then: the id of the new entity is one unit greater than the last one. """ entity = inserted_int_entity.__class__(id=-3, name="Entity with negative id") repo.add(entity, merge=merge) repo.commit() # act saved_entity = repo.last(type(inserted_int_entity)) # ignore: we know for sure that the id_ is an int assert saved_entity.id_ == inserted_int_entity.id_ + 1 # type: ignore assert saved_entity.name == "Entity with negative id"
def test_repository_raises_error_if_get_finds_more_than_one_entity( self, repo: Repository, inserted_entity: Entity ) -> None: """ Given: Two entities of different type with the same ID When: We get the ID without specifying the model Then: a TooManyEntitiesError error is raised """ other_entity = OtherEntity(id_=inserted_entity.id_, name="Other entity") repo.models = [type(inserted_entity), OtherEntity] # type: ignore repo.add(other_entity) repo.commit() with pytest.warns( UserWarning, match="In 2022-06-10.*deprecated" ), pytest.raises(TooManyEntitiesError, match=""): repo.get(inserted_entity.id_) # act
def test_repository_can_delete_an_entity( self, repo: Repository, inserted_entities: List[Entity], ) -> None: """ Given: a full repository. When: an entity is deleted. Then: the entity is not longer in the repository. """ entity_to_delete = inserted_entities[1] repo.delete(entity_to_delete) repo.commit() # act remaining_entities = repo.all(type(entity_to_delete)) assert entity_to_delete not in remaining_entities