Skip to content

Commit 0767cec

Browse files
committed
cleanup, format
1 parent ffa66ab commit 0767cec

File tree

16 files changed

+266
-401
lines changed

16 files changed

+266
-401
lines changed

src/pytest_api_cov/cli.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""pytest-api-cov CLI commands for setup and configuration."""
1+
"""CLI commands for setup and configuration."""
22

33
import argparse
44
import os
@@ -22,19 +22,16 @@ def detect_framework_and_app() -> Optional[tuple[str, str, str]]:
2222
for filename, attr_names in common_patterns:
2323
if os.path.exists(filename):
2424
try:
25-
# Read file to detect framework
2625
with open(filename, "r") as f:
2726
content = f.read()
2827

29-
# Simple detection based on imports
3028
if "from fastapi import" in content or "import fastapi" in content:
3129
framework = "FastAPI"
3230
elif "from flask import" in content or "import flask" in content:
3331
framework = "Flask"
3432
else:
3533
continue
3634

37-
# Look for app variable in file
3835
for attr_name in attr_names:
3936
if f"{attr_name} = " in content:
4037
return framework, filename, attr_name
@@ -97,14 +94,12 @@ def cmd_init():
9794
print("🚀 pytest-api-cov Setup Wizard")
9895
print("=" * 40)
9996

100-
# Detect existing setup
10197
detection_result = detect_framework_and_app()
10298

10399
if detection_result:
104100
framework, file_path, app_variable = detection_result
105101
print(f"✅ Detected {framework} app in {file_path} (variable: {app_variable})")
106102

107-
# Check if conftest.py already exists
108103
conftest_exists = os.path.exists("conftest.py")
109104
if conftest_exists:
110105
print("⚠️ conftest.py already exists")
@@ -118,7 +113,6 @@ def cmd_init():
118113
f.write(conftest_content)
119114
print("✅ Created conftest.py")
120115

121-
# Check if pyproject.toml exists
122116
pyproject_exists = os.path.exists("pyproject.toml")
123117
if pyproject_exists:
124118
print("ℹ️ pyproject.toml already exists")
@@ -183,7 +177,6 @@ def main():
183177

184178
subparsers = parser.add_subparsers(dest="command", help="Available commands")
185179

186-
# Init command
187180
subparsers.add_parser("init", help="Initialize pytest-api-cov setup")
188181

189182
args = parser.parse_args()

src/pytest_api_cov/config.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""src/pytest_api_cov/config.py: Handles the configuration for the API coverage report."""
1+
"""Configuration handling for the API coverage report."""
22

33
import sys
44
from typing import Any, Dict, List, Optional
@@ -8,7 +8,7 @@
88

99

1010
class ApiCoverageReportConfig(BaseModel):
11-
"""A Pydantic model for all API coverage configuration."""
11+
"""Configuration model for API coverage reporting."""
1212

1313
model_config = ConfigDict(populate_by_name=True)
1414

@@ -33,7 +33,7 @@ def read_toml_config() -> Dict[str, Any]:
3333

3434

