/
Vimgolfc8267ce
"""
Log parser for VimGolf executor
"""
import copy
import json
import os
from abc import ABC, abstractmethod
from typing import Type
from vimgolf_gym.dataclasses import VimGolfEnvResult
class AbstractLogParser(ABC):
def __init__(self): ...
@abstractmethod
def feed_line(self, line: str): ...
# We keep track of the log file attributes.
# Specifically, the file size.
# If the file size changes, we will reparse the log.
# In more advanced usage, we could seek to given point and parse from there, avoiding reparsing from the beginning.
class LogWatcher:
def __init__(self, log_file: str, parser_class: Type[AbstractLogParser]):
"""
Initialize the log watcher with a log file and a parser class.
Args:
log_file (str): The path to the log file.
parser_class (Type[AbstractLogParser]): A class that implements the AbstractLogParser interface.
Attributes:
log_file (str): The path to the log file.
parser_class (Type[AbstractLogParser]): The class of the parser.
parser (AbstractLogParser): An instance of the parser class.
last_filesize (int): The size of the log file when it was last checked.
last_position (int): The last read position in the log file.
"""
self.log_file = log_file
self.parser_class = parser_class
self.parser = parser_class()
self.last_filesize = 0
self.last_position = 0 # Track the last read position
def update(self, style="advanced"):
"""
Update the log watcher using one of three strategies.
The ``advanced`` strategy is the default. It checks the file size
and only reads new content if the file size has changed. If the file
size is smaller than the last recorded size, it resets the parser and
reads the entire file.
The ``simple`` strategy reads the entire log file every time it is
called. This is simple, but slow for large log files.
The ``naive`` strategy reads the entire log file every time it is
called, but it does not reset the parser. This is faster than the
``simple`` strategy, but it does not handle the case where the log
file is truncated.
Args:
style (str, optional): The update strategy to use. Must be one of
``advanced``, ``simple``, or ``naive``. Defaults to ``advanced``.
"""
if style == "advanced":
self.advanced_update()
elif style == "simple":
self.simple_update()
elif style == "naive":
self.naive_update()
else:
raise ValueError(
"Unrecognized update option: %s (should be in advanced, simple, naive)"
% style
)
def simple_update(self):
"""
Read the entire log file and reset the parser if the file size has changed.
The ``simple`` strategy reads the entire log file every time it is
called. This is simple, but slow for large log files.
"""
current_size = (
os.path.getsize(self.log_file) if os.path.exists(self.log_file) else 0
)
if current_size != self.last_filesize:
self.naive_update()
self.last_filesize = current_size
def naive_update(self):
"""
Reset the parser and read the entire log file.
This is slow for large log files, but it is simple and handles
the case where the log file is truncated.
"""
self.parser = self.parser_class()
if os.path.exists(self.log_file):
with open(self.log_file, "r") as f:
for line in f.readlines():
self.parser.feed_line(line)
self.last_filesize = (
os.path.getsize(self.log_file) if os.path.exists(self.log_file) else 0
)
self.last_position = self.last_filesize
def advanced_update(self):
"""
Read only the new content from the log file.
The ``advanced`` strategy reads only the new content from the log file,
starting from the last recorded position. This is fast for large log
files, but it requires keeping track of the last recorded position.
If the file size has decreased, it resets the parser and reads the
entire file.
"""
if not os.path.exists(self.log_file):
return
current_size = os.path.getsize(self.log_file)
# If file size decreased (file was truncated), do a full reset
if current_size < self.last_filesize:
self.naive_update()
return
# If file size increased, read only the new content
if current_size > self.last_filesize:
with open(self.log_file, "r") as f:
f.seek(self.last_position)
while True:
line = f.readline()
if not line: # End of file
break
self.parser.feed_line(line)
# Update tracking variables
self.last_filesize = current_size
self.last_position = f.tell()
class VimGolfLogWatcher(LogWatcher):
def __init__(self, log_file: str, update_style="advanced"):
"""
Initialize a VimGolfLogWatcher with a log file and an update style.
The VimGolfLogWatcher is a LogWatcher that is specialized for
reading VimGolf logs.
Args:
log_file (str): The path to the log file.
update_style (str, optional): The update strategy to use. Must be one of
``advanced``, ``simple``, or ``naive``. Defaults to ``advanced``.
Returns:
[return_type]: [description of return value]
"""
super().__init__(log_file=log_file, parser_class=VimGolfLogParser)
self.parser: VimGolfLogParser
self.update_style = update_style
def default_update(self):
"""
Call the update method with the default update style.
This method is a convenience wrapper around the update method,
calling it with the default update style specified in the
constructor.
See the update method for more details.
"""
self.update(style=self.update_style)
@property
def success(self):
"""
Check if the vimgolf challenge has been solved successfully.
This property checks if the vimgolf challenge has been solved
successfully. It updates the log watcher if necessary before
checking the success status.
Returns:
bool: True if the challenge has been solved successfully, False otherwise.
"""
self.default_update()
return self.parser.success
def get_best_success_result(self):
"""
Return the best success result in the log watcher.
This method returns the best success result in the log watcher,
updating the log watcher if necessary before returning the result.
Returns:
VimGolfEnvResult: The best success result in the log watcher, or None if
there is no success result.
"""
self.default_update()
return self.parser.get_best_success_result()
def get_last_success_result(self):
"""
Return the last success result in the log watcher.
This method returns the last success result in the log watcher,
updating the log watcher if necessary before returning the result.
Returns:
VimGolfEnvResult: The last success result in the log watcher, or None if
there is no success result.
"""
self.default_update()
return self.parser.get_last_success_result()
@property
def results(self):
"""
The results of the vimgolf challenge environment.
This property returns the results of the vimgolf challenge environment,
updating the log watcher if necessary before returning the results.
Returns:
list[VimGolfEnvResult]: The results of the vimgolf challenge environment, or an empty list if
there are no results.
"""
self.default_update()
return self.parser.results
@property
def success_results(self):
"""
The successful results of the vimgolf challenge environment.
This property returns the successful results of the vimgolf challenge
environment, updating the log watcher if necessary before returning the
results.
Returns:
list[VimGolfEnvResult]: The successful results of the vimgolf challenge environment, or an
empty list if there are no successful results.
"""
self.default_update()
return self.parser.success_results
class VimGolfLogParser(AbstractLogParser):
def __init__(self):
"""
Initialize the VimGolfLogParser.
The VimGolfLogParser is initialized with an empty list of results.
"""
self.results: list[VimGolfEnvResult] = []
def feed_line(self, line: str):
"""
Feed a line to the parser.
The line should be a JSON-formatted string. The parser will attempt to
parse the line as a JSON object, and if it is a dictionary, it will
check if the dictionary has an "event_type" key with value
"vimgolf_result". If so, it will attempt to parse the value of the
"event_data" key as a VimGolfEnvResult object and append it to the
results list.
If the line is not a valid JSON object, or if the JSON object does not
have the correct structure, the line will be ignored.
Args:
line (str): The line of text to feed to the parser.
"""
try:
data = json.loads(line.strip())
if type(data) == dict:
if data.get("event_type", None) == "vimgolf_result":
event_data = data.get("event_data", None)
if type(event_data) == dict:
parsed_result = VimGolfEnvResult.parse_obj(event_data)
self.results.append(parsed_result)
except json.JSONDecodeError:
...
@property
def success_results(self):
"""
The successful results of the vimgolf challenge environment.
This property returns the successful results of the vimgolf challenge
environment, which are the results in the results list where the
correct attribute is True.
Returns:
list[VimGolfEnvResult]: The successful results of the vimgolf challenge environment, or an
empty list if there are no successful results.
"""
return [it for it in self.results if it.correct]
@property
def success(self):
"""
Check if the vimgolf challenge has been solved successfully.
This property checks if the vimgolf challenge has been solved
successfully. It returns True if there are any successful results in
the results list, and False otherwise.
Returns:
bool: True if the challenge has been solved successfully, False otherwise.
"""
return len(self.success_results) != 0
def get_last_success_result(self):
"""
Return the last success result in the log watcher.
This method returns the last success result in the log watcher,
which is the last result in the success_results list.
Returns:
VimGolfEnvResult: The last success result in the log watcher, or None if there is no
success result.
"""
success_results = self.success_results
if success_results:
return success_results[-1]
def get_best_success_result(self):
"""Return the result with the lowest score"""
success_results = copy.deepcopy(self.success_results)
if success_results:
success_results.sort(key=lambda x: x.score)
return success_results[0]