Skip to content

Commit e24c3f5

Browse files
refactoring
1 parent d5dbf8e commit e24c3f5

8 files changed

Lines changed: 6634 additions & 4001 deletions

File tree

code2logic/cli.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,10 @@ def _maybe_print_pretty_help() -> bool:
608608
)
609609
parser.add_argument(
610610
'--function-logic',
611-
help='Write detailed function logic to a separate file (format inferred from extension: .logicml/.json/.yaml/.toon)'
611+
nargs='?',
612+
const='auto',
613+
default=None,
614+
help='Write detailed function logic to a separate file. If no path given, auto-generates based on output file or uses project.functions.logicml. Format inferred from extension: .logicml/.json/.yaml/.toon'
612615
)
613616
parser.add_argument(
614617
'--flat',
@@ -907,7 +910,24 @@ def _maybe_print_pretty_help() -> bool:
907910
# Optional: write detailed function logic to a separate file
908911
if args.function_logic:
909912
logic_gen = FunctionLogicGenerator()
910-
logic_path = str(args.function_logic)
913+
914+
# Auto-generate path if 'auto' was specified (--function-logic without argument)
915+
if args.function_logic == 'auto':
916+
if args.output:
917+
# Derive from output file: project.c2l.yaml -> project.functions.yaml
918+
base = args.output.rsplit('.', 1)[0]
919+
if base.endswith('.c2l'):
920+
base = base[:-4]
921+
ext = args.output.rsplit('.', 1)[-1] if '.' in args.output else 'logicml'
922+
logic_path = f"{base}.functions.{ext}"
923+
else:
924+
# Default path based on format
925+
ext_map = {'json': 'json', 'yaml': 'yaml', 'toon': 'toon'}
926+
ext = ext_map.get(args.format, 'logicml')
927+
logic_path = f"project.functions.{ext}"
928+
else:
929+
logic_path = str(args.function_logic)
930+
911931
lower = logic_path.lower()
912932
if lower.endswith('.json'):
913933
logic_out = logic_gen.generate_json(project, detail=args.detail)
@@ -917,10 +937,10 @@ def _maybe_print_pretty_help() -> bool:
917937
logic_out = logic_gen.generate_toon(project, detail=args.detail)
918938
else:
919939
logic_out = logic_gen.generate(project, detail=args.detail)
920-
with open(args.function_logic, 'w', encoding='utf-8') as f:
940+
with open(logic_path, 'w', encoding='utf-8') as f:
921941
f.write(logic_out)
922942
if args.verbose:
923-
log.success(f"Function logic written to: {args.function_logic}")
943+
log.success(f"Function logic written to: {logic_path}")
924944

925945
gen_time = time.time() - gen_start
926946

code2logic/generators.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,17 @@ def _constant_to_dict(self, constant: ConstantInfo) -> dict:
18971897
data['v'] = snippet
18981898
return data
18991899

1900+
def _field_to_dict(self, field: FieldInfo) -> dict:
1901+
"""Serialize dataclass FieldInfo to dictionary."""
1902+
data = {'name': field.name}
1903+
if getattr(field, 'type_annotation', None):
1904+
data['type'] = field.type_annotation
1905+
if getattr(field, 'default', None):
1906+
data['default'] = field.default
1907+
if getattr(field, 'default_factory', None):
1908+
data['factory'] = field.default_factory
1909+
return data
1910+
19001911

19011912
# ============================================================================
19021913
# CSV Generator

code2logic/parsers.py

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -341,13 +341,27 @@ def _parse_python(self, filepath: str, content: str, tree) -> ModuleInfo:
341341
if not func.name.startswith('_'):
342342
exports.append(func.name)
343343
elif node_type == 'decorated_definition':
344+
# Handle decorated functions
344345
inner_func = self._find_child(child, 'function_definition')
345346
if inner_func:
346347
func = self._extract_py_function(inner_func, content, child)
347348
if func:
348349
functions.append(func)
349350
if not func.name.startswith('_'):
350351
exports.append(func.name)
352+
353+
# Handle decorated classes (e.g., @dataclass)
354+
inner_class = self._find_child(child, 'class_definition')
355+
if inner_class:
356+
cls = self._extract_py_class(inner_class, content, decorated_node=child)
357+
if cls:
358+
classes.append(cls)
359+
if not cls.name.startswith('_'):
360+
exports.append(cls.name)
361+
362+
enum_type = self._extract_py_enum(inner_class, content)
363+
if enum_type:
364+
types.append(enum_type)
351365

352366
# Constants with enhanced extraction
353367
if node_type == 'expression_statement':
@@ -652,7 +666,7 @@ def _extract_py_enum(self, node, content: str) -> Optional[TypeInfo]:
652666
except Exception:
653667
return None
654668

655-
def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
669+
def _extract_py_class(self, node, content: str, decorated_node=None) -> Optional[ClassInfo]:
656670
"""Extract Python class from AST node."""
657671
name_node = self._find_child(node, 'identifier')
658672
if not name_node:
@@ -670,10 +684,10 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
670684
# Check for dataclass decorator
671685
decorators = []
672686
is_dataclass = False
673-
# Look for parent decorated_definition
674-
parent = getattr(node, 'parent', None)
675-
if parent and parent.type == 'decorated_definition':
676-
for c in parent.children:
687+
# Use provided decorated_node or try to find parent
688+
dec_source = decorated_node or getattr(node, 'parent', None)
689+
if dec_source and dec_source.type == 'decorated_definition':
690+
for c in dec_source.children:
677691
if c.type == 'decorator':
678692
dec_text = self._text(c, content).lstrip('@')
679693
decorators.append(dec_text.split('(')[0])
@@ -685,6 +699,7 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
685699
methods = []
686700
fields = []
687701
attributes = []
702+
properties = []
688703
body = self._find_child(node, 'block')
689704
if body:
690705
for i, child in enumerate(body.children):
@@ -697,24 +712,29 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
697712
m = self._extract_py_function(child, content)
698713
if m:
699714
methods.append(m)
715+
# Extract self.x = ... from __init__ method
716+
if m.name == '__init__' and not is_dataclass:
717+
init_attrs = self._extract_init_attributes(child, content)
718+
attributes.extend(init_attrs)
700719
elif child.type == 'decorated_definition':
701720
inner = self._find_child(child, 'function_definition')
702721
if inner:
703722
m = self._extract_py_function(inner, content, child)
704723
if m:
705724
methods.append(m)
706725

707-
# Extract dataclass fields
726+
# Extract dataclass fields (class-level annotated assignments)
708727
elif is_dataclass and child.type == 'expression_statement':
709728
field = self._extract_dataclass_field(child, content)
710729
if field:
711730
fields.append(field)
712731

713-
# Extract instance attributes (self.x = ...)
732+
# Extract class-level properties (annotated assignments without dataclass)
714733
elif child.type == 'expression_statement' and not is_dataclass:
715-
attr = self._extract_class_attribute(child, content)
716-
if attr:
717-
attributes.append(attr)
734+
# Check for annotated assignment like "x: int" or "x: int = 5"
735+
prop = self._extract_class_property(child, content)
736+
if prop:
737+
properties.append(prop)
718738

719739
return ClassInfo(
720740
name=name,
@@ -724,6 +744,7 @@ def _extract_py_class(self, node, content: str) -> Optional[ClassInfo]:
724744
is_dataclass=is_dataclass,
725745
fields=fields,
726746
attributes=attributes,
747+
properties=properties,
727748
methods=methods,
728749
is_interface=False,
729750
is_abstract='ABC' in bases or 'ABCMeta' in bases,
@@ -824,6 +845,46 @@ def _extract_class_attribute(self, node, content: str) -> Optional[AttributeInfo
824845
)
825846
return None
826847

848+
def _extract_init_attributes(self, func_node, content: str) -> List[AttributeInfo]:
849+
"""Extract self.x = ... assignments from __init__ method body."""
850+
attributes = []
851+
seen_names = set()
852+
853+
def scan_block(block_node):
854+
"""Recursively scan block for self.x assignments."""
855+
if not block_node:
856+
return
857+
for child in block_node.children:
858+
if child.type == 'expression_statement':
859+
attr = self._extract_class_attribute(child, content)
860+
if attr and attr.name not in seen_names:
861+
seen_names.add(attr.name)
862+
attributes.append(attr)
863+
# Also scan nested blocks (if/for/while/try)
864+
elif child.type in ('if_statement', 'for_statement', 'while_statement', 'try_statement'):
865+
for sub in child.children:
866+
if sub.type == 'block':
867+
scan_block(sub)
868+
869+
body = self._find_child(func_node, 'block')
870+
scan_block(body)
871+
return attributes[:15] # Limit to 15 attributes
872+
873+
def _extract_class_property(self, node, content: str) -> Optional[str]:
874+
"""Extract class-level property from annotated assignment."""
875+
try:
876+
stmt_text = self._text(node, content).strip()
877+
except Exception:
878+
return None
879+
880+
# Match annotated assignment: "name: Type" or "name: Type = value"
881+
m = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=]+?)(?:\s*=.*)?$', stmt_text)
882+
if m:
883+
name = m.group(1)
884+
type_ann = m.group(2).strip()
885+
return f"{name}: {type_ann}"
886+
return None
887+
827888
def _extract_py_import(self, node, content: str) -> List[str]:
828889
"""Extract import statement."""
829890
imports = []
@@ -1575,14 +1636,26 @@ def _extract_ast_class(self, node: ast.ClassDef) -> ClassInfo:
15751636
if isinstance(target, ast.Name):
15761637
properties.append(target.id)
15771638

1639+
# Extract decorators
1640+
decorators = []
1641+
for dec in node.decorator_list:
1642+
if isinstance(dec, ast.Name):
1643+
decorators.append(dec.id)
1644+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name):
1645+
decorators.append(dec.func.id)
1646+
elif isinstance(dec, ast.Attribute):
1647+
decorators.append(dec.attr)
1648+
15781649
return ClassInfo(
15791650
name=node.name,
15801651
bases=bases,
1652+
decorators=decorators,
15811653
docstring=ast.get_docstring(node)[:100] if ast.get_docstring(node) else None,
1654+
is_dataclass=is_dataclass,
15821655
methods=methods,
15831656
properties=properties,
15841657
is_interface=False,
1585-
is_abstract='ABC' in bases or is_dataclass,
1658+
is_abstract='ABC' in bases,
15861659
generic_params=[]
15871660
)
15881661

0 commit comments

Comments
 (0)