PythonPip3Dsc.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

using namespace System.Collections.Generic

$global:usePip3Exe = $false
$global:pip3ExePath = "$env:LOCALAPPDATA\Programs\Python\Python38\Scripts\pip3.exe"

# Assert once that pip3 is already installed on the system.
Assert-Pip3

enum Ensure
{
    Absent
    Present
}

#region DSCResources
[DSCResource()]
class Pip3Package
{
    # DSCResource requires a key. Do not set.
    [DscProperty(Key)]
    [string]$SID

    [DscProperty()]
    [Ensure] $Ensure = [Ensure]::Present

    [DscProperty(Mandatory)]
    [string[]]$Packages

    [DscProperty()]
    [string]$Arguments

    [DscProperty(NotConfigurable)]
    [bool]$InstalledStatus

    [DscProperty(NotConfigurable)]
    [string[]]$InstalledPackages

    [Pip3Package] Get()
    {
        $this.InstalledPackages = GetInstalledPip3Packages | ConvertFrom-Json | Select-Object -ExpandProperty name
        $this.InstalledStatus = $true
        foreach ($package in $this.Packages)
        {
            $this.InstalledStatus = $this.InstalledPackages -contains $package
        }

        return @{
            SID = $this.SID
            Ensure = $this.Ensure
            Packages = $this.Packages
            Arguments = $this.Arguments
            InstalledStatus = $this.InstalledStatus
            InstalledPackages = $this.InstalledPackages
        }       
    }

    [bool] Test()
    {
        $currentState = $this.Get()
        if ($currentState.Ensure -eq [Ensure]::Present)
        {
            return $currentState.InstalledStatus -eq $true
        }
        else
        {
            return $currentState.InstalledStatus -eq $false
        }
    }

    [void] Set()
    {
        $currentState = $this.Get()
        if ($currentState.Ensure -eq [Ensure]::Present)
        {
            if ($currentState.InstalledStatus -ne $true)
            {
                foreach ($package in $this.Packages)
                {
                    if ($currentState.InstalledPackages -notcontains $package)
                    {
                        Invoke-Pip3Install -PackageName $package -Arguments $this.Arguments
                    }
                }
            }
        }
        else
        {
            if ($currentState.InstalledStatus -ne $false)
            {
                foreach ($package in $this.Packages)
                {
                    if ($currentState.InstalledPackages -contains $package)
                    {
                        Invoke-Pip3Uninstall -PackageName $package -Arguments $this.Arguments
                    }
                }
            }
        }
    }
}

#endregion DSCResources

#region Functions
function Assert-Pip3
{
    # Try invoking pip3 help with the alias. If it fails, switch to calling npm.cmd directly.
    # This may occur if npm is installed in the same shell window and the alias is not updated until the shell window is restarted.
    try
    {
        Invoke-Pip3 -command 'help'
        return
    }
    catch {}

    if (Test-Path -Path $global:pip3ExePath)
    {
        $global:usePip3Exe = $true;
        Write-Host "Calling pip3.exe from default install location: $global:usePip3Exe"
    }
    else
    {
        throw "Python is not installed"
    }
}

function Invoke-Pip3Install
{
    param (
        [Parameter()]
        [string]$PackageName,

        [Parameter()]
        [string]$Arguments
    )

    $command = [List[string]]::new()
    $command.Add("install")
    $command.Add($PackageName)
    $command.Add($Arguments)
    return Invoke-Pip3 -command $command
}

function Invoke-Pip3Uninstall
{
    param (
        [Parameter()]
        [string]$PackageName,

        [Parameter()]
        [string]$Arguments
    )

    $command = [List[string]]::new()
    $command.Add("uninstall")
    $command.Add($PackageName)
    $command.Add($Arguments)

    # '--yes' is needed to ignore confrimation required for uninstalls
    $command.Add("--yes")
    return Invoke-Pip3 -command $command
}

function GetInstalledPip3Packages
{
    $Arguments = [List[string]]::new()
    $Arguments.Add("list")
    $Arguments.Add("--format=json")

    $command;
    if ($global:usePip3Exe)
    {
        $command = "& '$global:pip3ExePath' " + $Arguments
    }
    else
    {
        $command = "& pip3 " + $Arguments
    }

    return Invoke-Expression -Command $command
}

function Invoke-Pip3
{
    param (
        [Parameter(Mandatory)]
        [string]$command 
    )

    if ($global:usePip3Exe)
    {
        return Start-Process -FilePath $global:pip3ExePath -ArgumentList $command -Wait -PassThru -WindowStyle hidden
    }
    else
    {
        return Start-Process -FilePath pip3 -ArgumentList $command -Wait -PassThru -WindowStyle hidden
    }
}

#endregion Functions