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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions