Skip to content

Commit f2149a8

Browse files
committed
Refactor and modernize DotNetPythonEmbed library
- Updated .csproj with metadata, .NET 8 target, and NuGet packaging. - Refactored PythonEmbedManager with async methods and better error handling. - Added `PythonEmbedUrl` and `PythonDir` properties for configurability. - Introduced `RemovePythonEnvironment` and `GetEnvironmentVars` methods. - Enhanced `RunProcess` for async execution and real-time logging. - Updated Program.cs to use new async methods and callbacks. - Rewrote unit tests to support async methods and added mocks. - Added a detailed README with usage examples and API reference. - Improved cross-platform support and code maintainability.
1 parent ff7077d commit f2149a8

5 files changed

Lines changed: 548 additions & 153 deletions

File tree

readme.md

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
# DotNetPythonEmbed
2+
3+
[![NuGet](https://img.shields.io/nuget/v/DotNetPythonEmbed.svg)](https://www.nuget.org/packages/DotNetPythonEmbed/)
4+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5+
6+
A .NET 8 library that provides utilities for managing an embedded Python distribution and virtual environments within .NET applications. This package enables seamless integration of Python scripts and packages into your .NET projects without requiring users to have Python installed on their system.
7+
8+
## Features
9+
10+
- 🐍 **Automatic Python Distribution Management** - Downloads and extracts an embedded Python distribution on first use
11+
- 📦 **Virtual Environment Support** - Creates and manages isolated Python virtual environments
12+
- 🔧 **Pip Integration** - Installs Python packages from requirements.txt files
13+
- 🚀 **Script Execution** - Run Python scripts with full environment isolation
14+
- 🔄 **Cross-Platform Ready** - Supports both Windows and Unix-based systems
15+
- 🧹 **Cleanup Utilities** - Remove Python environments when no longer needed
16+
17+
## Installation
18+
19+
Install the package via NuGet:
20+
21+
```bash
22+
dotnet add package DotNetPythonEmbed
23+
```
24+
25+
Or via the Package Manager Console:
26+
27+
```powershell
28+
Install-Package DotNetPythonEmbed
29+
```
30+
31+
## Quick Start
32+
33+
```csharp
34+
using DotNetPythonEmbed;
35+
36+
// Define the directory where Python will be installed
37+
var pythonDirectory = Path.Combine(AppContext.BaseDirectory, "python-runtime");
38+
39+
// Create the manager
40+
var embedManager = new PythonEmbedManager(pythonDirectory);
41+
42+
// Initialize the Python environment (downloads Python if not present)
43+
await embedManager.InitPythonEnvironment(
44+
onOutput: Console.WriteLine,
45+
onError: Console.Error.WriteLine
46+
);
47+
48+
// Install dependencies from requirements.txt
49+
var requirementsPath = "path/to/requirements.txt";
50+
if (File.Exists(requirementsPath))
51+
{
52+
await embedManager.InstallRequirement(requirementsPath, Console.WriteLine, Console.Error.WriteLine);
53+
}
54+
55+
// Run a Python script
56+
var scriptPath = "path/to/script.py";
57+
await embedManager.RunPython(scriptPath, "--arg1 value1", null, Console.WriteLine, Console.Error.WriteLine);
58+
```
59+
60+
## Usage
61+
62+
### 1. Initialize the Python Environment
63+
64+
```csharp
65+
var pythonDir = Path.Combine(AppContext.BaseDirectory, "python-runtime");
66+
var manager = new PythonEmbedManager(pythonDir);
67+
68+
// Optional: Override the default Python version
69+
manager.PythonEmbedUrl = "https://www.python.org/ftp/python/3.13.9/python-3.13.9-embed-amd64.zip";
70+
71+
// Initialize (downloads and sets up Python + pip + virtualenv)
72+
await manager.InitPythonEnvironment(
73+
onOutput: msg => Console.WriteLine(msg),
74+
onError: err => Console.Error.WriteLine(err)
75+
);
76+
```
77+
78+
The `InitPythonEnvironment` method:
79+
- Checks if Python is already installed at the specified directory
80+
- If not, downloads the embedded Python distribution
81+
- Extracts the distribution
82+
- Installs pip
83+
- Creates a virtual environment named `venv`
84+
85+
### 2. Install Python Packages
86+
87+
```csharp
88+
var requirementsPath = Path.Combine(repoDirectory, "requirements.txt");
89+
90+
await manager.InstallRequirement(
91+
requirementsPath,
92+
onOutput: Console.WriteLine,
93+
onError: Console.Error.WriteLine
94+
);
95+
```
96+
97+
The `InstallRequirement` method uses pip within the virtual environment to install packages specified in a requirements.txt file.
98+
99+
### 3. Run Python Scripts
100+
101+
```csharp
102+
var scriptPath = Path.Combine(projectDirectory, "script.py");
103+
var workingDirectory = projectDirectory;
104+
var scriptArguments = "--input data.txt --output results.json";
105+
106+
int exitCode = await manager.RunPython(
107+
pythonScript: scriptPath,
108+
parameters: scriptArguments,
109+
workingDirectory: workingDirectory,
110+
onOutput: Console.WriteLine,
111+
onError: Console.Error.WriteLine
112+
);
113+
114+
if (exitCode != 0)
115+
{
116+
Console.WriteLine($"Script failed with exit code: {exitCode}");
117+
}
118+
```
119+
120+
### 4. Clean Up
121+
122+
```csharp
123+
// Remove the Python environment if needed
124+
manager.RemovePythonEnvironment(
125+
onOutput: Console.WriteLine,
126+
onError: Console.Error.WriteLine
127+
);
128+
```
129+
130+
## Complete Example
131+
132+
Here's a complete example that clones a GitHub repository and runs a Python script from it:
133+
134+
```csharp
135+
using DotNetPythonEmbed;
136+
137+
var repoUrl = "https://github.com/username/python-project.git";
138+
var baseDirectory = AppContext.BaseDirectory;
139+
var repoDirectory = Path.Combine(baseDirectory, "python-project");
140+
var pythonDirectory = Path.Combine(baseDirectory, "python-runtime");
141+
142+
try
143+
{
144+
// Clone the repository (using LibGit2Sharp or similar)
145+
Console.WriteLine($"Cloning '{repoUrl}' to '{repoDirectory}'.");
146+
if (!Directory.Exists(repoDirectory))
147+
{
148+
// Repository.Clone(repoUrl, repoDirectory);
149+
}
150+
151+
// Initialize Python environment
152+
Console.WriteLine("Initializing embedded Python environment.");
153+
var embedManager = new PythonEmbedManager(pythonDirectory);
154+
await embedManager.InitPythonEnvironment(OnOutput, OnError);
155+
156+
// Install requirements
157+
var requirementsPath = Path.Combine(repoDirectory, "requirements.txt");
158+
if (File.Exists(requirementsPath))
159+
{
160+
Console.WriteLine("Installing Python requirements.");
161+
await embedManager.InstallRequirement(requirementsPath, OnOutput, OnError);
162+
}
163+
164+
// Run the script
165+
var scriptPath = Path.Combine(repoDirectory, "main.py");
166+
Console.WriteLine($"Executing script '{scriptPath}'.");
167+
int exitCode = await embedManager.RunPython(
168+
scriptPath,
169+
string.Empty,
170+
repoDirectory,
171+
OnOutput,
172+
OnError
173+
);
174+
175+
Console.WriteLine($"Script completed with exit code: {exitCode}");
176+
177+
// Optional: Clean up
178+
embedManager.RemovePythonEnvironment(OnOutput, OnError);
179+
}
180+
catch (Exception ex)
181+
{
182+
Console.Error.WriteLine($"An error occurred: {ex.Message}");
183+
}
184+
185+
void OnOutput(string data)
186+
{
187+
if (!string.IsNullOrWhiteSpace(data))
188+
Console.WriteLine(data);
189+
}
190+
191+
void OnError(string data)
192+
{
193+
if (!string.IsNullOrWhiteSpace(data))
194+
Console.Error.WriteLine(data);
195+
}
196+
```
197+
198+
## API Reference
199+
200+
### PythonEmbedManager Constructor
201+
202+
```csharp
203+
public PythonEmbedManager(string pythonDir)
204+
```
205+
206+
Creates a new instance of the PythonEmbedManager.
207+
208+
**Parameters:**
209+
- `pythonDir`: The directory where Python will be installed and managed.
210+
211+
**Throws:**
212+
- `ArgumentException`: If `pythonDir` is null or empty.
213+
214+
### Properties
215+
216+
#### PythonEmbedUrl
217+
218+
```csharp
219+
public string PythonEmbedUrl { get; set; }
220+
```
221+
222+
Gets or sets the URL to download the embedded Python distribution. Default is Python 3.11.6 for Windows AMD64.
223+
224+
### Methods
225+
226+
#### InitPythonEnvironment
227+
228+
```csharp
229+
public async Task<int> InitPythonEnvironment(Action<string> onOutput, Action<string> onError)
230+
```
231+
232+
Initializes the Python environment by downloading, extracting, and setting up Python with pip and virtualenv.
233+
234+
**Returns:** `0` on success, otherwise an error code.
235+
236+
#### InstallRequirement
237+
238+
```csharp
239+
public async Task<int> InstallRequirement(string requirementPath, Action<string> onOutput, Action<string> onError)
240+
```
241+
242+
Installs Python packages from a requirements.txt file.
243+
244+
**Parameters:**
245+
- `requirementPath`: Path to the requirements.txt file.
246+
- `onOutput`: Callback for standard output.
247+
- `onError`: Callback for error output.
248+
249+
**Returns:** `0` on success, otherwise an error code.
250+
251+
**Throws:**
252+
- `ArgumentException`: If `requirementPath` is null or empty.
253+
- `FileNotFoundException`: If the requirements file doesn't exist.
254+
255+
#### RunPython
256+
257+
```csharp
258+
public async Task<int> RunPython(string pythonScript, string parameters, string workingDirectory, Action<string> onOutput, Action<string> onError)
259+
```
260+
261+
Executes a Python script with optional parameters.
262+
263+
**Parameters:**
264+
- `pythonScript`: Path to the Python script to execute.
265+
- `parameters`: Command-line arguments to pass to the script.
266+
- `workingDirectory`: Working directory for script execution (defaults to Python directory).
267+
- `onOutput`: Callback for standard output.
268+
- `onError`: Callback for error output.
269+
270+
**Returns:** `0` on success, otherwise an error code.
271+
272+
**Throws:**
273+
- `ArgumentException`: If `pythonScript` is null or empty.
274+
- `FileNotFoundException`: If the script file doesn't exist.
275+
276+
#### RemovePythonEnvironment
277+
278+
```csharp
279+
public int RemovePythonEnvironment(Action<string> onOutput, Action<string> onError)
280+
```
281+
282+
Completely removes the Python environment from disk.
283+
284+
**Returns:** `0` on success, `-1` on failure.
285+
286+
## Requirements
287+
288+
- .NET 8.0 or later
289+
- Internet connection (for initial Python download)
290+
- Disk space for Python distribution (~50MB) and packages
291+
292+
## Platform Support
293+
294+
- ✅ Windows (x64)
295+
- ✅ Linux (with appropriate Python embed URL)
296+
- ✅ macOS (with appropriate Python embed URL)
297+
298+
**Note:** The default `PythonEmbedUrl` points to a Windows AMD64 distribution. For other platforms, set the `PythonEmbedUrl` property to an appropriate Python embeddable distribution for your target platform.
299+
300+
## How It Works
301+
302+
1. **Download**: On first run, the embedded Python distribution is downloaded from the official Python FTP server.
303+
2. **Extract**: The distribution is extracted to the specified directory.
304+
3. **Bootstrap**: `get-pip.py` is downloaded and executed to install pip.
305+
4. **Virtual Environment**: A virtual environment (`venv`) is created using `virtualenv`.
306+
5. **Isolation**: All subsequent operations (package installation, script execution) occur within this isolated environment.
307+
308+
## Use Cases
309+
310+
- 📊 **Data Processing**: Run Python data analysis scripts from .NET applications
311+
- 🤖 **ML Integration**: Execute machine learning models built with Python frameworks
312+
- 🔧 **Scripting**: Leverage Python libraries for specific tasks within .NET apps
313+
- 🎨 **Content Generation**: Use Python tools (like edge-tts, image processing) in .NET workflows
314+
- 🧪 **Testing**: Run Python-based test scripts or validation tools
315+
316+
## Contributing
317+
318+
Contributions are welcome! Please feel free to submit a Pull Request.
319+
320+
## License
321+
322+
This project is licensed under the MIT License - see the LICENSE file for details.
323+
324+
## Acknowledgments
325+
326+
- Python Software Foundation for the embedded Python distributions
327+
- The .NET community for feedback and contributions
328+
329+
## Support
330+
331+
If you encounter any issues or have questions:
332+
- 🐛 [Report a bug](https://github.com/xqiu/DotNetPythonEmbed/issues)
333+
- 💡 [Request a feature](https://github.com/xqiu/DotNetPythonEmbed/issues)
334+
- 📖 [View documentation](https://github.com/xqiu/DotNetPythonEmbed)
335+
336+
---
337+
338+
Made with ❤️ by the DotNetPythonEmbed Contributors

src/DotNetPythonEmbed/DotNetPythonEmbed.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>net8.0</TargetFramework>
44
<ImplicitUsings>enable</ImplicitUsings>
@@ -9,6 +9,8 @@
99
<Description>Utilities for managing an embedded Python distribution and virtual environments.</Description>
1010
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1111
<RepositoryType>git</RepositoryType>
12+
<Title>Dotnet Python Embed</Title>
13+
<PackageProjectUrl>https://github.com/xqiu/DotNetPythonEmbed</PackageProjectUrl>
1214
</PropertyGroup>
1315
<ItemGroup>
1416
<PackageReference Include="System.IO.Compression.ZipFile" Version="4.3.0" />

0 commit comments

Comments
 (0)