pytest can't find helper modules located under the tests directory - python

When using PyCharm to launch my tests I have no import problems at all, but if I do it in the terminal pytest complains about modules that cannot find. Precisely those under tests/tools.
Project folder structure:
src/
app/
tests/
end_to_end/ # End to end/Acceptance tests
tools/ # Helper modules used by some tests
unit # Unit tests
setup.py
Relevant parts of setup.py:
setup(...
packages=find_packages('src'),
package_dir={'': 'src'},
install_requires=['requests'],
test_suite='tests',
tests_require=[
'flexmock',
'pytest',
'wsgi_intercept'
])
Import line that fails when executing py.test tests/end_to_end, no matter if the virtualenv is active or not. Modules imported from the app package are fine:
from tests.tools.foomodule import Foo # Fails
from app.barmodule import Bar # Doesn't fail
I had to install my package with pip install -e packagename in order to be available for pytest when running tests from the command line. I suppose I will have to add the tools folder as a package in my setup.py, but this feels ugly to me because that modules are only for aiding in the testing process and aren't meant to be distributed.
On the other hand I could launch my tests with python setup.py test following the integration advices from pytest website.

probably you do not have __init__.py in your tests and/or tests/tools directory.
If missed python do see them as packages and is not able to import them.
You can:
add __init__.py (BAD see here
add tests/tools to your PYTHONPATH in your conftest.py and import a file (from import tests.tools.MODULE to import MODULE)

To be able to import modules residing outside src folder I went to "Project settings" in PyCharm, and there in "Project structure" I marked tests/end_to_end and tests/unit as source folders.
I also changed my mind and decided to put my helper modules inside those folders. This way I can import them directly and my tests run fine in terminal too because the current working directory is always added to the PYTHONPATH.

Related

How can I import a module folder from a tests folder

I'm working on a project with this folder structure using pytest for tests
project
├── example
│   ├── __init__.py
│   └── example.py
├── setup.py
└── tests
└── test_1.py
I'm trying to import example.py from test_1.py but every way I've tried hasn't worked.
I've tried using sys.path.append('../) then import example and just using from ... import example but neither of these methods has worked. sys.path.append('../') didn't let me import example and from ... import example errored with ImportError: attempted relative import with no known parent package. I've also tried using python setup.py develop and it let me import example, but none of the methods are imported.
This is the code that is in test_1.py. It currently imports example but it has no content.
import sys
import os
sys.path.append("../example/")
import example
example.test()
Contents of example.py
class test:
def __init__(self):
self.a = 1
self.b = 2
The setup.py is the template from here
I think your problem is that the path resolution isn't functioning as you expect. In your example.py file do this:
import sys
import os
dir_path = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.realpath(f"{dir_path}/../example"))
import example
example.test()
Then, when in your root directory run python tests/test_1.py. This should execute test() from the example directory.
The realpath() function is the key here, as it will resolve the actual system path. If that isn't resolved it assumes whatever you're handing it is from the root (i.e. /example, which is wrong).
Use of sys.path.append results in flake8 reporting E402 module level import not at the top of the file. There are various hacks to this around which essentially depend on disabling E402; directly or by deception. There are at least two other ways.
Add __init__.py to the tests directory so that pytest loads the tests from within the parent dir and natively finds the include directory.
pip install -e . from within the parent and have the package loaded in a developer environment.
For the sake of consistency I started with uninstalling every package (so pip freeze returns nothing) and creating virtual environments for each of the approaches. I omitted the setup.py file favoring a minimal pyproject.toml file. Then pip install pytest and pip install flake8 in both. For packages, the second approach then shows an additional example package, i.e.
(.venv) PS C:\xxxx\Stackoverflow\proj_hatch> pip freeze
attrs==22.1.0
colorama==0.4.5
# Editable install with no version control (example==0.0.1)
-e c:\xxxx\stackoverflow\proj_hatch
flake8==5.0.4
iniconfig==1.1.1
mccabe==0.7.0
packaging==21.3
pluggy==1.0.0
py==1.11.0
pycodestyle==2.9.1
pyflakes==2.5.0
pyparsing==3.0.9
pytest==7.1.3
tomli==2.0.1
The net outcome is that both approaches have a working pytest.
(.venv) PS C:\xxxx\Stackoverflow\proj_hatch> pytest
================ test session starts =========================
platform win32 -- Python 3.10.8, pytest-7.1.3, pluggy-1.0.0
rootdir: C:\xxxx\Stackoverflow\proj_hatch
collected 1 item
tests\test_1.py . [100%]
=================== 1 passed in 0.01s ========================
(.venv) PS C:\xxxx\Stackoverflow\proj_hatch>
In both versions set example/__init__.py as below. This has an additional benefit of allowing the structure (which function in which file) to change later while keeping the import for other applications stable.
from example.example import test
__all__ = [
"test",
]
In both versions the test is:
import example
def test_1():
ex1 = example.Test()
assert ex1.b == 2
With the first approach it is necessary to add .\tests\__init__.py so that pytest loads the test from within the project directory and is able to find the example directory. I expect this has some implications for how you write tests.
With the second approach I started off using hatch however it seemed to work with little more than moving the example directory to src\example and by creating a pyproject.toml file. I've a suspicion there is some magic in the later versions of python that help. I am using 3.10.8 for this exercise.
[project]
name = "example"
version = "0.0.1"
Example:
sys.path.insert(0, "path_to_your_project")

