coveragepy icon indicating copy to clipboard operation
coveragepy copied to clipboard

Implicit namespace submodules not discovered

Open adamchainz opened this issue 5 years ago • 9 comments

Describe the bug

If an implicit namespace package (directory without __init__.py) exists within a source module and is not imported, coverage won't discover it or report it as uncovered.

I discovered this on Django project where an app had an idiomatic management.commands submodule without intermediate __init__.py files. This had two levels of implicit namespace packages, but it also happens with just one (what would be just management).

To Reproduce

Worked example:

$ coverage --version
Coverage.py, version 5.2.1 with C extension
Full documentation is at https://coverage.readthedocs.io
$ python --version
Python 3.8.3
$ tree example
example
├── management
│   └── submodule2.py
└── submodule.py

1 directory, 2 files
$ cat example/submodule.py
x = 1
$ cat example/management/submodule2.py
y = 2
$ cat test.py
import example.submodule
$ coverage erase && coverage run --source example -m test && coverage report
Name                   Stmts   Miss  Cover
------------------------------------------
example/submodule.py       1      0   100%

Expected behavior

I'd expect example/management/submodule2.py to be discovered as uncovered. Adding an __init__.py to management makes coverage discover it:

$ touch example/management/__init__.py
$ coverage erase && coverage run --source example -m test && coverage report
Name                               Stmts   Miss  Cover
------------------------------------------------------
example/management/__init__.py         0      0   100%
example/management/submodule2.py       1      1     0%
example/submodule.py                   1      0   100%
------------------------------------------------------
TOTAL                                  2      1    50%

Additional context

Implicit namespace packages "just work" with Python 3, and many new developers create them on purpose or accidentally.

Django itself has moved to, and then back from, implicit namespace packages for management commands: https://groups.google.com/g/django-developers/c/GVHMH2ciAnk/m/7RzvMIzyAQAJ . So there are at least some users who believe "__init__.py files are no longer needed".

adamchainz avatar Aug 26 '20 09:08 adamchainz

Coverage.py tries to guess what files in the tree might have been importable in order to tell you what wasn't imported. We don't want it to walk into a bin directory and find .py files and say they weren't imported. So I'm not sure what to do here.

Would it be acceptable to have to enable implicit package spidering with a config setting? Should we insist that you say in the config file what the implicit packages are?

nedbat avatar Aug 26 '20 22:08 nedbat

Would it be acceptable to have to enable implicit package spidering with a config setting?

For me, yes, but I fear it would only be discovered by a few. Why not go the other way and require explicit omit entries for directories? If example/bin/someting.py exists, it probably is importable as example.bin.something, even if the author didn't intend.

Should we insist that you say in the config file what the implicit packages are?

That wouldn't help in the situation I ran into - knowing you had to declare the module to coverage would be the same as knowing it'd be better to create the __init__.py files.

adamchainz avatar Aug 26 '20 23:08 adamchainz

could use something like this: https://github.com/pypa/setuptools/blob/edcf84faaf17e87e6e38796dd24f66d9236bf87c/setuptools/init.py#L110-L119

graingert avatar Sep 01 '20 22:09 graingert

[coverage:run]
find_namespace_packages = True
source = example

graingert avatar Sep 01 '20 22:09 graingert

If this isn't likely to be supported anytime soon, does anyone have an appropriate work around? I'm also stuck on this.

romesco avatar Jan 21 '21 23:01 romesco

@romesco I use https://pypi.org/project/flake8-no-pep420/ as a workaround

graingert avatar Jan 21 '21 23:01 graingert

Nice! Thanks @graingert! It looks like @adamchainz (OP) got tired of the problem and created this shortly after filing the issue 😆 .

For the record, I ended up adding both the dir above the namespace dir as well as the namespace dir itself to the tool.coverage.run source argument in my pyproject.toml and it seemed to end up working.

Here is the rough structure:

├── package-name
│   ├── namespace_package
│   │   └── subpackage
│   │       ├── __init__.py
│   │       └── module_x.py
│   └── tests
│       ├── __init__.py
│       └── test_module_x.py
└── pyproject.toml

In total my config was:

