Search code examples
pythonimportcommand-line-argumentsimporterrorargv

Python - command line argument extracted as a string ImportError


I am trying to do the following:

In CMD:

python __main__.py 127.0.0.1

The in main.py:

address = sys.argv[1]

Then in my config.py file, I am importing the address like this:

from __main__.py import address

...

EXAMPLE_URL = f"http://{address}/login"

And I am using the URL in a simple test scenario, which is imported from config and I get the following error:

ImportError: cannot import name 'address' from '__main__' (__main__.py)

This is my directory structure:

QA System/
├── config/
│   ├── config.py
│   ├── __init__.py
├── .... some other unneccessary stuff
└── tests/
    ├── test_scenarios
       ├── test_scenario_01.py
       ├── test_scenario_02.py
       ├── __init__.py
    |── test_suite.py
    |── __init__.py
|
|-- __main__.py   < --- I launch tests from here
|-- __init__.py

It seems the error is in config file during the imports, but I cannot understand where the error is. Thanks in advance!

main.py file:

import argparse
import sys

from tests.test_suite import runner

if __name__ == "__main__":
    address = str(sys.argv[1])
    runner()   # This runs the tests, and the tests also use config.py for
                 various settings, I am worried something happens with the
                 imports there.

Solution

  • You have a circular import

    When you import a module in Python, say, import __main__ as in your example, a module object is created for the namespace of the module, which is initially empty. Then, as the code in the body of the module is executed--variables assigned, functions and classes defined, etc. that namespace gets filled in in order. E.g. take the following script:

    $ cat a.py
    print(locals())
    an_int = 1
    
    print("after an_int = 1")
    print(locals())
    
    def a_func(): pass
    print("after def a_func(): pass")
    print(locals())
    

    Then run it:

    $ python a.py
    {'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__file__': 'a.py', '__doc__': None, '__package__': None}
    after an_int = 1
    {'an_int': 1, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'a.py', '__package__': None, '__name__': '__main__', '__doc__': None}
    after def a_func(): pass
    {'an_int': 1, 'a_func': <function a_func at 0x6ffffeed758>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'a.py', '__package__': None, '__name__': '__main__', '__doc__': None}
    

    You can see the namespace gets filled in line-by-line.

    Now say we modify it like:

    $ cat a.py
    print(locals())
    an_int = 1
    
    print("after an_int = 1")
    print(locals())
    
    import b
    print("after import b")
    
    def a_func(): pass
    print("after def a_func(): pass")
    print(locals())
    

    and add b.py:

    $ cat b.py
    import sys
    print('a is in progress of being imported:', sys.modules['a'])
    print("is a_func defined in a? `'a_func' in sys.modules['a'].__dict__`:",
          'a_func' in sys.modules['a'].__dict__)
    from a import a_func
    

    and run it like:

    python -c 'import a'
    

    You'll get some output ending in a Traceback:

    ...
    after an_int = 1
    {'an_int': 1, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'a.py', '__package__': None, '__name__': '__main__', '__doc__': None}
    a is in progress of being imported: <module 'a' from '/path/to/a.py'>
    is a_func defined in a? `'a_func' in sys.modules['a'].__dict__`: False
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "a.py", line 7, in <module>
        import b
      File "b.py", line 1, in <module>
        from a import a_func
    ImportError: cannot import name 'a_func'
    

    If however you move import b to after the definition of a_func:

    $ cat a.py
    print(locals())
    an_int = 1
    
    print("after an_int = 1")
    print(locals())
    
    def a_func(): pass
    print("after def a_func(): pass")
    print(locals())
    
    import b
    print("after import b")
    

    And again run python -c 'import a' you'll see it works, and the output ends with "after import b".

    BONUS QUESTION: Why did I run python -c 'import a' and not just python a.py? If you try the latter, the previous version will actually work and will appear to import a.py twice. This is because when you run python somemodule.py it is not imported initially as somemodule, but rather as __main__. So from the import system's perspective the a module has not been imported yet when running from a import a_func. A very confusing caveat.


    So in your case if you have something like __main__.py:

    import config
    address = 1
    

    and in config.py:

    from __main__ import address
    

    when you run python __main__.py, by the time it runs import config, address isn't assigned yet, so the code in config that tries to import address from __main__ results in an ImportError.

    In your case it's a little more complicated because you aren't importing config directly in __main__ from what it looks like, but indirectly that's still what's happening.

    In this case, you shouldn't be passing variables around between modules by way of import statements. In fact, __main__ should really just be a command-line front-end to your code, and the rest of your code should be able to work independently from it (e.g. a good design would allow you to run from tests.test_runner import runner and call runner() from an interactive Python prompt, in principle, even if you never actually use it that way).

    So instead make runner(...) take arguments for whatever options it requires. Then __main__.py would just take those arguments from command-line arguments. E.g.:

    def runner(address=None):
        # Or maybe just runner(address) if you don't want to make the
        # address argument optional
    

    Then

    if __name__ == '__main__':
        address = sys.argv[1]  # Use argparse instead if you can
        runner(address=address)