Skip to content

Commit 260eedf

Browse files
authored
feat: microgen - adds source file gathering functions (#2293)
* chore: removes old proof of concept * removes old __init__.py * Adds two utility files to handle basic tasks * Adds a configuration file for the microgenerator * Removes unused comment * chore: adds noxfile.py for the microgenerator * feat: microgen - adds two init file templates * feat: adds _helpers.py.js template * Updates with two usage examples * feat: adds two partial templates for creating method signatures * feat: Add microgenerator __init__.py Migrates the empty __init__.py file to the microgenerator package. * feat: Add AST analysis utilities Introduces the CodeAnalyzer class and helper functions for parsing Python code using the ast module. This provides the foundation for understanding service client structures. * feat: Add source file analysis capabilities Implements functions to analyze Python source files, including: - Filtering classes and methods based on configuration. - Building a schema of request classes and their arguments. - Processing service client files to extract relevant information. * feat: adds code generation logic * removes extraneous content
1 parent ba2a581 commit 260eedf

File tree

1 file changed

+141
-1
lines changed

1 file changed

+141
-1
lines changed

scripts/microgenerator/generate.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@
2424

2525
import ast
2626
import os
27+
import glob
28+
import logging
29+
import re
2730
from collections import defaultdict
28-
from typing import List, Dict, Any
31+
from typing import List, Dict, Any, Iterator
2932

33+
from . import name_utils
3034
from . import utils
3135

3236
# =============================================================================
@@ -352,3 +356,139 @@ def process_structure(
352356
return sorted(all_class_keys)
353357
else:
354358
return dict(results)
359+
360+
361+
# =============================================================================
362+
# Section 2: Source file data gathering
363+
# =============================================================================
364+
365+
366+
def _should_include_class(class_name: str, class_filters: Dict[str, Any]) -> bool:
367+
"""Checks if a class should be included based on filter criteria."""
368+
if class_filters.get("include_suffixes"):
369+
if not class_name.endswith(tuple(class_filters["include_suffixes"])):
370+
return False
371+
if class_filters.get("exclude_suffixes"):
372+
if class_name.endswith(tuple(class_filters["exclude_suffixes"])):
373+
return False
374+
return True
375+
376+
377+
def _should_include_method(method_name: str, method_filters: Dict[str, Any]) -> bool:
378+
"""Checks if a method should be included based on filter criteria."""
379+
if method_filters.get("include_prefixes"):
380+
if not any(
381+
method_name.startswith(p) for p in method_filters["include_prefixes"]
382+
):
383+
return False
384+
if method_filters.get("exclude_prefixes"):
385+
if any(method_name.startswith(p) for p in method_filters["exclude_prefixes"]):
386+
return False
387+
return True
388+
389+
390+
def _build_request_arg_schema(
391+
source_files: List[str], project_root: str
392+
) -> Dict[str, List[str]]:
393+
"""Parses type files to build a schema of request classes and their _id arguments."""
394+
request_arg_schema: Dict[str, List[str]] = {}
395+
for file_path in source_files:
396+
if "/types/" not in file_path:
397+
continue
398+
399+
# Correctly determine the module name from the file path
400+
relative_path = os.path.relpath(file_path, project_root)
401+
module_name = os.path.splitext(relative_path)[0].replace(os.path.sep, ".")
402+
403+
try:
404+
structure, _, _ = parse_file(file_path)
405+
if not structure:
406+
continue
407+
408+
for class_info in structure:
409+
class_name = class_info.get("class_name", "Unknown")
410+
if class_name.endswith("Request"):
411+
full_class_name = f"{module_name}.{class_name}"
412+
id_args = [
413+
attr["name"]
414+
for attr in class_info.get("attributes", [])
415+
if attr.get("name", "").endswith("_id")
416+
]
417+
if id_args:
418+
request_arg_schema[full_class_name] = id_args
419+
except Exception as e:
420+
logging.warning(f"Failed to parse {file_path}: {e}")
421+
return request_arg_schema
422+
423+
424+
def _process_service_clients(
425+
source_files: List[str], class_filters: Dict, method_filters: Dict
426+
) -> tuple[defaultdict, set, set]:
427+
"""Parses service client files to extract class and method information."""
428+
parsed_data = defaultdict(dict)
429+
all_imports: set[str] = set()
430+
all_types: set[str] = set()
431+
432+
for file_path in source_files:
433+
if "/services/" not in file_path:
434+
continue
435+
436+
structure, imports, types = parse_file(file_path)
437+
all_imports.update(imports)
438+
all_types.update(types)
439+
440+
for class_info in structure:
441+
class_name = class_info["class_name"]
442+
if not _should_include_class(class_name, class_filters):
443+
continue
444+
445+
parsed_data[class_name] # Ensure class is in dict
446+
447+
for method in class_info["methods"]:
448+
method_name = method["method_name"]
449+
if not _should_include_method(method_name, method_filters):
450+
continue
451+
parsed_data[class_name][method_name] = method
452+
return parsed_data, all_imports, all_types
453+
454+
455+
def analyze_source_files(
456+
config: Dict[str, Any],
457+
) -> tuple[Dict[str, Any], set[str], set[str], Dict[str, List[str]]]:
458+
"""
459+
Analyzes source files per the configuration to extract class and method info,
460+
as well as information on imports and typehints.
461+
462+
Args:
463+
config: The generator's configuration dictionary.
464+
465+
Returns:
466+
A tuple containing:
467+
- A dictionary containing the data needed for template rendering.
468+
- A set of all import statements required by the parsed methods.
469+
- A set of all type annotations found in the parsed methods.
470+
- A dictionary mapping request class names to their `_id` arguments.
471+
"""
472+
project_root = config["project_root"]
473+
source_patterns_dict = config.get("source_files", {})
474+
filter_rules = config.get("filter", {})
475+
class_filters = filter_rules.get("classes", {})
476+
method_filters = filter_rules.get("methods", {})
477+
478+
source_files = []
479+
for group in source_patterns_dict.values():
480+
for pattern in group:
481+
# Make the pattern absolute
482+
absolute_pattern = os.path.join(project_root, pattern)
483+
source_files.extend(glob.glob(absolute_pattern, recursive=True))
484+
485+
# PASS 1: Build the request argument schema from the types files.
486+
request_arg_schema = _build_request_arg_schema(source_files, project_root)
487+
488+
# PASS 2: Process the service client files.
489+
parsed_data, all_imports, all_types = _process_service_clients(
490+
source_files, class_filters, method_filters
491+
)
492+
493+
return parsed_data, all_imports, all_types, request_arg_schema
494+

0 commit comments

Comments
 (0)