# pyproject.toml
...
[tool.coverage.paths]
source = ["package-name"]

[tool.coverage.run]
branch = true
source = [
    "package-name",
    "package-name/namespace_package"
]
omit = ["*site-packages*", "*tests*"]
...

Hope this helps someone!

romesco avatar Jan 22 '21 03:01 romesco

Thanks @romesco! Your workaround well, worked :smile: Here's my diff:

 [coverage:run]
 branch = true
 parallel = true
 source =
-  src/
+  src/mkdocstrings/handlers/
   tests/

I had to specify the directory down to the first one which contains explicit packages:

% tree src 
src
└── mkdocstrings
    └── handlers
        └── python
            ├── __init__.py
            ├── collector.py
            └── renderer.py

EDIT: oops, actually coveragepy now reports renderer.py and collector.py, but with 0% coverage... still work needed :confused:

pawamoy avatar Dec 28 '21 17:12 pawamoy

Is there a way to see the exact file paths coveragepy finds and measures?

pawamoy avatar Dec 28 '21 17:12 pawamoy

I just got surprised by this, too.

Could it be a solution to support the sourcedir/** syntax in the --source CLI option? I am noticing that this does not work, but it could be made to mean "add and all subfolders to the source", regardless of presence of __init__.py files.

Note that this already works, i.e. using --source=sourcedir,sourcedir/subfolder correctly scans all files in sourcedir and sourcedir/subfolder.

matpen avatar Dec 11 '22 10:12 matpen

Oh, looks like this issue was a duplicate of #1383, which is fixed in pull #1387, and released as part of coverage 7.0.0b1. Can you give it a try?

nedbat avatar Dec 11 '22 14:12 nedbat

Wow, thank you for working on that. I just installed version 7.0.0b1:

Coverage.py, version 7.0.0b1 with C extension
Full documentation is at https://coverage.readthedocs.io/en/7.0.0b1

IIUC, there is a new config option [report] include_namespace_packages. However, in my environment I cannot use a config file, so I tried --include-namespace-packages on CLI, which gives no such option: --include-namespace-packages.

Is this intended, or am I missing something?

matpen avatar Dec 11 '22 14:12 matpen

Not all config options are available on the command line, to avoid clutter. Can you tell me more about why you can't use a configuration file? The settings can be in .coveragerc, setup.cfg, tox.ini, or pyproject.toml, or any other file you name.

nedbat avatar Dec 11 '22 14:12 nedbat

Because I am working in a CI environment that does not support them. Is there perhaps an environment variable for this?

matpen avatar Dec 11 '22 15:12 matpen

Sorry, I don't mean to be difficult, I want to understand the constraints people might encounter. Your CI system has your code files, so why wouldn't it have a configuration file that was also in your project?

nedbat avatar Dec 11 '22 15:12 nedbat

No worries at all. You mean to put the config file into source? I guess that would work. I was thinking more to a config file in the user's home folder or at system-level.

matpen avatar Dec 11 '22 15:12 matpen

Right. The way I see it, the need for implicit namespaces is a property of the project, not of the user, so the setting should be in a configuration file stored in the project. That way anyone running the tests will be using the same configuration.

nedbat avatar Dec 11 '22 17:12 nedbat

I see the logic in your reasoning. Still, it will be uncomfortable in our situation, because it still requires one additional file, but do not worry for now: we will find a workable setup.

Perhaps you could reconsider, as a low-priority task, the position expressed in https://github.com/nedbat/coveragepy/issues/1024#issuecomment-1345575077? Some tools are able to generate interfaces which can be configured via file, CLI args, and environment variables directly from one single "source of truth". I am not in a position to suggest one for Python, but boost.program_options is one such tool for C++.

That is just a suggestion for improvement, of course. Feel free to close this issue, and thank you again for the great work!

matpen avatar Dec 11 '22 17:12 matpen

I don't know what project you are working in. These days, there are dozens of reasons to need configuration in one of the four files I named, so most people don't need to add a file just to set one setting. Maybe you are in a more austere project.

I'll mark this as fixed, and released as part of coverage 7.0.0b1.

nedbat avatar Dec 11 '22 23:12 nedbat