Skip to content

Commit 271572e

Browse files
committed
1.2.0 tests, cleanup, formatting
1 parent 7326154 commit 271572e

File tree

7 files changed

+101
-319
lines changed

7 files changed

+101
-319
lines changed

example/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@pytest.fixture
1010
def client():
1111
"""Standard FastAPI test client fixture.
12-
12+
1313
The pytest-api-cov plugin will automatically discover this fixture,
1414
extract the app from it, and wrap it with coverage tracking.
1515
"""

src/pytest_api_cov/cli.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import argparse
44
import sys
5-
from pathlib import Path
6-
from typing import Optional, Tuple
75

86

97
def generate_conftest_content(framework: str, file_path: str, app_variable: str) -> str:

src/pytest_api_cov/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ class ApiCoverageReportConfig(BaseModel):
2121
report_path: Optional[str] = Field(None, alias="api-cov-report-path")
2222
force_sugar: bool = Field(default=False, alias="api-cov-force-sugar")
2323
force_sugar_disabled: bool = Field(default=False, alias="api-cov-force-sugar-disabled")
24-
client_fixture_names: List[str] = Field(["client", "test_client", "api_client", "app_client"], alias="api-cov-client-fixture-names")
24+
client_fixture_names: List[str] = Field(
25+
["client", "test_client", "api_client", "app_client"], alias="api-cov-client-fixture-names"
26+
)
2527
group_methods_by_endpoint: bool = Field(default=False, alias="api-cov-group-methods-by-endpoint")
2628

2729

