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 pathlib import Path
from typing import Any from typing import Any
from .colors import * from .colors import style_path, style_warning
from .config import * from .config import DEFAULT_CONFIG, Config, ConfigurationException
from .explore import * from .explore import find_config_files
from .known_files import * from .known_files import KnownFiles
from .process import * from .process import Processor
from .prompt import * from .prompt import prompt_choice
from .util import * from .util import CatastrophicError, LessCatastrophicError
LOG_STYLE = "{" LOG_STYLE = "{"
LOG_FORMAT = "{levelname:>7}: {message}" LOG_FORMAT = "{levelname:>7}: {message}"
#logging.basicConfig(level=logging.DEBUG, 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}") # logging.basicConfig(level=logging.INFO, style="{", format="{levelname:>7}: {message}")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HEADER_FILE_SUFFIX = ".evering-header" HEADER_FILE_SUFFIX = ".evering-header"
@ -62,8 +62,10 @@ Writing problems:
- can't write to known files (error) - can't write to known files (error)
""" """
def run(args: Any) -> None: 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) known_files = KnownFiles(config.known_files)
processor = Processor(config, known_files) processor = Processor(config, known_files)
@ -75,7 +77,8 @@ def run(args: Any) -> None:
except LessCatastrophicError as e: except LessCatastrophicError as e:
logger.error(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") raise CatastrophicError("Aborted")
for path in known_files.find_forgotten_files(): for path in known_files.find_forgotten_files():
@ -84,6 +87,7 @@ def run(args: Any) -> None:
known_files.save_final() known_files.save_final()
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-c", "--config-file", type=Path) 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) logging.basicConfig(level=level, style=LOG_STYLE, format=LOG_FORMAT)
if args.export_default_config is not None: 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: with open(args.export_default_config, "w") as f:
f.write(DEFAULT_CONFIG.to_config_file()) f.write(DEFAULT_CONFIG.to_config_file())
return return
@ -107,5 +112,6 @@ def main() -> None:
except ConfigurationException as e: except ConfigurationException as e:
logger.error(e) logger.error(e)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -5,13 +5,15 @@ escape sequences.
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple, Union from typing import Union
__all__ = [ __all__ = [
"CSI", "ERASE_LINE", "CSI", "ERASE_LINE",
"BOLD", "ITALIC", "UNDERLINE", "BOLD", "ITALIC", "UNDERLINE",
"Color", "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_sequence", "styled",
"style_path", "style_var", "style_error", "style_warning", "style_path", "style_var", "style_error", "style_warning",
] ]
@ -30,11 +32,13 @@ UNDERLINE = 4
# Colors # Colors
@dataclass @dataclass
class Color: class Color:
fg: int fg: int
bg: int bg: int
BLACK = Color(30, 40) BLACK = Color(30, 40)
RED = Color(31, 41) RED = Color(31, 41)
GREEN = Color(32, 42) GREEN = Color(32, 42)
@ -52,10 +56,12 @@ BRIGHT_MAGENTA = Color(95, 105)
BRIGHT_CYAN = Color(96, 106) BRIGHT_CYAN = Color(96, 106)
BRIGHT_WHITE = Color(97, 107) BRIGHT_WHITE = Color(97, 107)
def style_sequence(*args: int) -> str: def style_sequence(*args: int) -> str:
arglist = ";".join(str(arg) for arg in args) arglist = ";".join(str(arg) for arg in args)
return f"{CSI}{arglist}m" return f"{CSI}{arglist}m"
def styled(text: str, *args: int) -> str: def styled(text: str, *args: int) -> str:
if args: if args:
sequence = style_sequence(*args) sequence = style_sequence(*args)
@ -64,16 +70,20 @@ def styled(text: str, *args: int) -> str:
else: else:
return text # No styling necessary return text # No styling necessary
def style_path(path: Union[str, Path]) -> str: def style_path(path: Union[str, Path]) -> str:
if isinstance(path, Path): if isinstance(path, Path):
path = str(path) path = str(path)
return styled(path, BRIGHT_BLACK.fg, BOLD) return styled(path, BRIGHT_BLACK.fg, BOLD)
def style_var(text: str) -> str: def style_var(text: str) -> str:
return styled(repr(text), BLUE.fg) return styled(repr(text), BLUE.fg)
def style_error(text: str) -> str: def style_error(text: str) -> str:
return styled(text, RED.fg, BOLD) return styled(text, RED.fg, BOLD)
def style_warning(text: str) -> str: def style_warning(text: str) -> str:
return styled(text, YELLOW.fg, BOLD) return styled(text, YELLOW.fg, BOLD)

View file

@ -11,8 +11,9 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from .colors import * from .colors import style_error, style_path, style_var
from .util import * from .util import (ExecuteException, ReadFileException, copy_local_variables,
get_host, get_user, read_file, safer_exec)
__all__ = [ __all__ = [
"DEFAULT_LOCATIONS", "DEFAULT_LOCATIONS",
@ -28,18 +29,22 @@ DEFAULT_LOCATIONS = [
Path("~/.evering.py"), Path("~/.evering.py"),
] ]
class ConfigurationException(Exception): class ConfigurationException(Exception):
pass pass
@dataclass @dataclass
class DefaultConfigValue: class DefaultConfigValue:
# A short textual description of the value's function # A short textual description of the value's function
description: str description: str
# The actual default value # The actual default value
value: Any 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 has_constant_value: bool
class DefaultConfig: class DefaultConfig:
def __init__(self) -> None: def __init__(self) -> None:
self._values: Dict[str, DefaultConfigValue] = {} self._values: Dict[str, DefaultConfigValue] = {}
@ -60,7 +65,9 @@ class DefaultConfig:
return self._values.get(name) return self._values.get(name)
def to_local_vars(self) -> Dict[str, Any]: 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": def to_config(self) -> "Config":
config = Config(self.to_local_vars()) config = Config(self.to_local_vars())
@ -70,10 +77,10 @@ class DefaultConfig:
def to_config_file(self) -> str: def to_config_file(self) -> str:
""" """
Attempt to convert the DefaultConfig into a format that can be read by a Attempt to convert the DefaultConfig into a format that can be read by
python interpreter. This assumes that all names are valid variable names a python interpreter. This assumes that all names are valid variable
and that the repr() representations of each object can be read by the names and that the repr() representations of each object can be read by
interpreter. the interpreter.
This solution is quite hacky, so use at your own risk :P (At least make 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). sure that this works with all your default values before you use it).
@ -95,6 +102,7 @@ class DefaultConfig:
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"
DEFAULT_CONFIG = DefaultConfig() DEFAULT_CONFIG = DefaultConfig()
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
@ -120,17 +128,21 @@ DEFAULT_CONFIG.add(
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
"binary", "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) value=True)
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
"targets", "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=[]) value=[])
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
"action", "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) has_constant_value=False)
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
@ -147,12 +159,14 @@ DEFAULT_CONFIG.add(
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
"filename", "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) has_constant_value=False)
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
"target", "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) has_constant_value=False)
DEFAULT_CONFIG.add( DEFAULT_CONFIG.add(
@ -165,6 +179,7 @@ DEFAULT_CONFIG.add(
"Name of the current computer. Set during compilation", "Name of the current computer. Set during compilation",
has_constant_value=False) has_constant_value=False)
class Config: class Config:
@staticmethod @staticmethod
def load_config_file(path: Optional[Path]) -> "Config": def load_config_file(path: Optional[Path]) -> "Config":
@ -183,10 +198,13 @@ class Config:
conf = copy conf = copy
break break
except ConfigurationException as e: 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: else:
raise ConfigurationException(style_error( 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: else:
# Use the path # Use the path
try: try:
@ -208,7 +226,7 @@ class Config:
May raise: ConfigurationException 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 self.local_vars["base_dir"] = path.parent
try: try:
@ -228,10 +246,11 @@ class Config:
May raise: ConfigurationException May raise: ConfigurationException
""" """
if not name in self.local_vars: if name not in self.local_vars:
raise ConfigurationException( raise ConfigurationException(
style_error(f"Expected a variable named ") + style_error("Expected a variable named ") +
style_var(name)) style_var(name)
)
value = self.local_vars[name] value = self.local_vars[name]
@ -246,7 +265,7 @@ class Config:
return value return value
def _get_optional(self, name: str, *types: type) -> Optional[Any]: 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 return None
else: else:
return self._get(name, *types) return self._get(name, *types)
@ -273,10 +292,12 @@ class Config:
def _interpret_path(self, path: Union[str, Path]) -> Path: def _interpret_path(self, path: Union[str, Path]) -> Path:
path = Path(path).expanduser() path = Path(path).expanduser()
if path.is_absolute(): 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 return path
else: 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 return self.base_dir / path
@property @property
@ -353,7 +374,7 @@ class Config:
if len(delimiters[0]) < 1 or len(delimiters[1]) < 1: if len(delimiters[0]) < 1 or len(delimiters[1]) < 1:
raise ConfigurationException( raise ConfigurationException(
style_error("Expected both strings in variable ") + 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 return delimiters

View file

@ -3,28 +3,33 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from .colors import * from .colors import style_error, style_path, style_warning
from .util import * from .util import CatastrophicError
__all__ = ["FileInfo", "find_config_files"] __all__ = ["FileInfo", "find_config_files"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HEADER_FILE_SUFFIX = ".evering-header" HEADER_FILE_SUFFIX = ".evering-header"
@dataclass @dataclass
class FileInfo: class FileInfo:
path: Path path: Path
header: Optional[Path] = None header: Optional[Path] = None
def find_config_files(config_dir: Path) -> List[FileInfo]: def find_config_files(config_dir: Path) -> List[FileInfo]:
try: try:
return explore_dir(config_dir) return explore_dir(config_dir)
except OSError as e: 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]: def explore_dir(cur_dir: Path) -> List[FileInfo]:
if not cur_dir.is_dir(): 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] = {} files: Dict[Path, FileInfo] = {}
header_files: List[Path] = [] header_files: List[Path] = []
@ -51,9 +56,13 @@ def explore_dir(cur_dir: Path) -> List[FileInfo]:
matching_file_info = files.get(matching_file) matching_file_info = files.get(matching_file)
if matching_file_info is None: 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: 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 matching_file_info.header = header_file
# 3. Collect the resulting FileInfos # 3. Collect the resulting FileInfos
@ -64,6 +73,7 @@ def explore_dir(cur_dir: Path) -> List[FileInfo]:
try: try:
result.extend(explore_dir(subdir)) result.extend(explore_dir(subdir))
except OSError as e: 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 return result

View file

@ -1,14 +1,15 @@
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Set from typing import Dict, Optional, Set
from .colors import * from .colors import style_error, style_path
from .util import * from .util import CatastrophicError, WriteFileException, write_file
__all__ = ["KnownFiles"] __all__ = ["KnownFiles"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class KnownFiles: class KnownFiles:
def __init__(self, path: Path) -> None: def __init__(self, path: Path) -> None:
self._path = path self._path = path
@ -18,7 +19,7 @@ class KnownFiles:
try: try:
with open(self._path) as f: with open(self._path) as f:
self._old_known_files = self._read_known_files(f.read()) 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, " logger.debug(f"File {style_path(self._path)} does not exist, "
"creating a new file on the first upcoming save") "creating a new file on the first upcoming save")
@ -30,13 +31,16 @@ class KnownFiles:
raw_known_files = json.loads(text) raw_known_files = json.loads(text)
if not isinstance(raw_known_files, dict): 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(): for path, file_hash in raw_known_files.items():
if not isinstance(path, str): 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): 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)) path = self._normalize_path(Path(path))
known_files[path] = file_hash known_files[path] = file_hash
@ -61,7 +65,8 @@ class KnownFiles:
def save_incremental(self) -> None: def save_incremental(self) -> None:
to_save: Dict[str, str] = {} 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: if path in self._new_known_files:
to_save[str(path)] = self._new_known_files[path] to_save[str(path)] = self._new_known_files[path]
else: else:

View file

@ -1,7 +1,7 @@
from abc import ABC from abc import ABC
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from .util import * from .util import safer_eval
""" """
This parsing solution has the following structure: 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 1. Separate header and config file content, if necessary
2. Split up text into lines, if still necessary 2. Split up text into lines, if still necessary
3. Parse each line individually 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 5. Evaluate the blocks recursively
""" """
@ -18,6 +18,7 @@ __all__ = [
"ParseException", "Parser", "ParseException", "Parser",
] ]
def split_header_and_rest(text: str) -> Tuple[List[str], List[str]]: def split_header_and_rest(text: str) -> Tuple[List[str], List[str]]:
lines = text.splitlines() lines = text.splitlines()
@ -38,11 +39,13 @@ def split_header_and_rest(text: str) -> Tuple[List[str], List[str]]:
return header, rest return header, rest
class ParseException(Exception): class ParseException(Exception):
@classmethod @classmethod
def on_line(cls, line: "Line", text: str) -> "ParseException": def on_line(cls, line: "Line", text: str) -> "ParseException":
return ParseException(f"Line {line.line_number}: {text}") return ParseException(f"Line {line.line_number}: {text}")
class Parser: class Parser:
def __init__(self, def __init__(self,
raw_lines: List[str], raw_lines: List[str],
@ -71,6 +74,7 @@ class Parser:
lines = self.main_block.evaluate(local_vars) lines = self.main_block.evaluate(local_vars)
return "".join(f"{line}\n" for line in lines) return "".join(f"{line}\n" for line in lines)
# Line parsing (inline expressions) # Line parsing (inline expressions)
class Line(ABC): class Line(ABC):
@ -102,7 +106,10 @@ class Line(ABC):
self.parser = parser self.parser = parser
self.line_number = line_number 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}" start = f"{self.parser.statement_prefix} {statement_name}"
text = text.strip() text = text.strip()
if text.startswith(start): if text.startswith(start):
@ -111,7 +118,9 @@ class Line(ABC):
return None return None
def _parse_statement_noarg(self, text: str, statement_name: str) -> bool: 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): class ActualLine(Line):
def __init__(self, parser: Parser, text: str, line_number: int) -> None: def __init__(self, parser: Parser, text: str, line_number: int) -> None:
@ -149,7 +158,11 @@ class ActualLine(Line):
# Find expression suffix # Find expression suffix
cd = text.find(self.parser.expression_suffix, od_end) cd = text.find(self.parser.expression_suffix, od_end)
if cd == -1: 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) cd_end = cd + len(self.parser.expression_suffix)
# Split up into chunks # Split up into chunks
@ -164,7 +177,8 @@ class ActualLine(Line):
May raise: ExecuteException 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, def _evaluate_chunk(self,
chunk: Tuple[str, bool], chunk: Tuple[str, bool],
@ -179,6 +193,7 @@ class ActualLine(Line):
return str(safer_eval(chunk[0], local_vars)) return str(safer_eval(chunk[0], local_vars))
class IfStatement(Line): class IfStatement(Line):
def __init__(self, parser: Parser, text: str, line_number: int) -> None: def __init__(self, parser: Parser, text: str, line_number: int) -> None:
""" """
@ -191,6 +206,7 @@ class IfStatement(Line):
if self.argument is None: if self.argument is None:
raise ParseException.on_line(self, "Not an 'if' statement") raise ParseException.on_line(self, "Not an 'if' statement")
class ElifStatement(Line): class ElifStatement(Line):
def __init__(self, parser: Parser, text: str, line_number: int) -> None: def __init__(self, parser: Parser, text: str, line_number: int) -> None:
""" """
@ -203,6 +219,7 @@ class ElifStatement(Line):
if self.argument is None: if self.argument is None:
raise ParseException.on_line(self, "Not an 'elif' statement") raise ParseException.on_line(self, "Not an 'elif' statement")
class ElseStatement(Line): class ElseStatement(Line):
def __init__(self, parser: Parser, text: str, line_number: int) -> None: 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"): if not self._parse_statement_noarg(text, "else"):
raise ParseException.on_line(self, "Not an 'else' statement") raise ParseException.on_line(self, "Not an 'else' statement")
class EndifStatement(Line): class EndifStatement(Line):
def __init__(self, parser: Parser, text: str, line_number: int) -> None: 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"): if not self._parse_statement_noarg(text, "endif"):
raise ParseException.on_line(self, "Not an 'endif' statement") raise ParseException.on_line(self, "Not an 'endif' statement")
# Block parsing # Block parsing
class Block: class Block:
@ -259,6 +278,7 @@ class Block:
return lines return lines
class IfBlock(Block): class IfBlock(Block):
def __init__(self, parser: Parser, lines_queue: List[Line]) -> None: def __init__(self, parser: Parser, lines_queue: List[Line]) -> None:
""" """
@ -268,18 +288,19 @@ class IfBlock(Block):
self._sections: List[Tuple[Block, Optional[str]]] = [] self._sections: List[Tuple[Block, Optional[str]]] = []
if not lines_queue: if not lines_queue:
raise ParseException("Unexpected end of file, expected 'if' statement") raise ParseException("Unexpected end of file, expected 'if' "
"statement")
# If statement # If statement
# #
# This is the short version: # 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") # raise ParseException.on_line(lines_queue[-1], "Expected 'if' statement")
# self._sections.append((Block(parser, lines_queue), lines_queue.pop().argument)) # self._sections.append((Block(parser, lines_queue), lines_queue.pop().argument))
# #
# And this the long version, which mypy understands without errors: # And this the long version, which mypy understands without errors:
next_statement = lines_queue[-1] 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") raise ParseException.on_line(next_statement, "Expected 'if' statement")
lines_queue.pop() lines_queue.pop()
self._sections.append((Block(parser, lines_queue), next_statement.argument)) self._sections.append((Block(parser, lines_queue), next_statement.argument))

View file

@ -4,22 +4,27 @@ import shutil
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
from .colors import * from .colors import style_error, style_path, style_warning
from .config import * from .config import Config
from .known_files import * from .known_files import KnownFiles
from .parser import * from .parser import ParseException, Parser, split_header_and_rest
from .prompt import * from .prompt import prompt_yes_no
from .util import * from .util import (ExecuteException, LessCatastrophicError, ReadFileException,
WriteFileException, read_file, safer_exec, write_file)
__all__ = ["Processor"] __all__ = ["Processor"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Processor: class Processor:
def __init__(self, config: Config, known_files: KnownFiles) -> None: def __init__(self, config: Config, known_files: KnownFiles) -> None:
self.config = config self.config = config
self.known_files = known_files 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)}:") logger.info(f"{style_path(path)}:")
config = self.config.copy() config = self.config.copy()
@ -51,7 +56,11 @@ class Processor:
self._process_parseable(lines, config, path) 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)} " logger.debug(f"Processing file {style_path(path)} "
f"with header {style_path(header_path)}") f"with header {style_path(header_path)}")
@ -80,7 +89,7 @@ class Processor:
self._process_parseable(lines, config, path) self._process_parseable(lines, config, path)
def _process_binary(self, path: Path, config: Config) -> None: 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: if not config.targets:
logger.info(" (no targets)") logger.info(" (no targets)")
@ -96,7 +105,10 @@ class Processor:
try: try:
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
except IOError as e: 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 continue
try: try:
@ -108,11 +120,16 @@ class Processor:
try: try:
shutil.copymode(path, target) shutil.copymode(path, target)
except shutil.Error as e: 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) 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: if not config.targets:
logger.info(" (no targets)") logger.info(" (no targets)")
return return
@ -149,19 +166,24 @@ class Processor:
try: try:
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
except IOError as e: 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 continue
try: try:
write_file(target, text) write_file(target, text)
except WriteFileException as e: 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 continue
try: try:
shutil.copymode(source, target) shutil.copymode(source, target)
except shutil.Error as e: 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) self._update_known_hash(target)
@ -174,7 +196,8 @@ class Processor:
with open(path, "rb") as f: with open(path, "rb") as f:
while True: while True:
block = f.read(BLOCK_SIZE) block = f.read(BLOCK_SIZE)
if not block: break if not block:
break
h.update(block) h.update(block)
return h.hexdigest() return h.hexdigest()
@ -191,16 +214,19 @@ class Processor:
return False return False
if self.known_files.was_recently_modified(target): 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 return False
target_hash = self._obtain_hash(target) target_hash = self._obtain_hash(target)
if target_hash is None: 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) known_target_hash = self.known_files.get_hash(target)
if known_target_hash is None: 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 # The following condition is phrased awkwardly because I just
# feel better if the final statement in this function is not a # feel better if the final statement in this function is not a
@ -212,7 +238,8 @@ class Processor:
# last seen it. # last seen it.
return True 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: def _update_known_hash(self, target: Path) -> None:
target_hash = self._obtain_hash(target) target_hash = self._obtain_hash(target)

