From 57d060e600512a5130e2ff197cef2359e732537c Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Thu, 25 Jul 2019 09:16:52 -0700 Subject: [PATCH 1/6] Make Powershell Activation.ps1 script signable - Remove use of replacement text in the script - Make use of the pyvenv.cfg file for prompt value. - Add parameters to allow more flexibility - Make use of the current path, and assumptions about where env puts things, to compensate - Make the script a bit more 'idiomatic' Powershell - Add script documentation (Get-Help .\.venv\Scripts\Activate.ps1 shows PS help page now --- Lib/venv/scripts/common/Activate.ps1 | 211 +++++++++++++++++++++++---- 1 file changed, 183 insertions(+), 28 deletions(-) diff --git a/Lib/venv/scripts/common/Activate.ps1 b/Lib/venv/scripts/common/Activate.ps1 index de22962630aabb..7d4c8f8f5ce7dc 100644 --- a/Lib/venv/scripts/common/Activate.ps1 +++ b/Lib/venv/scripts/common/Activate.ps1 @@ -1,56 +1,211 @@ -function Script:add-bin([string]$envPath) { - $binPath = Join-Path -Path $env:VIRTUAL_ENV -ChildPath '__VENV_BIN_NAME__' - return ($binPath, $envPath) -join [IO.Path]::PathSeparator -} +<# +.Synopsis +Activate a Python virtual environment for the current Powershell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parenthesis and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parenthesis) while the virtual environment is active. + + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> function global:deactivate ([switch]$NonDestructive) { # Revert to original values - if (Test-Path function:_OLD_VIRTUAL_PROMPT) { - copy-item function:_OLD_VIRTUAL_PROMPT function:prompt - remove-item function:_OLD_VIRTUAL_PROMPT + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME } - if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) { - copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME - remove-item env:_OLD_VIRTUAL_PYTHONHOME + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH } - if (Test-Path env:_OLD_VIRTUAL_PATH) { - copy-item env:_OLD_VIRTUAL_PATH env:PATH - remove-item env:_OLD_VIRTUAL_PATH + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV } - if (Test-Path env:VIRTUAL_ENV) { - remove-item env:VIRTUAL_ENV + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if ($_PYTHON_VENV_PROMPT_PREFIX) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force } - if (!$NonDestructive) { - # Self destruct! - remove-item function:deactivate + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate } } +<# +.Description +Get-PyVenvCfgOverrides parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvCfgOverrides( + [String] + $ConfigDir +) { + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $PyVenvCfgPath -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + if ($pyvenvConfigPath) { + + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + $pyvenvConfig = @{ } + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0,1))) { + $val = $val.Substring(1, $m.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if (-not $VenvDir) { + $VenvDir = $VenvExecDir.Parent.FullName +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvCfgOverrides -VenvBaseDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if (-not $Promp) { + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + $Prompt = $pyvenvCfg['prompt']; + } + else { + $venvDir = Get-Item -Path $VenvDir + $Prompt = $venvDir.Name; + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. deactivate -nondestructive -$env:VIRTUAL_ENV="__VENV_DIR__" +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" -if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { # Set the prompt to include the env name # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT {""} - copy-item function:prompt function:_OLD_VIRTUAL_PROMPT + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + function global:prompt { - Write-Host -NoNewline -ForegroundColor Green '__VENV_PROMPT__' + Write-Host -NoNewline -ForegroundColor DarkGreen "($_PYTHON_VENV_PROMPT_PREFIX) " _OLD_VIRTUAL_PROMPT } } # Clear PYTHONHOME -if (Test-Path env:PYTHONHOME) { - copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME - remove-item env:PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME } # Add the venv to the PATH -copy-item env:PATH env:_OLD_VIRTUAL_PATH -$env:PATH = add-bin $env:PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" From f15d490561407514426198ed9df606dd72928f9b Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Thu, 25 Jul 2019 10:29:05 -0700 Subject: [PATCH 2/6] Add news entry --- .../NEWS.d/next/Library/2019-07-25-10-28-40.bpo-37354.RT3_3H.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2019-07-25-10-28-40.bpo-37354.RT3_3H.rst diff --git a/Misc/NEWS.d/next/Library/2019-07-25-10-28-40.bpo-37354.RT3_3H.rst b/Misc/NEWS.d/next/Library/2019-07-25-10-28-40.bpo-37354.RT3_3H.rst new file mode 100644 index 00000000000000..a314bcc9bf90f0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-07-25-10-28-40.bpo-37354.RT3_3H.rst @@ -0,0 +1 @@ +Make Activate.ps1 Powershell script static to allow for signing it. From d7e442e71eef3dde14a7b0e54b6f4a235e540700 Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Thu, 25 Jul 2019 13:29:55 -0700 Subject: [PATCH 3/6] Bug fixes --- Lib/venv/scripts/common/Activate.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/venv/scripts/common/Activate.ps1 b/Lib/venv/scripts/common/Activate.ps1 index 7d4c8f8f5ce7dc..c0d71c59667fd4 100644 --- a/Lib/venv/scripts/common/Activate.ps1 +++ b/Lib/venv/scripts/common/Activate.ps1 @@ -119,7 +119,7 @@ function Get-PyVenvCfgOverrides( $ConfigDir ) { # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $PyVenvCfgPath -ChildPath 'pyvenv.cfg' -ErrorAction Continue + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue if ($pyvenvConfigPath) { @@ -133,7 +133,7 @@ function Get-PyVenvCfgOverrides( # Remove extraneous quotations around a string value. if ("'""".Contains($val.Substring(0,1))) { - $val = $val.Substring(1, $m.Length - 2) + $val = $val.Substring(1, $val.Length - 2) } $pyvenvConfig[$keyval[0]] = $val @@ -159,7 +159,7 @@ if (-not $VenvDir) { # Next, read the `pyvenv.cfg` file to determine any required value such # as `prompt`. -$pyvenvCfg = Get-PyVenvCfgOverrides -VenvBaseDir $VenvDir +$pyvenvCfg = Get-PyVenvCfgOverrides -ConfigDir $VenvDir # Next, set the prompt from the command line, or the config file, or # just use the name of the virtual environment folder. From 1099c9b565e8d5c11e89c038e047370afa67a642 Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Thu, 25 Jul 2019 13:54:03 -0700 Subject: [PATCH 4/6] Promp(t) typo --- Lib/venv/scripts/common/Activate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/venv/scripts/common/Activate.ps1 b/Lib/venv/scripts/common/Activate.ps1 index c0d71c59667fd4..484df3c6195cc6 100644 --- a/Lib/venv/scripts/common/Activate.ps1 +++ b/Lib/venv/scripts/common/Activate.ps1 @@ -163,7 +163,7 @@ $pyvenvCfg = Get-PyVenvCfgOverrides -ConfigDir $VenvDir # Next, set the prompt from the command line, or the config file, or # just use the name of the virtual environment folder. -if (-not $Promp) { +if (-not $Prompt) { if ($pyvenvCfg -and $pyvenvCfg['prompt']) { $Prompt = $pyvenvCfg['prompt']; } From 481b7b71cf795129d05e30cc84281f344722d26c Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Fri, 26 Jul 2019 00:03:28 -0700 Subject: [PATCH 5/6] Correct problems - with prompt - with usage in Linux on PowerShell core --- Lib/venv/scripts/common/Activate.ps1 | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/Lib/venv/scripts/common/Activate.ps1 b/Lib/venv/scripts/common/Activate.ps1 index 484df3c6195cc6..18dc33e277d1dd 100644 --- a/Lib/venv/scripts/common/Activate.ps1 +++ b/Lib/venv/scripts/common/Activate.ps1 @@ -88,7 +88,7 @@ function global:deactivate ([switch]$NonDestructive) { } # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if ($_PYTHON_VENV_PROMPT_PREFIX) { + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force } @@ -118,11 +118,14 @@ function Get-PyVenvCfgOverrides( [String] $ConfigDir ) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue if ($pyvenvConfigPath) { + Write-Verbose "File exists, parse `key = value` lines" $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath $pyvenvConfig = @{ } @@ -137,6 +140,7 @@ function Get-PyVenvCfgOverrides( } $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" } } } @@ -150,11 +154,20 @@ function Get-PyVenvCfgOverrides( $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition $VenvExecDir = Get-Item -Path $VenvExecPath +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + # Set values required in priority: CmdLine, ConfigFile, Default # First, get the location of the virtual environment, it might not be # VenvExecDir if specified on the command line. -if (-not $VenvDir) { - $VenvDir = $VenvExecDir.Parent.FullName +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + $VenvDir = $VenvDir.Insert($VenvDir.Length, "/") + Write-Verbose "VenvDir=$VenvDir" } # Next, read the `pyvenv.cfg` file to determine any required value such @@ -163,13 +176,18 @@ $pyvenvCfg = Get-PyVenvCfgOverrides -ConfigDir $VenvDir # Next, set the prompt from the command line, or the config file, or # just use the name of the virtual environment folder. -if (-not $Prompt) { +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" $Prompt = $pyvenvCfg['prompt']; } else { - $venvDir = Get-Item -Path $VenvDir - $Prompt = $venvDir.Name; + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf } } @@ -195,7 +213,7 @@ if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt function global:prompt { - Write-Host -NoNewline -ForegroundColor DarkGreen "($_PYTHON_VENV_PROMPT_PREFIX) " + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " _OLD_VIRTUAL_PROMPT } } From 5874721c0aa6d35d874cb637601290ddaf4f24a0 Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Wed, 7 Aug 2019 13:48:10 -0700 Subject: [PATCH 6/6] PR suggestions --- Lib/venv/scripts/common/Activate.ps1 | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/venv/scripts/common/Activate.ps1 b/Lib/venv/scripts/common/Activate.ps1 index 18dc33e277d1dd..699c84097f1ac9 100644 --- a/Lib/venv/scripts/common/Activate.ps1 +++ b/Lib/venv/scripts/common/Activate.ps1 @@ -16,7 +16,7 @@ script is located within. .Parameter Prompt The prompt prefix to display when this virtual environment is activated. By default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parenthesis and followed by a single space (ie. '(.venv) '). +surrounded by parentheses and followed by a single space (ie. '(.venv) '). .Example Activate.ps1 @@ -35,7 +35,7 @@ Activates the Python virtual environment located in the specified location. Activate.ps1 -Prompt "MyPython" Activates the Python virtual environment that contains the Activate.ps1 script, and prefixes the current prompt with the specified string (surrounded in -parenthesis) while the virtual environment is active. +parentheses) while the virtual environment is active. #> @@ -100,7 +100,7 @@ function global:deactivate ([switch]$NonDestructive) { <# .Description -Get-PyVenvCfgOverrides parses the values from the pyvenv.cfg file located in the +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the given folder, and returns them in a map. For each line in the pyvenv.cfg file, if that line can be parsed into exactly @@ -114,7 +114,7 @@ stripped from the value before being captured. .Parameter ConfigDir Path to the directory that contains the `pyvenv.cfg` file. #> -function Get-PyVenvCfgOverrides( +function Get-PyVenvConfig( [String] $ConfigDir ) { @@ -123,11 +123,13 @@ function Get-PyVenvCfgOverrides( # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + if ($pyvenvConfigPath) { Write-Verbose "File exists, parse `key = value` lines" $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - $pyvenvConfig = @{ } $pyvenvConfigContent | ForEach-Object { $keyval = $PSItem -split "\s*=\s*", 2 @@ -172,7 +174,7 @@ if ($VenvDir) { # Next, read the `pyvenv.cfg` file to determine any required value such # as `prompt`. -$pyvenvCfg = Get-PyVenvCfgOverrides -ConfigDir $VenvDir +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir # Next, set the prompt from the command line, or the config file, or # just use the name of the virtual environment folder.