3535
def read_session_config(session_config: Any) -> Dict[str, Any]:
36-
"""Read configuration from the pytest session config (command-line flags)."""
36+
"""Read configuration from pytest session config (command-line flags)."""
3737
cli_options = {
3838
"api-cov-fail-under": "fail_under",
3939
"api-cov-show-uncovered-endpoints": "show_uncovered_endpoints",
@@ -47,7 +47,6 @@ def read_session_config(session_config: Any) -> Dict[str, Any]:
4747
config = {}
4848
for opt, key in cli_options.items():
4949
value = session_config.getoption(f"--{opt}")
50-
# We only want to include options that were actually provided or are booleans
5150
if value is not None and value != [] and value is not False:
5251
config[key] = value
5352
return config
@@ -68,10 +67,8 @@ def get_pytest_api_cov_report_config(session_config: Any) -> ApiCoverageReportCo
6867
toml_config = read_toml_config()
6968
cli_config = read_session_config(session_config)
7069

71-
# Start with defaults, then apply TOML, then apply CLI args
7270
final_config = {**toml_config, **cli_config}
7371

74-
# Set force_sugar based on environment if not explicitly set
7572
if final_config.get("force_sugar_disabled"):
7673
final_config["force_sugar"] = False
7774
elif "force_sugar" not in final_config:

src/pytest_api_cov/frameworks.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
"""src/pytest_api_cov/frameworks.py"""
1+
"""Framework adapters for Flask and FastAPI."""
22

33
from typing import TYPE_CHECKING, Any, List, Optional
44

55
if TYPE_CHECKING:
66
from .models import ApiCallRecorder
77

88

9-
# --- Base Adapter ---
109
class BaseAdapter:
1110
def __init__(self, app: Any):
1211
self.app = app
@@ -20,10 +19,8 @@ def get_tracked_client(self, recorder: Optional["ApiCallRecorder"], test_name: s
2019
raise NotImplementedError
2120

2221

23-
# --- Flask Adapter ---
2422
class FlaskAdapter(BaseAdapter):
2523
def get_endpoints(self) -> List[str]:
26-
# Exclude static and other non-API endpoints
2724
excluded_rules = ("/static/<path:filename>",)
2825
return sorted([rule.rule for rule in self.app.url_map.iter_rules() if rule.rule not in excluded_rules])
2926

@@ -38,20 +35,16 @@ def open(self, *args, **kwargs) -> Any:
3835
path = kwargs.get("path") or (args[0] if args else None)
3936
if path and hasattr(self.application.url_map, "bind"):
4037
try:
41-
# Fetch the endpoint *name*, e.g., 'root' not '/'
4238
endpoint_name, _ = self.application.url_map.bind("").match(path, method=kwargs.get("method"))
43-
# Find the rule object associated with that endpoint name
4439
endpoint_rule_string = next(self.application.url_map.iter_rules(endpoint_name)).rule
4540
recorder.record_call(endpoint_rule_string, test_name) # type: ignore[union-attr]
4641
except Exception:
47-
# Fallback for paths that might not match a rule
4842
pass
4943
return super().open(*args, **kwargs)
5044

5145
return TrackingFlaskClient(self.app, self.app.response_class)
5246

5347

54-
# --- FastAPI Adapter ---
5548
class FastAPIAdapter(BaseAdapter):
5649
def get_endpoints(self) -> List[str]:
5750
from fastapi.routing import APIRoute
@@ -64,17 +57,15 @@ def get_tracked_client(self, recorder: Optional["ApiCallRecorder"], test_name: s
6457
if recorder is None:
6558
return TestClient(self.app)
6659

67-
# FastAPI patches the 'send' method of the underlying client
6860
class TrackingFastAPIClient(TestClient):
6961
def send(self, *args, **kwargs) -> Any:
7062
request = args[0]
71-
recorder.record_call(request.url.path, test_name) # type: ignore[union-attr]
63+
recorder.record_call(request.url.path, test_name)
7264
return super().send(*args, **kwargs)
7365

7466
return TrackingFastAPIClient(self.app)
7567

7668

77-
# --- Factory Function ---
7869
def get_framework_adapter(app: Any) -> BaseAdapter:
7970
"""Detects the framework and returns the appropriate adapter."""
8071
app_type = type(app).__name__

src/pytest_api_cov/models.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Data models for pytest-api-cov."""
22

3-
from collections import defaultdict
43
from typing import Any, Dict, Iterator, List, Set, Tuple
54

65
from pydantic import BaseModel, Field
@@ -11,7 +10,6 @@ class ApiCallRecorder(BaseModel):
1110

1211
model_config = {"arbitrary_types_allowed": True}
1312

14-
# Map of endpoint paths to sets of test function names that called them
1513
calls: Dict[str, Set[str]] = Field(default_factory=dict)
1614

1715
def record_call(self, endpoint: str, test_name: str) -> None:
@@ -70,7 +68,7 @@ class EndpointDiscovery(BaseModel):
7068
"""Model for discovered API endpoints."""
7169

7270
endpoints: List[str] = Field(default_factory=list)
73-
discovery_source: str = Field(default="unknown") # e.g., "flask_adapter", "fastapi_adapter"
71+
discovery_source: str = Field(default="unknown")
7472

7573
def add_endpoint(self, endpoint: str) -> None:
7674
"""Add a discovered endpoint."""
@@ -86,7 +84,7 @@ def __len__(self) -> int:
8684
"""Return number of discovered endpoints."""
8785
return len(self.endpoints)
8886

89-
def __iter__(self) -> Iterator[str]:
87+
def __iter__(self) -> Iterator[str]: # type: ignore[override]
9088
"""Iterate over discovered endpoints."""
9189
return iter(self.endpoints)
9290

@@ -109,20 +107,16 @@ def add_discovered_endpoint(self, endpoint: str, source: str = "unknown") -> Non
109107

110108
def merge_worker_data(self, worker_recorder: Dict[str, Any], worker_endpoints: List[str]) -> None:
111109
"""Merge data from a worker process."""
112-
# Handle recorder data
113110
if isinstance(worker_recorder, dict):
114-
# Check if all values are lists (serializable format)
115111
all_lists = worker_recorder and all(isinstance(v, list) for v in worker_recorder.values())
116112
if all_lists:
117113
worker_api_recorder = ApiCallRecorder.from_serializable(worker_recorder)
118114
else:
119-
# Handle raw dict format or mixed types
120115
calls = {k: set(v) if isinstance(v, (list, set)) else {v} for k, v in worker_recorder.items()}
121116
worker_api_recorder = ApiCallRecorder(calls=calls)
122117

123118
self.recorder.merge(worker_api_recorder)
124119

125-
# Handle discovered endpoints
126120
if worker_endpoints:
127121
worker_discovery = EndpointDiscovery(endpoints=worker_endpoints, discovery_source="worker")
128122
self.discovered_endpoints.merge(worker_discovery)

src/pytest_api_cov/plugin.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""src/pytest_api_cov/plugin.py"""
1+
"""pytest plugin for API coverage tracking."""
22

33
import importlib
44
import importlib.util
@@ -32,7 +32,6 @@ def auto_discover_app() -> Optional[Any]:
3232
"""Automatically discover Flask/FastAPI apps in common locations."""
3333
logger.debug("> Auto-discovering app in common locations...")
3434

35-
# Common file patterns and variable names to check
3635
common_patterns = [
3736
("app.py", ["app", "application", "main"]),
3837
("main.py", ["app", "application", "main"]),
@@ -45,14 +44,12 @@ def auto_discover_app() -> Optional[Any]:
4544
if os.path.exists(filename):
4645
logger.debug(f"> Found {filename}, checking for app variables...")
4746
try:
48-
# Import the module
49-
module_name = filename[:-3] # Remove .py extension
47+
module_name = filename[:-3] # .py extension
5048
spec = importlib.util.spec_from_file_location(module_name, filename)
5149
if spec and spec.loader:
5250
module = importlib.util.module_from_spec(spec)
5351
spec.loader.exec_module(module)
5452

55-
# Check each possible app variable name
5653
for attr_name in attr_names:
5754
if hasattr(module, attr_name):
5855
app = getattr(module, attr_name)
@@ -112,22 +109,18 @@ def pytest_addoption(parser: pytest.Parser) -> None:
112109

113110
def pytest_configure(config: pytest.Config) -> None:
114111
"""Configure the pytest session and logging."""
115-
# Configure logging based on verbosity level
116112
if config.getoption("--api-cov-report"):
117113
verbosity = config.option.verbose
118114

119-
# Set up logging level based on pytest verbosity
120115
if verbosity >= 2: # -vv or more
121116
log_level = logging.DEBUG
122117
elif verbosity >= 1: # -v
123118
log_level = logging.INFO
124-
else: # normal run
119+
else:
125120
log_level = logging.WARNING
126121

127-
# Configure the logger
128122
logger.setLevel(log_level)
129123

130-
# Only add handler if we don't already have one
131124
if not logger.handlers:
132125
handler = logging.StreamHandler()
133126
handler.setLevel(log_level)
@@ -137,7 +130,6 @@ def pytest_configure(config: pytest.Config) -> None:
137130

138131
logger.info("Initializing API coverage plugin...")
139132

140-
# Register xdist plugin if available
141133
if config.pluginmanager.hasplugin("xdist"):
142134
config.pluginmanager.register(DeferXdistPlugin(), "defer_xdist_plugin")
143135

@@ -157,11 +149,9 @@ def client(request: pytest.FixtureRequest) -> Any:
157149
"""
158150
session = request.node.session
159151

160-
# Only proceed if API coverage is enabled
161152
if not session.config.getoption("--api-cov-report"):
162153
pytest.skip("API coverage not enabled. Use --api-cov-report flag.")
163154

164-
# Try to get app from existing fixture first
165155
app = None
166156
try:
167157
app = request.getfixturevalue("app")
@@ -170,13 +160,11 @@ def client(request: pytest.FixtureRequest) -> Any:
170160
logger.debug("> No 'app' fixture found, trying auto-discovery...")
171161
app = auto_discover_app()
172162

173-
# If still no app found, show helpful error
174163
if app is None:
175164
helpful_msg = get_helpful_error_message()
176165
print(helpful_msg)
177166
pytest.skip("No API app found. See error message above for setup guidance.")
178167

179-
# Validate the app is supported
180168
if not is_supported_framework(app):
181169
pytest.skip(f"Unsupported framework: {type(app).__name__}. pytest-api-coverage supports Flask and FastAPI.")
182170

@@ -189,7 +177,6 @@ def client(request: pytest.FixtureRequest) -> Any:
189177
if coverage_data is None:
190178
pytest.skip("API coverage data not initialized. This should not happen.")
191179

192-
# Discover endpoints on the first run of this fixture.
193180
if not coverage_data.discovered_endpoints.endpoints:
194181
try:
195182
endpoints = adapter.get_endpoints()
@@ -215,15 +202,13 @@ def pytest_sessionfinish(session: pytest.Session) -> None:
215202

216203
logger.debug(f"> pytest-api-coverage: Generating report for {len(coverage_data.recorder)} recorded endpoints.")
217204
if hasattr(session.config, "workeroutput"):
218-
# Send data to master process in serializable format
219205
serializable_recorder = coverage_data.recorder.to_serializable()
220206
session.config.workeroutput["api_call_recorder"] = serializable_recorder
221207
session.config.workeroutput["discovered_endpoints"] = coverage_data.discovered_endpoints.endpoints
222208
logger.debug("> Sent API call data and discovered endpoints to master process")
223209
else:
224210
logger.debug("> No workeroutput found, generating report for master data.")
225211

226-
# Get worker data from config if available
227212
worker_recorder_data = getattr(session.config, "worker_api_call_recorder", {})
228213
worker_endpoints = getattr(session.config, "worker_discovered_endpoints", [])
229214

@@ -247,7 +232,6 @@ def pytest_sessionfinish(session: pytest.Session) -> None:
247232
if hasattr(session, "api_coverage_data"):
248233
delattr(session, "api_coverage_data")
249234

250-
# Clear any module-level caches
251235
if hasattr(session.config, "worker_api_call_recorder"):
252236
delattr(session.config, "worker_api_call_recorder")
253237

@@ -281,7 +265,6 @@ def pytest_testnodedown(self, node: Any) -> None:
281265
node.config.worker_api_call_recorder = current
282266
logger.debug(f"> Updated current data: {current}")
283267

284-
# Merge discovered endpoints (take the first non-empty list we get)
285268
if discovered_endpoints and not getattr(node.config, "worker_discovered_endpoints", []):
286269
node.config.worker_discovered_endpoints = discovered_endpoints
287270
logger.debug(f"> Set discovered endpoints from worker: {discovered_endpoints}")

src/pytest_api_cov/pytest_flags.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""src/pytest_api_cov/pytest_flags.py"""
1+
"""pytest flag configuration for API coverage."""
22

33
from typing import TYPE_CHECKING
44

0 commit comments

Comments
 (0)