Skip to content

Commit f7aa722

Browse files
authored
249 raise doctest (#524)
* Update docstring for wrapper.py Signed-off-by: Mihai Criveti <[email protected]> * Update cli.py doctest Signed-off-by: Mihai Criveti <[email protected]> * Update doctest for translate.py Signed-off-by: Mihai Criveti <[email protected]> * Update doctest for gateway_service.py Signed-off-by: Mihai Criveti <[email protected]> * Update doctest for retry_manager.py Signed-off-by: Mihai Criveti <[email protected]> * Update doctest for db.py Signed-off-by: Mihai Criveti <[email protected]> * Update doctest for resource_cache.py Signed-off-by: Mihai Criveti <[email protected]> * Cleanup coverage annotated files Signed-off-by: Mihai Criveti <[email protected]> * Improve coverage for verify_credentials.py Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]>
1 parent c6f1ede commit f7aa722

File tree

15 files changed

+1783
-275
lines changed

15 files changed

+1783
-275
lines changed

Makefile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ FILES_TO_CLEAN := .coverage coverage.xml mcp.prof mcp.pstats \
3838
$(DOCS_DIR)/docs/test/sbom.md \
3939
$(DOCS_DIR)/docs/test/{unittest,full,index,test}.md \
4040
$(DOCS_DIR)/docs/images/coverage.svg $(LICENSES_MD) $(METRICS_MD) \
41-
*.db *.sqlite *.sqlite3 mcp.db-journal
41+
*.db *.sqlite *.sqlite3 mcp.db-journal *.py,cover
4242

4343
COVERAGE_DIR ?= $(DOCS_DIR)/docs/coverage
4444
LICENSES_MD ?= $(DOCS_DIR)/docs/test/licenses.md
@@ -193,6 +193,8 @@ clean:
193193
@rm -f $(FILES_TO_CLEAN)
194194
@# Delete Python bytecode
195195
@find . -name '*.py[cod]' -delete
196+
@# Delete coverage annotated files
197+
@find . -name '*.py,cover' -delete
196198
@echo "✅ Clean complete."
197199

198200

