Skip to content

Commit 1466881

Browse files
authored
refactor: microgen - rename variables, edit docstrings (#2319)
* fixes lint error related to f-string * updates noxfile to enable additional testing * updates variable name and adds logic to catch Dict and None * removes unnecessary blank line * refactors test_generate_parser to use analyze_classes * refactors structure -> analyzed_classes * refactors structure -> analyzed_classes in test file * updates logic to catch another annotation * Update test_generate_analyzer.py renamed variable.
1 parent 8fdc651 commit 1466881

File tree

4 files changed

+208
-42
lines changed

4 files changed

+208
-42
lines changed

scripts/microgenerator/generate.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class CodeAnalyzer(ast.NodeVisitor):
4747
"""
4848

4949
def __init__(self):
50-
self.structure: List[Dict[str, Any]] = []
50+
self.analyzed_classes: List[Dict[str, Any]] = []
5151
self.imports: set[str] = set()
5252
self.types: set[str] = set()
5353
self._current_class_info: Dict[str, Any] | None = None
@@ -106,13 +106,19 @@ def _collect_types_from_node(self, node: ast.AST | None) -> None:
106106
if type_str:
107107
self.types.add(type_str)
108108
elif isinstance(node, ast.Subscript):
109-
self._collect_types_from_node(node.value)
109+
# Add the base type of the subscript (e.g., "List", "Dict")
110+
if isinstance(node.value, ast.Name):
111+
self.types.add(node.value.id)
112+
self._collect_types_from_node(node.value) # Recurse on value just in case
110113
self._collect_types_from_node(node.slice)
111114
elif isinstance(node, (ast.Tuple, ast.List)):
112115
for elt in node.elts:
113116
self._collect_types_from_node(elt)
114-
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
115-
self.types.add(node.value)
117+
elif isinstance(node, ast.Constant):
118+
if isinstance(node.value, str): # Forward references
119+
self.types.add(node.value)
120+
elif node.value is None: # None type
121+
self.types.add("None")
116122
elif isinstance(node, ast.BinOp) and isinstance(
117123
node.op, ast.BitOr
118124
): # For | union type
@@ -164,7 +170,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
164170
type_str = self._get_type_str(item.annotation)
165171
class_info["attributes"].append({"name": attr_name, "type": type_str})
166172

167-
self.structure.append(class_info)
173+
self.analyzed_classes.append(class_info)
168174
self._current_class_info = class_info
169175
self._depth += 1
170176
self.generic_visit(node)
@@ -260,6 +266,7 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
260266
# directly within the class body, not inside a method.
261267
elif isinstance(target, ast.Name) and not self._is_in_method:
262268
self._add_attribute(target.id, self._get_type_str(node.annotation))
269+
self._collect_types_from_node(node.annotation)
263270
self.generic_visit(node)
264271

265272

@@ -280,7 +287,7 @@ def parse_code(code: str) -> tuple[List[Dict[str, Any]], set[str], set[str]]:
280287
tree = ast.parse(code)
281288
analyzer = CodeAnalyzer()
282289
analyzer.visit(tree)
283-
return analyzer.structure, analyzer.imports, analyzer.types
290+
return analyzer.analyzed_classes, analyzer.imports, analyzer.types
284291

285292

286293
def parse_file(file_path: str) -> tuple[List[Dict[str, Any]], set[str], set[str]]:
@@ -332,10 +339,10 @@ def list_code_objects(
332339
all_class_keys = []
333340

334341
def process_structure(
335-
structure: List[Dict[str, Any]], file_name: str | None = None
342+
analyzed_classes: List[Dict[str, Any]], file_name: str | None = None
336343
):
337344
"""Populates the results dictionary from the parsed AST structure."""
338-
for class_info in structure:
345+
for class_info in analyzed_classes:
339346
key = class_info["class_name"]
340347
if file_name:
341348
key = f"{key} (in {file_name})"
@@ -361,13 +368,13 @@ def process_structure(
361368

362369
# Determine if the path is a file or directory and process accordingly
363370
if os.path.isfile(path) and path.endswith(".py"):
364-
structure, _, _ = parse_file(path)
365-
process_structure(structure)
371+
analyzed_classes, _, _ = parse_file(path)
372+
process_structure(analyzed_classes)
366373
elif os.path.isdir(path):
367374
# This assumes `utils.walk_codebase` is defined elsewhere.
368375
for file_path in utils.walk_codebase(path):
369-
structure, _, _ = parse_file(file_path)
370-
process_structure(structure, file_name=os.path.basename(file_path))
376+
analyzed_classes, _, _ = parse_file(file_path)
377+
process_structure(analyzed_classes, file_name=os.path.basename(file_path))
371378

372379
# Return the data in the desired format based on the flags
373380
if not show_methods and not show_attributes:
@@ -419,11 +426,11 @@ def _build_request_arg_schema(
419426
module_name = os.path.splitext(relative_path)[0].replace(os.path.sep, ".")
420427

421428
try:
422-
structure, _, _ = parse_file(file_path)
423-
if not structure:
429+
analyzed_classes, _, _ = parse_file(file_path)
430+
if not analyzed_classes:
424431
continue
425432

426-
for class_info in structure:
433+
for class_info in analyzed_classes:
427434
class_name = class_info.get("class_name", "Unknown")
428435
if class_name.endswith("Request"):
429436
full_class_name = f"{module_name}.{class_name}"
@@ -451,11 +458,11 @@ def _process_service_clients(
451458
if "/services/" not in file_path:
452459
continue
453460

454-
structure, imports, types = parse_file(file_path)
461+
analyzed_classes, imports, types = parse_file(file_path)
455462
all_imports.update(imports)
456463
all_types.update(types)
457464

458-
for class_info in structure:
465+
for class_info in analyzed_classes:
459466
class_name = class_info["class_name"]
460467
if not _should_include_class(class_name, class_filters):
461468
continue

scripts/microgenerator/noxfile.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from functools import wraps
1818
import pathlib
19-
import os
2019
import nox
2120
import time
2221

@@ -26,7 +25,7 @@
2625
BLACK_VERSION = "black==23.7.0"
2726
BLACK_PATHS = (".",)
2827

29-
DEFAULT_PYTHON_VERSION = "3.9"
28+
DEFAULT_PYTHON_VERSION = "3.13"
3029
UNIT_TEST_PYTHON_VERSIONS = ["3.9", "3.11", "3.12", "3.13"]
3130
CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
3231

@@ -190,9 +189,8 @@ def lint(session):
190189
session.install("flake8", BLACK_VERSION)
191190
session.install("-e", ".")
192191
session.run("python", "-m", "pip", "freeze")
193-
session.run("flake8", os.path.join("scripts"))
192+
session.run("flake8", ".")
194193
session.run("flake8", "tests")
195-
session.run("flake8", "benchmark")
196194
session.run("black", "--check", *BLACK_PATHS)
197195

198196

scripts/microgenerator/tests/unit/test_generate_analyzer.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_import_extraction(self, code_snippet, expected_imports):
9595

9696
class TestCodeAnalyzerAttributes:
9797
@pytest.mark.parametrize(
98-
"code_snippet, expected_structure",
98+
"code_snippet, expected_analyzed_classes",
9999
[
100100
pytest.param(
101101
"""
@@ -243,22 +243,24 @@ def __init__(self):
243243
),
244244
],
245245
)
246-
def test_attribute_extraction(self, code_snippet: str, expected_structure: list):
246+
def test_attribute_extraction(
247+
self, code_snippet: str, expected_analyzed_classes: list
248+
):
247249
"""Tests the extraction of class and instance attributes."""
248250
analyzer = CodeAnalyzer()
249251
tree = ast.parse(code_snippet)
250252
analyzer.visit(tree)
251253

