From 2ed8c507d7c76780592b7b8b64944f26de84bb4b Mon Sep 17 00:00:00 2001 From: tomasvana10 Date: Sun, 28 Apr 2024 21:38:14 +1000 Subject: [PATCH] Improve crossword/crossword category viewing logic Reduced the possibility of errors when viewing/loading crossword categories and crosswords. Additionally, some methods were added to utils.py to help the program infer the contents of an info.json file for a crossword directory with an empty info.json file, as well as randomly generate an info.json file for bare crossword category directories. This will allow the user more easily to create their own crosswords (if they really want to). --- crossword_puzzle/errors.py | 4 +-- crossword_puzzle/main.py | 49 +++++++++++++++++++++++++++---------- crossword_puzzle/utils.py | 50 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/crossword_puzzle/errors.py b/crossword_puzzle/errors.py index 728f4a1ba..cb806950f 100644 --- a/crossword_puzzle/errors.py +++ b/crossword_puzzle/errors.py @@ -1,6 +1,4 @@ -class CrosswordBaseError(Exception): ... # Useful in error handling to see if - # an error is related to crossword - # generation +class CrosswordBaseError(Exception): ... class EmptyDefinitions(CrosswordBaseError): diff --git a/crossword_puzzle/main.py b/crossword_puzzle/main.py index b342d1d5d..129f0954a 100644 --- a/crossword_puzzle/main.py +++ b/crossword_puzzle/main.py @@ -41,7 +41,6 @@ ) from crossword_puzzle.cword_gen import Crossword from crossword_puzzle.cword_webapp.app import _create_app_process, terminate_app -from crossword_puzzle.errors import CrosswordBaseError from crossword_puzzle.utils import ( _get_colour_palette_for_webapp, _get_language_options, @@ -49,6 +48,8 @@ _update_config, find_best_crossword, load_definitions, + _make_cword_info_json, + _make_category_info_json ) @@ -525,7 +526,7 @@ def terminate_cword_webapp(self) -> None: terminate_app() def _rollback_states(self) -> None: - # User might not have selected a crossword within their category, so + # User might not have selected a crossword within their category, so # this if condition is requried. if hasattr(self, "selected_category_object"): self.selected_category_object.b_close_category.configure( @@ -593,11 +594,8 @@ def load_selected_cword(self) -> None: ) ) except Exception as ex: - if issubclass(type(ex), CrosswordBaseError): - print(f"{type(ex).__name__}: {ex}") - return AppHelper.show_messagebox(cword_gen_err=True) - else: - return print(f"{type(ex).__name__}: {ex}") + print(f"{type(ex).__name__}: {ex}") + return AppHelper.show_messagebox(cword_gen_err=True) # Only modify states after error checking has been conducted self.b_load_selected_cword.configure(state="disabled") @@ -724,6 +722,13 @@ def _generate_crossword_category_blocks(self) -> None: for category in [ f for f in scandir(Paths.BASE_CWORDS_PATH) if f.is_dir() ]: + # Make the ``info.json`` file if it doesn't exist already + if ( + "info.json" not in listdir(category.path) + or path.getsize(path.join(category.path, "info.json")) <= 0 + ): + _make_category_info_json(path.join(category.path, "info.json")) + block = CrosswordCategoryBlock( self.info_block_container, self, category.name, i ) @@ -816,6 +821,8 @@ def __init__( self.category = category self.value = value + # Represents the currently selected crossword radiobutton selector + # within the crosswords of this category (once open) self.selected_block = IntVar() self.selected_block.set(-1) @@ -867,10 +874,15 @@ def _place_content(self) -> None: ) def _get_colour_tag_hex(self) -> str: + """Get the hex colour of the bottom tag of this category, read from + its ``info.json`` file.""" with open( path.join(Paths.BASE_CWORDS_PATH, self.category, "info.json") ) as file: - return load(file)["bottom_tag_colour"] + try: + return load(file)["bottom_tag_colour"] + except: + return "#abcdef" def _sort_category_content(self, arr: list[str]) -> list[str]: """Sort the cword content of a category by the cword suffixes (-easy @@ -880,10 +892,11 @@ def _sort_category_content(self, arr: list[str]) -> list[str]: return sorted( arr, key=lambda i: CrosswordDifficulties.DIFFICULTIES.index( - i.split("-")[-1].capitalize() + i.name.split("-")[-1].capitalize() ), ) - except Exception: + except Exception: # Could not find the "-" in the crossword name, so + # don't sort this category return arr def _configure_cword_blocks_state( @@ -898,6 +911,8 @@ def _configure_cword_blocks_state( def _view_category(self) -> None: """View all crossword info blocks for a specific category.""" self.b_view_category.configure(state="disabled") + # Reset crossword canvas xview so it is at the beginning + self.master.info_block_container._parent_canvas.xview("moveto", 0.0) for block in self.master.category_block_objects: # Remove all category # blocks block.pack_forget() @@ -909,16 +924,24 @@ def _view_category(self) -> None: self.cword_block_objects: list[CrosswordInfoBlock] = [] # Gather all crossword directories with an info.json file. crosswords = [ - f.name + f for f in scandir(path.join(Paths.BASE_CWORDS_PATH, self.category)) - if f.is_dir() and "info.json" in listdir(f.path) + if f.is_dir() + and "definitions.json" in listdir(f.path) + and path.getsize(path.join(f.path, "definitions.json")) > 0 ] + i: int = 1 for cword in self._sort_category_content(crosswords): + # Make the ``info.json`` file if it doesn't exist already + info_path = path.join(cword.path, "info.json") + if not path.exists(info_path) or path.getsize(info_path) <= 0: + _make_cword_info_json(cword.path, cword.name, self.category) + block = CrosswordInfoBlock( self.master.info_block_container, self.master, - cword, + cword.name, self.category, self, i, diff --git a/crossword_puzzle/utils.py b/crossword_puzzle/utils.py index 7d39b52e9..584d2b81d 100644 --- a/crossword_puzzle/utils.py +++ b/crossword_puzzle/utils.py @@ -3,12 +3,13 @@ from __future__ import annotations from configparser import ConfigParser -from json import load +from json import load, dump from os import path, scandir +from random import randint from babel import Locale -from crossword_puzzle.constants import Colour, Paths +from crossword_puzzle.constants import Colour, Paths, CrosswordDifficulties def _update_config( @@ -154,5 +155,50 @@ def _load_attempts_db() -> dict[str, int]: integral to the crossword optimisation process, as crossword generation time scales logarithmically with word count. """ + with open(Paths.ATTEMPTS_DB_PATH) as file: return load(file) + + +def _make_cword_info_json(path_, cword_name, category) -> None: + """Make an info.json file for a given crossword since it does not exist. + Makes it easier for the end-user to make their own crossword if they + really want to. + """ + + with open(path.join(path_, "info.json"), "w") as info_obj, \ + open(path.join(path_, "definitions.json"), "r") as def_obj: + total_definitions: int = len(load(def_obj)) + + # Infer the difficulty and crossword name if possible + try: + parsed_cword_name_components = path.basename(path_).split("-") + difficulty: int = CrosswordDifficulties.DIFFICULTIES.index( + parsed_cword_name_components[-1].title() + ) + adjusted_cword_name = "-".join(parsed_cword_name_components[0:-1]) + except Exception: + difficulty: int = 0 + adjusted_cword_name = cword_name + + return dump( + { + "total_definitions": total_definitions, + "difficulty": difficulty, + "symbol": "0x2717", + "name": adjusted_cword_name, + "translated_name": "", + "category": category, + }, + info_obj, + indent=4, + ) + + +def _make_category_info_json(path_) -> None: + """Write a new info.json to a category since it does not exist in the a + category's directory. + """ + hex_ = "#%06X" % randint(0, 0xFFFFFF) + with open(path_, "w") as f: + return dump({"bottom_tag_colour": hex_}, f, indent=4)