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();