tasks/python.tasks.ps1
$SkipInstallPythonPoetry = $false $SkipInitialisePythonPoetry = $false $SkipRunFlake8 = $false $SkipRunPyTest = $false $SkipRunBehave = $false $SkipBuildPythonPackages = $false $SkipPublishPythonPackages = $false $PythonPoetryProject = "" $PythonPublishUser = "user" $PythonRepositoryName = "ci-python-feed" $PythonPackageRepoUrl = "" # e.g. https://pkgs.dev.azure.com/myOrg/Project/_packaging/myfeed/pypi/upload $PythonPackagePreReleaseTag = $null $PythonPackagesFilenameFilter = "*.whl" $PythonSourceDirectory = "src" $PoetryPath = Get-Command poetry -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path $PyTestResultsPath = Join-Path $here "pytest-test-results.xml" $BehaveResultsPath = Join-Path $here "behave-test-results.xml" $PythonCoverageReportPath = Join-Path $CoverageDir "coverage.xml" task EnsurePython -If { $PythonPoetryProject -ne "" } { if (!(Get-Command python -ErrorAction SilentlyContinue)) { throw "A Python installation could not be found. Please install Python and ensure it is available on the PATH environment variable." } } task InstallPythonPoetry -If { !$SkipInstallPythonPoetry } EnsurePython,{ $existingPoetry = Get-Command poetry -ErrorAction SilentlyContinue if (!$existingPoetry -and !$PoetryPath) { # The install script will honour this environment variable. If not explicitly set, we set it to: # - On build servers we install within the working directory to ensure it's part of the build agent caching # - Otherwise, we install to the user profile directory in a cross-platform way $env:POETRY_HOME ??= $IsRunningOnBuildServer ? (Join-Path $here ".poetry") : (Join-Path ($IsWindows ? $env:USERPROFILE : $env:HOME) ".poetry") $poetryBinPath = Join-Path $env:POETRY_HOME "bin" # If the poetry binary is not found, install it if (!(Test-Path (Join-Path $poetryBinPath "poetry"))) { Write-Build White "Installing Poetry $env:POETRY_VERSION: $env:POETRY_HOME" Invoke-WebRequest -Uri https://install.python-poetry.org/ -OutFile get-poetry.py exec { & python get-poetry.py --yes } Remove-Item get-poetry.py -Force } # Ensure the poetry tool is avaliable to the rest of the build process $script:PoetryPath = Join-Path $poetryBinPath "poetry" Write-Build Green "Poetry now available: $PoetryPath" if ($poetryBinPath -notin ($env:PATH -split [System.IO.Path]::PathSeparator)) { Write-Build White "Adding Poetry to PATH: $poetryBinPath" $env:PATH = "$poetryBinPath{0}$env:PATH" -f [System.IO.Path]::PathSeparator } } else { if (!$PoetryPath) { # Ensure $PoetryPath is set if poetry was already available in the PATH $script:PoetryPath = $existingPoetry.Path } Write-Build Green "Poetry already installed: $PoetryPath" } } task UpdatePoetryLockfile -If { !$IsRunningOnBuildServer } InstallPythonPoetry,{ Write-Build White "Ensuring poetry.lock is up-to-date - no packages will be updated" Push-Location $PythonPoetryProject exec { & $script:PoetryPath lock --no-update } } task InitialisePythonPoetry -If { $PythonPoetryProject -ne "" -and !$SkipInitialisePythonPoetry } InstallPythonPoetry,UpdatePoetryLockfile,{ if (!(Test-Path (Join-Path $PythonPoetryProject "pyproject.toml"))) { throw "pyproject.toml not found in $PythonPoetryProject" } # Default to using virtual environments in the project directory, unless already set $env:POETRY_VIRTUALENVS_IN_PROJECT ??= "true" # Define the global poetry arguments we will use for all poetry commands $script:poetryGlobalArgs = @( "--no-interaction" "--directory=$PythonPoetryProject" "-v" ) Write-Build White "poetryGlobalArgs: $poetryGlobalArgs" exec { & $script:PoetryPath install @poetryGlobalArgs --with dev,test } } task RunFlake8 -If { $PythonPoetryProject -ne "" -and !$SkipRunFlake8 } InitialisePythonPoetry,{ Write-Build White "Running flake8" # Explicitly change directory as Flake8 does not run when Poetry has the '--directory' argument Set-Location $PythonPoetryProject exec { & $script:PoetryPath run --no-interaction -v flake8 src -vv } } task RunPythonTests -If { $PythonPoetryProject -ne "" || ($SkipRunPyTest && $SkipRunBehave) } InitialisePythonPoetry,{ Write-Build White "Removing previous Python coverage results" # Explicity change directory as 'coverage erase' does not run when Poetry has the '--directory' argument Set-Location $PythonPoetryProject exec { & $script:PoetryPath run coverage erase } $pythonTestErrors = @() try { try { if (!$SkipRunPyTest) { _runPyTests } } catch { $pythonTestErrors += "PyTest Errors, check previous output for details" } try { if (!$SkipRunBehave) { _runBehave } } catch { $pythonTestErrors += "Behave Errors, check previous output for details" } } finally { _generatePythonCoverageXml if ($pythonTestErrors) { $pythonTestsErrorMsg = "{0}{1}" -f [Environment]::NewLine, ($pythonTestErrors -join [Environment]::NewLine) throw $pythonTestsErrorMsg } } } function _runPyTests { # Explicity change directory as PyTest does not run when Poetry has the '--directory' argument Set-Location $PythonPoetryProject exec { & $script:PoetryPath run --no-interaction -v ` pytest ` --cov=$PythonSourceDirectory ` --cov-report= ` --cov-append ` --junitxml=$PyTestResultsPath } } function _runBehave { # Explicity change directory as PyTest does not run when Poetry has the '--directory' argument Set-Location $PythonPoetryProject $testReportsPath = (Join-Path $here "behave-test-reports-temp") if (Test-Path $testReportsPath) { Remove-Item -Path $testReportsPath -Recurse -Force } New-Item -Path $testReportsPath -ItemType Directory | Out-Null try { exec { & $script:PoetryPath run coverage run --source=$PythonSourceDirectory -m behave --junit --junit-directory $testReportsPath } } finally { exec { & $script:PoetryPath run junitparser merge --glob $testReportsPath/*.xml $BehaveResultsPath } } } function _generatePythonCoverageXml { # Explicity change directory as PyTest does not run when Poetry has the '--directory' argument Set-Location $PythonPoetryProject Write-Build White "Generating Python coverage report" exec { & $script:PoetryPath run coverage xml -o $PythonCoverageReportPath } } task BuildPythonPackages -If { $PythonPoetryProject -ne "" -and !$SkipBuildPythonPackages } Version,EnsurePackagesDir,InitialisePythonPoetry,{ if (Test-Path (Join-Path $PythonPoetryProject "dist")) { Remove-Item (Join-Path $PythonPoetryProject "dist") -Recurse -Force } # Apply python pre-release versioning conventions # Ideally the repo will have a GitVersion configuration such that: # - The master/main branch has a pre-release tag of 'rc' (release candidate) # - All other branches have a pre-release tag of 'b' (beta) # - Tagged commits have no pre-release tag # However, as a fallback we must ensure that we always have a PEP440 compliant pre-release tag # even if GitVersion does not $safePreReleaseLabel = Get-PythonPackagePreReleaseLabelFromSemVer -PreReleaseLabel $GitVersion.PreReleaseLabel $PythonPackagePreReleaseTag ??= "{0}{1}" -f $safePreReleaseLabel, $GitVersion.PreReleaseNumber $pythonPackageVersion = "$($GitVersion.MajorMinorPatch)$PythonPackagePreReleaseTag" Write-Build White "Building Python packages with version: $pythonPackageVersion" exec { & $script:PoetryPath version @poetryGlobalArgs $pythonPackageVersion } # Make the Python package version available to the rest of the build, since it could be different to the GitVersion Set-BuildServerVariable -Name "PythonPackageVersion" -Value $pythonPackageVersion # Build the package(s) # For the moment we live with the fact that Poetry's output path is not configurable exec { & $script:PoetryPath build @poetryGlobalArgs } } task PublishPythonPackages -If { $PythonPoetryProject -ne "" -and !$SkipPublishPythonPackages } InitialisePythonPoetry,{ # Copy the Python packages from the standard packaging output folder to where Poetry expects to find them $distPath = Join-Path $PythonPoetryProject "dist" $pythonPackages = gci $distPath/$PythonPackagesFilenameFilter if (!$pythonPackages ) { Write-Warning "No Python packages found, skipping publish" } elseif (!$PythonPackageRepoUrl) { Write-Warning "PythonPackageRepoUrl build variable not set, skipping publish" } elseif (!$env:PYTHON_PACKAGE_REPOSITORY_KEY) { Write-Warning "PYTHON_PACKAGE_REPOSITORY_KEY environment variable not set, skipping publish" } else { exec { Write-Build White "Registering Python repository $PythonRepositoryName -> $PythonPackageRepoUrl" & $script:PoetryPath config @poetryGlobalArgs repositories.$PythonRepositoryName $PythonPackageRepoUrl } exec { Write-Build White "Publishing Python packages to $PythonRepositoryName" & $script:PoetryPath publish @poetryGlobalArgs -u $PythonPublishUser -p $env:PYTHON_PACKAGE_REPOSITORY_KEY -r $PythonRepositoryName } } } task BuildPython -If { $PythonPoetryProject -ne "" } InitialisePythonPoetry,RunFlake8 |