pytest-datadriver 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ """pytest-datadriver: A pytest plugin for data-driven testing with YAML/JSON external data files."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from pytest_datadriver.exceptions import (
6
+ DataDriverError,
7
+ DataFileNotFoundError,
8
+ DataFileFormatError,
9
+ DuplicateModuleError,
10
+ CaseNotFoundError,
11
+ ExtractionError,
12
+ )
13
+ from pytest_datadriver.models import CaseEntry, ParamSet, ExportContext, LayoutMode, OverrideMode
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "DataDriverError",
18
+ "DataFileNotFoundError",
19
+ "DataFileFormatError",
20
+ "DuplicateModuleError",
21
+ "CaseNotFoundError",
22
+ "ExtractionError",
23
+ "CaseEntry",
24
+ "ParamSet",
25
+ "ExportContext",
26
+ "LayoutMode",
27
+ "OverrideMode",
28
+ ]
@@ -0,0 +1,61 @@
1
+ """Constants and default values for pytest-datadriver."""
2
+
3
+ from pytest_datadriver.models import (
4
+ DataFormat,
5
+ ExportFilter,
6
+ ExportFormat,
7
+ LayoutMode,
8
+ ModuleMatchMode,
9
+ OverrideMode,
10
+ )
11
+
12
+ # Default data file search paths (relative to project root)
13
+ DEFAULT_DATA_PATHS = [
14
+ "TestData/data.yaml",
15
+ "TestData/data.yml",
16
+ "TestData/data.json",
17
+ ]
18
+
19
+ # Default test directory for extraction
20
+ DEFAULT_TEST_PATH = "tests/"
21
+
22
+ # Default output path for extraction
23
+ DEFAULT_OUTPUT_PATH = "TestData/data.yaml"
24
+
25
+ # CLI option defaults
26
+ DEFAULT_LAYOUT = LayoutMode.CASE
27
+ DEFAULT_MODE = OverrideMode.SMART
28
+ DEFAULT_DATA_FORMAT = DataFormat.AUTO
29
+ DEFAULT_EXPORT_FORMAT = ExportFormat.SIMPLE
30
+ DEFAULT_EXPORT_FILTER = ExportFilter.ALL
31
+ DEFAULT_MODULE_MATCH = ModuleMatchMode.FULLPATH
32
+
33
+ # Mark names used by the library
34
+ MARK_CASE_NO = "caseNo"
35
+ MARK_CASE_NAME = "caseName"
36
+
37
+ # pytest hook marker for skipping items in replace mode
38
+ SKIP_MARKER_NAME = "_skip_by_datadriver"
39
+
40
+ # Case number prefix for auto-generated IDs
41
+ AUTO_CASE_NO_PREFIX = "AUTO-"
42
+
43
+ # CLI option names
44
+ OPTION_DATA_FILE = "--data-file"
45
+ OPTION_DATA_MODE = "--data-mode"
46
+ OPTION_DATA_LAYOUT = "--data-layout"
47
+ OPTION_DATA_FORMAT = "--data-format"
48
+ OPTION_DATA_EXTRACT = "--data-extract"
49
+ OPTION_DATA_OUTPUT = "--data-output"
50
+ OPTION_DATA_EXPORT_FORMAT = "--data-export-format"
51
+ OPTION_DATA_EXPORT_FILTER = "--data-export-filter"
52
+ OPTION_DATA_MODULE_MATCH = "--data-module-match"
53
+ OPTION_TEST_PATH = "--test-path"
54
+
55
+ # Valid choices for CLI options
56
+ VALID_DATA_MODES = [m.value for m in OverrideMode]
57
+ VALID_LAYOUTS = [m.value for m in LayoutMode]
58
+ VALID_FORMATS = [m.value for m in DataFormat]
59
+ VALID_EXPORT_FORMATS = [m.value for m in ExportFormat]
60
+ VALID_EXPORT_FILTERS = [m.value for m in ExportFilter]
61
+ VALID_MODULE_MATCHES = [m.value for m in ModuleMatchMode]
@@ -0,0 +1,52 @@
1
+ """Custom exceptions for pytest-datadriver."""
2
+
3
+
4
+ class DataDriverError(Exception):
5
+ """Base exception for all pytest-datadriver errors."""
6
+ pass
7
+
8
+
9
+ class DataFileNotFoundError(DataDriverError):
10
+ """Raised when the specified data file does not exist."""
11
+
12
+ def __init__(self, file_path: str):
13
+ self.file_path = file_path
14
+ super().__init__(f"Data file not found: {file_path}")
15
+
16
+
17
+ class DataFileFormatError(DataDriverError):
18
+ """Raised when the data file has invalid YAML/JSON syntax."""
19
+
20
+ def __init__(self, file_path: str, detail: str):
21
+ self.file_path = file_path
22
+ self.detail = detail
23
+ super().__init__(f"Data file format error in '{file_path}': {detail}")
24
+
25
+
26
+ class DuplicateModuleError(DataDriverError):
27
+ """Raised when duplicate module filenames are detected in function-first layout with filename matching."""
28
+
29
+ def __init__(self, filename: str, paths: list):
30
+ self.filename = filename
31
+ self.paths = paths
32
+ super().__init__(
33
+ f"Duplicate module filename '{filename}' found in multiple directories: {paths}. "
34
+ f"Use --data-module-match fullpath to resolve."
35
+ )
36
+
37
+
38
+ class CaseNotFoundError(DataDriverError):
39
+ """Raised when a case_no in the data file has no matching test function in code."""
40
+
41
+ def __init__(self, case_no: str):
42
+ self.case_no = case_no
43
+ super().__init__(f"Case '{case_no}' not found in any collected test item")
44
+
45
+
46
+ class ExtractionError(DataDriverError):
47
+ """Raised when AST extraction encounters an error."""
48
+
49
+ def __init__(self, file_path: str, detail: str):
50
+ self.file_path = file_path
51
+ self.detail = detail
52
+ super().__init__(f"Extraction error in '{file_path}': {detail}")
@@ -0,0 +1,60 @@
1
+ """Data exporter factory with auto-detection of file format."""
2
+
3
+ from typing import List
4
+
5
+ from pytest_datadriver.exporters.base import DataExporter
6
+ from pytest_datadriver.exporters.yaml_exporter import YamlExporter
7
+ from pytest_datadriver.exporters.json_exporter import JsonExporter
8
+ from pytest_datadriver.models import CaseEntry, DataFormat, LayoutMode
9
+
10
+
11
+ def create_exporter(fmt: DataFormat = DataFormat.AUTO) -> DataExporter:
12
+ """Create a data exporter for the specified format.
13
+
14
+ Args:
15
+ fmt: Output format (AUTO uses extension detection, YAML or JSON).
16
+
17
+ Returns:
18
+ A DataExporter instance.
19
+
20
+ Raises:
21
+ ValueError: If the format is not supported.
22
+ """
23
+ if fmt == DataFormat.YAML:
24
+ return YamlExporter()
25
+ elif fmt == DataFormat.JSON:
26
+ return JsonExporter()
27
+ elif fmt == DataFormat.AUTO:
28
+ # Default to YAML
29
+ return YamlExporter()
30
+ else:
31
+ raise ValueError(f"Unsupported export format: {fmt}")
32
+
33
+
34
+ def export_cases(
35
+ cases: List[CaseEntry],
36
+ output_path: str,
37
+ layout: LayoutMode = LayoutMode.CASE,
38
+ fmt: DataFormat = DataFormat.AUTO,
39
+ ) -> None:
40
+ """Export cases to a data file, auto-detecting format from extension if AUTO.
41
+
42
+ Args:
43
+ cases: List of CaseEntry objects to export.
44
+ output_path: Path to write the output file.
45
+ layout: Layout mode (case-first or function-first).
46
+ fmt: Output format.
47
+ """
48
+ if fmt == DataFormat.AUTO:
49
+ lower = output_path.lower()
50
+ if lower.endswith(".json"):
51
+ exporter = JsonExporter()
52
+ else:
53
+ exporter = YamlExporter()
54
+ else:
55
+ exporter = create_exporter(fmt)
56
+
57
+ exporter.export(cases, output_path, layout)
58
+
59
+
60
+ __all__ = ["DataExporter", "YamlExporter", "JsonExporter", "create_exporter", "export_cases"]
@@ -0,0 +1,63 @@
1
+ """Abstract base class for data exporters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List
5
+
6
+ from pytest_datadriver.models import CaseEntry, LayoutMode
7
+
8
+
9
+ class DataExporter(ABC):
10
+ """Abstract base class for exporting CaseEntry objects to data files."""
11
+
12
+ @abstractmethod
13
+ def export(self, cases: List[CaseEntry], output_path: str, layout: LayoutMode = LayoutMode.CASE) -> None:
14
+ """
15
+ Export a list of CaseEntry objects to a data file.
16
+
17
+ Args:
18
+ cases: List of CaseEntry objects to export.
19
+ output_path: Path to write the output file.
20
+ layout: Layout mode (case-first or function-first).
21
+ """
22
+ ...
23
+
24
+ def _to_case_first(self, cases: List[CaseEntry]) -> dict:
25
+ """Convert cases to case-first layout dict."""
26
+ result = {}
27
+ for case in cases:
28
+ entry = {
29
+ "case_name": case.case_name,
30
+ "module": case.module,
31
+ "function": case.function,
32
+ "params": case.params,
33
+ }
34
+ # Add optional fields if present
35
+ if case.fixtures:
36
+ entry["fixtures"] = case.fixtures
37
+ if case.marks:
38
+ entry["marks"] = case.marks
39
+ if case.parametrize_meta:
40
+ entry["parametrize_meta"] = case.parametrize_meta
41
+ if case.source_location:
42
+ entry["source_location"] = case.source_location
43
+ result[case.case_no] = entry
44
+ return result
45
+
46
+ def _to_function_first(self, cases: List[CaseEntry]) -> dict:
47
+ """Convert cases to function-first layout dict."""
48
+ result = {}
49
+ for case in cases:
50
+ key = f"{case.module}::{case.function}"
51
+ if key not in result:
52
+ result[key] = []
53
+ entry = {
54
+ "case_no": case.case_no,
55
+ "case_name": case.case_name,
56
+ "params": case.params,
57
+ }
58
+ if case.fixtures:
59
+ entry["fixtures"] = case.fixtures
60
+ if case.marks:
61
+ entry["marks"] = case.marks
62
+ result[key].append(entry)
63
+ return result
@@ -0,0 +1,24 @@
1
+ """JSON data exporter."""
2
+
3
+ import json
4
+ import os
5
+ from typing import List
6
+
7
+ from pytest_datadriver.exporters.base import DataExporter
8
+ from pytest_datadriver.models import CaseEntry, LayoutMode
9
+
10
+
11
+ class JsonExporter(DataExporter):
12
+ """Export test case data to JSON files."""
13
+
14
+ def export(self, cases: List[CaseEntry], output_path: str, layout: LayoutMode = LayoutMode.CASE) -> None:
15
+ """Export cases to a JSON file."""
16
+ if layout == LayoutMode.CASE:
17
+ data = self._to_case_first(cases)
18
+ else:
19
+ data = self._to_function_first(cases)
20
+
21
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
22
+
23
+ with open(output_path, "w", encoding="utf-8") as f:
24
+ json.dump(data, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,32 @@
1
+ """YAML data exporter."""
2
+
3
+ import os
4
+ from typing import List
5
+
6
+ import yaml
7
+
8
+ from pytest_datadriver.exporters.base import DataExporter
9
+ from pytest_datadriver.models import CaseEntry, LayoutMode
10
+
11
+
12
+ class YamlExporter(DataExporter):
13
+ """Export test case data to YAML files."""
14
+
15
+ def export(self, cases: List[CaseEntry], output_path: str, layout: LayoutMode = LayoutMode.CASE) -> None:
16
+ """Export cases to a YAML file."""
17
+ if layout == LayoutMode.CASE:
18
+ data = self._to_case_first(cases)
19
+ else:
20
+ data = self._to_function_first(cases)
21
+
22
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
23
+
24
+ with open(output_path, "w", encoding="utf-8") as f:
25
+ yaml.dump(
26
+ data,
27
+ f,
28
+ default_flow_style=False,
29
+ allow_unicode=True,
30
+ sort_keys=False,
31
+ indent=2,
32
+ )