py_engine_desktop 0.0.1 copy "py_engine_desktop: ^0.0.1" to clipboard
py_engine_desktop: ^0.0.1 copied to clipboard

A Flutter plugin for managing embedded Python runtimes on desktop platforms (Windows, macOS, Linux). Run Python scripts and interactive REPLs in your Flutter desktop apps.

py_engine_desktop #

pub package License: MIT

A Flutter plugin for managing embedded Python runtimes on desktop platforms (Windows, macOS, Linux). This plugin allows you to run Python scripts and interactive REPLs directly from your Flutter desktop applications.

🎯 Production Ready - Tested on Windows & macOS, Linux testing in progress

Features #

  • 🐍 Embedded Python Runtime: Automatically downloads and extracts portable Python distributions
  • πŸ–₯️ Desktop Support: Works on Windows, macOS, and Linux
  • πŸ“œ Script Execution: Run Python scripts with real-time stdout/stderr output
  • πŸ”„ Interactive REPL: Start Python REPLs and send commands interactively
  • πŸ“¦ Package Management: Install Python packages using pip
  • πŸš€ Easy Setup: One-time initialization handles everything automatically

Supported Platforms #

Platform Support Architecture Tested
Windows βœ… x64 βœ…
macOS βœ… x64 βœ…
Linux βœ… x64 πŸ”„ In Progress
Android ❌ - -
iOS ❌ - -
Web ❌ - -

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  py_engine_desktop: ^0.0.1

Then run:

flutter pub get

🍎 Important: macOS Configuration #

For macOS apps, you MUST disable sandbox mode to allow Python executable execution.

In your Flutter macOS project, update the following files:

macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements

Change:

<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
</dict>

To:

<dict>
    <key>com.apple.security.app-sandbox</key>
    <false/>
</dict>

⚠️ Note: Disabling sandbox mode is required for executing Python processes. This is a limitation of macOS security model when running external executables. Without this change, you'll get ProcessException: Operation not permitted errors.

Quick Start #

1. Add dependency #

Add this to your pubspec.yaml:

dependencies:
  py_engine_desktop: ^0.0.1

2. Import and Initialize #

import 'package:py_engine_desktop/py_engine_desktop.dart';

class MyPythonApp extends StatefulWidget {
  @override
  _MyPythonAppState createState() => _MyPythonAppState();
}

class _MyPythonAppState extends State<MyPythonApp> {
  bool _initialized = false;
  
  @override
  void initState() {
    super.initState();
    _initializePython();
  }
  
  Future<void> _initializePython() async {
    try {
      await PyEngineDesktop.init();
      setState(() => _initialized = true);
      print('Python engine ready!');
    } catch (e) {
      print('Failed to initialize: $e');
    }
  }
}

3. Run Python Scripts #

Future<void> runPythonScript() async {
  if (!_initialized) return;
  
  // Create a simple Python script
  final script = '''
import math
print("Hello from Python!")
print(f"Pi = {math.pi}")
for i in range(5):
    print(f"Count: {i}")
  ''';
  
  // Write script to temp file
  final tempDir = await getTemporaryDirectory();
  final scriptFile = File(path.join(tempDir.path, 'my_script.py'));
  await scriptFile.writeAsString(script);
  
  // Execute the script
  final pythonScript = await PyEngineDesktop.startScript(scriptFile.path);
  
  // Listen to output
  pythonScript.stdout.listen((line) {
    print('Python Output: $line');
  });
  
  pythonScript.stderr.listen((line) {
    print('Python Error: $line');
  });
  
  // Wait for completion
  await pythonScript.exitCode;
  print('Script completed!');
}

4. Interactive Python REPL #

PythonRepl? _repl;

Future<void> startInteractivePython() async {
  if (!_initialized) return;
  
  _repl = await PyEngineDesktop.startRepl();
  
  // Listen to all output
  _repl!.output.listen((output) {
    print('REPL: $output');
  });
  
  // Send some commands
  _repl!.send('import numpy as np');
  _repl!.send('arr = np.array([1, 2, 3, 4, 5])');
  _repl!.send('print("Array:", arr)');
  _repl!.send('print("Mean:", np.mean(arr))');
}

void sendCommand(String command) {
  if (_repl != null) {
    _repl!.send(command);
  }
}

5. Package Management #

Future<void> setupPythonPackages() async {
  if (!_initialized) return;
  
  // Install essential packages
  await PyEngineDesktop.pipInstall('numpy');
  await PyEngineDesktop.pipInstall('pandas');
  await PyEngineDesktop.pipInstall('requests');
  
  print('Packages installed successfully!');
}

