From 97f1636c57f0572d6e5e7cb4711df85fc400f81e Mon Sep 17 00:00:00 2001 From: yenru0 Date: Tue, 5 May 2026 17:42:51 +0900 Subject: [PATCH] refactoring stage 4 - modify StateManager to accept Language - fix load command - modify create command --- run.py | 280 ++++++++++++++++++++++++++------------------------------- 1 file changed, 127 insertions(+), 153 deletions(-) diff --git a/run.py b/run.py index 3c7afcc..089e389 100755 --- a/run.py +++ b/run.py @@ -72,23 +72,6 @@ def _parse_range_string_list(str_list: list[str]) -> list[int]: return list(result) -def _dispatch_target(target: str) -> tuple[str, str, Language]: - splited = target.split("/") - if len(splited) != 2: - raise ValueError("Invalid target format. Expected format: /.") - - prob_parts = splited[1].rsplit(".", 1) - if len(prob_parts) != 2: - raise ValueError("Invalid target format. Expected format: /.") - - prob_name, ext = prob_parts - lang = Language.convert_ext(ext) - if lang is Language.UNDEFINED: - raise ValueError(f"Unknown language: {ext}") - - return splited[0], prob_name, lang - - @unique class Language(Enum): """ @@ -225,6 +208,27 @@ class Language(Enum): return Language.UNDEFINED +def _dispatch_target(target: str) -> tuple[str, str, Language]: + splited = target.split("/") + if len(splited) != 2: + raise ValueError( + "Invalid target format. Expected format: /." + ) + + prob_parts = splited[1].rsplit(".", 1) + if len(prob_parts) != 2: + raise ValueError( + "Invalid target format. Expected format: /." + ) + + prob_name, ext = prob_parts + lang = Language.convert_ext(ext) + if lang is Language.UNDEFINED: + raise ValueError(f"Unknown language: {ext}") + + return splited[0], prob_name, lang + + class StateManager: _instance: "StateManager | None" = None _state: dict | None = None @@ -252,21 +256,39 @@ class StateManager: with open(STATE_PATH, "w", encoding="utf-8") as f: yaml.safe_dump(self._state, f) - def get_space(self, lang: str) -> dict | None: + def check_space(self, lang: Language) -> bool: state = self._load() - return state.get("space", {}).get(lang) + return lang.value in state.get("space", {}) and bool(state["space"][lang.value]) - def set_space(self, lang: str, data: dict) -> None: + def get_space(self, lang: Language) -> dict: + state = self._load() + if not self.check_space(lang): + raise ValueError(f"No space found for language: {lang}") + return state["space"][lang.value] + + def add_space( + self, lang: Language, file: str, location: str, is_completed: bool + ) -> None: state = self._load() if "space" not in state: state["space"] = {} - state["space"][lang] = data + state["space"][lang.value] = { + "file": file, + "location": location, + "is_completed": is_completed, + } self._state = state - def clear_space(self, lang: str) -> None: + def remove_space(self, lang: Language) -> None: state = self._load() - if "space" in state and lang in state["space"]: - del state["space"][lang] + if "space" in state and lang.value in state["space"]: + state["space"][lang.value] = {} + self._state = state + + def clear_space(self, lang: Language) -> None: + state = self._load() + if "space" in state and lang.value in state["space"]: + del state["space"][lang.value] self._state = state def reload(self) -> None: @@ -319,9 +341,9 @@ class StorageManager: return None def save_file( - self, location: str, lang: str, filename: str, content: bytes, completed: bool + self, location: str, lang: Language, filename: str, content: bytes, completed: bool ) -> None: - path = STORAGE_DIR / location / lang / ("completed" if completed else "") + path = STORAGE_DIR / location / lang.value / ("completed" if completed else "") os.makedirs(path, exist_ok=True) with open(f"{path}/{filename}", "wb") as f: f.write(content) @@ -401,23 +423,59 @@ def register(location: str): @click.command(name="create") @click.argument("target", type=str, nargs=1, required=True) -@click.option("--completed/--no-completed", "-c/-nc", default=False, help="Mark as completed") -def create(target: str, completed: bool): +@click.option( + "--completed/--no-completed", "-c/-nc", default=False, help="Mark as completed" +) +@click.option( + "--no-template", + "-nt", + is_flag=True, + help="Do not use template content (create empty file instead)", +) +@click.option( + "--force", "-f", is_flag=True, help="Overwrite existing file if it already exists" +) +def create(target: str, completed: bool, template: bool, force: bool): """ 지정된 location을 storage에 생성하는 명령어 """ loc, prob, lang = _dispatch_target(target) - storage_manager = StorageManager() - if not storage_manager.check_location(loc): - raise click.ClickException(f"Location '{loc}' is not registered. Please register it first.") + + if not StorageManager().check_location(loc): + raise click.ClickException( + f"Location '{loc}' is not registered. Please register it first." + ) + filename = prob - if storage_manager.check_location_lang(loc, lang): - existing_file = storage_manager.check_get_file(loc, lang, filename) - if existing_file: - click.echo(f"File '{filename}.{lang.value}' already exists in location '{loc}'.") + existing = StorageManager().check_get_file(loc, lang, filename) + if existing: + click.secho( + f"File '{filename}.{lang.value}' already exists in location '{loc}'", + fg="yellow", + bold=True, + ) + if not force: return - storage_manager.save_file(loc, lang.value, f"{filename}.{lang.value}", b"", completed) # create empty file - click.echo(f"Problem '{filename}.{lang.value}' created successfully in location '{loc}' with completed status: {completed}.") + + if not template: + content = b"" + else: + template_path = TEMPLATES_DIR / f"{lang.value}.txt" + if template_path.is_file(): + content = template_path.read_bytes() + click.secho(f"Using template: {template_path}", fg="cyan") + else: + content = b"" + click.secho(f"Template not found: {template_path}", fg="yellow") + + StorageManager().save_file( + loc, lang, f"{filename}.{lang.value}", content, completed + ) + click.secho( + f"Created '{filename}.{lang.value}' in location '{loc}'", + fg="cyan", + bold=True, + ) @click.command(name="run") @@ -436,11 +494,9 @@ def run(lang: str, testcase: list[str], verbose: bool): raise click.ClickException("Undefined Language Exception") # load된 state가 있는지 확인 - space = StateManager().get_space(target_language.value) - - if space is None: + if not StateManager().check_space(target_language): raise click.ClickException( - f"No loaded state for language '{target_language.value}'" + f"No loaded file for language '{target_language.value}'. Please load a file first." ) # 테케 분석 @@ -471,8 +527,10 @@ def run(lang: str, testcase: list[str], verbose: bool): f_in_path = f"{TC_DIR}/{tc}.in" f_out_path = f"{TC_DIR}/{tc}.out" - with open(f_in_path, "r", encoding="utf-8") as f_in, \ - open(f_out_path, "r", encoding="utf-8") as f_out: + with ( + open(f_in_path, "r", encoding="utf-8") as f_in, + open(f_out_path, "r", encoding="utf-8") as f_out, + ): expected = f_out.read().strip() proc = subprocess.Popen( @@ -496,7 +554,9 @@ def run(lang: str, testcase: list[str], verbose: bool): continue flag = stdout.strip() == expected - symbol = click.style("✓", fg="green") if flag else click.style("✗", fg="red") + symbol = ( + click.style("✓", fg="green") if flag else click.style("✗", fg="red") + ) click.echo(f"{tc:02d}: {symbol}") if verbose: @@ -510,127 +570,41 @@ def run(lang: str, testcase: list[str], verbose: bool): @click.command(name="load") -@click.argument("file", type=str, nargs=1, required=True) -@click.option("--to", type=str, default="") # language value -@click.option("--from", "from_", type=str, required=True) +@click.argument("target", type=str, nargs=1, required=True) @click.option("--force", "-f", is_flag=True) -def load(file: str, to: str, from_: str, force: bool): +def load(target: str, force: bool): """ Storage의 파일을 각 언어의 src-space로 이동시키는 명령. + - force는 src-space에 이미 파일이 존재할 때, 덮어쓸지 결정 """ # 파일의 src-space가 어딘지 확인 - to_language: Language - file_name: str - file_sep_ext = file.split(".") - if len(file_sep_ext) == 1: - file_name = file_sep_ext[0] - to_language = Language.convert_name(to) # if blank, undefined - elif len(file_sep_ext) == 2: - file_name = file_sep_ext[0] - to_language = Language.convert_name(file_sep_ext[1]) - else: - file_name = "" - to_language = Language.UNDEFINED + loc, prob, lang = _dispatch_target(target) - if to_language is Language.UNDEFINED: - raise click.UsageError("LoadError: `to` or `file` argument is wrong") - - try: - with open(STATE_PATH, "r", encoding="utf-8") as f_state: - state = yaml.safe_load(f_state) - except FileNotFoundError as e: - raise click.ClickException(e) - except yaml.YAMLError as e: - raise click.ClickException(e) - - if "space" not in state: - raise click.ClickException("state.yml is wrong") - elif not state["space"]: - click.secho("state.space has no member", fg="yellow", bold=True) - state["space"] = {} - - lang_space_state: dict | None - if to_language.value not in state["space"]: - lang_space_state = None - else: - lang_space_state = state["space"][to_language.value] - - if not force: - if lang_space_state is None or not lang_space_state: - pass - elif lang_space_state["file"] is None: - pass - else: + if StateManager().check_space(lang): + if not force: raise click.ClickException( - f"File {lang_space_state['file']}.{to_language.value} already in source space {to_language.value}" + f"Space for language '{lang.value}' is already occupied." ) - # 파일이 존재하는지 확인 - # uncompleted - file_is_exist: bool = False - file_is_completed: bool = False - file_loc: str = None - if os.path.isfile( - f"{STORAGE_DIR}/{from_}/{to_language.value}/{file_name}.{to_language.value}" - ): - file_is_exist = True - file_is_completed = False - file_loc = ( - f"{STORAGE_DIR}/{from_}/{to_language.value}/{file_name}.{to_language.value}" - ) - elif os.path.isfile( - f"{STORAGE_DIR}/{from_}/{to_language.value}/completed/{file_name}.{to_language.value}" - ): - file_is_exist = True - file_is_completed = True - file_loc = f"{STORAGE_DIR}/{from_}/{to_language.value}/completed/{file_name}.{to_language.value}" - # 존재하면 move - # 존재하지 않으면 빈 파일(또는 템플릿 파일)을 생성하고 - # state에 기록함 - if file_is_exist: - with ( - open(file_loc, "rb") as f_src, - open( - f"{SRC_SPACE_DIR}/src-{to_language.value}/src/main.{to_language.value}", - "wb", - ) as f_dst, - ): - f_dst.write(f_src.read()) - os.remove(file_loc) - else: - if TEMPLATES_DIR and os.path.isfile( - f"{TEMPLATES_DIR}/template.{to_language.value}" - ): - with ( - open(f"{TEMPLATES_DIR}/template.{to_language.value}", "rb") as f_src, - open( - f"{SRC_SPACE_DIR}/src-{to_language.value}/src/main.{to_language.value}", - "wb", - ) as f_dst, - ): - f_dst.write(f_src.read()) - else: - with open( - f"{SRC_SPACE_DIR}/src-{to_language.value}/src/main.{to_language.value}", - "wb", - ) as f_dst: - pass + path = StorageManager().check_get_file(loc, lang, prob) - with open("state.yml", "w", encoding="utf-8") as f: - if lang_space_state is None: - state["space"][to_language.value] = {} - state["space"][to_language.value]["file"] = file_name - state["space"][to_language.value]["location"] = from_ - state["space"][to_language.value]["is_completed"] = file_is_completed - - yaml.safe_dump(state, f) - if file_is_exist: - click.echo( - f"File {file_name}.{to_language.value} from {from_}{'/completed' if file_is_completed else ''} is successfully loaded" + if path is None: + raise click.ClickException( + f"File '{prob}.{lang.value}' not found in location '{loc}'. Use 'create' command to create it first." ) - else: - click.echo(f"File {file_name}.{to_language.value} is successfully created") + + dest_path = SRC_SPACE_DIR / f"src-{lang.value}" / "src" / f"main.{lang.value}" + + with open(path, "rb") as f_src, open(dest_path, "wb") as f_dst: + f_dst.write(f_src.read()) + StateManager().remove_space(lang) + StateManager().add_space(lang, prob, loc, False) + click.secho( + f"Loaded '{prob}.{lang.value}' from location '{loc}' to src-space.", + fg="cyan", + bold=True, + ) @click.command(name="export")