Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
Jürgen Edelbluth | 4767491b33 | |
Jürgen Edelbluth | 4ec8b0f1ed | |
Jürgen Edelbluth | 68ee89f065 | |
Jürgen Edelbluth | 9c5d28a58c | |
Jürgen Edelbluth | fcea9e7349 | |
Jürgen Edelbluth | 0f3c7b26b3 | |
Jürgen Edelbluth | f1a1155d9d | |
Jürgen Edelbluth | 7774992eae | |
Jürgen Edelbluth | bef3987479 | |
Jürgen Edelbluth | 5b4b62139f | |
Jürgen Edelbluth | 5d5441c3ee | |
Jürgen Edelbluth | 3c0892d8ee | |
Jürgen Edelbluth | 8a55418bb3 | |
Jürgen Edelbluth | 82584ab395 | |
Jürgen Edelbluth | 755d8f9276 | |
Jürgen Edelbluth | 725dc359c7 | |
Jürgen Edelbluth | de8cb8f858 | |
Jürgen Edelbluth | 4eeeef5353 | |
Jürgen Edelbluth | 87ca957c68 | |
Jürgen Edelbluth | f370ac7ce2 | |
Jürgen Edelbluth | 45abe33d7f | |
Jürgen Edelbluth | e0733a2ce6 | |
Jürgen Edelbluth | d0409aa6bb | |
Jürgen Edelbluth | f40e3e2a66 | |
Jürgen Edelbluth | b2f4f8b245 | |
Jürgen Edelbluth | 9448b369c7 | |
Jürgen Edelbluth | 4cf7d98984 | |
Jürgen Edelbluth | d87edb29ca | |
Jürgen Edelbluth | 8c888d450a | |
Jürgen Edelbluth | 9b744d8b4c | |
Jürgen Edelbluth | dfccf9ef8a | |
Jürgen Edelbluth | 56fd486899 | |
Jürgen Edelbluth | ffa0de1ffb | |
Jürgen Edelbluth | bcc67be373 | |
Jürgen Edelbluth | 01a0764c1f | |
Jürgen Edelbluth | d768f230d8 |
|
@ -0,0 +1,53 @@
|
||||||
|
pipeline {
|
||||||
|
|
||||||
|
environment {
|
||||||
|
TOX_PARALLEL_NO_SPINNER = "1"
|
||||||
|
PY_COLORS = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
agent any
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Prepare') {
|
||||||
|
steps {
|
||||||
|
sh 'poetry install --remove-untracked --no-root'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Tox') {
|
||||||
|
matrix {
|
||||||
|
axes {
|
||||||
|
axis {
|
||||||
|
name 'PYTHON_VERSION'
|
||||||
|
values 'py38', 'py39', 'py310', 'py311'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Tox Test') {
|
||||||
|
options {
|
||||||
|
lock('single-tox-build')
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh 'poetry run tox -r -e ${PYTHON_VERSION}'
|
||||||
|
xunit checksName: "Tests ${PYTHON_VERSION}", tools: [JUnit(excludesPattern: '', pattern: 'test-reports/*.xml', stopProcessingIfError: true)]
|
||||||
|
cobertura autoUpdateStability: false, coberturaReportFile: 'coverage.xml', conditionalCoverageTargets: '70, 0, 0', failUnhealthy: false, failUnstable: false, lineCoverageTargets: '80, 0, 0', maxNumberOfBuilds: 0, methodCoverageTargets: '80, 0, 0', onlyStable: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
deleteDir()
|
||||||
|
}
|
||||||
|
always {
|
||||||
|
withCredentials([string(credentialsId: 'jed-notification-email', variable: 'EMAIL')]) {
|
||||||
|
mail to: '$EMAIL',
|
||||||
|
subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}",
|
||||||
|
body: "Duration: ${currentBuild.durationString} / Jenkins URL: ${env.BUILD_URL}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -389,8 +389,8 @@ preferred-modules=
|
||||||
[EXCEPTIONS]
|
[EXCEPTIONS]
|
||||||
|
|
||||||
# Exceptions that will emit a warning when caught.
|
# Exceptions that will emit a warning when caught.
|
||||||
overgeneral-exceptions=BaseException,
|
overgeneral-exceptions=builtins.BaseException,
|
||||||
Exception
|
builtins.Exception
|
||||||
|
|
||||||
|
|
||||||
[REFACTORING]
|
[REFACTORING]
|
||||||
|
@ -443,7 +443,7 @@ max-attributes=7
|
||||||
max-bool-expr=5
|
max-bool-expr=5
|
||||||
|
|
||||||
# Maximum number of branch for function / method body.
|
# Maximum number of branch for function / method body.
|
||||||
max-branches=12
|
max-branches=16
|
||||||
|
|
||||||
# Maximum number of locals for function / method body.
|
# Maximum number of locals for function / method body.
|
||||||
max-locals=20
|
max-locals=20
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
3.10.6
|
3.11.4
|
||||||
3.9.13
|
3.10.12
|
||||||
3.8.13
|
3.9.17
|
||||||
3.11-dev
|
3.8.17
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
Contributions are always welcome. There are different ways may help:
|
||||||
|
|
||||||
|
- Reporting bugs and errors
|
||||||
|
- Suggesting new features
|
||||||
|
- Improving code
|
||||||
|
- Improving documentation
|
||||||
|
- Implementing new features
|
||||||
|
|
||||||
|
The instance [git.codebau.dev](https://git.codebau.dev/) does not accept new registrations currently. Please contact me
|
||||||
|
under `csv_params`(at)`jued.de` if you have any contribution. We'll send you an invitation for the source code server or
|
||||||
|
find another way.
|
188
README.md
188
README.md
|
@ -1,13 +1,21 @@
|
||||||
|
![pytest-csv-params](https://docs.codebau.dev/pytest-plugins/pytest-csv-params/_images/pytest-csv-params.png)
|
||||||
|
|
||||||
# pytest-csv-params
|
# pytest-csv-params
|
||||||
|
|
||||||
A pytest plugin to parametrize tests by CSV files.
|
A pytest plugin to parametrize data-driven tests by CSV files.
|
||||||
|
|
||||||
|
[![Build Status](https://build.codebau.dev/buildStatus/icon?job=pytest-csv-params&style=flat)](https://git.codebau.dev/pytest-plugins/pytest-csv-params)
|
||||||
|
[![PyPI - Downloads](https://img.shields.io/pypi/dw/pytest-csv-params?label=PyPI%20downloads&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
[![PyPI - Version](https://img.shields.io/pypi/v/pytest-csv-params?label=PyPI%20version&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
[![PyPI - Status](https://img.shields.io/pypi/status/pytest-csv-params?label=PyPI%20status&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
[![PyPI - Format](https://img.shields.io/pypi/format/pytest-csv-params?label=PyPI%20format&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.8, 3.9 or 3.10
|
- Python 3.8, 3.9, 3.10, 3.11
|
||||||
- pytest >= 7.1
|
- pytest >= 7.4
|
||||||
|
|
||||||
There's no operating system dependend code in this plugin, so it should run anywhere where pytest runs.
|
There's no operating system dependent code in this plugin, so it should run anywhere where pytest runs.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -23,47 +31,31 @@ pip install pytest-csv-params
|
||||||
poetry add --dev pytest-csv-params
|
poetry add --dev pytest-csv-params
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Documentation / User Guide
|
||||||
|
|
||||||
Simply decorate your test method with `@csv_params` and the following parameters:
|
**Detailed documentation can be found under
|
||||||
|
[docs.codebau.dev/pytest-plugins/pytest-csv-params/](https://docs.codebau.dev/pytest-plugins/pytest-csv-params/)**
|
||||||
|
|
||||||
|
## Usage: Command Line Argument
|
||||||
|
|
||||||
|
| Argument | Required | Description | Example |
|
||||||
|
|-------------------------|---------------|----------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `--csv-params-base-dir` | no (optional) | Define a base dir for all relative-path CSV data files (since 0.1.0) | `pytest --csv-params-base-dir /var/testdata` |
|
||||||
|
|
||||||
|
## Usage: Decorator
|
||||||
|
|
||||||
|
Simply decorate your test method with `@csv_params` (`pytest_csv_params.decorator.csv_params`) and the following parameters:
|
||||||
|
|
||||||
| Parameter | Type | Description | Example |
|
| Parameter | Type | Description | Example |
|
||||||
|--------------|--------------------------|-----------------------------------------------------------|-------------------------------------|
|
|------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
|
||||||
| `data_file` | `str` | The CSV file to use, relative or absolute path | `/var/testdata/test1.csv` |
|
| `data_file` | `str` | The CSV file to use, relative or absolute path | `"/var/testdata/test1.csv"` |
|
||||||
| `base_dir` | `str` (optional) | Directory to look up relative CSV files (see `data_file`) | `join(dirname(__file__), "assets")` |
|
| `base_dir` | `str` (optional) | Directory to look up relative CSV files (see `data_file`); overrides the command line argument | `join(dirname(__file__), "assets")` |
|
||||||
| `id_col` | `str` (optional) | Column name of the CSV that contains test case IDs | `ID#` |
|
| `id_col` | `str` (optional) | Column name of the CSV that contains test case IDs | `"ID#"` |
|
||||||
| `dialect` | `csv.Dialect` (optional) | CSV Dialect definition (see [1]) | `csv.excel_tab` |
|
| `dialect` | `csv.Dialect` (optional) | CSV Dialect definition (see [Python CSV Documentation](https://docs.python.org/3/library/csv.html#dialects-and-formatting-parameters)) | `csv.excel_tab` |
|
||||||
| `data_casts` | `dict` (optional) | Cast Methods for the CSV Data (see "Data Casting" below) | `{ "a": int, "b": float }` |
|
| `data_casts` | `dict` (optional) | Cast Methods for the CSV Data (see "Data Casting" below) | `{ "a": int, "b": float }` |
|
||||||
|
| `header_renames` | `dict` (optional) | Replace headers from the CSV file, so that they can be used as parameters for the test function (since 0.3.0) | `{ "Annual Amount of Bananas": "banana_count", "Cherry export price": "cherry_export_price" }` |
|
||||||
|
|
||||||
[1] [Python CSV Documentation](https://docs.python.org/3/library/csv.html#dialects-and-formatting-parameters)
|
## CSV Format
|
||||||
|
|
||||||
### Data Casting
|
|
||||||
|
|
||||||
When data is read from CSV, they are always parsed as `str`. If you need them in other formats, you can set a method that should be called with the value.
|
|
||||||
|
|
||||||
These methods can also be lambdas, and are also good for further transformations.
|
|
||||||
|
|
||||||
#### Data Casting Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
from pytest_csv_params.decorator import csv_params
|
|
||||||
|
|
||||||
def normalize(x: str) -> str:
|
|
||||||
return x.strip().upper()
|
|
||||||
|
|
||||||
@csv_params(
|
|
||||||
data_file="/test/data.csv",
|
|
||||||
data_casts={
|
|
||||||
"col_x": normalize,
|
|
||||||
"col_y": float,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
def test_something(col_x, col_y):
|
|
||||||
# Test something...
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### CSV Format
|
|
||||||
|
|
||||||
The default CSV format is:
|
The default CSV format is:
|
||||||
|
|
||||||
|
@ -72,16 +64,7 @@ The default CSV format is:
|
||||||
- If you need a `"` in the value, use `""` (double quote)
|
- If you need a `"` in the value, use `""` (double quote)
|
||||||
- Fields are separated by comma (`,`)
|
- Fields are separated by comma (`,`)
|
||||||
|
|
||||||
#### Example CSV
|
## Usage Example
|
||||||
|
|
||||||
```text
|
|
||||||
"ID#", "part_a", "part_b", "expected_result"
|
|
||||||
"first", 1, 2, 3
|
|
||||||
"second", 3, 4, 7
|
|
||||||
"third", 10, 11, 21
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage Example
|
|
||||||
|
|
||||||
This example uses the CSV example from above.
|
This example uses the CSV example from above.
|
||||||
|
|
||||||
|
@ -90,7 +73,7 @@ from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
@csv_params(
|
@csv_params(
|
||||||
data_file="/data/test-lib/cases/addition.csv",
|
data_file="/data/test-lib/cases/addition.csv",
|
||||||
id_col="#ID",
|
id_col="ID#",
|
||||||
data_casts={
|
data_casts={
|
||||||
"part_a": int,
|
"part_a": int,
|
||||||
"part_b": int,
|
"part_b": int,
|
||||||
|
@ -101,19 +84,94 @@ def test_addition(part_a, part_b, expected_result):
|
||||||
assert part_a + part_b == expected_result
|
assert part_a + part_b == expected_result
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
Shorthand example (no ID col, only string values):
|
||||||
|
|
||||||
### Build and test
|
```python
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
You need [Poetry](https://python-poetry.org/) in order to build this project.
|
@csv_params("/data/test-lib/cases/texts.csv")
|
||||||
|
def test_texts(text_a, text_b, text_c):
|
||||||
|
assert f"{text_a}:{text_b}" == text_c
|
||||||
|
```
|
||||||
|
|
||||||
Tests are implemented with `pytest`, and `tox` is used to orchestrate them for the supported python versions.
|
### More complex example
|
||||||
|
|
||||||
- Checkout this repo
|
This example features nearly all things the plugin has to offer. You find this example also in the test cases, see `tests/test_complex_example.py`.
|
||||||
- Run `poetry install`
|
|
||||||
- Run `poetry run tox` (for all supported python versions) or `poetry run pytest` (for your current version)
|
|
||||||
|
|
||||||
### Bugs etc.
|
The CSV file (`tests/assets/example.csv`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
"Test ID","Bananas shipped","Single Banana Weight","Apples shipped","Single Apple Weight","Container Size"
|
||||||
|
"Order-7","1503","0.5","2545","0.25","1500"
|
||||||
|
"Order-15","101","0.55","1474","0.33","550"
|
||||||
|
```
|
||||||
|
|
||||||
|
The Test (`tests/test_complex_example.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from math import ceil
|
||||||
|
from os.path import join, dirname
|
||||||
|
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
|
||||||
|
@csv_params(
|
||||||
|
data_file="example.csv",
|
||||||
|
base_dir=join(dirname(__file__), "assets"),
|
||||||
|
id_col="Test ID",
|
||||||
|
header_renames={
|
||||||
|
"Bananas shipped": "bananas_shipped",
|
||||||
|
"Single Banana Weight": "banana_weight",
|
||||||
|
"Apples shipped": "apples_shipped",
|
||||||
|
"Single Apple Weight": "apple_weight",
|
||||||
|
"Container Size": "container_size",
|
||||||
|
},
|
||||||
|
data_casts={
|
||||||
|
"bananas_shipped": int,
|
||||||
|
"banana_weight": float,
|
||||||
|
"apples_shipped": int,
|
||||||
|
"apple_weight": float,
|
||||||
|
"container_size": int,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_container_size_is_big_enough(
|
||||||
|
bananas_shipped: int, banana_weight: float, apples_shipped: int, apple_weight: float, container_size: int
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This is just an example test case for the documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
gross_weight = (banana_weight * bananas_shipped) + (apple_weight * apples_shipped)
|
||||||
|
assert ceil(gross_weight) <= container_size
|
||||||
|
```
|
||||||
|
|
||||||
|
If you decide not to rename the columns, the test would look like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@csv_params(
|
||||||
|
data_file="example.csv",
|
||||||
|
base_dir=join(dirname(__file__), "assets"),
|
||||||
|
id_col="Test ID",
|
||||||
|
data_casts={
|
||||||
|
"Bananas_Shipped": int,
|
||||||
|
"Single_Banana_Weight": float,
|
||||||
|
"Apples_Shipped": int,
|
||||||
|
"Single_Apple_Weight": float,
|
||||||
|
"Container_Size": int,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_container_size_is_big_enough(
|
||||||
|
Bananas_Shipped: int, Single_Banana_Weight: float, Apples_Shipped: int, Single_Apple_Weight: float, Container_Size: int
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- A detailed changelog is here:
|
||||||
|
[docs.codebau.dev/pytest-plugins/pytest-csv-params/pages/changelog.html](https://docs.codebau.dev/pytest-plugins/pytest-csv-params/pages/changelog.html)
|
||||||
|
|
||||||
|
## Bugs etc.
|
||||||
|
|
||||||
Please send your issues to `csv-params_issues` (at) `jued.de`. Please include the following:
|
Please send your issues to `csv-params_issues` (at) `jued.de`. Please include the following:
|
||||||
|
|
||||||
|
@ -123,10 +181,16 @@ Please send your issues to `csv-params_issues` (at) `jued.de`. Please include th
|
||||||
|
|
||||||
It would be great if you could include example code that clarifies your issue.
|
It would be great if you could include example code that clarifies your issue.
|
||||||
|
|
||||||
### Pull Requests
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
Pull requests are always welcome. Since this Gitea instance is not open to public, just send an e-mail to discuss options.
|
Pull requests are always welcome. Since this Gitea instance is not open to public, just send an e-mail to discuss options.
|
||||||
|
|
||||||
## License
|
Any changes that are made are to be backed by tests. Please give me a sign if you're going to break the existing API and let us discuss ways to handle that.
|
||||||
|
|
||||||
Code is under MIT license. See `LICENSE.txt` for details.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
|
## Where are the sources?
|
||||||
|
|
||||||
|
The source code is available under [git.codebau.dev/pytest-plugins/pytest-csv-params](https://git.codebau.dev/pytest-plugins/pytest-csv-params).
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""
|
||||||
|
This pytest plugin requires command line arguments that are parsed from the pytest framework. This module contains code
|
||||||
|
to instruct pytest to deliver the required values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from _pytest.config.argparsing import Parser
|
||||||
|
|
||||||
|
HELP_TEXT = "set base dir for getting CSV data files from"
|
||||||
|
"""
|
||||||
|
This is the help text for the command line arguments that is added by :meth:`pytest_addoption`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser: Parser, plugin_name: str = "csv-params") -> None:
|
||||||
|
"""
|
||||||
|
Entrypoint for pytest to extend the own :class:`Parser` with the things we need extra.
|
||||||
|
|
||||||
|
:param parser: The pytest command line argument parser
|
||||||
|
:param plugin_name: The name of our plugin, with default value
|
||||||
|
"""
|
||||||
|
|
||||||
|
group = parser.getgroup(plugin_name)
|
||||||
|
group.addoption(
|
||||||
|
f"--{plugin_name}-base-dir",
|
||||||
|
action="store",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
required=False,
|
||||||
|
help=HELP_TEXT,
|
||||||
|
)
|
|
@ -1,19 +1,28 @@
|
||||||
"""
|
"""
|
||||||
Pytest Plugin Configuration
|
The pytest plugin needs a setup (:meth:`pytest_configure`) and a teardown (:meth:`pytest_unconfigure`) method
|
||||||
|
registered. This module contains the required methods for that.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from _pytest.config import Config
|
||||||
|
|
||||||
from _ptcsvp.plugin import Plugin
|
from _ptcsvp.plugin import Plugin
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config, plugin_name="csv_params"):
|
def pytest_configure(config: Config, plugin_name: str = "csv_params") -> None:
|
||||||
"""
|
"""
|
||||||
Register our Plugin
|
Register our Plugin
|
||||||
|
|
||||||
|
:param config: Pytets configuration class
|
||||||
|
:param plugin_name: The name of the pytest plugin, with default value
|
||||||
"""
|
"""
|
||||||
config.pluginmanager.register(Plugin(config), f"{plugin_name}_plugin")
|
config.pluginmanager.register(Plugin(config), name=f"{plugin_name}_plugin")
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config, plugin_name="csv_params"):
|
def pytest_unconfigure(config: Config, plugin_name: str = "csv_params") -> None:
|
||||||
"""
|
"""
|
||||||
Remove our Plugin
|
Remove our Plugin
|
||||||
|
|
||||||
|
:param config: Pytest configuration class
|
||||||
|
:param plugin_name: The name of the pytest plgin, with default value
|
||||||
"""
|
"""
|
||||||
config.pluginmanager.unregister(f"{plugin_name}_plugin")
|
config.pluginmanager.unregister(name=f"{plugin_name}_plugin")
|
||||||
|
|
|
@ -3,31 +3,42 @@ Parametrize a test function by CSV file
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, TypedDict
|
from typing import Any, List, Optional, TypedDict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.mark import MarkDecorator
|
||||||
|
|
||||||
|
from _ptcsvp.plugin import BASE_DIR_KEY, Plugin
|
||||||
|
from _ptcsvp.varname import make_name_valid
|
||||||
from pytest_csv_params.dialect import CsvParamsDefaultDialect
|
from pytest_csv_params.dialect import CsvParamsDefaultDialect
|
||||||
from pytest_csv_params.exception import (
|
from pytest_csv_params.exception import (
|
||||||
|
CsvHeaderNameInvalid,
|
||||||
CsvParamsDataFileInaccessible,
|
CsvParamsDataFileInaccessible,
|
||||||
CsvParamsDataFileInvalid,
|
CsvParamsDataFileInvalid,
|
||||||
CsvParamsDataFileNotFound,
|
CsvParamsDataFileNotFound,
|
||||||
)
|
)
|
||||||
from pytest_csv_params.types import BaseDir, CsvDialect, DataCastDict, DataFile, IdColName
|
from pytest_csv_params.types import BaseDir, CsvDialect, DataCasts, DataFile, HeaderRenames, IdColName
|
||||||
|
|
||||||
|
|
||||||
class TestCaseParameters(TypedDict):
|
class TestCaseParameters(TypedDict):
|
||||||
"""
|
"""
|
||||||
Type for Test Case
|
Type for a single test case. Contains the optional :attr:`test_id` and the test :attr:`data`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
test_id: Optional[str]
|
test_id: Optional[str]
|
||||||
data: List
|
data: List[Any]
|
||||||
|
|
||||||
|
|
||||||
def read_csv(base_dir: BaseDir, data_file: DataFile, dialect: CsvDialect):
|
def read_csv(base_dir: BaseDir, data_file: DataFile, dialect: CsvDialect) -> List[List[str]]:
|
||||||
"""
|
"""
|
||||||
Get Data from CSV
|
Get Data from CSV
|
||||||
|
|
||||||
|
:param base_dir: Optional directory to look up non-absolute CSV files (given as :attr:`data_file`)
|
||||||
|
:param data_file: The CSV file to read. If this is an absolute path, :attr:`base_dir` will be ignored; if not, the
|
||||||
|
:attr:`base_dir` is prepended.
|
||||||
|
:param dialect: The CSV file dialect (definition of the format of a CSV file).
|
||||||
|
|
||||||
|
:returns: A list of rows, each row contains a list of columns; all type `str`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if data_file is None:
|
if data_file is None:
|
||||||
|
@ -51,16 +62,53 @@ def read_csv(base_dir: BaseDir, data_file: DataFile, dialect: CsvDialect):
|
||||||
return csv_lines
|
return csv_lines
|
||||||
|
|
||||||
|
|
||||||
def add_parametrization(
|
def clean_headers(current_headers: List[str], replacing: HeaderRenames) -> List[str]:
|
||||||
|
"""
|
||||||
|
Clean the CSV file headers
|
||||||
|
|
||||||
|
:param current_headers: List of the current headers, as read from the CSV file (without the ID column, as far as it
|
||||||
|
exists)
|
||||||
|
:param replacing: Dictionary of replacements for headers
|
||||||
|
|
||||||
|
:returns: List of cleaned header names
|
||||||
|
|
||||||
|
:raises CsvHeaderNameInvalid: When non-unique names appear
|
||||||
|
"""
|
||||||
|
if replacing is not None:
|
||||||
|
for index, header in enumerate(current_headers):
|
||||||
|
replacement = replacing.get(header, None)
|
||||||
|
if replacement is not None:
|
||||||
|
current_headers[index] = replacement
|
||||||
|
current_headers = list(map(make_name_valid, current_headers))
|
||||||
|
if len(current_headers) != len(set(current_headers)):
|
||||||
|
raise CsvHeaderNameInvalid("Header names are not unique")
|
||||||
|
return current_headers
|
||||||
|
|
||||||
|
|
||||||
|
def add_parametrization( # pylint: disable=too-many-arguments
|
||||||
|
data_file: DataFile,
|
||||||
base_dir: BaseDir = None,
|
base_dir: BaseDir = None,
|
||||||
data_file: DataFile = None,
|
|
||||||
id_col: IdColName = None,
|
id_col: IdColName = None,
|
||||||
data_casts: DataCastDict = None,
|
data_casts: DataCasts = None,
|
||||||
dialect: CsvDialect = CsvParamsDefaultDialect,
|
dialect: CsvDialect = CsvParamsDefaultDialect,
|
||||||
):
|
header_renames: HeaderRenames = None,
|
||||||
|
) -> MarkDecorator:
|
||||||
"""
|
"""
|
||||||
Get data from the files and add things to the tests
|
Parametrize a test function with data from a CSV file.
|
||||||
|
|
||||||
|
For the public decorator, see :meth:`pytest_csv_params.decorator.csv_params`.
|
||||||
|
|
||||||
|
:param data_file: The CSV file to read the data from
|
||||||
|
:param base_dir: Optional base directory to look for non-absolute :attr:`data_file` in
|
||||||
|
:param id_col: Optional name of a column that shall be used to make the test IDs from
|
||||||
|
:param data_casts: Methods to cast a column's data into a format that is required for the test
|
||||||
|
:param dialect: The CSV file dialect (CSV file format) to use
|
||||||
|
:param header_renames: A dictonary mapping the header names from the CSV file to usable names for the tests
|
||||||
|
|
||||||
|
:returns: :meth:`pytest.mark.parametrize` mark decorator, filled with all the data from the CSV.
|
||||||
"""
|
"""
|
||||||
|
if base_dir is None:
|
||||||
|
base_dir = getattr(Plugin, BASE_DIR_KEY, None)
|
||||||
csv_lines = read_csv(base_dir, data_file, dialect)
|
csv_lines = read_csv(base_dir, data_file, dialect)
|
||||||
if len(csv_lines) < 2:
|
if len(csv_lines) < 2:
|
||||||
raise CsvParamsDataFileInvalid("File does not contain a single data row") from None
|
raise CsvParamsDataFileInvalid("File does not contain a single data row") from None
|
||||||
|
@ -74,6 +122,7 @@ def add_parametrization(
|
||||||
raise CsvParamsDataFileInvalid(f"Cannot find ID column '{id_col}'") from err
|
raise CsvParamsDataFileInvalid(f"Cannot find ID column '{id_col}'") from err
|
||||||
if len(headers) == 0:
|
if len(headers) == 0:
|
||||||
raise CsvParamsDataFileInvalid("File seems only to have IDs") from None
|
raise CsvParamsDataFileInvalid("File seems only to have IDs") from None
|
||||||
|
headers = clean_headers(headers, header_renames)
|
||||||
data: List[TestCaseParameters] = []
|
data: List[TestCaseParameters] = []
|
||||||
for data_line in csv_lines:
|
for data_line in csv_lines:
|
||||||
line = list(map(str, data_line))
|
line = list(map(str, data_line))
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
"""
|
"""
|
||||||
The main Plugin implementation
|
This module contains the main plugin class. By the time of writing, it is quite unspectacular.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from _pytest.config import Config
|
||||||
|
|
||||||
|
BASE_DIR_KEY = "__pytest_csv_params__config__base_dir"
|
||||||
|
"""
|
||||||
|
The class attribute key for :class:`Plugin` to store the base dir command line argument value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Plugin: # pylint: disable=too-few-public-methods
|
class Plugin: # pylint: disable=too-few-public-methods
|
||||||
"""
|
"""
|
||||||
Plugin Class
|
The main plugin class
|
||||||
|
|
||||||
|
Currently, this class is nothing more than the keeper of the value of the command line argument (as defined by
|
||||||
|
:meth:`_ptcsvp.cmdline.pytest_addoption`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config: Config) -> None:
|
||||||
"""
|
"""
|
||||||
Hold the pytest config
|
Initialize the class, and simply store the value of the command line argument, as class attribute.
|
||||||
|
|
||||||
|
:param config: Pytest configuration
|
||||||
"""
|
"""
|
||||||
self.config = config
|
setattr(Plugin, BASE_DIR_KEY, config.option.csv_params_base_dir)
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""
|
||||||
|
This module contains code to validate variable/argument/parameter names or to make them valid ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import keyword
|
||||||
|
import re
|
||||||
|
from string import ascii_letters, digits
|
||||||
|
|
||||||
|
from pytest_csv_params.exception import CsvHeaderNameInvalid
|
||||||
|
|
||||||
|
VALID_CHARS = ascii_letters + digits
|
||||||
|
"""
|
||||||
|
Valid characters a variable/parameter/argument name can consist of
|
||||||
|
"""
|
||||||
|
|
||||||
|
VARIABLE_NAME = re.compile(r"^[a-zA-Z_][A-Za-z0-9_]{0,1023}$")
|
||||||
|
"""
|
||||||
|
Regular expression that defines a valid variable/parameter/argument name
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_name(name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if the variable name is valid
|
||||||
|
|
||||||
|
:param name: The name to be checked
|
||||||
|
:returns: `True`, when the name is valid
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
keyword.iskeyword(name)
|
||||||
|
or (hasattr(keyword, "issoftkeyword") and getattr(keyword, "issoftkeyword")(name))
|
||||||
|
or getattr(builtins, name, None) is not None
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return VARIABLE_NAME.match(name) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def make_name_valid(name: str, replacement_char: str = "_") -> str:
|
||||||
|
"""
|
||||||
|
Make a name a valid name by replacing invalid chars with the as :attr:`replacement_char` given char
|
||||||
|
|
||||||
|
:param name: The name to make a valid one
|
||||||
|
:param replacement_char: The char to replace invalid chars with, default is an underscore `_`
|
||||||
|
:returns: A valid name
|
||||||
|
:raises CsvHeaderNameInvalid: If the fixed name is still an invalid name
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixed_name = name
|
||||||
|
|
||||||
|
for index, character in enumerate(name):
|
||||||
|
if character in VALID_CHARS:
|
||||||
|
continue
|
||||||
|
fixed_name = f"{fixed_name[:index]}{replacement_char}{fixed_name[index+1:]}"
|
||||||
|
if fixed_name[0] not in ascii_letters:
|
||||||
|
fixed_name = f"{replacement_char}{fixed_name[1:]}"
|
||||||
|
if not is_valid_name(fixed_name):
|
||||||
|
raise CsvHeaderNameInvalid(f"'{fixed_name}' is not a valid variable name")
|
||||||
|
return fixed_name
|
|
@ -1,24 +1,34 @@
|
||||||
"""
|
"""
|
||||||
Check Version Information
|
This module contains two methods to check if the python version is recent enough (:meth:`check_python_version`) and if
|
||||||
|
the pytest version is recent enough (:meth:`check_pytest_version`).
|
||||||
|
|
||||||
|
During the setup phase of the plugin (see :mod:`pytest_csv_params.plugin`) these methods are called.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from attr.exceptions import PythonTooOldError
|
from attr.exceptions import PythonTooOldError
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
|
|
||||||
|
|
||||||
def check_python_version(min_version=(3, 8)):
|
def check_python_version(min_version: Tuple[int, int] = (3, 8)) -> None:
|
||||||
"""
|
"""
|
||||||
Check if the current version is at least 3.8
|
Check if the current version is at least 3.8
|
||||||
|
|
||||||
|
:param min_version: The minimum version required, as tuple, default is 3.8
|
||||||
|
:raises PythonTooOldError: When the python version is too old/unsupported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if sys.version_info < min_version:
|
if sys.version_info < min_version:
|
||||||
raise PythonTooOldError(f"At least Python {'.'.join(map(str, min_version))} required")
|
raise PythonTooOldError(f"At least Python {'.'.join(map(str, min_version))} required")
|
||||||
|
|
||||||
|
|
||||||
def check_pytest_version(min_version=(7, 1)):
|
def check_pytest_version(min_version: Tuple[int, int] = (7, 4)) -> None:
|
||||||
"""
|
"""
|
||||||
Check if the current version is at least 7.1
|
Check if the current version is at least 7.4
|
||||||
|
|
||||||
|
:param min_version: The minimum version required, as tuple, default is 7.4
|
||||||
|
:raises RuntimeError: When the pytest version is too old/unsupported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pytest import __version__ as pytest_version # pylint: disable=import-outside-toplevel
|
from pytest import __version__ as pytest_version # pylint: disable=import-outside-toplevel
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = ../dist/docs
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -0,0 +1,48 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
src: url('woff2/FiraCode-Light.woff2') format('woff2'),
|
||||||
|
url("woff/FiraCode-Light.woff") format("woff");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
src: url('woff2/FiraCode-Regular.woff2') format('woff2'),
|
||||||
|
url("woff/FiraCode-Regular.woff") format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
src: url('woff2/FiraCode-Medium.woff2') format('woff2'),
|
||||||
|
url("woff/FiraCode-Medium.woff") format("woff");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
src: url('woff2/FiraCode-SemiBold.woff2') format('woff2'),
|
||||||
|
url("woff/FiraCode-SemiBold.woff") format("woff");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
src: url('woff2/FiraCode-Bold.woff2') format('woff2'),
|
||||||
|
url("woff/FiraCode-Bold.woff") format("woff");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code VF';
|
||||||
|
src: url('woff2/FiraCode-VF.woff2') format('woff2-variations'),
|
||||||
|
url('woff/FiraCode-VF.woff') format('woff-variations');
|
||||||
|
/* font-weight requires a range: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide#Using_a_variable_font_font-face_changes */
|
||||||
|
font-weight: 300 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,16 @@
|
||||||
|
/* #### Generated By: http://www.cufonfonts.com #### */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Vollkorn Regular';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src: local('Vollkorn Regular'), url('Vollkorn-Regular.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Vollkorn Bold';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src: local('Vollkorn Bold'), url('Vollkorn-Bold.woff') format('woff');
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends '!layout.html' %}
|
||||||
|
|
||||||
|
{%- block font %}
|
||||||
|
<!-- Fully omitting Google Fonts from Remote -->
|
||||||
|
<link href="{{ pathto('_static/font/vollkorn/style.css', 1) }}" rel="stylesheet">
|
||||||
|
<link href="{{ pathto('_static/font/fira/style.css', 1) }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
input {
|
||||||
|
font-family: "Vollkorn Regular", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
pre {
|
||||||
|
font-family: "Fira Code VF", "Fira Code", "Courier New", Courier, monospace !important;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{%- endblock %}
|
|
@ -0,0 +1,41 @@
|
||||||
|
Sitemap
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: Basics
|
||||||
|
|
||||||
|
pages/install
|
||||||
|
pages/guide
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: Advanced Usage
|
||||||
|
|
||||||
|
pages/config
|
||||||
|
pages/examples
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: Developers
|
||||||
|
|
||||||
|
pages/developer
|
||||||
|
pages/api
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: Project Information
|
||||||
|
|
||||||
|
pages/changelog
|
||||||
|
pages/license
|
||||||
|
pages/issues
|
||||||
|
pages/contributing
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: Meta
|
||||||
|
|
||||||
|
genindex
|
||||||
|
py-modindex
|
||||||
|
🌍 Project Page <https://git.codebau.dev/pytest-plugins/pytest-csv-params>
|
||||||
|
🌍 juergen.rocks <https://juergen.rocks/>
|
|
@ -0,0 +1,110 @@
|
||||||
|
# pylint: skip-file
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from os.path import abspath, dirname, join
|
||||||
|
|
||||||
|
import tomli
|
||||||
|
|
||||||
|
sys.path.insert(0, abspath(join(dirname(__file__), "..")))
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = "pytest-csv-params"
|
||||||
|
copyright = "2022, Jürgen Edelbluth"
|
||||||
|
author = "Jürgen Edelbluth"
|
||||||
|
# release = "0.0.0"
|
||||||
|
|
||||||
|
with open(abspath(join(dirname(__file__), "..", "pyproject.toml")), "rt", encoding="utf-8") as pyproject_toml:
|
||||||
|
toml_data = tomli.loads(pyproject_toml.read())
|
||||||
|
release = toml_data["tool"]["poetry"]["version"]
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.intersphinx",
|
||||||
|
"sphinx.ext.duration",
|
||||||
|
"sphinx.ext.doctest",
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.autosummary",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
"sphinx_autodoc_typehints",
|
||||||
|
"myst_parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
intersphinx_mapping = {
|
||||||
|
"python": ("https://docs.python.org/3", (None, "python-inv.txt")),
|
||||||
|
"pytest": ("https://docs.pytest.org/en/7.1.x/", (None, "pytest-inv.txt")),
|
||||||
|
}
|
||||||
|
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
source_suffix = {
|
||||||
|
".rst": "restructuredtext",
|
||||||
|
".txt": "markdown",
|
||||||
|
".md": "markdown",
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = "sphinx_material"
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
|
html_title = "pytest-csv-params Documentation"
|
||||||
|
html_short_title = "pytest-csv-params"
|
||||||
|
|
||||||
|
html_logo = "icon/pytest-csv-params.png"
|
||||||
|
|
||||||
|
pygments_style = "emacs"
|
||||||
|
|
||||||
|
# Material theme options (see theme.conf for more information)
|
||||||
|
html_theme_options = {
|
||||||
|
# Set the name of the project to appear in the navigation.
|
||||||
|
"nav_title": "Pytest CSV Params Plugin",
|
||||||
|
# Set you GA account ID to enable tracking
|
||||||
|
"google_analytics_account": None,
|
||||||
|
# Specify a base_url used to generate sitemap.xml. If not
|
||||||
|
# specified, then no sitemap will be built.
|
||||||
|
"base_url": None,
|
||||||
|
# Set the color and the accent color
|
||||||
|
"color_primary": "teal",
|
||||||
|
"color_accent": "deep-orange",
|
||||||
|
"repo_url": "https://git.codebau.dev/pytest-plugins/pytest-csv-params",
|
||||||
|
"repo_name": "git.codebau.dev",
|
||||||
|
"repo_type": None,
|
||||||
|
# Visible levels of the global TOC; -1 means unlimited
|
||||||
|
"globaltoc_depth": 1,
|
||||||
|
# If False, expand all TOC entries
|
||||||
|
"globaltoc_collapse": True,
|
||||||
|
# If True, show hidden TOC entries
|
||||||
|
"globaltoc_includehidden": False,
|
||||||
|
"css_minify": True,
|
||||||
|
"html_minify": True,
|
||||||
|
"master_doc": True,
|
||||||
|
"theme_color": "#008080",
|
||||||
|
}
|
||||||
|
|
||||||
|
myst_enable_extensions = [
|
||||||
|
"colon_fence",
|
||||||
|
]
|
||||||
|
|
||||||
|
html_sidebars = {
|
||||||
|
"**": [
|
||||||
|
"logo-text.html",
|
||||||
|
"globaltoc.html",
|
||||||
|
"localtoc.html",
|
||||||
|
"searchbox.html",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
autodoc_typehints_format = "fully-qualified"
|
||||||
|
autodoc_preserve_defaults = True
|
|
@ -0,0 +1,2 @@
|
||||||
|
Index
|
||||||
|
=====
|
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
|
@ -0,0 +1,39 @@
|
||||||
|
```{image} icon/pytest-csv-params.png
|
||||||
|
:alt: Logo, shows a data table with an arrow to a test tube
|
||||||
|
:class: bg-primary
|
||||||
|
:width: 256px
|
||||||
|
:align: center
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
# Data-driven test parametrization für pytest with CSV files
|
||||||
|
|
||||||
|
[![Build Status](https://build.codebau.dev/buildStatus/icon?job=pytest-csv-params&style=flat)](https://git.codebau.dev/pytest-plugins/pytest-csv-params)
|
||||||
|
[![PyPI - Downloads](https://img.shields.io/pypi/dw/pytest-csv-params?label=PyPI%20downloads&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
[![PyPI - Version](https://img.shields.io/pypi/v/pytest-csv-params?label=PyPI%20version&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
[![PyPI - Status](https://img.shields.io/pypi/status/pytest-csv-params?label=PyPI%20status&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
[![PyPI - Format](https://img.shields.io/pypi/format/pytest-csv-params?label=PyPI%20format&style=flat&logo=pypi)](https://pypi.org/project/pytest-csv-params/)
|
||||||
|
|
||||||
|
This pytest plugin allows you to parametrize your pytest tests by CSV files. Manage your test data independently of
|
||||||
|
your tests. This site guides you through [installation](pages/install) and [usage](pages/guide).
|
||||||
|
|
||||||
|
The plugin is [open source](https://git.codebau.dev/pytest-plugins/pytest-csv-params) and
|
||||||
|
[released under MIT license](pages/license).
|
||||||
|
It is listed in the [Python Package Index (PyPI)](https://pypi.org/project/pytest-csv-params/).
|
||||||
|
|
||||||
|
Your feedback, bug reports, improvements are appreciated! Get in touch via e-mail: `csv_params`@`jued`.`de`. Together
|
||||||
|
we'll find a way for your contribution.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. include:: _toc.rst
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
This project is not affiliated with the pytest project, but heavily relies on it. Visit [pytest.org](https://pytest.org)
|
||||||
|
for more information about this great testing framework.
|
||||||
|
```
|
|
@ -0,0 +1,35 @@
|
||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=.
|
||||||
|
set BUILDDIR=..\dist\docs
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.https://www.sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Private Elements
|
||||||
|
|
||||||
|
## Command Line Arguments
|
||||||
|
|
||||||
|
**Module:** `_ptcsvp.cmdline`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: _ptcsvp.cmdline
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Configuration
|
||||||
|
|
||||||
|
**Module:** `_ptcsvp.configure`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: _ptcsvp.configure
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Parametrization
|
||||||
|
|
||||||
|
**Module:** `_ptcsvp.parametrize`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: _ptcsvp.parametrize
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Plugin Class
|
||||||
|
|
||||||
|
**Module:** `_ptcsvp.plugin`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: _ptcsvp.plugin
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Variable Name Validation
|
||||||
|
|
||||||
|
**Module:** `_ptcsvp.varname`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: _ptcsvp.varname
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Version Checks
|
||||||
|
|
||||||
|
**Module:** `_ptcsvp.version`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: _ptcsvp.version
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Public Elements
|
||||||
|
|
||||||
|
## The Decorator
|
||||||
|
|
||||||
|
**Module:** `pytest_csv_params.decorator`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pytest_csv_params.decorator
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## The CSV Dialect
|
||||||
|
|
||||||
|
**Module:** `pytest_csv_params.dialect`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pytest_csv_params.dialect
|
||||||
|
:members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Exceptions
|
||||||
|
|
||||||
|
**Module:** `pytest_csv_params.exception`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pytest_csv_params.exception
|
||||||
|
:members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Code
|
||||||
|
|
||||||
|
**Module:** `pytest_csv_params.plugin`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pytest_csv_params.plugin
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
**Module:** `pytest_csv_params.types`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: pytest_csv_params.types
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
|
@ -0,0 +1,127 @@
|
||||||
|
# Tests
|
||||||
|
|
||||||
|
This page should give you an overview over all tests for this plugin.
|
||||||
|
|
||||||
|
## Global `conftest.py`
|
||||||
|
|
||||||
|
**Module:** `tests.conftest`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.conftest
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Tests
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_clean_headers
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_parametrize
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_read_csv
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_varname
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_version_check
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Tests
|
||||||
|
|
||||||
|
These tests test the plugin code by inserting the plugin into a test pytest instance.
|
||||||
|
|
||||||
|
### Plugin `conftest.py`
|
||||||
|
|
||||||
|
**Module:** `tests.plugin.conftest`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.plugin.conftest
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.plugin.test_cmd_line
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.plugin.test_plugin
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## POC Tests
|
||||||
|
|
||||||
|
### POC `conftest.py`
|
||||||
|
|
||||||
|
**Module:** `tests.poc.conftest`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.poc.conftest
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.poc.test_parametrize_with_generator
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_docs_example
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_blog_example
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: tests.test_complex_example
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
|
@ -0,0 +1,11 @@
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
The following documents are generated from the source code documentation.
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
api-ref/public
|
||||||
|
api-ref/private
|
||||||
|
api-ref/tests
|
||||||
|
```
|
|
@ -0,0 +1,141 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Version 1.1.0
|
||||||
|
|
||||||
|
<u>Special Announcement:</u>
|
||||||
|
|
||||||
|
- This is the last version that supports Python 3.8
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u>
|
||||||
|
|
||||||
|
- Pytest >= 7.4 is required (was >= 7.1 before)
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Maintenance: Dependency versions
|
||||||
|
- Maintenance: Support for Python 3.11
|
||||||
|
- Maintenance: CI Configuration cleanup
|
||||||
|
- Maintenance: Project Configuration cleanup (supported versions etc.)
|
||||||
|
- Changed Pytest plugin hook
|
||||||
|
- Tests: Changed version interpretation tests, as the standard lib has a better interpretation now
|
||||||
|
- Coding conventions: Black and isort updated, changed things accordingly
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v1.1.0) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v1.0.0...v1.1.0)
|
||||||
|
|
||||||
|
## Version 1.0.0
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u> ✓ None
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Cleanup of codebase for the big step of going to version 1.0
|
||||||
|
- Project marked as "stable"
|
||||||
|
- Some minor documentation changes
|
||||||
|
- Dependencies updated
|
||||||
|
- Fine-tuning of the tox/test configuration for running on CI
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v1.0.0) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.4.0...v1.0.0)
|
||||||
|
|
||||||
|
## Version 0.4.0
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u> ✓ None
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Structured Documentation (see source folder `docs/`), it is an important milestone to version 1.0; published under:
|
||||||
|
[docs.codebau.dev/pytest-plugins/pytest-csv-params](https://docs.codebau.dev/pytest-plugins/pytest-csv-params)
|
||||||
|
- Documentation widely extended with a lot of extra information
|
||||||
|
- A more detailed changelog as part of this documentation
|
||||||
|
- Some source code / API documentation
|
||||||
|
- `README.md` reduced in favor of the structured documentation
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.4.0) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.3.0...v0.4.0)
|
||||||
|
|
||||||
|
## Version 0.3.0
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u>
|
||||||
|
|
||||||
|
- Column names are now tested for reserved names. If you used reserved names in the past, this might break your tests,
|
||||||
|
despite the fact, that you are greater trouble already.
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Much better handling of column names (headers) in CSV files:
|
||||||
|
- Invalid characters are replaced by a `_`
|
||||||
|
- Names are checked if they are reserved keywords or builtin names
|
||||||
|
- A new parameter `header_renames` to the decorator `@csv_params` allows you to bring your CSV column names to clean
|
||||||
|
variable names
|
||||||
|
- See `README.md` for further details
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.3.0) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.2.2...v0.3.0)
|
||||||
|
|
||||||
|
## Version 0.2.2
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u> ✓ None
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Library updates
|
||||||
|
- For Developers: Added a few extra tests for base functionality of pytest that is used in this plugin.
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.2.2) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.2.0...v0.2.2)
|
||||||
|
|
||||||
|
## Version 0.2.0
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u>
|
||||||
|
|
||||||
|
- The order of the parameters for the decorator `@csv_params` changed to realize the new shorthand form of the decorator
|
||||||
|
(see below). If you used the decorator with keyword parameters only (like it is written in the documentation), you are
|
||||||
|
fine.
|
||||||
|
|
||||||
|
<u>Changes</u>
|
||||||
|
|
||||||
|
- New shorthand form for the decorator `@csv_params`. This is very handy when you have CSV files with no ID column and
|
||||||
|
column names that match the test functions parameters names. Together with the command line parameter introduced in
|
||||||
|
version 0.1.0 you can create very short decorators. See `README.md` for details.
|
||||||
|
- For Developers: Mypy has been added to the test chain for typing analysis
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.2.0) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.1.0...v0.2.0)
|
||||||
|
|
||||||
|
## Version 0.1.0
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u> ✓ None
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- A new command line argument `--csv-params-base-dir` allows you to set a base dir for all relative CSV files. This is
|
||||||
|
great when you have a central storage for your test data. See `README.md` for more details.
|
||||||
|
- Some documentation fixes
|
||||||
|
- Some changes to the tox configuration in order to report coverage correctly
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.1.0) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.0.4...v0.1.0)
|
||||||
|
|
||||||
|
## Version 0.0.4
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u> ✓ None
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Minor documentation bugfixes
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.0.4) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.0.3...v0.0.4)
|
||||||
|
|
||||||
|
## Version 0.0.3
|
||||||
|
|
||||||
|
<u>Breaking Changes:</u> ✓ None
|
||||||
|
|
||||||
|
<u>Changes:</u>
|
||||||
|
|
||||||
|
- Initial Public Release
|
||||||
|
- Delivered the `@csv_params` decorator
|
||||||
|
|
||||||
|
[Downloads](https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases/tag/v0.0.3) |
|
||||||
|
[Technical Changelog](https://git.codebau.dev/pytest-plugins/pytest-csv-params/compare/v0.0.2...v0.0.3)
|
|
@ -0,0 +1,254 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Decorator Parameters
|
||||||
|
|
||||||
|
These are the parameters for the decorator {meth}`pytest_csv_params.decorator.csv_params`.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
| Parameter | Type | Description | Example |
|
||||||
|
|------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
|
||||||
|
| `data_file` | `str` | The CSV file to use, relative or absolute path | `"/var/testdata/test1.csv"` |
|
||||||
|
| `base_dir` | `str` (optional) | Directory to look up relative CSV files (see `data_file`); overrides the command line argument | `join(dirname(__file__), "assets")` |
|
||||||
|
| `id_col` | `str` (optional) | Column name of the CSV that contains test case IDs | `"ID#"` |
|
||||||
|
| `dialect` | `csv.Dialect` (optional) | CSV Dialect definition (see [Python CSV Documentation](https://docs.python.org/3/library/csv.html#dialects-and-formatting-parameters)) | `csv.excel_tab` |
|
||||||
|
| `data_casts` | `dict` (optional) | Cast Methods for the CSV Data (see "Data Casting" below) | `{ "a": int, "b": float }` |
|
||||||
|
| `header_renames` | `dict` (optional) | Replace headers from the CSV file, so that they can be used as parameters for the test function (since 0.3.0) | `{ "Annual Amount of Bananas": "banana_count", "Cherry export price": "cherry_export_price" }` |
|
||||||
|
|
||||||
|
### Detailed Description
|
||||||
|
|
||||||
|
#### `data_file`
|
||||||
|
|
||||||
|
This points to the CSV file to load for this test. You can use relative or absolute paths. If you use a relative path
|
||||||
|
and a `base_dir`, the `base_dir` is prepended to the `data_file`.
|
||||||
|
|
||||||
|
````{admonition} Hint
|
||||||
|
It's a good idea to put your CSV data files in a `test-assets` folder on the same level than your `test_something.py`
|
||||||
|
file.
|
||||||
|
|
||||||
|
Example Layout:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/
|
||||||
|
+- test-assets/
|
||||||
|
| +- case1.csv
|
||||||
|
| +- case2.csv
|
||||||
|
+- test_case1.py
|
||||||
|
+- test_case2.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Now use this for `data_file` and `base_dir` (in one of the `test_caseX.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from os.path import dirname, join
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
@csv_params(data_file="case1.csv", base_dir=join(dirname(__file__), "test-assets"))
|
||||||
|
def test_case1():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
#### `base_dir`
|
||||||
|
|
||||||
|
This is an optional parameter. Set it to the directory where the CSV file from the `data_file` parameter should be
|
||||||
|
looked up. If not `None` (which is the default value), the value will be prepended to the `data_file` value, as long as
|
||||||
|
`data_file` is not an absolute path.
|
||||||
|
|
||||||
|
See `--csv-params-base-dir` command line argument below also.
|
||||||
|
|
||||||
|
```{warning}
|
||||||
|
Setting `base_dir` to something that is not `None` overrides anything that is set by the `--csv-params-base-dir`
|
||||||
|
command line argument.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `id_col`
|
||||||
|
|
||||||
|
Name the column that contains the test case IDs. If `None` (which is the default value), no test case IDs will be
|
||||||
|
generated. In this case, pytest will create its own IDs based on the parameters for the test. The column name does not
|
||||||
|
need to be valid variable/argument name.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"Test Case ID#", "val_a", "val_b"
|
||||||
|
"test-12 / 4", "1234", "4321"
|
||||||
|
"test-13 / 7", "3210", "0123"
|
||||||
|
"test-14 / 9", "5432", "2345"
|
||||||
|
```
|
||||||
|
|
||||||
|
The test case ID is in the column "Test Case ID#". You'd configure it like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from os.path import dirname, join
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
@csv_params(data_file=join(dirname(__file__), "test-assets", "case1.csv"), id_col="Test Case ID#")
|
||||||
|
def test_case1(param_1: str, param_2: str) -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `dialect`
|
||||||
|
|
||||||
|
Set the CSV dialect, it must be of the type {class}`csv.Dialect`. A dialect defines how a CSV file looks like.
|
||||||
|
|
||||||
|
The default dialect is {class}`pytest_csv_params.dialect.CsvParamsDefaultDialect`.
|
||||||
|
|
||||||
|
A dialect consists of the following settings:
|
||||||
|
|
||||||
|
| Setting | Default value in {class}`~pytest_csv_params.dialect.CsvParamsDefaultDialect` |
|
||||||
|
|---------------------------------------|------------------------------------------------------------------------------|
|
||||||
|
| {attr}`~csv.Dialect.delimiter` | `","` |
|
||||||
|
| {attr}`~csv.Dialect.doublequote` | `True` |
|
||||||
|
| {attr}`~csv.Dialect.escapechar` | `None` |
|
||||||
|
| {attr}`~csv.Dialect.lineterminator` | `"\r\n"` |
|
||||||
|
| {attr}`~csv.Dialect.quotechar` | `'"'` |
|
||||||
|
| {attr}`~csv.Dialect.quoting` | {data}`csv.QUOTE_ALL` |
|
||||||
|
| {attr}`~csv.Dialect.skipinitialspace` | `True` |
|
||||||
|
| {attr}`~csv.Dialect.strict` | `True` |
|
||||||
|
|
||||||
|
See [Usage Examples](examples) to learn how to create your own decorator that would always use your own specific CSV
|
||||||
|
file dialect.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
Regardless of the format parameters you are defining, all values from the CSV file are read as `str`. You may need to
|
||||||
|
convert them into other types. This is where `data_casts` are for.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `data_casts`
|
||||||
|
|
||||||
|
This dictionary allows you to setup methods to convert the string values from the CSV files into types or formats
|
||||||
|
required for test execution.
|
||||||
|
|
||||||
|
```{admonition} Rule of thumb
|
||||||
|
1. You can use any method that accepts a single `str` parameter. It can return anything you need.
|
||||||
|
2. If you need to test your test code, you should prefer conversion methods over conversion lambdas.
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"Test Case ID#", "val_a", "val_b", "val_c", "val_d", "val_e"
|
||||||
|
"test-12 / 4", "2.022", "152", "1 x 3", "abcd", "flox"
|
||||||
|
"test-13 / 7", "3.125", "300", "2 x 4", "defg", "trox"
|
||||||
|
"test-14 / 9", "4.145", "150", "3x6x9", "hijk", "bank"
|
||||||
|
```
|
||||||
|
|
||||||
|
- The values of column "Test Case ID#" do not need any conversion. The column will serve as `id_col`.
|
||||||
|
- The values of column "val_a" should be converted into `float`. Since `float` is also a method, it can be used
|
||||||
|
directly.
|
||||||
|
- The values of column "val_b" should be converted into `int`. Since `int` is also a method, it can be used directly.
|
||||||
|
- The values of column "val_c" must be converted a bit more complex. We'll use a `lambda` for that.
|
||||||
|
- The values of column "val_d" don't need to be converted. They are `str`.
|
||||||
|
- The values of column "val_e" will be converted with a helper method (`convert_val_e`).
|
||||||
|
|
||||||
|
Implementation of this example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
def convert_val_e(value: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
str_val = None
|
||||||
|
bool_val = value.endswith("ox")
|
||||||
|
if bool_val:
|
||||||
|
str_val = value[:2]
|
||||||
|
return bool_val, str_val
|
||||||
|
|
||||||
|
@csv_params(
|
||||||
|
data_file="test1.csv",
|
||||||
|
id_col="Test Case ID#",
|
||||||
|
data_casts={
|
||||||
|
"val_a": float,
|
||||||
|
"val_b": int,
|
||||||
|
"val_c": lambda x: list(map(lambda y: y.strip(), x.split("x"))),
|
||||||
|
"val_e": convert_val_e,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_something(val_a: float, val_b: int, val_c: List[int], val_d: str, val_e: Tuple[bool, Optional[str]]) -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
In this example, the columns were named as valid argument/parameter names. So there's no need for `header_renames` here.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `header_renames`
|
||||||
|
|
||||||
|
This dictionary allows to rename the column headers into valid argument names for your test methods. The plugin will try
|
||||||
|
to rename invalid header names by replacing invalid chars with underscores, but this might not result in well-formed and
|
||||||
|
readable names.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"Test Case ID#", "Flux Compensator Setting", "Power Level"
|
||||||
|
"101 / 885 / 31", "1-1-2-1-2-7-5-3-4-9/7", "100 %"
|
||||||
|
"109 / 995 / 21", "3-2-2-2-6-4-2-2-1-2/8", "15 %"
|
||||||
|
"658 / 555 / 54", "3-2-3-4-5-6-7-3-2-3/2", "25 %"
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration of the decorator:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
@csv_params(
|
||||||
|
data_file="test.csv",
|
||||||
|
id_col="Test Case ID#",
|
||||||
|
header_renames={
|
||||||
|
"Flux Compensator Setting": "flux_setting",
|
||||||
|
"Power Level": "power_level",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_something_else(fux_setting: str, power_level: str) -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
```{warning}
|
||||||
|
`data_casts` dictionary keys must match the renamed column names!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command Line Arguments
|
||||||
|
|
||||||
|
These are the command line arguments for the pytest run.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
| Argument | Required | Description | Example |
|
||||||
|
|-------------------------|---------------|----------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `--csv-params-base-dir` | no (optional) | Define a base dir for all relative-path CSV data files (since 0.1.0) | `pytest --csv-params-base-dir /var/testdata` |
|
||||||
|
|
||||||
|
### Detailed Description
|
||||||
|
|
||||||
|
#### `--csv-params-base-dir`
|
||||||
|
|
||||||
|
This is a convenience command line argument. It allows you to set a base directory for all your CSV parametrized test
|
||||||
|
cases. If you use relative `data_file`s, this can be automatically prepended. You can still override this setting per
|
||||||
|
test by using the `base_dir` configuration.
|
||||||
|
|
||||||
|
## How a CSV file is found
|
||||||
|
|
||||||
|
```text
|
||||||
|
+-----------------------------------+ /-----------------------------------\
|
||||||
|
| data_dir is absolute path? | --- yes --- | use this path |
|
||||||
|
+-----------------------------------+ \-----------------------------------/
|
||||||
|
|
|
||||||
|
no
|
||||||
|
|
|
||||||
|
+-----------------------------------+ /-----------------------------------\
|
||||||
|
| is a base_dir set on the test? | --- yes --- | prepend base_dir to data_file |
|
||||||
|
+-----------------------------------+ \-----------------------------------/
|
||||||
|
|
|
||||||
|
no
|
||||||
|
|
|
||||||
|
+-----------------------------------+ /-----------------------------------\
|
||||||
|
| is command line argument given? | --- yes --- | prepend arg value to data_file |
|
||||||
|
+-----------------------------------+ \-----------------------------------/
|
||||||
|
|
|
||||||
|
no
|
||||||
|
|
|
||||||
|
/-----------------------------------\
|
||||||
|
| use data_file as relative path |
|
||||||
|
\-----------------------------------/
|
||||||
|
```
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. include:: ../../CONTRIBUTING.md
|
||||||
|
:parser: myst_parser.sphinx_
|
||||||
|
```
|
|
@ -0,0 +1,195 @@
|
||||||
|
# Developer Guide
|
||||||
|
|
||||||
|
If you want to develop for / with the Pytest CSV Params Plugin, consider to clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.codebau.dev/pytest-plugins/pytest-csv-params.git
|
||||||
|
```
|
||||||
|
|
||||||
|
You need **Python 3.8** or newer.
|
||||||
|
|
||||||
|
The project's dependencies and building are managed by `poetry`. Please follow the instructions from
|
||||||
|
[python-poetry.org](https://python-poetry.org/) to install `poetry` on your system.
|
||||||
|
|
||||||
|
Install all the dependencies, including the development dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Signing
|
||||||
|
|
||||||
|
Commit signing is mandatory for all commits for the `main` branch. Please make sure, your public key is set up and
|
||||||
|
registered with `git.codebau.dev`.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests are implemented with `pytest`. You find them in the `tests` folder. Besides unit and integration tests, some other
|
||||||
|
checks are executed by `pytest` plugins:
|
||||||
|
|
||||||
|
- **`pytest-black`:** This plugin checks code formatting with [`black`](https://github.com/psf/black). If tests fail,
|
||||||
|
try `poetry run black .` from the project root to fix formatting issues. Configuration: `pyproject.toml`, section
|
||||||
|
`[tool.black]`.
|
||||||
|
- **`pytest-isort`:** This plugin checks import sorting with [`isort`](https://github.com/PyCQA/isort). If tests fail,
|
||||||
|
try `poetry run isort .` from the project root to fix import sorting issues. Configuration: `pyproject.toml`, section
|
||||||
|
`[tool.isort]`.
|
||||||
|
- **`pytest-pylint`:** This plugin does a static code analysis with [`pylint`](https://github.com/PyCQA/pylint). The
|
||||||
|
test configuration can be found in `.pylintrc` in the project root.
|
||||||
|
- **`pytest-bandit`:** This plugin performs a static security analysis of the code with
|
||||||
|
[`bandit`](https://github.com/PyCQA/bandit). The configuration is part of the `[tool.pytest.ini_options]` section in
|
||||||
|
the `pyproject.toml`, config keys `bandit_*`.
|
||||||
|
- **`pytest-mypy`:** This plugin uses [`mypy`](https://mypy.readthedocs.io/en/stable/) to perform typing checks against
|
||||||
|
the code. The configuration can be found in the `pyproject.toml`, section `[tool.mypy]`.
|
||||||
|
|
||||||
|
Most plugins are enabled by the `addopts` switches, configured in the `pyproject.toml`, section
|
||||||
|
`[tool.pytest.ini_options]`. Some plugins have extra configuration switches even there.
|
||||||
|
|
||||||
|
Additionally, the code coverage is measured by `pytest-cov` using [`coverage.py`](https://github.com/nedbat/coveragepy).
|
||||||
|
A high coverage alone is not a very good metric, but it helps to find and fix coverage weaknesses. The configuration for
|
||||||
|
coverage measurement is in the `pyproject.toml`, sections `[tool.coverage]`, `[tool.coverage.run]` and
|
||||||
|
`[tool.coverage.report]`.
|
||||||
|
|
||||||
|
There are some other pytest plugins installed and used for tests:
|
||||||
|
|
||||||
|
- **`pytest-mock`:** Simplified mocking
|
||||||
|
- **`pytest-clarity`:** Better output of assertion errors
|
||||||
|
- **`pytest-order`:** Execute tests in a given order (used in {mod}`tests.poc.test_parametrize_with_generator`).
|
||||||
|
|
||||||
|
### Test runs with `pytest`
|
||||||
|
|
||||||
|
Just run all the tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test runs with `tox`
|
||||||
|
|
||||||
|
`tox` is used to execute all tests under the different supported Python versions. Make sure you installed all relevant
|
||||||
|
versions on your system, for example with [`pyenv`](https://github.com/pyenv/pyenv).
|
||||||
|
|
||||||
|
To execute them all, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry run tox
|
||||||
|
```
|
||||||
|
|
||||||
|
If you experience strange `tox` errors, try to recreate the `tox` environments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry run tox -r
|
||||||
|
```
|
||||||
|
|
||||||
|
`tox` is configured in the `pyproject.toml`, section `[tool.tox]`.
|
||||||
|
|
||||||
|
```{admonition} No new or changed code without test
|
||||||
|
If you add or change code, please make sure your changes are covered by meaningful tests.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
There are two different things to build from the source code: The **Wheel distribution package** from the Python code
|
||||||
|
and the **documentation**.
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
The building and deployment is managed by `poetry`. The complete build and deploy configuration takes place in the
|
||||||
|
`pyproject.toml`. Besides the standard configuration in section `[tool.poetry]`, additional URLs are defined in section
|
||||||
|
`[tool.poetry.urls]`. As a speciality for this plugin, an entry point is defined in section
|
||||||
|
`[tool.poetry.plugins."pytest11"]`.
|
||||||
|
|
||||||
|
To build the packages, just run `poetry build` from the project root.
|
||||||
|
|
||||||
|
(build-docs)=
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
The docs are in the `docs` folder. There is a `conf.py` that contains all the settings. Documentation is managed by
|
||||||
|
[`sphinx`](https://www.sphinx-doc.org/). There is a `make` file (`Makefile`) as well as a `make.bat`, they contain some
|
||||||
|
configuration also.
|
||||||
|
|
||||||
|
The `serve.py` scripts starts a live reload server to preview the documentation.
|
||||||
|
|
||||||
|
To build the documentation, run `poetry run make html` (respectively `poetry run make.bat html` on Windows) from the
|
||||||
|
`docs` directory.
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
```{warning}
|
||||||
|
The following section is more a reference for project members. If you not belong to the project, you'll not be able to
|
||||||
|
publish or update packages.
|
||||||
|
|
||||||
|
Maybe you find it helpful as a boiler plate for your own projects.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Increase Version
|
||||||
|
|
||||||
|
If not already done, increase the version in the `pyproject.toml`. This can be done manually, but `poetry` offers a
|
||||||
|
helper for that:
|
||||||
|
|
||||||
|
| `poetry` command | Effect |
|
||||||
|
|------------------------|----------------|
|
||||||
|
| `poetry version patch` | increase patch |
|
||||||
|
| `poetry version minor` | increase minor |
|
||||||
|
| `poetry version major` | increase major |
|
||||||
|
|
||||||
|
### Complete Changelog
|
||||||
|
|
||||||
|
Update the `docs/pages/changelog.md` file with all relevant things happened since the last release. Set a compare link
|
||||||
|
and a link to the release page. You can set them up even if the release does not exist at the moment.
|
||||||
|
|
||||||
|
Don't forget to commit now!
|
||||||
|
|
||||||
|
### Tag the release
|
||||||
|
|
||||||
|
Set a git tag in the format `vX.Y.Z` (with the leading `v`). Push all your commits and the tag now.
|
||||||
|
|
||||||
|
### PyPI
|
||||||
|
|
||||||
|
```{admonition} Poetry configuration for publishing
|
||||||
|
If not already done, you need to setup `poetry` for publishing.
|
||||||
|
|
||||||
|
**1. Configuration for production PyPI**
|
||||||
|
|
||||||
|
- Get your token from [pypi.org](https://pypi.org/)
|
||||||
|
- Set your token with `poetry config pypi-token.pypi pypi-YOUR_PROD_TOKEN`
|
||||||
|
|
||||||
|
**2. Configuration for test PyPI**
|
||||||
|
|
||||||
|
- Get your token from [test.pypi.org](https://test.pypi.org/)
|
||||||
|
- Setup the test repo: `poetry config repositories.test.url https://test.pypi.org/legacy/`
|
||||||
|
- Set your token with `poetry config pypi-token.test pypi-YOUR_TEST_TOKEN`
|
||||||
|
|
||||||
|
**3. Configuration for Codebau Package Repository**
|
||||||
|
|
||||||
|
- Get your token from [git.codebau.dev](https://git.codebau.dev/)
|
||||||
|
- Setup the codebau repo:
|
||||||
|
`poetry config repositories.codebau.url https://git.codebau.dev/api/packages/pytest-plugins/pypi`
|
||||||
|
- Setup your token with `poetry config pypi-token.codebau YOUR_CODEBAU_TOKEN`
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Publish to test.pypi.org
|
||||||
|
|
||||||
|
It's a good practice to publish a new package to [test.pypi.org](https://test.pypi.org/) first.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry publish --build -r test
|
||||||
|
```
|
||||||
|
|
||||||
|
You can omit the `--build` param when you already built the package.
|
||||||
|
|
||||||
|
#### Publish to production pypi.org
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry publish --build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Publish to git.codebau.dev Package Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry publish --build -r codebau
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
The documentation is automatically build from the `main` branch and published to `docs.codebau.dev`. If you want to
|
||||||
|
build by yourself, see {ref}`Building / Docs <build-docs>`. You find the compiled docs under `dist/docs/html`.
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Usage Examples
|
||||||
|
|
||||||
|
## Build your own annotation
|
||||||
|
|
||||||
|
Using another CSV format? The same `data_casts` methods each time? There is only one name for a test ID column? You can
|
||||||
|
easily build your own annotation. Just create a method that contains your common stuff:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
def my_csv_params(data_file: str, **kwargs):
|
||||||
|
kwargs.setdefault("base_dir", "/var/test-data")
|
||||||
|
kwargs.setdefault("id_col", "Test Case ID")
|
||||||
|
kwargs.setdefault("header_renames", {
|
||||||
|
"Order #": "order_number",
|
||||||
|
"Price Total": "total_price",
|
||||||
|
})
|
||||||
|
kwargs.setdefault("data_casts", {
|
||||||
|
"total_price": float,
|
||||||
|
})
|
||||||
|
return csv_params(data_file, **kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
When you now write a test, you can decorate it with `@my_csv_params("test-file-1.csv")`. You can override any of your
|
||||||
|
default settings by just adding it as a keyword argument: `@my_csv_params("test-file-1.csv", id_col="Test ID")`.
|
|
@ -0,0 +1,128 @@
|
||||||
|
# User Guide
|
||||||
|
|
||||||
|
This guide will lead you to your first CSV-file parametrized pytest test. It starts with designing your test, preparing
|
||||||
|
your data, writing the test method and finally execute your new test.
|
||||||
|
|
||||||
|
## The Scenario
|
||||||
|
|
||||||
|
Let's say, you have to test this method:
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../tests/test_docs_example.py
|
||||||
|
:language: python
|
||||||
|
:lines: 10,12,18-23,37-41
|
||||||
|
```
|
||||||
|
|
||||||
|
Parts of the code are from a more complex example written for
|
||||||
|
[a German blog post](https://juergen.rocks/blog/articles/data-driven-tests-mit-pytest-csv-params.html). The example code
|
||||||
|
is part of the source code and can be found unter `tests/test_blog_example.py`. It is documented as
|
||||||
|
{mod}`~tests.test_blog_example`.
|
||||||
|
|
||||||
|
## Prepare your data
|
||||||
|
|
||||||
|
Your test data resides in an CSV file. CSV files can have different formats, when it comes to:
|
||||||
|
|
||||||
|
- Field separators and delimiters
|
||||||
|
- Quoting
|
||||||
|
- Line Termination
|
||||||
|
|
||||||
|
The class {class}`pytest_csv_params.dialect.CsvParamsDefaultDialect` defines a default CSV format that should fit most
|
||||||
|
requirements:
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../pytest_csv_params/dialect.py
|
||||||
|
:language: python
|
||||||
|
:lines: 5-6,8,18-
|
||||||
|
```
|
||||||
|
|
||||||
|
You can derive your own CSV format class from there (or from {class}`csv.Dialect`), if your files look any other.
|
||||||
|
|
||||||
|
Your test data for the method above could look like this:
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../tests/assets/doc-example.csv
|
||||||
|
:language: text
|
||||||
|
:emphasize-lines: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
- We have a header line in the first line, that names the single columns
|
||||||
|
- The column names are not good for argument names
|
||||||
|
- The value in the dimensions column needs to be transformed in order to get tested
|
||||||
|
- There is a column that tells if an exception is to be expected, and the last two lines expect one
|
||||||
|
|
||||||
|
## Design and write the test
|
||||||
|
|
||||||
|
The test must call the ``get_smallest_possible_container`` method with the right parameters. The CSV file has all
|
||||||
|
information, but maybe not in the right format. We take care of that in a second.
|
||||||
|
|
||||||
|
The test may expect an exception, that should also be considered.
|
||||||
|
|
||||||
|
The parameters of the test method should reflect the input parameters for the method under test, and the expectations.
|
||||||
|
|
||||||
|
So let's build it:
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../tests/test_docs_example.py
|
||||||
|
:language: python
|
||||||
|
:lines: 14-15,75-81,91-
|
||||||
|
:emphasize-lines: 4-8
|
||||||
|
```
|
||||||
|
|
||||||
|
- The test could now get all parameters needed to execute the `get_smallest_container_method`, as well as for the
|
||||||
|
expectations
|
||||||
|
- Based on the expectation for an exception, the test goes in two different directions
|
||||||
|
|
||||||
|
Now it's time for getting stuff from the CSV file.
|
||||||
|
|
||||||
|
## Add the parameters from the CSV file
|
||||||
|
|
||||||
|
Here comes the {meth}`~pytest_csv_params.decorator.csv_params` decorator. But one step after the other.
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../tests/test_docs_example.py
|
||||||
|
:language: python
|
||||||
|
:lines: 14,16-17,58-81
|
||||||
|
:emphasize-lines: 5,6,8,16,18
|
||||||
|
```
|
||||||
|
|
||||||
|
- With the parameter `data_file` you point to your CSV file
|
||||||
|
- With the parameter `id_col` you name the column of the CSV file that contains the test case ID; the test case ID is
|
||||||
|
shown in the execution logs
|
||||||
|
- With the `header_renames` dictionary you define how a column is represented as argument name for your test method; the
|
||||||
|
highlighted example transforms "Number of items" to `number_of_items`
|
||||||
|
- The `data_casts` dictionary you define how data needs to be transformed to be usable for the test; you can use
|
||||||
|
`lambda`s or method pointers; all values from the CSV arrive as `str`
|
||||||
|
|
||||||
|
All possible parameters are explained under [Configuration](config), or more technically, in the source documentation of
|
||||||
|
{meth}`pytest_csv_params.decorator.csv_params`.
|
||||||
|
|
||||||
|
The `data_casts` method `get_dimensions` looks like the following:
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../tests/test_docs_example.py
|
||||||
|
:language: python
|
||||||
|
:lines: 44,52-55
|
||||||
|
:emphasize-lines: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
The method is called during the test collection phase. If the {class}`ValueError` raises, the run would end in an error.
|
||||||
|
|
||||||
|
## Execute the test
|
||||||
|
|
||||||
|
There is nothing special to do now. Just run your tests as always. Your run should look like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Small Container 1] PASSED [ 12%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Small Container 2] PASSED [ 25%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Small Container 3] PASSED [ 37%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Medium Container] PASSED [ 50%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Large Container 1] PASSED [ 62%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Large Container 2] PASSED [ 75%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Not fitting 1] PASSED [ 87%]
|
||||||
|
tests/test.py::test_get_smallest_possible_container[Not fitting 2] PASSED [100%]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analyse test failures
|
||||||
|
|
||||||
|
- Is it a failure for all test data elements or just for a few?
|
||||||
|
- When only some tests fail, the Test ID should tell you where to look at
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
There are serveral ways to install the package. The `pip` and the [`poetry`](https://python-poetry.org/) ways are
|
||||||
|
recommended.
|
||||||
|
|
||||||
|
The minimum requirements are:
|
||||||
|
|
||||||
|
- Python >= 3.8, < 3.12
|
||||||
|
- Pytest >= 7.4
|
||||||
|
|
||||||
|
The plugin should run anywhere where these two things can be used.
|
||||||
|
|
||||||
|
## Install via `pip`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pytest_csv_params
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can use the codebau.dev package repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --extra-index-url https://git.codebau.dev/api/packages/pytest-plugins/pypi/simple pytest-csv-params
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install via `poetry`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry add --group dev pytest_csv_params
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can use the codebau.dev package repository with `poetry` also:
|
||||||
|
|
||||||
|
- Enable the repository as an explicit source (you should only need to do this once):
|
||||||
|
```bash
|
||||||
|
poetry source add --priority=explicit codebau_pytest_plugins https://git.codebau.dev/api/packages/pytest-plugins/pypi/simple
|
||||||
|
```
|
||||||
|
|
||||||
|
- Install the package:
|
||||||
|
```bash
|
||||||
|
poetry add --source codebau_pytest_plugins --group dev pytest_csv_params
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about `poetry`, visit [python-poetry.org](https://python-poetry.org/)
|
||||||
|
|
||||||
|
## For development
|
||||||
|
|
||||||
|
Please checkout the repository from [git.codebau.dev](https://git.codebau.dev/pytest-plugins/pytest-csv-params).
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Issues
|
||||||
|
|
||||||
|
Please report issues to `csv_params-issues`(at)`jued.de`. We'll put them in our issue tracker under
|
||||||
|
[git.codebau.dev/pytest-plugins/pytest-csv-params/issues](https://git.codebau.dev/pytest-plugins/pytest-csv-params/issues).
|
|
@ -0,0 +1,9 @@
|
||||||
|
# License
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. literalinclude:: ../../LICENSE.txt
|
||||||
|
:language: text
|
||||||
|
```
|
||||||
|
|
||||||
|
The source code is published on [git.codebau.dev](https://git.codebau.dev/pytest-plugins/pytest-csv-params).
|
||||||
|
The `LICENSE.txt` file is in the repository root.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Python Module Index
|
||||||
|
===================
|
|
@ -0,0 +1,28 @@
|
||||||
|
# pylint: skip-file
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from os.path import dirname, join
|
||||||
|
|
||||||
|
from livereload import Server, shell
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmd = "make html"
|
||||||
|
if "win32" in sys.platform.lower():
|
||||||
|
cmd = "make.bat html"
|
||||||
|
|
||||||
|
server = Server()
|
||||||
|
project_dir = join(dirname(__file__), "..")
|
||||||
|
watch_dirs = [
|
||||||
|
join(project_dir, "pytest_csv_params", "**/*"),
|
||||||
|
join(project_dir, "_ptcsvp", "**/*"),
|
||||||
|
join(project_dir, "tests", "**/*"),
|
||||||
|
join(project_dir, "docs", "**/*"),
|
||||||
|
]
|
||||||
|
for watch_dir in watch_dirs:
|
||||||
|
server.watch(watch_dir, shell(cmd), delay=1)
|
||||||
|
server.serve(
|
||||||
|
root=join(project_dir, "dist", "docs", "html"),
|
||||||
|
host="127.0.0.1",
|
||||||
|
port="8000",
|
||||||
|
)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pytest-csv-params"
|
name = "pytest-csv-params"
|
||||||
version = "0.0.2"
|
version = "1.1.0"
|
||||||
description = "Pytest plugin for Test Case Parametrization with CSV files"
|
description = "Pytest plugin for Test Case Parametrization with CSV files"
|
||||||
authors = ["Juergen Edelbluth <csv_params@jued.de>"]
|
authors = ["Juergen Edelbluth <csv_params@jued.de>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -8,11 +8,12 @@ repository = "https://git.codebau.dev/pytest-plugins/pytest-csv-params"
|
||||||
homepage = "https://git.codebau.dev/pytest-plugins/pytest-csv-params"
|
homepage = "https://git.codebau.dev/pytest-plugins/pytest-csv-params"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = [
|
keywords = [
|
||||||
"py.test", "pytest", "csv", "params", "parametrize", "pytest-plugin",
|
"py.test", "pytest", "csv", "params", "parametrize", "pytest-plugin", "ddt", "data-driven"
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Plugins",
|
"Environment :: Plugins",
|
||||||
|
"Environment :: Console",
|
||||||
"Framework :: Pytest",
|
"Framework :: Pytest",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
|
@ -35,13 +36,15 @@ packages = [
|
||||||
"Issue Tracker" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/issues"
|
"Issue Tracker" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/issues"
|
||||||
"Wiki" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/wiki"
|
"Wiki" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/wiki"
|
||||||
"Releases" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases"
|
"Releases" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases"
|
||||||
"Documentation" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/src/branch/main/README.md"
|
"Documentation" = "https://docs.codebau.dev/pytest-plugins/pytest-csv-params/"
|
||||||
|
"Changelog" = "https://docs.codebau.dev/pytest-plugins/pytest-csv-params/pages/changelog.html"
|
||||||
|
|
||||||
[tool.poetry.plugins."pytest11"]
|
[tool.poetry.plugins."pytest11"]
|
||||||
"pytest-csv-params" = "pytest_csv_params.plugin"
|
"pytest-csv-params" = "pytest_csv_params.plugin"
|
||||||
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--black --isort --pylint --pylint-rcfile=.pylintrc --cov --cov-report=term-missing --junitxml=test-reports/pytest_csv_params.xml"
|
addopts = "--mypy --black --isort --pylint --pylint-rcfile=.pylintrc --cov --cov-report=term-missing --cov-report=xml --junitxml=test-reports/pytest_csv_params.xml"
|
||||||
filterwarnings=[
|
filterwarnings=[
|
||||||
"ignore:.*BlackItem.*:_pytest.warning_types.PytestDeprecationWarning",
|
"ignore:.*BlackItem.*:_pytest.warning_types.PytestDeprecationWarning",
|
||||||
"ignore:.*BlackItem.*:_pytest.warning_types.PytestRemovedIn8Warning",
|
"ignore:.*BlackItem.*:_pytest.warning_types.PytestRemovedIn8Warning",
|
||||||
|
@ -80,6 +83,11 @@ omit = [
|
||||||
"*pip-build-env*",
|
"*pip-build-env*",
|
||||||
"*/test_plugin_test_multiplication.py",
|
"*/test_plugin_test_multiplication.py",
|
||||||
"*/test_plugin_test_error.py",
|
"*/test_plugin_test_error.py",
|
||||||
|
"*/test_base_dir_param.py",
|
||||||
|
"*/test_plugin_test_text_shorthand.py",
|
||||||
|
"*/test_plugin_test_all_one.py",
|
||||||
|
"*/test_plugin_test_all_two.py",
|
||||||
|
"*/test_plugin_test_all_three.py",
|
||||||
]
|
]
|
||||||
relative_files = true
|
relative_files = true
|
||||||
|
|
||||||
|
@ -93,41 +101,60 @@ exclude_lines = [
|
||||||
"if __name__ == .__main__.:",
|
"if __name__ == .__main__.:",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.8"
|
||||||
|
strict = true
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
[tool.tox]
|
[tool.tox]
|
||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
[tox]
|
[tox]
|
||||||
minversion = 3.25.0
|
minversion = 3.25.0
|
||||||
envlist = py38,py39,py310
|
envlist = clean,py38,py39,py310,py311
|
||||||
|
isolated_build = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
pytest
|
pytest --cov-append
|
||||||
|
|
||||||
|
[testenv:clean]
|
||||||
|
skip_install = true
|
||||||
|
commands =
|
||||||
|
coverage erase
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
target-version = ['py38', 'py39', 'py310']
|
target-version = ['py38', 'py39', 'py310', 'py311']
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
line_length = 120
|
line_length = 120
|
||||||
include_trailing_comma = "True"
|
include_trailing_comma = true
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
pytest = "^7.1.2"
|
pytest = "^7.4.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
tox = "^3.25.1"
|
tox = "^3.25.1"
|
||||||
tox-poetry = "^0.4.1"
|
tox-poetry = "^0.4.1"
|
||||||
pytest-black = "^0.3.12"
|
pytest-black = "^0.3.12"
|
||||||
pytest-isort = "^3.0.0"
|
pytest-isort = "^3.0.0"
|
||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^4.1.0"
|
||||||
pytest-pylint = "^0.18.0"
|
pytest-pylint = "^0.18.0"
|
||||||
pytest-mock = "^3.8.2"
|
pytest-mock = "^3.8.2"
|
||||||
pytest-clarity = "^1.0.1"
|
pytest-clarity = "^1.0.1"
|
||||||
pytest-bandit = "^0.6.1"
|
pytest-bandit = "^0.6.1"
|
||||||
|
pytest-mypy = "^0.9.1"
|
||||||
|
pytest-order = "^1.0.1"
|
||||||
|
Sphinx = "^5.1.1"
|
||||||
|
myst-parser = "^0.18.0"
|
||||||
|
sphinx-material = "^0.0.35"
|
||||||
|
sphinx-autodoc-typehints = "^1.19.2"
|
||||||
|
livereload = "^2.6.3"
|
||||||
|
tomli = "^2.0.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
"""
|
"""
|
||||||
Add CSV Data to test
|
This module defines/publishes the main decorator.
|
||||||
"""
|
"""
|
||||||
from _ptcsvp.parametrize import add_parametrization
|
from _ptcsvp.parametrize import add_parametrization
|
||||||
|
|
||||||
csv_params = add_parametrization
|
csv_params = add_parametrization
|
||||||
|
"""
|
||||||
|
Decorator ``@csv_params``
|
||||||
|
|
||||||
|
For supported arguments, see :py:meth:`~_ptcsvp.parametrize.add_parametrization`.
|
||||||
|
"""
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
"""
|
"""
|
||||||
CSV Dialects
|
Definition of CSV dialects (CSV file formats). At the moment, there is only the default dialect
|
||||||
|
:class:`~pytest_csv_params.dialect.CsvParamsDefaultDialect`.
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
|
|
||||||
class CsvParamsDefaultDialect(csv.Dialect): # pylint: disable=too-few-public-methods
|
class CsvParamsDefaultDialect(csv.Dialect): # pylint: disable=too-few-public-methods
|
||||||
"""
|
"""
|
||||||
Basic CSV Dialect for most Tests
|
This is the default dialect (or CSV file format) for parametrizing test. It is used when no other dialect is
|
||||||
|
defined.
|
||||||
|
|
||||||
|
One can easily adapt it to match your own CSV files. Just use this or :class:`csv.Dialect` as base class.
|
||||||
|
|
||||||
|
See :class:`csv.Dialect` for configuration reference.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
delimiter = ","
|
delimiter = ","
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
"""
|
"""
|
||||||
Exceptions
|
Collection of all plugin specific exceptions. All exceptions are derived from very common base types, such as
|
||||||
|
:class:`FileNotFoundError`, :class:`IOError` or :class:`ValueError` to ease the exception handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CsvParamsDataFileNotFound(FileNotFoundError):
|
class CsvParamsDataFileNotFound(FileNotFoundError):
|
||||||
"""
|
"""
|
||||||
File Not Found
|
This exception is thrown when a CSV file was not found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CsvParamsDataFileInaccessible(IOError):
|
class CsvParamsDataFileInaccessible(IOError):
|
||||||
"""
|
"""
|
||||||
Cannot Access the File
|
This exception is thrown when the CSV file is inaccessible.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CsvParamsDataFileInvalid(ValueError):
|
class CsvParamsDataFileInvalid(ValueError):
|
||||||
"""
|
"""
|
||||||
CSV Data is somehow invalid
|
This exception is thrown when a CSV file contains invalid data.
|
||||||
|
|
||||||
|
See the exception message for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CsvHeaderNameInvalid(ValueError):
|
||||||
|
"""
|
||||||
|
This exception is thrown when a CSV file contains an invalid header name that could not be replaced.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
"""
|
"""
|
||||||
Pytest Plugin Entrypoint
|
Pytest Plugin Entrypoint:
|
||||||
|
This module contains all the code to initialize the pytest plugin. This is the entrypoint configured in the
|
||||||
|
`pyproject.toml` as `pytest11`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from _ptcsvp.cmdline import pytest_addoption as _pytest_addoption
|
||||||
from _ptcsvp.configure import pytest_configure as _pytest_configure
|
from _ptcsvp.configure import pytest_configure as _pytest_configure
|
||||||
from _ptcsvp.configure import pytest_unconfigure as _pytest_unconfigure
|
from _ptcsvp.configure import pytest_unconfigure as _pytest_unconfigure
|
||||||
from _ptcsvp.version import check_pytest_version, check_python_version
|
from _ptcsvp.version import check_pytest_version, check_python_version
|
||||||
|
@ -12,4 +15,17 @@ check_pytest_version()
|
||||||
|
|
||||||
# Basic config
|
# Basic config
|
||||||
pytest_configure = _pytest_configure
|
pytest_configure = _pytest_configure
|
||||||
|
"""
|
||||||
|
Hook our :meth:`_ptcsvp.configure.pytest_configure` method to setup the plugin setup
|
||||||
|
"""
|
||||||
|
|
||||||
pytest_unconfigure = _pytest_unconfigure
|
pytest_unconfigure = _pytest_unconfigure
|
||||||
|
"""
|
||||||
|
Hook our :meth:`_ptcsvp.configure.pytest_unconfigure` method to setup the plugin teardown
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Command Line Arguments
|
||||||
|
pytest_addoption = _pytest_addoption
|
||||||
|
"""
|
||||||
|
Hook our :meth:`_ptcsvp.cmdline.pytest_addoption` method to setup our command line arguments
|
||||||
|
"""
|
||||||
|
|
|
@ -1,17 +1,65 @@
|
||||||
"""
|
"""
|
||||||
Types to ease the usage of the API
|
This module contains type definitions to ease the usage of the API and its documentation.
|
||||||
|
|
||||||
|
Some types are somewhat complex, and it is easier to use a single word/reference instead of a complex typing construct.
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
from typing import Callable, Dict, Optional, Type, TypeVar
|
from typing import Any, Callable, Dict, Optional, Type
|
||||||
|
|
||||||
T = TypeVar("T")
|
DataCast = Callable[[str], Any]
|
||||||
|
"""
|
||||||
|
A :class:`DataCast` describes how a data casting callable must be implemented. It requires one parameter of the type
|
||||||
|
:class:`str` and can return anything that is required.
|
||||||
|
"""
|
||||||
|
|
||||||
DataCast = Callable[[str], T]
|
|
||||||
DataCastDict = Dict[str, DataCast]
|
DataCastDict = Dict[str, DataCast]
|
||||||
|
"""
|
||||||
|
A :class:`DataCastDict` describes how a dictionary of data casting callables must look like. The key is a :class:`str`
|
||||||
|
describing the column name, the value is a :class:`DataCast`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DataCasts = Optional[DataCastDict]
|
||||||
|
"""
|
||||||
|
The :class:`DataCasts` type describes the type of the `data_casts` parameter of the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator. An optional :class:`DataCastDict`.
|
||||||
|
"""
|
||||||
|
|
||||||
BaseDir = Optional[str]
|
BaseDir = Optional[str]
|
||||||
|
"""
|
||||||
|
The :class:`BaseDir` describes the type of the `base_dir` parameter of the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator to search for non-absolute CSV files. It is simply an optional
|
||||||
|
:class:`str`.
|
||||||
|
"""
|
||||||
|
|
||||||
IdColName = Optional[str]
|
IdColName = Optional[str]
|
||||||
|
"""
|
||||||
|
The :class:`IdColName` describes the type of the `id_col` parameter of the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator to name the ID column from a CSV file. It is simply an
|
||||||
|
optional :class:`str`.
|
||||||
|
"""
|
||||||
|
|
||||||
DataFile = str
|
DataFile = str
|
||||||
|
"""
|
||||||
|
The :class:`DataFile` describes the type if the `data_file` parameter of the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator to define the CSV file to use. It is an obligatory
|
||||||
|
:class:`str`.
|
||||||
|
"""
|
||||||
|
|
||||||
CsvDialect = Type[csv.Dialect]
|
CsvDialect = Type[csv.Dialect]
|
||||||
|
"""
|
||||||
|
The :class:`CsvDialect` describes the type of the `dialect` parameter of the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator. It is required, but it has an default value in
|
||||||
|
:class:`pytest_csv_params.dialect.CsvParamsDefaultDialect`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
HeaderRenamesDict = Dict[str, str]
|
||||||
|
"""
|
||||||
|
The :class:`HeaderRenamesDict` describes how a dictionary of header renames must look. Keys and values must both be of
|
||||||
|
type :class:`str`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
HeaderRenames = Optional[HeaderRenamesDict]
|
||||||
|
"""
|
||||||
|
The :class:`HeaderRenames` describes the type of the `header_renames` parameter of the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator. It is just an optional :class:`HeaderRenamesDict`.
|
||||||
|
"""
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
"Order-Ref #", "Anz. Schrauben-Päck.", "Dim. Schrauben-Päck.", "Anz. Scheiben-Päck.", "Dim. Scheiben-Päck.", "Volumen Container"
|
||||||
|
"221-12-A-24", "670", "30 x 50 x 70 mm", "150", "40 x 50 x 70 mm", "1 m³"
|
||||||
|
"281-13-C-15", "5000", "30 x 50 x 70 mm", "10000", "40 x 50 x 70 mm", "5 m³"
|
||||||
|
"281-13-C-76", "50000", "35 x 55 x 75 mm", "5000", "50 x 60 x 90 mm", "10 m³"
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
"Test-ID", "Number of items", "Dimensions of item", "Expected Container Size", "Expect Exception?", "Expected Message"
|
||||||
|
"Small Container 1", "15", "1 x 2 x 3", "1000", "N", ""
|
||||||
|
"Small Container 2", "125", "2 x 2 x 2", "1000", "N", ""
|
||||||
|
"Small Container 3", "16", "3 x 4 x 5", "1000", "N", ""
|
||||||
|
"Medium Container", "17", "3 x 4 x 5", "2500", "N", ""
|
||||||
|
"Large Container 1", "2", "15 x 12 x 10", "7500", "N", ""
|
||||||
|
"Large Container 2", "1", "16 x 20 x 20", "7500", "N", ""
|
||||||
|
"Not fitting 1", "2", "16 x 20 x 18", "0", "Y", "No container available"
|
||||||
|
"Not fitting 2", "7501", "1 x 1 x 1", "0", "Y", "No container available"
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
"Test ID","Bananas shipped","Single Banana Weight","Apples shipped","Single Apple Weight","Container Size"
|
||||||
|
"Order-7","1503","0.5","2545","0.25","1500"
|
||||||
|
"Order-15","101","0.55","1474","0.33","550"
|
|
|
@ -1,5 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Conftest project global
|
Project global fixtures, plugins etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pytest_plugins = ["pytester"]
|
pytest_plugins = ["pytester"]
|
||||||
|
"""
|
||||||
|
Load the fixture `pytester` for all tests. Even if we don't need it everywhere (we need it only during the plugin
|
||||||
|
tests), this fixture requires to be loaded in the topmost :mod:`~tests.conftest` module.
|
||||||
|
"""
|
||||||
|
|
|
@ -9,5 +9,5 @@ from pytest_csv_params.decorator import csv_params
|
||||||
"expected_length": int,
|
"expected_length": int,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_fruits(word, expected_length):
|
def test_{{test_name}}(word, expected_length):
|
||||||
assert len(word) == expected_length
|
assert len(word) == expected_length
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
"text_a","text_b","text_c"
|
||||||
|
"aaa","bbb","aaa:bbb"
|
||||||
|
"abc","xyz","xyz:abc"
|
||||||
|
"xyz","abc","xyz:abc"
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import pytest
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
|
||||||
|
@csv_params(r"{{data_file}}")
|
||||||
|
def test_texts(text_a, text_b, text_c):
|
||||||
|
assert f"{text_a}:{text_b}" == text_c
|
|
@ -1,16 +1,21 @@
|
||||||
"""
|
"""
|
||||||
Configuration for the tests
|
Local configuration and fixture providing for the Plugin tests
|
||||||
... and local Plugins
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
|
from typing import Callable, Generator, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.config import Config
|
||||||
|
|
||||||
|
|
||||||
def get_csv(csv: str):
|
def get_csv(csv: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get CSV data
|
Helper Method: Read CSV file from the tests assets directory under ``tests/plugin/assets``.
|
||||||
|
|
||||||
|
:param csv: Name of the CSV file, without the .csv extension
|
||||||
|
:returns: CSV data as string
|
||||||
"""
|
"""
|
||||||
with open(join(dirname(__file__), "assets", f"{csv}.csv"), "rb") as csv_fh:
|
with open(join(dirname(__file__), "assets", f"{csv}.csv"), "rb") as csv_fh:
|
||||||
csv_data = csv_fh.read()
|
csv_data = csv_fh.read()
|
||||||
|
@ -18,35 +23,69 @@ def get_csv(csv: str):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def simple_test_csv():
|
def simple_test_csv() -> str:
|
||||||
"""
|
"""
|
||||||
Provide simple CSV data
|
Test fixture: Good simple CSV
|
||||||
|
|
||||||
|
:returns: CSV data as string
|
||||||
"""
|
"""
|
||||||
return get_csv("simple-test")
|
return get_csv("simple-test")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def bad_test_csv():
|
def bad_test_csv() -> str:
|
||||||
"""
|
"""
|
||||||
Provide bad CSV data
|
Test fixture: Bad CSV
|
||||||
|
|
||||||
|
:returns: Bad CSV data as string
|
||||||
"""
|
"""
|
||||||
return get_csv("bad-test")
|
return get_csv("bad-test")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def simple_fruit_test():
|
def text_test_csv() -> str:
|
||||||
"""
|
"""
|
||||||
Provide simple test case
|
Test Fixture: Text-only CSV
|
||||||
|
|
||||||
|
:returns: Text-only CSV data as string
|
||||||
|
"""
|
||||||
|
return get_csv("text-only")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def simple_fruit_test() -> Union[Callable[[str], str], Callable[[str, str], str]]:
|
||||||
|
"""
|
||||||
|
Test Fixture: Template of a simple test case
|
||||||
|
|
||||||
|
:returns: A method where a data file can be filled in and what will return a valid pytest test case that can be
|
||||||
|
saved to a .py file
|
||||||
"""
|
"""
|
||||||
with open(join(dirname(__file__), "assets", "fruit_test.tpl"), "rt", encoding="utf-8") as test_fh:
|
with open(join(dirname(__file__), "assets", "fruit_test.tpl"), "rt", encoding="utf-8") as test_fh:
|
||||||
test_data = test_fh.read()
|
test_data = test_fh.read()
|
||||||
|
return lambda file, test="fruit": test_data.replace("{{data_file}}", file).replace("{{test_name}}", test)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def simple_text_test() -> Callable[[str], str]:
|
||||||
|
"""
|
||||||
|
Test Fixture: Template of a simple text test case
|
||||||
|
|
||||||
|
:returns: A method where a data file can be filled in and what will return a valid pytest test case that can be
|
||||||
|
saved to a .py file
|
||||||
|
"""
|
||||||
|
with open(join(dirname(__file__), "assets", "text_test.tpl"), "rt", encoding="utf-8") as test_fh:
|
||||||
|
test_data = test_fh.read()
|
||||||
return lambda file: test_data.replace("{{data_file}}", file)
|
return lambda file: test_data.replace("{{data_file}}", file)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def install_plugin_locally(pytestconfig):
|
def install_plugin_locally(pytestconfig: Config) -> Generator[None, None, None]:
|
||||||
"""
|
"""
|
||||||
Install the local package
|
Auto-use Test Fixture to install our plugin in the test environment, so that it can be used with the ``pytester``
|
||||||
|
fixture. The package is removed after the test session automatically.
|
||||||
|
|
||||||
|
:param pytestconfig: Fixture from pytest that contains the test configuration
|
||||||
|
:returns: An empty generator (from the ``yield``), to let the tests run and cleanup afterwards
|
||||||
"""
|
"""
|
||||||
root = pytestconfig.rootpath
|
root = pytestconfig.rootpath
|
||||||
_ = subprocess.run(["pip", "install", "-e", "."], shell=True, cwd=root, check=True, capture_output=True)
|
_ = subprocess.run(["pip", "install", "-e", "."], shell=True, cwd=root, check=True, capture_output=True)
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
"""
|
||||||
|
Command line argument handling
|
||||||
|
==============================
|
||||||
|
|
||||||
|
**Module:** ``tests.plugin.test_cmd_line``
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest.pytester import Pytester
|
||||||
|
|
||||||
|
from _ptcsvp.cmdline import HELP_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["base_dir"],
|
||||||
|
[
|
||||||
|
(True,),
|
||||||
|
(False,),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_base_dir_param(
|
||||||
|
pytester: Pytester,
|
||||||
|
base_dir: bool,
|
||||||
|
simple_test_csv: str,
|
||||||
|
simple_fruit_test: Callable[[str], str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Test if the ``--csv-params-base-dir`` command line argument is valued. For laziness, it uses a poor parametrization.
|
||||||
|
|
||||||
|
:param pytester: Pytester fixture
|
||||||
|
:param base_dir: Shall the base dir parameter be set?
|
||||||
|
:param simple_test_csv: Fixture :meth:`~tests.plugin.conftest.simple_test_csv`
|
||||||
|
:param simple_fruit_test: Fixture :meth:`~tests.plugin.conftest.simple_fruit_test`
|
||||||
|
"""
|
||||||
|
|
||||||
|
csv_file = str(pytester.makefile(".csv", simple_test_csv).absolute())
|
||||||
|
|
||||||
|
parameters = ["-p", "no:bandit"]
|
||||||
|
if base_dir:
|
||||||
|
path = Path(csv_file)
|
||||||
|
parameters.extend(["--csv-params-base-dir", str(path.parent.absolute())])
|
||||||
|
csv_file = path.name
|
||||||
|
|
||||||
|
pytester.makepyfile(simple_fruit_test(csv_file))
|
||||||
|
|
||||||
|
result = pytester.runpytest(*parameters)
|
||||||
|
result.assert_outcomes(passed=3, failed=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_help(pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Test that the pytest help now contains our command line argument with our help text.
|
||||||
|
|
||||||
|
:param pytester: Pytester fixture
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = pytester.runpytest("--help")
|
||||||
|
index_csv_params = -1
|
||||||
|
index_minus_minus = -1
|
||||||
|
index_help = -1
|
||||||
|
|
||||||
|
for index, line in enumerate(result.stdout.lines):
|
||||||
|
if "csv-params:" in line and index_csv_params < 0:
|
||||||
|
index_csv_params = index
|
||||||
|
continue
|
||||||
|
if "--csv-params-base-dir" in line and index_minus_minus < 0:
|
||||||
|
index_minus_minus = index
|
||||||
|
if HELP_TEXT in line and index_help < 0:
|
||||||
|
index_help = index
|
||||||
|
|
||||||
|
assert index_csv_params >= 0
|
||||||
|
assert index_csv_params < index_minus_minus <= index_help
|
|
@ -1,11 +1,24 @@
|
||||||
"""
|
"""
|
||||||
Just try to call our plugin
|
Plugin Calls
|
||||||
|
============
|
||||||
|
|
||||||
|
**Module:** ``tests.plugin.test_plugin``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
def test_plugin_test_multiplication(pytester, simple_test_csv, simple_fruit_test):
|
from _pytest.pytester import Pytester
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_test_multiplication(
|
||||||
|
pytester: Pytester, simple_test_csv: str, simple_fruit_test: Callable[[str], str]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Simple Roundtrip Smoke Test
|
Test a simple round trip (positive test case)
|
||||||
|
|
||||||
|
:param pytester: Pytester fixture
|
||||||
|
:param simple_test_csv: Fixture :meth:`~tests.plugin.conftest.simple_test_csv`
|
||||||
|
:param simple_fruit_test: Fixture :meth:`~tests.plugin.conftest.simple_fruit_test`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
csv_file = str(pytester.makefile(".csv", simple_test_csv).absolute())
|
csv_file = str(pytester.makefile(".csv", simple_test_csv).absolute())
|
||||||
|
@ -16,9 +29,13 @@ def test_plugin_test_multiplication(pytester, simple_test_csv, simple_fruit_test
|
||||||
result.assert_outcomes(passed=3, failed=1)
|
result.assert_outcomes(passed=3, failed=1)
|
||||||
|
|
||||||
|
|
||||||
def test_plugin_test_error(pytester, bad_test_csv, simple_fruit_test):
|
def test_plugin_test_error(pytester: Pytester, bad_test_csv: str, simple_fruit_test: Callable[[str], str]) -> None:
|
||||||
"""
|
"""
|
||||||
Simple Error Behaviour Test
|
Test if a test error is correctly recognized
|
||||||
|
|
||||||
|
:param pytester: Pytester fixture
|
||||||
|
:param bad_test_csv: Fixture :meth:`~tests.plugin.conftest.bad_test_csv`
|
||||||
|
:param simple_fruit_test: Fixture :meth:`~tests.plugin.conftest.simple_fruit_test`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
csv_file = str(pytester.makefile(".csv", bad_test_csv).absolute())
|
csv_file = str(pytester.makefile(".csv", bad_test_csv).absolute())
|
||||||
|
@ -27,3 +44,55 @@ def test_plugin_test_error(pytester, bad_test_csv, simple_fruit_test):
|
||||||
|
|
||||||
result = pytester.runpytest("-p", "no:bandit")
|
result = pytester.runpytest("-p", "no:bandit")
|
||||||
result.assert_outcomes(errors=1)
|
result.assert_outcomes(errors=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_test_text_shorthand(
|
||||||
|
pytester: Pytester, text_test_csv: str, simple_text_test: Callable[[str], str]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Test the shorthand version of the plugin's decorator
|
||||||
|
|
||||||
|
:param pytester: Pytester fixture
|
||||||
|
:param text_test_csv: Fixture :meth:`~tests.plugin.conftest.text_test_csv`
|
||||||
|
:param simple_text_test: Fixture :meth:`~tests.plugin.conftest.simple_text_test`
|
||||||
|
"""
|
||||||
|
|
||||||
|
csv_file = str(pytester.makefile(".csv", text_test_csv).absolute())
|
||||||
|
|
||||||
|
pytester.makepyfile(simple_text_test(csv_file))
|
||||||
|
|
||||||
|
result = pytester.runpytest("-p", "no:bandit")
|
||||||
|
result.assert_outcomes(passed=2, failed=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_all_tests_at_once( # pylint: disable=too-many-arguments
|
||||||
|
pytester: Pytester,
|
||||||
|
text_test_csv: str,
|
||||||
|
bad_test_csv: str,
|
||||||
|
simple_test_csv: str,
|
||||||
|
simple_fruit_test: Callable[[str, str], str],
|
||||||
|
simple_text_test: Callable[[str], str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This is a meta test to check if multiple files would work also. Basically, it's a combination of all the other
|
||||||
|
plugin invocation tests of the module :mod:`tests.plugin.test_plugin`.
|
||||||
|
|
||||||
|
We can't run the error-prone test here, because it would stop all tests.
|
||||||
|
|
||||||
|
:param pytester: Pytester fixture
|
||||||
|
:param text_test_csv: Fixture :meth:`~tests.plugin.conftest.text_test_csv`
|
||||||
|
:param bad_test_csv: Fixture :meth:`~tests.plugin.conftest.bad_test_csv`
|
||||||
|
:param text_test_csv: Fixture :meth:`~tests.plugin.conftest.text_test_csv`
|
||||||
|
:param simple_fruit_test: Fixture :meth:`~tests.plugin.conftest.simple_fruit_test`
|
||||||
|
:param simple_text_test: Fixture :meth:`~tests.plugin.conftest.simple_text_test`
|
||||||
|
"""
|
||||||
|
|
||||||
|
csv_file_text = str(pytester.makefile(".1.csv", text_test_csv).absolute())
|
||||||
|
pytester.makepyfile(test_plugin_test_all_one=simple_text_test(csv_file_text))
|
||||||
|
csv_file_bad = str(pytester.makefile(".2.csv", bad_test_csv).absolute())
|
||||||
|
pytester.makepyfile(test_plugin_test_all_two=simple_fruit_test(csv_file_bad, "bad_one"))
|
||||||
|
csv_file_good = str(pytester.makefile(".3.csv", simple_test_csv).absolute())
|
||||||
|
pytester.makepyfile(test_plugin_test_all_three=simple_fruit_test(csv_file_good, "good_one"))
|
||||||
|
|
||||||
|
result = pytester.runpytest("-p", "no:bandit", "test_plugin_test_all_one.py", "test_plugin_test_all_three.py")
|
||||||
|
result.assert_outcomes(passed=5, failed=2, errors=0)
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""
|
||||||
|
Local configuration and fixture providing for POC tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class CheapCounter:
|
||||||
|
"""
|
||||||
|
A simple cheap counter that is required for counting executions
|
||||||
|
"""
|
||||||
|
|
||||||
|
counter: Dict[str, int] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_value(cls, counter: str) -> int:
|
||||||
|
"""
|
||||||
|
Get the value of the counter
|
||||||
|
|
||||||
|
:param counter: Name of the counter
|
||||||
|
:returns: Value of the counter
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_value = cls.counter.get(counter, None)
|
||||||
|
if current_value is None:
|
||||||
|
cls.counter[counter] = 0
|
||||||
|
return 0
|
||||||
|
return current_value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def increment(cls, counter: str) -> None:
|
||||||
|
"""
|
||||||
|
Increment the value of the counter
|
||||||
|
|
||||||
|
:param counter: Name of the counter to increment
|
||||||
|
"""
|
||||||
|
|
||||||
|
cls.counter[counter] = cls.get_value(counter) + 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def cheap_counter() -> Type[CheapCounter]:
|
||||||
|
"""
|
||||||
|
Deliver a simple counter as fixture
|
||||||
|
|
||||||
|
:returns: The Cheap Counter Class
|
||||||
|
"""
|
||||||
|
|
||||||
|
return CheapCounter
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""
|
||||||
|
Pytest feature: Parametrization
|
||||||
|
===============================
|
||||||
|
|
||||||
|
**Module:** ``tests.poc.test_parametrize_with_generator``
|
||||||
|
|
||||||
|
We are using a pytest feature heavily: Parametrization. These tests make sure this feature works still as expected.
|
||||||
|
|
||||||
|
Tests in this module run in a predefined order!
|
||||||
|
"""
|
||||||
|
|
||||||
|
from string import ascii_letters
|
||||||
|
from typing import Generator, List, Type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .conftest import CheapCounter
|
||||||
|
|
||||||
|
|
||||||
|
def data_generator() -> Generator[List[str], None, None]:
|
||||||
|
"""
|
||||||
|
Helper method: Create Test Data, but keep them as a generator
|
||||||
|
|
||||||
|
This helper is used by :meth:`~tests.poc.test_parametrize_with_generator.test_2_generator_parametrize`.
|
||||||
|
|
||||||
|
:returns: A bunch of test data as generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
for val_a in ascii_letters[0:5]:
|
||||||
|
for val_b in ascii_letters[5:10]:
|
||||||
|
yield [val_a, val_b, f"{val_a}-{val_b}"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.order(1)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["val_a", "val_b", "val_c"],
|
||||||
|
[
|
||||||
|
["a", "b", "a:b"],
|
||||||
|
["c", "d", "c:d"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_1_simple_parametrize(val_a: str, val_b: str, val_c: str, cheap_counter: Type[CheapCounter]) -> None:
|
||||||
|
"""
|
||||||
|
Test the simple parametrization from pytest.
|
||||||
|
|
||||||
|
:param val_a: Test value A
|
||||||
|
:param val_b: Test value B
|
||||||
|
:param val_c: Test value C
|
||||||
|
:param cheap_counter: Fixture :meth:`~tests.poc.conftest.cheap_counter`
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert f"{val_a}:{val_b}" == val_c
|
||||||
|
cheap_counter.increment("simple")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.order(2)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["val_a", "val_b", "val_c"],
|
||||||
|
data_generator(),
|
||||||
|
)
|
||||||
|
def test_2_generator_parametrize(val_a: str, val_b: str, val_c: str, cheap_counter: Type[CheapCounter]) -> None:
|
||||||
|
"""
|
||||||
|
Test the generator parametrization from pytest.
|
||||||
|
|
||||||
|
:param val_a: Test value A
|
||||||
|
:param val_b: Test value B
|
||||||
|
:param val_c: Test value C
|
||||||
|
:param cheap_counter: Fixture :meth:`~tests.poc.conftest.cheap_counter`
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert f"{val_a}-{val_b}" == val_c
|
||||||
|
cheap_counter.increment("generator")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.order(3)
|
||||||
|
def test_3_evaluation(cheap_counter: Type[CheapCounter]) -> None:
|
||||||
|
"""
|
||||||
|
Evaluate the values of the :meth:`~tests.poc.conftest.cheap_counter` fixture.
|
||||||
|
|
||||||
|
:param cheap_counter: Fixture :meth:`~tests.poc.conftest.cheap_counter`
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert cheap_counter.get_value("simple") == 2
|
||||||
|
assert cheap_counter.get_value("generator") == 25
|
|
@ -0,0 +1,97 @@
|
||||||
|
"""
|
||||||
|
Example Code for a blog post on juergen.rocks
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_blog_example``
|
||||||
|
|
||||||
|
This is a test example for
|
||||||
|
`a blog post on juergen.rocks <https://juergen.rocks/blog/articles/data-driven-tests-mit-pytest-csv-params.html>`_.
|
||||||
|
|
||||||
|
The example consists of serval helper methods and a lot of configuration for the
|
||||||
|
:meth:`~pytest_csv_params.decorator.csv_params` decorator.
|
||||||
|
|
||||||
|
The CSV file looks like this:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../tests/assets/blog-example.csv
|
||||||
|
:language: text
|
||||||
|
|
||||||
|
You find the CSV file in ``tests/assets/blog-example.csv``.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from os.path import dirname, join
|
||||||
|
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
|
||||||
|
def get_volume(size_data: str) -> int:
|
||||||
|
"""
|
||||||
|
Get the volume from size data, return it as mm³.
|
||||||
|
|
||||||
|
Helper method, will be used as a data caster.
|
||||||
|
|
||||||
|
:param size_data: String from the CSV file.
|
||||||
|
:returns: Volume in mm³
|
||||||
|
:raises: ValueError: When the test data cannot be converted
|
||||||
|
"""
|
||||||
|
matcher = re.compile(r"^\D*(?P<l>\d+)\D+(?P<d>\d+)\D+(?P<h>\d+)\D+$").match(size_data)
|
||||||
|
if matcher is None:
|
||||||
|
raise ValueError("Bad Test Data") from None
|
||||||
|
return int(matcher.group("l")) * int(matcher.group("d")) * int(matcher.group("h"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_container_volume(container_size: str) -> int:
|
||||||
|
"""
|
||||||
|
Get the container size (remove the unit, as mm³).
|
||||||
|
|
||||||
|
Helper method, will be used as a data caster.
|
||||||
|
|
||||||
|
:param container_size: String from the CSV file.
|
||||||
|
:returns: Volume of the container in mm³
|
||||||
|
:raises: ValueError: When the test data cannot be converted
|
||||||
|
"""
|
||||||
|
|
||||||
|
matcher = re.compile(r"^\D*(?P<size>\d+)\D+$").match(container_size)
|
||||||
|
if matcher is None:
|
||||||
|
raise ValueError("Bad Test Data") from None
|
||||||
|
return int(matcher.group("size")) * 1_000_000_000
|
||||||
|
|
||||||
|
|
||||||
|
@csv_params(
|
||||||
|
data_file=join(dirname(__file__), "assets", "blog-example.csv"),
|
||||||
|
id_col="Order-Ref #",
|
||||||
|
header_renames={
|
||||||
|
"Anz. Schrauben-Päck.": "anz_schrauben",
|
||||||
|
"Dim. Schrauben-Päck.": "vol_schrauben",
|
||||||
|
"Anz. Scheiben-Päck.": "anz_scheiben",
|
||||||
|
"Dim. Scheiben-Päck.": "vol_scheiben",
|
||||||
|
"Volumen Container": "vol_container",
|
||||||
|
},
|
||||||
|
data_casts={
|
||||||
|
"anz_schrauben": int,
|
||||||
|
"anz_scheiben": int,
|
||||||
|
"vol_schrauben": get_volume,
|
||||||
|
"vol_scheiben": get_volume,
|
||||||
|
"vol_container": get_container_volume,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_does_it_fit(
|
||||||
|
anz_schrauben: int, vol_schrauben: int, anz_scheiben: int, vol_scheiben: int, vol_container: int
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
A test example that tries to figure out if all the Schrauben and Scheiben fit in the container, and if the smallest
|
||||||
|
possible container is chosen.
|
||||||
|
|
||||||
|
:param anz_schrauben: Number of Schraubenpäckchen
|
||||||
|
:param vol_schrauben: Volume (mm³) of a single Schraubenpäckchen
|
||||||
|
:param anz_scheiben: Number of Scheibenpäckchen
|
||||||
|
:param vol_scheiben: Volume (mm³) of a single Scheibenpäckchen
|
||||||
|
:param vol_container: Volume (mm³) of the selected container
|
||||||
|
"""
|
||||||
|
|
||||||
|
available_container_sizes = map(lambda x: x * 1_000_000_000, [1, 5, 10])
|
||||||
|
|
||||||
|
size_required = (anz_schrauben * vol_schrauben) + (anz_scheiben * vol_scheiben)
|
||||||
|
|
||||||
|
# Smallest possible Container ordered?
|
||||||
|
smallest_possible_container = min(filter(lambda x: x >= size_required, available_container_sizes))
|
||||||
|
assert vol_container == smallest_possible_container
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""
|
||||||
|
Test for header cleaning
|
||||||
|
========================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_clean_headers``
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Optional, Type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from _ptcsvp.parametrize import clean_headers
|
||||||
|
from pytest_csv_params.exception import CsvHeaderNameInvalid
|
||||||
|
from pytest_csv_params.types import HeaderRenames
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["current_headers", "replacing", "expect_exception", "expect_message", "expect_result"],
|
||||||
|
[
|
||||||
|
(
|
||||||
|
["alpha 3", "_beta_5", "Ga Mm A"],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
["alpha_3", "_beta_5", "Ga_Mm_A"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["abcd" * 300, "_be:ta_ :23", "Ra -/2"],
|
||||||
|
None,
|
||||||
|
CsvHeaderNameInvalid,
|
||||||
|
f"'{'abcd' * 300}' is not a valid variable name",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["next_var", "_be:ta_ :23", "Ra -/2"],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
["next_var", "_be_ta___23", "Ra___2"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["next_var", "_be:ta_ :23", "Ra -/2", "Ra___2"],
|
||||||
|
None,
|
||||||
|
CsvHeaderNameInvalid,
|
||||||
|
"Header names are not unique",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["next_var", "_be:ta_ :23", "Ra -/2"],
|
||||||
|
{"Ra -/2": "ra", "not here": "not_here"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
["next_var", "_be_ta___23", "ra"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["next", "_be:ta_ :23", "Ra -/2"],
|
||||||
|
None,
|
||||||
|
CsvHeaderNameInvalid,
|
||||||
|
"'next' is not a valid variable name",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_header_cleaning(
|
||||||
|
current_headers: List[str],
|
||||||
|
replacing: HeaderRenames,
|
||||||
|
expect_exception: Optional[Type[ValueError]],
|
||||||
|
expect_message: Optional[str],
|
||||||
|
expect_result: List[str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This test case tests mainly the :meth:`_ptcsvp.parametrize.clean_headers` method.
|
||||||
|
|
||||||
|
There are many single test cases built with parametrization.
|
||||||
|
|
||||||
|
:param current_headers: List of headers before cleaning
|
||||||
|
:param replacing: A replacement dictionary
|
||||||
|
:param expect_exception: Exception to expect during method call
|
||||||
|
:param expect_message: Exception message to be expected
|
||||||
|
:param expect_result: Expected cleaned headers
|
||||||
|
"""
|
||||||
|
if expect_exception is not None:
|
||||||
|
with pytest.raises(expect_exception) as raised_error:
|
||||||
|
clean_headers(current_headers, replacing)
|
||||||
|
assert raised_error.value.args[0] == expect_message
|
||||||
|
else:
|
||||||
|
result = clean_headers(current_headers, replacing)
|
||||||
|
assert result == expect_result
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""
|
||||||
|
Example Code for a test case for the `README.md` documentation
|
||||||
|
==============================================================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_complex_example``
|
||||||
|
|
||||||
|
This module contains a quite simple, yet complex configured test to show what's possible with the plugin.
|
||||||
|
|
||||||
|
The example uses this CSV data, as found under ``tests/assets/example.csv``:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../tests/assets/example.csv
|
||||||
|
:language: text
|
||||||
|
|
||||||
|
The test idea here is much the same as the :mod:`tests.test_blog_example` test case.
|
||||||
|
|
||||||
|
Why is such a test case here? That's simple: To make sure, the code samples in the documentation still work as designed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from math import ceil
|
||||||
|
from os.path import dirname, join
|
||||||
|
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
|
||||||
|
@csv_params(
|
||||||
|
data_file="example.csv",
|
||||||
|
base_dir=join(dirname(__file__), "assets"),
|
||||||
|
id_col="Test ID",
|
||||||
|
header_renames={
|
||||||
|
"Bananas shipped": "bananas_shipped",
|
||||||
|
"Single Banana Weight": "banana_weight",
|
||||||
|
"Apples shipped": "apples_shipped",
|
||||||
|
"Single Apple Weight": "apple_weight",
|
||||||
|
"Container Size": "container_size",
|
||||||
|
},
|
||||||
|
data_casts={
|
||||||
|
"bananas_shipped": int,
|
||||||
|
"banana_weight": float,
|
||||||
|
"apples_shipped": int,
|
||||||
|
"apple_weight": float,
|
||||||
|
"container_size": int,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_container_size_is_big_enough(
|
||||||
|
bananas_shipped: int, banana_weight: float, apples_shipped: int, apple_weight: float, container_size: int
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This is just an example test case for the documentation.
|
||||||
|
|
||||||
|
:param bananas_shipped: How many mananas were shipped?
|
||||||
|
:param banana_weight: What's the weight of one banana?
|
||||||
|
:param apples_shipped: How many apples where shipped?
|
||||||
|
:param apple_weight: What's the weight of one apple?
|
||||||
|
:param container_size: How large was the container?
|
||||||
|
"""
|
||||||
|
|
||||||
|
gross_weight = (banana_weight * bananas_shipped) + (apple_weight * apples_shipped)
|
||||||
|
assert ceil(gross_weight) <= container_size
|
|
@ -0,0 +1,97 @@
|
||||||
|
"""
|
||||||
|
Example Code for a test case for the documentation site
|
||||||
|
=======================================================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_docs_example``
|
||||||
|
|
||||||
|
This is the test code for the documentation site's :doc:`User guide </pages/guide>`. It contains everything that's
|
||||||
|
needed to follow the example -- and makes sure the code example is working.
|
||||||
|
"""
|
||||||
|
from functools import reduce
|
||||||
|
from os.path import dirname, join
|
||||||
|
from typing import List, Tuple, Union
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pytest_csv_params.decorator import csv_params
|
||||||
|
|
||||||
|
|
||||||
|
def get_smallest_possible_container(
|
||||||
|
number_of_items: int,
|
||||||
|
dimensions_of_item: Tuple[int, int, int],
|
||||||
|
available_container_sizes: Union[List[int], Tuple[int, ...]] = (1000, 2500, 7500),
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
This is the method to be tested. It searches for the smallest possible container after calculating the volume of the
|
||||||
|
things to be loaded into the container. A container can only contain items of one product, so it is enough to know
|
||||||
|
about the size of a single product, and how many of them need to be stored in a container.
|
||||||
|
|
||||||
|
The method raises a :class:`ValueError` when the items do not fit any container.
|
||||||
|
|
||||||
|
:param number_of_items: Number of items to be packed
|
||||||
|
:param dimensions_of_item: Edge lengths of a single item
|
||||||
|
:param available_container_sizes: What container sizes are available? This parameter has a default value
|
||||||
|
``(1000, 2500, 7500)``.
|
||||||
|
:raises ValueError: When no matching container can be found
|
||||||
|
"""
|
||||||
|
volume = reduce(lambda x, y: x * y, [*dimensions_of_item, number_of_items])
|
||||||
|
possible_containers = list(filter(lambda s: s >= volume, available_container_sizes))
|
||||||
|
if len(possible_containers) == 0:
|
||||||
|
raise ValueError("No container available") from None
|
||||||
|
return min(possible_containers)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dimensions(dimensions_str: str) -> Tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Read the dimensions from a string. A helper method to build the dimensions tuple.
|
||||||
|
|
||||||
|
:param dimensions_str: The dimensions from the CSV file
|
||||||
|
:raises ValueError: When the dimensions cannot be converted
|
||||||
|
:returns: The dimensions as int tuple
|
||||||
|
"""
|
||||||
|
dimensions_tuple = tuple(map(lambda x: int(x.strip()), dimensions_str.split("x")))
|
||||||
|
if len(dimensions_tuple) != 3:
|
||||||
|
raise ValueError("Dimensions invalid") from None
|
||||||
|
return dimensions_tuple # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@csv_params(
|
||||||
|
data_file=join(dirname(__file__), "assets", "doc-example.csv"),
|
||||||
|
id_col="Test-ID",
|
||||||
|
header_renames={
|
||||||
|
"Number of items": "number_of_items",
|
||||||
|
"Dimensions of item": "dimensions_of_item",
|
||||||
|
"Expected Container Size": "expected_container_size",
|
||||||
|
"Expect Exception?": "expect_exception",
|
||||||
|
"Expected Message": "expected_message",
|
||||||
|
},
|
||||||
|
data_casts={
|
||||||
|
"number_of_items": int,
|
||||||
|
"dimensions_of_item": get_dimensions,
|
||||||
|
"expected_container_size": int,
|
||||||
|
"expect_exception": lambda x: x == "Y",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_smallest_possible_container(
|
||||||
|
number_of_items: int,
|
||||||
|
dimensions_of_item: Tuple[int, int, int],
|
||||||
|
expected_container_size: int,
|
||||||
|
expect_exception: bool,
|
||||||
|
expected_message: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This is the test method for the documentation.
|
||||||
|
|
||||||
|
:param number_of_items: The number of items
|
||||||
|
:param dimensions_of_item: The dimensions of a single item
|
||||||
|
:param expected_container_size: Expected container size
|
||||||
|
:param expect_exception: Expect a :class:`ValueError`
|
||||||
|
:param expected_message: Message of the exception
|
||||||
|
"""
|
||||||
|
if expect_exception:
|
||||||
|
with pytest.raises(ValueError) as expected_exception:
|
||||||
|
get_smallest_possible_container(number_of_items, dimensions_of_item)
|
||||||
|
assert expected_exception.value.args[0] == expected_message
|
||||||
|
else:
|
||||||
|
container_size = get_smallest_possible_container(number_of_items, dimensions_of_item)
|
||||||
|
assert container_size == expected_container_size
|
|
@ -1,7 +1,12 @@
|
||||||
"""
|
"""
|
||||||
Test the Parametrization Feature
|
Test for the internal parametrization feature
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_parametrize``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
|
from typing import List, Optional, Tuple, Type
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -76,11 +81,26 @@ from pytest_csv_params.exception import CsvParamsDataFileInvalid, CsvParamsDataF
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parametrization(
|
def test_parametrization( # pylint: disable=too-many-arguments
|
||||||
csv_file, id_col, result, ids, expect_exception, expect_message
|
csv_file: str,
|
||||||
): # pylint: disable=too-many-arguments
|
id_col: Optional[str],
|
||||||
|
result: Optional[Tuple[List[str], List[List[str]]]],
|
||||||
|
ids: Optional[List[str]],
|
||||||
|
expect_exception: Optional[Type[Exception]],
|
||||||
|
expect_message: Optional[str],
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Test the parametrization method
|
This test case tests mainly the internal :meth:`_ptcsvp.parametrize.add_parametrization` method, which is the
|
||||||
|
backbone of the public :meth:`~pytest_csv_params.decorator.csv_params` decorator.
|
||||||
|
|
||||||
|
The test is heavily parametrized. See source code for detail.
|
||||||
|
|
||||||
|
:param csv_file: CSV file for the test
|
||||||
|
:param id_col: The ID column name of the CSV file
|
||||||
|
:param result: Expected result, as it would be handed over to the :meth:`pytest.mark.parametrize` mark decorator
|
||||||
|
:param ids: Expected test case IDs
|
||||||
|
:param expect_exception: Expected exception during call
|
||||||
|
:param expect_message: Expected exception message during call
|
||||||
"""
|
"""
|
||||||
data_file = join(dirname(__file__), "assets", f"{csv_file}.csv")
|
data_file = join(dirname(__file__), "assets", f"{csv_file}.csv")
|
||||||
if expect_exception:
|
if expect_exception:
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
"""
|
"""
|
||||||
Test the reading of the CSV
|
Test the reading of CSV files
|
||||||
|
=============================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_read_csv``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -33,9 +38,22 @@ from pytest_csv_params.exception import CsvParamsDataFileInvalid, CsvParamsDataF
|
||||||
(None, None, -1, CsvParamsDataFileInvalid, None),
|
(None, None, -1, CsvParamsDataFileInvalid, None),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_csv_reader(csv_file, base_dir, expect_lines, expect_exception, expect_message):
|
def test_csv_reader(
|
||||||
|
csv_file: str,
|
||||||
|
base_dir: Optional[str],
|
||||||
|
expect_lines: Optional[int],
|
||||||
|
expect_exception: Optional[Type[Exception]],
|
||||||
|
expect_message: Optional[str],
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Test behaviour of the CSV loading
|
This test case tests several CSV reading scenarios (by parametrization). CSV test files are in the ``tests/assets``
|
||||||
|
folder. The tests target the :meth:`_ptcsvp.parametrize.read_csv` method.
|
||||||
|
|
||||||
|
:param csv_file: The file to test with
|
||||||
|
:param base_dir: The base dir to load the :attr:`csv_file` from
|
||||||
|
:param expect_lines: Expected read lines from the CSV file
|
||||||
|
:param expect_exception: Expected exception when running the method
|
||||||
|
:param expect_message: Expected exception message when running the method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if expect_exception is not None:
|
if expect_exception is not None:
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""
|
||||||
|
Test the header name handling
|
||||||
|
=============================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_varname``
|
||||||
|
|
||||||
|
The tests in this module aim at testing the validation and cleaning of header/column names of CSV files. Those names
|
||||||
|
serve as arguments to test methods, and must therefore be valid and not shadow builtin names. Reserved names are checked
|
||||||
|
also.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from _ptcsvp.varname import is_valid_name, make_name_valid
|
||||||
|
from pytest_csv_params.exception import CsvHeaderNameInvalid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["var_name", "is_valid"],
|
||||||
|
[
|
||||||
|
("var_name", True),
|
||||||
|
("varname", True),
|
||||||
|
("_varname", True),
|
||||||
|
("_varName", True),
|
||||||
|
("VarName", True),
|
||||||
|
("VarName_", True),
|
||||||
|
("Var1Name_", True),
|
||||||
|
("Var1Name_0", True),
|
||||||
|
("0_var_name", False),
|
||||||
|
("var name", False),
|
||||||
|
("Var Name", False),
|
||||||
|
("Var-Name", False),
|
||||||
|
("Var.Name", False),
|
||||||
|
("Var:Name", False),
|
||||||
|
(":VarName", False),
|
||||||
|
(".VarName", False),
|
||||||
|
(";VarName", False),
|
||||||
|
("VarName+", False),
|
||||||
|
("VarName#", False),
|
||||||
|
("VarNäme", False),
|
||||||
|
("VarNÖme", False),
|
||||||
|
("Varßn_ame", False),
|
||||||
|
("def", False),
|
||||||
|
("next", False),
|
||||||
|
("if", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_valid_name(var_name: str, is_valid: bool) -> None:
|
||||||
|
"""
|
||||||
|
This test case checks that the method :meth:`_ptcsvp.varname.is_valid_name` gives the right results. The test method
|
||||||
|
is parametrized.
|
||||||
|
|
||||||
|
:param var_name: The name to check
|
||||||
|
:param is_valid: Expectation if this is a valid name
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert is_valid_name(var_name) == is_valid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["var_name", "valid_var_name", "raises_error"],
|
||||||
|
[
|
||||||
|
("var_name", "var_name", False),
|
||||||
|
("4var_name", "_var_name", False),
|
||||||
|
("44_var_name", "_4_var_name", False),
|
||||||
|
("varName", "varName", False),
|
||||||
|
("var Name", "var_Name", False),
|
||||||
|
(" varName", "_varName", False),
|
||||||
|
(":varName", "_varName", False),
|
||||||
|
("var-name", "var_name", False),
|
||||||
|
("a" * 1025, None, True),
|
||||||
|
("abcd" * 300, None, True),
|
||||||
|
("continue", None, True),
|
||||||
|
("float", None, True),
|
||||||
|
("str", None, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_make_name_valid(var_name: str, valid_var_name: Optional[str], raises_error: bool) -> None:
|
||||||
|
"""
|
||||||
|
This test case checks the method :meth:`_ptcsvp.varname.make_name_valid` builds valid names or throws matching
|
||||||
|
exceptions if not possible. Therefore, it is parametrized.
|
||||||
|
|
||||||
|
:param var_name: The variable name to try to make valid
|
||||||
|
:param valid_var_name: The name as expected after made valid
|
||||||
|
:param raises_error: Expect an error?
|
||||||
|
"""
|
||||||
|
|
||||||
|
if raises_error:
|
||||||
|
with pytest.raises(CsvHeaderNameInvalid) as raised_error:
|
||||||
|
make_name_valid(var_name)
|
||||||
|
assert raised_error.value.args[0] == f"'{var_name}' is not a valid variable name"
|
||||||
|
else:
|
||||||
|
should_be_valid = make_name_valid(var_name)
|
||||||
|
assert should_be_valid == valid_var_name
|
||||||
|
assert is_valid_name(should_be_valid)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Only relevant for python 3.10 and above")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["name"],
|
||||||
|
[
|
||||||
|
("match",),
|
||||||
|
("_",),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_310_names(name: str) -> None:
|
||||||
|
"""
|
||||||
|
There are a few names that are not valid when using python 3.10 and above. This parametrized test checks if they are
|
||||||
|
marked as invalid by the method :meth:`_ptcsvp.varname.is_valid_name`.
|
||||||
|
|
||||||
|
This test will be skipped on python versions below 3.10.
|
||||||
|
|
||||||
|
:param name: An invalid name since python 3.10.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert not is_valid_name(name)
|
|
@ -1,19 +1,31 @@
|
||||||
"""
|
"""
|
||||||
We are checking the python version for the plugin.
|
Test the checks for required versions
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
**Module:** ``tests.test_version_checks``
|
||||||
|
|
||||||
|
Checking the versions this plugin depends on is crucial for the correct function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from typing import List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from attr.exceptions import PythonTooOldError
|
from attr.exceptions import PythonTooOldError
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from _ptcsvp.version import check_pytest_version, check_python_version
|
from _ptcsvp.version import check_pytest_version, check_python_version
|
||||||
|
|
||||||
|
|
||||||
def build_version(p_version):
|
def build_version(p_version: str) -> Tuple[Union[int, str], ...]:
|
||||||
"""
|
"""
|
||||||
Build a Version
|
Test helper method: Build a version Tuple of a given version string. It is used by the
|
||||||
|
:meth:`~tests.test_version_check.test_python_version` test case.
|
||||||
|
|
||||||
|
:param p_version: Version string
|
||||||
|
:returns: The version as tuple
|
||||||
"""
|
"""
|
||||||
elements = []
|
elements: List[Union[int, str]] = []
|
||||||
for v_part in p_version.split("."):
|
for v_part in p_version.split("."):
|
||||||
try:
|
try:
|
||||||
elements.append(int(v_part))
|
elements.append(int(v_part))
|
||||||
|
@ -58,9 +70,18 @@ def build_version(p_version):
|
||||||
("3", (PythonTooOldError, "At least Python 3.8 required")),
|
("3", (PythonTooOldError, "At least Python 3.8 required")),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_python_version(mocker, p_version, expect_error):
|
def test_python_version(
|
||||||
|
mocker: MockerFixture, p_version: str, expect_error: Optional[Tuple[Type[Exception], str]]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Test python versions
|
Test if the python version is correctly recognized and if old versions raise an exception. This tests mainly the
|
||||||
|
:meth:`_ptcsvp.version.check_python_version` method and is parametrized with a lot of different version combos.
|
||||||
|
|
||||||
|
This test uses mocking, and the :meth:`~tests.test_version_check.build_version` helper method.
|
||||||
|
|
||||||
|
:param mocker: Mocking fixture
|
||||||
|
:param p_version: Version string
|
||||||
|
:param expect_error: Expected error for the given version
|
||||||
"""
|
"""
|
||||||
mocker.patch.object(sys, "version_info", build_version(p_version))
|
mocker.patch.object(sys, "version_info", build_version(p_version))
|
||||||
if expect_error is not None:
|
if expect_error is not None:
|
||||||
|
@ -74,39 +95,54 @@ def test_python_version(mocker, p_version, expect_error):
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
["p_version", "expect_error"],
|
["p_version", "expect_error"],
|
||||||
[
|
[
|
||||||
("7.1", None),
|
("7.4", None),
|
||||||
("7.1.1", None),
|
("7.4.1", None),
|
||||||
("7.1.1.2", None),
|
("7.4.1.2", None),
|
||||||
("7.1.1.2.3", None),
|
("7.4.1.2.3", None),
|
||||||
("7.1.1.455555", None),
|
("7.4.1.455555", None),
|
||||||
("7.1.1.2-145", None),
|
("7.4.1.2-145", None),
|
||||||
("7.1.1.090909090990", None),
|
("7.4.1.090909090990", None),
|
||||||
("7.2", None),
|
("7.5", None),
|
||||||
("7.2.5", None),
|
("7.5.5", None),
|
||||||
("7.2.5.6", None),
|
("7.5.5.6", None),
|
||||||
("7.2.5.6.3333333", None),
|
("7.5.5.6.3333333", None),
|
||||||
("7.2.5.6.333333333.4", None),
|
("7.5.5.6.333333333.4", None),
|
||||||
("8.0", None),
|
("8.0", None),
|
||||||
("8.0.1", None),
|
("8.0.1", None),
|
||||||
("8.0.beta", None),
|
("8.0.beta", None),
|
||||||
("8.0.beta.3", None),
|
("8.0.beta.3", None),
|
||||||
("8.0.2.3", None),
|
("8.0.2.3", None),
|
||||||
("7.0", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.0", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("7.0.1", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.0.1", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("7.0.111111", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.0.111111", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("7.0.111111.extra", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.1", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("7.0.111111.extra.git-22", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.1.1", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("7", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.1.111111", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("6", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.2", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("6.1", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.2.1", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("6.1.2", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.2.111111", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("6.1.2.final", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.3", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
("6.1.2.final.3", (RuntimeError, "At least Pytest 7.1 required")),
|
("7.3.1", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
|
("7.3.111111", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
|
("7", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
|
("6", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
|
("6.1", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
|
("6.1.2", (RuntimeError, "At least Pytest 7.4 required")),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_pytest_version(mocker, p_version, expect_error):
|
def test_pytest_version(
|
||||||
|
mocker: MockerFixture, p_version: str, expect_error: Optional[Tuple[Type[Exception], str]]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Test pytest versions
|
Test if the pytest version is correctly recognized and if a too old version raises an exception. This test focuses
|
||||||
|
on the :meth:`_ptcsvp.version.check_pytest_version` method and is parametrized with a lot of different version
|
||||||
|
strings.
|
||||||
|
|
||||||
|
This test uses mocking.
|
||||||
|
|
||||||
|
:param mocker: Mocking fixture
|
||||||
|
:param p_version: Version string
|
||||||
|
:param expect_error: Expected error and error message
|
||||||
"""
|
"""
|
||||||
mocker.patch.object(pytest, "__version__", p_version)
|
mocker.patch.object(pytest, "__version__", p_version)
|
||||||
assert pytest.__version__ == p_version
|
assert pytest.__version__ == p_version
|
||||||
|
|
Loading…
Reference in New Issue