I’ve never used Elm properly, but since I found out about it a couple years ago, have been jelous of it’s automated versioning.
In theory at least (and from what I hear in practice too!), when you publish a package for Elm, it compares the type signature between your current and previous version, and determines from that, whether you are introducing a breaking change. The logic is pretty solid, if you have a fully typed language, the type signatures of your functions should indicate what calls will and won’t work, and therefore what is and isn’t a breaking change. I think it works along the lines of:
- Major: Changes type signature of existing functions
- Minor: Introduces new functions
- Patch: No type signature changes
I work with data pipelines/models/charts/predictions etc day-to-day, so Python’s been my professional use language for a while now. I’ve often wondered if something like this could work in theory for Python. I heard a joke once that most libraries are “brag-ver” where the places are based on how much you want to show off your release, where you have MAJOR.MINOR.PATCH it breaks down to:
- MAJOR: CHECK OUT MY RELEASE! (Big deal!)
- MINOR: It’s a release! (Normal deal)
- PATCH: Hope nobody notices this. . . (bugfix)
Anyway, I’ve been noodling with actually guaranteeing semver within python, and a tiny bit of experimenting is leaving me feeling optimistic. If we take an approach of:
- Build out a hashable dataclass for each function, class and method
- Capture argument and return specs in those dataclasses (and additional metadata like, asynchronous-ness and importable location)
- Use python’s AST to parse through a package and build these dataclasses into a set
We can then just use basic set comparison to provide a good-enough(ish) guarantee of semver. Set equality is, no change in function, class or method signatures, which is a patch change, something like:
def cool_func(n: int) -> int:
return n * 7
to:
def cool_func(n: int) -> int:
return n * 7
Superset is a minor change, such as changing to:
def cool_func(n: int) -> int:
return n * 7
def cooler_func(n: int) -> int:
return n * 7 * 7
And anything else we can treat as a major (breaking) change like:
def cool_func(n: int, x: int) -> int:
return n * x
Currently this has a pretty big ommision, in that we’d also be treating this as a breaking change:
def cool_func(n: int, x: int = 7) -> int:
return n * x
But I think making clever use of the __hash__ function could potentially solve this.
Anyway, that’s where I’m at now. I’m gonna continue goofing around with this idea, and see if i can resolve the args issue, and ideally integrate with git. We’ll see how this goes, I’m also curious if, assuming I can get this up and running, version bumps are more or less frequent than I’d expect.