View file

@ -2,6 +2,7 @@ from typing import Optional
__all__ = ["prompt_choice", "prompt_yes_no"] __all__ = ["prompt_choice", "prompt_yes_no"]
def prompt_choice(question: str, options: str) -> str: def prompt_choice(question: str, options: str) -> str:
default_option = None default_option = None
for char in options: for char in options:
@ -22,6 +23,7 @@ def prompt_choice(question: str, options: str) -> str:
else: else:
print(f"Invalid answer, please choose one of [{option_string}].") print(f"Invalid answer, please choose one of [{option_string}].")
def prompt_yes_no(question: str, default_answer: Optional[bool]) -> bool: def prompt_yes_no(question: str, default_answer: Optional[bool]) -> bool:
if default_answer is None: if default_answer is None:
options = "yn" options = "yn"

View file

@ -14,6 +14,7 @@ __all__ = [
"CatastrophicError", "LessCatastrophicError", "CatastrophicError", "LessCatastrophicError",
] ]
def copy_local_variables(local: Dict[str, Any]) -> Dict[str, Any]: def copy_local_variables(local: Dict[str, Any]) -> Dict[str, Any]:
""" """
Attempts to deep-copy a set of local variables, but keeping 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 return local_copy
def get_user() -> str: def get_user() -> str:
return getpass.getuser() return getpass.getuser()
def get_host() -> str: def get_host() -> str:
return socket.gethostname() return socket.gethostname()
class ExecuteException(Exception): class ExecuteException(Exception):
pass pass
def safer_exec(code: str, local_vars: Dict[str, Any]) -> None: def safer_exec(code: str, local_vars: Dict[str, Any]) -> None:
""" """
May raise: ExecuteException May raise: ExecuteException
@ -52,6 +57,7 @@ def safer_exec(code: str, local_vars: Dict[str, Any]) -> None:
except Exception as e: except Exception as e:
raise ExecuteException(e) raise ExecuteException(e)
def safer_eval(code: str, local_vars: Dict[str, Any]) -> Any: def safer_eval(code: str, local_vars: Dict[str, Any]) -> Any:
""" """
May raise: ExecuteException May raise: ExecuteException
@ -62,9 +68,11 @@ def safer_eval(code: str, local_vars: Dict[str, Any]) -> Any:
except Exception as e: except Exception as e:
raise ExecuteException(e) raise ExecuteException(e)
class ReadFileException(Exception): class ReadFileException(Exception):
pass pass
def read_file(path: Path) -> str: def read_file(path: Path) -> str:
""" """
May raise: ReadFileException May raise: ReadFileException
@ -76,9 +84,11 @@ def read_file(path: Path) -> str:
except OSError as e: except OSError as e:
raise ReadFileException(e) raise ReadFileException(e)
class WriteFileException(Exception): class WriteFileException(Exception):
pass pass
def write_file(path: Path, text: str) -> None: def write_file(path: Path, text: str) -> None:
""" """
May raise: WriteFileException May raise: WriteFileException
@ -90,8 +100,10 @@ def write_file(path: Path, text: str) -> None:
except OSError as e: except OSError as e:
raise WriteFileException(e) raise WriteFileException(e)
class CatastrophicError(Exception): class CatastrophicError(Exception):
pass pass
class LessCatastrophicError(Exception): class LessCatastrophicError(Exception):
pass pass