You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

20 KiB

BadGUI Workflow Analysis: From Python to Vue.js

Overview

BadGUI is a Python framework that generates complete Vue.js/Quasar projects using a NiceGUI-like syntax. This document traces the complete workflow from Python code to a running Vue.js application.

Table of Contents

  1. Python Code Structure
  2. Component Creation and Storage
  3. Page Management
  4. Build Process
  5. Vue.js Generation
  6. Project Structure Creation
  7. Development Server
  8. Areas for Improvement

1. Python Code Structure

Example Input Code

import badgui as bg

with bg.page("/", "HomePage", "Home"):
    with bg.column() as main_col:
        main_col.classes("q-pa-lg q-gutter-md")
        bg.label("Hello BadGUI!").classes("text-h3")
        
        name_input = bg.input("Enter your name")
        name_input.bind_value("userName")
        
        bg.button("Greet").on_click("greetUser").classes("q-btn-primary")

bg.script("""
const userName = ref('');
const greetUser = () => {
    $q.notify(`Hello, ${userName.value}!`);
};
""")

bg.dev(port=3000)

Key Framework Entry Points

File: badgui/__init__.py

from .core import App

# Main app instance
app = App()

# Convenience functions that delegate to the main app
def label(text: str, **kwargs):
    return app.label(text, **kwargs)

def button(text: str, icon: str = None, color: str = 'primary', **kwargs):
    return app.button(text, icon, color, **kwargs)

Analysis: All component functions are convenience wrappers around a singleton App instance. This centralizes state management but could limit flexibility for multiple app instances.


2. Component Creation and Storage

Component Class Structure

File: badgui/core.py

class Component:
    def __init__(self, component_type: str, **props):
        self.component_type = component_type
        self.id = f"{component_type}_{id(self)}"
        self._props = props
        self._classes = []
        self._style = {}
        self.children = []
        self._vue_ref = None
    
    def classes(self, class_names: Union[str, List[str]]):
        if isinstance(class_names, str):
            self._classes = class_names.split()
        else:
            self._classes = class_names
        return self
    
    def vue_ref(self, ref_name: str):
        self._vue_ref = ref_name
        self._props['ref'] = ref_name
        return self

Analysis: Components use method chaining for fluent API. Each component gets a unique ID based on memory address. Props, classes, and styles are stored separately but could be unified.

Component Addition Process

File: badgui/core.py

class App:
    def _add_component(self, component: Component) -> Component:
        page = self._get_current_page()
        
        # Add to current container (if in context) or page
        if page.current_container:
            page.current_container.children.append(component)
        else:
            page.components.append(component)
        
        return component
    
    def label(self, text: str, **kwargs) -> Component:
        component = Component('label', text=text, **kwargs)
        return self._add_component(component)

Analysis: Components are added to either the current page or the current container (for nested structures). The container stack manages nesting levels.


3. Page Management

Page Class and Context Management

File: badgui/core.py

class Page:
    def __init__(self, path: str, name: str, title: str = None):
        self.path = path
        self.name = name
        self.title = title or name
        self.components = []
        self.current_container = None
        self._container_stack = []
        self._scripts = []

class App:
    def __init__(self):
        self.pages = {}
        self.current_page = None
    
    @contextmanager
    def page(self, path: str, name: str = None, title: str = None):
        # Create or get existing page
        page = Page(path, name or self._generate_page_name(path), title)
        self.pages[path] = page
        
        # Set as current page
        old_page = self.current_page
        self.current_page = page
        
        try:
            yield page
        finally:
            self.current_page = old_page

Analysis: Pages use context managers to maintain state. The current page is tracked globally, which could cause issues with concurrent page creation.

Container Context Management

@contextmanager
def column(self, **kwargs):
    component = Component('column', classes=['column'], **kwargs)
    component = self._add_component(component)
    
    # Enter container context
    page = self._get_current_page()
    page._container_stack.append(page.current_container)
    page.current_container = component
    
    try:
        yield component
    finally:
        # Exit container context
        page.current_container = page._container_stack.pop()

Analysis: Container nesting uses a stack-based approach. This works well but could be error-prone if exceptions occur during context management.


4. Build Process

Build Initiation

File: badgui/core.py

