Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

Conditional coverage based on any rules you define!

Some project have different parts that relies on different environments:
Some projects have different parts that relies on different environments:

- Python version, some code is only executed on specific versions and ignored on others
- OS version, some code might be Windows, Mac, or Linux only
- External packages, some code is only executed when some 3rd party package is installed

Current best practice is to use `# pragma: no cover` for this places in our project.
This project allows to use configurable pragmas
that include code to the coverage if some condition evaluates to true,
This project allows to use configurable pragmas
that include code to the coverage if some condition evaluates to true,
and fallback to ignoring this code when condition is false.

Read [the announcing post](https://sobolevn.me/2020/02/conditional-coverage).
Expand All @@ -28,7 +28,7 @@ Read [the announcing post](https://sobolevn.me/2020/02/conditional-coverage).
pip install coverage-conditional-plugin
```

Then you will need to add to your `setup.cfg` or `.coveragerc` file
Then you will need to add to your `setup.cfg` or `.coveragerc` file
some extra configuration:

```ini
Expand Down Expand Up @@ -73,20 +73,20 @@ rules =

```

When running tests with and without `django` installed
When running tests with and without `django` installed
you will have `100%` coverage in both cases.

But, different lines will be included.
With `django` installed it will include
But, different lines will be included.
With `django` installed it will include
both `try:` and `if django is not None:` conditions.

When running without `django` installed,
it will include `except ImportError:` line.


## Writting pragma rules
## Writing pragma rules

Format for pragma rules is:
Format for pragma rules is:

```
"pragma-condition": pragma-name
Expand All @@ -96,16 +96,24 @@ Code inside `"pragma-condition"` is evaluted with `eval`.
Make sure that the input you pass there is trusted!
`"pragma-condition"` must return `bool` value after evaluation.

We also provide a bunch of helpers to make writing rules easier:
We support all environment markers specified in [PEP-496](https://www.python.org/dev/peps/pep-0496/).
See [Strings](https://www.python.org/dev/peps/pep-0496/#strings)
and [Version Numbers](https://www.python.org/dev/peps/pep-0496/#version-numbers)
sections for available values. Also, we provide a bunch of additional markers:

- `sys_version_info` is the same as [`sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
- `os_name` is the same as [`os.name`](https://docs.python.org/3/library/os.html#os.name)
- `os_environ` is the same as [`os.environ`](https://docs.python.org/3/library/os.html#os.environ)
- `platform_system` is the same as [`platform.system()`](https://docs.python.org/3/library/platform.html#platform.system)
- `platform_release` is the same as [`platform.release()`](https://docs.python.org/3/library/platform.html#platform.release)
- `is_installed` is our custom function that tries to import the passed string, returns `bool` value
- `package_version` is our custom function that tries to get package version from `pkg_resources` and returns its [parsed version](https://packaging.pypa.io/en/latest/version/#packaging.version.parse)

Use `get_env_info` to get values for the current environment:

```python
from coverage_conditional_plugin import get_env_info

get_env_info()
```


## License

Expand Down
42 changes: 23 additions & 19 deletions coverage_conditional_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
# -*- coding: utf-8 -*-

import os
import platform
import sys
import traceback
from typing import ClassVar, Optional, Tuple
from importlib import import_module
from typing import ClassVar, Dict, Optional, Tuple

import pkg_resources
from coverage import CoveragePlugin
from coverage.config import CoverageConfig
from packaging import version
from packaging.markers import default_environment


def get_env_info() -> Dict[str, object]:
"""Public helper to get the same env we pass to the plugin."""
env_info: Dict[str, object] = {}
env_info.update(default_environment())
# Feel free to send PRs that extend this dict:
env_info.update({
'sys_version_info': sys.version_info,
'os_environ': os.environ,
'is_installed': _is_installed,
'package_version': _package_version,
})
return env_info


class _ConditionalCovPlugin(CoveragePlugin):
Expand Down Expand Up @@ -64,22 +79,12 @@ def _should_be_applied(self, code: str) -> bool:
this code will be included to the coverage on 3.8+ releases.

"""
env_info = get_env_info()
try:
return eval(code, { # noqa: WPS421, S307
# Feel free to send PRs that extend this dict:
'sys_version_info': sys.version_info,
'os_name': os.name,
'os_environ': os.environ,
'platform_system': platform.system(),
'platform_release': platform.release(),
'is_installed': _is_installed,
'package_version': _package_version,
})
return eval(code, env_info) # noqa: WPS421, S307
except Exception:
print( # noqa: WPS421
'Exception during conditional coverage evaluation:',
traceback.format_exc(),
)
msg = 'Exception during conditional coverage evaluation:'
print(msg, traceback.format_exc()) # noqa: WPS421
return False

def _ignore_marker(self, config: CoverageConfig, marker: str) -> None:
Expand All @@ -92,11 +97,10 @@ def _ignore_marker(self, config: CoverageConfig, marker: str) -> None:
def _is_installed(package: str) -> bool:
"""Helper function to detect if some package is installed."""
try:
__import__(package) # noqa: WPS421
import_module(package)
except ImportError:
return False
else:
return True
return True


def _package_version(
Expand Down