Get rid of some orange lines emacs was showing me

This commit is contained in:
Joscha 2020-08-26 13:36:45 +00:00
parent 321ab89b18
commit 267a51124f
9 changed files with 203 additions and 89 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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 ") +

View file

@ -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))

View file

@ -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)

View file

@ -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"

View file

@ -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