Skip to content

Commit 3638525

Browse files
committed
cli changes etc
1 parent b3569b9 commit 3638525

File tree

9 files changed

+120
-62
lines changed

9 files changed

+120
-62
lines changed

Makefile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,18 @@ clean:
4646

4747
build:
4848
@echo "Building plugin..."
49+
@uv sync
4950
@uv build
5051

5152
publish:
5253
@echo "Publishing plugin..."
53-
# @uv publish --token $(PYPI_TOKEN)
54+
@uv publish --token $(PYPI_TOKEN)
5455

5556
publish-test:
5657
@echo "Publishing plugin to test PyPI..."
57-
@echo $(TEST_PYPI_TOKEN)
58-
@uv publish --token $(TEST_PYPI_TOKEN)
58+
@echo //$(TEST_PYPI_TOKEN)//
59+
@uv publish --token $(TEST_PYPI_TOKEN) --index testpypi
60+
61+
verify-publish:
62+
@echo "Verifying plugin was published to PyPI..."
63+
@uv run --with pytest-api-cov --no-project -- python -c "import pytest_api_cov; print(f'Plugin verified successfully. Version: {pytest_api_cov.__version__}')"

pyproject.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pytest-api-cov"
3-
version = "1.0.0"
3+
version = "0.1.2"
44
description = "Api Coverage Report Pytest Plugin"
55
readme = "README.md"
66
authors = [
@@ -17,12 +17,12 @@ dependencies = [
1717
"rich>=14.0.0",
1818
"starlette>=0.47.1",
1919
"tomli>=2.2.1",
20+
"pytest>=8.4.1",
2021
]
2122

2223
[dependency-groups]
2324
dev = [
2425
"mypy>=1.17.0",
25-
"pytest>=8.4.1",
2626
"pytest-cov>=6.2.1",
2727
"pytest-sugar>=1.0.0",
2828
"pytest-xdist>=3.8.0",
@@ -69,6 +69,12 @@ include = [
6969
"pyproject.toml",
7070
]
7171

72+
# [[tool.uv.index]]
73+
# name = "testpypi"
74+
# url = "https://test.pypi.org/simple/"
75+
# publish-url = "https://test.pypi.org/legacy/"
76+
# explicit = true
77+
7278
[project.entry-points."pytest11"]
7379
pytest_api_cov = "pytest_api_cov.plugin"
7480

src/pytest_api_cov/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
# This file makes the pytest_api_cov directory a Python package
1+
"""init pytest_api_cov."""
2+
3+
try:
4+
from importlib.metadata import version
5+
__version__ = version("pytest-api-cov")
6+
except ImportError:
7+
__version__ = "unknown"

src/pytest_api_cov/cli.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,52 +11,66 @@ def detect_framework_and_app() -> Optional[tuple[str, str, str]]:
1111
Detect framework and app location.
1212
Returns (framework, file_path, app_variable) or None.
1313
"""
14-
common_patterns = [
15-
("app.py", ["app", "application", "main"]),
16-
("main.py", ["app", "application", "main"]),
17-
("server.py", ["app", "application", "server"]),
18-
("wsgi.py", ["app", "application"]),
19-
("asgi.py", ["app", "application"]),
20-
]
21-
22-
for filename, attr_names in common_patterns:
23-
if os.path.exists(filename):
24-
try:
25-
with open(filename, "r") as f:
26-
content = f.read()
27-
28-
if "from fastapi import" in content or "import fastapi" in content:
29-
framework = "FastAPI"
30-
elif "from flask import" in content or "import flask" in content:
31-
framework = "Flask"
32-
else:
33-
continue
34-
35-
for attr_name in attr_names:
36-
if f"{attr_name} = " in content:
37-
return framework, filename, attr_name
38-
39-
except Exception:
14+
import glob
15+
16+
# Search for app files at any depth in current directory
17+
app_patterns = ["app.py", "main.py", "server.py", "wsgi.py", "asgi.py"]
18+
common_vars = ["app", "application", "main", "server"]
19+
20+
# Find all matching files recursively
21+
found_files = []
22+
for pattern in app_patterns:
23+
found_files.extend(glob.glob(f"**/{pattern}", recursive=True))
24+
25+
# Sort by depth (shallowest first) and then by filename priority
26+
found_files.sort(key=lambda x: (x.count(os.sep), app_patterns.index(os.path.basename(x))))
27+
28+
for file_path in found_files:
29+
try:
30+
with open(file_path, "r") as f:
31+
content = f.read()
32+
33+
if "from fastapi import" in content or "import fastapi" in content:
34+
framework = "FastAPI"
35+
elif "from flask import" in content or "import flask" in content:
36+
framework = "Flask"
37+
else:
4038
continue
4139

40+
for var_name in common_vars:
41+
if f"{var_name} = " in content:
42+
return framework, file_path, var_name
43+
44+
except Exception:
45+
continue
46+
4247
return None
4348

4449

4550
def generate_conftest_content(framework: str, file_path: str, app_variable: str) -> str:
4651
"""Generate conftest.py content based on detected framework."""
47-
module_name = file_path[:-3] # Remove .py
52+
# Convert file path to import path (e.g., "src/main.py" -> "src.main")
53+
module_path = file_path.replace("/", ".").replace("\\", ".").replace(".py", "")
4854

4955
return f'''"""conftest.py - Auto-generated by pytest-api-cov init"""
5056
5157
import pytest
5258
5359
# Import your {framework} app
54-
from {module_name} import {app_variable}
60+
from {module_path} import {app_variable}
5561
5662
5763
@pytest.fixture
58-
def app():
59-
"""Provide the {framework} app for API coverage testing."""
64+
def client():
65+
"""Provide the {framework} client for API coverage testing.
66+
67+
In your test:
68+
```
69+
def test_root_endpoint(client):
70+
response = client.get("/")
71+
assert response.status_code == 200
72+
```
73+
"""
6074
return {app_variable}
6175
'''
6276

@@ -152,9 +166,9 @@ def test_root_endpoint(client):
152166
print("❌ No FastAPI or Flask app detected in common locations")
153167
print()
154168
print("Please ensure you have one of these files with a Flask/FastAPI app:")
155-
print("• app.py")
156-
print("• main.py")
157-
print("• server.py")
169+
print("• app.py, main.py, server.py, wsgi.py, or asgi.py")
170+
print("• Files can be in the current directory or any subdirectory")
171+
print("• The file must contain a variable named 'app', 'application', 'main', or 'server'")
158172
print()
159173
print("Example app.py:")
160174
print("""

tests/integration/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# This file makes the integration directory a Python package
1+
"""init integration tests."""

tests/unit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# This file makes the unit directory a Python package
1+
"""init unit tests."""

tests/unit/test_cli.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class TestDetectFrameworkAndApp:
1818

1919
def test_no_files_exist(self):
2020
"""Test detection when no app files exist."""
21-
with patch("os.path.exists", return_value=False):
21+
with patch("glob.glob", return_value=[]):
2222
result = detect_framework_and_app()
2323
assert result is None
2424

@@ -28,6 +28,8 @@ def test_no_files_exist(self):
2828
("FastAPI", "from fastapi import FastAPI", "app", "app.py", "app"),
2929
("Flask", "from flask import Flask", "application", "app.py", "application"),
3030
("FastAPI", "import fastapi\nfrom fastapi import FastAPI", "main", "main.py", "main"),
31+
("FastAPI", "from fastapi import FastAPI", "app", "src/main.py", "app"),
32+
("Flask", "from flask import Flask", "app", "example/src/main.py", "app"),
3133
],
3234
)
3335
def test_framework_app_detection(self, framework, import_stmt, var_name, expected_file, expected_var):
@@ -53,10 +55,7 @@ def root():
5355
return "hello"
5456
"""
5557

56-
def mock_exists(path):
57-
return path == expected_file
58-
59-
with patch("os.path.exists", side_effect=mock_exists), patch("builtins.open", mock_open(read_data=app_content)):
58+
with patch("glob.glob", return_value=[expected_file]), patch("builtins.open", mock_open(read_data=app_content)):
6059
result = detect_framework_and_app()
6160
assert result == (framework, expected_file, expected_var)
6261

@@ -68,7 +67,7 @@ def test_no_framework_detected(self):
6867
def hello():
6968
return "hello"
7069
"""
71-
with patch("os.path.exists", return_value=True), patch("builtins.open", mock_open(read_data=app_content)):
70+
with patch("glob.glob", return_value=["app.py"]), patch("builtins.open", mock_open(read_data=app_content)):
7271
result = detect_framework_and_app()
7372
assert result is None
7473