@@ -202,7 +204,7 @@ clean:
202204
# help: 🧪 TESTING
203205
# help: smoketest - Run smoketest.py --verbose (build container, add MCP server, test endpoints)
204206
# help: test - Run unit tests with pytest
205-
# help: coverage - Run tests with coverage, emit md/HTML/XML + badge
207+
# help: coverage - Run tests with coverage, emit md/HTML/XML + badge, generate annotated files
206208
# help: htmlcov - (re)build just the HTML coverage report into docs
207209
# help: test-curl - Smoke-test API endpoints with curl script
208210
# help: pytest-examples - Run README / examples through pytest-examples
@@ -243,7 +245,9 @@ coverage:
243245
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage html -d $(COVERAGE_DIR) --include=app/*"
244246
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage xml"
245247
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage-badge -fo $(DOCS_DIR)/docs/images/coverage.svg"
246-
@echo "✅ Coverage artefacts: md, HTML in $(COVERAGE_DIR), XML & badge ✔"
248+
@echo "🔍 Generating annotated coverage files..."
249+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage annotate -d ."
250+
@echo "✅ Coverage artefacts: md, HTML in $(COVERAGE_DIR), XML, badge & annotated files (.py,cover) ✔"
247251

248252
htmlcov:
249253
@echo "📊 Generating HTML coverage report..."

mcpgateway/cache/resource_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
>>> cache.get('a')
2020
1
2121
>>> import time
22-
>>> time.sleep(1.1)
22+
>>> time.sleep(1.2)
2323
>>> cache.get('a') is None
2424
True
2525
>>> cache.set('a', 1)
@@ -71,7 +71,7 @@ class ResourceCache:
7171
>>> cache.get('a')
7272
1
7373
>>> import time
74-
>>> time.sleep(1.1)
74+
>>> time.sleep(1.2)
7575
>>> cache.get('a') is None
7676
True
7777
>>> cache.set('a', 1)

mcpgateway/cli.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ def _needs_app(arg_list: List[str]) -> bool:
7272
7373
Returns:
7474
bool: Returns *True* when the CLI invocation has *no* positional APP path
75+
76+
Examples:
77+
>>> _needs_app([])
78+
True
79+
>>> _needs_app(["--reload"])
80+
True
81+
>>> _needs_app(["myapp.main:app"])
82+
False
7583
"""
7684

7785
return len(arg_list) == 0 or arg_list[0].startswith("-")
@@ -85,6 +93,14 @@ def _insert_defaults(raw_args: List[str]) -> List[str]:
8593
8694
Returns:
8795
List[str]: List of arguments
96+
97+
Examples:
98+
>>> result = _insert_defaults([])
99+
>>> result[0]
100+
'mcpgateway.main:app'
101+
>>> result = _insert_defaults(["myapp.main:app", "--reload"])
102+
>>> result[0]
103+
'myapp.main:app'
88104
"""
89105

90106
args = list(raw_args) # shallow copy - we'll mutate this
@@ -109,7 +125,15 @@ def _insert_defaults(raw_args: List[str]) -> List[str]:
109125

110126

111127
def main() -> None: # noqa: D401 - imperative mood is fine here
112-
"""Entry point for the *mcpgateway* console script (delegates to Uvicorn)."""
128+
"""Entry point for the *mcpgateway* console script (delegates to Uvicorn).
129+
130+
Processes command line arguments, handles version requests, and forwards
131+
all other arguments to Uvicorn with sensible defaults injected.
132+
133+
Environment Variables:
134+
MCG_HOST: Default host (default: "127.0.0.1")
135+
MCG_PORT: Default port (default: "4444")
136+
"""
113137

114138
# Check for version flag
115139
if "--version" in sys.argv or "-V" in sys.argv:

mcpgateway/db.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
>>> from mcpgateway.db import connect_args
1818
>>> isinstance(connect_args, dict)
1919
True
20+
>>> 'keepalives' in connect_args or 'check_same_thread' in connect_args or len(connect_args) == 0
21+
True
2022
"""
2123

2224
# Standard
@@ -126,6 +128,10 @@ def utc_now() -> datetime:
126128
>>> now = utc_now()
127129
>>> now.tzinfo is not None
128130
True
131+
>>> str(now.tzinfo)
132+
'UTC'
133+
>>> isinstance(now, datetime)
134+
True
129135
"""
130136
return datetime.now(timezone.utc)
131137

@@ -624,6 +630,28 @@ def content(self) -> ResourceContent:
624630
625631
Raises:
626632
ValueError: If the resource has no content available.
633+
634+
Examples:
635+
>>> resource = Resource(uri="test://example", name="test")
636+
>>> resource.text_content = "Hello, World!"
637+
>>> content = resource.content
638+
>>> content.text
639+
'Hello, World!'
640+
>>> content.type
641+
'resource'
642+
643+
>>> binary_resource = Resource(uri="test://binary", name="binary")
644+
>>> binary_resource.binary_content = b"\\x00\\x01\\x02"
645+
>>> binary_content = binary_resource.content
646+
>>> binary_content.blob
647+
b'\\x00\\x01\\x02'
648+
649+
>>> empty_resource = Resource(uri="test://empty", name="empty")
650+
>>> try:
651+
... empty_resource.content
652+
... except ValueError as e:
653+
... str(e)
654+
'Resource has no content'
627655
"""
628656

629657
if self.text_content is not None:
@@ -801,6 +829,24 @@ def validate_arguments(self, args: Dict[str, str]) -> None:
801829
Raises:
802830
ValueError: If the arguments do not conform to the schema.
803831
832+
Examples:
833+
>>> prompt = Prompt(
834+
... name="test_prompt",
835+
... template="Hello {name}",
836+
... argument_schema={
837+
... "type": "object",
838+
... "properties": {
839+
... "name": {"type": "string"}
840+
... },
841+
... "required": ["name"]
842+
... }
843+
... )
844+
>>> prompt.validate_arguments({"name": "Alice"}) # No exception
845+
>>> try:
846+
... prompt.validate_arguments({"age": 25}) # Missing required field
847+
... except ValueError as e:
848+
... "name" in str(e)
849+
True
804850
"""
805851
try:
806852
jsonschema.validate(args, self.argument_schema)
@@ -980,6 +1026,18 @@ def failure_rate(self) -> float:
9801026
9811027
Returns:
9821028
float: The failure rate as a value between 0 and 1.
1029+
1030+
Examples:
1031+
>>> tool = Tool(original_name="test_tool", original_name_slug="test-tool", input_schema={})
1032+
>>> tool.failure_rate # No metrics yet
1033+
0.0
1034+
>>> tool.metrics = [
1035+
... ToolMetric(tool_id=tool.id, response_time=1.0, is_success=True),
1036+
... ToolMetric(tool_id=tool.id, response_time=2.0, is_success=False),
1037+
... ToolMetric(tool_id=tool.id, response_time=1.5, is_success=True),
1038+
... ]
1039+
>>> tool.failure_rate
1040+
0.3333333333333333
9831041
"""
9841042
total: int = self.execution_count
9851043
if total == 0:
@@ -1225,6 +1283,16 @@ def get_db():
12251283
12261284
Yields:
12271285
SessionLocal: A SQLAlchemy database session.
1286+
1287+
Examples:
1288+
>>> from mcpgateway.db import get_db
1289+
>>> gen = get_db()
1290+
>>> db = next(gen)
1291+
>>> hasattr(db, 'query')
1292+
True
1293+
>>> hasattr(db, 'commit')
1294+
True
1295+
>>> gen.close()
12281296
"""
12291297
db = SessionLocal()
12301298
try:

mcpgateway/main.py

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -267,19 +267,22 @@ async def validation_exception_handler(_request: Request, exc: ValidationError):
267267
validation error details.
268268
269269
Examples:
270-
>>> # This handler is automatically invoked by FastAPI when a ValidationError occurs
271-
>>> # For example, when request data fails Pydantic model validation:
272-
>>> # POST /tools with invalid data would trigger this handler
273-
>>> # Response format:
274-
>>> # {
275-
>>> # "detail": [
276-
>>> # {
277-
>>> # "loc": ["body", "name"],
278-
>>> # "msg": "field required",
279-
>>> # "type": "value_error.missing"
280-
>>> # }
281-
>>> # ]
282-
>>> # }
270+
>>> from pydantic import ValidationError, BaseModel
271+
>>> from fastapi import Request
272+
>>> import asyncio
273+
>>>
274+
>>> class TestModel(BaseModel):
275+
... name: str
276+
... age: int
277+
>>>
278+
>>> # Create a validation error
279+
>>> try:
280+
... TestModel(name="", age="invalid")
281+
... except ValidationError as e:
282+
... # Test our handler
283+
... result = asyncio.run(validation_exception_handler(None, e))
284+
... result.status_code
285+
422
283286
"""
284287
return JSONResponse(status_code=422, content=ErrorFormatter.format_validation_error(exc))
285288

@@ -445,6 +448,22 @@ def get_db():
445448
446449
Ensures:
447450
The database session is closed after the request completes, even in the case of an exception.
451+
452+
Examples:
453+
>>> # Test that get_db returns a generator
454+
>>> db_gen = get_db()
455+
>>> hasattr(db_gen, '__next__')
456+
True
457+
>>> # Test cleanup happens
458+
>>> try:
459+
... db = next(db_gen)
460+
... type(db).__name__
461+
... finally:
462+
... try:
463+
... next(db_gen)
464+
... except StopIteration:
465+
... pass # Expected - generator cleanup
466+
'Session'
448467
"""
449468
db = SessionLocal()
450469
try:
@@ -454,8 +473,7 @@ def get_db():
454473

455474

456475
def require_api_key(api_key: str) -> None:
457-
"""
458-
Validates the provided API key.
476+
"""Validates the provided API key.
459477
460478
This function checks if the provided API key matches the expected one
461479
based on the settings. If the validation fails, it raises an HTTPException
@@ -466,6 +484,22 @@ def require_api_key(api_key: str) -> None:
466484
467485
Raises:
468486
HTTPException: If the API key is invalid, a 401 Unauthorized error is raised.
487+
488+
Examples:
489+
>>> from mcpgateway.config import settings
490+
>>> settings.auth_required = True
491+
>>> settings.basic_auth_user = "admin"
492+
>>> settings.basic_auth_password = "secret"
493+
>>>
494+
>>> # Valid API key
495+
>>> require_api_key("admin:secret") # Should not raise
496+
>>>
497+
>>> # Invalid API key
498+
>>> try:
499+
... require_api_key("wrong:key")
500+
... except HTTPException as e:
501+
... e.status_code
502+
401
469503
"""
470504
if settings.auth_required:
471505
expected = f"{settings.basic_auth_user}:{settings.basic_auth_password}"
@@ -482,6 +516,17 @@ async def invalidate_resource_cache(uri: Optional[str] = None) -> None:
482516
483517
Args:
484518
uri (Optional[str]): The URI of the resource to invalidate from the cache. If None, the entire cache is cleared.
519+
520+
Examples:
521+
>>> import asyncio
522+
>>> # Test with specific URI
523+
>>> result = asyncio.run(invalidate_resource_cache("/test/uri"))
524+
>>> result is None
525+
True
526+
>>> # Test with no URI (clear all)
527+
>>> result = asyncio.run(invalidate_resource_cache())
528+
>>> result is None
529+
True
485530
"""
486531
if uri:
487532
resource_cache.delete(uri)

0 commit comments

Comments
 (0)