Import a module from a sub package not working

My file structure is
project/
__init__.py
features/
__init__.py
CompareText.py
tests/
test.py
in test.py I am trying to import CompareText
from project.features import CompareText
I get an error of:
ModuleNotFoundError: No module named 'features'`
I checked the documentation and I think my import statement is correct. How can I fix it?
Add an __init__ file in test. Your project directory should look like this:
project/
__init__.py
features/
__init__.py
CompareText.py
tests/
__init__.py
test.py
Then in project/tests/test.py the following import statement will work:
from ..features import CompareText
Oh, and this will still raise an error if you try to run it directly. In the question you said you tried to import it like this:
from project.features import CompareText
This will only work if the parent directory of project is in Python's module search path. So, if you want to run the tests directly then modify the module search path as needed (See: sys.path).
Your import statement is supposed to look like this :
(But make sure your working directory is the same directory as your project folder is located during execution)
from project.features import CompareText
This is supposed to work if your current path while executing the script has the project folder
If you execute it while inside project folder you can use:
from .features import CompareText
Hope this helps!
I assume you are running test.py as a script. test.py needs to find the project package and two ways to do that are to make your project installable or to hack sys.path.
Installable
First, change your directory structure a bit so that project is a subdirectory of some anonymous directory you happen to be using for development. If you are checking this stuff into source control, it needs to be written so that it can be checked out anywhere. Move tests down one directory.
mydevdir/
setup.py
project/
__init__.py
features/
__init__.py
CompareText.py
tests/
test.py
How write a setup.py. This can get quite complicated. You can read Building and Distributing Packages with Setuptools and lookup other resources on the net, but a minimalist setup.py is
#!/usr/bin/env python
from setuptools import setup, find_packages
setup(name='project',
version='0.1',
description='This is project: project',
packages=find_packages(),
)
Now, while in mydevdir do python setup.py develop. Or you can actually produce an install package and put it in a virtual env for test.
Hack sys.path
It may be easier to hack paths in test.py. Note that this will need to be undone if you make project installable later. Just add to the top of test.py
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).absolute().parents[2]))
This puts the parent directory in the python path and now project will be found on import. It runs the risk that a .py file in the same directory as project can mask an installed module. If you have a local csv.py and you import csv, you'd get the local.

Why does a submodule's code try to import a sibling submodule both directly and via relative import?

I am writing a script that will be executable from the command line (by adding an entry_points argument to setup.py). My directory structure is as follows:
# Edit: package hasn't followed src packaging pattern, renamed to root_dir
root_dir/
conftest.py
setup.py
module1/
__init__.py
script1.py
script2.py
module2/
__init__.py
other_script1.py
other_script2.py
tests/
conftest.py
some_tests/
conftest.py
test_some_parts.py
some_other_tests/
conftest.py
test_these_other_parts.py
Edit: My setup.py has an entry_points argument as follows:
entry_points={
'console_scripts': ['load_data = module1.script1:main']
},
At the moment, at the top of script1.py I have:
try:
from script2 import func2
except ModuleNotFoundError:
from .script2 import func2
When I install the package (pip install .) and run load_data via the command line, it wants from script2 import func2. But when I run tests, it wants from .script2 import func2. I think this matters, but I run tests while in the src/ directory (pytest tests/).
This odd try/except is a bandaid that allows me to run tests and execute my script. Is there a better solution to this or at least explanation?
This one is correct:
from .script2 import func2
Remove the try/except hack. With a correctly installed package, pytest command should not care which directory you're in for the purposes of resolving imports. Note that pytest will add any directories where conftest.py files are discovered to sys.path, which can be masking or exacerbating packaging issues.
This is a packaging problem. You're doing the src layout wrong: setup.py should be outside of the src root. The tests subdir should be at the same level as src. Re-read this or this and try again (and destroy/recreate venv might be advisable).
For a couple of examples of a popular open-source project structure correctly using the src layout, checkout attrs or cryptography.
And, last but not least, don't feel bad for getting it wrong. Python packaging is a mess and it's extremely tricky to get it right.

Change cwd before running tests

I have a bunch of unittest test cases in separate directories. There is also a directory which just contains helper scripts for the tests. So my file tree looks like this
test_dir1
test_dir2
test_dir3
helper_scripts
Each python file in test_dir* will have these lines:
import sys
sys.path.append('../helper_scripts')
import helper_script
This all works fine, as long as I run the tests from within their directory. However, I would like to be at the project root and just run:
py.test
and have it traverse all the directories and run each test it finds. The problem is that the tests are being run from the wrong directory, so the sys.path.append doesn't append the helper_scripts directory, it appends the parent of the project root. This makes all the imports fail with an Import Error.
Is there a way to tell py.test to run the test scripts from their directory? ie. change the cwd before executing them? If not, is there another test runner I can use that will?
What I usually do is structure my project like this:
myproject/
setup.py
myproject/
__init__.py
mymodule.py
tests/
__init__.py
test_dir1/
test_mymodule.py
helper_scripts/
__init__.py
helper_script.py
For running tests, I use a virtualenv with myproject installed in development mode using one of the following commands in the myproject root directory:
pip install -e .
python setup.py develop
Now in test_mymodule.py I can just say
from myproject.tests.helper_scripts import helper_script
I can then just run pytest and there's no need to change the working directory in tests at all.
See Pytest's Good Integration Practices for a great summary of pros and cons for different project directory structures.
os.chdir("newdir")
will change your current working directory
I would suggest that you instead configure your environment so that import helper_scripts will work regardless of the current directory. This is the recommended approach.
If you absolutely must though, you can use relative imports instead:
from .. import helper_script

Tests and python package structure

I have some problems in structuring my python project. Currently it is a bunch of files in the same folder. I have tried to structure it like
proj/
__init__.py
foo.py
...
bar/
__init__.py
foobar.py
...
tests/
foo_test.py
foobar_test.py
...
The problem is that I'm not able, from inner directories, to import modules from the outer directories. This is particularly annoying with tests.
I have read PEP 328 about relative imports and PEP 366 about relative imports from the main module. But both these methods require the base package to be in my PYTHONPATH. Indeed I obtain the following error
ValueError: Attempted relative import in non-package.
So I added the following boilerplate code on top of the test files
import os, sys
sys.path.append(os.path.join(os.getcwd(), os.path.pardir))
Still I get the same error. What is the correct way to
structure a package, complete with tests, and
add the base directory to the path to allow imports?
EDIT As requested in the comment, I add an example import that fails (in the file foo_test.py)
import os, sys
sys.path.append(os.path.join(os.getcwd(), os.path.pardir))
from ..foo import Foo
When you use the -m switch to run code, the current directory is added to sys.path. So the easiest way to run your tests is from the parent directory of proj, using the command:
python -m proj.tests.foo_test
To make that work, you will need to include an __init__.py file in your tests directory so that the tests are correctly recognised as part of the package.
I like to import modules using the full proj.NAME package prefix whenever possible. This is the approach the Google Python styleguide recommends.
One option to allow you to keep your package structure, use full package paths, and still move forward with development would be to use a virtualenv and put your project in develop mode. Your project's setup.py will need to use setuptools instead of distutils, to get the develop command.
This will let you avoid the sys.path.append stuff above:
% virtualenv ~/virt
% . ~/virt/bin/activate
(virt)~% cd ~/myproject
(virt)~/myproject% python setup.py develop
(virt)~/myproject% python tests/foo_test.py
Where foo_test.py uses:
from proj.foo import Foo
Now when you run python from within your virtualenv your PYTHONPATH will point to all of the packages in your project. You can create a shorter shell alias to enter your virtualenv without having to type . ~/virt/bin/activate every time.

Categories

Resources