src/pytest_api_cov/plugin.py

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""pytest plugin for API coverage tracking."""
22

33
import logging
4-
from typing import Any, Optional
4+
from typing import Any, Optional, Tuple
55

66
import pytest
77

@@ -27,6 +27,7 @@ def is_supported_framework(app: Any) -> bool:
2727
or (module_name == "fastapi" and app_type == "FastAPI")
2828
)
2929

30+
3031
def extract_app_from_client(client: Any) -> Optional[Any]:
3132
"""Extract app from various client types."""
3233
# Typical attributes used by popular clients
@@ -46,10 +47,11 @@ def extract_app_from_client(client: Any) -> Optional[Any]:
4647

4748
# Flask's test client may expose the application via "application" or "app"
4849
if hasattr(client, "_app"):
49-
return getattr(client, "_app")
50+
return client._app
5051

5152
return None
5253

54+
5355
def pytest_addoption(parser: pytest.Parser) -> None:
5456
"""Add API coverage flags to the pytest parser."""
5557
add_pytest_api_cov_flags(parser)
@@ -137,7 +139,9 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
137139
try:
138140
app = request.getfixturevalue("app")
139141
except pytest.FixtureLookupError:
140-
logger.warning(f"> Coverage not enabled and no existing fixture available for '{fixture_name}', returning None")
142+
logger.warning(
143+
f"> Coverage not enabled and no existing fixture available for '{fixture_name}', returning None"
144+
)
141145
yield None
142146
return
143147
# if we have an app, attempt to create a tracked client using adapter without recorder
@@ -146,11 +150,12 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
146150

147151
adapter = get_framework_adapter(app)
148152
client = adapter.get_tracked_client(None, request.node.name)
149-
yield client
150-
return
151-
except Exception:
153+
except Exception: # noqa: BLE001
152154
yield existing_client
153155
return
156+
else:
157+
yield client
158+
return
154159

155160
# At this point coverage is enabled and coverage_data exists
156161
if existing_client is None:
@@ -187,7 +192,9 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
187192
for endpoint_method in endpoints:
188193
method, path = endpoint_method.split(" ", 1)
189194
coverage_data.add_discovered_endpoint(path, method, f"{framework_name.lower()}_adapter")
190-
logger.info(f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints when creating '{fixture_name}'.")
195+
logger.info(
196+
f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints when creating '{fixture_name}'."
197+
)
191198
except Exception as e: # noqa: BLE001
192199
logger.warning(f"> pytest-api-coverage: Could not discover endpoints from app. Error: {e}")
193200

@@ -203,13 +210,17 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
203210

204211
adapter = get_framework_adapter(app)
205212
client = adapter.get_tracked_client(coverage_data.recorder, request.node.name)
206-
yield client
207-
return
208213
except Exception as e: # noqa: BLE001
209214
logger.warning(f"> Failed to create tracked client for '{fixture_name}': {e}")
215+
else:
216+
yield client
217+
return
210218

211219
# Last resort: yield None but do not skip
212-
logger.warning(f"> create_coverage_fixture('{fixture_name}') could not provide a client; tests will run without API coverage for this fixture.")
220+
logger.warning(
221+
f"> create_coverage_fixture('{fixture_name}') could not provide a client; "
222+
"tests will run without API coverage for this fixture."
223+
)
213224
yield None
214225

215226
fixture_func.__name__ = fixture_name
@@ -225,7 +236,7 @@ class CoverageWrapper:
225236
def __init__(self, wrapped_client: Any) -> None:
226237
self._wrapped = wrapped_client
227238

228-
def _extract_path_and_method(self, name: str, args: Any, kwargs: Any) -> Optional[tuple]:
239+
def _extract_path_and_method(self, name: str, args: Any, kwargs: Any) -> Optional[Tuple[str, str]]:
229240
# Try several strategies to obtain a path and method
230241
path = None
231242
method = None
@@ -243,9 +254,10 @@ def _extract_path_and_method(self, name: str, args: Any, kwargs: Any) -> Optiona
243254
try:
244255
path = first.url.path
245256
method = getattr(first, "method", name).upper()
246-
return path, method
247-
except Exception:
257+
except Exception: # noqa: BLE001
248258
pass
259+
else:
260+
return path, method
249261

250262
# Try kwargs-based FlaskClient open signature
251263
if kwargs:
@@ -272,7 +284,8 @@ def tracked_method(*args: Any, **kwargs: Any) -> Any:
272284

273285
return tracked_method
274286

275-
elif name == "open":
287+
if name == "open":
288+
276289
def tracked_open(*args: Any, **kwargs: Any) -> Any:
277290
response = attr(*args, **kwargs)
278291
if recorder is not None:
@@ -285,6 +298,7 @@ def tracked_open(*args: Any, **kwargs: Any) -> Any:
285298
return tracked_open
286299

287300
return attr
301+
288302
return CoverageWrapper(client)
289303

290304

@@ -300,30 +314,48 @@ def coverage_client(request: pytest.FixtureRequest) -> Any:
300314
coverage_data = getattr(session, "api_coverage_data", None)
301315
if coverage_data is None:
302316
pytest.skip("API coverage data not initialized. This should not happen.")
303-
317+
304318
client = None
305319
for fixture_name in config.client_fixture_names:
306320
try:
307321
client = request.getfixturevalue(fixture_name)
308322
logger.info(f"> Found custom fixture '{fixture_name}', wrapping with coverage tracking")
309323
break
310324
except pytest.FixtureLookupError:
311-
logger.warning(f"> Custom fixture '{fixture_name}' not found, trying next one")
325+
logger.debug(f"> Custom fixture '{fixture_name}' not found, trying next one")
312326
continue
313-
327+
328+
if client is None:
329+
# Try to fallback to an 'app' fixture and create a tracked client
330+
try:
331+
app = request.getfixturevalue("app")
332+
logger.info("> Found 'app' fixture, creating tracked client from app")
333+
from .frameworks import get_framework_adapter
334+
335+
adapter = get_framework_adapter(app)
336+
client = adapter.get_tracked_client(coverage_data.recorder, request.node.name)
337+
except pytest.FixtureLookupError:
338+
logger.warning("> No test client fixture found and no 'app' fixture available. Falling back to None")
339+
client = None
340+
except Exception as e: # noqa: BLE001
341+
logger.warning(f"> Failed to create tracked client from 'app' fixture: {e}")
342+
client = None
343+
314344
if client is None:
315-
logger.warning("> No test client fixture found, skipping coverage tracking")
345+
logger.warning("> Coverage client could not be created; tests will run without API coverage for this session.")
316346
return None
317347

318348
app = extract_app_from_client(client)
319349
logger.debug(f"> Extracted app from client: {app}, app type: {type(app).__name__ if app else None}")
320350

321351
if app is None:
322-
logger.warning("> No app found, skipping coverage tracking")
352+
logger.warning("> No app found, returning client without coverage tracking")
323353
return client
324354

325355
if not is_supported_framework(app):
326-
logger.warning(f"> Unsupported framework: {type(app).__name__}. pytest-api-coverage supports Flask and FastAPI.")
356+
logger.warning(
357+
f"> Unsupported framework: {type(app).__name__}. pytest-api-coverage supports Flask and FastAPI."
358+
)
327359
return client
328360

329361
try:
@@ -348,9 +380,6 @@ def coverage_client(request: pytest.FixtureRequest) -> Any:
348380
except Exception as e: # noqa: BLE001
349381
logger.warning(f"> pytest-api-coverage: Could not discover endpoints. Error: {e}")
350382
return client
351-
352-
else:
353-
logger.debug(f"> Endpoints already discovered: {len(coverage_data.discovered_endpoints.endpoints)}")
354383

355384
return wrap_client_with_coverage(client, coverage_data.recorder, request.node.name)
356385

tests/integration/test_plugin_integration.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def test_with_custom_client(coverage_client):
9999

100100
result = pytester.runpytest(
101101
"--api-cov-report",
102-
"--api-cov-client-fixture-name=my_custom_client",
102+
"--api-cov-client-fixture-names=my_custom_client",
103103
"--api-cov-show-covered-endpoints",
104104
)
105105

@@ -150,7 +150,7 @@ def test_with_custom_client(coverage_client):
150150

151151
result = pytester.runpytest(
152152
"--api-cov-report",
153-
"--api-cov-client-fixture-name=my_api_client",
153+
"--api-cov-client-fixture-names=my_api_client",
154154
"--api-cov-show-covered-endpoints",
155155
)
156156

@@ -188,7 +188,7 @@ def test_root(coverage_client):
188188

189189
result = pytester.runpytest(
190190
"--api-cov-report",
191-
"--api-cov-client-fixture-name=nonexistent_fixture",
191+
"--api-cov-client-fixture-names=nonexistent_fixture",
192192
)
193193

194194
assert result.ret == 0
@@ -268,6 +268,14 @@ def from_app():
268268
def from_main():
269269
return "From main.py"
270270
""",
271+
"conftest.py": """
272+
import pytest
273+
from app import app as app_instance
274+
275+
@pytest.fixture
276+
def app():
277+
return app_instance
278+
""",
271279
"test_multiple.py": """
272280
def test_endpoint(coverage_client):
273281
response = coverage_client.get("/from-app-py")

0 commit comments

Comments
 (0)