def build(self, output_dir: str = "./badgui-output", project_name: str = "badgui-app", **kwargs):
    from .generator import VueGenerator
    
    print(f"🔧 Building BadGUI project...")
    print(f"📁 Output directory: {output_dir}")
    
    generator = VueGenerator(self)
    generator.generate_project(output_dir, project_name, **kwargs)
    
    print("✅ BadGUI project generated successfully!")

Analysis: Build process is straightforward but lacks error handling and progress reporting for large projects.

Generator Initialization

File: badgui/generator.py

class VueGenerator:
    def __init__(self, app: App):
        self.app = app
    
    def generate_project(self, output_dir: str, project_name: str = "badgui-app", **kwargs):
        os.makedirs(output_dir, exist_ok=True)
        
        self._create_project_structure(output_dir, project_name)
        self._generate_package_json(output_dir, project_name)
        self._generate_quasar_config(output_dir)
        self._generate_main_layout(output_dir)
        self._generate_pages(output_dir)
        self._generate_router(output_dir)
        self._generate_app_vue(output_dir)
        self._generate_boot_files(output_dir)
        self._generate_css_files(output_dir)
        self._copy_static_files(output_dir)

Analysis: Generation follows a fixed sequence. Each step is independent, which is good for maintainability but doesn't allow for conditional generation based on features used.


5. Vue.js Generation

Component-to-Vue Template Conversion

File: badgui/core.py

def to_vue_template(self, indent: int = 0) -> str:
    spaces = "  " * indent
    
    # Build attributes
    attrs = []
    attrs.append(f'id="{self.id}"')
    
    if self._classes:
        attrs.append(f'class="{" ".join(self._classes)}"')
    
    # Add other props
    for key, value in self._props.items():
        if key not in ['classes', 'style', 'id']:
            if isinstance(value, bool) and value:
                attrs.append(key)
            elif isinstance(value, str):
                attrs.append(f'{key}="{value}"')
    
    attrs_str = " " + " ".join(attrs)
    vue_tag = self.get_vue_tag()
    
    # Generate template
    if self.children:
        template = f"{spaces}<{vue_tag}{attrs_str}>\n"
        for child in self.children:
            template += child.to_vue_template(indent + 1)
        template += f"{spaces}</{vue_tag}>\n"
    else:
        content = self.get_content()
        if content:
            template = f"{spaces}<{vue_tag}{attrs_str}>{content}</{vue_tag}>\n"
        else:
            template = f"{spaces}<{vue_tag}{attrs_str} />\n"
    
    return template

def get_vue_tag(self) -> str:
    component_map = {
        'label': 'q-item-label',
        'button': 'q-btn',
        'input': 'q-input',
        'column': 'div',
        'card': 'q-card',
        'dialog': 'q-dialog',
        'div': 'div'
    }
    return component_map.get(self.component_type, self.component_type)

Analysis: Template generation is recursive and handles nested components well. The component mapping is hardcoded, which makes adding new components require code changes.

Page Component Generation

File: badgui/generator.py

def _generate_page_component(self, output_dir: str, page):
    template_content = self._generate_vue_template_for_page(page)
    script_imports, script_content = self._process_page_scripts(page, output_dir)
    
    page_content = f'''<template>
  <q-page class="row items-center justify-evenly">
{template_content}
  </q-page>
</template>

<script setup>
import {{ ref, onMounted, watch }} from 'vue'
import {{ useQuasar }} from 'quasar'
{script_imports}

// Component state
const state = ref({{}})
const $q = useQuasar()

// Dialog state for Quasar dialogs
const dialogOpen = ref(false)

// Make window object available to template
const window = globalThis.window

{script_content}
</script>'''
    
    page_path = os.path.join(output_dir, 'src', 'pages', f'{page.name}.vue')
    with open(page_path, 'w') as f:
        f.write(page_content)

Analysis: Page generation is template-based with string formatting. This approach works but is fragile and hard to modify. A proper template engine would be more robust.

Script Processing

