Skip to content

Instantly share code, notes, and snippets.

@remcoboerma
Last active March 3, 2018 17:14
Show Gist options
  • Save remcoboerma/a8b999239565d124a46c08ab63784d94 to your computer and use it in GitHub Desktop.
Save remcoboerma/a8b999239565d124a46c08ab63784d94 to your computer and use it in GitHub Desktop.
from __future__ import print_function
from collections import namedtuple
# don't use the semver library, it doesn't work well with compare, sort and those simple things.
# the parse_version result from setuptools does a lot better job.
# found this gem at https://stackoverflow.com/a/21065570/3563630
# Seems to be https://www.python.org/dev/peps/pep-0440/ compliant
from pkg_resources import parse_version
FixInfo = namedtuple('FixInfo', 'seq,fn')
class StopUpgradingException(Exception):
'''StopUpgradingException is thrown to notify fix_record an error has occurred in a decorated fix method. '''
def __init__(self, record, fn, exception):
super(StopUpgradingException, self).__init__(self)
self.record = record
self.fn = fn
self.exception = exception
class Upgrader(object):
def __init__(self):
self.fixes = []
def greater_version(self,a,b):
return a > b
def update_record_version(self, record, version):
'''Updates record with the new version.'''
record['version'] = version
def version_getter(self, record):
'''Get the version for a given record. Should be comparable to sequence. '''
return record.get('version', -1)
def commit(self, record, final=False):
'''For subclasses, overwrite to handle altered records if they need special treatment.
Final is true only once per distinct record after all fixes have been applied. Commit will also be called
after each successful upgrade with final=False.
'''
pass
def abort(self, record, stop_exception):
'''For subclasses, abort is issued when the fix raised an exception.
investigate stop_exception.fn, .record and .exception for more details
'''
print('Aborting upgrade of ', record, ' using ', stop_exception.fn, ' because of ', stop_exception.exception)
def register_fix(self, sequence):
'''Register a fix function.
Decorate a fixer with @register_fix(sequence). All decorated functions are registered so when upgrading a record
all applicable fixes will be applied. In the fixing process, if no Exception is raised, self.commit(record) is
issued. This is ussued on every fix per record. In case of an error, self.abort()
'''
# create the decorator
def deco(fn):
# this is the patch function, which will wrap the real fix.
def patch(record, *p, **kw):
'''Execute fn on record. If succesfull: upgrade the record version and commit, raise StopUpgradingException otherwise. '''
try:
# try the function
fn(record, *p, **kw)
except Exception as e:
# if anything went wrong, signal fix_record to cancel the upgrade process for this record
raise StopUpgradingException(record, fn, e)
else:
# if all went well, upgrade the version and commit this record
self.update_record_version(record, sequence)
self.commit(record, final=False)
# the following is only run once per registration of a new function.
# check if the list of fixes is still in order
sort_order_is_okay = self.fixes and self.greater_version(sequence,self.fixes[-1].seq)
# append this new fix
self.fixes.append(FixInfo(fn=patch, seq=sequence))
if not sort_order_is_okay:
# reorder is so required
self.fixes.sort(key=lambda rec: rec.seq)
return patch
return deco
def fix_record(self, record, *p, **kw):
'''Fix a given record by applying all fixes registered for higher version in sequential order.
calls self.abort on failure.
*p and **kw are passed as is onto the fixes '''
try:
dirty = False
for fix in self.fixes:
# find the fix whose version matches a newer version than that of the record
if self.greater_version(fix.seq,self.version_getter(record)):
# mark a possible commit later
dirty = True
print('>>>> Dirty is true!!', fix.seq, self.version_getter(record))
# apply the fix
fix.fn(record, *p, **kw)
except StopUpgradingException as e:
# hang on, something went wrong?
# let's notify Updater to do whatever necessary to handle this error
self.abort(record, e)
else:
# if all went well
if dirty:
# and we have changed the record then commit for the last time
# signalling commit that all in-between commits for this record have been committed.
self.commit(record, final=True)
class SemverUpgrader(Upgrader):
def register_fix(self, sequence):
return super(SemverUpgrader, self).register_fix(parse_version(sequence))
def update_record_version(self, record, version):
super(SemverUpgrader, self).update_record_version(record, str(version))
def version_getter(self, record):
version = super(SemverUpgrader, self).version_getter(record)
version = '0.0.0' if version == -1 else version
return parse_version(version)
if __name__=='__main__':
import pprint
from upgrader import Upgrader, SemverUpgrader
upgrader = Upgrader()
@upgrader.register_fix(1)
def fix1(record, **kwp):
record['load'] += 1
@upgrader.register_fix(2)
def fix2(record, bla='bla'):
print(record, 1, bla)
0 / 0
print(record, 2)
record['load'] += 10
@upgrader.register_fix(4)
def fix4(record, **kwp):
print(record, 4)
record['load'] += 1000
@upgrader.register_fix(3)
def fix3(record, **kwp):
print(record, 3)
record['load'] += 100
records = [dict(version=v, load=0) for v in (1, 2, 3, 4, 5)]
for r in records:
print('\nRecord: ', r)
upgrader.fix_record(r, bla='BLA!!')
pprint.pprint(records)
print('\n\n--- SEMVER ---')
# semver upgrader
svu = SemverUpgrader()
@svu.register_fix('1.0.0')
def u100alfa(record):
print(record, '1.0.0')
@svu.register_fix('1.0.99')
def u100beta(record):
print(record, '1.0.99')
@svu.register_fix('1.0.100')
def u100beta(record):
print(record, '1.0.100')
@svu.register_fix('1.0.100post1')
def u100beta(record):
print(record, '1.0.100post1')
@svu.register_fix('1.1.0')
def u100(record):
print(record, '1.1.0')
@svu.register_fix('0.1.0')
def u010(record):
print(record, '0.1.0')
records = [dict(version=v, load=0) for v in ('1.0.0', '1.0.1')]
for r in records:
print('\nRecord: ', r)
svu.fix_record(r)
pprint.pprint(svu.fixes)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment