Runtime code versioning in Python
Written by Marco Vellinga on 28th June 2017
In our quest to find solutions for versioning internal code paths we found a lot of information about versioning. The only problem with this information is that 99% of the sources are talking about how you should version your API for consumers. That, of course, is a very interesting topic but we wanted to know some more about how you would organize the code behind those versions.
Questions without answers
Do you copy paste existing functions and into a new module called v2? Do you implement some sort of ‘if elif else’ structure to call the right function when someone needs version 1 or version 2? How are you going to maintain these different versions and code? How do you keep track of which function is used for what version?
We ended up with a lot of question and no answers.
Versioning internal code
The next step was to think about what would be really awesome for versioning internal code. The most obvious solutions like a switch-like structure or a folder structure with duplicate code weren’t really satisfying. What if we could just import the function or class we needed only once and call/init different versions with it? What if we could use inheritance to only modify that what was needed for the new version and leave the rest untouched? Our main goal was to find a way to facilitate those solutions and build it.
How to version runtime code?
We ended up creating a decorator to indicate versions for a certain function or class. This decorator builds a version table of all versions of the same function/class. It then wraps the function or class in a proxy-like class that exposes the different versions of the function or class. This way you only have to import the function once and have access to all possible versions. This works for function, classes and the classes you inherit from. Time to talk code, this is how it would look like:
from versionary.decorators import versioned @versioned(1) def return_number(): return 1 @versioned(2) def return_number() return 2 assert return_number.v1() == 1 assert return_number.v2() == 2 @versioned(1) class NumberCache(object): def get_int(): return 1 def get_float(): return 13.37 @versioned(2) class NumberCache(NumberCache.v1): def get_int(): return 2 assert NumberCache.v1().get_int() == 1 assert NumberCache.v1().get_float() == 13.37 assert NumberCache.v2().get_int() == 2 assert NumberCache.v2().get_float() == 13.37
The solution: Versionary
As you can see this makes it very easy to version internal code and call a different version. You also only have to import the function/class once. We wrote some functions to validate your whole system for missing or wrong use of versioning. We called the package “Versionary” and it is open source. You can find it on PyPi and GitHub.