@@ -79,14 +78,14 @@ def test_framework_but_no_app_variable(self):
7978
8079
# No app variable defined
8180
"""
82-
with patch("os.path.exists", return_value=True), patch("builtins.open", mock_open(read_data=app_content)):
81+
with patch("glob.glob", return_value=["app.py"]), patch("builtins.open", mock_open(read_data=app_content)):
8382
result = detect_framework_and_app()
8483
assert result is None
8584

8685
def test_file_read_exception(self):
8786
"""Test handling of file read exceptions."""
8887
with (
89-
patch("os.path.exists", return_value=True),
88+
patch("glob.glob", return_value=["app.py"]),
9089
patch("builtins.open", side_effect=IOError("Cannot read file")),
9190
):
9291
result = detect_framework_and_app()
@@ -99,18 +98,19 @@ def test_multiple_files_checked(self):
9998
server = Flask(__name__)
10099
"""
101100

102-
def mock_exists(path):
103-
return path in ["app.py", "server.py"]
101+
with patch("glob.glob", return_value=["app.py", "server.py"]):
104102

105-
def mock_open_handler(path, mode="r"):
106-
if path == "app.py":
107-
return mock_open(read_data="# just a comment")()
108-
elif path == "server.py":
109-
return mock_open(read_data=flask_content)()
103+
def mock_open_handler(path, mode="r"):
104+
if path == "app.py":
105+
return mock_open(read_data="# foobar")()
106+
elif path == "server.py":
107+
return mock_open(read_data=flask_content)()
108+
else:
109+
return mock_open(read_data="")()
110110

