Stop importing file conditionally

I have a situation where a legacy python2 application has code that I cannot change. However it has an upgrade mechanism, where it checks if a provided file is of a newer version than itself, and replaces itself by that newer file. However, it does import said upgrade file to check if the version is newer.

Think of it like so:

# Pseudo-code  "upgrade" mechanism in Python2 version:

# ...

if upgrade_file:
    maybe_new_version = import_file(upgrade_file)
    if maybe_new_version.__version__ > current_version:
        replace_self_with(upgrade_file)

# ...

now for the upgrade file, which is python3, I’d like to do something like this:

# start python2 compatible code

import sys

__version__ = "0.3.0"  # this will trigger the self-update 

if sys.version_info[0] < 3:
    stop_importing_file_here()  # Ignore the rest of the file! Avoid Python 2 running into Python3-syntax in the rest of the file

# end python2 compatible code

... # rest of the file is the new python 3 version

Now is there some mechanism I can make the python2 import system ignore the rest of the file, i.e. the part of the code that is python3 and not python2 compatible? I.e. I’m searching for a real world thing to replace stop_importing_file_here() in the example code. And I cannot change the code in the older version or get multiple files to the system.

Calling sys.exit(0) would end the parent process doing the importing, scratch that.

Python first parses code, triggering syntax errors even in unreachable code, before executing it in a second pass. So I don’t think this is solvable at runtime within the same file.

Can you put the Python 3 code in another dependency, which is only imported if sys.version_info >= (3,):?

1 Like

Sadly no, I can only get a single file to the system in question.

I’m almost certain there’s some low-level mechanism or hack in python to achieve what I want, but I’m not that deep down the rabbit hole.

You can put the actual code into strings, one for py2 and one for py3. You can then conditionally select one of them and run it with exec.

5 Likes
import sys

def check_version()
    return sys.version_info[0] >= 3

def process():
    # other imports and logics
    ...
    
if check_version():
    process()

?
Or do you need a custom loading of modules in the form of files?
For the second case there are various options through

That won’t work, as all syntax is first parsed by Python, even if that code is unreachable at run-time, and only executed afterwards on a second pass. Even if it did work, none of the Python 3 only stuff would be importable, as it’s only within the closure of process() (the function body’s scope).

process.py

import sys

def check_version():
    return sys.version_info[0] >= 3

def process():
    f'other imports and logics for Python 3.  {sys.version_info=}'
    
if check_version():
    process()
c:\...\>py -2.7 process.py
  File "process.py", line 7
    f'other imports and logics for Python 3.  {sys.version_info=}'
                                                                 ^
SyntaxError: invalid syntax

Oh, I thought you were picking on the syntax.
Yes, I was thinking of discussing this further.
In this module can leave only the basic logic of imports and transfer all potentially problematic code to modules that will be loaded from this.
That’s, it will again be a kind of proxy implementation to the main python3 code.
In general, can create separate logic on proxy modules, which will be full-fledged classes and objects. There is such a point in my security concept. But the implementation will be much more difficult.
And then can create the concept of lazy code loading. Although this has already been implemented in importlib through LazyLoader. By the way, can really try to apply it in this case.

You can have a python2 file and a python3 file and import that based on whether 2 or 3 is running.

import sys

__version__ = "0.3.0"  # this will trigger the self-update 

if sys.version_info[0] < 3:
    from python2_file import *
else:
    from python 3_file import *

This should accomplish what you need.

2 Likes

So far the “make it a string and then eval” suggestion seems to be the only one working, though its not pretty. Thanks for that in any case, it’s probably good enough. Another solution would be to comment out the python3 code and then extract that comment and eval it conditionally if on py3.

If I could get two files on the machine, I’d probably do:

import sys

__version__ = "0.3.0"

print("I'm a shim for the py3 version")

if sys.version_info >= (3,):
    import os.path

    dir_ = os.path.dirname(os.path.realpath(__file__))
    file_ = os.path.join(dir_, "py3version.py")
    os.exeve("python3", file_, sys.argv[1:])

Also you can make custom loads by

import sys

def load(name, path):
    import importlib.util
    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module

if sys.version_info[0] >= 3:
    load(some_name, some_path)

You are still failing to understand the constraints OP has: they can’t freely load files onto the server, they only get one python file.

@OP if you ever need more files, you can do something similar to ensurepip: base64 encoded zip archive, unpacked on demand into a temp file location and imported.

1 Like

It is unclear why there is a restriction on a single file.

Yes, you’re right. The module zipimport has been in existence since version 2.3.
It seems like this is the best variant.
By the way, a question. And are there any other standard modules besides them where loaders are implemented?

The restriction is there because it’s a closed system and it is legacy. It is stupid and arbitrary but it is what it is. :cry:

It doesn’t matter. You have been offered a great universal variant

Moreover, its support is available by default in the main import. So you just make an archive and import it.

1 Like