def _process_page_scripts(self, page, output_dir: str) -> tuple:
    script_imports = ""
    script_content = ""
    
    # Collect Vue refs
    vue_refs = self._collect_vue_refs(page)
    if vue_refs:
        for ref_name in vue_refs:
            script_content += f"\n// Vue ref for component\nconst {ref_name} = ref(null);\n"
    
    # Process script components
    if hasattr(page, '_scripts'):
        for script_component in page._scripts:
            props = script_component._props
            
            if props.get('_is_inline'):
                content = props.get('_script_content', '')
                script_content += f"\n// Inline script\n{content}\n"
                
            elif props.get('_external_file'):
                filename = props.get('_external_file')
                import_mode = props.get('_import_mode', False)
                
                if import_mode:
                    # ES6 module import
                    utils_dir = os.path.join(output_dir, 'src', 'utils')
                    os.makedirs(utils_dir, exist_ok=True)
                    
                    module_name = os.path.splitext(filename)[0]
                    script_imports += f"import * as {module_name} from '../utils/{filename}';\n"
                else:
                    # Traditional script tag
                    js_dir = os.path.join(output_dir, 'public', 'js')
                    os.makedirs(js_dir, exist_ok=True)
    
    return script_imports, script_content

Analysis: Script processing handles both inline and external scripts with ES6 module support. The dual approach (traditional vs module imports) adds complexity but provides flexibility.


6. Project Structure Creation

Package.json Generation

File: badgui/generator.py

def _generate_package_json(self, output_dir: str, project_name: str):
    package_json = {
        "name": project_name,
        "version": "0.0.1",
        "scripts": {
            "dev": "quasar dev",
            "build": "quasar build"
        },
        "dependencies": {
            "@quasar/extras": "^1.16.4",
            "quasar": "^2.12.0",
            "vue": "^3.3.4",
            "vue-router": "^4.2.4"
        },
        "devDependencies": {
            "@quasar/app-vite": "^1.3.0",
            "typescript": "^5.1.6"
        }
    }
    
    with open(os.path.join(output_dir, 'package.json'), 'w') as f:
        json.dump(package_json, f, indent=2)

Analysis: Dependencies are hardcoded with specific versions. This ensures consistency but makes updates difficult and doesn't allow for feature-based dependency inclusion.

Router Generation

def _generate_router(self, output_dir: str):
    page_routes = []
    for path, page in self.app.pages.items():
        route = f"      {{ path: '{path}', component: () => import('pages/{page.name}.vue') }}"
        page_routes.append(route)
    
    routes_content = f'''const routes = [
  {{
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
{",".join([chr(10) + route for route in page_routes])}
    ]
  }}
]

export default routes'''

Analysis: Router generation is dynamic based on defined pages. This works well for basic routing but doesn't support advanced features like route guards or nested routing.


7. Development Server

Dev Server Implementation

File: badgui/core.py

def dev(self, port: int = 9000, host: str = "localhost", auto_reload: bool = False, 
        project_name: str = "badgui-dev", **kwargs):
    import tempfile
    import subprocess
    
    # Create temporary build
    temp_dir = tempfile.mkdtemp(prefix="badgui_dev_")
    print(f"🔧 Creating temporary build...")
    print(f"📁 Temp directory: {temp_dir}")
    
    self.build(temp_dir, project_name, **kwargs)
    
    # Install dependencies and run dev server
    print("📦 Installing dependencies...")
    subprocess.run(["npm", "install"], cwd=temp_dir, check=True, capture_output=True)
    
    print(f"🚀 Starting dev server at http://{host}:{port}")
    env = os.environ.copy()
    env['PORT'] = str(port)
    env['HOST'] = host
    
    try:
        subprocess.run(["npm", "run", "dev"], cwd=temp_dir, env=env, check=True)
    except KeyboardInterrupt:
        print("\n🛑 Dev server stopped")
    finally:
        # Cleanup temp directory
        shutil.rmtree(temp_dir, ignore_errors=True)

Analysis: Dev server creates temporary builds, which is inefficient for development. A proper dev server would support hot reloading and incremental builds.


8. Areas for Improvement

8.1 Architecture Improvements

Issue: Singleton App Instance

Current Code:

# badgui/__init__.py
app = App()  # Global singleton

def label(text: str, **kwargs):
    return app.label(text, **kwargs)

Problem: Single global instance limits flexibility and testing.

Improvement: Support multiple app instances:

class BadGUI:
    def __init__(self):
        self.app = App()
    
    def label(self, text: str, **kwargs):
        return self.app.label(text, **kwargs)

# Default instance for convenience
bg = BadGUI()
label = bg.label

Issue: Component Mapping Hardcoded

Current Code:

def get_vue_tag(self) -> str:
    component_map = {
        'label': 'q-item-label',
        'button': 'q-btn',
        # ... hardcoded mapping
    }
    return component_map.get(self.component_type, self.component_type)

Improvement: Plugin-based component system:

class ComponentRegistry:
    def __init__(self):
        self._components = {}
    
    def register(self, component_type: str, vue_tag: str, props_mapper=None):
        self._components[component_type] = {
            'vue_tag': vue_tag,
            'props_mapper': props_mapper
        }
    
    def get_vue_tag(self, component_type: str) -> str:
        return self._components.get(component_type, {}).get('vue_tag', component_type)

8.2 Performance Improvements

Issue: Inefficient Dev Server

Current Code:

def dev(self, port: int = 9000, **kwargs):
    temp_dir = tempfile.mkdtemp(prefix="badgui_dev_")
    self.build(temp_dir, project_name, **kwargs)  # Full rebuild every time
    subprocess.run(["npm", "install"], cwd=temp_dir)  # Reinstall dependencies

Improvement: Incremental builds and dependency caching:

class DevServer:
    def __init__(self, app: App):
        self.app = app
        self._build_cache = {}
        self._dependency_cache = None
    
    def start(self, port: int = 9000):
        if not self._needs_full_rebuild():
            self._incremental_build()
        else:
            self._full_build()
        
        if not self._dependency_cache:
            self._install_dependencies()

Issue: String-based Template Generation

Current Code:

page_content = f'''<template>
  <q-page class="row items-center justify-evenly">
{template_content}
  </q-page>
</template>'''

Improvement: Template engine with caching:

from jinja2 import Environment, FileSystemLoader

class TemplateGenerator:
    def __init__(self):
        self.env = Environment(loader=FileSystemLoader('templates'))
        self._template_cache = {}
    
    def render_page(self, page: Page) -> str:
        template = self.env.get_template('page.vue.j2')
        return template.render(page=page, components=page.components)

8.3 Error Handling and Validation

Issue: No Component Validation

Current Code:

def button(self, text: str, icon: str = None, **kwargs) -> Component:
    props = {'label': text, **kwargs}
    component = Component('button', **props)
    return self._add_component(component)

Improvement: Schema validation:

from pydantic import BaseModel, validator

class ButtonProps(BaseModel):
    label: str
    icon: Optional[str] = None
    color: str = 'primary'
    
    @validator('color')
    def validate_color(cls, v):
        valid_colors = ['primary', 'secondary', 'accent', 'positive', 'negative']
        if v not in valid_colors:
            raise ValueError(f'Invalid color: {v}')
        return v

def button(self, text: str, **kwargs) -> Component:
    props = ButtonProps(label=text, **kwargs)
    component = Component('button', **props.dict())
    return self._add_component(component)

8.4 Type Safety and IDE Support

Issue: Limited Type Hints

Current Code:

def classes(self, class_names):  # No type hints
    # Implementation

Improvement: Full type safety:

from typing import Union, List, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from .core import Component

def classes(self, class_names: Union[str, List[str]]) -> 'Component':
    """Add CSS classes to the component."""
    # Implementation

8.5 Testing and Debugging

Issue: No Built-in Testing Support

Improvement: Add testing utilities:

class TestRenderer:
    def __init__(self, app: App):
        self.app = app
    
    def render_component(self, component: Component) -> Dict:
        """Render component to testable structure."""
        return {
            'type': component.component_type,
            'props': component._props,
            'classes': component._classes,
            'children': [self.render_component(child) for child in component.children]
        }
    
    def find_components(self, component_type: str) -> List[Component]:
        """Find all components of a specific type."""
        # Implementation

Conclusion

BadGUI provides a solid foundation for generating Vue.js applications from Python code. The workflow is straightforward and functional, but there are several areas where improvements could significantly enhance the framework:

  1. Architecture: Move away from singleton pattern, implement plugin system
  2. Performance: Add incremental builds, template caching, dependency management
  3. Developer Experience: Improve error handling, add type safety, better IDE support
  4. Extensibility: Plugin-based component system, customizable generators
  5. Testing: Built-in testing utilities and debugging tools

The current implementation demonstrates the core concepts well but would benefit from a more modular, extensible architecture for production use.


Document generated on: September 28, 2025
BadGUI Version: 0.2.0