// Test if packages work
Future<void> testPackages() async {
  final repl = await PyEngineDesktop.startRepl();
  
  repl.output.listen((output) => print(output));
  
  // Test numpy
  repl.send('import numpy as np');
  repl.send('print("NumPy version:", np.__version__)');
  
  // Test pandas
  repl.send('import pandas as pd');
  repl.send('df = pd.DataFrame({"A": [1,2,3], "B": [4,5,6]})');
  repl.send('print(df)');
  
  repl.stop();
}

6. Complete Widget Example #

class PythonConsole extends StatefulWidget {
  @override
  _PythonConsoleState createState() => _PythonConsoleState();
}

class _PythonConsoleState extends State<PythonConsole> {
  final TextEditingController _controller = TextEditingController();
  final List<String> _output = [];
  PythonRepl? _repl;
  bool _initialized = false;

  @override
  void initState() {
    super.initState();
    _initPython();
  }

  Future<void> _initPython() async {
    await PyEngineDesktop.init();
    _repl = await PyEngineDesktop.startRepl();
    
    _repl!.output.listen((line) {
      setState(() => _output.add(line));
    });
    
    setState(() => _initialized = true);
  }

  void _sendCommand() {
    final command = _controller.text.trim();
    if (command.isNotEmpty && _repl != null) {
      setState(() => _output.add('>>> $command'));
      _repl!.send(command);
      _controller.clear();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: Container(
            padding: EdgeInsets.all(8),
            color: Colors.black,
            child: ListView.builder(
              itemCount: _output.length,
              itemBuilder: (context, index) => Text(
                _output[index],
                style: TextStyle(color: Colors.green, fontFamily: 'monospace'),
              ),
            ),
          ),
        ),
        if (_initialized)
          Padding(
            padding: EdgeInsets.all(8),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(hintText: 'Enter Python command...'),
                    onSubmitted: (_) => _sendCommand(),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.send),
                  onPressed: _sendCommand,
                ),
              ],
            ),
          ),
      ],
    );
  }

  @override
  void dispose() {
    _repl?.stop();
    super.dispose();
  }
}

API Reference #

PyEngineDesktop #

Main class providing static methods for Python engine management.

PyEngineDesktop.init()

Purpose: Initializes the Python engine by extracting and setting up the embedded Python runtime.

What it does:

  • Extracts Python runtime from bundled assets (first run only)
  • Sets up Python executable with proper permissions
  • Configures site-packages directory for pip installations
  • Caches runtime for faster subsequent launches
// Basic initialization
await PyEngineDesktop.init();

// With error handling
try {
  await PyEngineDesktop.init();
  print('Python engine ready!');
} catch (e) {
  if (e is UnsupportedError) {
    print('Platform not supported');
  } else {
    print('Initialization failed: $e');
  }
}

PyEngineDesktop.startScript(String scriptPath)

Purpose: Executes a Python script file and returns a PythonScript object for monitoring.

What it does:

  • Validates script file exists before execution
  • Starts Python process with the script
  • Automatically includes site-packages in Python path
  • Returns PythonScript object for output monitoring and control
// Basic script execution
final script = await PyEngineDesktop.startScript('/path/to/script.py');

// Listen to output streams
script.stdout.listen((line) => print('Output: $line'));
script.stderr.listen((line) => print('Error: $line'));

// Wait for completion
final exitCode = await script.exitCode;
print('Script finished with code: $exitCode');

// Or stop manually if needed
script.stop();

PyEngineDesktop.startRepl()

Purpose: Starts an interactive Python REPL (Read-Eval-Print Loop) session.

What it does:

  • Launches Python in interactive mode using code.interact()
  • Automatically includes site-packages for installed packages
  • Combines stdout/stderr into single output stream
  • Allows sending commands via send() method
// Start REPL and send commands
final repl = await PyEngineDesktop.startRepl();

// Listen to all output (both results and prompts)
repl.output.listen((output) => print(output));

// Send Python commands
repl.send('print("Hello Python!")');
repl.send('x = 5 + 3');
repl.send('print(f"Result: {x}")');

// Send multi-line code
repl.send('for i in range(3):');
repl.send('    print(f"Count: {i}")');

// Stop when done
repl.stop();

PyEngineDesktop.pipInstall(String package) / PyEngineDesktop.pipUninstall(String package)

Purpose: Manages Python packages using pip package manager.

What it does:

  • pipInstall: Downloads and installs Python packages from PyPI
  • pipUninstall: Removes installed Python packages
  • Automatically downloads and sets up pip if not available
  • Installs packages to embedded Python's site-packages directory
