Optimization generate-notebook-from-module

Python 모듈 완성 시 대응하는 Jupyter Notebook을 자동 생성한다. AST 파싱으로 public API를 추출하고 사용 예시 템플릿을 제공한다. convention-jupyter-setup의 '모듈 완성 시 노트북 필수' 규칙을 자동화한다.

install
source · Clone the upstream repo
git clone https://github.com/sunLeee/optimization
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/sunLeee/optimization "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/utility/generate-notebook-from-module" ~/.claude/skills/sunleee-optimization-generate-notebook-from-module && rm -rf "$T"
manifest: .claude/skills/utility/generate-notebook-from-module/SKILL.md
source content

Jupyter Notebook 자동 생성 (모듈 기반)

Python 모듈 완성 시 대응하는 Jupyter Notebook을 자동 생성한다.

목적

  • 모듈 완성 시 노트북 필수 규칙 자동화
  • 모듈 API를 분석하여 사용 예시 템플릿 제공
  • convention-jupyter-setup 규칙 준수
  • 노트북 작성 시간 단축

사용법

/generate-notebook-from-module <module_path>

예시:

/generate-notebook-from-module src/data_loader.py
→ notebooks/03_data_loader.ipynb 생성

/generate-notebook-from-module src/models/classifier.py
→ notebooks/04_classifier.ipynb 생성

워크플로우

Phase 1: 모듈 분석

1.1 AST 파싱

import ast
from pathlib import Path

def analyze_module(file_path: Path) -> dict:
    """모듈에서 public API 추출."""
    with open(file_path) as f:
        tree = ast.parse(f.read())

    # Public 클래스 추출 (이름이 _로 시작하지 않음)
    classes = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef) and not node.name.startswith('_'):
            # 클래스의 public 메서드 추출
            methods = [m.name for m in node.body
                      if isinstance(m, ast.FunctionDef)
                      and not m.name.startswith('_')]
            classes.append({
                'name': node.name,
                'methods': methods,
                'docstring': ast.get_docstring(node)
            })

    # Public 함수 추출
    functions = []
    for node in tree.body:
        if isinstance(node, ast.FunctionDef) and not node.name.startswith('_'):
            functions.append({
                'name': node.name,
                'docstring': ast.get_docstring(node)
            })

    # Import 문 추출
    imports = []
    for node in tree.body:
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            imports.append(ast.unparse(node))

    return {
        'classes': classes,
        'functions': functions,
        'imports': imports
    }

1.2 노트북 파일명 결정

def get_notebook_path(module_path: Path) -> Path:
    """모듈 경로에서 노트북 경로 결정."""
    # src/data_loader.py → notebooks/0X_data_loader.ipynb

    # notebooks/ 디렉토리 확인
    notebooks_dir = Path('notebooks')
    if not notebooks_dir.exists():
        notebooks_dir = Path.cwd() / 'notebooks'

    # 번호 결정 (3단계 우선순위)
    next_num = get_notebook_number(module_path, notebooks_dir)

    # 파일명 생성
    module_name = module_path.stem  # data_loader
    notebook_name = f"{next_num:02d}_{module_name}.ipynb"

    return notebooks_dir / notebook_name


def get_notebook_number(module_path: Path, notebooks_dir: Path) -> int:
    """노트북 번호 결정 (3단계 우선순위)."""

    # 1순위: docs/tasks/ YAML 파싱
    tasks_dir = Path('docs/tasks')
    if tasks_dir.exists():
        task_files = sorted(tasks_dir.glob('*.md'))
        for task_file in task_files:
            # YAML frontmatter에서 순서 추출
            with open(task_file) as f:
                content = f.read()
                if '---' in content:
                    yaml_block = content.split('---')[1]
                    # 모듈명이 task 파일에 언급되는지 확인
                    if module_path.stem in content.lower():
                        # task 순서 번호 추출
                        import re
                        match = re.search(r'order:\s*(\d+)', yaml_block)
                        if match:
                            return int(match.group(1))

    # 2순위: 마지막 번호 + 1
    existing = sorted(notebooks_dir.glob('*.ipynb'))
    if existing:
        numbers = []
        for nb in existing:
            # 파일명에서 숫자 부분 추출 (01, 02, ...)
            import re
            match = re.match(r'(\d+)_', nb.stem)
            if match:
                numbers.append(int(match.group(1)))

        if numbers:
            return max(numbers) + 1

    # 3순위: len(existing) + 1 (기존 로직)
    return len(existing) + 1

