From 267a51124fdbbdbbce47e6e83fb16c27dac6ad40 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 26 Aug 2020 13:36:45 +0000 Subject: [PATCH] Get rid of some orange lines emacs was showing me --- evering/__main__.py | 30 ++++++++++-------- evering/colors.py | 16 ++++++++-- evering/config.py | 69 +++++++++++++++++++++++++++--------------- evering/explore.py | 26 +++++++++++----- evering/known_files.py | 23 ++++++++------ evering/parser.py | 47 ++++++++++++++++++++-------- evering/process.py | 67 ++++++++++++++++++++++++++++------------ evering/prompt.py | 2 ++ evering/util.py | 12 ++++++++ 9 files changed, 203 insertions(+), 89 deletions(-) diff --git a/evering/__main__.py b/evering/__main__.py index 0067828..85d380e 100644 --- a/evering/__main__.py +++ b/evering/__main__.py @@ -3,18 +3,18 @@ import logging from pathlib import Path from typing import Any -from .colors import * -from .config import * -from .explore import * -from .known_files import * -from .process import * -from .prompt import * -from .util import * +from .colors import style_path, style_warning +from .config import DEFAULT_CONFIG, Config, ConfigurationException +from .explore import find_config_files +from .known_files import KnownFiles +from .process import Processor +from .prompt import prompt_choice +from .util import CatastrophicError, LessCatastrophicError LOG_STYLE = "{" LOG_FORMAT = "{levelname:>7}: {message}" -#logging.basicConfig(level=logging.DEBUG, style="{", format="{levelname:>7}: {message}") -#logging.basicConfig(level=logging.INFO, style="{", format="{levelname:>7}: {message}") +# logging.basicConfig(level=logging.DEBUG, style="{", format="{levelname:>7}: {message}") +# logging.basicConfig(level=logging.INFO, style="{", format="{levelname:>7}: {message}") logger = logging.getLogger(__name__) HEADER_FILE_SUFFIX = ".evering-header" @@ -62,8 +62,10 @@ Writing problems: - can't write to known files (error) """ + def run(args: Any) -> None: - config = Config.load_config_file(args.config_file and Path(args.config_file) or None) + config = Config.load_config_file(args.config_file + and Path(args.config_file) or None) known_files = KnownFiles(config.known_files) processor = Processor(config, known_files) @@ -75,7 +77,8 @@ def run(args: Any) -> None: except LessCatastrophicError as e: logger.error(e) - if prompt_choice("[C]ontinue to the next file or [A]bort the program?", "Ca") == "a": + if prompt_choice("[C]ontinue to the next file or [A]bort the " + "program?", "Ca") == "a": raise CatastrophicError("Aborted") for path in known_files.find_forgotten_files(): @@ -84,6 +87,7 @@ def run(args: Any) -> None: known_files.save_final() + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("-c", "--config-file", type=Path) @@ -95,7 +99,8 @@ def main() -> None: logging.basicConfig(level=level, style=LOG_STYLE, format=LOG_FORMAT) if args.export_default_config is not None: - logger.info(f"Exporting default config to {style_path(args.export_default_config)}") + logger.info("Exporting default config to " + f"{style_path(args.export_default_config)}") with open(args.export_default_config, "w") as f: f.write(DEFAULT_CONFIG.to_config_file()) return @@ -107,5 +112,6 @@ def main() -> None: except ConfigurationException as e: logger.error(e) + if __name__ == "__main__": main() diff --git a/evering/colors.py b/evering/colors.py index 4cba858..8f148de 100644 --- a/evering/colors.py +++ b/evering/colors.py @@ -5,13 +5,15 @@ escape sequences. from dataclasses import dataclass from pathlib import Path -from typing import Optional, Tuple, Union +from typing import Union __all__ = [ "CSI", "ERASE_LINE", "BOLD", "ITALIC", "UNDERLINE", "Color", - "BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE", "BRIGHT_BLACK", "BRIGHT_RED", "BRIGHT_GREEN", "BRIGHT_YELLOW", "BRIGHT_BLUE", "BRIGHT_MAGENTA", "BRIGHT_CYAN", "BRIGHT_WHITE", + "BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE", + "BRIGHT_BLACK", "BRIGHT_RED", "BRIGHT_GREEN", "BRIGHT_YELLOW", + "BRIGHT_BLUE", "BRIGHT_MAGENTA", "BRIGHT_CYAN", "BRIGHT_WHITE", "style_sequence", "styled", "style_path", "style_var", "style_error", "style_warning", ] @@ -30,11 +32,13 @@ UNDERLINE = 4 # Colors + @dataclass class Color: fg: int bg: int + BLACK = Color(30, 40) RED = Color(31, 41) GREEN = Color(32, 42) @@ -52,28 +56,34 @@ BRIGHT_MAGENTA = Color(95, 105) BRIGHT_CYAN = Color(96, 106) BRIGHT_WHITE = Color(97, 107) + def style_sequence(*args: int) -> str: arglist = ";".join(str(arg) for arg in args) return f"{CSI}{arglist}m" + def styled(text: str, *args: int) -> str: if args: sequence = style_sequence(*args) reset = style_sequence() return f"{sequence}{text}{reset}" else: - return text # No styling necessary + return text # No styling necessary + def style_path(path: Union[str, Path]) -> str: if isinstance(path, Path): path = str(path) return styled(path, BRIGHT_BLACK.fg, BOLD) + def style_var(text: str) -> str: return styled(repr(text), BLUE.fg) + def style_error(text: str) -> str: return styled(text, RED.fg, BOLD) + def style_warning(text: str) -> str: return styled(text, YELLOW.fg, BOLD) diff --git a/evering/config.py b/evering/config.py index fa233ac..bd6e02f 100644 --- a/evering/config.py +++ b/evering/config.py @@ -11,8 +11,9 @@ from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -from .colors import * -from .util import * +from .colors import style_error, style_path, style_var +from .util import (ExecuteException, ReadFileException, copy_local_variables, + get_host, get_user, read_file, safer_exec) __all__ = [ "DEFAULT_LOCATIONS", @@ -28,18 +29,22 @@ DEFAULT_LOCATIONS = [ Path("~/.evering.py"), ] + class ConfigurationException(Exception): pass + @dataclass class DefaultConfigValue: # A short textual description of the value's function description: str # The actual default value value: Any - # Whether this variable even has a default value or the value is set at runtime + # Whether this variable even has a default value or the value is set at + # runtime has_constant_value: bool + class DefaultConfig: def __init__(self) -> None: self._values: Dict[str, DefaultConfigValue] = {} @@ -49,7 +54,7 @@ class DefaultConfig: description: str, value: Any = None, has_constant_value: bool = True - ) -> None: + ) -> None: if name in self._values: raise ConfigurationException(f"Value {name!r} already exists") @@ -60,7 +65,9 @@ class DefaultConfig: return self._values.get(name) def to_local_vars(self) -> Dict[str, Any]: - return {name: d.value for name, d in self._values.items() if d.has_constant_value} + return {name: d.value + for name, d in self._values.items() + if d.has_constant_value} def to_config(self) -> "Config": config = Config(self.to_local_vars()) @@ -70,10 +77,10 @@ class DefaultConfig: def to_config_file(self) -> str: """ - Attempt to convert the DefaultConfig into a format that can be read by a - python interpreter. This assumes that all names are valid variable names - and that the repr() representations of each object can be read by the - interpreter. + Attempt to convert the DefaultConfig into a format that can be read by + a python interpreter. This assumes that all names are valid variable + names and that the repr() representations of each object can be read by + the interpreter. This solution is quite hacky, so use at your own risk :P (At least make sure that this works with all your default values before you use it). @@ -95,6 +102,7 @@ class DefaultConfig: return "\n".join(lines) + "\n" + DEFAULT_CONFIG = DefaultConfig() DEFAULT_CONFIG.add( @@ -120,17 +128,21 @@ DEFAULT_CONFIG.add( DEFAULT_CONFIG.add( "binary", - "When interpreting a header file: When True, the corresponding file is copied directly to the target instead of compiled. Has no effect if a file has no header file", + ("When interpreting a header file: When True, the corresponding file is " + "copied directly to the target instead of compiled. Has no effect if a " + "file has no header file"), value=True) DEFAULT_CONFIG.add( "targets", - "The locations a config file should be placed in. Either a path or a list of paths", + ("The locations a config file should be placed in. Either a path or a " + "list of paths"), value=[]) DEFAULT_CONFIG.add( "action", - "Whether a file should be treated as an action with a certain name. If set, must be a string", + ("Whether a file should be treated as an action with a certain name. If " + "set, must be a string"), has_constant_value=False) DEFAULT_CONFIG.add( @@ -147,12 +159,14 @@ DEFAULT_CONFIG.add( DEFAULT_CONFIG.add( "filename", - "Name of the file currently being compiled, as a string. Set during compilation", + ("Name of the file currently being compiled, as a string. Set during " + "compilation"), has_constant_value=False) DEFAULT_CONFIG.add( "target", - "Location the file is currently being compiled for, as a Path. Set during compilation", + ("Location the file is currently being compiled for, as a Path. Set " + "during compilation"), has_constant_value=False) DEFAULT_CONFIG.add( @@ -165,6 +179,7 @@ DEFAULT_CONFIG.add( "Name of the current computer. Set during compilation", has_constant_value=False) + class Config: @staticmethod def load_config_file(path: Optional[Path]) -> "Config": @@ -183,10 +198,13 @@ class Config: conf = copy break except ConfigurationException as e: - logger.debug(f"Tried default config file at {style_path(path)} and it didn't work: {e}") + logger.debug("Tried default config file at " + f"{style_path(path)} and it didn't work: {e}") else: raise ConfigurationException(style_error( - "No valid config file found in any of the default locations")) + "No valid config file found in any of the default " + "locations" + )) else: # Use the path try: @@ -208,7 +226,7 @@ class Config: May raise: ConfigurationException """ - if not "base_dir" in self.local_vars: + if "base_dir" not in self.local_vars: self.local_vars["base_dir"] = path.parent try: @@ -228,10 +246,11 @@ class Config: May raise: ConfigurationException """ - if not name in self.local_vars: + if name not in self.local_vars: raise ConfigurationException( - style_error(f"Expected a variable named ") + - style_var(name)) + style_error("Expected a variable named ") + + style_var(name) + ) value = self.local_vars[name] @@ -246,7 +265,7 @@ class Config: return value def _get_optional(self, name: str, *types: type) -> Optional[Any]: - if not name in self.local_vars: + if name not in self.local_vars: return None else: return self._get(name, *types) @@ -273,10 +292,12 @@ class Config: def _interpret_path(self, path: Union[str, Path]) -> Path: path = Path(path).expanduser() if path.is_absolute(): - logger.debug(style_path(path) + " is absolute, no interpreting required") + logger.debug(f"{style_path(path)} is absolute, no interpreting " + "required") return path else: - logger.debug(style_path(path) + " is relative, interpreting as " + style_path(self.base_dir / path)) + logger.debug(f"{style_path(path)} is relative, interpreting as " + f"{style_path(self.base_dir / path)}") return self.base_dir / path @property @@ -353,7 +374,7 @@ class Config: if len(delimiters[0]) < 1 or len(delimiters[1]) < 1: raise ConfigurationException( style_error("Expected both strings in variable ") + - style_var(name) + style_error( "to be of length >= 1")) + style_var(name) + style_error("to be of length >= 1")) return delimiters diff --git a/evering/explore.py b/evering/explore.py index 931377e..d7569fa 100644 --- a/evering/explore.py +++ b/evering/explore.py @@ -3,28 +3,33 @@ from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional -from .colors import * -from .util import * +from .colors import style_error, style_path, style_warning +from .util import CatastrophicError __all__ = ["FileInfo", "find_config_files"] logger = logging.getLogger(__name__) HEADER_FILE_SUFFIX = ".evering-header" + @dataclass class FileInfo: path: Path header: Optional[Path] = None + def find_config_files(config_dir: Path) -> List[FileInfo]: try: return explore_dir(config_dir) except OSError as e: - raise CatastrophicError(style_error("could not access config dir ") + style_path(config_dir) + f": {e}") + raise CatastrophicError(style_error("could not access config dir ") + + style_path(config_dir) + f": {e}") + def explore_dir(cur_dir: Path) -> List[FileInfo]: if not cur_dir.is_dir(): - raise CatastrophicError(style_path(cur_dir) + style_error(" is not a directory")) + raise CatastrophicError(style_path(cur_dir) + + style_error(" is not a directory")) files: Dict[Path, FileInfo] = {} header_files: List[Path] = [] @@ -47,13 +52,17 @@ def explore_dir(cur_dir: Path) -> List[FileInfo]: # 2. Assign the header files to their respective files for header_file in header_files: - matching_file = header_file.with_suffix("") # Remove last suffix + matching_file = header_file.with_suffix("") # Remove last suffix matching_file_info = files.get(matching_file) if matching_file_info is None: - logger.warning(style_warning("No corresponding file for header file ") + style_path(header_file)) + logger.warning( + style_warning("No corresponding file for header file ") + + style_path(header_file) + ) else: - logger.debug(f"Assigned header file {style_path(header_file)} to file {style_path(matching_file)}") + logger.debug(f"Assigned header file {style_path(header_file)} to " + f"file {style_path(matching_file)}") matching_file_info.header = header_file # 3. Collect the resulting FileInfos @@ -64,6 +73,7 @@ def explore_dir(cur_dir: Path) -> List[FileInfo]: try: result.extend(explore_dir(subdir)) except OSError as e: - logger.warning(style_warning("Could not descend into folder ") + style_path(subdir) + f": {e}") + logger.warning(style_warning("Could not descend into folder ") + + style_path(subdir) + f": {e}") return result diff --git a/evering/known_files.py b/evering/known_files.py index abeb937..63bd094 100644 --- a/evering/known_files.py +++ b/evering/known_files.py @@ -1,14 +1,15 @@ import json import logging from pathlib import Path -from typing import Dict, List, Optional, Set +from typing import Dict, Optional, Set -from .colors import * -from .util import * +from .colors import style_error, style_path +from .util import CatastrophicError, WriteFileException, write_file __all__ = ["KnownFiles"] logger = logging.getLogger(__name__) + class KnownFiles: def __init__(self, path: Path) -> None: self._path = path @@ -18,7 +19,7 @@ class KnownFiles: try: with open(self._path) as f: self._old_known_files = self._read_known_files(f.read()) - except FileNotFoundError as e: + except FileNotFoundError: logger.debug(f"File {style_path(self._path)} does not exist, " "creating a new file on the first upcoming save") @@ -30,13 +31,16 @@ class KnownFiles: raw_known_files = json.loads(text) if not isinstance(raw_known_files, dict): - raise CatastrophicError(style_error("Root level structure is not a dictionary")) + raise CatastrophicError(style_error( + "Root level structure is not a dictionary")) for path, file_hash in raw_known_files.items(): if not isinstance(path, str): - raise CatastrophicError(style_error(f"Path {path!r} is not a string")) + raise CatastrophicError(style_error( + f"Path {path!r} is not a string")) if not isinstance(file_hash, str): - raise CatastrophicError(style_error(f"Hash {hash!r} at path {path!r} is not a string")) + raise CatastrophicError(style_error( + f"Hash {hash!r} at path {path!r} is not a string")) path = self._normalize_path(Path(path)) known_files[path] = file_hash @@ -61,7 +65,8 @@ class KnownFiles: def save_incremental(self) -> None: to_save: Dict[str, str] = {} - for path in self._old_known_files.keys() | self._new_known_files.keys(): + paths = self._old_known_files.keys() | self._new_known_files.keys() + for path in paths: if path in self._new_known_files: to_save[str(path)] = self._new_known_files[path] else: @@ -93,7 +98,7 @@ class KnownFiles: try: write_file(path, text) - path.replace(self._path) # Assumed to be atomic + path.replace(self._path) # Assumed to be atomic except (WriteFileException, OSError) as e: raise CatastrophicError( style_error("Error saving known files to ") + diff --git a/evering/parser.py b/evering/parser.py index 5b9fb1e..a37dd05 100644 --- a/evering/parser.py +++ b/evering/parser.py @@ -1,7 +1,7 @@ from abc import ABC from typing import Any, Dict, List, Optional, Tuple, Union -from .util import * +from .util import safer_eval """ This parsing solution has the following structure: @@ -9,7 +9,7 @@ This parsing solution has the following structure: 1. Separate header and config file content, if necessary 2. Split up text into lines, if still necessary 3. Parse each line individually -4. Use a recursive descent approach to group the lines into blocks and if-blocks +4. Use recursive descent approach to group the lines into blocks and if-blocks 5. Evaluate the blocks recursively """ @@ -18,6 +18,7 @@ __all__ = [ "ParseException", "Parser", ] + def split_header_and_rest(text: str) -> Tuple[List[str], List[str]]: lines = text.splitlines() @@ -38,18 +39,20 @@ def split_header_and_rest(text: str) -> Tuple[List[str], List[str]]: return header, rest + class ParseException(Exception): @classmethod def on_line(cls, line: "Line", text: str) -> "ParseException": return ParseException(f"Line {line.line_number}: {text}") + class Parser: def __init__(self, raw_lines: List[str], statement_prefix: str, expression_prefix: str, expression_suffix: str, - ) -> None: + ) -> None: """ May raise: ParseException """ @@ -71,6 +74,7 @@ class Parser: lines = self.main_block.evaluate(local_vars) return "".join(f"{line}\n" for line in lines) + # Line parsing (inline expressions) class Line(ABC): @@ -102,7 +106,10 @@ class Line(ABC): self.parser = parser self.line_number = line_number - def _parse_statement(self, text: str, statement_name: str) -> Optional[str]: + def _parse_statement(self, + text: str, + statement_name: str + ) -> Optional[str]: start = f"{self.parser.statement_prefix} {statement_name}" text = text.strip() if text.startswith(start): @@ -111,7 +118,9 @@ class Line(ABC): return None def _parse_statement_noarg(self, text: str, statement_name: str) -> bool: - return text.strip() == f"{self.parser.statement_prefix} {statement_name}" + target = f"{self.parser.statement_prefix} {statement_name}" + return text.strip() == target + class ActualLine(Line): def __init__(self, parser: Parser, text: str, line_number: int) -> None: @@ -143,13 +152,17 @@ class ActualLine(Line): od = text.find(self.parser.expression_prefix, i) if od == -1: chunks.append((text[i:], False)) - break # We've consumed the entire string. + break # We've consumed the entire string. od_end = od + len(self.parser.expression_prefix) # Find expression suffix cd = text.find(self.parser.expression_suffix, od_end) if cd == -1: - raise ParseException.on_line(self, f"No matching expression suffix\n{text[:od_end]} <-- to THIS expression prefix") + raise ParseException.on_line( + self, + f"No matching expression suffix\n{text[:od_end]} " + "<-- to THIS expression prefix" + ) cd_end = cd + len(self.parser.expression_suffix) # Split up into chunks @@ -164,12 +177,13 @@ class ActualLine(Line): May raise: ExecuteException """ - return "".join(self._evaluate_chunk(chunk, local_vars) for chunk in self.chunks) + return "".join(self._evaluate_chunk(chunk, local_vars) + for chunk in self.chunks) def _evaluate_chunk(self, chunk: Tuple[str, bool], local_vars: Dict[str, Any], - ) -> str: + ) -> str: """ May raise: ExecuteException """ @@ -179,6 +193,7 @@ class ActualLine(Line): return str(safer_eval(chunk[0], local_vars)) + class IfStatement(Line): def __init__(self, parser: Parser, text: str, line_number: int) -> None: """ @@ -191,6 +206,7 @@ class IfStatement(Line): if self.argument is None: raise ParseException.on_line(self, "Not an 'if' statement") + class ElifStatement(Line): def __init__(self, parser: Parser, text: str, line_number: int) -> None: """ @@ -203,6 +219,7 @@ class ElifStatement(Line): if self.argument is None: raise ParseException.on_line(self, "Not an 'elif' statement") + class ElseStatement(Line): def __init__(self, parser: Parser, text: str, line_number: int) -> None: """ @@ -214,6 +231,7 @@ class ElseStatement(Line): if not self._parse_statement_noarg(text, "else"): raise ParseException.on_line(self, "Not an 'else' statement") + class EndifStatement(Line): def __init__(self, parser: Parser, text: str, line_number: int) -> None: """ @@ -225,6 +243,7 @@ class EndifStatement(Line): if not self._parse_statement_noarg(text, "endif"): raise ParseException.on_line(self, "Not an 'endif' statement") + # Block parsing class Block: @@ -236,7 +255,7 @@ class Block: self._elements: List[Union[ActualLine, IfBlock]] = [] while lines_queue: - next_line = lines_queue[-1] # Peek + next_line = lines_queue[-1] # Peek if isinstance(next_line, ActualLine): lines_queue.pop() self._elements.append(next_line) @@ -259,6 +278,7 @@ class Block: return lines + class IfBlock(Block): def __init__(self, parser: Parser, lines_queue: List[Line]) -> None: """ @@ -268,18 +288,19 @@ class IfBlock(Block): self._sections: List[Tuple[Block, Optional[str]]] = [] if not lines_queue: - raise ParseException("Unexpected end of file, expected 'if' statement") + raise ParseException("Unexpected end of file, expected 'if' " + "statement") # If statement # # This is the short version: - # if not isinstance(lines_queue[-1], IfStatement): # This should never happen + # if not isinstance(lines_queue[-1], IfStatement): # Should never happen # raise ParseException.on_line(lines_queue[-1], "Expected 'if' statement") # self._sections.append((Block(parser, lines_queue), lines_queue.pop().argument)) # # And this the long version, which mypy understands without errors: next_statement = lines_queue[-1] - if not isinstance(next_statement, IfStatement): # This should never happen + if not isinstance(next_statement, IfStatement): # Should never happen raise ParseException.on_line(next_statement, "Expected 'if' statement") lines_queue.pop() self._sections.append((Block(parser, lines_queue), next_statement.argument)) diff --git a/evering/process.py b/evering/process.py index 348c452..659c198 100644 --- a/evering/process.py +++ b/evering/process.py @@ -4,22 +4,27 @@ import shutil from pathlib import Path from typing import List, Optional -from .colors import * -from .config import * -from .known_files import * -from .parser import * -from .prompt import * -from .util import * +from .colors import style_error, style_path, style_warning +from .config import Config +from .known_files import KnownFiles +from .parser import ParseException, Parser, split_header_and_rest +from .prompt import prompt_yes_no +from .util import (ExecuteException, LessCatastrophicError, ReadFileException, + WriteFileException, read_file, safer_exec, write_file) __all__ = ["Processor"] logger = logging.getLogger(__name__) + class Processor: def __init__(self, config: Config, known_files: KnownFiles) -> None: self.config = config self.known_files = known_files - def process_file(self, path: Path, header_path: Optional[Path] = None) -> None: + def process_file(self, + path: Path, + header_path: Optional[Path] = None + ) -> None: logger.info(f"{style_path(path)}:") config = self.config.copy() @@ -51,7 +56,11 @@ class Processor: self._process_parseable(lines, config, path) - def _process_file_with_header(self, path: Path, header_path: Path, config: Config) -> None: + def _process_file_with_header(self, + path: Path, + header_path: Path, + config: Config + ) -> None: logger.debug(f"Processing file {style_path(path)} " f"with header {style_path(header_path)}") @@ -80,7 +89,7 @@ class Processor: self._process_parseable(lines, config, path) def _process_binary(self, path: Path, config: Config) -> None: - logger.debug(f"Processing as a binary file") + logger.debug("Processing as a binary file") if not config.targets: logger.info(" (no targets)") @@ -96,7 +105,10 @@ class Processor: try: target.parent.mkdir(parents=True, exist_ok=True) except IOError as e: - logger.warning(style_warning("Could not create target directory") + f": {e}") + logger.warning( + style_warning("Could not create target directory") + + f": {e}" + ) continue try: @@ -108,11 +120,16 @@ class Processor: try: shutil.copymode(path, target) except shutil.Error as e: - logger.warning(style_warning("Could not copy permissions") + f": {e}") + logger.warning(style_warning("Could not copy permissions") + + f": {e}") self._update_known_hash(target) - def _process_parseable(self, lines: List[str], config: Config, source: Path) -> None: + def _process_parseable(self, + lines: List[str], + config: Config, + source: Path + ) -> None: if not config.targets: logger.info(" (no targets)") return @@ -149,19 +166,24 @@ class Processor: try: target.parent.mkdir(parents=True, exist_ok=True) except IOError as e: - logger.warning(style_warning("Could not create target directory") + f": {e}") + logger.warning( + style_warning("Could not create target directory") + + f": {e}" + ) continue try: write_file(target, text) except WriteFileException as e: - logger.warning(style_warning("Could not write to target") + f": {e}") + logger.warning(style_warning("Could not write to target") + + f": {e}") continue try: shutil.copymode(source, target) except shutil.Error as e: - logger.warning(style_warning("Could not copy permissions") + f": {e}") + logger.warning(style_warning("Could not copy permissions") + + f": {e}") self._update_known_hash(target) @@ -174,7 +196,8 @@ class Processor: with open(path, "rb") as f: while True: block = f.read(BLOCK_SIZE) - if not block: break + if not block: + break h.update(block) return h.hexdigest() @@ -191,16 +214,19 @@ class Processor: return False if self.known_files.was_recently_modified(target): - logger.warning(style_warning("This target was already overwritten earlier")) + logger.warning(style_warning("This target was already overwritten " + "earlier")) return False target_hash = self._obtain_hash(target) if target_hash is None: - return prompt_yes_no("Overwriting a file that could not be hashed, continue?", False) + return prompt_yes_no("Overwriting a file that could not be " + "hashed, continue?", False) known_target_hash = self.known_files.get_hash(target) if known_target_hash is None: - return prompt_yes_no("Overwriting an unknown file, continue?", False) + return prompt_yes_no("Overwriting an unknown file, continue?", + False) # The following condition is phrased awkwardly because I just # feel better if the final statement in this function is not a @@ -212,7 +238,8 @@ class Processor: # last seen it. return True - return prompt_yes_no("Overwriting a file that was modified since it was last overwritten, continue?", False) + return prompt_yes_no("Overwriting a file that was modified since it " + "was last overwritten, continue?", False) def _update_known_hash(self, target: Path) -> None: target_hash = self._obtain_hash(target) diff --git a/evering/prompt.py b/evering/prompt.py index e78cb89..1c9cf8d 100644 --- a/evering/prompt.py +++ b/evering/prompt.py @@ -2,6 +2,7 @@ from typing import Optional __all__ = ["prompt_choice", "prompt_yes_no"] + def prompt_choice(question: str, options: str) -> str: default_option = None for char in options: @@ -22,6 +23,7 @@ def prompt_choice(question: str, options: str) -> str: else: print(f"Invalid answer, please choose one of [{option_string}].") + def prompt_yes_no(question: str, default_answer: Optional[bool]) -> bool: if default_answer is None: options = "yn" diff --git a/evering/util.py b/evering/util.py index 252a652..4d43b9d 100644 --- a/evering/util.py +++ b/evering/util.py @@ -14,6 +14,7 @@ __all__ = [ "CatastrophicError", "LessCatastrophicError", ] + def copy_local_variables(local: Dict[str, Any]) -> Dict[str, Any]: """ Attempts to deep-copy a set of local variables, but keeping @@ -33,15 +34,19 @@ def copy_local_variables(local: Dict[str, Any]) -> Dict[str, Any]: return local_copy + def get_user() -> str: return getpass.getuser() + def get_host() -> str: return socket.gethostname() + class ExecuteException(Exception): pass + def safer_exec(code: str, local_vars: Dict[str, Any]) -> None: """ May raise: ExecuteException @@ -52,6 +57,7 @@ def safer_exec(code: str, local_vars: Dict[str, Any]) -> None: except Exception as e: raise ExecuteException(e) + def safer_eval(code: str, local_vars: Dict[str, Any]) -> Any: """ May raise: ExecuteException @@ -62,9 +68,11 @@ def safer_eval(code: str, local_vars: Dict[str, Any]) -> Any: except Exception as e: raise ExecuteException(e) + class ReadFileException(Exception): pass + def read_file(path: Path) -> str: """ May raise: ReadFileException @@ -76,9 +84,11 @@ def read_file(path: Path) -> str: except OSError as e: raise ReadFileException(e) + class WriteFileException(Exception): pass + def write_file(path: Path, text: str) -> None: """ May raise: WriteFileException @@ -90,8 +100,10 @@ def write_file(path: Path, text: str) -> None: except OSError as e: raise WriteFileException(e) + class CatastrophicError(Exception): pass + class LessCatastrophicError(Exception): pass