Search code examples
pythonenvironment-variablesdirectory-structure

Avoid using environment variables when doing variable imports


I have been working with a project for which I give the choice of using two backends (backends 1 and 2, let's say) following what they do in this project. However, that project counts on having already defined environment variables for deciding which backend to use before executing the code. It would not be the case for the code I'm writing.

I would like to know if there is any alternative to use environment variables in this scenario so that, on execution time, I can load one backend or another depending on the value of a variable. The general structure of my project is as follows:

Project

I thought of directly setting the environment variable directly in the python code (os.environ['NAME_OF_ENV_VARIABLE'] = 'BACKEND 1'), but that feels potentially insecure and I really dislike the idea, even if the variable name is... unique. Given this need, I would like to know if it is possible to have some kind of variable spanning different files so that, when I do import a module, the __init__.py file can disambiguate between the backends.

PS: Maybe what I'm doing makes no sense whatsoever.


[UPDATE] Some more information about the problem, reduced to its minimal extension. My main file processes some data and is the following:

from argparse import ArgumentParser
from utils.loader import load_data
from utils.do import do_stuff

def main(config):
    data = load_data(config)
    do_stuff(config, data)

if __name__ == '__main__':
    # Retrieve input data
    parser = ArgumentParser()
    parser.add_argument('--backend', type=str, default='backend 1', help='backend to use')
    inputs = parser.parse_args()

    config = "backend 1" if inputs.backend == "1" else "backend 2"

    # Call main function
    main(config)

The data loader load_data(config) I guess is not important for this. Then, the file containing do_stuff(data) is the following:

import backend

def do_stuff(config, data):
    # Do some really important stuff that is coded in backend 1 and backend 2
    a = backend.do_something(data)
    print(a)

It simply loads the backend (!!!) and does something. The do_stuff(data) function itself does something coded in either backend 1 or backend 2:

def do_something(data):
    data.values = "Value obtained through functions of 'BACKEND 1' (same function names and inputs, different backends used)"

and

def do_something(data):
    data.values = "Value obtained through functions of 'BACKEND 2' (same function names and inputs, different backends used)"

Finally, the backend module has in itself the following __init__.py file:

from .load_backend import do_something

Which loads from the load_backend.py file, which simply disambiguates the backend given an environmental variable:

from __future__ import absolute_import
from __future__ import print_function
import os
import sys

# Default backend: backend 1
if 'ENVIRONMENT_VARIABLE' in os.environ:
    _BACKEND = os.environ['ENVIRONMENT_VARIABLE']
else:
    _BACKEND = 'backend 1'

# Import backend functions.
if _BACKEND == "backend 1":
    sys.stderr.write('Using backend 1\n')
    from .backend_1 import *
elif _BACKEND == "backend 2":
    sys.stderr.write('Using backend 2\n')
    from .backend_2 import *
else:
    raise ValueError('Unable to import backend : ' + str(_BACKEND))


def backend():
    """Publicly accessible method
    for determining the current backend.
    # Returns
        String, the name of the backend
    # Example
    ```python
        >>> backend.backend()
        'backend 1'
    ```
    """
    return _BACKEND

What I want is to reduce this last environment variable with anything else, but I don't know what can I use.


Solution

  • Like @DanielRoseman asked, I would just pass the backend argument around. For example in load_backend, while changing your code as litte as possible:

    from __future__ import absolute_import
    from __future__ import print_function
    import os
    import sys
    
    def backend(backend):
        """Returns the wanted backend module"""
        # Import backend functions.
        if backend == "backend 1":
            sys.stderr.write('Using backend 1\n')
            from . import backend_1 as backend_module
        elif backend == "backend 2":
            sys.stderr.write('Using backend 2\n')
            from . import backend_2 as backend_module
        else:
            raise ValueError('Unable to import backend : ' + str(_BACKEND))
    
        return backend_module
    

    An improvement could be to use importlib to dynamically import the backend and move the magic strings to a constant:

    ...
    import importlib
    
    BACKENDS = {
        "backend 1": "backend_1",
        "backend 2": "backend_2"
    }
    
    def load_backend(backend):
        try:
            module = importlib.import_module(
                BACKENDS[backend]
            )
        except KeyError:
            raise ImportError('Unable to import backend : %s' % backend)
    
        sys.stderr.write('Using %s\n' % backend)
        return module
    

    So you can do this in the do_stuff file:

    import load_backend
    
    def do_stuff(config, data):
        # Do some really important stuff that is coded in backend 1 and backend 2
        backend = load_backend.backend(config)
        a = backend.do_something(data)
        print(a)
    

    Another way to go about this is to use a singleton pattern, where you set the backend variable once (and other settings you want broadly available):

    in a settings.py or whereever:

    class SettingSingleton(object):
        _backend = None
    
        def __new__(cls, backend=None, *args, **kwargs):
            cls._backend = cls._backend or backend
            return super(SettingsSingleton, cls).__new__(cls, *args, **kwargs)
    
        @property
        def backend(self):
            return self._backend
    
    

    You can initialize that in the main.

    from argparse import ArgumentParser
    from utils.loader import load_data
    from utils.do import do_stuff
    from settings import SettingSingleton
    
    
    def main(config):
        SettingsSingleton(backend=config)
        data = load_data(config)
        do_stuff(config, data)
    
    ...
    

    Now you can do something like:

    from __future__ import absolute_import
    from __future__ import print_function
    import os
    import sys
    
    from settings import SettingsSingleton
    
    _BACKEND = SettingsSingleton().backend
    
    # Import backend functions.
    if _BACKEND == "backend 1":
        sys.stderr.write('Using backend 1\n')
        from .backend_1 import *
    elif _BACKEND == "backend 2":
        sys.stderr.write('Using backend 2\n')
        from .backend_2 import *
    else:
        raise ValueError('Unable to import backend : ' + str(_BACKEND))
    
    

    Downside to this is that it is somewhat implicit.