Phase 2: 노트북 생성

핵심 변경 (v2.0): 모든 셀을 markdown 셀 + code 셀 쌍으로 생성한다. 설명은 markdown 셀에, 코드는 code 셀에 분리.

2.1 Cell 구조

cells = [
    # Markdown + Code 쌍으로 생성
    *create_autoreload_cells(),      # 2 cells (md + code)
    *create_import_cells(module_info, module_path),  # 2 cells
    *create_api_cells(module_info),  # N * 2 cells
    *create_conclusion_cells()       # 2 cells
]

2.2 Cell 1: Jupyter 환경 설정

def create_autoreload_cells() -> list[dict]:
    """환경 설정 셀 생성 (markdown + code)."""
    return [
        # Markdown Cell
        {
            'cell_type': 'markdown',
            'metadata': {},
            'source': [
                '## 1. Jupyter 환경 설정\n',
                '\n',
                '자동 리로드 및 경고 억제 설정. **노트북 시작 시 반드시 먼저 실행.**\n',
                '\n',
                '> ⚠️ Magic command는 셀 최상단에 작성해야 한다.'
            ]
        },
        # Code Cell
        {
            'cell_type': 'code',
            'metadata': {},
            'source': [
                '%load_ext autoreload\n',
                '%autoreload 2\n',
                '%matplotlib inline\n',
                '\n',
                'import warnings\n',
                'warnings.filterwarnings("ignore", category=DeprecationWarning)\n',
                '\n',
                'print("✓ Jupyter 환경 설정 완료")'
            ],
            'outputs': [],
            'execution_count': None
        }
    ]

2.3 Cell 2: Import

def create_import_cells(module_info: dict, module_path: Path) -> list[dict]:
    """Import 셀 생성 (markdown + code)."""
    module_import_path = '.'.join(module_path.with_suffix('').parts)
    api_names = [c['name'] for c in module_info['classes']]
    api_names += [f['name'] for f in module_info['functions']]

    return [
        # Markdown Cell
        {
            'cell_type': 'markdown',
            'metadata': {},
            'source': [
                '## 2. 라이브러리 Import\n',
                '\n',
                '**Import 순서**: 표준 라이브러리 → 서드파티 → 로컬 모듈'
            ]
        },
        # Code Cell
        {
            'cell_type': 'code',
            'metadata': {},
            'source': [
                '# 1. 표준 라이브러리\n',
                'import os\n',
                'import sys\n',
                'from pathlib import Path\n',
                'from datetime import datetime\n',
                '\n',
                '# 2. 서드파티 라이브러리\n',
                'import numpy as np\n',
                'import pandas as pd\n',
                'import matplotlib.pyplot as plt\n',
                '\n',
                '# 3. 로컬 모듈\n',
                f'from {module_import_path} import {", ".join(api_names)}\n',
                '\n',
                '# 상수 정의\n',
                'DATA_DIR = Path("data")\n',
                'OUTPUT_DIR = Path("outputs")\n',
                'RANDOM_SEED = 42\n',
                '\n',
                'np.random.seed(RANDOM_SEED)\n',
                'print("✓ 라이브러리 및 설정 완료")'
            ],
            'outputs': [],
            'execution_count': None
        }
    ]

2.4 Cell 3-N: API별 사용 예시

def create_api_cells(module_info: dict) -> list[dict]:
    """API별 사용 예시 셀 생성 (markdown + code 쌍)."""
    cells = []
    cell_num = 3

    # 클래스별 셀
    for cls in module_info['classes']:
        # Markdown Cell
        cells.append({
            'cell_type': 'markdown',
            'metadata': {},
            'source': [
                f'## {cell_num}. {cls["name"]} 사용 예시\n',
                '\n',
                f'{cls["docstring"] or "클래스 설명 없음"}\n',
                '\n',
                '**사용 가능한 메서드**:\n',
                *[f'- `{m}`\n' for m in cls['methods']]
            ]
        })
        # Code Cell
        cells.append({
            'cell_type': 'code',
            'metadata': {},
            'source': [
                f'# TODO: {cls["name"]} 클래스 사용\n',
                f'# instance = {cls["name"]}(...)\n',
                f'# result = instance.{cls["methods"][0] if cls["methods"] else "method"}()\n',
                '# print(result)'
            ],
            'outputs': [],
            'execution_count': None
        })
        cell_num += 1

    # 함수별 셀
    for func in module_info['functions']:
        # Markdown Cell
        cells.append({
            'cell_type': 'markdown',
            'metadata': {},
            'source': [
                f'## {cell_num}. {func["name"]} 함수 사용\n',
                '\n',
                f'{func["docstring"] or "함수 설명 없음"}'
            ]
        })
        # Code Cell
        cells.append({
            'cell_type': 'code',
            'metadata': {},
            'source': [
                f'# TODO: {func["name"]} 함수 사용\n',
                f'# result = {func["name"]}(...)\n',
                '# print(result)'
            ],
            'outputs': [],
            'execution_count': None
        })
        cell_num += 1

    return cells

2.5 Cell N+1: 결론

def create_conclusion_cells() -> list[dict]:
    """결론 셀 생성 (markdown + code)."""
    return [
        # Markdown Cell
        {
            'cell_type': 'markdown',
            'metadata': {},
            'source': [
                '## 결론\n',
                '\n',
                '분석 결과 요약 및 후속 작업.'
            ]
        },
        # Code Cell
        {
            'cell_type': 'code',
            'metadata': {},
            'source': [
                '# TODO: 분석 결과 요약\n',
                'print(f"생성 일시: {datetime.now().strftime(\'%Y-%m-%d %H:%M:%S\')}")'
            ],
            'outputs': [],
            'execution_count': None
        }
    ]

Phase 3: 노트북 저장

import json

def save_notebook(cells: list[dict], output_path: Path):
    """Jupyter Notebook 형식으로 저장."""
    notebook = {
        'cells': cells,
        'metadata': {
            'kernelspec': {
                'display_name': 'Python 3',
                'language': 'python',
                'name': 'python3'
            },
            'language_info': {
                'codemirror_mode': {'name': 'ipython', 'version': 3},
                'file_extension': '.py',
                'mimetype': 'text/x-python',
                'name': 'python',
                'nbconvert_exporter': 'python',
                'pygments_lexer': 'ipython3',
                'version': '3.11.0'
            }
        },
        'nbformat': 4,
        'nbformat_minor': 5
    }

    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(notebook, f, indent=1, ensure_ascii=False)

실행 예시

입력: src/data_loader.py

"""데이터 로더 모듈."""

from pathlib import Path
import pandas as pd

class DataLoader:
    """CSV 파일을 로드하는 클래스."""

    def __init__(self, file_path: Path):
        """초기화."""
        self.file_path = file_path

    def load(self) -> pd.DataFrame:
        """데이터 로드."""
        return pd.read_csv(self.file_path)

    def validate(self, df: pd.DataFrame) -> bool:
        """데이터 검증."""
        return not df.empty

def preprocess_data(df: pd.DataFrame) -> pd.DataFrame:
    """데이터 전처리."""
    return df.dropna()

출력: notebooks/03_data_loader.ipynb

# 번호 03의 결정 이유:
# - 1순위: docs/tasks/feature-x.md에 order: 3 명시 (또는)
# - 2순위: 기존 01, 02 존재 → max(1,2) + 1 = 3 (또는)
# - 3순위: 노트북 2개 존재 → len([01, 02]) + 1 = 3

[Markdown Cell 1]

## 1. Jupyter 환경 설정

자동 리로드 및 경고 억제 설정. **노트북 시작 시 반드시 먼저 실행.**

[Code Cell 1]

%load_ext autoreload
%autoreload 2
%matplotlib inline

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
print("✓ Jupyter 환경 설정 완료")

[Markdown Cell 2]

## 2. 라이브러리 Import

**Import 순서**: 표준 라이브러리 → 서드파티 → 로컬 모듈