// Install packages
try {
  await PyEngineDesktop.pipInstall('numpy');
  await PyEngineDesktop.pipInstall('requests==2.28.1'); // Specific version
  print('Packages installed successfully');
} catch (e) {
  print('Installation failed: $e');
}

// Uninstall packages  
try {
  await PyEngineDesktop.pipUninstall('numpy');
  print('Package uninstalled successfully');
} catch (e) {
  print('Uninstallation failed: $e');
}

PyEngineDesktop.pythonPath

Purpose: Gets the absolute path to the embedded Python executable.

What it does:

  • Returns full path to Python executable after initialization
  • Throws StateError if called before init()
  • Path points to embedded Python runtime, not system Python
// Get Python executable path
await PyEngineDesktop.init();
final pythonPath = PyEngineDesktop.pythonPath;
print('Python executable: $pythonPath');
// Output example: C:\Users\...\AppData\Roaming\py_engine_desktop\python\python.exe

PyEngineDesktop.stopScript(PythonScript script)

Purpose: Stops a running Python script process.

What it does:

  • Terminates the script process immediately
  • Closes output streams
  • Safe to call multiple times
final script = await PyEngineDesktop.startScript('script.py');
// ... later
await PyEngineDesktop.stopScript(script);
// Or use script.stop() directly

PyEngineDesktop.stopRepl(PythonRepl repl)

Purpose: Stops a running Python REPL session.

What it does:

  • Terminates the REPL process
  • Closes output streams
  • Safe to call multiple times
final repl = await PyEngineDesktop.startRepl();
// ... later
await PyEngineDesktop.stopRepl(repl);
// Or use repl.stop() directly

PythonScript #

Object returned by startScript() representing a running Python script.

Properties:

  • Stream<String> stdout - Script's standard output (line by line)
  • Stream<String> stderr - Script's error output (line by line)
  • Process process - Underlying Dart process object
  • Future<int> exitCode - Completes when script finishes with exit code

Methods:

  • void stop() - Terminates the script immediately

PythonRepl #

Object returned by startRepl() representing an interactive Python session.

Properties:

  • Stream<String> output - Combined stdout/stderr output stream
  • Process process - Underlying Dart process object
  • Future<int> exitCode - Completes when REPL session ends

Methods:

  • void send(String code) - Sends Python code to execute
  • void stop() - Terminates the REPL session

Example Usage #

Check out the example directory for a complete demo application that shows:

  • Python engine initialization
  • Running Python scripts with output display
  • Interactive REPL with command input
  • Installing and testing NumPy package

How It Works #

Python Runtime Distribution #

The plugin automatically downloads portable Python distributions:

  • Windows: Embeddable Python distribution (python.org)
  • macOS/Linux: Standalone Python builds (python-build-standalone)

File Locations #

Python runtimes are extracted to platform-specific application support directories:

  • Windows: %AppData%/py_engine_desktop/python
  • macOS: ~/Library/Application Support/py_engine_desktop/python
  • Linux: ~/.local/share/py_engine_desktop/python

First Run Setup #

On first initialization:

  1. Detects the current platform
  2. Extracts the appropriate Python runtime from assets
  3. Sets up the Python environment
  4. Subsequent runs use the cached Python installation

Error Handling #

The plugin provides comprehensive error handling:

try {
  await PyEngineDesktop.init();
} catch (e) {
  if (e is UnsupportedError) {
    print('Platform not supported: $e');
  } else {
    print('Initialization failed: $e');
  }
}

Limitations #

  • Desktop Only: Only works on desktop platforms (Windows, macOS, Linux)
  • Single Architecture: Currently supports x64 architectures only
  • Python Version: Uses Python 3.11.x
  • Size: Python runtimes add ~10-30MB to your app's size
  • First Run: Initial setup requires extracting Python runtime (one-time delay)

Performance & Size #

Platform Runtime Size First Init Time
Windows ~15MB 2-5 seconds
macOS ~25MB 3-7 seconds
Linux ~30MB 3-8 seconds

Note: Python runtime is cached after first initialization. Subsequent app launches are instant.

Development #

Building from Source #

git clone https://github.com/NagarChinmay/py_engine_desktop.git
cd py_engine_desktop
flutter pub get
cd example
flutter run

Running Tests #

flutter test

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Issues & Support #

License #

This project is licensed under the MIT License - see the LICENSE file for details.

Changelog #

See CHANGELOG.md for a list of changes.

2
likes
130
points
98
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for managing embedded Python runtimes on desktop platforms (Windows, macOS, Linux). Run Python scripts and interactive REPLs in your Flutter desktop apps.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

archive, flutter, path, path_provider, plugin_platform_interface

More

Packages that depend on py_engine_desktop

Packages that implement py_engine_desktop