Compare commits

...

4 Commits

Author SHA1 Message Date
Jürgen Edelbluth d87edb29ca
Docs typo 2022-08-15 17:42:36 +00:00
Jürgen Edelbluth 8c888d450a
Preparing next release
- More typing
- All failing mypy tests fixed
2022-08-15 17:32:09 +00:00
Jürgen Edelbluth 9b744d8b4c
Preparing next release
- Fixing all the mypy warnings - Part 1
- Changed README
- Breaking Change about parameter order
- Tests for it
2022-08-15 18:19:39 +02:00
Jürgen Edelbluth dfccf9ef8a
Extra quality checks
Typing is now checked with mypy.
2022-08-15 16:11:25 +02:00
17 changed files with 196 additions and 40 deletions

View File

@ -2,7 +2,10 @@
A pytest plugin to parametrize tests by CSV files. A pytest plugin to parametrize tests by CSV files.
[:toc:] [![PyPI - Downloads](https://img.shields.io/pypi/dw/pytest-csv-params?label=PyPI%20downloads&style=for-the-badge)](https://pypi.org/project/pytest-csv-params/)
[![PyPI - Version](https://img.shields.io/pypi/v/pytest-csv-params?label=PyPI%20version&style=for-the-badge)](https://pypi.org/project/pytest-csv-params/)
[![PyPI - Status](https://img.shields.io/pypi/status/pytest-csv-params?label=PyPI%20status&style=for-the-badge)](https://pypi.org/project/pytest-csv-params/)
[![PyPI - Format](https://img.shields.io/pypi/format/pytest-csv-params?label=PyPI%20format&style=for-the-badge)](https://pypi.org/project/pytest-csv-params/)
## Requirements ## Requirements
@ -118,6 +121,22 @@ def test_addition(part_a, part_b, expected_result):
assert part_a + part_b == expected_result assert part_a + part_b == expected_result
``` ```
Shorthand example (no ID col, only string values):
```python
from pytest_csv_params.decorator import csv_params
@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
```
## Breaking Changes
### Version 0.2.0
- The parameter order for `pytest_csv_params.decorator.csv_params` changed to allow the shorthand usage with only a `data_file` as positional argument. If you used keyword arguments only (like the docs recommend), you will not run into trouble.
## Contributing ## Contributing
### Build and test ### Build and test
@ -144,6 +163,20 @@ It would be great if you could include example code that clarifies your issue.
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.
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.
### Quality Measures
Backed with pytest plugins:
- Pylint _(static code analysis and best practises)_
- black _(code formatting standards)_
- isort _(keep imports sorted)_
- Bandit _(basic static security tests)_
- mypy _(typechecking)_
Please to a complete pytest run (`poetry run pytest`), and consider running it on all supported platforms with (`poetry run tox`).
## License ## License
Code is under MIT license. See `LICENSE.txt` for details. Code is under MIT license. See `LICENSE.txt` for details.

View File

@ -1,12 +1,12 @@
""" """
Command Line Options Command Line Options
""" """
from _pytest.config.argparsing import Parser
HELP_TEXT = "set base dir for getting CSV data files from" HELP_TEXT = "set base dir for getting CSV data files from"
def pytest_addoption(parser, plugin_name="csv-params"): def pytest_addoption(parser: Parser, plugin_name: str = "csv-params") -> None:
""" """
Add Command Line Arguments for pytest execution Add Command Line Arguments for pytest execution
""" """

View File

@ -1,18 +1,19 @@
""" """
Pytest Plugin Configuration Pytest Plugin Configuration
""" """
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
""" """
config.pluginmanager.register(Plugin(config), f"{plugin_name}_plugin") config.pluginmanager.register(Plugin(config), 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
""" """

View File

@ -3,9 +3,10 @@ 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.plugin import BASE_DIR_KEY, Plugin
from pytest_csv_params.dialect import CsvParamsDefaultDialect from pytest_csv_params.dialect import CsvParamsDefaultDialect
@ -14,7 +15,7 @@ from pytest_csv_params.exception import (
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, IdColName
class TestCaseParameters(TypedDict): class TestCaseParameters(TypedDict):
@ -23,10 +24,10 @@ class TestCaseParameters(TypedDict):
""" """
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
""" """
@ -53,12 +54,12 @@ def read_csv(base_dir: BaseDir, data_file: DataFile, dialect: CsvDialect):
def add_parametrization( def add_parametrization(
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,
): ) -> MarkDecorator:
""" """
Get data from the files and add things to the tests Get data from the files and add things to the tests
""" """

View File

@ -2,6 +2,7 @@
The main Plugin implementation The main Plugin implementation
""" """
from _pytest.config import Config
BASE_DIR_KEY = "__pytest_csv_plugins__config__base_dir" BASE_DIR_KEY = "__pytest_csv_plugins__config__base_dir"
@ -11,7 +12,7 @@ class Plugin: # pylint: disable=too-few-public-methods
Plugin Class Plugin Class
""" """
def __init__(self, config): def __init__(self, config: Config) -> None:
""" """
Hold the pytest config Hold the pytest config
""" """

View File

@ -2,12 +2,13 @@
Check Version Information Check Version Information
""" """
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
""" """
@ -16,7 +17,7 @@ def check_python_version(min_version=(3, 8)):
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, 1)) -> None:
""" """
Check if the current version is at least 7.1 Check if the current version is at least 7.1
""" """

42
poetry.lock generated
View File

@ -209,6 +209,24 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "mypy"
version = "0.971"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10"
[package.extras]
reports = ["lxml"]
python2 = ["typed-ast (>=1.4.0,<2)"]
dmypy = ["psutil (>=4.0)"]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
@ -426,6 +444,26 @@ pytest = ">=5.0"
[package.extras] [package.extras]
dev = ["pytest-asyncio", "tox", "pre-commit"] dev = ["pytest-asyncio", "tox", "pre-commit"]
[[package]]
name = "pytest-mypy"
version = "0.9.1"
description = "Mypy static type checker plugin for Pytest"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
attrs = ">=19.0"
filelock = ">=3.0"
mypy = [
{version = ">=0.780", markers = "python_version >= \"3.9\""},
{version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""},
]
pytest = [
{version = ">=4.6", markers = "python_version >= \"3.6\" and python_version < \"3.10\""},
{version = ">=6.2", markers = "python_version >= \"3.10\""},
]
[[package]] [[package]]
name = "pytest-pylint" name = "pytest-pylint"
version = "0.18.0" version = "0.18.0"
@ -588,7 +626,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "109ba73e69f54a4f4610c2cc9153499d8d8fb6db5dffc48ab767779e4525be78" content-hash = "cd95b8ffb0cffc324682ca1e38a133f6a8643a4f8145d0ac26a0411d1145b376"
[metadata.files] [metadata.files]
astroid = [] astroid = []
@ -621,6 +659,7 @@ iniconfig = [
isort = [] isort = []
lazy-object-proxy = [] lazy-object-proxy = []
mccabe = [] mccabe = []
mypy = []
mypy-extensions = [ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
@ -668,6 +707,7 @@ pytest-cov = [
] ]
pytest-isort = [] pytest-isort = []
pytest-mock = [] pytest-mock = []
pytest-mypy = []
pytest-pylint = [] pytest-pylint = []
pyyaml = [] pyyaml = []
rich = [] rich = []

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pytest-csv-params" name = "pytest-csv-params"
version = "0.1.0" version = "0.2.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"
@ -41,7 +41,7 @@ packages = [
"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 --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",
@ -81,6 +81,7 @@ omit = [
"*/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_base_dir_param.py",
"*/test_plugin_test_text_shorthand.py",
] ]
relative_files = true relative_files = true
@ -94,6 +95,10 @@ exclude_lines = [
"if __name__ == .__main__.:", "if __name__ == .__main__.:",
] ]
[tool.mypy]
python_version = "3.8"
strict = true
[tool.tox] [tool.tox]
legacy_tox_ini = """ legacy_tox_ini = """
[tox] [tox]
@ -117,7 +122,7 @@ 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]
@ -134,6 +139,7 @@ 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"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@ -2,12 +2,11 @@
Types to ease the usage of the API Types to ease the usage of the API
""" """
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]
DataCast = Callable[[str], T]
DataCastDict = Dict[str, DataCast] DataCastDict = Dict[str, DataCast]
DataCasts = Optional[DataCastDict]
BaseDir = Optional[str] BaseDir = Optional[str]
IdColName = Optional[str] IdColName = Optional[str]

View File

@ -0,0 +1,4 @@
"text_a","text_b","text_c"
"aaa","bbb","aaa:bbb"
"abc","xyz","xyz:abc"
"xyz","abc","xyz:abc"
1 text_a text_b text_c
2 aaa bbb aaa:bbb
3 abc xyz xyz:abc
4 xyz abc xyz:abc

View File

@ -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

View File

@ -4,11 +4,13 @@ Configuration for the tests
""" """
import subprocess import subprocess
from os.path import dirname, join from os.path import dirname, join
from typing import Callable, Generator
import pytest import pytest
from _pytest.config import Config
def get_csv(csv: str): def get_csv(csv: str) -> str:
""" """
Get CSV data Get CSV data
""" """
@ -18,7 +20,7 @@ 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 Provide simple CSV data
""" """
@ -26,7 +28,7 @@ def simple_test_csv():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def bad_test_csv(): def bad_test_csv() -> str:
""" """
Provide bad CSV data Provide bad CSV data
""" """
@ -34,7 +36,15 @@ def bad_test_csv():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def simple_fruit_test(): def text_test_csv() -> str:
"""
Provide text-only CSV data
"""
return get_csv("text-only")
@pytest.fixture(scope="session")
def simple_fruit_test() -> Callable[[str], str]:
""" """
Provide simple test case Provide simple test case
""" """
@ -43,8 +53,18 @@ def simple_fruit_test():
return lambda file: test_data.replace("{{data_file}}", file) return lambda file: test_data.replace("{{data_file}}", file)
@pytest.fixture(scope="session")
def simple_text_test() -> Callable[[str], str]:
"""
Provide simple text test case
"""
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)
@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 Install the local package
""" """

View File

@ -2,8 +2,10 @@
Test the usage of the Command Line Test the usage of the Command Line
""" """
from pathlib import Path from pathlib import Path
from typing import Callable
import pytest import pytest
from _pytest.pytester import Pytester
from _ptcsvp.cmdline import HELP_TEXT from _ptcsvp.cmdline import HELP_TEXT
@ -15,7 +17,9 @@ from _ptcsvp.cmdline import HELP_TEXT
(False,), (False,),
], ],
) )
def test_base_dir_param(pytester, base_dir, simple_test_csv, simple_fruit_test): def test_base_dir_param(
pytester: Pytester, base_dir: bool, simple_test_csv: str, simple_fruit_test: Callable[[str], str]
) -> None:
""" """
Test that the cmd arg is valued Test that the cmd arg is valued
""" """
@ -34,7 +38,7 @@ def test_base_dir_param(pytester, base_dir, simple_test_csv, simple_fruit_test):
result.assert_outcomes(passed=3, failed=1) result.assert_outcomes(passed=3, failed=1)
def test_help(pytester): def test_help(pytester: Pytester) -> None:
""" """
Test if the plugin is in the help Test if the plugin is in the help
""" """

View File

@ -1,9 +1,14 @@
""" """
Just try to call our plugin Just try to call our plugin
""" """
from typing import Callable
from _pytest.pytester import Pytester
def test_plugin_test_multiplication(pytester, simple_test_csv, simple_fruit_test): def test_plugin_test_multiplication(
pytester: Pytester, simple_test_csv: str, simple_fruit_test: Callable[[str], str]
) -> None:
""" """
Simple Roundtrip Smoke Test Simple Roundtrip Smoke Test
""" """
@ -16,7 +21,7 @@ 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 Simple Error Behaviour Test
""" """
@ -27,3 +32,18 @@ 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:
"""
Simple Roundtrip Smoke 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)

View File

@ -2,6 +2,7 @@
Test the Parametrization Feature Test the Parametrization Feature
""" """
from os.path import dirname, join from os.path import dirname, join
from typing import List, Optional, Tuple, Type
import pytest import pytest
@ -76,9 +77,14 @@ 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 Test the parametrization method
""" """

View File

@ -2,6 +2,7 @@
Test the reading of the CSV Test the reading of the CSV
""" """
from os.path import dirname, join from os.path import dirname, join
from typing import Optional, Type
import pytest import pytest
@ -33,7 +34,13 @@ 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 Test behaviour of the CSV loading
""" """

View File

@ -2,18 +2,20 @@
We are checking the python version for the plugin. We are checking the python version for the plugin.
""" """
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 Build a Version
""" """
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,7 +60,9 @@ 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 python versions
""" """
@ -104,7 +108,9 @@ def test_python_version(mocker, p_version, expect_error):
("6.1.2.final.3", (RuntimeError, "At least Pytest 7.1 required")), ("6.1.2.final.3", (RuntimeError, "At least Pytest 7.1 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 pytest versions
""" """