Skip to content

API Reference

This document provides details about the API of the markdown_to_testcase package.

TestCaseParser

The TestCaseParser class is responsible for parsing markdown files and extracting test cases.

from markdown_to_testcase.parser import TestCaseParser

parser = TestCaseParser(verbose=True)
test_cases = parser.parse_file("path/to/markdown_file.md")

markdown_to_testcase.parser.TestCaseParser

Parser for extracting test cases from markdown files.

Source code in markdown_to_testcase/parser.py
class TestCaseParser:
    """Parser for extracting test cases from markdown files."""

    def __init__(self, verbose: bool = False, check_yaml_lint: bool = False):
        """
        Initialize the parser.

        Args:
            verbose: Whether to output detailed error messages and suggestions.
            check_yaml_lint: Whether to run yamllint on YAML content before parsing.
        """
        self.md_parser = MarkdownIt()
        self.verbose = verbose
        self.check_yaml_lint = check_yaml_lint

    def parse_file(self, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Parse a markdown file and extract test cases.

        Args:
            file_path: Path to the markdown file.

        Returns:
            Dictionary with test case file names as keys and lists of test case dictionaries as values.
        """
        if not os.path.exists(file_path):
            logger.error(f"File not found: {file_path}")
            return {}

        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()

        return self.parse_content(content, file_path)

    def run_yamllint(self, yaml_content: str, file_name: str, _source_path: str) -> List[str]:
        """
        Run yamllint on the YAML content and return any errors.

        Args:
            yaml_content: YAML content as string.
            file_name: Name of the file for logging purposes.
            source_path: Source file path for logging purposes.

        Returns:
            List of yamllint error messages.
        """
        try:
            # Create a temporary file with the YAML content
            temp_file = Path(f"/tmp/yamllint_temp_{file_name.replace('/', '_')}.yaml")
            temp_file.write_text(yaml_content, encoding="utf-8")

            # Run yamllint on the temporary file
            result = subprocess.run(["yamllint", "-f", "parsable", str(temp_file)], capture_output=True, text=True, check=False)

            # Remove the temporary file
            temp_file.unlink()

            # Process the output
            if result.returncode == 0:
                return []
            else:
                # Format the error messages to be more readable
                error_lines = result.stdout.strip().split("\n")
                formatted_errors = []

                for line in error_lines:
                    if line:
                        # Extract line number and error message from yamllint output format
                        parts = line.split(":", 2)
                        if len(parts) >= 3:
                            line_num = parts[1]
                            error_msg = parts[2].strip()
                            formatted_errors.append(f"Line {line_num}: {error_msg}")
                        else:
                            formatted_errors.append(line)

                return formatted_errors

        except (IOError, subprocess.SubprocessError) as e:
            logger.warning(f"Failed to run yamllint: {str(e)}")
            return []

    def parse_content(self, content: str, source_path: str = "") -> Dict[str, List[Dict[str, Any]]]:
        """
        Parse markdown content and extract test cases.

        Args:
            content: Markdown content as string.
            source_path: Source file path (for logging purposes).

        Returns:
            Dictionary with test case file names as keys and lists of test case dictionaries as values.
        """
        test_cases = {}

        # Find all "### TestCases ($file_name)" sections
        test_case_sections = re.finditer(r"### TestCases\s+\(([^)]+)\)(.*?)(?=###|\Z)", content, re.DOTALL)

        for match in test_case_sections:
            file_name = match.group(1).strip()
            yaml_content = match.group(2).strip()

            # Check YAML with yamllint if enabled
            if self.check_yaml_lint:
                lint_errors = self.run_yamllint(yaml_content, file_name, source_path)
                if lint_errors:
                    logger.error(f"YAML lint errors in section for {file_name} in {source_path}:")
                    for error in lint_errors:
                        logger.error(f"  {error}")
                    logger.info("Please fix the YAML formatting issues before processing.")
                    continue

            try:
                # Try to parse the YAML content
                parsed_test_cases = yaml.safe_load(yaml_content)

                if not parsed_test_cases:
                    logger.warning(f"No test cases found in section for {file_name} in {source_path}")
                    continue

                # Ensure the result is a list
                if not isinstance(parsed_test_cases, list):
                    if self.verbose:
                        logger.error(f"YAML content in section for {file_name} is not a list. Found type: {type(parsed_test_cases)}")
                        logger.error("Content should start with '- ' for each test case item")
                    else:
                        logger.error(f"YAML parse error: Expected list format in section for {file_name}")
                    continue

                test_cases[file_name] = parsed_test_cases
                logger.info(f"Successfully parsed {len(parsed_test_cases)} test cases from section for {file_name}")

            except yaml.YAMLError as e:
                if self.verbose:
                    logger.error(f"YAML parse error in section for {file_name} in {source_path}: {str(e)}")
                    logger.debug(f"Problematic YAML content:\n{yaml_content}")
                    logger.info("Suggestion: Check for proper indentation and YAML syntax.")
                else:
                    logger.error(f"YAML parse error in section for {file_name}. Use --verbose for details.")

        if not test_cases:
            logger.warning(f"No test case sections found in {source_path}")

        return test_cases

    def parse_yaml_file(self, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Parse a YAML file containing test cases directly.

        Args:
            file_path: Path to the YAML file.

        Returns:
            Dictionary with test case file names as keys and lists of test case dictionaries as values.
        """
        if not os.path.exists(file_path):
            logger.error(f"File not found: {file_path}")
            return {}

        # Check YAML with yamllint if enabled
        if self.check_yaml_lint:
            with open(file_path, "r", encoding="utf-8") as f:
                yaml_content = f.read()

            lint_errors = self.run_yamllint(yaml_content, Path(file_path).name, file_path)
            if lint_errors:
                logger.error(f"YAML lint errors in file {file_path}:")
                for error in lint_errors:
                    logger.error(f"  {error}")
                logger.info("Please fix the YAML formatting issues before processing.")
                return {}

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                content = yaml.safe_load(f)

            if not isinstance(content, dict):
                logger.error(f"YAML file {file_path} should contain a dictionary mapping file names to test cases")
                return {}

            # Validate the structure
            for file_name, test_cases in content.items():
                if not isinstance(test_cases, list):
                    logger.error(f"Test cases for {file_name} should be a list")
                    continue

            logger.info(f"Successfully parsed YAML file {file_path} with {len(content)} test case sections")
            return content

        except yaml.YAMLError as e:
            if self.verbose:
                logger.error(f"YAML parse error in file {file_path}: {str(e)}")
                logger.info("Suggestion: Check for proper indentation and YAML syntax.")
            else:
                logger.error(f"YAML parse error in file {file_path}. Use --verbose for details.")
            return {}
        except (IOError, subprocess.SubprocessError) as e:
            logger.error(f"Error parsing YAML file {file_path}: {str(e)}")
            return {}

__init__(verbose=False, check_yaml_lint=False)

Initialize the parser.

Parameters:

Name Type Description Default
verbose bool

Whether to output detailed error messages and suggestions.

False
check_yaml_lint bool

Whether to run yamllint on YAML content before parsing.

False
Source code in markdown_to_testcase/parser.py
def __init__(self, verbose: bool = False, check_yaml_lint: bool = False):
    """
    Initialize the parser.

    Args:
        verbose: Whether to output detailed error messages and suggestions.
        check_yaml_lint: Whether to run yamllint on YAML content before parsing.
    """
    self.md_parser = MarkdownIt()
    self.verbose = verbose
    self.check_yaml_lint = check_yaml_lint

parse_content(content, source_path='')

Parse markdown content and extract test cases.

Parameters:

Name Type Description Default
content str

Markdown content as string.

required
source_path str

Source file path (for logging purposes).

''

Returns:

Type Description
Dict[str, List[Dict[str, Any]]]

Dictionary with test case file names as keys and lists of test case dictionaries as values.

Source code in markdown_to_testcase/parser.py
def parse_content(self, content: str, source_path: str = "") -> Dict[str, List[Dict[str, Any]]]:
    """
    Parse markdown content and extract test cases.

    Args:
        content: Markdown content as string.
        source_path: Source file path (for logging purposes).

    Returns:
        Dictionary with test case file names as keys and lists of test case dictionaries as values.
    """
    test_cases = {}

    # Find all "### TestCases ($file_name)" sections
    test_case_sections = re.finditer(r"### TestCases\s+\(([^)]+)\)(.*?)(?=###|\Z)", content, re.DOTALL)

    for match in test_case_sections:
        file_name = match.group(1).strip()
        yaml_content = match.group(2).strip()

        # Check YAML with yamllint if enabled
        if self.check_yaml_lint:
            lint_errors = self.run_yamllint(yaml_content, file_name, source_path)
            if lint_errors:
                logger.error(f"YAML lint errors in section for {file_name} in {source_path}:")
                for error in lint_errors:
                    logger.error(f"  {error}")
                logger.info("Please fix the YAML formatting issues before processing.")
                continue

        try:
            # Try to parse the YAML content
            parsed_test_cases = yaml.safe_load(yaml_content)

            if not parsed_test_cases:
                logger.warning(f"No test cases found in section for {file_name} in {source_path}")
                continue

            # Ensure the result is a list
            if not isinstance(parsed_test_cases, list):
                if self.verbose:
                    logger.error(f"YAML content in section for {file_name} is not a list. Found type: {type(parsed_test_cases)}")
                    logger.error("Content should start with '- ' for each test case item")
                else:
                    logger.error(f"YAML parse error: Expected list format in section for {file_name}")
                continue

            test_cases[file_name] = parsed_test_cases
            logger.info(f"Successfully parsed {len(parsed_test_cases)} test cases from section for {file_name}")

        except yaml.YAMLError as e:
            if self.verbose:
                logger.error(f"YAML parse error in section for {file_name} in {source_path}: {str(e)}")
                logger.debug(f"Problematic YAML content:\n{yaml_content}")
                logger.info("Suggestion: Check for proper indentation and YAML syntax.")
            else:
                logger.error(f"YAML parse error in section for {file_name}. Use --verbose for details.")

    if not test_cases:
        logger.warning(f"No test case sections found in {source_path}")

    return test_cases

parse_file(file_path)

Parse a markdown file and extract test cases.

Parameters:

Name Type Description Default
file_path str

Path to the markdown file.

required

Returns:

Type Description
Dict[str, List[Dict[str, Any]]]

Dictionary with test case file names as keys and lists of test case dictionaries as values.

Source code in markdown_to_testcase/parser.py
def parse_file(self, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
    """
    Parse a markdown file and extract test cases.

    Args:
        file_path: Path to the markdown file.

    Returns:
        Dictionary with test case file names as keys and lists of test case dictionaries as values.
    """
    if not os.path.exists(file_path):
        logger.error(f"File not found: {file_path}")
        return {}

    with open(file_path, "r", encoding="utf-8") as f:
        content = f.read()

    return self.parse_content(content, file_path)

parse_yaml_file(file_path)

Parse a YAML file containing test cases directly.

Parameters:

Name Type Description Default
file_path str

Path to the YAML file.

required

Returns:

Type Description
Dict[str, List[Dict[str, Any]]]

Dictionary with test case file names as keys and lists of test case dictionaries as values.

Source code in markdown_to_testcase/parser.py
def parse_yaml_file(self, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
    """
    Parse a YAML file containing test cases directly.

    Args:
        file_path: Path to the YAML file.

    Returns:
        Dictionary with test case file names as keys and lists of test case dictionaries as values.
    """
    if not os.path.exists(file_path):
        logger.error(f"File not found: {file_path}")
        return {}

    # Check YAML with yamllint if enabled
    if self.check_yaml_lint:
        with open(file_path, "r", encoding="utf-8") as f:
            yaml_content = f.read()

        lint_errors = self.run_yamllint(yaml_content, Path(file_path).name, file_path)
        if lint_errors:
            logger.error(f"YAML lint errors in file {file_path}:")
            for error in lint_errors:
                logger.error(f"  {error}")
            logger.info("Please fix the YAML formatting issues before processing.")
            return {}

    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = yaml.safe_load(f)

        if not isinstance(content, dict):
            logger.error(f"YAML file {file_path} should contain a dictionary mapping file names to test cases")
            return {}

        # Validate the structure
        for file_name, test_cases in content.items():
            if not isinstance(test_cases, list):
                logger.error(f"Test cases for {file_name} should be a list")
                continue

        logger.info(f"Successfully parsed YAML file {file_path} with {len(content)} test case sections")
        return content

    except yaml.YAMLError as e:
        if self.verbose:
            logger.error(f"YAML parse error in file {file_path}: {str(e)}")
            logger.info("Suggestion: Check for proper indentation and YAML syntax.")
        else:
            logger.error(f"YAML parse error in file {file_path}. Use --verbose for details.")
        return {}
    except (IOError, subprocess.SubprocessError) as e:
        logger.error(f"Error parsing YAML file {file_path}: {str(e)}")
        return {}

run_yamllint(yaml_content, file_name, _source_path)

Run yamllint on the YAML content and return any errors.

Parameters:

Name Type Description Default
yaml_content str

YAML content as string.

required
file_name str

Name of the file for logging purposes.

required
source_path

Source file path for logging purposes.

required

Returns:

Type Description
List[str]

List of yamllint error messages.

Source code in markdown_to_testcase/parser.py
def run_yamllint(self, yaml_content: str, file_name: str, _source_path: str) -> List[str]:
    """
    Run yamllint on the YAML content and return any errors.

    Args:
        yaml_content: YAML content as string.
        file_name: Name of the file for logging purposes.
        source_path: Source file path for logging purposes.

    Returns:
        List of yamllint error messages.
    """
    try:
        # Create a temporary file with the YAML content
        temp_file = Path(f"/tmp/yamllint_temp_{file_name.replace('/', '_')}.yaml")
        temp_file.write_text(yaml_content, encoding="utf-8")

        # Run yamllint on the temporary file
        result = subprocess.run(["yamllint", "-f", "parsable", str(temp_file)], capture_output=True, text=True, check=False)

        # Remove the temporary file
        temp_file.unlink()

        # Process the output
        if result.returncode == 0:
            return []
        else:
            # Format the error messages to be more readable
            error_lines = result.stdout.strip().split("\n")
            formatted_errors = []

            for line in error_lines:
                if line:
                    # Extract line number and error message from yamllint output format
                    parts = line.split(":", 2)
                    if len(parts) >= 3:
                        line_num = parts[1]
                        error_msg = parts[2].strip()
                        formatted_errors.append(f"Line {line_num}: {error_msg}")
                    else:
                        formatted_errors.append(line)

            return formatted_errors

    except (IOError, subprocess.SubprocessError) as e:
        logger.warning(f"Failed to run yamllint: {str(e)}")
        return []

TestCaseConverter

The TestCaseConverter class converts test cases to CSV and Excel formats.

from markdown_to_testcase.converter import TestCaseConverter

converter = TestCaseConverter(output_dir="output")
csv_files = converter.convert_to_csv(test_cases)
excel_file = converter.convert_to_excel(test_cases)

markdown_to_testcase.converter.TestCaseConverter

Converter for transforming test cases to various formats.

Source code in markdown_to_testcase/converter.py
class TestCaseConverter:
    """Converter for transforming test cases to various formats."""

    def __init__(self, output_dir: str = "output"):
        """
        Initialize the converter.

        Args:
            output_dir: Directory to store output files.
        """
        self.output_dir = output_dir
        self._ensure_output_dir()

    def _ensure_output_dir(self):
        """Ensure the output directory exists."""
        os.makedirs(self.output_dir, exist_ok=True)

    def convert_to_csv(self, test_cases: Dict[str, List[Dict[str, Any]]], force: bool = False) -> List[str]:
        """
        Convert test cases to CSV files.

        Args:
            test_cases: Dictionary with test case file names as keys and lists of test case dictionaries as values.
            force: Whether to overwrite existing files without asking.

        Returns:
            List of paths to the created CSV files.
        """
        csv_files = []

        for file_name, cases in test_cases.items():
            if not cases:
                logger.warning(f"No test cases to convert for {file_name}")
                continue

            csv_path = os.path.join(self.output_dir, file_name)

            # Check if file exists and prompt for overwrite if not forced
            if os.path.exists(csv_path) and not force:
                logger.warning(f"File {csv_path} already exists. Use --force to overwrite.")
                continue

            # Get field names from the first test case
            fieldnames = list(cases[0].keys())

            try:
                with open(csv_path, "w", newline="", encoding="utf-8") as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows(cases)

                logger.info(f"Created CSV file: {csv_path}")
                csv_files.append(csv_path)

            except (IOError, csv.Error) as e:
                logger.error(f"Error creating CSV file {csv_path}: {str(e)}")

        return csv_files

    def convert_to_excel(self, test_cases: Dict[str, List[Dict[str, Any]]], force: bool = False) -> Optional[str]:
        """
        Convert test cases to an Excel file with multiple sheets.

        Args:
            test_cases: Dictionary with test case file names as keys and lists of test case dictionaries as values.
            force: Whether to overwrite existing files without asking.

        Returns:
            Path to the created Excel file, or None if no file was created.
        """
        if not test_cases:
            logger.warning("No test cases to convert to Excel")
            return None

        excel_path = os.path.join(self.output_dir, "test_cases.xlsx")

        # Check if file exists and prompt for overwrite if not forced
        if os.path.exists(excel_path) and not force:
            logger.warning(f"File {excel_path} already exists. Use --force to overwrite.")
            return None

        try:
            wb = openpyxl.Workbook()

            # Remove the default sheet
            default_sheet = wb.active
            wb.remove(default_sheet)

            for file_name, cases in test_cases.items():
                if not cases:
                    logger.warning(f"No test cases to add to Excel for {file_name}")
                    continue

                # Create a new sheet for this file
                sheet_name = Path(file_name).stem
                sheet = wb.create_sheet(title=sheet_name)

                # Get field names from the first test case
                fieldnames = list(cases[0].keys())

                # Add header row
                for col_idx, field in enumerate(fieldnames, 1):
                    cell = sheet.cell(row=1, column=col_idx, value=field)
                    cell.font = Font(bold=True)
                    cell.fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid")
                    cell.alignment = Alignment(horizontal="center", vertical="center")

                # Add data rows
                for row_idx, case in enumerate(cases, 2):
                    for col_idx, field in enumerate(fieldnames, 1):
                        value = case.get(field, "")
                        sheet.cell(row=row_idx, column=col_idx, value=value)

                # Auto-adjust column widths
                for col in sheet.columns:
                    max_length = 0
                    column = col[0].column_letter  # Get column letter
                    for cell in col:
                        try:
                            if len(str(cell.value)) > max_length:
                                max_length = len(str(cell.value))
                        except (TypeError, ValueError):
                            pass
                    adjusted_width = (max_length + 2) * 1.2
                    sheet.column_dimensions[column].width = adjusted_width

            wb.save(excel_path)
            logger.info(f"Created Excel file: {excel_path}")
            return excel_path

        except (IOError, openpyxl.utils.exceptions.InvalidFileException) as e:
            logger.error(f"Error creating Excel file {excel_path}: {str(e)}")
            return None

__init__(output_dir='output')

Initialize the converter.

Parameters:

Name Type Description Default
output_dir str

Directory to store output files.

'output'
Source code in markdown_to_testcase/converter.py
def __init__(self, output_dir: str = "output"):
    """
    Initialize the converter.

    Args:
        output_dir: Directory to store output files.
    """
    self.output_dir = output_dir
    self._ensure_output_dir()

convert_to_csv(test_cases, force=False)

Convert test cases to CSV files.

Parameters:

Name Type Description Default
test_cases Dict[str, List[Dict[str, Any]]]

Dictionary with test case file names as keys and lists of test case dictionaries as values.

required
force bool

Whether to overwrite existing files without asking.

False

Returns:

Type Description
List[str]

List of paths to the created CSV files.

Source code in markdown_to_testcase/converter.py
def convert_to_csv(self, test_cases: Dict[str, List[Dict[str, Any]]], force: bool = False) -> List[str]:
    """
    Convert test cases to CSV files.

    Args:
        test_cases: Dictionary with test case file names as keys and lists of test case dictionaries as values.
        force: Whether to overwrite existing files without asking.

    Returns:
        List of paths to the created CSV files.
    """
    csv_files = []

    for file_name, cases in test_cases.items():
        if not cases:
            logger.warning(f"No test cases to convert for {file_name}")
            continue

        csv_path = os.path.join(self.output_dir, file_name)

        # Check if file exists and prompt for overwrite if not forced
        if os.path.exists(csv_path) and not force:
            logger.warning(f"File {csv_path} already exists. Use --force to overwrite.")
            continue

        # Get field names from the first test case
        fieldnames = list(cases[0].keys())

        try:
            with open(csv_path, "w", newline="", encoding="utf-8") as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows(cases)

            logger.info(f"Created CSV file: {csv_path}")
            csv_files.append(csv_path)

        except (IOError, csv.Error) as e:
            logger.error(f"Error creating CSV file {csv_path}: {str(e)}")

    return csv_files

convert_to_excel(test_cases, force=False)

Convert test cases to an Excel file with multiple sheets.

Parameters:

Name Type Description Default
test_cases Dict[str, List[Dict[str, Any]]]

Dictionary with test case file names as keys and lists of test case dictionaries as values.

required
force bool

Whether to overwrite existing files without asking.

False

Returns:

Type Description
Optional[str]

Path to the created Excel file, or None if no file was created.

Source code in markdown_to_testcase/converter.py
def convert_to_excel(self, test_cases: Dict[str, List[Dict[str, Any]]], force: bool = False) -> Optional[str]:
    """
    Convert test cases to an Excel file with multiple sheets.

    Args:
        test_cases: Dictionary with test case file names as keys and lists of test case dictionaries as values.
        force: Whether to overwrite existing files without asking.

    Returns:
        Path to the created Excel file, or None if no file was created.
    """
    if not test_cases:
        logger.warning("No test cases to convert to Excel")
        return None

    excel_path = os.path.join(self.output_dir, "test_cases.xlsx")

    # Check if file exists and prompt for overwrite if not forced
    if os.path.exists(excel_path) and not force:
        logger.warning(f"File {excel_path} already exists. Use --force to overwrite.")
        return None

    try:
        wb = openpyxl.Workbook()

        # Remove the default sheet
        default_sheet = wb.active
        wb.remove(default_sheet)

        for file_name, cases in test_cases.items():
            if not cases:
                logger.warning(f"No test cases to add to Excel for {file_name}")
                continue

            # Create a new sheet for this file
            sheet_name = Path(file_name).stem
            sheet = wb.create_sheet(title=sheet_name)

            # Get field names from the first test case
            fieldnames = list(cases[0].keys())

            # Add header row
            for col_idx, field in enumerate(fieldnames, 1):
                cell = sheet.cell(row=1, column=col_idx, value=field)
                cell.font = Font(bold=True)
                cell.fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid")
                cell.alignment = Alignment(horizontal="center", vertical="center")

            # Add data rows
            for row_idx, case in enumerate(cases, 2):
                for col_idx, field in enumerate(fieldnames, 1):
                    value = case.get(field, "")
                    sheet.cell(row=row_idx, column=col_idx, value=value)

            # Auto-adjust column widths
            for col in sheet.columns:
                max_length = 0
                column = col[0].column_letter  # Get column letter
                for cell in col:
                    try:
                        if len(str(cell.value)) > max_length:
                            max_length = len(str(cell.value))
                    except (TypeError, ValueError):
                        pass
                adjusted_width = (max_length + 2) * 1.2
                sheet.column_dimensions[column].width = adjusted_width

        wb.save(excel_path)
        logger.info(f"Created Excel file: {excel_path}")
        return excel_path

    except (IOError, openpyxl.utils.exceptions.InvalidFileException) as e:
        logger.error(f"Error creating Excel file {excel_path}: {str(e)}")
        return None