16

I try to organized my Python projects using a folder structure. When I need to make tests I use something like the following.

.
|-- src
|   |-- b.py
|   `-- main.py
`-- tests
    `-- test_main.py

There is just one big problem with this approach. Pytest won't run if main.py is importing b.py.

So far I've tried placing empty __init__.py files inside the src and tests folders, both independently and together, but any of those seems to work.

It seems to me this is a pretty standard project, but I haven't been able to find a solution online. Should I use a different folder structure? Is there any recommended way to use pytest with this kind of projects?


This are the contents of the files:

# b.py
def triplicate(x):
        return x * 3
# main.py
from b import triplicate

def duplicate(x):
        return x * 2
# test_main.py
from src.main import duplicate


def test_duplicate():
        assert duplicate(2) == 4

And this is the error I get when running pytest:

==================================================================================================== ERRORS ====================================================================================================
_____________________________________________________________________________________ ERROR collecting tests/test_main.py ______________________________________________________________________________________
ImportError while importing test module 'C:\Users\edwar\test_pytest\tests\test_main.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
c:\python39\lib\importlib\__init__.py:127: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tests\test_main.py:1: in <module>
    from src.main import duplicate
src\main.py:1: in <module>
    from b import triplicate
E   ModuleNotFoundError: No module named 'b'
=========================================================================================== short test summary info ============================================================================================
ERROR tests/test_main.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
=============================================================================================== 1 error in 0.15s ===============================================================================================

Ed1123
  • 185
  • 1
  • 1
  • 11
  • What's the actual error you get? How does `main.py` import `b`? – chepner Jul 21 '21 at 22:25
  • 1
    And how does `test_main.py` import `main`? In short, please provide a [mcve]. – chepner Jul 21 '21 at 22:32
  • `ModuleNotFoundError: No module named 'b'`. `main.py` has this line: `from b import triplicate`. – Ed1123 Jul 21 '21 at 22:34
  • That's an absolute import; it requires `b` to be found in a directory on `sys.path`. That's probably the case when you run `main.py` as a script, but not if you import it (like `test_main.py` does) when `pytest` runs. – chepner Jul 21 '21 at 22:38
  • I get no errors when running main.py. The problem is when running pytest. I thought this folder structure was the proper one to use. How to do it if not this one? – Ed1123 Jul 21 '21 at 22:42
  • Throw an empty `__init__.py` file in the `tests` directory. – Abstract Jul 21 '21 at 22:48
  • Unfortunately that doesn't work, @Abstract. I still get the same error. – Ed1123 Jul 21 '21 at 22:53
  • `from .b import triplicate` in `main.py` - Take notice of the `.` in front of `b` – Abstract Jul 21 '21 at 23:04
  • Do you ever plan on *installing* the code, or only running from this hierarchy. `main.py` and `b.py` should probably not be in the same package if it will be installed somewhere. – chepner Jul 21 '21 at 23:12
  • If you mean as a package, no, in this case it is not installable. This particular case I'm working on is a scraper. I just use the folder structure to organize my code and I was wondering if this is the correct one, if there is one. – Ed1123 Jul 21 '21 at 23:19

3 Answers3

4

Python uses the 'environment variable' PYTHONPATH to look for sources to import code from. By default, the directory you execute a python program is automatically included, but you want to include something like this when you test:

PYTHONPATH=$PYTHONPATH,../src python test_main.py

This is if you're executing a test from the source directory. Tools like IntelliJ (PyCharm) will let you add this as a value in your test invocation. Alternatively you can use export PYTHONPATH=.... (Note this is for a *nix environment, your mileage on windows may vary.)

The upshot is that every directory in PYTHONPATH will be loaded and Python will attempt to use it as a 'root' for modules you try to import. Your basic directory structure is the most idiomatic.

  • See this answer for more on configuring PYTHONPATH correctly.
  • See this doc for more about how the PYTHONPATH is modified and used 'under the hood'.
  • See this answer for options to include the src directory when running pytest tests.
  • See this blog post about using autoenv (a Python library) to enable the usage of .env files to manage this for you (at least within a virtualenv setup - a good idea generally).
  • setup.py is also idiomatic for including many modules, and may provide a more convenient path for the situation you're handling.
Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
  • And when I run `python src/main.py` how the interpreter knows that `b.py` is inside `src/` if that folder is not explicitly included in the PATH? – Ed1123 Jul 21 '21 at 23:29
  • 4
    Adjusting the `PYTHONPATH` for each project doesn't seem like a good solution. – Abstract Jul 22 '21 at 01:09
  • 1
    @Ed1123 Because the `main.py` file is inside the same folder. See [this](https://docs.python.org/3/install/index.html#modifying-python-s-search-path) documentation link. The `''` that is the first element in the `sys.path` value indicates 'the point of entrance' of your program. You won't need to have a modified `PYTHONPATH` when you run it without tests sinces nothing in your main code should refer to your test code - you only need your test code to see your main code. – Nathaniel Ford Jul 22 '21 at 13:31
  • 1
    @Abstract Sure it is. Note he is only needs this for tests, so they are properly modularized from his code. I don't know a lot about his setup otherwise, but as mentioned tools like IntelliJ or test runners can incorporate this path modification seamlessly. `virtualenv` and `pipenv` also provide ways of doing this per-project. The alternative is to ship your test code with your main code, which is poor encapsulation. – Nathaniel Ford Jul 22 '21 at 13:34
  • @NathanielFord, it is just a general structure I try to use in multiple projects. Shall I use different structures depending on the project? Also, as you say it, including the test code in the main code is not a clean option at all. – Ed1123 Jul 22 '21 at 17:30
  • 1
    @ed1123 Your folder structure is fine - ideal even. The difficulty (and this is a python-wide issue) is that you need to provide additional information for your specific project. `virtualenv` is in wide use for exactly this reason: python assumes some global states that are not particular project-specific. So the tooling to do this is there. What is your IDE? How do you normally execute your tests? (Pytest? Something else?) These answers would help me be more specific. – Nathaniel Ford Jul 22 '21 at 17:53
  • I use VS Code and do my tests with pytest. Before organizing my projects in folders (the tests in the same root directory as the main code), I just run pytest in the terminal or use the built-in testing function of VS Code. – Ed1123 Jul 22 '21 at 23:21
3

Edit: After posting, I realized that someone has already posted this solution in a different question, many years ago.

If you just want to run all the tests from the root directory without having to worry about the PYTHONPATH, the easiest solution would be to simply invoke the tests by typing in the terminal:

python -m pytest

While just running pytest would fail in the OP example, python -m pytest would work, as according to the docs

... calling via python will also add the current directory to sys.path.

Hope that helps!

Michalis P
  • 71
  • 1
  • 7
-2

You can put your source files in the root directory and adjust the import paths as such:

structure

main.py

# main.py
from stackoverflow.b import triplicate

def duplicate(x):
        return x * 2

b.py

# b.py
def triplicate(x):
        return x * 3

test_main.py

# test_main.py
from stackoverflow.main import duplicate


def test_duplicate():
        assert duplicate(2) == 4

Then running pytest . from the root directory stackoverflow:

collected 1 item

tests\test_main.py .                                                                                                                                                                      [100%] 

====================================================================================== 1 passed in 0.03s ======================================================================================= 

Or, if you wish to keep the src folder, your import would be this:

from stackoverflow.src.b import triplicate
Abstract
  • 985
  • 2
  • 8
  • 18
  • It doesn't look too pythonic, though. I'm sure there must be a better way to do it. – Ed1123 Jul 21 '21 at 23:20
  • @Ed1123 Fully qualified import paths are beneficial as they remove ambiguity. – Abstract Jul 21 '21 at 23:31
  • I didn't know that. Thanks for pointing it out. Do you know if that's include in PEP8? – Ed1123 Jul 21 '21 at 23:57
  • 1
    @Ed1123 Yes it is. https://www.python.org/dev/peps/pep-0008/#imports "Absolute imports are recommended". So I'm not really sure why this answer was not preferred and down-voted. It's standard. – Abstract Jul 21 '21 at 23:59
  • I think it may be due to some projects needing to have a `src` folder since without it all the files will get cluttered at the root of the project. – Ed1123 Jul 22 '21 at 00:05
  • @Ed1123 The clutter will either happen in `src` or in the root directory lol. Adding an additional layer of folder structure for a project is much the same as creating a class for every tiny piece of functionality and over-structuring the code-base to the point of chaos. – Abstract Jul 22 '21 at 00:12
  • 1
    It is common industry practice to separate test code from functionality code. Most projects contain resource, build, metadata and documentation that are all separate from `src` code - it is completely reasonable to put your source code in its own directory one level down from root so these non-production code files are separate. – Nathaniel Ford Jul 22 '21 at 13:38
  • I really want you to mentor me, @NathanielFord. A question that is a little bit out of this. What would you call production code? An example of src and production code will be really helpful. – Ed1123 Jul 22 '21 at 23:30
  • 1
    @Ed1123 Production code is just code you expect to end up in the hands of a user. Something that needs to have some expectation of stability around it. Test code is not typically included because the end user doesn't need it - you need the tests to ship it, but the code you ship is already tested. Similarly, scripts you use to build your code or other resources files that are consumed in that process needn't be sent to the customer, so it is not considered 'production code'. – Nathaniel Ford Jul 25 '21 at 18:49