[Code Cell 2]

from pathlib import Path
import numpy as np
import pandas as pd

from src.data_loader import DataLoader, preprocess_data

DATA_DIR = Path("data")
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
print("✓ 라이브러리 및 설정 완료")

[Markdown Cell 3]

## 3. DataLoader 사용 예시

CSV 파일을 로드하는 클래스.

**사용 가능한 메서드**:
- `load`
- `validate`

[Code Cell 3]

# TODO: DataLoader 클래스 사용
# loader = DataLoader(DATA_DIR / "file.csv")
# df = loader.load()
# print(df.shape)

[Markdown Cell 4]

## 4. preprocess_data 함수 사용

데이터 전처리.

[Code Cell 4]

# TODO: preprocess_data 함수 사용
# df_clean = preprocess_data(df)
# print(df_clean.shape)

넘버링 전략

우선순위 시스템

노트북 번호는 다음 순서로 결정된다:

flowchart TD
    A[넘버링 시작] --> B{docs/tasks/<br/>존재?}
    B -->|Yes| C[YAML frontmatter<br/>order 필드 파싱]
    C --> D{모듈명<br/>매칭?}
    D -->|Yes| E[order 값 사용]
    D -->|No| F{기존 노트북<br/>존재?}
    B -->|No| F
    F -->|Yes| G[마지막 번호 + 1]
    F -->|No| H[len(existing) + 1]

    E --> I[번호 확정]
    G --> I
    H --> I

1순위: Task 파일 연동

docs/tasks/
디렉토리에 YAML frontmatter가 있는 경우:

---
order: 3
module: data_loader
---

→ 노트북 번호: 03

2순위: 마지막 번호 + 1

기존: 01_eda.ipynb, 03_model.ipynb (02 삭제됨)

→ max(1, 3) + 1 = 04

3순위: 개수 기반 (기존 로직)

기존: 01, 02, 03 (연속)

→ len([01, 02, 03]) + 1 = 04

충돌 회피

상황처리
Task order와 기존 번호 충돌Task order 우선 (1순위)
번호 공백 (01, 03, ...)마지막 번호 +1로 메움 (2순위)
빈 디렉토리01부터 시작 (3순위)

자동화 범위

항목자동 생성사용자 작성
Cell 1-2✅ 완전 자동
Import 문✅ 모듈 API 기반
API 목록✅ AST 파싱
코드 스니펫✅ 최소 예시⚠️ 파라미터 수정
데이터 출력❌ 완전히 수동
분석 로직❌ 비즈니스 규칙

체크리스트

스킬 실행 후:

  • 생성된 노트북 경로 확인
  • Cell 1 실행: 자동 리로드 설정
  • Cell 2 실행: Import 문 동작 확인
  • Cell 3-N: TODO 주석 확인
  • 각 셀에 실제 코드 작성
  • 노트북 전체 실행하여 검증

관련 스킬

스킬역할
[@skills/convention-jupyter-setup/SKILL.md]노트북 규칙 참조
[@skills/extract-module-from-notebook/SKILL.md]노트북 → 모듈 역방향 추출
[@skills/check-notebook-coverage/SKILL.md]모듈-노트북 커버리지 검증

한계 및 주의사항

자동화 불가능한 부분:

  • 어떤 데이터를 출력할지 (비즈니스 로직)
  • 어떤 파라미터를 사용할지 (도메인 지식)
  • 어떤 시각화를 그릴지 (분석 목적)

사용자 책임:

  • TODO 주석을 실제 코드로 채우기
  • 실제 파일 경로 입력
  • 분석 목표에 맞는 출력 선택

원칙:

스킬은 "어떻게(How)" 작성할지 알려주지만, "무엇을(What)" 분석할지는 당신이 결정한다.


Changelog

날짜버전변경 내용
2026-02-022.0.0Breaking: 셀 구조 변경 - markdown + code 셀 쌍으로 생성. convention-jupyter-setup v2.0.0 동기화. Magic keyword 주의사항 추가.
2026-01-281.1.0넘버링 로직 개선: Task 파일 연동, 마지막 번호 +1 우선순위
2026-01-271.0.0초기 생성 - AST 기반 노트북 자동 생성