diff --git a/src/CSnakes.Runtime.Tests/CSnakes.Runtime.Tests.csproj b/src/CSnakes.Runtime.Tests/CSnakes.Runtime.Tests.csproj
index 9909f58b..1d99e139 100644
--- a/src/CSnakes.Runtime.Tests/CSnakes.Runtime.Tests.csproj
+++ b/src/CSnakes.Runtime.Tests/CSnakes.Runtime.Tests.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/CSnakes.Runtime.Tests/Converter/ConverterTestBase.cs b/src/CSnakes.Runtime.Tests/Converter/ConverterTestBase.cs
index e896a7ec..6f0cb089 100644
--- a/src/CSnakes.Runtime.Tests/Converter/ConverterTestBase.cs
+++ b/src/CSnakes.Runtime.Tests/Converter/ConverterTestBase.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
namespace CSnakes.Runtime.Tests.Converter;
public class ConverterTestBase : IDisposable
@@ -16,6 +17,8 @@ public ConverterTestBase()
pb.WithHome(Environment.CurrentDirectory);
pb.FromNuGet("3.12.4").FromMacOSInstallerLocator("3.12").FromEnvironmentVariable("Python3_ROOT_DIR", "3.12.4");
+
+ services.AddLogging(builder => builder.AddXUnit());
})
.Build();
diff --git a/src/CSnakes.Runtime/IPythonEnvironment.cs b/src/CSnakes.Runtime/IPythonEnvironment.cs
index ce277a44..e6107389 100644
--- a/src/CSnakes.Runtime/IPythonEnvironment.cs
+++ b/src/CSnakes.Runtime/IPythonEnvironment.cs
@@ -1,4 +1,5 @@
using CSnakes.Runtime.CPython;
+using Microsoft.Extensions.Logging;
namespace CSnakes.Runtime;
@@ -11,4 +12,6 @@ public string Version
return CPythonAPI.Py_GetVersion() ?? "No version available";
}
}
+
+ public ILogger Logger { get; }
}
diff --git a/src/CSnakes.Runtime/IServiceCollectionExtensions.cs b/src/CSnakes.Runtime/IServiceCollectionExtensions.cs
index c1bbac97..fb03354f 100644
--- a/src/CSnakes.Runtime/IServiceCollectionExtensions.cs
+++ b/src/CSnakes.Runtime/IServiceCollectionExtensions.cs
@@ -1,6 +1,7 @@
using CSnakes.Runtime.Locators;
using CSnakes.Runtime.PackageManagement;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace CSnakes.Runtime;
///
@@ -24,10 +25,11 @@ public static IPythonEnvironmentBuilder WithPython(this IServiceCollection servi
var envBuilder = sp.GetRequiredService();
var locators = sp.GetServices();
var installers = sp.GetServices();
+ var logger = sp.GetRequiredService>();
var options = envBuilder.GetOptions();
- return PythonEnvironment.GetPythonEnvironment(locators, installers, options);
+ return PythonEnvironment.GetPythonEnvironment(locators, installers, options, logger);
});
return pythonBuilder;
diff --git a/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs b/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs
index c90ac561..e10f747a 100644
--- a/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs
+++ b/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs
@@ -34,6 +34,7 @@ private void InstallPackagesWithPip(string home, string? virtualEnvironmentLocat
if (virtualEnvironmentLocation is not null)
{
+ logger.LogInformation("Using virtual environment at {VirtualEnvironmentLocation} to install packages with pip.", virtualEnvironmentLocation);
string venvScriptPath = Path.Combine(virtualEnvironmentLocation, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Scripts" : "bin");
startInfo.FileName = Path.Combine(venvScriptPath, pipBinaryName);
startInfo.EnvironmentVariables["PATH"] = $"{venvScriptPath};{Environment.GetEnvironmentVariable("PATH")}";
@@ -66,6 +67,7 @@ private void InstallPackagesWithPip(string home, string? virtualEnvironmentLocat
if (process.ExitCode != 0)
{
+ logger.LogError("Failed to install packages.");
throw new InvalidOperationException("Failed to install packages.");
}
}
diff --git a/src/CSnakes.Runtime/PythonEnvironment.cs b/src/CSnakes.Runtime/PythonEnvironment.cs
index 1ce7ea80..111d6ac7 100644
--- a/src/CSnakes.Runtime/PythonEnvironment.cs
+++ b/src/CSnakes.Runtime/PythonEnvironment.cs
@@ -1,6 +1,7 @@
using CSnakes.Runtime.CPython;
using CSnakes.Runtime.Locators;
using CSnakes.Runtime.PackageManagement;
+using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Runtime.InteropServices;
@@ -8,39 +9,50 @@ namespace CSnakes.Runtime;
internal class PythonEnvironment : IPythonEnvironment
{
+ public ILogger Logger { get; private set; }
+
private readonly CPythonAPI api;
private bool disposedValue;
private static IPythonEnvironment? pythonEnvironment;
private readonly static object locker = new();
- public static IPythonEnvironment GetPythonEnvironment(IEnumerable locators, IEnumerable packageInstallers, PythonEnvironmentOptions options)
+ public static IPythonEnvironment GetPythonEnvironment(IEnumerable locators, IEnumerable packageInstallers, PythonEnvironmentOptions options, Microsoft.Extensions.Logging.ILogger logger)
{
- if (pythonEnvironment is null)
+ if (pythonEnvironment is null)
+ {
+ lock (locker)
{
- lock(locker)
- {
- pythonEnvironment ??= new PythonEnvironment(locators, packageInstallers, options);
- }
+ pythonEnvironment ??= new PythonEnvironment(locators, packageInstallers, options, logger);
}
- return pythonEnvironment;
+ }
+ return pythonEnvironment;
}
private PythonEnvironment(
IEnumerable locators,
IEnumerable packageInstallers,
- PythonEnvironmentOptions options)
+ PythonEnvironmentOptions options,
+ ILogger logger)
{
+ Logger = logger;
+
var location = locators
.Where(locator => locator.IsSupported())
.Select(locator => locator.LocatePython())
- .FirstOrDefault(loc => loc is not null)
- ?? throw new InvalidOperationException("Python installation not found.");
+ .FirstOrDefault(loc => loc is not null);
+
+ if (location is null)
+ {
+ logger.LogError("Python installation not found. There were {LocatorCount} locators registered.", locators.Count());
+ throw new InvalidOperationException("Python installation not found.");
+ }
string home = options.Home;
if (!Directory.Exists(home))
{
+ logger.LogError("Python home directory does not exist: {Home}", home);
throw new DirectoryNotFoundException("Python home directory does not exist.");
}
@@ -49,6 +61,8 @@ private PythonEnvironment(
EnsureVirtualEnvironment(location, options.VirtualEnvironmentPath);
}
+ logger.LogInformation("Setting up Python environment from {PythonLocation} using home of {Home}", location.Folder, home);
+
foreach (var installer in packageInstallers)
{
installer.InstallPackages(home, options.VirtualEnvironmentPath);
@@ -68,33 +82,58 @@ private PythonEnvironment(
if (options.ExtraPaths is { Length: > 0 })
{
+ logger.LogInformation("Adding extra paths to PYTHONPATH: {ExtraPaths}", options.ExtraPaths);
api.PythonPath = api.PythonPath + sep + string.Join(sep, options.ExtraPaths);
}
api.Initialize();
}
- private static void EnsureVirtualEnvironment(PythonLocationMetadata pythonLocation, string? venvPath)
+ private void EnsureVirtualEnvironment(PythonLocationMetadata pythonLocation, string? venvPath)
{
if (venvPath is null)
{
+ Logger.LogError("Virtual environment location is not set but it was requested to be created.");
throw new ArgumentNullException(nameof(venvPath), "Virtual environment location is not set.");
}
if (!Directory.Exists(venvPath))
{
+ Logger.LogInformation("Creating virtual environment at {VirtualEnvPath}", venvPath);
+
ProcessStartInfo startInfo = new()
{
WorkingDirectory = pythonLocation.Folder,
FileName = "python",
Arguments = $"-m venv {venvPath}"
};
+ startInfo.RedirectStandardError = true;
+ startInfo.RedirectStandardOutput = true;
+
using Process process = new() { StartInfo = startInfo };
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Logger.LogInformation("{Data}", e.Data);
+ }
+ };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Logger.LogError("{Data}", e.Data);
+ }
+ };
+
process.Start();
+ process.BeginErrorReadLine();
+ process.BeginOutputReadLine();
process.WaitForExit();
}
}
- private static CPythonAPI SetupStandardLibrary(PythonLocationMetadata pythonLocationMetadata, string versionPath, string majorVersion, char sep)
+ private CPythonAPI SetupStandardLibrary(PythonLocationMetadata pythonLocationMetadata, string versionPath, string majorVersion, char sep)
{
string pythonDll = string.Empty;
string pythonPath = string.Empty;
@@ -129,6 +168,10 @@ private static CPythonAPI SetupStandardLibrary(PythonLocationMetadata pythonLoca
{
throw new PlatformNotSupportedException("Unsupported platform.");
}
+
+ Logger.LogInformation("Python DLL: {PythonDLL}", pythonDll);
+ Logger.LogInformation("Python path: {PythonPath}", pythonPath);
+
var api = new CPythonAPI(pythonDll)
{
PythonPath = pythonPath
@@ -152,7 +195,7 @@ protected virtual void Dispose(bool disposing)
api.Dispose();
if (pythonEnvironment is not null)
{
- lock(locker)
+ lock (locker)
{
if (pythonEnvironment is not null)
{
diff --git a/src/CSnakes.Tests/BasicSmokeTests.cs b/src/CSnakes.Tests/BasicSmokeTests.cs
index d31d99b8..aba060d9 100644
--- a/src/CSnakes.Tests/BasicSmokeTests.cs
+++ b/src/CSnakes.Tests/BasicSmokeTests.cs
@@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
+using Microsoft.Extensions.Logging;
using PythonSourceGenerator.Parser;
using PythonSourceGenerator.Reflection;
using System.ComponentModel;
@@ -54,6 +55,7 @@ public void TestGeneratedSignature(string code, string expected)
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverter).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(IReadOnlyDictionary<,>).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(IPythonEnvironmentBuilder).Assembly.Location))
+ .AddReferences(MetadataReference.CreateFromFile(typeof(ILogger<>).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "netstandard").Location)) // TODO: (track) Ensure 2.0
.AddReferences(MetadataReference.CreateFromFile(AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "System.Runtime").Location))
.AddReferences(MetadataReference.CreateFromFile(AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "System.Collections").Location))
diff --git a/src/CSnakes/PythonStaticGenerator.cs b/src/CSnakes/PythonStaticGenerator.cs
index 98f10469..1b1ac37b 100644
--- a/src/CSnakes/PythonStaticGenerator.cs
+++ b/src/CSnakes/PythonStaticGenerator.cs
@@ -28,8 +28,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Convert snakecase to pascal case
var pascalFileName = string.Join("", fileName.Split('_').Select(s => char.ToUpperInvariant(s[0]) + s.Substring(1)));
-
- IEnumerable methods;
// Read the file
var code = file.GetText(sourceContext.CancellationToken);
@@ -45,8 +43,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
sourceContext.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("PSG004", "PythonStaticGenerator", error.Message, "PythonStaticGenerator", DiagnosticSeverity.Error, true), errorLocation));
}
- if (result) {
- methods = ModuleReflection.MethodsFromFunctionDefinitions(functions, fileName);
+ if (result) {
+ IEnumerable methods = ModuleReflection.MethodsFromFunctionDefinitions(functions, fileName);
string source = FormatClassFromMethods(@namespace, pascalFileName, methods, fileName);
sourceContext.AddSource($"{pascalFileName}.py.cs", source);
sourceContext.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("PSG002", "PythonStaticGenerator", $"Generated {pascalFileName}.py.cs", "PythonStaticGenerator", DiagnosticSeverity.Info, true), Location.None));
@@ -70,14 +68,20 @@ public static string FormatClassFromMethods(string @namespace, string pascalFile
using System.Collections.Generic;
using System.ComponentModel;
+ using Microsoft.Extensions.Logging;
+
namespace {{@namespace}}
{
public static class {{pascalFileName}}Extensions
{
- private static readonly I{{pascalFileName}} instance = new {{pascalFileName}}Internal();
+ private static I{{pascalFileName}}? instance;
public static I{{pascalFileName}} {{pascalFileName}}(this IPythonEnvironment env)
{
+ if (instance is null)
+ {
+ instance = new {{pascalFileName}}Internal(env.Logger);
+ }
return instance;
}
@@ -87,16 +91,21 @@ private class {{pascalFileName}}Internal : I{{pascalFileName}}
private readonly PyObject module;
- internal {{pascalFileName}}Internal()
+ private readonly ILogger logger;
+
+ internal {{pascalFileName}}Internal(ILogger logger)
{
+ this.logger = logger;
using (GIL.Acquire())
{
+ logger.LogInformation("Importing module {ModuleName}", "{{fileName}}");
module = Import.ImportModule("{{fileName}}");
}
}
public void Dispose()
{
+ logger.LogInformation("Disposing module {ModuleName}", "{{fileName}}");
module.Dispose();
}
diff --git a/src/CSnakes/Reflection/MethodReflection.cs b/src/CSnakes/Reflection/MethodReflection.cs
index 6569359c..2312f67d 100644
--- a/src/CSnakes/Reflection/MethodReflection.cs
+++ b/src/CSnakes/Reflection/MethodReflection.cs
@@ -141,11 +141,23 @@ public static MethodDefinition FromMethod(PythonFunctionDefinition function, str
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("__underlyingPythonFunc"),
IdentifierName("Call")),
- ArgumentList(
- SeparatedList(pythonCastArguments))))))))
- .WithUsingKeyword(
- Token(SyntaxKind.UsingKeyword));
+ ArgumentList(SeparatedList(pythonCastArguments))))))))
+ .WithUsingKeyword(Token(SyntaxKind.UsingKeyword));
StatementSyntax[] statements = [
+ ExpressionStatement(
+ InvocationExpression(
+ MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ IdentifierName("logger"),
+ IdentifierName("LogInformation")))
+ .WithArgumentList(
+ ArgumentList(
+ SeparatedList(
+ [
+ Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("Invoking Python function: {FunctionName}"))),
+ Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(function.Name)))
+ ])))
+ ),
moduleDefinition,
.. pythonConversionStatements,
callStatement,
diff --git a/src/Integration.Tests/Integration.Tests.csproj b/src/Integration.Tests/Integration.Tests.csproj
index 7567b3ff..7fa7293b 100644
--- a/src/Integration.Tests/Integration.Tests.csproj
+++ b/src/Integration.Tests/Integration.Tests.csproj
@@ -52,6 +52,7 @@
+
diff --git a/src/Integration.Tests/IntegrationTestBase.cs b/src/Integration.Tests/IntegrationTestBase.cs
index 4207b882..0afa90ce 100644
--- a/src/Integration.Tests/IntegrationTestBase.cs
+++ b/src/Integration.Tests/IntegrationTestBase.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
namespace Integration.Tests;
public class IntegrationTestBase : IDisposable
@@ -15,7 +16,9 @@ public IntegrationTestBase()
var pb = services.WithPython();
pb.WithHome(Path.Join(Environment.CurrentDirectory, "python"));
- pb.FromNuGet("3.12.4").FromMacOSInstallerLocator("3.12").FromEnvironmentVariable("Python3_ROOT_DIR", "3.12.4");
+ pb.FromNuGet("3.12.4").FromMacOSInstallerLocator("3.12").FromEnvironmentVariable("Python3_ROOT_DIR", "3.12.4");
+
+ services.AddLogging(builder => builder.AddXUnit());
})
.Build();