From 281831cff38386d87102e8a5779f9693b6668cd0 Mon Sep 17 00:00:00 2001 From: Frank Showalter Date: Thu, 23 Nov 2023 12:06:50 -0500 Subject: [PATCH] update --- booklog/cli/add_reading.py | 10 +- booklog/cli/add_work.py | 2 +- booklog/cli/select_work.py | 12 +- booklog/exports/api.py | 32 +-- booklog/exports/authors.py | 40 ++- booklog/exports/reading_stats.py | 98 +++++--- booklog/exports/repository_data.py | 11 + booklog/exports/reviewed_works.py | 26 +- ...imeline_entries.py => timeline_entries.py} | 50 ++-- booklog/exports/unreviewed_works.py | 27 +- booklog/repository/api.py | 6 +- booklog/repository/json_works.py | 9 +- setup.cfg | 4 +- tests/cli/conftest.py | 2 +- tests/cli/prompt_utils.py | 34 +++ tests/cli/test_add_author.py | 17 +- tests/cli/test_add_reading.py | 238 +++++++++++++++--- tests/cli/test_add_work.py | 98 ++++++-- tests/exports/test_api.py | 2 +- 19 files changed, 497 insertions(+), 221 deletions(-) create mode 100644 booklog/exports/repository_data.py rename booklog/exports/{reading_timeline_entries.py => timeline_entries.py} (51%) create mode 100644 tests/cli/prompt_utils.py diff --git a/booklog/cli/add_reading.py b/booklog/cli/add_reading.py index 6bc4f62d..5800bde4 100644 --- a/booklog/cli/add_reading.py +++ b/booklog/cli/add_reading.py @@ -201,9 +201,8 @@ def ask_for_edition(state: State) -> State: state.stage = "ask_for_timeline" return state - if confirm("{0}?".format(selected_edition)): - state.edition = selected_edition - state.stage = "ask_for_grade" + state.edition = selected_edition + state.stage = "ask_for_grade" return state @@ -244,8 +243,7 @@ def ask_for_grade(state: State) -> State: state.stage = "ask_for_edition" return state - if confirm(review_grade): # noqa: WPS323 - state.grade = review_grade - state.stage = "persist_reading" + state.grade = review_grade + state.stage = "persist_reading" return state diff --git a/booklog/cli/add_work.py b/booklog/cli/add_work.py index e4737fc3..72a00adc 100644 --- a/booklog/cli/add_work.py +++ b/booklog/cli/add_work.py @@ -25,7 +25,7 @@ @dataclass(kw_only=True) class State(object): stage: Stages = "ask_for_authors" - kind: Optional[repository_api.WORK_KIND_TYPE] = None + kind: Optional[repository_api.Kind] = None title: Optional[str] = None work_authors: list[repository_api.WorkAuthor] = field(default_factory=list) subtitle: Optional[str] = None diff --git a/booklog/cli/select_work.py b/booklog/cli/select_work.py index 77ee18f0..b2d4cf71 100644 --- a/booklog/cli/select_work.py +++ b/booklog/cli/select_work.py @@ -35,11 +35,13 @@ def prompt() -> Optional[repository_api.Work]: return selected_work -def search_works(query: str) -> Iterable[repository_api.Work]: - return filter( - lambda work: query.lower() - in "{0}: {1}".format(work.title, work.subtitle).lower(), - repository_api.works(), +def search_works(query: str) -> list[repository_api.Work]: + return list( + filter( + lambda work: query.lower() + in "{0}: {1}".format(work.title, work.subtitle).lower(), + repository_api.works(), + ) ) diff --git a/booklog/exports/api.py b/booklog/exports/api.py index 91944b43..5214eb47 100644 --- a/booklog/exports/api.py +++ b/booklog/exports/api.py @@ -3,30 +3,24 @@ from booklog.exports import ( authors, reading_stats, - reading_timeline_entries, reviewed_works, + timeline_entries, unreviewed_works, ) +from booklog.exports.repository_data import RepositoryData from booklog.repository import api as repository_api def export_data() -> None: - all_authors = list(repository_api.authors()) - all_works = list(repository_api.works()) - all_reviews = list(repository_api.reviews()) - all_readings = list(repository_api.readings()) - - authors.export( - all_authors=all_authors, all_works=all_works, all_reviews=all_reviews - ) - reviewed_works.export( - all_works=all_works, - all_authors=all_authors, - all_reviews=all_reviews, - all_readings=all_readings, - ) - reading_timeline_entries.export(readings=repository_api.readings()) - unreviewed_works.export(works=repository_api.works()) - reading_stats.export( - readings=repository_api.readings(), reviews=repository_api.reviews() + repository_data = RepositoryData( + authors=list(repository_api.authors()), + works=list(repository_api.works()), + reviews=list(repository_api.reviews()), + readings=list(repository_api.readings()), ) + + authors.export(repository_data) + reviewed_works.export(repository_data) + timeline_entries.export(repository_data) + unreviewed_works.export(repository_data) + reading_stats.export(repository_data) diff --git a/booklog/exports/authors.py b/booklog/exports/authors.py index 7d627c29..c46ea6e8 100644 --- a/booklog/exports/authors.py +++ b/booklog/exports/authors.py @@ -1,6 +1,7 @@ from typing import Optional, TypedDict from booklog.exports import exporter, json_work_author +from booklog.exports.repository_data import RepositoryData from booklog.repository import api as repository_api from booklog.utils.logging import logger @@ -36,8 +37,7 @@ def build_json_author_work( work: repository_api.Work, review: Optional[repository_api.Review], - all_works: list[repository_api.Work], - all_authors: list[repository_api.Author], + repository_data: RepositoryData, ) -> JsonAuthorWork: return JsonAuthorWork( title=work.title, @@ -50,23 +50,26 @@ def build_json_author_work( gradeValue=review.grade_value if review else None, authors=[ json_work_author.build_json_work_author( - work_author=work_author, all_authors=all_authors + work_author=work_author, all_authors=repository_data.authors ) for work_author in work.work_authors ], - includedInSlugs=[work.slug for work in work.included_in_works(all_works)], + includedInSlugs=[ + work.slug for work in work.included_in_works(repository_data.works) + ], ) def build_json_author( - author: repository_api.Author, - all_works: list[repository_api.Work], - all_reviews: list[repository_api.Review], - all_authors: list[repository_api.Author], + author: repository_api.Author, repository_data: RepositoryData ) -> JsonAuthor: - author_works = list(author.works(all_works)) + author_works = list(author.works(repository_data.works)) reviewed_work_count = len( - [author_work for author_work in author_works if author_work.review(all_reviews)] + [ + author_work + for author_work in author_works + if author_work.review(repository_data.reviews) + ] ) return JsonAuthor( @@ -76,9 +79,8 @@ def build_json_author( works=[ build_json_author_work( work=work, - review=work.review(all_reviews), - all_works=all_works, - all_authors=all_authors, + review=work.review(repository_data.reviews), + repository_data=repository_data, ) for work in author_works ], @@ -87,21 +89,15 @@ def build_json_author( ) -def export( - all_authors: list[repository_api.Author], - all_works: list[repository_api.Work], - all_reviews: list[repository_api.Review], -) -> None: +def export(repository_data: RepositoryData) -> None: logger.log("==== Begin exporting {}...", "authors") json_authors = [ build_json_author( author=author, - all_works=all_works, - all_reviews=all_reviews, - all_authors=all_authors, + repository_data=repository_data, ) - for author in all_authors + for author in repository_data.authors ] exporter.serialize_dicts_to_folder( diff --git a/booklog/exports/reading_stats.py b/booklog/exports/reading_stats.py index 827e19bc..43f6eda1 100644 --- a/booklog/exports/reading_stats.py +++ b/booklog/exports/reading_stats.py @@ -1,9 +1,10 @@ from collections import defaultdict from datetime import date -from typing import Callable, Iterable, TypedDict, TypeVar +from typing import Callable, TypedDict, TypeVar from booklog.exports import exporter, list_tools -from booklog.repository.api import Reading, Review, Work +from booklog.exports.repository_data import RepositoryData +from booklog.repository import api as repository_api from booklog.utils.logging import logger JsonMostReadAuthorReading = TypedDict( @@ -59,7 +60,7 @@ def build_json_distributions( - distribution_items: Iterable[ListType], key: Callable[[ListType], str] + distribution_items: list[ListType], key: Callable[[ListType], str] ) -> list[JsonDistribution]: distribution = list_tools.group_list_by_key(distribution_items, key) @@ -69,7 +70,7 @@ def build_json_distributions( ] -def date_finished_or_abandoned(reading: Reading) -> date: +def date_finished_or_abandoned(reading: repository_api.Reading) -> date: return next( timeline_entry.date for timeline_entry in reading.timeline @@ -78,19 +79,21 @@ def date_finished_or_abandoned(reading: Reading) -> date: def group_readings_by_author( - readings: list[Reading], -) -> dict[str, list[Reading]]: - readings_by_author: dict[str, list[Reading]] = defaultdict(list) + readings: list[repository_api.Reading], repository_data: RepositoryData +) -> dict[str, list[repository_api.Reading]]: + readings_by_author: dict[str, list[repository_api.Reading]] = defaultdict(list) for reading in readings: - for work_author in reading.work().work_authors: + for work_author in reading.work(repository_data.works).work_authors: readings_by_author[work_author.author_slug].append(reading) return readings_by_author -def build_json_most_read_author_reading(reading: Reading) -> JsonMostReadAuthorReading: - work = reading.work() +def build_json_most_read_author_reading( + reading: repository_api.Reading, repository_data: RepositoryData +) -> JsonMostReadAuthorReading: + work = reading.work(repository_data.works) return JsonMostReadAuthorReading( sequence=reading.sequence, @@ -101,26 +104,32 @@ def build_json_most_read_author_reading(reading: Reading) -> JsonMostReadAuthorR title=work.title, yearPublished=work.year, includedInSlugs=[ - included_in_work.slug for included_in_work in work.included_in_works() + included_in_work.slug + for included_in_work in work.included_in_works(repository_data.works) ], ) -def build_most_read_authors(readings: list[Reading]) -> list[JsonMostReadAuthor]: - readings_by_author = group_readings_by_author(readings=readings) +def build_most_read_authors( + readings: list[repository_api.Reading], repository_data: RepositoryData +) -> list[JsonMostReadAuthor]: + readings_by_author = group_readings_by_author( + readings=readings, repository_data=repository_data + ) return [ JsonMostReadAuthor( name=next( - reading_work_author.author().name - for reading in readings - for reading_work_author in reading.work().work_authors - if reading_work_author.author_slug == author_slug + author.name + for author in repository_data.authors + if author.slug == author_slug ), count=len(readings), slug=author_slug, readings=[ - build_json_most_read_author_reading(reading=reading) + build_json_most_read_author_reading( + reading=reading, repository_data=repository_data + ) for reading in readings ], ) @@ -129,43 +138,50 @@ def build_most_read_authors(readings: list[Reading]) -> list[JsonMostReadAuthor] ] -def build_grade_distribution(reviews: list[Review]) -> list[JsonDistribution]: +def build_grade_distribution( + reviews: list[repository_api.Review], +) -> list[JsonDistribution]: return build_json_distributions(reviews, lambda review: review.grade) -def build_kind_distribution(works: list[Work]) -> list[JsonDistribution]: +def build_kind_distribution(works: list[repository_api.Work]) -> list[JsonDistribution]: return build_json_distributions(works, lambda work: work.kind) def build_edition_distribution( - readings: list[Reading], + readings: list[repository_api.Reading], ) -> list[JsonDistribution]: return build_json_distributions(readings, lambda reading: reading.edition) -def build_decade_distribution(works: list[Work]) -> list[JsonDistribution]: +def build_decade_distribution( + works: list[repository_api.Work], +) -> list[JsonDistribution]: return build_json_distributions(works, lambda work: "{0}0s".format(work.year[:3])) -def book_count(readings: list[Reading]) -> int: - works = [reading.work() for reading in readings] +def book_count( + readings: list[repository_api.Reading], repository_data: RepositoryData +) -> int: + works = [reading.work(repository_data.works) for reading in readings] return len([work for work in works if work.kind not in {"Short Story", "Novella"}]) def build_json_reading_stats( span: str, - readings: list[Reading], - reviews: list[Review], + readings: list[repository_api.Reading], + reviews: list[repository_api.Review], most_read_authors: list[JsonMostReadAuthor], + repository_data: RepositoryData, ) -> JsonReadingStats: - works = [reading.work() for reading in readings] + works = [reading.work(repository_data.works) for reading in readings] return JsonReadingStats( span=span, reviews=len(reviews), readWorks=len(readings), - books=book_count(readings), + books=book_count(readings, repository_data=repository_data), gradeDistribution=build_grade_distribution(reviews), kindDistribution=build_kind_distribution(works), editionDistribution=build_edition_distribution(readings), @@ -174,30 +190,27 @@ def build_json_reading_stats( ) -def export( - readings: Iterable[Reading], - reviews: Iterable[Review], -) -> None: +def export(repository_data: RepositoryData) -> None: logger.log("==== Begin exporting {}...", "reading_stats") - all_readings = list(readings) - all_reviews = list(reviews) - json_reading_stats = [ build_json_reading_stats( span="all-time", - reviews=all_reviews, - readings=all_readings, - most_read_authors=build_most_read_authors(readings=all_readings), + reviews=repository_data.reviews, + readings=repository_data.readings, + most_read_authors=build_most_read_authors( + readings=repository_data.readings, repository_data=repository_data + ), + repository_data=repository_data, ) ] reviews_by_year = list_tools.group_list_by_key( - all_reviews, lambda review: str(review.date.year) + repository_data.reviews, lambda review: str(review.date.year) ) readings_by_year = list_tools.group_list_by_key( - all_readings, + repository_data.readings, lambda reading: str(date_finished_or_abandoned(reading).year), ) @@ -207,7 +220,10 @@ def export( span=year, reviews=reviews_by_year[year], readings=readings_for_year, - most_read_authors=build_most_read_authors(readings=readings_for_year), + most_read_authors=build_most_read_authors( + readings=readings_for_year, repository_data=repository_data + ), + repository_data=repository_data, ) ) diff --git a/booklog/exports/repository_data.py b/booklog/exports/repository_data.py new file mode 100644 index 00000000..9d122441 --- /dev/null +++ b/booklog/exports/repository_data.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from booklog.repository import api as repository_api + + +@dataclass +class RepositoryData(object): + authors: list[repository_api.Author] + works: list[repository_api.Work] + readings: list[repository_api.Reading] + reviews: list[repository_api.Review] diff --git a/booklog/exports/reviewed_works.py b/booklog/exports/reviewed_works.py index 51622b79..b3633f0f 100644 --- a/booklog/exports/reviewed_works.py +++ b/booklog/exports/reviewed_works.py @@ -2,6 +2,7 @@ from typing import Optional, TypedDict from booklog.exports import exporter, json_work_author +from booklog.exports.repository_data import RepositoryData from booklog.repository import api as repository_api from booklog.utils.logging import logger @@ -76,8 +77,7 @@ def build_json_reviewed_work( work: repository_api.Work, readings_for_work: list[repository_api.Reading], review: repository_api.Review, - all_authors: list[repository_api.Author], - all_works: list[repository_api.Work], + repository_data: RepositoryData, ) -> JsonReviewedWork: most_recent_reading = sorted( readings_for_work, key=lambda reading: reading.sequence, reverse=True @@ -96,32 +96,27 @@ def build_json_reviewed_work( date=review.date, authors=[ json_work_author.build_json_work_author( - work_author=work_author, all_authors=all_authors + work_author=work_author, all_authors=repository_data.authors ) for work_author in work.work_authors ], readings=[build_json_reading(reading) for reading in readings_for_work], includedInSlugs=[ included_in_work.slug - for included_in_work in work.included_in_works(all_works) + for included_in_work in work.included_in_works(repository_data.works) ], yearReviewed=review.date.year, ) -def export( - all_reviews: list[repository_api.Review], - all_works: list[repository_api.Work], - all_authors: list[repository_api.Author], - all_readings: list[repository_api.Reading], -) -> None: - logger.log("==== Begin exporting {}...", "reviewed_works") +def export(repository_data: RepositoryData) -> None: + logger.log("==== Begin exporting {}...", "reviewed-works") json_reviewed_works = [] - for review in all_reviews: - work = review.work(all_works) - readings_for_work = list(work.readings(all_readings)) + for review in repository_data.reviews: + work = review.work(repository_data.works) + readings_for_work = list(work.readings(repository_data.readings)) if not readings_for_work: continue @@ -130,8 +125,7 @@ def export( work=work, readings_for_work=readings_for_work, review=review, - all_authors=all_authors, - all_works=all_works, + repository_data=repository_data, ) ) diff --git a/booklog/exports/reading_timeline_entries.py b/booklog/exports/timeline_entries.py similarity index 51% rename from booklog/exports/reading_timeline_entries.py rename to booklog/exports/timeline_entries.py index 0640bb5f..0b43b3db 100644 --- a/booklog/exports/reading_timeline_entries.py +++ b/booklog/exports/timeline_entries.py @@ -1,20 +1,21 @@ import datetime -from typing import Iterable, TypedDict +from typing import TypedDict from booklog.exports import exporter -from booklog.repository.api import Reading, TimelineEntry +from booklog.exports.repository_data import RepositoryData +from booklog.repository import api as repository_api from booklog.utils.logging import logger -ExportsReadingTimelineEntryAuthor = TypedDict( - "ExportsReadingTimelineEntryAuthor", +JsonTimelineEntryAuthor = TypedDict( + "JsonTimelineEntryAuthor", { "name": str, }, ) -ExportsReadingTimelineEntry = TypedDict( - "ExportsReadingTimelineEntry", +JsonTimelineEntry = TypedDict( + "JsonTimelineEntry", { "sequence": str, "slug": str, @@ -26,20 +27,21 @@ "yearPublished": str, "title": str, "kind": str, - "authors": list[ExportsReadingTimelineEntryAuthor], + "authors": list[JsonTimelineEntryAuthor], "includedInSlugs": list[str], }, ) -def build_json_reading_progress( - reading: Reading, - timeline_entry: TimelineEntry, -) -> ExportsReadingTimelineEntry: - work = reading.work() - reviewed = bool(work.review()) +def build_json_timeline_entry( + reading: repository_api.Reading, + timeline_entry: repository_api.TimelineEntry, + repository_data: RepositoryData, +) -> JsonTimelineEntry: + work = reading.work(repository_data.works) + reviewed = bool(work.review(repository_data.reviews)) - return ExportsReadingTimelineEntry( + return JsonTimelineEntry( sequence="{0}-{1}".format(timeline_entry.date, reading.sequence), slug=work.slug, edition=reading.edition, @@ -51,30 +53,32 @@ def build_json_reading_progress( title=work.title, readingYear=timeline_entry.date.year, authors=[ - ExportsReadingTimelineEntryAuthor(name=work_author.author().name) + JsonTimelineEntryAuthor( + name=work_author.author(repository_data.authors).name + ) for work_author in work.work_authors ], includedInSlugs=[ - included_in_work.slug for included_in_work in work.included_in_works() + included_in_work.slug + for included_in_work in work.included_in_works(repository_data.works) ], ) -def export( - readings: Iterable[Reading], -) -> None: - logger.log("==== Begin exporting {}...", "reading_timeline_entries") +def export(repository_data: RepositoryData) -> None: + logger.log("==== Begin exporting {}...", "timeline-entries") json_progress = [ - build_json_reading_progress( + build_json_timeline_entry( reading=reading, timeline_entry=timeline_entry, + repository_data=repository_data, ) - for reading in readings + for reading in repository_data.readings for timeline_entry in reading.timeline ] exporter.serialize_dicts( sorted(json_progress, key=lambda progress: progress["sequence"], reverse=True), - "reading-timeline-entries", + "timeline-entries", ) diff --git a/booklog/exports/unreviewed_works.py b/booklog/exports/unreviewed_works.py index 4b18ff86..95205533 100644 --- a/booklog/exports/unreviewed_works.py +++ b/booklog/exports/unreviewed_works.py @@ -1,11 +1,11 @@ -from typing import Iterable, Optional, TypedDict +from typing import Optional, TypedDict from booklog.exports import exporter, json_work_author -from booklog.repository.api import Work +from booklog.exports.repository_data import RepositoryData from booklog.utils.logging import logger -ExportsUnreviewedWork = TypedDict( - "ExportsUnreviewedWork", +JsonUnreviewedWork = TypedDict( + "JsonUnreviewedWork", { "slug": str, "includedInSlugs": list[str], @@ -19,13 +19,11 @@ ) -def export( - works: Iterable[Work], -) -> None: - logger.log("==== Begin exporting {}...", "unreviewed works") +def export(repository_data: RepositoryData) -> None: + logger.log("==== Begin exporting {}...", "unreviewed-works") json_unreviewed_works = [ - ExportsUnreviewedWork( + JsonUnreviewedWork( slug=work.slug, title=work.title, subtitle=work.subtitle, @@ -33,15 +31,18 @@ def export( yearPublished=work.year, kind=work.kind, authors=[ - json_work_author.build_json_work_author(work_author=work_author) + json_work_author.build_json_work_author( + work_author=work_author, all_authors=repository_data.authors + ) for work_author in work.work_authors ], includedInSlugs=[ - included_in_work.slug for included_in_work in work.included_in_works() + included_in_work.slug + for included_in_work in work.included_in_works(repository_data.works) ], ) - for work in works - if not work.review() + for work in repository_data.works + if not work.review(repository_data.reviews) ] exporter.serialize_dicts_to_folder( diff --git a/booklog/repository/api.py b/booklog/repository/api.py index 532d55a8..6da7b601 100644 --- a/booklog/repository/api.py +++ b/booklog/repository/api.py @@ -8,7 +8,7 @@ WORK_KINDS = json_works.KINDS -WORK_KIND_TYPE = json_works.KIND_TYPE +Kind = json_works.Kind SequenceError = json_readings.SequenceError @@ -48,7 +48,7 @@ class Work(object): year: str sort_title: str slug: str - kind: json_works.KIND_TYPE + kind: Kind included_work_slugs: list[str] work_authors: list[WorkAuthor] @@ -165,7 +165,7 @@ def create_work( # noqa: WPS211 subtitle: Optional[str], year: str, work_authors: list[WorkAuthor], - kind: json_works.KIND_TYPE, + kind: Kind, included_work_slugs: Optional[list[str]] = None, ) -> Work: return hydrate_json_work( diff --git a/booklog/repository/json_works.py b/booklog/repository/json_works.py index aefffac4..2028c86f 100644 --- a/booklog/repository/json_works.py +++ b/booklog/repository/json_works.py @@ -13,7 +13,7 @@ FOLDER_NAME = "works" -KIND_TYPE = Literal[ +Kind = Literal[ "Anthology", "Collection", "Nonfiction", @@ -21,7 +21,7 @@ "Novella", "Short Story", ] -KINDS = get_args(KIND_TYPE) +KINDS = get_args(Kind) JsonWorkAuthor = TypedDict( "JsonWorkAuthor", @@ -40,7 +40,7 @@ "sortTitle": str, "authors": list[JsonWorkAuthor], "slug": str, - "kind": KIND_TYPE, + "kind": Kind, "includedWorks": list[str], }, ) @@ -74,7 +74,7 @@ def create( # noqa: WPS211 subtitle: Optional[str], year: str, work_authors: list[CreateWorkAuthor], - kind: KIND_TYPE, + kind: Kind, included_work_slugs: Optional[list[str]] = None, ) -> JsonWork: slug = slugify( @@ -104,7 +104,6 @@ def create( # noqa: WPS211 def read_all() -> Iterable[JsonWork]: - print("here") for file_path in glob(os.path.join(FOLDER_NAME, "*.json")): with open(file_path, "r") as json_file: yield (cast(JsonWork, json.load(json_file))) diff --git a/setup.cfg b/setup.cfg index 10561808..68d72d9e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,8 +12,8 @@ max-module-members = 14 max-imports = 14 show-source = True per-file-ignores = - tests/*.py: S101, WPS118, WPS202, WPS432, WPS442 - ; tests/*.py: E501, S101, S105, S404, S603, S607, WPS118, WPS202, WPS211, WPS221, WPS226, WPS323, WPS355, WPS402, WPS420, WPS430, WPS432, WPS442 + tests/*.py: S101, WPS118, WPS202, WPS432, WPS442, WPS204 + booklog/repository/api.py: WPS202 [isort] include_trailing_comma = True diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 8ec57dcb..6ad4c3e5 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -13,7 +13,7 @@ @pytest.fixture(autouse=True, scope="function") def mock_input() -> Generator[MockInput, None, None]: with create_pipe_input() as pipe_input: - with create_app_session(input=pipe_input, output=DummyOutput()): + with create_app_session(input=pipe_input): def factory(input_sequence: Sequence[str]) -> None: # noqa: WPS 430 pipe_input.send_text("".join(input_sequence)) diff --git a/tests/cli/prompt_utils.py b/tests/cli/prompt_utils.py new file mode 100644 index 00000000..3f78d7e8 --- /dev/null +++ b/tests/cli/prompt_utils.py @@ -0,0 +1,34 @@ +import itertools +from typing import Literal, Optional + +from tests.cli.keys import Down, Enter + +ConfirmType = Literal["y", "n"] + + +def enter_text(text: Optional[str], confirm: Optional[ConfirmType] = None) -> list[str]: + input_stream = [Enter] + + if text: + input_stream.insert(0, text) + + if confirm: + input_stream.append(confirm) + + return input_stream + + +def select_option( + option_number: int, confirm: Optional[Literal[ConfirmType]] = None +) -> list[str]: + input_stream = [] + + if option_number > 1: + input_stream = [Down for _ in itertools.repeat(None, option_number - 1)] + + input_stream.append(Enter) + + if confirm: + input_stream.append(confirm) + + return input_stream diff --git a/tests/cli/test_add_author.py b/tests/cli/test_add_author.py index 25de12d9..f759da7a 100644 --- a/tests/cli/test_add_author.py +++ b/tests/cli/test_add_author.py @@ -6,6 +6,7 @@ from booklog.cli import add_author from tests.cli.conftest import MockInput from tests.cli.keys import Enter, Escape +from tests.cli.prompt_utils import enter_text @pytest.fixture @@ -16,13 +17,7 @@ def mock_create_author(mocker: MockerFixture) -> MagicMock: def test_calls_create_author( mock_input: MockInput, mock_create_author: MagicMock ) -> None: - mock_input( - [ - "Stephen King", - Enter, - "y", - ] - ) + mock_input(enter_text("Stephen King", confirm="y")) add_author.prompt() @@ -60,12 +55,8 @@ def test_can_correct_input( ) -> None: mock_input( [ - "Steven Kang", - Enter, - "n", - "Stephen King", - Enter, - "y", + *enter_text("Steven Kang", confirm="n"), + *enter_text("Stephen King", confirm="y"), ] ) diff --git a/tests/cli/test_add_reading.py b/tests/cli/test_add_reading.py index 5ed89ab7..346792fb 100644 --- a/tests/cli/test_add_reading.py +++ b/tests/cli/test_add_reading.py @@ -8,7 +8,8 @@ from booklog.cli import add_reading from booklog.repository import api as repository_api from tests.cli.conftest import MockInput -from tests.cli.keys import Backspace, Down, Enter +from tests.cli.keys import Backspace, Escape +from tests.cli.prompt_utils import ConfirmType, enter_text, select_option @pytest.fixture @@ -59,7 +60,27 @@ def stub_editions(mocker: MockerFixture) -> None: ) -def clear_default_date() -> list[str]: +def enter_title(title: str) -> list[str]: + return enter_text(title) + + +def select_title_search_result(confirm: ConfirmType) -> list[str]: + return select_option(1, confirm=confirm) + + +def select_search_again() -> list[str]: + return select_option(1) + + +def enter_notes(notes: Optional[str] = None) -> list[str]: + return enter_text(notes) + + +def select_edition_kindle() -> list[str]: + return select_option(3) + + +def enter_date(date: str) -> list[str]: return [ Backspace, Backspace, @@ -71,45 +92,141 @@ def clear_default_date() -> list[str]: Backspace, Backspace, Backspace, + *enter_text(date), ] -def enter_title(title: str = "Cellar") -> list[str]: - return [title, Enter] +def enter_progress(progress: str) -> list[str]: + return enter_text(progress) -def select_title_search_result() -> list[str]: - return [Enter] +def enter_grade(grade: str) -> list[str]: + return enter_text(grade) -def enter_notes(notes: Optional[str] = None) -> list[str]: - if notes: - return [notes, Enter] +def test_calls_add_reading_and_add_review( + mock_input: MockInput, + mock_create_reading: MagicMock, + mock_create_review: MagicMock, + work_fixture: repository_api.Work, +) -> None: + mock_input( + [ + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), + *enter_date("2016-03-10"), + *enter_progress("15"), + *enter_date("2016-03-11"), + *enter_progress("50"), + *enter_date("2016-03-12"), + *enter_progress("F"), + *select_edition_kindle(), + *enter_grade("A+"), + "n", + ] + ) - return [Enter] + add_reading.prompt() + mock_create_reading.assert_called_once_with( + work=work_fixture, + edition="Kindle", + timeline=[ + repository_api.TimelineEntry(date=date(2016, 3, 10), progress="15%"), + repository_api.TimelineEntry(date=date(2016, 3, 11), progress="50%"), + repository_api.TimelineEntry(date=date(2016, 3, 12), progress="Finished"), + ], + ) -def select_edition_kindle() -> list[str]: - return [ - Down, - Down, - Enter, - ] + mock_create_review.assert_called_once_with( + work=work_fixture, grade="A+", date=date(2016, 3, 12) + ) -def enter_date(date: str) -> list[str]: - return [date, Enter] +def test_can_search_again( + mock_input: MockInput, + mock_create_reading: MagicMock, + mock_create_review: MagicMock, + work_fixture: repository_api.Work, +) -> None: + mock_input( + [ + *enter_title("The Typo"), + *select_search_again(), + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), + *enter_date("2016-03-10"), + *enter_progress("15"), + *enter_date("2016-03-11"), + *enter_progress("50"), + *enter_date("2016-03-12"), + *enter_progress("F"), + *select_edition_kindle(), + *enter_grade("A+"), + "n", + ] + ) + add_reading.prompt() -def enter_progress(progress: str) -> list[str]: - return [progress, Enter] + mock_create_reading.assert_called_once_with( + work=work_fixture, + edition="Kindle", + timeline=[ + repository_api.TimelineEntry(date=date(2016, 3, 10), progress="15%"), + repository_api.TimelineEntry(date=date(2016, 3, 11), progress="50%"), + repository_api.TimelineEntry(date=date(2016, 3, 12), progress="Finished"), + ], + ) + mock_create_review.assert_called_once_with( + work=work_fixture, grade="A+", date=date(2016, 3, 12) + ) -def enter_grade(grade: str) -> list[str]: - return [grade, Enter] +def test_can_escape_from_first_date( + mock_input: MockInput, + mock_create_reading: MagicMock, + mock_create_review: MagicMock, + work_fixture: repository_api.Work, +) -> None: + mock_input( + [ + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), + Escape, + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), + *enter_date("2016-03-10"), + *enter_progress("15"), + *enter_date("2016-03-11"), + *enter_progress("50"), + *enter_date("2016-03-12"), + *enter_progress("F"), + *select_edition_kindle(), + *enter_grade("A+"), + "n", + ] + ) -def test_calls_add_reading_and_add_review( + add_reading.prompt() + + mock_create_reading.assert_called_once_with( + work=work_fixture, + edition="Kindle", + timeline=[ + repository_api.TimelineEntry(date=date(2016, 3, 10), progress="15%"), + repository_api.TimelineEntry(date=date(2016, 3, 11), progress="50%"), + repository_api.TimelineEntry(date=date(2016, 3, 12), progress="Finished"), + ], + ) + + mock_create_review.assert_called_once_with( + work=work_fixture, grade="A+", date=date(2016, 3, 12) + ) + + +def test_can_escape_from_second_progress_and_date( mock_input: MockInput, mock_create_reading: MagicMock, mock_create_review: MagicMock, @@ -117,22 +234,22 @@ def test_calls_add_reading_and_add_review( ) -> None: mock_input( [ - *enter_title(), - *select_title_search_result(), - "y", - *clear_default_date(), + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), + Escape, + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), *enter_date("2016-03-10"), *enter_progress("15"), - *clear_default_date(), + *enter_date("2017-03-11"), + Escape, + Escape, *enter_date("2016-03-11"), *enter_progress("50"), - *clear_default_date(), *enter_date("2016-03-12"), *enter_progress("F"), *select_edition_kindle(), - "y", *enter_grade("A+"), - "y", "n", ] ) @@ -152,3 +269,62 @@ def test_calls_add_reading_and_add_review( mock_create_review.assert_called_once_with( work=work_fixture, grade="A+", date=date(2016, 3, 12) ) + + +def test_can_escape_from_progress( + mock_input: MockInput, + mock_create_reading: MagicMock, + mock_create_review: MagicMock, + work_fixture: repository_api.Work, +) -> None: + mock_input( + [ + *enter_title("The Cellar"), + *select_title_search_result(confirm="y"), + *enter_date("2016-03-10"), + *enter_progress("15"), + *enter_date("2015-03-11"), + Escape, + *enter_date("2016-03-11"), + *enter_progress("50"), + *enter_date("2016-03-12"), + *enter_progress("F"), + *select_edition_kindle(), + *enter_grade("A+"), + "n", + ] + ) + + add_reading.prompt() + + mock_create_reading.assert_called_once_with( + work=work_fixture, + edition="Kindle", + timeline=[ + repository_api.TimelineEntry(date=date(2016, 3, 10), progress="15%"), + repository_api.TimelineEntry(date=date(2016, 3, 11), progress="50%"), + repository_api.TimelineEntry(date=date(2016, 3, 12), progress="Finished"), + ], + ) + + mock_create_review.assert_called_once_with( + work=work_fixture, grade="A+", date=date(2016, 3, 12) + ) + + +def test_can_escape_from_title( + mock_input: MockInput, + mock_create_reading: MagicMock, + mock_create_review: MagicMock, +) -> None: + mock_input( + [ + Escape, + ] + ) + + add_reading.prompt() + + mock_create_reading.assert_not_called() + + mock_create_review.assert_not_called() diff --git a/tests/cli/test_add_work.py b/tests/cli/test_add_work.py index 8484b5d2..6caeb38b 100644 --- a/tests/cli/test_add_work.py +++ b/tests/cli/test_add_work.py @@ -7,7 +7,8 @@ from booklog.cli import add_work from booklog.repository import api as repository_api from tests.cli.conftest import MockInput -from tests.cli.keys import Down, Enter, Escape +from tests.cli.keys import Escape +from tests.cli.prompt_utils import ConfirmType, enter_text, select_option @pytest.fixture @@ -20,36 +21,57 @@ def author_fixture() -> repository_api.Author: return repository_api.create_author("Richard Laymon") -def enter_author(name: str = "Laymon") -> list[str]: - return [name, Enter] +@pytest.fixture(autouse=True) +def work_fixture(author_fixture: repository_api.Author) -> repository_api.Work: + return repository_api.create_work( + title="The Cellar", + subtitle=None, + year="1980", + kind="Novel", + included_work_slugs=[], + work_authors=[ + repository_api.WorkAuthor( + author_slug=author_fixture.slug, + notes=None, + ) + ], + ) + + +def enter_author(name: str) -> list[str]: + return enter_text(name) def select_author_search_result() -> list[str]: - return [Enter] + return select_option(1) def enter_notes(notes: Optional[str] = None) -> list[str]: - if notes: - return [notes, Enter] - - return [Enter] + return enter_text(notes) def select_kind_novel() -> list[str]: - return [ - Down, - Down, - Down, - Enter, - ] + return select_option(4) + + +def select_kind_collection() -> list[str]: + return select_option(2) + +def select_title_search_result(confirm: ConfirmType) -> list[str]: + return select_option(1, confirm=confirm) -def enter_title(title: str) -> list[str]: - return [title, Enter] + +def enter_title(title: str, confirm: ConfirmType) -> list[str]: + return enter_text(title, confirm=confirm) + + +def enter_included_work_title(title: str) -> list[str]: + return enter_text(title) def enter_year_published(year: str) -> list[str]: - return [year, Enter] + return enter_text(year) def test_calls_create_work( @@ -64,8 +86,7 @@ def test_calls_create_work( *enter_notes(), "n", *select_kind_novel(), - *enter_title("The Cellar"), - "y", + *enter_title("The Cellar", confirm="y"), *enter_year_published("1980"), "n", ] @@ -88,6 +109,45 @@ def test_calls_create_work( ) +def test_calls_create_work_for_collection( + mock_input: MockInput, + author_fixture: repository_api.Author, + mock_create_work: MagicMock, + work_fixture: repository_api.Work, +) -> None: + mock_input( + [ + *enter_author(author_fixture.name[:6]), + *select_author_search_result(), + *enter_notes(), + "n", + *select_kind_collection(), + *enter_title("The Richard Laymon Collection Volume 1", confirm="y"), + *enter_year_published("2006"), + *enter_included_work_title(work_fixture.title), + *select_title_search_result("y"), + "n", + "n", + ] + ) + + add_work.prompt() + + mock_create_work.assert_called_once_with( + title="The Richard Laymon Collection Volume 1", + subtitle=None, + work_authors=[ + repository_api.WorkAuthor( + author_slug=author_fixture.slug, + notes=None, + ) + ], + year="2006", + kind="Collection", + included_work_slugs=[work_fixture.slug], + ) + + def test_can_cancel_out_of_author_name( mock_input: MockInput, mock_create_work: MagicMock ) -> None: diff --git a/tests/exports/test_api.py b/tests/exports/test_api.py index 6695938c..66c3f900 100644 --- a/tests/exports/test_api.py +++ b/tests/exports/test_api.py @@ -112,7 +112,7 @@ def test_exports_reading_timeline_entries( exports_api.export_data() with open( - os.path.join(tmp_path / "exports", "reading-timeline-entries.json"), + os.path.join(tmp_path / "exports", "timeline-entries.json"), "r", ) as output_file: file_content = json.load(output_file)