-
Notifications
You must be signed in to change notification settings - Fork 0
#30 refactor(app): Refresh the user-interface #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
f314362
#30 feat: Add GitHub Actions workflow for running tests and ignore py…
Asifdotexe 902f231
#30 refactor(ui) updated the UI
Asifdotexe 10b8a2a
#30 feat: Add AST-based Python flowchart generation with comprehensiv…
Asifdotexe 64d2f36
#30 feat: Extract themes and examples into a new resources module and…
Asifdotexe 98dd796
#30 refactor(app): remove generate chart button
Asifdotexe 2d8d1c9
#30 refactor: fix grammer
Asifdotexe 07623d8
#30 test: Implement flowchart generation for `break`, `continue`, and…
Asifdotexe 7a854dc
#30 feat: add node type for break, continue and pass blocks
Asifdotexe 07c8c0a
#30 refactor: add OS error handling
Asifdotexe 7ceef37
test: add test to verify break and continue node coloring in generate…
Asifdotexe 9ca826c
#30 refactor(test): replace print statements with pytest assertions
Asifdotexe c3fddb4
#30 chore: update pre-commit config file
Asifdotexe 60a660f
#30 style: enforce PEP-8
Asifdotexe 8fa3973
#30 docs: add module docstrings
Asifdotexe b85384f
#30 ci: added a ci workflow for pylint
Asifdotexe 85b8363
#30 ci: consolidated the pre-commit hook
Asifdotexe d7be57a
#30 remove unused import
Asifdotexe ba9ec66
#30 refactor: comply with constant naming conversation
Asifdotexe 5e7229e
#30 chore: move pylintrc config to pyproject.toml
Asifdotexe e9f25bf
#30 refactor: make the function names snake_case
Asifdotexe 9ab2596
#30 ci: sanitize body content
Asifdotexe ed87671
#30 fix: issues reported by pylint
Asifdotexe 27870ba
#30 refactor: import textwrap at the top of the module
Asifdotexe 26aa5be
#30 refactor: add project root for smoke testing
Asifdotexe 078e2b7
#30 refactor: Extract core flowchart generation logic to a new module…
Asifdotexe 6c6dd10
#30 refactor: Synchronize code input text area with session state usi…
Asifdotexe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,4 +30,4 @@ | |
| "forwardPorts": [ | ||
| 8501 | ||
| ] | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| name: Pylint | ||
|
|
||
| on: [pull_request] | ||
|
|
||
| jobs: | ||
| pylint: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.10" | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install poetry | ||
| poetry install | ||
|
|
||
| - name: Run Pylint | ||
| id: pylint | ||
| continue-on-error: true | ||
| run: | | ||
| # Run pylint on source, app.py, and tests | ||
| # Redirect output to file, also capture stderr | ||
| poetry run pylint src/serpent app.py tests > pylint_report.txt 2>&1 | ||
|
|
||
| - name: Comment on PR | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| try { | ||
| const report = fs.readFileSync('pylint_report.txt', 'utf8'); | ||
| const MAX_LENGTH = 60000; // GitHub comment limit is ~65536 | ||
| let bodyContent = report; | ||
|
|
||
| if (report.length > MAX_LENGTH) { | ||
| bodyContent = report.substring(0, MAX_LENGTH) + "\n\n... (Output truncated due to length)"; | ||
| } | ||
|
|
||
| if (!bodyContent.trim()) { | ||
| bodyContent = "No output captured from Pylint."; | ||
| } | ||
|
|
||
| // Sanitize bodyContent to avoid breaking the template literal | ||
| bodyContent = bodyContent.replace(/`/g, '\\`').replace(/\$\{/g, '\\${'); | ||
|
|
||
| const outcome = '${{ steps.pylint.outcome }}'; | ||
| const icon = outcome === 'success' ? '✅' : '⚠️'; | ||
| const summary = outcome === 'success' ? 'Pylint passed' : 'Pylint found issues'; | ||
|
|
||
| await github.rest.issues.createComment({ | ||
| issue_number: context.issue.number, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body: `### ${icon} ${summary}\n\n<details>\n<summary>View Pylint Report</summary>\n\n\`\`\`\n${bodyContent}\n\`\`\`\n</details>` | ||
| }); | ||
|
|
||
| // If we want to fail the workflow if pylint failed: | ||
| if (outcome === 'failure') { | ||
| core.setFailed('Pylint found issues.'); | ||
| } | ||
| } catch (error) { | ||
| core.setFailed(error.message); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ __pycache__/ | |
| .idea/ | ||
| SERPENT.egg-info/ | ||
| build/ | ||
| .pytest_cache/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| """ | ||
| Launch the Streamlit web application for converting Python functions into flowcharts. | ||
| """ | ||
|
|
||
| import ast | ||
| import logging | ||
| import re | ||
| from typing import Any | ||
| import shutil | ||
| from pathlib import Path | ||
|
|
||
| import streamlit as st | ||
| from PIL import Image | ||
| from streamlit_extras.badges import badge | ||
| from streamlit_extras.stylable_container import stylable_container | ||
|
|
||
| from serpent.core import generate_graphviz_flowchart | ||
| from serpent.resources import EXAMPLES, THEMES | ||
|
|
||
| assets_dir = Path(__file__).parent / "assets" | ||
| try: | ||
| LOGO = Image.open(assets_dir / "serpent_logo_transparent.png") | ||
| ICON = ( | ||
| Image.open(assets_dir / "serpent_logo_compact.png") | ||
| if (assets_dir / "serpent_logo_compact.png").exists() | ||
| else LOGO | ||
| ) | ||
| except (FileNotFoundError, OSError): | ||
| LOGO = None | ||
| ICON = None | ||
|
|
||
| # Page Configuration | ||
| st.set_page_config( | ||
| page_title="SERPENT", | ||
| page_icon=ICON or "🐍", | ||
| layout="wide", | ||
| initial_sidebar_state="expanded", | ||
| ) | ||
|
|
||
| # Custom CSS for cleaner look | ||
| st.markdown( | ||
| """ | ||
| <style> | ||
| #MainMenu {visibility: hidden;} | ||
| footer {visibility: hidden;} | ||
| .stButton>button {width: 100%;} | ||
| /* Hide top padding */ | ||
| .block-container {padding-top: 2rem;} | ||
| </style> | ||
| """, | ||
| unsafe_allow_html=True, | ||
| ) | ||
|
|
||
|
|
||
| def _render_sidebar() -> tuple[str, str, str]: | ||
| """Render the sidebar and return settings.""" | ||
| with st.sidebar: | ||
| if LOGO: | ||
| st.image(LOGO, width="stretch") | ||
| else: | ||
| st.title("SERPENT 🐍") | ||
|
Asifdotexe marked this conversation as resolved.
|
||
|
|
||
| st.write("Turn your Python functions into clear flowcharts.") | ||
| st.divider() | ||
|
|
||
| def update_example(): | ||
| """Callback to update code input when example changes.""" | ||
| ex = st.session_state.example_selector | ||
| if ex != "(Custom)": | ||
| st.session_state.code_area_widget = EXAMPLES[ex] | ||
| st.session_state.code_input = EXAMPLES[ex] | ||
|
|
||
| st.subheader("⚙️ Settings") | ||
|
|
||
| selected_example = st.selectbox( | ||
| "Load Example", | ||
| ["(Custom)"] + list(EXAMPLES.keys()), | ||
| index=0, | ||
| help="Select an example to see how it works.", | ||
| key="example_selector", | ||
| on_change=update_example, | ||
| ) | ||
|
|
||
| st.caption("Appearance") | ||
| rankdir = st.selectbox( | ||
| "Orientation", | ||
| options=["TB", "LR"], | ||
| format_func=lambda x: "Top-Down" if x == "TB" else "Left-Right", | ||
| ) | ||
|
|
||
| theme_name = st.selectbox("Theme", options=list(THEMES.keys())) | ||
|
|
||
| st.divider() | ||
|
|
||
| with st.expander("About & Help"): | ||
| st.markdown( | ||
| """ | ||
| **How to use:** | ||
| 1. Paste your Python function. | ||
| 2. The flowchart updates automatically. | ||
| 3. Download the result. | ||
|
|
||
| **Tips:** | ||
| - Works best with single functions. | ||
| - Supports `if/else`, `loops`, `break/continue`. | ||
| """ | ||
| ) | ||
|
|
||
| st.caption("Created by Asif Sayyed") | ||
| badge( | ||
| type="github", | ||
| name="Asifdotexe/SERPENT", | ||
| url="https://github.com/Asifdotexe/SERPENT", | ||
| ) | ||
| return selected_example, rankdir, theme_name | ||
|
|
||
|
|
||
| def _render_input_area(selected_example: str) -> tuple[str, str]: | ||
| """Render the code input area.""" | ||
| st.subheader("📝 Input Code") | ||
|
|
||
| if selected_example != "(Custom)": | ||
| default_code = EXAMPLES[selected_example] | ||
| else: | ||
| # Keep previous input if possible, else empty | ||
| default_code = "def my_func():\n pass" | ||
|
|
||
| # Initial state | ||
| if "code_input" not in st.session_state: | ||
| st.session_state.code_input = default_code | ||
|
|
||
| def _sync_code_input(): | ||
| """Sync widget state to session state code_input.""" | ||
| st.session_state.code_input = st.session_state.code_area_widget | ||
|
|
||
| # We handle updates via callback now, so we remove the imperative check | ||
| code = st.text_area( | ||
| "Python Code", | ||
| value=st.session_state.code_input, | ||
| height=400, | ||
| label_visibility="collapsed", | ||
| key="code_area_widget", | ||
| on_change=_sync_code_input, | ||
| ) | ||
|
|
||
| chart_title = st.text_input("Chart Title", placeholder="Enter a title (optional)") | ||
| return code, chart_title | ||
|
|
||
|
|
||
| def _render_output_area( | ||
| code: str, chart_title: str, rankdir: str, theme_name: str | ||
| ) -> None: | ||
| """Render the flowchart output area.""" | ||
| valid_graph = None | ||
| st.subheader("🖼️ Flowchart") | ||
|
|
||
| with stylable_container( | ||
| key="output_container", | ||
| css_styles=""" | ||
| { | ||
| border: 1px solid rgba(49, 51, 63, 0.2); | ||
| border-radius: 0.5rem; | ||
| padding: 1rem; | ||
| min-height: 480px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| background-color: rgba(255, 255, 255, 0.05); | ||
| } | ||
| """, | ||
| ): | ||
| if not code.strip(): | ||
| st.info("Waiting for code input...") | ||
| else: | ||
| try: | ||
| # Validate | ||
| ast.parse(code) | ||
|
|
||
| # Generate | ||
| final_title = chart_title or "Flowchart" | ||
| selected_theme = THEMES[theme_name] | ||
|
|
||
| graph = generate_graphviz_flowchart( | ||
| code, | ||
| title=final_title, | ||
| rankdir=rankdir, | ||
| style_config=selected_theme, | ||
| ) | ||
|
|
||
| st.graphviz_chart(graph, width="stretch") | ||
|
|
||
| # Store for download buttons below | ||
| valid_graph = graph | ||
|
|
||
| except SyntaxError as e: | ||
| st.error(f"Syntax Error: {e}") | ||
| valid_graph = None | ||
| except Exception as e: | ||
| st.error(f"An error occurred: {e}") | ||
| logging.exception("Graph generation failed") | ||
| valid_graph = None | ||
| return valid_graph | ||
|
|
||
|
|
||
| def _render_download_buttons(valid_graph: Any, chart_title: str) -> None: | ||
| """Render download buttons if graph is valid.""" | ||
| if not valid_graph: | ||
| return | ||
|
|
||
| st.divider() | ||
| d_col1, d_col2 = st.columns([1, 1]) | ||
|
|
||
| # Sanitize filename | ||
| safe_title = re.sub( | ||
| r"[^a-z0-9_\-]", "", (chart_title or "flowchart").lower().replace(" ", "_") | ||
| ) | ||
|
|
||
| with d_col1: | ||
| if shutil.which("dot"): | ||
| try: | ||
| png_bytes = valid_graph.pipe(format="png") | ||
| st.download_button( | ||
| "📥 Download PNG", | ||
| data=png_bytes, | ||
| file_name=f"{safe_title}.png", | ||
| mime="image/png", | ||
| width="stretch", | ||
| ) | ||
| except Exception: | ||
| st.warning("Could not generate PNG (Check Graphviz installation).") | ||
|
|
||
| with d_col2: | ||
| st.download_button( | ||
| "📄 Download DOT", | ||
| data=valid_graph.source, | ||
| file_name=f"{safe_title}.dot", | ||
| mime="text/vnd.graphviz", | ||
| width="stretch", | ||
| ) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| """Main application entry point.""" | ||
| selected_example, rankdir, theme_name = _render_sidebar() | ||
|
|
||
| col_header, _ = st.columns([3, 1]) | ||
| with col_header: | ||
| st.title("Flowchart Generator") | ||
|
|
||
| input_col, output_col = st.columns(2) | ||
|
|
||
| with input_col: | ||
| code, chart_title = _render_input_area(selected_example) | ||
|
|
||
| with output_col: | ||
| valid_graph = _render_output_area(code, chart_title, rankdir, theme_name) | ||
|
|
||
| # Download Area (Full width below columns) | ||
| _render_download_buttons(valid_graph, chart_title) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Template-literal injection risk — pylint output may contain backticks or
${.bodyContentis interpolated directly into a JS template literal on line 59. Pylint output frequently contains code snippets with backticks (`) or expressions like${...}, which would break the template literal or cause unintended evaluation.Use string concatenation or escape backticks before interpolation.
🔧 Proposed fix
🤖 Prompt for AI Agents