252-
extracted = analyzer.structure
254+
extracted = analyzer.analyzed_classes
253255
# Normalize attributes for order-independent comparison
254256
for item in extracted:
255257
if "attributes" in item:
256258
item["attributes"].sort(key=lambda x: x["name"])
257-
for item in expected_structure:
259+
for item in expected_analyzed_classes:
258260
if "attributes" in item:
259261
item["attributes"].sort(key=lambda x: x["name"])
260262

261-
assert extracted == expected_structure
263+
assert extracted == expected_analyzed_classes
262264

263265

264266
# --- Mock Types ---
@@ -284,8 +286,8 @@ class MyClass:
284286
analyzer = CodeAnalyzer()
285287
tree = ast.parse(code)
286288
analyzer.visit(tree)
287-
assert len(analyzer.structure) == 1
288-
assert analyzer.structure[0]["class_name"] == "MyClass"
289+
assert len(analyzer.analyzed_classes) == 1
290+
assert analyzer.analyzed_classes[0]["class_name"] == "MyClass"
289291

290292

291293
def test_codeanalyzer_finds_multiple_classes():
@@ -302,8 +304,8 @@ class ClassB:
302304
analyzer = CodeAnalyzer()
303305
tree = ast.parse(code)
304306
analyzer.visit(tree)
305-
assert len(analyzer.structure) == 2
306-
class_names = sorted([c["class_name"] for c in analyzer.structure])
307+
assert len(analyzer.analyzed_classes) == 2
308+
class_names = sorted([c["class_name"] for c in analyzer.analyzed_classes])
307309
assert class_names == ["ClassA", "ClassB"]
308310

