pygraph-mcp 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/PKG-INFO +1 -1
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/pyproject.toml +1 -1
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/flask.py +60 -1
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/implements.py +2 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/types.py +2 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_extractors.py +70 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/uv.lock +1 -1
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/.github/workflows/publish.yml +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/.gitignore +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/AGENTS.md +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/README.md +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/TODOS.md +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/opencode.json +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/__main__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/builder.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/cli.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/boundaries.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/callees.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/callers.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/changes.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/complexity.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/context.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/coupling.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/deps.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/focus.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/graph_report.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/hotspot.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/impact.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/imports_cmd.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/node.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/opencode_plugin.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/orphans.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/path_cmd.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/plan.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/public.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/query_cmd.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/review.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/source.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/stale.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/trace.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/config.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/calls.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/decorators.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/env.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/errors.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/http_calls.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/imports.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/symbols.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/tests.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/boundaries.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/cache.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/serialize.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/query.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/scanner/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/scanner/gitignore.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/scanner/walker.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/server.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/conftest.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/fixtures/__init__.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_boundaries.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_changes_stale.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_commands.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_graph_report.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_graph_types.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_incremental_build.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_mcp_server.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_opencode_plugin.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_plan_review.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_plugins.py +0 -0
- {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_scanner.py +0 -0
|
@@ -375,6 +375,65 @@ def _extract_add_url_rule_calls(tree: ast.Module, file_path: str) -> list[HTTPRo
|
|
|
375
375
|
return routes
|
|
376
376
|
|
|
377
377
|
|
|
378
|
+
def _extract_flask_restful_routes(tree: ast.Module, file_path: str) -> list[HTTPRoute]:
|
|
379
|
+
routes: list[HTTPRoute] = []
|
|
380
|
+
|
|
381
|
+
class_defs: dict[str, ast.ClassDef] = {}
|
|
382
|
+
for node in ast.iter_child_nodes(tree):
|
|
383
|
+
if isinstance(node, ast.ClassDef):
|
|
384
|
+
class_defs[node.name] = node
|
|
385
|
+
|
|
386
|
+
for node in ast.walk(tree):
|
|
387
|
+
if not isinstance(node, ast.Call):
|
|
388
|
+
continue
|
|
389
|
+
func = node.func
|
|
390
|
+
if not isinstance(func, ast.Attribute) or func.attr != "add_resource":
|
|
391
|
+
continue
|
|
392
|
+
if len(node.args) < 2:
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
cls_node = node.args[0]
|
|
396
|
+
cls_name = ast.unparse(cls_node) if isinstance(cls_node, ast.Name) else ""
|
|
397
|
+
if not cls_name:
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
path_args: list[str] = []
|
|
401
|
+
for arg in node.args[1:]:
|
|
402
|
+
p = _string_from_node(arg)
|
|
403
|
+
if p:
|
|
404
|
+
path_args.append(p)
|
|
405
|
+
|
|
406
|
+
if not path_args:
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
http_methods: list[str] = []
|
|
410
|
+
cls_def = class_defs.get(cls_name)
|
|
411
|
+
if cls_def:
|
|
412
|
+
for item in cls_def.body:
|
|
413
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
414
|
+
method_upper = {
|
|
415
|
+
"get": "GET", "post": "POST", "put": "PUT",
|
|
416
|
+
"delete": "DELETE", "patch": "PATCH",
|
|
417
|
+
"head": "HEAD", "options": "OPTIONS",
|
|
418
|
+
}.get(item.name.lower())
|
|
419
|
+
if method_upper:
|
|
420
|
+
http_methods.append(method_upper)
|
|
421
|
+
if not http_methods:
|
|
422
|
+
http_methods = ["GET"]
|
|
423
|
+
|
|
424
|
+
for path in path_args:
|
|
425
|
+
for method in http_methods:
|
|
426
|
+
routes.append(HTTPRoute(
|
|
427
|
+
method=method,
|
|
428
|
+
path=path,
|
|
429
|
+
handler=f"{file_path}::{cls_name}",
|
|
430
|
+
file=file_path,
|
|
431
|
+
line=node.lineno,
|
|
432
|
+
))
|
|
433
|
+
|
|
434
|
+
return routes
|
|
435
|
+
|
|
436
|
+
|
|
378
437
|
def extract_flask(source: str, file_path: str) -> dict[str, Any]:
|
|
379
438
|
try:
|
|
380
439
|
tree = ast.parse(source)
|
|
@@ -390,7 +449,7 @@ def extract_flask(source: str, file_path: str) -> dict[str, Any]:
|
|
|
390
449
|
}
|
|
391
450
|
|
|
392
451
|
return {
|
|
393
|
-
"routes": _extract_routes(tree, file_path) + _extract_add_url_rule_calls(tree, file_path),
|
|
452
|
+
"routes": _extract_routes(tree, file_path) + _extract_add_url_rule_calls(tree, file_path) + _extract_flask_restful_routes(tree, file_path),
|
|
394
453
|
"blueprints": _detect_blueprint_assignments(tree),
|
|
395
454
|
"blueprint_registrations": _extract_blueprint_registrations(tree, file_path),
|
|
396
455
|
"template_refs": _extract_template_refs(tree, file_path),
|
|
@@ -355,6 +355,76 @@ migrate.init_app(app)
|
|
|
355
355
|
assert result["blueprints"] == []
|
|
356
356
|
assert result["template_refs"] == []
|
|
357
357
|
|
|
358
|
+
def test_flask_restful_add_resource_single_path(self) -> None:
|
|
359
|
+
src = """
|
|
360
|
+
from flask_restful import Resource, Api
|
|
361
|
+
|
|
362
|
+
api = Api(app)
|
|
363
|
+
|
|
364
|
+
class UserResource(Resource):
|
|
365
|
+
def get(self, id):
|
|
366
|
+
pass
|
|
367
|
+
def post(self):
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
api.add_resource(UserResource, '/users/<id>')
|
|
371
|
+
"""
|
|
372
|
+
from pygraph.extractors.flask import extract_flask
|
|
373
|
+
|
|
374
|
+
result = extract_flask(src, "app.py")
|
|
375
|
+
routes = [r for r in result["routes"] if r.method == "WRAPPER" or r.method in ("GET", "POST", "PUT", "DELETE")]
|
|
376
|
+
matching = [r for r in routes if r.path == "/users/<id>"]
|
|
377
|
+
assert len(matching) == 2
|
|
378
|
+
methods = {r.method for r in matching}
|
|
379
|
+
assert methods == {"GET", "POST"}
|
|
380
|
+
assert all(r.handler == "app.py::UserResource" for r in matching)
|
|
381
|
+
|
|
382
|
+
def test_flask_restful_add_resource_multi_path(self) -> None:
|
|
383
|
+
src = """
|
|
384
|
+
from flask_restful import Resource, Api
|
|
385
|
+
|
|
386
|
+
api = Api(app)
|
|
387
|
+
|
|
388
|
+
class ItemResource(Resource):
|
|
389
|
+
def get(self):
|
|
390
|
+
pass
|
|
391
|
+
def delete(self):
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
api.add_resource(ItemResource, '/items', '/items/<id>')
|
|
395
|
+
"""
|
|
396
|
+
from pygraph.extractors.flask import extract_flask
|
|
397
|
+
|
|
398
|
+
result = extract_flask(src, "app.py")
|
|
399
|
+
matching = [r for r in result["routes"] if "ItemResource" in r.handler]
|
|
400
|
+
assert len(matching) == 4 # 2 paths × 2 methods
|
|
401
|
+
paths = {r.path for r in matching}
|
|
402
|
+
assert paths == {"/items", "/items/<id>"}
|
|
403
|
+
methods = {r.method for r in matching}
|
|
404
|
+
assert methods == {"GET", "DELETE"}
|
|
405
|
+
|
|
406
|
+
def test_flask_restful_no_methods_no_crash(self) -> None:
|
|
407
|
+
src = """
|
|
408
|
+
api = Api(app)
|
|
409
|
+
api.add_resource(PlainClass, '/plain')
|
|
410
|
+
"""
|
|
411
|
+
from pygraph.extractors.flask import extract_flask
|
|
412
|
+
|
|
413
|
+
result = extract_flask(src, "app.py")
|
|
414
|
+
matching = [r for r in result["routes"] if r.path == "/plain"]
|
|
415
|
+
assert len(matching) == 1
|
|
416
|
+
assert matching[0].method == "GET"
|
|
417
|
+
|
|
418
|
+
def test_flask_restful_not_add_resource_not_detected(self) -> None:
|
|
419
|
+
src = """
|
|
420
|
+
api.add_url_rule('/regular', view_func=handler)
|
|
421
|
+
"""
|
|
422
|
+
from pygraph.extractors.flask import extract_flask
|
|
423
|
+
|
|
424
|
+
result = extract_flask(src, "app.py")
|
|
425
|
+
assert len(result["routes"]) == 1
|
|
426
|
+
assert result["routes"][0].path == "/regular"
|
|
427
|
+
|
|
358
428
|
|
|
359
429
|
class TestComplexityExtraction:
|
|
360
430
|
def test_base_complexity_one(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|