Skip to content

feat: add configurable JsonClient class for dependency injection #19

@ngjunsiang

Description

@ngjunsiang

Feature Request: Configurable JsonClient Class for Dependency Injection

Problem

When testing Campus services that use campus_python.Campus, we need to replace the default CampusRequest with a test-compatible version that routes to Flask test clients instead of making real HTTP requests.

Current Workaround: Monkey-Patching

Currently, we have to monkey-patch the CampusRequest class:

import campus_python
from tests.flask_test import TestCampusRequest

# Replace CampusRequest globally
campus_python.json_client.CampusRequest = TestCampusRequest
campus_python.CampusRequest = TestCampusRequest  # Also patch module reference

Problems with this approach:

  • ❌ Fragile - requires patching multiple module-level references
  • ❌ Hard to debug - changes global state
  • ❌ Confusing - not obvious that CampusRequest has been replaced
  • ❌ Brittle - may break if campus-api-python internals change

Proposed Solution

Add a configurable class attribute to allow dependency injection of the JsonClient class:

class Campus:
    """Unified Campus client interface."""
    
    # Configurable JsonClient class
    json_client_class: type[JsonClient] = CampusRequest
    
    @property
    def auth(self) -> AuthRoot:
        if not hasattr(self, "_auth"):
            # Use json_client_class instead of hardcoded CampusRequest
            self._auth = AuthRoot(
                json_client=self.json_client_class(
                    base_url=base_url,
                    timeout=self.timeout,
                )
            )
        return self._auth
    
    @property
    def api(self) -> ApiRoot:
        if not hasattr(self, "_api"):
            self._api = ApiRoot(
                json_client=self.json_client_class(
                    base_url=base_url,
                    timeout=self.timeout,
                )
            )
        return self._api

Usage in Tests

import campus_python
from tests.flask_test import TestCampusRequest

def setup():
    # Configure campus_python to use test client
    campus_python.Campus.json_client_class = TestCampusRequest
    
    # Now all Campus instances use TestCampusRequest
    campus = campus_python.Campus(timeout=60)
    campus.auth.root.authenticate(...)  # Uses Flask test clients!

Benefits

  1. Clean dependency injection - No monkey-patching required
  2. Explicit configuration - Clear what JsonClient is being used
  3. Backward compatible - Defaults to CampusRequest
  4. Test-friendly - Easy to inject test doubles
  5. Flexible - Allows custom JsonClient implementations for:
    • Testing (Flask test clients)
    • Mocking (for unit tests)
    • Custom HTTP backends (async, retry logic, etc.)

Implementation Details

Changes Required

File: campus_python/__init__.py

  1. Add class attribute:

    class Campus:
        json_client_class: type[JsonClient] = CampusRequest
  2. Replace hardcoded CampusRequest(...) with self.json_client_class(...):

    • In auth property (line ~81)
    • In api property (line ~107)

Example Custom JsonClient

from campus_python.json_client.interface import JsonClient, JsonResponse

class CustomJsonClient(JsonClient):
    """Custom JsonClient with special behavior."""
    
    def __init__(self, base_url: str | None = None, **kwargs):
        self.base_url = base_url or ""
        # ... custom initialization ...
    
    def get(self, path: str, query: dict | None = None) -> JsonResponse:
        # ... custom implementation ...
        pass
    
    # ... implement other methods ...

# Use it
campus_python.Campus.json_client_class = CustomJsonClient

Backward Compatibility

Fully backward compatible - Default value is CampusRequest, so existing code continues to work without changes.

Related

Alternatives Considered

  1. Constructor parameter (Campus(json_client_class=...))

    • ❌ Doesn't work for services that instantiate Campus() internally (campus.auth, campus.api)
  2. Global function (set_json_client_class())

    • ❌ More verbose than class attribute
    • ❌ Requires additional function to maintain
  3. Keep monkey-patching

    • ❌ Fragile and confusing

Implementation Checklist

  • Add json_client_class class attribute to Campus
  • Update auth property to use self.json_client_class
  • Update api property to use self.json_client_class
  • Add docstring explaining the configuration option
  • Add example to README or documentation
  • Release as minor version bump (e.g., v2.1.0)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions