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.
[: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
@ -118,6 +121,22 @@ def test_addition(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
### 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.
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
Code is under MIT license. See `LICENSE.txt` for details.

View File

@ -1,12 +1,12 @@
"""
Command Line Options
"""
from _pytest.config.argparsing import Parser
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
"""

View File

@ -1,18 +1,19 @@
"""
Pytest Plugin Configuration
"""
from _pytest.config import Config
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
"""
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
"""

View File

@ -3,9 +3,10 @@ Parametrize a test function by CSV file
"""
import csv
from pathlib import Path
from typing import List, Optional, TypedDict
from typing import Any, List, Optional, TypedDict
import pytest
from _pytest.mark import MarkDecorator
from _ptcsvp.plugin import BASE_DIR_KEY, Plugin
from pytest_csv_params.dialect import CsvParamsDefaultDialect
@ -14,7 +15,7 @@ from pytest_csv_params.exception import (
CsvParamsDataFileInvalid,
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):
@ -23,10 +24,10 @@ class TestCaseParameters(TypedDict):
"""
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
"""
@ -53,12 +54,12 @@ def read_csv(base_dir: BaseDir, data_file: DataFile, dialect: CsvDialect):
def add_parametrization(
data_file: DataFile,
base_dir: BaseDir = None,
data_file: DataFile = None,
id_col: IdColName = None,
data_casts: DataCastDict = None,
data_casts: DataCasts = None,
dialect: CsvDialect = CsvParamsDefaultDialect,
):
) -> MarkDecorator:
"""
Get data from the files and add things to the tests
"""

View File

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

View File

@ -2,12 +2,13 @@
Check Version Information
"""
import sys
from typing import Tuple
from attr.exceptions import PythonTooOldError
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
"""
@ -16,7 +17,7 @@ def check_python_version(min_version=(3, 8)):
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
"""

42
poetry.lock generated
View File

@ -209,6 +209,24 @@ category = "dev"
optional = false
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]]
name = "mypy-extensions"
version = "0.4.3"
@ -426,6 +444,26 @@ pytest = ">=5.0"
[package.extras]
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]]
name = "pytest-pylint"
version = "0.18.0"
@ -588,7 +626,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "109ba73e69f54a4f4610c2cc9153499d8d8fb6db5dffc48ab767779e4525be78"
content-hash = "cd95b8ffb0cffc324682ca1e38a133f6a8643a4f8145d0ac26a0411d1145b376"
[metadata.files]
astroid = []
@ -621,6 +659,7 @@ iniconfig = [
isort = []
lazy-object-proxy = []
mccabe = []
mypy = []
mypy-extensions = [
{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"},
@ -668,6 +707,7 @@ pytest-cov = [
]
pytest-isort = []
pytest-mock = []
pytest-mypy = []
pytest-pylint = []
pyyaml = []
rich = []

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "pytest-csv-params"
version = "0.1.0"
version = "0.2.0"
description = "Pytest plugin for Test Case Parametrization with CSV files"
authors = ["Juergen Edelbluth <csv_params@jued.de>"]
license = "MIT"
@ -41,7 +41,7 @@ packages = [
"pytest-csv-params" = "pytest_csv_params.plugin"
[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=[
"ignore:.*BlackItem.*:_pytest.warning_types.PytestDeprecationWarning",
"ignore:.*BlackItem.*:_pytest.warning_types.PytestRemovedIn8Warning",
@ -81,6 +81,7 @@ omit = [
"*/test_plugin_test_multiplication.py",
"*/test_plugin_test_error.py",
"*/test_base_dir_param.py",
"*/test_plugin_test_text_shorthand.py",
]
relative_files = true
@ -94,6 +95,10 @@ exclude_lines = [
"if __name__ == .__main__.:",
]
[tool.mypy]
python_version = "3.8"
strict = true
[tool.tox]
legacy_tox_ini = """
[tox]
@ -117,7 +122,7 @@ include = '\.pyi?$'
[tool.isort]
line_length = 120
include_trailing_comma = "True"
include_trailing_comma = true
multi_line_output = 3
[tool.poetry.dependencies]
@ -134,6 +139,7 @@ pytest-pylint = "^0.18.0"
pytest-mock = "^3.8.2"
pytest-clarity = "^1.0.1"
pytest-bandit = "^0.6.1"
pytest-mypy = "^0.9.1"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@ -2,12 +2,11 @@
Types to ease the usage of the API
"""
import csv
from typing import Callable, Dict, Optional, Type, TypeVar
from typing import Any, Callable, Dict, Optional, Type
T = TypeVar("T")
DataCast = Callable[[str], T]
DataCast = Callable[[str], Any]
DataCastDict = Dict[str, DataCast]
DataCasts = Optional[DataCastDict]
BaseDir = 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
from os.path import dirname, join
from typing import Callable, Generator
import pytest
from _pytest.config import Config
def get_csv(csv: str):
def get_csv(csv: str) -> str:
"""
Get CSV data
"""
@ -18,7 +20,7 @@ def get_csv(csv: str):
@pytest.fixture(scope="session")
def simple_test_csv():
def simple_test_csv() -> str:
"""
Provide simple CSV data
"""
@ -26,7 +28,7 @@ def simple_test_csv():
@pytest.fixture(scope="session")
def bad_test_csv():
def bad_test_csv() -> str:
"""
Provide bad CSV data
"""
@ -34,7 +36,15 @@ def bad_test_csv():
@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
"""
@ -43,8 +53,18 @@ def simple_fruit_test():
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)
def install_plugin_locally(pytestconfig):
def install_plugin_locally(pytestconfig: Config) -> Generator[None, None, None]:
"""
Install the local package
"""

View File

@ -2,8 +2,10 @@
Test the usage of the Command Line
"""
from pathlib import Path
from typing import Callable
import pytest
from _pytest.pytester import Pytester
from _ptcsvp.cmdline import HELP_TEXT
@ -15,7 +17,9 @@ from _ptcsvp.cmdline import HELP_TEXT
(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
"""
@ -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)
def test_help(pytester):
def test_help(pytester: Pytester) -> None:
"""
Test if the plugin is in the help
"""

View File

@ -1,9 +1,14 @@
"""
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
"""
@ -16,7 +21,7 @@ def test_plugin_test_multiplication(pytester, simple_test_csv, simple_fruit_test
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
"""
@ -27,3 +32,18 @@ def test_plugin_test_error(pytester, bad_test_csv, simple_fruit_test):
result = pytester.runpytest("-p", "no:bandit")
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
"""
from os.path import dirname, join
from typing import List, Optional, Tuple, Type
import pytest
@ -76,9 +77,14 @@ from pytest_csv_params.exception import CsvParamsDataFileInvalid, CsvParamsDataF
),
],
)
def test_parametrization(
csv_file, id_col, result, ids, expect_exception, expect_message
): # pylint: disable=too-many-arguments
def test_parametrization( # pylint: disable=too-many-arguments
csv_file: str,
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
"""

View File

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

View File

@ -2,18 +2,20 @@
We are checking the python version for the plugin.
"""
import sys
from typing import List, Optional, Tuple, Type, Union
import pytest
from attr.exceptions import PythonTooOldError
from pytest_mock import MockerFixture
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
"""
elements = []
elements: List[Union[int, str]] = []
for v_part in p_version.split("."):
try:
elements.append(int(v_part))
@ -58,7 +60,9 @@ def build_version(p_version):
("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
"""
@ -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")),
],
)
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
"""