feat: add configurable JsonClient class for dependency injection

Open Beginner friendly
#19 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
api

Research direction

Start in campus_python/init.py, focusing on the Campus class and its auth and api properties. Review tests/flask_test/campus_request.py for the injected client shape, then verify that both properties use the configured class while the default remains CampusRequest; the documentation example and minor-version release are listed as additional checklist items.

Written by the indexing model from the issue text.

Description

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)
Dominant language
Python
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from nyjc-computing/campus-api-python

All issues in nyjc-computing/campus-api-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.