<commit_before>import datetime
import threading
import time

def parse_version_string():
    import os
    import os.path
    import subprocess
    
    path = os.path.abspath(__file__)
    while os.path.islink(path):
        path = os.path.join(os.path.dirname(path), os.readlink(path))
    path = os.path.dirname(path) # go up one level, from repo/loops.py to repo, where README.md is located
    while os.path.islink(path):
        path = os.path.join(os.path.dirname(path), os.readlink(path))
    try:
        version = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=path).decode('utf-8').strip('\n')
        if version == 'master':
            try:
                with open(os.path.join(path, 'README.md')) as readme:
                    for line in readme.read().splitlines():
                        if line.startswith('This is `python-xdg-basedir` version'):
                            return line.split(' ')[4]
            except:
                pass
        return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=path).decode('utf-8').strip('\n')
    except:
        pass

__version__ = parse_version_string()

class IterThread(threading.Thread):
    """Helper class used in loops."""
    def __init__(self, iterator):
        super().__init__()
        self.daemon = True
        self.iterator = iterator
        self.stopped = False
    
    def run(self):
        try:
            self.value = next(self.iterator)
        except StopIteration:
            self.stopped = True

class Loop(threading.Thread):
    """Generic loop thread that periodically checks if it should stop while waiting for the iterable to yield.
    
    Keyword-only arguments:
    iterable -- The iterable to be looped over. By default, self.get_iterable is called.
    process_value -- A function which will be called with each yielded value as argument. Defaults to self.process_value.
    sleep_length -- A datetime.timedelta representing how long to sleep between each check for the next value or the stop signal. Defaults to half a second.
    """
    def __init__(self, *, iterable=None, process_value=None, sleep_length=datetime.timedelta(seconds=0.5)):
        super().__init__()
        if iterable is None:
            self.iterable = self.iterable()
        else:
            self.iterable = iterable
        if process_value is not None:
            self.process_value = process_value
        self.stopped = False
        self.sleep_length = sleep_length
    
    @staticmethod
    def iterable():
        """The iterable to be looped over. Must be overridden in a subclass, or by passing the `iterable' keyword argument to the constructor."""
        raise NotImplementedError('iterable must be overwritten in subclasses, or set explicitly')
    
    def run(self):
        iterator = iter(self.iterable)
        iter_thread = IterThread(iterator)
        iter_thread.start() # get the first value
        while not self.stopped:
            if not iter_thread.is_alive():
                if iter_thread.stopped: # iterator exhausted
                    return
                else: # iterator has yielded a value
                    self.process_value(iter_thread.value)
                    iter_thread = IterThread(iterator)
                    iter_thread.start() # get the next value
                    continue
            time.sleep(self.sleep_length.total_seconds())
    
    @staticmethod
    def process_value(value):
        """Will be called with each yielded value as argument. Must be overridden in a subclass, or by passing the `process_value' keyword argument to the constructor."""
        raise NotImplementedError('process_value must be overwritten in subclasses, or set explicitly')
    
    def start(self):
        self.stopped = False
        super().start()
    
    def stop(self):
        self.stopped = True

def timeout_single(iterable, timeout, sleep_length=datetime.timedelta(seconds=0.5)):<commit_msg>Add optional on_exception argument to Loop constructor<commit_after>import datetime
import threading
import time

def parse_version_string():
    import os
    import os.path
    import subprocess
    
    path = os.path.abspath(__file__)
    while os.path.islink(path):
        path = os.path.join(os.path.dirname(path), os.readlink(path))
    path = os.path.dirname(path) # go up one level, from repo/loops.py to repo, where README.md is located
    while os.path.islink(path):
        path = os.path.join(os.path.dirname(path), os.readlink(path))
    try:
        version = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=path).decode('utf-8').strip('\n')
        if version == 'master':
            try:
                with open(os.path.join(path, 'README.md')) as readme:
                    for line in readme.read().splitlines():
                        if line.startswith('This is `python-xdg-basedir` version'):
                            return line.split(' ')[4]
            except:
                pass
        return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=path).decode('utf-8').strip('\n')
    except:
        pass

__version__ = parse_version_string()

class IterThread(threading.Thread):
    """Helper class used in loops."""
    def __init__(self, iterator):
        super().__init__()
        self.daemon = True
        self.iterator = iterator
        self.stopped = False
    
    def run(self):
        try:
            self.value = next(self.iterator)
        except StopIteration:
            self.stopped = True

class Loop(threading.Thread):
    """Generic loop thread that periodically checks if it should stop while waiting for the iterable to yield.
    
    Keyword-only arguments:
    iterable -- The iterable to be looped over. By default, self.get_iterable is called.
    on_exception -- What to do when an exception occurs in process_value. If given, must be an iterable of actions, which will be done in order. Possible actions are 'log_stdout' (write traceback to sys.stdout), 'log_stderr' (write traceback to sys.stderr), or 'raise' (the default; lets the exception through to threading's default handling). Set to an empty iterable to ignore exceptions and continue the loop.
    process_value -- A function which will be called with each yielded value as argument. Defaults to self.process_value.
    sleep_length -- A datetime.timedelta representing how long to sleep between each check for the next value or the stop signal. Defaults to half a second.
    """
    def __init__(self, *, iterable=None, on_exception=('raise',), process_value=None, sleep_length=datetime.timedelta(seconds=0.5)):
        super().__init__()
        if iterable is None:
            self.iterable = self.iterable()
        else:
            self.iterable = iterable
        if process_value is not None:
            self.process_value = process_value
        self.stopped = False
        self.sleep_length = sleep_length
    
    @staticmethod
    def iterable():
        """The iterable to be looped over. Must be overridden in a subclass, or by passing the `iterable' keyword argument to the constructor."""
        raise NotImplementedError('iterable must be overwritten in subclasses, or set explicitly')
    
    def run(self):
        iterator = iter(self.iterable)
        iter_thread = IterThread(iterator)
        iter_thread.start() # get the first value
        while not self.stopped:
            if not iter_thread.is_alive():
                if iter_thread.stopped: # iterator exhausted
                    return
                else: # iterator has yielded a value
                    try:
                        self.process_value(iter_thread.value)
                    except:
                        for exception_action in self.on_exception:
                            if exception_action == 'log_stdout':
                                traceback.print_exc(file=sys.stdout)
                            elif exception_action == 'log_stderr':
                                traceback.print_exc(file=sys.stderr)
                            elif exception_action == 'raise':
                                raise
                            else:
                                raise ValueError('Unrecognized exception action: {!r}'.format(exception_action))
                    iter_thread = IterThread(iterator)
                    iter_thread.start() # get the next value
                    continue
            time.sleep(self.sleep_length.total_seconds())
    
    @staticmethod
    def process_value(value):
        """Will be called with each yielded value as argument. Must be overridden in a subclass, or by passing the `process_value' keyword argument to the constructor."""
        raise NotImplementedError('process_value must be overwritten in subclasses, or set explicitly')
    
    def start(self):
        self.stopped = False
        super().start()
    
    def stop(self):
        self.stopped = True

def timeout_single(iterable, timeout, sleep_length=datetime.timedelta(seconds=0.5)):
{'fraction_non_alphanumeric': 0.09333039841514418, 'fraction_numerical': 0.0015408320493066256, 'mean_word_length': 2.244198500535523, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1900243', 'n_tokens_mistral': 2439, 'n_tokens_neox': 2390, 'n_words': 749}