I am trying to use Pytest to test a largish (~100k LOC, 1k files) project and there are several other similar projects for which I'd like to do the same, eventually. This is not a standard Python package; it's part of a heavily customized system that I have little power to change, at least in the near term. Test modules are integrated with the code, rather than being in a separate directory, and that is important to us. The configuration is fairly similar to this question, and my answer there that might provide helpful background, too.
The issue I'm having is that the projects use PEP 420 implicit namespace packages almost exclusively; that is, there are almost no __init__.py
files in any of the package directories. I haven't seen any cases yet where the packages had to be namespace packages, but given that this project is combined with other projects that also have Python code, this could happen (or already be happening and I've just not noticed it).
Consider a repository that looks like the following. (For a runnable copy of it, including the tests described below, clone 0cjs/pytest-impl-ns-pkg
from GitHub.) All tests below are assumed to be in project/thing/thing_test.py
.
repo/
project/
util/
thing.py
thing_test.py
I have enough control over the testing configurations that I can ensure sys.path
is set appropriately for imports of the code under test to work properly. That is, the following test will pass:
def test_good_import():
import project.util.thing
However, Pytest is determining package names from files using its usual system, giving package names that are not the standard ones for my configuration and adding subdirectories of my project to sys.path
. So the following two tests fail:
def test_modulename():
assert 'project.util.thing_test' == __name__
# Result: AssertionError: assert 'project.util.thing_test' == 'thing_test'
def test_bad_import():
''' While we have a `project.util.thing` deep in our hierarchy, we do
not have a top-level `thing` module, so this import should fail.
'''
with raises(ImportError):
import thing
# Result: Failed: DID NOT RAISE <class 'ImportError'>
As you can see, while thing.py
can always be imported as project.util.thing
, thing_test.py
is project.util.thing_test
outside of Pytest, but in a Pytest run project/util
is added to sys.path
and the module is named thing_test
.
This introduces a number of problems:
- Module namespace collisions (e.g., between
project/util/thing_test.py
andproject/otherstuff/thing_test.py
). - Bad import statements not being caught because the code under test is also using these non-production import paths.
- Relative imports may not work in test code because the module has been "moved" in the hierarchy.
- In general I'm quite nervous about having a large number of extra paths added to
sys.path
in testing that will be absent in production as I see a lot of potential for errors in this. But let's call that the first (and at the moment, I guess, default) option.
What I think I would like to be able to do would be to tell Pytest that it should determine module names relative to specific filesystem paths that I provide, rather than itself deciding what paths to used based on presence and absence of __init__.py
files. However, I see no way to do this with Pytest. (It's not out of the question for me to add this to Pytest, but that also won't happen in the near future as I think I'd want a much deeper understanding of Pytest before even proposing exactly how to do this.)
A third option (after just living with the current situation and changing pytest as above) is simply to add dozens of __init__.py
files to the project. However, while using extend_path
in them would (I think) deal with the namespace vs. regular package issue in the normal Python world, I think it would break our unusual release system for packages declared in multiple projects. (That is, if another project had a project.util.other
module and was combined for release with our project, the collision between their project/util/__init__.py
and our project/util/__init__.py
would be a major problem.) Fixing this would be a major challenge since we'd have to, among other things, add some way to declare that some directories containing an __init__.py
are actually namespace packages.
Are there ways to improve the above options? Are there other options I'm missing?