Import problem when separating applications and package tests

Solution 1:

Your goal is really a bit unpythonic. But sometimes, you have to break the rules to free your heart.

You can solve the problem by checking for the __package__ attribute in myapp/mypackage/__init__.py like this:


# hint from there: https://stackoverflow.com/a/65426846/4865723
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

if __package__:
    from ._mymoduleA import foo
else:
    from _mymoduleA import *
    

In this case myapp/mypackage/tests/test_all.py the code gets a little simpler:


import importlib
import unittest

if not importlib.util.find_spec('mypackage'):
    from __init__ import *

import mypackage
from mypackage import mymoduleB


class TestAll(unittest.TestCase):
    def test_A(self):
        self.assertEqual(1, mypackage.foo())

    def test_B(self):
        self.assertEqual(2, mymoduleB.bar())

All other files remain unchanged.

As a result, you get the ability to run tests from both /myapp and /myapp/mypackage folder. At the same time, there is no need to hardcode any absolute paths. The app can be copied to any other file system locations.

I hope it will useful for you.

Solution 2:

I created an import library a couple of years ago. It works on pathing. I used it to create a plugin system where I could essentially install and import multiple versions of any library (with some limitations).

For this we get the current path of the module. Then we import the package using the path. This library will automatically add the proper path to sys.path.

All you need to do is install pylibimp pip install pylibimp and edit myapp/mypackage/tests/test_all.py

import os
import pylibimp
import unittest


path_tests = os.path.join(os.path.dirname(__file__))
path_mypackage = os.path.dirname(path_tests)
path_myapp = os.path.dirname(path_mypackage)
mypackage = pylibimp.import_module(os.path.join(path_myapp, 'mypackage'), reset_modules=False)


class TestAll(unittest.TestCase):
    def test_A(self):
        self.assertEqual(1, mypackage.foo())

    def test_B(self):
        self.assertEqual(2, mypackage.mymoduleB.bar())

I believe the background is fairly simple.

import os
import sys

sys.path.insert(0, os.path.abspath('path/to/myapp'))

# Since path is added we can "import mypackage"
mypackage = __import__('mypackage')

sys.path.pop(0)  # remove the added path to not mess with other imports

I hope this is what you are looking for.