Reputation:
I. Preface: The application directory structure and modules are listed at the end of the post.
II. Statement of the Problem: If the PYTHONPATH is not set the application runs, but the unittest fails with the ImportError: No module named models.transactions. This happens when trying to import
Transactions in app.py. If the PYTHONPATH is set to /sandbox/app
both the application and
unittest run with no errors. The constraints for a solution are that the PYTHONPATH should not have to be set, and
the sys.path should not have to be modified programmatically.
III. Details: Consider the case when the PYTHONPATH is set and the test_app.py
is run as a package /sandbox$ python -m unittest tests.test_app
. Looking at the print statements for __main__
sprinkled throughout the code:
models : app.models.transactions
models : models.transactions
resources: app.resources.transactions
app : app.app
test : tests.test_app
The unittest imports the app first, and so there is app.models.transactions
. The next import that the app
attempts is resources.transactions
. When it is imported it makes its own import of models.transactions
, and
then we see the __name__
for app.resources.transactions
. This is followed by the app.app
import, and then finally the unittest module tests.test.app
. Setting the PYTHONPATH allows the application to resolve models.transactions!
One solution is to put the models.transactions
inside resources.transaction
. But is there another way to handle the problem?
For completeness, when the application is run the print statements for __main__
are:
models : models.transactions
resources: resources.transactions
app : __main__
This is expected, and no imports are being attempted which are above /sandbox/app
or sideways.
IV. Appendix
A.1 Directory Structure:
|-- sandbox
|-- app
|-- models
|-- __init__.py
|-- transactions.py
|-- resources
|-- __init__.py
|-- transactions.py
|-- __init__.py
|-- app.py
|-- tests
|-- __init__.py
|-- test_app.py
A.2 Modules:
(1) app:
from flask import Flask
from models.transactions import TransactionsModel
from resources.transactions import Transactions
print ' app : ', __name__
def create_app():
app = Flask(__name__)
return app
app = create_app()
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)
(2) models.transactions
print ' model : ', __name__
class TransactionsModel:
pass
(3) resources.transactions:
from models.transactions import TransactionsModel
print ' resources: ', __name__
class Transactions:
pass
(4) tests.test_app
import unittest
from app.app import create_app
from app.resources.transactions import Transactions
print ' test : ', __name__
class DonationTestCase(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_transactions_get_with_none_ids(self):
self.assertEqual(0, 0)
if __name__ == '__main__':
unittest.main()
Upvotes: 6
Views: 5402
Reputation:
It is worthwhile mentioning up front that the Flask documents say to run the application as a package, and set the environment variable: FLASK_APP
. The application then runs from the project root: $ python -m flask run
. Now the imports will include the application root, such as app.models.transactions
. Since the unittest is being run in the same way, as a package from the project root, all imports are resolved there as well.
The crux of the problem can be described in the following way. The test_app.py needs access to sideways imports, but if it runs as a script like:
/sandbox/test$ python test_app.py
it has
. This means that imports, such as __name__
==__main__
from models.transactions import TransactionsModel
, will not be resolved because they are sideways and not lower in the hierarchy. To work around this the test_app.py can be run as a package:
/sandbox$ python unittest -m test.test_app
The -m
switch is what tells Python to do this. Now the package has access to app.model
because it is running in /sandbox
. The imports in test_app.py
must reflect this change and become something like:
from app.models.transactions import TransactionsModel
To make the test run, the imports in the application must now be relative. For example, in app.resources:
from ..models.transactions import TransactionsModel
So the tests run successfully, but if the application is run it fails! This is the crux to the problem. When the application is run as a script from /sandbox/app$ python app.py
it hits this relative import ..models.transactions
and returns an error that the program is trying to import above the top level. Fix one, and break the other.
How can one work around this without having to set the PYTHONPATH? A possible solution is to use a conditional in the package
to do conditional imports. An example of what this looks like for the __init__
.pyresources
package is:
if __name__ == 'resources':
from models.transactions import TransactionsModel
from controllers.transactions import get_transactions
elif __name__ == 'app.resources':
from ..models.transactions import TransactionsModel
from ..controllers.transactions import get_transactions
The last obstacle to overcome is how do we get this pulled into the resources.py
. The imports done in the
are bound to that file and are not available to __init__
.pyresources.py
. Normally one would include the following import in resources.py
:
import resources
But, again, is it resources
or app.resources
? It would seem like the difficulty has just moved further down the road for us. The tools offered by importlib
can help here, for example the following will make the correct import:
from importlib import import_module
import_module(__name__)
There are other methods that can be used. For example,
TransactionsModel = getattr(import_module(__name__), 'TransactionsModel')
This fixed the error in the present context.
Another, more direct solution is using absolute imports in the modules themselves. For example, in resources:
models_root = os.path.join(os.path.dirname(__file__), '..', 'models')
fp, file_path, desc = imp.find_module(module_name, [models_root])
TransactionsModel = imp.load_module(module_name, fp, file_path,
desc).TransactionsModel
TransactionType = imp.load_module(module_name, fp, file_path,
desc).TransactionType
Just a note concerning changing the PYTHONPATH with sys.path.append(app_root)
in resources.py
. This works well and is a few lines of code located where it needs to be. Additionally, it only changes the path for the executing file and reverts back when finished. Seems like a good use case for unittest. One concern might be when the application is moved to different environments.
Upvotes: 3
Reputation: 3012
TL;DR: if you can, you should change the two imports:
from models.transactions import TransactionsModel
from resources.transactions import Transactions
to
from app.models.transactions import TransactionsModel
from app.resources.transactions import Transactions
Longer version
When you say:
If the PYTHONPATH is not set the application runs...
how do you run the app? I am guessing something like...
cd sandbox/app
python app.py
because either the imports in app.py
are incorrect (missing the top-most app
module) or else running the app
should equally fail.
IMO you also don't need (stricly) to make tests
a module (i.e., you can drop tests/__init__.py
) and just run the tests as:
python tests/test_app.py
The whole point is that .
(your current directory) is always by default included in PYTHONPATH
and it is from there that modules are loaded/searched for imports - in which case, when your tests executes from app.app import create_app
the first line in app.py
:
from models.transactions import TransactionsModel
will cause the error (there is no ./models
module/directory).
If you can't change the imports in the app.py
application module, or are otherwise constrained, the only option I can think of (other than modifying PYTHONPATH
or manipulating sys.path
, which you explicitly excluded), the only other option would be to:
cd sandbox/app
python ../tests/test_app.py
but then you'd have to change, in your unit tests, your imports:
from app import create_app
from resources.transactions import Transactions
In other words, you can't have your cake and eat it :)
without changing the Python lookup path, you need to have all modules to start from the same place (.
) and thus be consistent.
Upvotes: 1