<commit_before>import collections, datetime, functools, json, glob, logging, os, shutil, sys, time


logger = logging.getLogger(__name__)


class Config(collections.MutableMapping):
    """Configuration JSON storage class"""
    def __init__(self, filename, default=None, failsafe_backups=0):
        self.filename = filename
        self.default = None
        self.config = {}
        self.changed = False
        self.failsafe_backups = failsafe_backups
        self.load()

    def _make_failsafe_backup(self):
        try:
            json.load(open(self.filename))
        except IOError:
            return False
        except ValueError:
            logger.warning("{} is corrupted, aborting backup".format(self.filename))
            return False

        existing = sorted(glob.glob(self.filename + ".*.bak"))
        while len(existing) > (self.failsafe_backups - 1):
            os.remove(existing.pop(0))

        backup_file = self.filename + "." + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + ".bak"
        shutil.copy2(self.filename, backup_file)

        return True

    def load(self):
        """Load config from file"""
        try:
            self.config = json.load(open(self.filename))
            logger.info("{} read".format(self.filename))

        except IOError:
            self.config = {}

        except ValueError:
            logger.exception("malformed json: {}".format(self.filename))

            # instructive error-handling for n00bs, including me!
            print("EXCEPTION: .json likely malformed")
            print("  check {}".format(self.filename))
            print("  {}".format(sys.exc_info()[1]))

            sys.exit(0)

        self.changed = False

    def force_taint(self):
        self.changed = True

    def loads(self, json_str):
        """Load config from JSON string"""
        self.config = json.loads(json_str)
        self.changed = True

    def save(self):
        """Save config to file (only if config has changed)"""
        if self.changed:
            start_time = time.time()

            if self.failsafe_backups:
                self._make_failsafe_backup()<commit_msg>when failsafe backups are on, allow recovery from backups, closes #258<commit_after>import collections, datetime, functools, json, glob, logging, os, shutil, sys, time


logger = logging.getLogger(__name__)


class Config(collections.MutableMapping):
    """Configuration JSON storage class"""
    def __init__(self, filename, default=None, failsafe_backups=0):
        self.filename = filename
        self.default = None
        self.config = {}
        self.changed = False
        self.failsafe_backups = failsafe_backups
        self.load()

    def _make_failsafe_backup(self):
        try:
            json.load(open(self.filename))
        except IOError:
            return False
        except ValueError:
            logger.warning("{} is corrupted, aborting backup".format(self.filename))
            return False

        existing = sorted(glob.glob(self.filename + ".*.bak"))
        while len(existing) > (self.failsafe_backups - 1):
            os.remove(existing.pop(0))

        backup_file = self.filename + "." + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + ".bak"
        shutil.copy2(self.filename, backup_file)

        return True

    def _recover_from_failsafe(self):
        existing = sorted(glob.glob(self.filename + ".*.bak"))
        while len(existing) > 0:
            try:
                recovery_filename = existing.pop()
                json.load(open(recovery_filename))
                shutil.copy2(recovery_filename, self.filename)
                self.load(recovery=True)
                logger.info("recovery successful: {}".format(recovery_filename))
                return True
            except IOError:
                pass
            except ValueError:
                logger.error("corrupted recovery: {}".format(self.filename))
        return False

    def load(self, recovery=False):
        """Load config from file"""
        try:
            self.config = json.load(open(self.filename))
            logger.info("{} read".format(self.filename))

        except IOError:
            self.config = {}

        except ValueError:
            if not recovery and self.failsafe_backups > 0 and self._recover_from_failsafe():
                return

            logger.exception("malformed json: {}".format(self.filename))

            # instructive error-handling for n00bs, including me!
            print("EXCEPTION: .json likely malformed")
            print("  check {}".format(self.filename))
            print("  {}".format(sys.exc_info()[1]))

            sys.exit(0)

        self.changed = False

    def force_taint(self):
        self.changed = True

    def loads(self, json_str):
        """Load config from JSON string"""
        self.config = json.loads(json_str)
        self.changed = True

    def save(self):
        """Save config to file (only if config has changed)"""
        if self.changed:
            start_time = time.time()

            if self.failsafe_backups:
                self._make_failsafe_backup()
{'fraction_non_alphanumeric': 0.1159448436589629, 'fraction_numerical': 0.004272674305690425, 'mean_word_length': 2.1402439024390243, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, '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': '768539', 'n_tokens_mistral': 1480, 'n_tokens_neox': 1402, 'n_words': 325}