309311

@@ -318,9 +320,9 @@ def my_method(self):
318320
analyzer = CodeAnalyzer()
319321
tree = ast.parse(code)
320322
analyzer.visit(tree)
321-
assert len(analyzer.structure) == 1
322-
assert len(analyzer.structure[0]["methods"]) == 1
323-
assert analyzer.structure[0]["methods"][0]["method_name"] == "my_method"
323+
assert len(analyzer.analyzed_classes) == 1
324+
assert len(analyzer.analyzed_classes[0]["methods"]) == 1
325+
assert analyzer.analyzed_classes[0]["methods"][0]["method_name"] == "my_method"
324326

325327

326328
def test_codeanalyzer_finds_multiple_methods():
@@ -337,8 +339,8 @@ def method_b(self):
337339
analyzer = CodeAnalyzer()
338340
tree = ast.parse(code)
339341
analyzer.visit(tree)
340-
assert len(analyzer.structure) == 1
341-
method_names = sorted([m["method_name"] for m in analyzer.structure[0]["methods"]])
342+
assert len(analyzer.analyzed_classes) == 1
343+
method_names = sorted([m["method_name"] for m in analyzer.analyzed_classes[0]["methods"]])
342344
assert method_names == ["method_a", "method_b"]
343345

344346

@@ -352,7 +354,7 @@ def top_level_function():
352354
analyzer = CodeAnalyzer()
353355
tree = ast.parse(code)
354356
analyzer.visit(tree)
355-
assert len(analyzer.structure) == 0
357+
assert len(analyzer.analyzed_classes) == 0
356358

357359

358360
def test_codeanalyzer_class_with_no_methods():
@@ -365,9 +367,9 @@ class MyClass:
365367
analyzer = CodeAnalyzer()
366368
tree = ast.parse(code)
367369
analyzer.visit(tree)
368-
assert len(analyzer.structure) == 1
369-
assert analyzer.structure[0]["class_name"] == "MyClass"
370-
assert len(analyzer.structure[0]["methods"]) == 0
370+
assert len(analyzer.analyzed_classes) == 1
371+
assert analyzer.analyzed_classes[0]["class_name"] == "MyClass"
372+
assert len(analyzer.analyzed_classes[0]["methods"]) == 0
371373

372374

373375
# --- Test Data for Parameterization ---
@@ -487,10 +489,10 @@ class TestCodeAnalyzerArgsReturns:
487489
"code_snippet, expected_args, expected_return", TYPE_TEST_CASES
488490
)
489491
def test_type_extraction(self, code_snippet, expected_args, expected_return):
490-
structure, imports, types = parse_code(code_snippet)
492+
analyzed_classes, imports, types = parse_code(code_snippet)
491493

492-
assert len(structure) == 1, "Should parse one class"
493-
class_info = structure[0]
494+
assert len(analyzed_classes) == 1, "Should parse one class"
495+
class_info = analyzed_classes[0]
494496
assert class_info["class_name"] == "TestClass"
495497

496498
assert len(class_info["methods"]) == 1, "Should find one method"
@@ -506,3 +508,4 @@ def test_type_extraction(self, code_snippet, expected_args, expected_return):
506508

507509
assert extracted_args == expected_args
508510
assert method_info.get("return_type") == expected_return
511+

0 commit comments

Comments
 (0)