Skip to content
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
46 changes: 46 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,49 @@ venv.bak/
.mypy_cache/

.idea/

# Testing
.pytest_cache/
.coverage
.coverage.*
htmlcov/
coverage.xml
*.cover
*.py,cover
.hypothesis/
pytest_cache/
.tox/

# Claude settings
.claude/*

# Virtual environments
.venv/
venv/
ENV/
env/
.python-version

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

# OS files
.DS_Store
Thumbs.db

# Build artifacts
build/
dist/
*.egg-info/
*.egg
wheels/
pip-wheel-metadata/
share/python-wheels/
*.so
__pycache__/
*.py[cod]
*$py.class
332 changes: 332 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

96 changes: 96 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
[tool.poetry]
name = "drymail"
version = "0.0.6"
description = "Drymail is a minimalist wrapper over Python's existing smtplib and email libraries, designed to be friendly but unrestrictive."
authors = ["Sumit Ghosh <[email protected]>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/SkullTech/drymail"
repository = "https://github.com/SkullTech/drymail"
keywords = ["smtp", "email"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12"
]
packages = [{include = "drymail.py"}]

[tool.poetry.dependencies]
python = "^3.8"
mistune = "*"
beautifulsoup4 = "*"

[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"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--cov=drymail",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=80"
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow tests"
]

[tool.coverage.run]
source = ["drymail"]
branch = true
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/site-packages/*"
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod"
]
show_missing = true
precision = 2

[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.
114 changes: 114 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Shared pytest fixtures and configuration for drymail tests."""

import os
import tempfile
from pathlib import Path
from unittest.mock import Mock, MagicMock
import pytest


@pytest.fixture
def temp_dir():
"""Provide a temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmp_dir:
yield Path(tmp_dir)


@pytest.fixture
def mock_smtp_server():
"""Mock SMTP server for testing email functionality."""
server = MagicMock()
server.send_message = MagicMock(return_value=None)
server.sendmail = MagicMock(return_value={})
server.quit = MagicMock(return_value=None)
server.__enter__ = MagicMock(return_value=server)
server.__exit__ = MagicMock(return_value=None)
return server


@pytest.fixture
def mock_email_config():
"""Provide mock email configuration."""
return {
'host': 'smtp.example.com',
'port': 587,
'user': '[email protected]',
'password': 'test_password',
'ssl': False,
'tls': True
}


@pytest.fixture
def sample_email_data():
"""Provide sample email data for testing."""
return {
'sender': '[email protected]',
'receivers': ['[email protected]', '[email protected]'],
'subject': 'Test Email Subject',
'text': 'This is a test email body.',
'html': '<h1>This is a test email body.</h1>',
'attachments': []
}


@pytest.fixture
def mock_attachment():
"""Provide a mock file attachment."""
attachment = Mock()
attachment.name = 'test_file.txt'
attachment.read = Mock(return_value=b'Test file content')
return attachment


@pytest.fixture
def temp_file(temp_dir):
"""Create a temporary file and return its path."""
def _create_temp_file(filename='test.txt', content='Test content'):
file_path = temp_dir / filename
file_path.write_text(content)
return file_path
return _create_temp_file


@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():
"""Capture log messages during tests."""
import logging
from io import StringIO

log_capture = StringIO()
handler = logging.StreamHandler(log_capture)
handler.setLevel(logging.DEBUG)

logger = logging.getLogger()
original_level = logger.level
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)

yield log_capture

logger.removeHandler(handler)
logger.setLevel(original_level)


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"
)
Empty file added tests/integration/__init__.py
Empty file.
106 changes: 106 additions & 0 deletions tests/test_infrastructure_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Validation tests to verify the testing infrastructure is properly set up."""

import pytest
import tempfile
from pathlib import Path
from unittest.mock import Mock


class TestInfrastructureValidation:
"""Test class to validate pytest infrastructure."""

@pytest.mark.unit
def test_pytest_is_working(self):
"""Verify that pytest can discover and run tests."""
assert True
assert 1 + 1 == 2

@pytest.mark.unit
def test_fixtures_are_available(self, temp_dir, mock_smtp_server, mock_email_config):
"""Verify that custom fixtures from conftest.py are accessible."""
assert isinstance(temp_dir, Path)
assert temp_dir.exists()
assert temp_dir.is_dir()

assert mock_smtp_server is not None
assert hasattr(mock_smtp_server, 'send_message')

assert isinstance(mock_email_config, dict)
assert 'host' in mock_email_config
assert mock_email_config['host'] == 'smtp.example.com'

@pytest.mark.unit
def test_temp_file_fixture(self, temp_file):
"""Verify that the temp_file fixture works correctly."""
test_file = temp_file('validation.txt', 'Test content for validation')
assert test_file.exists()
assert test_file.read_text() == 'Test content for validation'

@pytest.mark.unit
def test_mocking_works(self, mocker):
"""Verify that pytest-mock is properly installed and working."""
mock_func = mocker.Mock(return_value=42)
result = mock_func()
assert result == 42
mock_func.assert_called_once()

@pytest.mark.unit
def test_coverage_is_tracked(self):
"""Verify that code coverage is being tracked."""
def example_function(x, y):
if x > y:
return x
else:
return y

assert example_function(5, 3) == 5
assert example_function(2, 7) == 7

@pytest.mark.integration
def test_markers_work(self):
"""Verify that custom markers are recognized."""
assert True

@pytest.mark.slow
def test_slow_marker(self):
"""Verify that the slow marker is recognized."""
import time
time.sleep(0.1) # Simulate a slow test
assert True

def test_sample_email_data_fixture(self, sample_email_data):
"""Verify the sample_email_data fixture provides expected data."""
assert 'sender' in sample_email_data
assert 'receivers' in sample_email_data
assert isinstance(sample_email_data['receivers'], list)
assert len(sample_email_data['receivers']) == 2

def test_mock_attachment_fixture(self, mock_attachment):
"""Verify the mock_attachment fixture works correctly."""
assert mock_attachment.name == 'test_file.txt'
assert mock_attachment.read() == b'Test file content'

def test_capture_logs_fixture(self, capture_logs):
"""Verify the capture_logs fixture works correctly."""
import logging
logger = logging.getLogger(__name__)
logger.info("Test log message")

log_output = capture_logs.getvalue()
assert "Test log message" in log_output


def test_module_level_test():
"""Verify that module-level tests are discovered."""
assert True


@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 4),
(3, 6),
(4, 8),
])
def test_parametrized_tests(input, expected):
"""Verify that parametrized tests work correctly."""
assert input * 2 == expected
Empty file added tests/unit/__init__.py
Empty file.