Skip to content

feat: Set up comprehensive Python testing infrastructure with Poetry #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,66 @@ output
__pycache__
content

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
*.coverage
.hypothesis/
.tox/
nosetests.xml
test-results/

# Virtual environments
venv/
env/
.venv/
virtualenv/

# Poetry - do not ignore lock file
# poetry.lock

# Build artifacts
build/
dist/
*.egg-info/
*.egg
wheels/
pip-wheel-metadata/

# IDE files
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store

# Claude settings
.claude/*

# Python
*.py[cod]
*$py.class
*.so
.Python
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
.cache

282 changes: 282 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
[tool.poetry]
name = "cookbook-2nd"
version = "0.1.0"
description = "IPython Cookbook, Second Edition - Code Examples"
authors = ["Your Name <[email protected]>"]
readme = "README.md"
package-mode = false

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.1"

[tool.poetry.scripts]
test = "pytest:main"
tests = "pytest:main"

[tool.pytest.ini_options]
minversion = "7.0"
addopts = [
"-v",
"--strict-markers",
"--cov=.",
"--cov-report=html",
"--cov-report=xml",
"--cov-report=term-missing",
"--cov-fail-under=80", # Set to 80% as requested, will apply once actual code is added
]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_functions = ["test_*"]
python_classes = ["Test*"]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]

[tool.coverage.run]
source = ["."]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/venv/*",
"*/env/*",
"*/.venv/*",
"*/virtualenv/*",
"*/website/*",
"*/conftest.py",
"*/setup.py",
"*/.pytest_cache/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"@abstract",
"@abc.abstractmethod",
]

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
196 changes: 196 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""Shared pytest fixtures and configuration for the test suite."""

import os
import shutil
import tempfile
from pathlib import Path
from typing import Generator, Any

import pytest


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for testing.

Yields:
Path: Path to the temporary directory.
"""
temp_path = tempfile.mkdtemp()
yield Path(temp_path)
shutil.rmtree(temp_path, ignore_errors=True)


@pytest.fixture
def temp_file(temp_dir: Path) -> Generator[Path, None, None]:
"""Create a temporary file for testing.

Args:
temp_dir: The temporary directory fixture.

Yields:
Path: Path to the temporary file.
"""
temp_file = temp_dir / "test_file.txt"
temp_file.write_text("test content")
yield temp_file


@pytest.fixture
def mock_config() -> dict[str, Any]:
"""Provide a mock configuration dictionary.

Returns:
dict: Mock configuration settings.
"""
return {
"debug": True,
"verbose": False,
"output_dir": "/tmp/output",
"max_retries": 3,
"timeout": 30,
}


@pytest.fixture
def sample_data() -> dict[str, Any]:
"""Provide sample data for testing.

Returns:
dict: Sample data structure.
"""
return {
"users": [
{"id": 1, "name": "Alice", "email": "[email protected]"},
{"id": 2, "name": "Bob", "email": "[email protected]"},
],
"projects": [
{"id": 1, "name": "Project A", "status": "active"},
{"id": 2, "name": "Project B", "status": "completed"},
],
}


@pytest.fixture
def mock_notebook_data() -> dict[str, Any]:
"""Provide mock Jupyter notebook data.

Returns:
dict: Mock notebook structure.
"""
return {
"cells": [
{
"cell_type": "code",
"source": "print('Hello, World!')",
"metadata": {},
"outputs": [],
},
{
"cell_type": "markdown",
"source": "# Test Notebook\nThis is a test.",
"metadata": {},
},
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3",
}
},
"nbformat": 4,
"nbformat_minor": 5,
}


@pytest.fixture(autouse=True)
def reset_environment():
"""Reset environment variables before each test."""
original_env = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def capture_logs(caplog):
"""Fixture to capture log messages during tests.

Args:
caplog: pytest's built-in log capture fixture.

Returns:
The caplog fixture configured for testing.
"""
caplog.set_level("DEBUG")
return caplog


@pytest.fixture(scope="session")
def test_resources_dir() -> Path:
"""Get the path to test resources directory.

Returns:
Path: Path to the test resources directory.
"""
return Path(__file__).parent / "resources"


class MockResponse:
"""Mock HTTP response for testing."""

def __init__(self, json_data=None, status_code=200, text=""):
self.json_data = json_data
self.status_code = status_code
self.text = text

def json(self):
return self.json_data

def raise_for_status(self):
if self.status_code >= 400:
raise Exception(f"HTTP Error {self.status_code}")


@pytest.fixture
def mock_http_response():
"""Factory fixture for creating mock HTTP responses.

Returns:
Callable: Function to create MockResponse objects.
"""
def _make_response(json_data=None, status_code=200, text=""):
return MockResponse(json_data, status_code, text)
return _make_response


# Markers for test categorization
def pytest_configure(config):
"""Configure pytest with custom markers."""
config.addinivalue_line(
"markers", "unit: mark test as a unit test"
)
config.addinivalue_line(
"markers", "integration: mark test as an integration test"
)
config.addinivalue_line(
"markers", "slow: mark test as slow running"
)


# Hooks for test execution
def pytest_runtest_setup(item):
"""Run before each test method."""
if "slow" in item.keywords and not item.config.getoption("--runslow"):
pytest.skip("need --runslow option to run slow tests")


def pytest_addoption(parser):
"""Add custom command line options."""
parser.addoption(
"--runslow",
action="store_true",
default=False,
help="run slow tests"
)
Empty file added tests/integration/__init__.py
Empty file.
Loading