Show HN: Python library to overload functions based on interpreter version
github.com
github.com
The project should also maybe note that it doesn’t help at all with Python 2 compatibility. A newer Python user might assume it enables freely mixing Python 2 and Python 3 in the same file, but even without reading the code I’m pretty sure the library doesn’t (and couldn’t) support this.
Lacking that, there's no way to write a (single file) script where a Python 2 interpreter would just get a "You need Python 3" error instead of a syntax error. Because testing for the interpreter version happens too late...after the syntax error already happened.
Perl's "BEGIN {}" block lets you easily test for versions or language features before the script itself is parsed.
You can also catch a SyntaxError like any other exception.
Yes, people are able to write backwards compatible scripts... by avoiding some features, or by using more than a single file.
What I'm describing was just a desire for a very simple "this script requires python version >= X" error, with the ability to write unfettered python version X code below it, in a single file script.
import sys
if sys.version_info.major < 3:
raise Exception("this script requires python version >= 3")
plus an extra line or two if you're worried about minor versions.print(f"whee")
And run it with python2, or a python3 < 3.6.
> cat test.py
import sys
if sys.version_info.major < 3:
raise Exception("this script requires python version >= 3")
print(f"test")
> python2 test.py
File "test.py", line 5
print(f"test")
^
SyntaxError: invalid syntax
Doesn't seem that simple from __future__ import absolute_import, division, print_function, unicode_literals
This will give you syntax much closer to Python 3 in Python 2.7. The remaining differences can be papered over with function and class helpers. Well, except for metaclasses, but I doubt you'd have need for those in a single-file script. import sys
if sys.version_info[0] < 3:
print("Needs Python 3+")
sys.exit(1)
else:
prog = r'''
def help():
print("Help: ...")
def do_spam(*args):
print("spamming")
def main(args):
match args:
case [subcommand, *args]:
return globals()[f'do_{subcommand}'](*args)
case _:
return help()
if __name__ == '__main__':
try:
status = main(sys.argv[1:])
sys.exit(int(status if status is not None else 0))
except Exception as e:
import traceback
traceback.print_exc(e)
sys.exit(1)
'''
compile(prog, '', 'exec')
exec(prog) 0_0 # Python >= 3.6 is required
I have a collection of such incantations for many Python versions here: 0_0
syntactically okay? I typed it into an AST visualizer (I'm on mobile) and if I'm not mistaken this is equivalent to `0`. But why?Your question about 0_0 is just 00 which is 0.
(This is really common in other modern languages. Readable example: 1_000_000 vs 1000000)
(It's only Standards Track at the moment)
I imagine it would have been more popular than 17% if it were available when v2 -> v3 pain was at the high point.
The whole reason this thought popped in my head is that I still have people asking today: "Tried to run your script, and I got a syntax error...is it broken?". When the issue is that the default python on their box is 2.x.
Nowaday, I use shiv to produce the zipapp:
https://shiv.readthedocs.io/en/latest/
It's handy because you can also now embed 3rd party dependencies, even for small quick scripts if you wish.
Therefore, I suspect it is not possible to write a test in the BEGIN block which avoids a version-dependent syntax error elsewhere in favor of terminating with a graceful error.
# -*- coding: ... -*-
The trick is that encoding names come from an extensible registry of codecs (https://docs.python.org/3/library/codecs.html#codecs.registe...), so you can register your own. And when you look at codec API, you get the raw byte stream as input, and spit out strings - so it's basically as powerful as reader macros.The problem is getting that registration code to run before your script starts. Easy if you have multiple scripts, but it requires .pth hacks if you go for single-file.
I think 200LOC and a complex registration/namespace/cache system is a bit much to avoid an if statement around a def for a handful of functions. This is especially true since this just turns the if into a decorator, and imports will still need to be done the old way for compatibility. The cognitive overhead of this is not a good tradeoff.
My next goal is to see if I can make the resolution "static", i.e. Once all the functions are loaded, avoid the dictionary lookup at runtime, and just assign the appropriate function version as the bare module attribute.
It's fun to do things like this in python, simply because you can. But it's also important to point out that there's a lot of things you _shouldn't_ (this may be one of them!). I'm glad to see this sparked some discussion!
If I think of this problem in a Lisp frame, it does not scream "use macros" at me.
Btw, I've taken the liberty to post your project to my Python News aggregator here: https://news.python.sc/item/ff8f92de-666e-4913-8aa3-27af90df...
If you own all the callers of your api in the same repo, you probably don’t want this, you just replace all the old callers along with the implementation.
What is the point of making two versions of the functions, when one of them already supports every Python you target?
How are you "using the latest stuff", which here is a syntax shortcut, if you have to write out and maintain the old syntax too?