SpecRec ObjectFactory dependency injection for Python
Project description
SpecRec ObjectFactory for Python
A clean, powerful dependency injection factory for making legacy Python code testable with minimal changes.
Features
- Minimal Code Changes: Replace
newinstantiation with factory calls - Test Double Injection: Seamless mocking, stubbing, and spying capabilities
- Curried Syntax: Clean functional API inspired by TypeScript implementation
- Duck Typing: No complex interface mappings - Python's duck typing handles compatibility
- Context Managers: Built-in test isolation with Python's
withstatements - Thread Safe: Concurrent-safe global singleton and test double management
- Type Hints: Full type safety for modern Python development
- All 9 Microfeatures: Complete implementation of SpecRec ObjectFactory pattern
Quick Start
Installation
pip install specrec-python
Basic Usage
from specrec import create
# Replace this:
service = EmailService("smtp.gmail.com", 587)
# With this:
service = create(EmailService)("smtp.gmail.com", 587)
Test Double Injection
from specrec import create, set_one, context
def test_email_service():
mock_service = MockEmailService()
with context():
set_one(EmailService, mock_service)
# Your code under test
user_service = create(UserService)()
user_service.send_welcome_email("user@example.com")
# Verify the mock was used
assert len(mock_service.sent_emails) == 1
API Reference
Core Functions
create(cls)
Returns a curried function for creating instances of the specified class.
from specrec import create
# Create a factory function
create_service = create(EmailService)
# Use it multiple times
service1 = create_service("smtp1.example.com", 587)
service2 = create_service("smtp2.example.com", 465)
create_direct(cls, *args, **kwargs)
Create an instance directly with constructor arguments.
from specrec import create_direct
service = create_direct(EmailService, "smtp.gmail.com", port=587, username="user")
Test Double Management
Single-Use Test Doubles
from specrec import set_one
def test_service():
mock = MockEmailService()
set_one(EmailService, mock)
# Next creation returns mock
service = create(EmailService)("smtp.example.com")
assert service is mock
# Subsequent creation returns real instance
service2 = create(EmailService)("smtp.example.com")
assert isinstance(service2, EmailService)
Persistent Test Doubles
from specrec import set_always, clear_one
def test_service_always():
mock = MockEmailService()
set_always(EmailService, mock)
# All creations return mock
service1 = create(EmailService)("smtp.example.com")
service2 = create(EmailService)("smtp.example.com")
assert service1 is mock
assert service2 is mock
# Clean up
clear_one(EmailService)
Context Managers for Test Isolation
Python's context managers provide perfect test isolation:
from specrec import context, set_one, create
def test_isolated_mock():
mock = MockEmailService()
with context():
set_one(EmailService, mock)
service = create(EmailService)("smtp.example.com")
assert service is mock
# Outside context, creates real instances
service2 = create(EmailService)("smtp.example.com")
assert isinstance(service2, EmailService)
Constructor Parameter Tracking
Track how objects are constructed for debugging and verification:
from specrec.interfaces import IConstructorCalledWith, ConstructorParameterInfo
from typing import List
class TrackedService(IConstructorCalledWith):
def __init__(self, name: str, port: int, enabled: bool = True):
self.name = name
self.port = port
self.enabled = enabled
self.constructor_params: List[ConstructorParameterInfo] = []
def constructor_called_with(self, params: List[ConstructorParameterInfo]) -> None:
self.constructor_params = params
# Usage
service = create(TrackedService)("api-service", 8080, enabled=False)
print(service.constructor_params)
# [
# {"index": 0, "name": "name", "value": "api-service", "type_name": "str"},
# {"index": 1, "name": "port", "value": 8080, "type_name": "int"},
# {"index": 2, "name": "enabled", "value": False, "type_name": "bool"}
# ]
Object Registration
Register objects with IDs for clean logging and tracking:
from specrec import register_object, get_registered_object
service = create(EmailService)("smtp.gmail.com")
object_id = register_object(service, "main-email-service")
# Later retrieve it
retrieved = get_registered_object("main-email-service")
assert retrieved is service
Migrating Legacy Code
Before: Direct Instantiation
class UserService:
def __init__(self):
self.email_service = EmailService("smtp.company.com", 587)
self.db = SqlRepository("server=prod;...")
def create_user(self, email: str) -> User:
user = User(email)
self.db.save(user)
self.email_service.send_welcome(email)
return user
After: Factory Pattern
from specrec import create
class UserService:
def __init__(self):
self.email_service = create(EmailService)("smtp.company.com", 587)
self.db = create(SqlRepository)("server=prod;...")
def create_user(self, email: str) -> User:
user = User(email)
self.db.save(user)
self.email_service.send_welcome(email)
return user
Testable with Minimal Changes
def test_user_service():
mock_email = MockEmailService()
mock_db = MockRepository()
with context():
set_one(EmailService, mock_email)
set_one(SqlRepository, mock_db)
service = UserService()
user = service.create_user("test@example.com")
assert len(mock_db.saved_users) == 1
assert len(mock_email.sent_emails) == 1
Duck Typing Benefits
Python's duck typing eliminates the need for explicit interface mappings:
# No need for explicit interface declarations
class IEmailService:
def send(self, to: str, subject: str) -> bool: ...
class EmailService: # No need to explicitly implement IEmailService
def send(self, to: str, subject: str) -> bool:
# Real implementation
return True
class MockEmailService: # No need to explicitly implement IEmailService
def send(self, to: str, subject: str) -> bool:
# Mock implementation
return True
# Both work seamlessly
service: IEmailService = create(EmailService)("smtp.server")
mock: IEmailService = MockEmailService()
Advanced Usage
Custom Factory Instances
For advanced scenarios, create dedicated factory instances:
from specrec import ObjectFactory
# Create dedicated factory for a module
api_factory = ObjectFactory()
# Use it independently
create_api_service = api_factory.create(ApiService)
service = create_api_service("https://api.example.com")
Nested Context Managers
def test_nested_contexts():
outer_mock = MockEmailService()
inner_mock = MockEmailService()
with context():
set_always(EmailService, outer_mock)
with context():
set_always(EmailService, inner_mock)
service = create(EmailService)("smtp.example.com")
assert service is inner_mock
# Back to outer context
service = create(EmailService)("smtp.example.com")
assert service is outer_mock
# Outside all contexts
service = create(EmailService)("smtp.example.com")
assert isinstance(service, EmailService)
Comparison with Other Languages
| Feature | Python | TypeScript | C# | Java |
|---|---|---|---|---|
| API Style | create(Class)(args) |
create(Class)(args) |
Create<T>(args) |
create(Class.class).with(args) |
| Type Safety | Type hints | Compile-time | Generics | Generics |
| Interface Mapping | Duck typing (not needed) | Duck typing (not needed) | Explicit mapping | Explicit mapping |
| Test Isolation | Context managers | Manual cleanup | Manual cleanup | Manual cleanup |
| Parameter Handling | *args, **kwargs |
Type inference | Reflection | Reflection |
Why Python's Implementation is Special
- Duck Typing: No complex interface-to-implementation mapping
- Context Managers: Built-in test isolation pattern
- Flexible Parameters:
*args, **kwargshandle any constructor signature - Introspection: Rich parameter tracking without reflection complexity
- Pythonic: Snake case naming, protocols, and idiomatic patterns
Contributing
See the main SpecRec repository for contribution guidelines.
License
This project is licensed under the PolyForm Noncommercial License 1.0.0. See LICENSE.md for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file specrec_python-0.1.0.tar.gz.
File metadata
- Download URL: specrec_python-0.1.0.tar.gz
- Upload date:
- Size: 18.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de02993f0ca2a8f0609dd00ceb114275dc4cd9e7d3358db223b6bd0db4c2046d
|
|
| MD5 |
4b9fe472acd0f0fa40817ee7ce3feb66
|
|
| BLAKE2b-256 |
f4bf5491b60d3788c6e24e1e67b4dbac2efa2485b55082fe6ce48e4229edbe8f
|
File details
Details for the file specrec_python-0.1.0-py3-none-any.whl.
File metadata
- Download URL: specrec_python-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ef1c19b5cdc925b3a4627779f59121e825858e7507818fa0e22456d8dc6250f
|
|
| MD5 |
1a60064edb9d7b44b8220f48a99cbbf7
|
|
| BLAKE2b-256 |
5b9ca2dcf96fb758d84cb67c4d2d0c64d220e68633564743ba80d9ed95763a2e
|