LibreDevOpsHelpers.Python/LibreDevOpsHelpers.Python.psm1
|
Set-StrictMode -Version Latest function Get-LdoPythonCommand { # Internal. Returns the name of the available Python executable, preferring python3. [CmdletBinding()] [OutputType([string])] param() if (Get-Command python3 -ErrorAction SilentlyContinue) { return 'python3' } if (Get-Command python -ErrorAction SilentlyContinue) { return 'python' } throw 'Python not found on PATH.' } function New-LdoVenv { <# .SYNOPSIS Creates a Python virtual environment. .DESCRIPTION Creates a venv at <VenvPath>/<VenvName> using the available Python executable. Does nothing when the environment already exists. .PARAMETER VenvName Name of the virtual environment folder. Defaults to .venv. .PARAMETER VenvPath Parent folder for the environment. Defaults to the current directory. .EXAMPLE New-LdoVenv .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [string]$VenvName = '.venv', [string]$VenvPath = (Get-Location).Path ) $pythonCmd = Get-LdoPythonCommand $pythonVersion = & $pythonCmd --version Write-LdoLog -Level INFO -Message "Python version: $pythonVersion" $virtualEnvPath = Join-Path $VenvPath $VenvName if (Test-Path $virtualEnvPath) { Write-LdoLog -Level WARN -Message "Virtual environment $virtualEnvPath already exists." return } Write-LdoLog -Level INFO -Message "Running: $pythonCmd -m venv $virtualEnvPath" & $pythonCmd -m venv $virtualEnvPath if ($LASTEXITCODE -ne 0) { throw "Failed to create virtual environment (exit $LASTEXITCODE)." } Write-LdoLog -Level SUCCESS -Message "Virtual environment '$VenvName' created at '$virtualEnvPath'." } function Initialize-LdoVenv { <# .SYNOPSIS Activates an existing Python virtual environment in the current session. .DESCRIPTION Activates the venv at <VenvPath>/<VenvName>. On Windows the Activate.ps1 script is dot-sourced; on other platforms VIRTUAL_ENV and PATH are set directly. Throws when the environment is missing, or when -VerifyVenv is set and the active interpreter does not resolve to the expected location. .PARAMETER VenvName Name of the virtual environment folder. Defaults to .venv. .PARAMETER VenvPath Parent folder for the environment. Defaults to the current directory. .PARAMETER VerifyVenv When set, verifies the active interpreter resolves to the environment. .EXAMPLE Initialize-LdoVenv -VerifyVenv .OUTPUTS None #> [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidGlobalFunctions', '', Justification = 'Venv activation must override the global prompt function.')] [CmdletBinding()] [OutputType([void])] param( [string]$VenvName = '.venv', [string]$VenvPath = (Get-Location).Path, [switch]$VerifyVenv ) $virtualEnvPath = Join-Path $VenvPath $VenvName if (-not (Test-Path $virtualEnvPath)) { throw "No virtual environment at '$virtualEnvPath'" } if ($IsWindows) { . (Join-Path $virtualEnvPath 'Scripts\Activate.ps1') } else { $env:VIRTUAL_ENV = $virtualEnvPath $env:PATH = "$virtualEnvPath/bin:$env:PATH" if (-not (Test-Path function:\__origPrompt)) { Set-Item function:\__origPrompt (Get-Command prompt) } function global:prompt { "($(Split-Path $env:VIRTUAL_ENV -Leaf)) " + (& __origPrompt) } } if ($VerifyVenv) { $venvPython = if ($IsWindows) { Join-Path $virtualEnvPath 'Scripts/python.exe' } else { Join-Path $virtualEnvPath 'bin/python' } $prefix = & $venvPython -c 'import sys, pathlib; print(pathlib.Path(sys.prefix).resolve())' if ($prefix -ne (Get-Item $virtualEnvPath).FullName) { throw "Venv verification failed; expected $virtualEnvPath, got $prefix" } } Write-LdoLog -Level INFO -Message "Virtual environment '$VenvName' activated." } function Use-LdoVenv { <# .SYNOPSIS Creates a Python virtual environment if needed and activates it. .DESCRIPTION Creates the venv at <VenvPath>/<VenvName> when it does not exist, then activates it for the current session. .PARAMETER VenvPath Parent folder for the environment. Defaults to the current directory. .PARAMETER VenvName Name of the virtual environment folder. Defaults to .venv. .EXAMPLE Use-LdoVenv .OUTPUTS None #> [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidGlobalFunctions', '', Justification = 'Venv activation must override the global prompt function.')] [CmdletBinding()] [OutputType([void])] param( [string]$VenvPath = (Get-Location).Path, [string]$VenvName = '.venv' ) $venvRoot = Join-Path $VenvPath $VenvName if (-not (Test-Path $venvRoot)) { $pythonCmd = Get-LdoPythonCommand Write-LdoLog -Level INFO -Message "Creating venv with: $pythonCmd -m venv $venvRoot" & $pythonCmd -m venv $venvRoot if ($LASTEXITCODE -ne 0) { throw "Failed to create virtual environment (exit $LASTEXITCODE)." } } if ($IsWindows) { $activateScript = Join-Path $venvRoot 'Scripts\Activate.ps1' if (-not (Test-Path $activateScript)) { throw "Cannot find $activateScript" } Write-LdoLog -Level INFO -Message "Dot-sourcing $activateScript" . $activateScript } else { Write-LdoLog -Level INFO -Message 'Setting PATH/VIRTUAL_ENV for POSIX PowerShell.' $env:VIRTUAL_ENV = (Resolve-Path $venvRoot) $env:PATH = "$env:VIRTUAL_ENV/bin$([IO.Path]::PathSeparator)$env:PATH" if (-not (Get-Command __origPrompt -ErrorAction SilentlyContinue)) { $orig = Get-Command prompt -ErrorAction SilentlyContinue if ($orig) { Set-Item function:\__origPrompt $orig } } function global:prompt { "($(Split-Path $env:VIRTUAL_ENV -Leaf)) " + (& { & __origPrompt }) } } Write-LdoLog -Level INFO -Message "Virtual environment '$VenvName' is active." } function Clear-LdoVenv { <# .SYNOPSIS Deactivates the currently active Python virtual environment. .DESCRIPTION Calls the venv deactivate function when one is active, otherwise logs a warning. .PARAMETER VenvName Name used only for logging. Defaults to .venv. .EXAMPLE Clear-LdoVenv .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [string]$VenvName = '.venv' ) if (Get-Command -Name deactivate -CommandType Function -ErrorAction SilentlyContinue) { Write-LdoLog -Level INFO -Message 'Running: deactivate' deactivate Write-LdoLog -Level INFO -Message "Virtual environment '$VenvName' deactivated." } else { Write-LdoLog -Level WARN -Message 'No virtual environment is currently active.' } } function Remove-LdoVenv { <# .SYNOPSIS Removes a Python virtual environment. .DESCRIPTION Deletes the venv folder at <VenvPath>/<VenvName> when present, otherwise logs a warning. .PARAMETER VenvName Name of the virtual environment folder. Defaults to .venv. .PARAMETER VenvPath Parent folder for the environment. Defaults to the current directory. .EXAMPLE Remove-LdoVenv .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [string]$VenvName = '.venv', [string]$VenvPath = (Get-Location).Path ) $virtualEnvPath = Join-Path $VenvPath $VenvName if (-not (Test-Path $virtualEnvPath)) { Write-LdoLog -Level WARN -Message "Virtual environment '$VenvName' not found at '$virtualEnvPath'." return } Write-LdoLog -Level INFO -Message "Removing $virtualEnvPath" Remove-Item -Path $virtualEnvPath -Recurse -Force Write-LdoLog -Level SUCCESS -Message "Virtual environment '$VenvName' removed from '$virtualEnvPath'." } function Invoke-LdoPythonInstallRequirements { <# .SYNOPSIS Installs Python requirements into a project-local package directory. .DESCRIPTION Runs pip install -r against the requirements file, targeting <ProjectPath>/.python_packages/lib/site-packages (the layout used by Azure Functions Python apps). Throws on failure. .PARAMETER ProjectPath Project folder containing the requirements file. Defaults to the current directory. .PARAMETER RequirementsFile Requirements file name. Defaults to requirements.txt. .PARAMETER Upgrade When set, passes --upgrade to pip. .EXAMPLE Invoke-LdoPythonInstallRequirements -ProjectPath ./app .OUTPUTS None #> [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingularNouns', '', Justification = 'Installs multiple requirements.')] [CmdletBinding()] [OutputType([void])] param( [ValidateScript({ Test-Path $_ -PathType Container })] [string]$ProjectPath = (Get-Location).Path, [string]$RequirementsFile = 'requirements.txt', [switch]$Upgrade ) $pythonCmd = Get-LdoPythonCommand $reqPath = Join-Path $ProjectPath $RequirementsFile if (-not (Test-Path $reqPath)) { throw "Requirements file not found: $reqPath" } $pyArgs = @('-m', 'pip', 'install', '-r', "$reqPath", '--target', "$ProjectPath/.python_packages/lib/site-packages") if ($Upgrade) { $pyArgs += '--upgrade' } Write-LdoLog -Level INFO -Message "$pythonCmd $($pyArgs -join ' ')" & $pythonCmd @pyArgs if ($LASTEXITCODE -ne 0) { throw "pip install failed (exit $LASTEXITCODE)." } Write-LdoLog -Level SUCCESS -Message 'Dependencies installed.' } function Remove-LdoPythonPackages { <# .SYNOPSIS Removes the project-local .python_packages directory. .DESCRIPTION Deletes <ProjectPath>/.python_packages when present, otherwise logs a warning. .PARAMETER ProjectPath Project folder. Defaults to the current directory. .EXAMPLE Remove-LdoPythonPackages -ProjectPath ./app .OUTPUTS None #> [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingularNouns', '', Justification = 'Removes multiple packages.')] [CmdletBinding()] [OutputType([void])] param( [string]$ProjectPath = (Get-Location).Path ) $packagesPath = Join-Path $ProjectPath '.python_packages' if (-not (Test-Path $packagesPath)) { Write-LdoLog -Level WARN -Message "Python packages directory not found: $packagesPath" return } Remove-Item -Path $packagesPath -Recurse -Force Write-LdoLog -Level SUCCESS -Message 'Python packages directory removed.' } function Invoke-LdoPytestRun { <# .SYNOPSIS Runs pytest, optionally emitting JUnit XML and coverage reports. .DESCRIPTION Runs pytest in a project folder. JUnit XML, coverage XML, and coverage HTML outputs are produced unless their parameters are set to an empty string. Extra CLI arguments can be supplied as a JSON array. Throws on test failure. The original working directory is always restored. .PARAMETER ProjectPath Project folder to run pytest in. .PARAMETER PythonExe Python executable to use. Defaults to python. .PARAMETER JUnitXmlPath JUnit XML output path. Set to '' to skip. Defaults to pytest-results.xml. .PARAMETER CoverageXmlPath Coverage XML output path. Set to '' to skip. Defaults to coverage.xml. .PARAMETER CoverageHtmlDir Coverage HTML output directory. Set to '' to skip. Defaults to htmlcov. .PARAMETER CliExtraArgsJson Additional pytest arguments as a JSON array string. .EXAMPLE Invoke-LdoPytestRun -ProjectPath ./app .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container })] [string]$ProjectPath, [string]$PythonExe = 'python', [string]$JUnitXmlPath = 'pytest-results.xml', [string]$CoverageXmlPath = 'coverage.xml', [string]$CoverageHtmlDir = 'htmlcov', [string]$CliExtraArgsJson ) $orig = Get-Location try { Set-Location $ProjectPath $extra = @() if ($CliExtraArgsJson) { try { $extra = [string[]]($CliExtraArgsJson | ConvertFrom-Json) } catch { throw "CliExtraArgsJson is not a valid JSON array: $($_.Exception.Message)" } } $cmd = @('-m', 'pytest') if ($JUnitXmlPath) { $cmd += "--junitxml=$JUnitXmlPath" } if ($CoverageXmlPath) { $cmd += @('--cov', '.', '--cov-report', "xml:$CoverageXmlPath") if ($CoverageHtmlDir) { $cmd += '--cov-report', "html:$CoverageHtmlDir" } } if ($extra) { $cmd += $extra } Write-LdoLog -Level INFO -Message "$PythonExe $($cmd -join ' ')" & $PythonExe @cmd if ($LASTEXITCODE -ne 0) { throw "pytest failed (exit $LASTEXITCODE)." } Write-LdoLog -Level SUCCESS -Message 'pytest completed successfully.' } finally { Set-Location $orig } } Export-ModuleMember -Function ` New-LdoVenv, ` Initialize-LdoVenv, ` Use-LdoVenv, ` Clear-LdoVenv, ` Remove-LdoVenv, ` Invoke-LdoPythonInstallRequirements, ` Remove-LdoPythonPackages, ` Invoke-LdoPytestRun |