4

I'm trying to build a project via the method described in The Hitchhiker's Guide To Python. I'm running into problems with the test structure.

My file structure looks like this:

.
├── __init__.py
├── sample
│   ├── __init__.py
│   └── core.py
├── setup.py
└── tests
    ├── __init__.py
    ├── context.py
    └── test_core.py

With:

# sample/core.py

class SampleClass:
    def got_it(self):
        return True 

And:

# tests/context.py

import os
import sys
sys.path.insert(0, os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..')
))

import sample

And:

# tests/test_core.py

import unittest

from .context import sample


class SampleClassTest(unittest.TestCase):
    def test_got_it(self):
        # ...
        pass 
    
if __name__ == '__main__':
    unittest.main()

(Note that I just thru the __init__.py files in the root and tests to see if that helped, but it didn't.)

When I try to run tests/test_core.py with Python 3.7. I get this error:

ImportError: attempted relative import with no known parent package

That happens if I run the test file from outside the tests directory or in it.

If I remote the . and do this in tests/test_core.py:

from context import sample

Everything loads, but I can't access SampleClass.

The things I've tried are:

sc = SampleClass()
NameError: name 'SampleClass' is not defined

sc = sample.SampleClass()
AttributeError: module 'sample' has no attribute 'SampleClass'

sc = sample.core.SampleClass()
AttributeError: module 'sample' has no attribute 'core'

sc = core.SampleClass()
NameError: name 'core' is not defined

I tried on Python 2 as well and had similar problems (with slightly different error methods). I also tried calling a function instead of a class and had similar problems there as well.

Can someone point me in the direction of what I'm doing wrong?

Alan W. Smith
  • 24,647
  • 4
  • 70
  • 96
  • 1
    I keep forgetting how this stuff works so I can't really explain why this is happening, but using the `sys.path.insert` hack for testing is weird and IMO you should just install your package prior to running the tests (create&activate a virtual environment, then execute `pip install --editable .` or `python ./setup.py develop`), and make the tests just import stuff directly. – Czaporka Nov 15 '20 at 23:05
  • The change of sys.path looks very unusual - is it really needed? – Evgeny Nov 15 '20 at 23:58
  • You may want to consult https://docs.pytest.org/en/stable/goodpractices.html for test folder layout. Also note it matter what folder you invoke tests from and if you did install them a local package in editable mode. – Evgeny Nov 16 '20 at 00:03

2 Answers2

2

This once caused me some headaches, until I realized that it just depended on the way the test is started. If you use python tests/test_core.py, then you are starting a simple script outside any package. So relative imports are forbidden.

But if (still from the project root folder) you use:

python -m tests.test_core

then you are starting the module test_core from the tests package, and relative imports are allowed.

From that time, I always start my tests as modules and not as scripts. What is even better is that unittest discover knows about packages. So if you are consistent in naming the test folder and scripts with a initial test, you can forget about the number of tests and just use:

python -m unittest discover

and that is enough to run the whole test suite.

Happy testing!

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Thank you for your help, i'm having problems with this too. I'm confused though, what does it mean when you say "I always start my tests as modules and not as scripts"? Grateful for help with this! – sunyata Feb 05 '21 at 09:39
  • @sunyata: it just means that if I want to start a test individually I use `python -m test.test1` (as a module) and not `python test/test1.py` (as a script). – Serge Ballesta Feb 05 '21 at 11:39
0

Since you have a proper package complete with setup.py, you're better off using pip install -e . instead of using a sys.path hack.

Minimal setup.py

#setup.py
from setuptools import setup, find_packages
setup(name='sample', version='0.0.1', packages=find_packages(exclude=('tests')))

Then you should import everything from sample packages anywhere you want using the same syntax (i.e. absolute path). To import SampleClass for example:

from sample.core import SampleClass

Also, to answer your question on why you're getting on relative import:

ImportError: attempted relative import with no known parent package

You'd have to avoid invoking the file as a script using python ... because relative imports do not work on scripts. You should invoke the file as a module using python -m .... Here is a very nice explanation that's been viewed a billion times:

Leonardus Chen
  • 1,103
  • 6
  • 20