111-
with patch("os.path.exists", side_effect=mock_exists), patch("builtins.open", side_effect=mock_open_handler):
112-
result = detect_framework_and_app()
113-
assert result == ("Flask", "server.py", "server")
111+
with patch("builtins.open", side_effect=mock_open_handler):
112+
result = detect_framework_and_app()
113+
assert result == ("Flask", "server.py", "server")
114114

115115

116116
class TestGenerateConftestContent:
@@ -136,6 +136,26 @@ def test_flask_conftest(self):
136136
assert "Provide the Flask app" in content
137137
assert "return application" in content
138138

139+
def test_subdirectory_conftest(self):
140+
"""Test generating conftest for app in subdirectory."""
141+
content = generate_conftest_content("FastAPI", "src/main.py", "app")
142+
143+
assert "import pytest" in content
144+
assert "from src.main import app" in content
145+
assert "def app():" in content
146+
assert "Provide the FastAPI app" in content
147+
assert "return app" in content
148+
149+
def test_nested_subdirectory_conftest(self):
150+
"""Test generating conftest for app in nested subdirectory."""
151+
content = generate_conftest_content("Flask", "example/src/main.py", "app")
152+
153+
assert "import pytest" in content
154+
assert "from example.src.main import app" in content
155+
assert "def app():" in content
156+
assert "Provide the Flask app" in content
157+
assert "return app" in content
158+
139159

140160
class TestGeneratePyprojectConfig:
141161
"""Tests for generate_pyproject_config function."""

tests/unit/test_plugin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
class TestSupportedFramework:
2222
"""Tests for framework detection utility functions."""
2323

24+
def test_package_version(self):
25+
"""Test that package version is accessible."""
26+
import pytest_api_cov
27+
assert hasattr(pytest_api_cov, "__version__")
28+
assert isinstance(pytest_api_cov.__version__, str)
29+
assert pytest_api_cov.__version__ == "0.1.1"
30+
2431
def test_is_supported_framework_none(self):
2532
"""Test framework detection with None."""
2633
assert is_supported_framework(None) is False

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)