venv active
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.log
|
||||||
@@ -51,7 +51,7 @@ def get_weigth():
|
|||||||
|
|
||||||
def get_luggage_weigth():
|
def get_luggage_weigth():
|
||||||
weigth = db.fetch_luggage_weigth()
|
weigth = db.fetch_luggage_weigth()
|
||||||
print(f"The luggage has a weigth of {weigth}kg")
|
print(f"The luggage has a weigth of {weigth}g")
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
menu()
|
menu()
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.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 parentheses 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
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
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
|
||||||
|
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-PyVenvConfig(
|
||||||
|
[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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
$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, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($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
|
||||||
|
|
||||||
|
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 ($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("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$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.
|
||||||
|
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 {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# 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'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
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 "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear 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 -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# You cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||||
|
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||||
|
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||||
|
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||||
|
export VIRTUAL_ENV=$(cygpath /home/jakob/Documents/py_luggage/venv)
|
||||||
|
else
|
||||||
|
# use the path as-is
|
||||||
|
export VIRTUAL_ENV=/home/jakob/Documents/py_luggage/venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1='(venv) '"${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT='(venv) '
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV /home/jakob/Documents/py_luggage/venv
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = '(venv) '"$prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/). You cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV /home/jakob/Documents/py_luggage/venv
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
end
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/jakob/Documents/py_luggage/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/jakob/Documents/py_luggage/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/jakob/Documents/py_luggage/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: mariadb
|
||||||
|
Version: 1.1.14
|
||||||
|
Summary: Python MariaDB extension
|
||||||
|
Home-page: https://www.github.com/mariadb-corporation/mariadb-connector-python
|
||||||
|
Author: Georg Richter
|
||||||
|
License: LGPL 2.1
|
||||||
|
Project-URL: Bug Tracker, https://jira.mariadb.org/
|
||||||
|
Project-URL: Documentation, https://mariadb-corporation.github.io/mariadb-connector-python/
|
||||||
|
Project-URL: Source Code, https://www.github.com/mariadb-corporation/mariadb-connector-python
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Console
|
||||||
|
Classifier: Environment :: MacOS X
|
||||||
|
Classifier: Environment :: Win32 (MS Windows)
|
||||||
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)
|
||||||
|
Classifier: Programming Language :: C
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Operating System :: MacOS
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: Intended Audience :: End Users/Desktop
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Intended Audience :: System Administrators
|
||||||
|
Classifier: Topic :: Database
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: packaging
|
||||||
|
Dynamic: author
|
||||||
|
Dynamic: classifier
|
||||||
|
Dynamic: description
|
||||||
|
Dynamic: description-content-type
|
||||||
|
Dynamic: home-page
|
||||||
|
Dynamic: license
|
||||||
|
Dynamic: license-file
|
||||||
|
Dynamic: project-url
|
||||||
|
Dynamic: requires-dist
|
||||||
|
Dynamic: requires-python
|
||||||
|
Dynamic: summary
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="http://mariadb.com/">
|
||||||
|
<img src="https://mariadb.com/kb/static/images/logo-2018-black.png">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# MariaDB Connector/Python
|
||||||
|
|
||||||
|
[![License (LGPL version 2.1)][licence-image]](LICENSE)
|
||||||
|
[![Python 3.7][python-image]][python-url]
|
||||||
|
[](https://app.travis-ci.com/mariadb-corporation/mariadb-connector-python)
|
||||||
|
<a href="https://scan.coverity.com/projects/mariadb-connector-python">
|
||||||
|
<img alt="Coverity Scan Build Status"
|
||||||
|
src="https://scan.coverity.com/projects/21386/badge.svg"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
MariaDB Connector/Python enables python programs to access MariaDB and MySQL databases, using an API
|
||||||
|
which is compliant with the Python DB API 2.0 (PEP-249). It is written in C and uses MariaDB Connector/C
|
||||||
|
client library for client server communication.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MariaDB Connector/Python is licensed under the LGPL 2.1 or later (LGPL-2.1-or-later)
|
||||||
|
|
||||||
|
## Source code
|
||||||
|
|
||||||
|
MariaDB Connector/Python source code is hosted on [Github](https://github.com/mariadb-corporation/mariadb-connector-python)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
MariaDB Connector/Python documentation can be found on [Github Pages](https://mariadb-corporation.github.io/mariadb-connector-python/)
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
|
||||||
|
Bugs and feature requests should be filed in the [MariaDB bug ticket system](https://jira.mariadb.org/)
|
||||||
|
|
||||||
|
|
||||||
|
[licence-image]:https://img.shields.io/badge/license-GNU%20LGPL%20version%202.1-green.svg?style=flat-square
|
||||||
|
[python-image]:https://img.shields.io/badge/python-3.7-blue.svg
|
||||||
|
[python-url]:https://www.python.org/downloads/release/python-370/
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
mariadb-1.1.14.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
mariadb-1.1.14.dist-info/METADATA,sha256=o10KfHqiosQTjeSGQY-QL-VpfJuYLnGGqGSKNG7taYw,3372
|
||||||
|
mariadb-1.1.14.dist-info/RECORD,,
|
||||||
|
mariadb-1.1.14.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
mariadb-1.1.14.dist-info/WHEEL,sha256=UKM62ksD4jd3M6k5acFAaw8CHnQfpjto_KljRXbypP0,104
|
||||||
|
mariadb-1.1.14.dist-info/licenses/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
|
||||||
|
mariadb-1.1.14.dist-info/top_level.txt,sha256=lLz5H7vCj1A7w7jg0yMFXrwc-PADp2SjVl1GiJrDiWE,8
|
||||||
|
mariadb/__init__.py,sha256=K4u7Dm0M69FMj0_6LZ5STOpT4JJVlqTVtTxVugDhxwQ,6011
|
||||||
|
mariadb/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
mariadb/__pycache__/connectionpool.cpython-312.pyc,,
|
||||||
|
mariadb/__pycache__/connections.cpython-312.pyc,,
|
||||||
|
mariadb/__pycache__/cursors.cpython-312.pyc,,
|
||||||
|
mariadb/__pycache__/dbapi20.cpython-312.pyc,,
|
||||||
|
mariadb/__pycache__/field.cpython-312.pyc,,
|
||||||
|
mariadb/__pycache__/release_info.cpython-312.pyc,,
|
||||||
|
mariadb/_mariadb.cpython-312-x86_64-linux-gnu.so,sha256=N4w2vVrqBmOaYg0rWPYEyj284p9hlkuzoe_NTYHG1-w,345768
|
||||||
|
mariadb/connectionpool.py,sha256=SH1OObb3vKDk9vgjWE_wGA7sv1LEcqrlF8lsywQ9zjE,11566
|
||||||
|
mariadb/connections.py,sha256=WM29p_vqIGqxYz6F8a8A1C4Nf_DsH6UkQSqkcyIliVw,23546
|
||||||
|
mariadb/constants/CAPABILITY.py,sha256=clOg_Ce5fAP8lgPyQ-xtJ0EI-FGbJeW7rp9IV6WYfRA,831
|
||||||
|
mariadb/constants/CLIENT.py,sha256=9kCIi59s6kh7mEXWUSB8UzZmr-mfgnu4FoQLz6pezKY,828
|
||||||
|
mariadb/constants/CURSOR.py,sha256=V6PcHC8Cmt6kLuejVybuMKYY_IvPdkcLArUNWGRfgSI,209
|
||||||
|
mariadb/constants/ERR.py,sha256=q2K17DRfulpSMHaPRwl2KHiOB45CO-YJZO7ftiLmMkk,41779
|
||||||
|
mariadb/constants/EXT_FIELD_TYPE.py,sha256=69XJ1IpDBjAS98Zgzl1X9vde6EcD7mKpGl6Of9Y9Fls,355
|
||||||
|
mariadb/constants/FIELD_FLAG.py,sha256=KUlMVnQHnPJw7CsQxxL3ncs76mC5DJGfTUAltilJDOI,609
|
||||||
|
mariadb/constants/FIELD_TYPE.py,sha256=u5tYaiIde-7SVlGbtbaGV6gEg70FqEJc1ny44xgr4AI,630
|
||||||
|
mariadb/constants/INDICATOR.py,sha256=sTJ6mqwy_9NYf2tegHeZ-zLRuQ8aQgQrIe77VewISTE,354
|
||||||
|
mariadb/constants/INFO.py,sha256=u4IpUmSltwu4FESI-6bMpJnMl1xhHU6w72lSAMnp2Rs,743
|
||||||
|
mariadb/constants/STATUS.py,sha256=XDbqAslsz43g3tjOkuuY4oaQtqmATdxw2EcRLN8In5Y,426
|
||||||
|
mariadb/constants/TPC_STATE.py,sha256=ferMGNtes-xdYqayroN6l226I5ux_hZ_uxrZuErZV4s,29
|
||||||
|
mariadb/constants/__init__.py,sha256=BljXLv1ZQyIyHHTrw137vODnJdQZTkrk7QIyEUlpaA4,114
|
||||||
|
mariadb/constants/__pycache__/CAPABILITY.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/CLIENT.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/CURSOR.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/ERR.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/EXT_FIELD_TYPE.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/FIELD_FLAG.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/FIELD_TYPE.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/INDICATOR.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/INFO.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/STATUS.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/TPC_STATE.cpython-312.pyc,,
|
||||||
|
mariadb/constants/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
mariadb/cursors.py,sha256=ucKYI41d9htkRhmkOtBr8bo3tWhaotoKlsfNUieP8p4,21624
|
||||||
|
mariadb/dbapi20.py,sha256=5jmqnHYaGC1_CnKlnBfimQaMLKPAA2k7qw-uNFLtXzU,3311
|
||||||
|
mariadb/field.py,sha256=LtmB5Hq8sPveY17bpSsBney57qW2Lxq13uOYx-eMErM,2363
|
||||||
|
mariadb/release_info.py,sha256=v5u0LkjznAoDyHJkYcBKnzahtEBgPAhlsEjXjIEshLU,82
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (84.0.0)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp312-cp312-linux_x86_64
|
||||||
|
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms of the
|
||||||
|
ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library. It is
|
||||||
|
safest to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||||
|
USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James Random
|
||||||
|
Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
mariadb
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
'''
|
||||||
|
MariaDB Connector/Python module enables python programs to access MariaDB and
|
||||||
|
MySQL databases, using an API which is compliant with the Python DB API 2.0
|
||||||
|
(PEP-249).
|
||||||
|
'''
|
||||||
|
import mariadb
|
||||||
|
from ._mariadb import (
|
||||||
|
DataError,
|
||||||
|
DatabaseError,
|
||||||
|
Error,
|
||||||
|
IntegrityError,
|
||||||
|
InterfaceError,
|
||||||
|
InternalError,
|
||||||
|
NotSupportedError,
|
||||||
|
OperationalError,
|
||||||
|
PoolError,
|
||||||
|
ProgrammingError,
|
||||||
|
Warning,
|
||||||
|
mariadbapi_version,
|
||||||
|
_have_asan,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .field import fieldinfo
|
||||||
|
from mariadb.dbapi20 import * # noqa: F401,F403
|
||||||
|
from mariadb.connectionpool import * # noqa: F401,F403
|
||||||
|
from mariadb.cursors import Cursor
|
||||||
|
from mariadb.release_info import __version__ as __version__
|
||||||
|
from mariadb.release_info import __version_info__ as __version_info__
|
||||||
|
from mariadb.release_info import __author__ as __author__
|
||||||
|
from mariadb.connections import Connection
|
||||||
|
# disable for now, until tests are in place
|
||||||
|
# from mariadb.pooling import *
|
||||||
|
|
||||||
|
_POOLS = _CONNECTION_POOLS = {}
|
||||||
|
|
||||||
|
__all__ = ["DataError", "DatabaseError", "Error", "IntegrityError",
|
||||||
|
"InterfaceError", "InternalError", "NotSupportedError",
|
||||||
|
"OperationalError", "PoolError", "ProgrammingError",
|
||||||
|
"Warning", "Connection", "__version__", "__version_info__",
|
||||||
|
"__author__", "Cursor", "fieldinfo", "_have_asan"]
|
||||||
|
|
||||||
|
|
||||||
|
def connect(*args, connectionclass=mariadb.connections.Connection, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a MariaDB Connection object.
|
||||||
|
|
||||||
|
By default, the standard connectionclass mariadb.connections.Connection
|
||||||
|
will be created.
|
||||||
|
|
||||||
|
Parameter connectionclass specifies a subclass of
|
||||||
|
mariadb.Connection object. If not specified, default will be used.
|
||||||
|
This optional parameter was added in version 1.1.0.
|
||||||
|
|
||||||
|
Connection parameters are provided as a set of keyword arguments:
|
||||||
|
|
||||||
|
- **`host`** - The host name or IP address of the database server. If MariaDB Connector/Python was built with MariaDB Connector/C 3.3, it is also possible to provide a comma separated list of hosts for simple fail over in case of one or more hosts are not available.
|
||||||
|
- **`user`, `username`** - The username used to authenticate with the database server
|
||||||
|
- **`password`, `passwd`** - The password of the given user
|
||||||
|
- **`database`, `db`** - Database (schema) name to use when connecting with the database server
|
||||||
|
- **`unix_socket`** - The location of the unix socket file to use instead of using an IP port to connect. If socket authentication is enabled, this can also be used in place of a password.
|
||||||
|
- **`port`** - Port number of the database server. If not specified, the default value of 3306 will be used.
|
||||||
|
- **`connect_timeout`** - Connect timeout in seconds
|
||||||
|
- **`read_timeout`** - Read timeout in seconds
|
||||||
|
- **`write_timeout`** - Write timeout in seconds
|
||||||
|
- **`local_infile`** - Enables or disables the use of LOAD DATA LOCAL INFILE statements.
|
||||||
|
- **`compress`** (default: `False`) - Uses the compressed protocol for client server communication. If the server doesn't support compressed protocol, the default protocol will be used.
|
||||||
|
- **`init_command`** - Command(s) which will be executed when connecting and reconnecting to the database server
|
||||||
|
- **`default_file`** - Read options from the specified option file. If the file is an empty string, default configuration file(s) will be used
|
||||||
|
- **`default_group`** - Read options from the specified group
|
||||||
|
- **`plugin_dir`** - Directory which contains MariaDB client plugins.
|
||||||
|
- **`reconnect`** - Enables or disables automatic reconnect. Available since version 1.1.4
|
||||||
|
- **`ssl_key`** - Defines a path to a private key file to use for TLS. This option requires that you use the absolute path, not a relative path. The specified key must be in PEM format
|
||||||
|
- **`ssl_cert`** - Defines a path to the X509 certificate file to use for TLS. This option requires that you use the absolute path, not a relative path. The X609 certificate must be in PEM format.
|
||||||
|
- **`ssl_ca`** - Defines a path to a PEM file that should contain one or more X509 certificates for trusted Certificate Authorities (CAs) to use for TLS. This option requires that you use the absolute path, not a relative path.
|
||||||
|
- **`ssl_capath`** - Defines a path to a directory that contains one or more PEM files that contains one X509 certificate for a trusted Certificate Authority (CA)
|
||||||
|
- **`ssl_cipher`** - Defines a list of permitted cipher suites to use for TLS
|
||||||
|
- **`ssl_crlpath`** - Defines a path to a PEM file that should contain one or more revoked X509 certificates to use for TLS. This option requires that you use the absolute path, not a relative path.
|
||||||
|
- **`ssl_verify_cert`** - Enables server certificate verification.
|
||||||
|
- **`ssl`** - The connection must use TLS security, or it will fail.
|
||||||
|
- **`tls_version`** - A comma-separated list (without whitespaces) of TLS versions. Valid versions are TLSv1.0, TLSv1.1,TLSv1.2 and TLSv1.3. Added in version 1.1.7.
|
||||||
|
- **`autocommit`** (default: `False`) - Specifies the autocommit settings. True will enable autocommit, False will disable it (default).
|
||||||
|
- **`converter`** - Specifies a conversion dictionary, where keys are FIELD_TYPE values and values are conversion functions
|
||||||
|
|
||||||
|
"""
|
||||||
|
if kwargs:
|
||||||
|
if "pool_name" in kwargs:
|
||||||
|
if not kwargs["pool_name"] in mariadb._CONNECTION_POOLS:
|
||||||
|
pool = mariadb.ConnectionPool(**kwargs)
|
||||||
|
else:
|
||||||
|
pool = mariadb._CONNECTION_POOLS[kwargs["pool_name"]]
|
||||||
|
c = pool.get_connection()
|
||||||
|
return c
|
||||||
|
|
||||||
|
connection = connectionclass(*args, **kwargs)
|
||||||
|
if not isinstance(connection, mariadb.connections.Connection):
|
||||||
|
raise mariadb.ProgrammingError("%s is not an instance of "
|
||||||
|
"mariadb.Connection" % connection)
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
client_version_info = tuple(int(x, 10) for x in mariadbapi_version.split('.'))
|
||||||
|
client_version = client_version_info[0] * 10000 +\
|
||||||
|
client_version_info[1] * 1000 + client_version_info[2]
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,301 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2020-2021 Georg Richter and MariaDB Corporation AB
|
||||||
|
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Library General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Library General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Library General Public
|
||||||
|
# License along with this library; if not see <http://www.gnu.org/licenses>
|
||||||
|
# or write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin St., Fifth Floor, Boston, MA 02110, USA
|
||||||
|
#
|
||||||
|
|
||||||
|
import mariadb
|
||||||
|
import _thread
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mariadb.constants import STATUS
|
||||||
|
|
||||||
|
MAX_POOL_SIZE = 64
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionPool(object):
|
||||||
|
"""
|
||||||
|
Class defining a pool of database connections
|
||||||
|
|
||||||
|
MariaDB Connector/Python supports simple connection pooling.
|
||||||
|
A connection pool holds a number of open connections and handles thread safety when providing connections to threads.
|
||||||
|
|
||||||
|
The size of a connection pool is configurable at creation time, but cannot be changed afterward. The maximum size of a connection pool is limited to 64 connections.
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
|
||||||
|
- **`pool_name`** (``str``) - Name of connection pool
|
||||||
|
- **`pool_size`** (``int``) - Size of pool. The Maximum allowed number is 64. Default to 5
|
||||||
|
- **`pool_reset_connection`** (``bool``) - Will reset the connection before returning it to the pool. Default to True.
|
||||||
|
- **`pool_validation_interval`** (``int``) - Specifies the validation interval in milliseconds after which the status of a connection requested from the pool is checked. A value of 0 means that the status will always be checked. Default to 500 (Added in version 1.1.6)
|
||||||
|
- **\*\*kwargs** - Optional additional connection arguments, as described in mariadb.connect() method.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a connection pool class
|
||||||
|
|
||||||
|
:param str pool_name:
|
||||||
|
Name of connection pool
|
||||||
|
|
||||||
|
:param int pool_size:
|
||||||
|
Size of pool. If not specified, the default value of 5 will be used.
|
||||||
|
The Maximum allowed number is 64.
|
||||||
|
|
||||||
|
:param bool pool_reset_connection:
|
||||||
|
Will reset the connection before returning it to the pool.
|
||||||
|
The Default value is True.
|
||||||
|
|
||||||
|
:param **kwargs kwargs:
|
||||||
|
Optional additional connection arguments, as described in
|
||||||
|
mariadb.connect() method.
|
||||||
|
"""
|
||||||
|
self._connections_free = []
|
||||||
|
self._connections_used = []
|
||||||
|
self._pool_args = {}
|
||||||
|
self._conn_args = {}
|
||||||
|
self._lock_pool = _thread.RLock()
|
||||||
|
self.__closed = 0
|
||||||
|
|
||||||
|
key_words = ["pool_name", "pool_size", "pool_reset_connection",
|
||||||
|
"pool_validation_interval"]
|
||||||
|
|
||||||
|
# check if pool_name was provided
|
||||||
|
if kwargs and "pool_name" in kwargs:
|
||||||
|
|
||||||
|
# check if pool_name already exists
|
||||||
|
if kwargs["pool_name"] in mariadb._CONNECTION_POOLS:
|
||||||
|
raise mariadb.ProgrammingError("Pool '%s' already exists"
|
||||||
|
% kwargs["pool_name"])
|
||||||
|
else:
|
||||||
|
raise mariadb.ProgrammingError("No pool name specified")
|
||||||
|
|
||||||
|
# save pool keyword arguments
|
||||||
|
self._pool_args["name"] = kwargs.get("pool_name")
|
||||||
|
self._pool_args["size"] = int(kwargs.get("pool_size", 5))
|
||||||
|
self._pool_args["reset_connection"] = \
|
||||||
|
bool(kwargs.get("pool_reset_connection", True))
|
||||||
|
self._pool_args["validation_interval"] = \
|
||||||
|
int(kwargs.get("pool_validation_interval", 500))
|
||||||
|
|
||||||
|
# validate pool size (must be in range between 1 and MAX_POOL_SIZE)
|
||||||
|
if not (0 < self._pool_args["size"] <= MAX_POOL_SIZE):
|
||||||
|
raise mariadb.ProgrammingError("Pool size must be in range of "
|
||||||
|
"1 and %s" % MAX_POOL_SIZE)
|
||||||
|
|
||||||
|
# store pool and connection arguments
|
||||||
|
self._conn_args = kwargs.copy()
|
||||||
|
for key in key_words:
|
||||||
|
if key in self._conn_args:
|
||||||
|
del self._conn_args[key]
|
||||||
|
|
||||||
|
if len(self._conn_args) > 0:
|
||||||
|
with self._lock_pool:
|
||||||
|
# fill connection pool
|
||||||
|
for i in range(0, self._pool_args["size"]):
|
||||||
|
try:
|
||||||
|
connection = mariadb.Connection(**self._conn_args)
|
||||||
|
except mariadb.Error:
|
||||||
|
# if an error occurred, close all connections
|
||||||
|
# and raise exception
|
||||||
|
for j in reversed(range(0, len(self._connections_free))):
|
||||||
|
try:
|
||||||
|
self._connections_free[j].close()
|
||||||
|
except mariadb.Error:
|
||||||
|
# connect failed, so we are not
|
||||||
|
# interested in errors
|
||||||
|
# from close() method
|
||||||
|
pass
|
||||||
|
del self._connections_free[j]
|
||||||
|
raise
|
||||||
|
self.add_connection(connection)
|
||||||
|
|
||||||
|
# store connection pool in _CONNECTION_POOLS
|
||||||
|
mariadb._CONNECTION_POOLS[self._pool_args["name"]] = self
|
||||||
|
|
||||||
|
def _replace_connection(self, connection):
|
||||||
|
"""
|
||||||
|
Removes the given connection and adds a new connection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
if connection in self._connections_free:
|
||||||
|
x = self._connections_free.index(connection)
|
||||||
|
del self._connections_free[x]
|
||||||
|
elif connection in self._connections_used:
|
||||||
|
x = self._connections_used.index(connection)
|
||||||
|
del self._connections_used[x]
|
||||||
|
|
||||||
|
connection._Connection__pool = None
|
||||||
|
connection.close()
|
||||||
|
return self.add_connection()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if (self.__closed):
|
||||||
|
return "<mariadb.connectionPool.ConnectionPool object (closed) "\
|
||||||
|
"at %s>" % (hex(id(self)),)
|
||||||
|
else:
|
||||||
|
return "<mariadb.connectionPool.ConnectionPool object (name=%s) "\
|
||||||
|
"at %s>" % (self.pool_name, hex(id(self)))
|
||||||
|
|
||||||
|
def add_connection(self, connection=None):
|
||||||
|
"""
|
||||||
|
Adds a connection object to the connection pool.
|
||||||
|
|
||||||
|
In case that the pool doesn’t have a free slot or is not configured,
|
||||||
|
a PoolError exception will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._conn_args:
|
||||||
|
raise mariadb.PoolError("Couldn't get configuration for pool %s" %
|
||||||
|
self._pool_args["name"])
|
||||||
|
|
||||||
|
if (connection is not None and
|
||||||
|
not isinstance(connection, mariadb.connections.Connection)):
|
||||||
|
raise mariadb.ProgrammingError("Passed parameter is not a "
|
||||||
|
"connection object")
|
||||||
|
|
||||||
|
if connection is None and len(self._conn_args) == 0:
|
||||||
|
raise mariadb.PoolError("Can't get configuration for pool %s" %
|
||||||
|
self._pool_args["name"])
|
||||||
|
|
||||||
|
total = len(self._connections_free + self._connections_used)
|
||||||
|
if total >= self._pool_args["size"]:
|
||||||
|
raise mariadb.PoolError("Can't add connection to pool %s: "
|
||||||
|
"No free slot available (%s)." %
|
||||||
|
(self._pool_args["name"],
|
||||||
|
total))
|
||||||
|
|
||||||
|
with self._lock_pool:
|
||||||
|
if connection is None:
|
||||||
|
connection = mariadb.Connection(**self._conn_args)
|
||||||
|
|
||||||
|
connection._Connection__pool = self
|
||||||
|
connection.__last_used = time.perf_counter_ns()
|
||||||
|
self._connections_free.append(connection)
|
||||||
|
return connection
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
"""
|
||||||
|
Returns a connection from the connection pool or raises a PoolError
|
||||||
|
exception if a connection is not available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
|
||||||
|
with self._lock_pool:
|
||||||
|
for i in range(0, len(self._connections_free)):
|
||||||
|
conn = self._connections_free[i]
|
||||||
|
dt = (time.perf_counter_ns() - conn.__last_used) / 1000000
|
||||||
|
if dt > self._pool_args["validation_interval"]:
|
||||||
|
try:
|
||||||
|
conn.ping()
|
||||||
|
except mariadb.Error:
|
||||||
|
conn = self._replace_connection(conn)
|
||||||
|
if not conn:
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn._used += 1
|
||||||
|
self._connections_used.append(conn)
|
||||||
|
idx = self._connections_free.index(conn)
|
||||||
|
del self._connections_free[idx]
|
||||||
|
return conn
|
||||||
|
|
||||||
|
raise mariadb.PoolError("No connection available")
|
||||||
|
|
||||||
|
def _close_connection(self, connection):
|
||||||
|
"""
|
||||||
|
Returns connection to the pool. Internally used
|
||||||
|
by connection object.
|
||||||
|
"""
|
||||||
|
with self._lock_pool:
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._pool_args["reset_connection"]:
|
||||||
|
connection.reset()
|
||||||
|
elif connection.server_status & STATUS.IN_TRANS:
|
||||||
|
connection.rollback()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._replace_connection(connection)
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
if connection in self._connections_used:
|
||||||
|
x = self._connections_used.index(connection)
|
||||||
|
del self._connections_used[x]
|
||||||
|
connection.__last_used = time.perf_counter_ns()
|
||||||
|
self._connections_free.append(connection)
|
||||||
|
|
||||||
|
def set_config(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Sets the connection configuration for the connection pool.
|
||||||
|
For valid connection arguments, check the mariadb.connect() method.
|
||||||
|
|
||||||
|
Note: This method doesn't create connections in the pool.
|
||||||
|
To fill the pool, one has to use add_connection() ḿethod.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._conn_args = kwargs
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Closes connection pool and all connections."""
|
||||||
|
try:
|
||||||
|
for c in (self._connections_free + self._connections_used):
|
||||||
|
c._Connection__pool = None
|
||||||
|
c.close()
|
||||||
|
finally:
|
||||||
|
self._connections_free = None
|
||||||
|
self._connections_used = None
|
||||||
|
del mariadb._CONNECTION_POOLS[self._pool_args["name"]]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pool_name(self):
|
||||||
|
"""Returns the name of the connection pool."""
|
||||||
|
|
||||||
|
return self._pool_args["name"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pool_size(self):
|
||||||
|
"""Returns the size of the connection pool."""
|
||||||
|
|
||||||
|
return self._pool_args["size"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_size(self):
|
||||||
|
"Returns the maximum size for connection pools."""
|
||||||
|
|
||||||
|
return MAX_POOL_SIZE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection_count(self):
|
||||||
|
"Returns the number of connections in connection pool."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return len(self._connections_free + self._connections_used)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pool_reset_connection(self):
|
||||||
|
"""
|
||||||
|
If set to true, the connection will be reset on both client and server
|
||||||
|
side after .close() method was called
|
||||||
|
"""
|
||||||
|
return self._pool_args["reset_connection"]
|
||||||
|
|
||||||
|
@pool_reset_connection.setter
|
||||||
|
def pool_reset_connection(self, reset):
|
||||||
|
self._pool_args["reset_connection"] = reset
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2020-2021 Georg Richter and MariaDB Corporation AB
|
||||||
|
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Library General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Library General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Library General Public
|
||||||
|
# License along with this library; if not see <http://www.gnu.org/licenses>
|
||||||
|
# or write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin St., Fifth Floor, Boston, MA 02110, USA
|
||||||
|
#
|
||||||
|
|
||||||
|
import mariadb
|
||||||
|
import socket
|
||||||
|
import mariadb.cursors
|
||||||
|
|
||||||
|
from mariadb.constants import STATUS, TPC_STATE, INFO
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
_DEFAULT_CHARSET = "utf8mb4"
|
||||||
|
_DEFAULT_COLLATION = "utf8mb4_general_ci"
|
||||||
|
_MAX_TPC_XID_SIZE = 64
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(mariadb._mariadb.connection):
|
||||||
|
"""
|
||||||
|
MariaDB Connector/Python Connection Object
|
||||||
|
|
||||||
|
Handles the connection to a MariaDB or MySQL database server.
|
||||||
|
It encapsulates a database session.
|
||||||
|
|
||||||
|
Connections are created using the method mariadb.connect()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _check_closed(self):
|
||||||
|
if self._closed:
|
||||||
|
raise mariadb.ProgrammingError("Invalid connection or "
|
||||||
|
"not connected")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Establishes a connection to a database server and returns a connection
|
||||||
|
object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._socket = None
|
||||||
|
self._used = 0
|
||||||
|
self._last_executed_statement = None
|
||||||
|
self._socket = None
|
||||||
|
self.__pool = None
|
||||||
|
self.__last_used = 0
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
self._xid = None
|
||||||
|
|
||||||
|
autocommit = kwargs.pop("autocommit", False)
|
||||||
|
reconnect = kwargs.pop("reconnect", False)
|
||||||
|
self._converter = kwargs.pop("converter", None)
|
||||||
|
|
||||||
|
# if host contains a connection string or multiple hosts,
|
||||||
|
# we need to check if it's supported by Connector/C
|
||||||
|
if "host" in kwargs:
|
||||||
|
host = kwargs.get("host")
|
||||||
|
if version.Version(mariadb.mariadbapi_version) <\
|
||||||
|
version.Version('3.3.0') and ',' in host:
|
||||||
|
raise mariadb.ProgrammingError("Host failover list requires "
|
||||||
|
"MariaDB Connector/C 3.3.0 "
|
||||||
|
"or newer")
|
||||||
|
|
||||||
|
# compatibility feature: if SSL is provided as a dictionary,
|
||||||
|
# we will map it's content
|
||||||
|
if "ssl" in kwargs and not isinstance(kwargs["ssl"], bool):
|
||||||
|
ssl = kwargs.pop("ssl", None)
|
||||||
|
for key in ["ca", "cert", "capath", "key", "cipher"]:
|
||||||
|
if key in ssl:
|
||||||
|
kwargs["ssl_%s" % key] = ssl[key]
|
||||||
|
kwargs["ssl"] = True
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.autocommit = autocommit
|
||||||
|
self.auto_reconnect = reconnect
|
||||||
|
|
||||||
|
def cursor(self, cursorclass=mariadb.cursors.Cursor, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a new cursor object for the current connection.
|
||||||
|
|
||||||
|
If no cursorclass was specified, a cursor with default mariadb.Cursor class will be created.
|
||||||
|
|
||||||
|
Optional keyword parameters:
|
||||||
|
|
||||||
|
- **buffered** (default: ``True``) - If disabled, the result will be unbuffered, which means before executing another statement with the same connection, the entire result set must be fetched. Please note that the default was False for MariaDB Connector/Python versions < 1.1.0.
|
||||||
|
- **dictionary** (default: ``False``) - Return fetch values as dictionary when enabled.
|
||||||
|
- **named_tuple** (default: ``False``) - Return fetch values as named tuple. This feature exists for compatibility reasons and should be avoided due to possible inconsistency.
|
||||||
|
- **cursor_type** (default: ``CURSOR.NONE``) - If cursor_type is set to CURSOR.READ_ONLY, a cursor is opened for the statement invoked with cursors execute() method.
|
||||||
|
- **prepared** (default: ``False``) - When enabled, the cursor will remain in prepared state after the first execute() method was called. Further calls to execute() method will ignore the SQL statement.
|
||||||
|
- **binary** (default: ``False``) - Always execute statement in MariaDB client/server binary protocol.
|
||||||
|
|
||||||
|
In versions prior to 1.1.0 results were unbuffered by default, which means before executing another statement with the same connection, the entire result set must be fetched.
|
||||||
|
|
||||||
|
fetch* methods of the cursor class by default return result set values as a tuple, unless dictionary or named_tuple was specified. The latter one exists for compatibility reasons and should be avoided due to possible inconsistency in case two or more fields in a result set have the same name.
|
||||||
|
|
||||||
|
If cursor_type is set to CURSOR.READ_ONLY, a cursor is opened for the statement invoked with cursors execute() method.
|
||||||
|
"""
|
||||||
|
self._check_closed()
|
||||||
|
cursor = cursorclass(self, **kwargs)
|
||||||
|
if not isinstance(cursor, mariadb._mariadb.cursor):
|
||||||
|
raise mariadb.ProgrammingError("%s is not an instance of "
|
||||||
|
"mariadb.cursor" % cursor)
|
||||||
|
return cursor
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._check_closed()
|
||||||
|
if self._Connection__pool:
|
||||||
|
self._Connection__pool._close_connection(self)
|
||||||
|
else:
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._check_closed()
|
||||||
|
"Returns a copy of the connection."
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self._check_closed()
|
||||||
|
"Closes connection."
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
"""
|
||||||
|
Commit any pending transaction to the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self.tpc_state > TPC_STATE.NONE:
|
||||||
|
raise mariadb.ProgrammingError("commit() is not allowed if "
|
||||||
|
"a TPC transaction is active")
|
||||||
|
self._execute_command("COMMIT")
|
||||||
|
self._read_response()
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
"""
|
||||||
|
Causes the database to roll back to the start of any pending
|
||||||
|
transaction
|
||||||
|
|
||||||
|
Closing a connection without committing the changes first will
|
||||||
|
cause an implicit rollback to be performed.
|
||||||
|
Note that rollback() will not work as expected if autocommit mode
|
||||||
|
was set to True or the storage engine does not support transactions."
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self.tpc_state > TPC_STATE.NONE:
|
||||||
|
raise mariadb.ProgrammingError("rollback() is not allowed if a "
|
||||||
|
"TPC transaction is active")
|
||||||
|
self._execute_command("ROLLBACK")
|
||||||
|
self._read_response()
|
||||||
|
|
||||||
|
def kill(self, id: int):
|
||||||
|
"""
|
||||||
|
This function is used to ask the server to kill a database connection
|
||||||
|
specified by the processid parameter.
|
||||||
|
|
||||||
|
The connection id can be retrieved by `SHOW PROCESSLIST` SQL command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if not isinstance(id, int):
|
||||||
|
raise mariadb.ProgrammingError("id must be of type int.")
|
||||||
|
stmt = "KILL %s" % id
|
||||||
|
self._execute_command(stmt)
|
||||||
|
self._read_response()
|
||||||
|
|
||||||
|
def begin(self):
|
||||||
|
"""
|
||||||
|
Start a new transaction which can be committed by .commit() method,
|
||||||
|
or canceled by .rollback() method.
|
||||||
|
"""
|
||||||
|
self._check_closed()
|
||||||
|
self._execute_command("BEGIN")
|
||||||
|
self._read_response()
|
||||||
|
|
||||||
|
def select_db(self, new_db: str):
|
||||||
|
"""
|
||||||
|
Gets the default database for the current connection.
|
||||||
|
|
||||||
|
The default database can also be obtained or changed by database
|
||||||
|
attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
self.database = new_db
|
||||||
|
|
||||||
|
def get_server_version(self):
|
||||||
|
"""
|
||||||
|
Returns a tuple representing the version of the connected server in
|
||||||
|
the following format: (MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.server_version_info
|
||||||
|
|
||||||
|
def show_warnings(self):
|
||||||
|
"""
|
||||||
|
Shows error, warning and note messages from last executed command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if (not self.warnings):
|
||||||
|
return None
|
||||||
|
|
||||||
|
cursor = self.cursor()
|
||||||
|
cursor.execute("SHOW WARNINGS")
|
||||||
|
ret = cursor.fetchall()
|
||||||
|
del cursor
|
||||||
|
return ret
|
||||||
|
|
||||||
|
class xid(tuple):
|
||||||
|
"""
|
||||||
|
xid(format_id: int, global_transaction_id: str, branch_qualifier: str)
|
||||||
|
|
||||||
|
Creates a transaction ID object suitable for passing to the .tpc_*()
|
||||||
|
methods of this connection.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- format_id: Format id. Default to value `0`.
|
||||||
|
|
||||||
|
- global_transaction_id: Global transaction qualifier, which must be
|
||||||
|
unique. The maximum length of the global transaction id is
|
||||||
|
limited to 64 characters.
|
||||||
|
|
||||||
|
- branch_qualifier: Branch qualifier which represents a local
|
||||||
|
transaction identifier. The maximum length of the branch qualifier
|
||||||
|
is limited to 64 characters.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __new__(self, format_id, transaction_id, branch_qualifier):
|
||||||
|
if not isinstance(format_id, int):
|
||||||
|
raise mariadb.ProgrammingError("argument 1 must be int, "
|
||||||
|
"not %s",
|
||||||
|
type(format_id).__name__)
|
||||||
|
if not isinstance(transaction_id, str):
|
||||||
|
raise mariadb.ProgrammingError("argument 2 must be str, "
|
||||||
|
"not %s",
|
||||||
|
type(transaction_id).__mane__)
|
||||||
|
if not isinstance(branch_qualifier, str):
|
||||||
|
raise mariadb.ProgrammingError("argument 3 must be str, "
|
||||||
|
"not %s",
|
||||||
|
type(transaction_id).__name__)
|
||||||
|
if len(transaction_id) > _MAX_TPC_XID_SIZE:
|
||||||
|
raise mariadb.ProgrammingError("Maximum length of "
|
||||||
|
"transaction_id exceeded.")
|
||||||
|
if len(branch_qualifier) > _MAX_TPC_XID_SIZE:
|
||||||
|
raise mariadb.ProgrammingError("Maximum length of "
|
||||||
|
"branch_qualifier exceeded.")
|
||||||
|
if format_id == 0:
|
||||||
|
format_id = 1
|
||||||
|
return super().__new__(self, (format_id,
|
||||||
|
transaction_id,
|
||||||
|
branch_qualifier))
|
||||||
|
|
||||||
|
def tpc_begin(self, xid):
|
||||||
|
"""
|
||||||
|
Parameter:
|
||||||
|
xid: xid object which was created by .xid() method of connection
|
||||||
|
class
|
||||||
|
|
||||||
|
Begins a TPC transaction with the given transaction ID xid.
|
||||||
|
|
||||||
|
This method should be called outside a transaction
|
||||||
|
(i.e., nothing may have been executed since the last .commit()
|
||||||
|
or .rollback()).
|
||||||
|
Furthermore, it is an error to call .commit() or .rollback() within
|
||||||
|
the TPC transaction. A ProgrammingError is raised if the application
|
||||||
|
calls .commit() or .rollback() during an active TPC transaction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if type(xid).__name__ != "xid":
|
||||||
|
raise mariadb.ProgrammingError("argument 1 must be xid "
|
||||||
|
"not %s", type(xid).__name__)
|
||||||
|
stmt = "XA BEGIN '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
try:
|
||||||
|
self._execute_command(stmt)
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
raise
|
||||||
|
self.tpc_state = TPC_STATE.XID
|
||||||
|
self._xid = xid
|
||||||
|
|
||||||
|
def tpc_commit(self, xid=None):
|
||||||
|
"""
|
||||||
|
Optional parameter:
|
||||||
|
|
||||||
|
- xid
|
||||||
|
: xid object which was created by .xid() method of connection class.
|
||||||
|
|
||||||
|
When called with no arguments, .tpc_commit() commits a TPC transaction
|
||||||
|
previously prepared with .tpc_prepare().
|
||||||
|
|
||||||
|
If .tpc_commit() is called prior to .tpc_prepare(), a single phase
|
||||||
|
commit is performed. A transaction manager may choose to do this if
|
||||||
|
only a single resource is participating in the global transaction.
|
||||||
|
When called with a transaction ID xid, the database commits the given
|
||||||
|
transaction. If an invalid transaction ID is provided,
|
||||||
|
a ProgrammingError will be raised.
|
||||||
|
This form should be called outside a transaction, and
|
||||||
|
is intended for use in recovery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if not xid:
|
||||||
|
xid = self._xid
|
||||||
|
|
||||||
|
if self.tpc_state == TPC_STATE.NONE:
|
||||||
|
raise mariadb.ProgrammingError("Transaction not started.")
|
||||||
|
if xid is None and self.tpc_state != TPC_STATE.PREPARE:
|
||||||
|
raise mariadb.ProgrammingError("Transaction is not prepared.")
|
||||||
|
if xid and type(xid).__name__ != "xid":
|
||||||
|
raise mariadb.ProgrammingError("argument 1 must be xid "
|
||||||
|
"not %s" % type(xid).__name__)
|
||||||
|
|
||||||
|
if self.tpc_state < TPC_STATE.PREPARE:
|
||||||
|
stmt = "XA END '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
self._execute_command(stmt)
|
||||||
|
try:
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
raise
|
||||||
|
|
||||||
|
stmt = "XA COMMIT '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
if self.tpc_state < TPC_STATE.PREPARE:
|
||||||
|
stmt = stmt + " ONE PHASE"
|
||||||
|
try:
|
||||||
|
self._execute_command(stmt)
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
raise
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
|
||||||
|
def tpc_prepare(self):
|
||||||
|
"""
|
||||||
|
Performs the first phase of a transaction started with .tpc_begin().
|
||||||
|
A ProgrammingError will be raised if this method was called outside
|
||||||
|
a TPC transaction.
|
||||||
|
|
||||||
|
After calling .tpc_prepare(), no statements can be executed until
|
||||||
|
.tpc_commit() or .tpc_rollback() have been called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self.tpc_state == TPC_STATE.NONE:
|
||||||
|
raise mariadb.ProgrammingError("Transaction not started.")
|
||||||
|
if self.tpc_state == TPC_STATE.PREPARE:
|
||||||
|
raise mariadb.ProgrammingError("Transaction is already in "
|
||||||
|
"prepared state.")
|
||||||
|
|
||||||
|
xid = self._xid
|
||||||
|
stmt = "XA END '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
try:
|
||||||
|
self._execute_command(stmt)
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
raise
|
||||||
|
|
||||||
|
stmt = "XA PREPARE '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
try:
|
||||||
|
self._execute_command(stmt)
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.tpc_state = TPC_STATE.PREPARE
|
||||||
|
|
||||||
|
def tpc_rollback(self, xid=None):
|
||||||
|
"""
|
||||||
|
Parameter:
|
||||||
|
xid: xid object which was created by .xid() method of connection
|
||||||
|
class
|
||||||
|
|
||||||
|
Performs the first phase of a transaction started with .tpc_begin().
|
||||||
|
A ProgrammingError will be raised if this method outside a TPC
|
||||||
|
transaction.
|
||||||
|
|
||||||
|
After calling .tpc_prepare(), no statements can be executed until
|
||||||
|
.tpc_commit() or .tpc_rollback() have been called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self.tpc_state == TPC_STATE.NONE:
|
||||||
|
raise mariadb.ProgrammingError("Transaction not started.")
|
||||||
|
if xid and type(xid).__name__ != "xid":
|
||||||
|
raise mariadb.ProgrammingError("argument 1 must be xid "
|
||||||
|
"not %s" % type(xid).__name__)
|
||||||
|
|
||||||
|
if not xid:
|
||||||
|
xid = self._xid
|
||||||
|
|
||||||
|
if self.tpc_state < TPC_STATE.PREPARE:
|
||||||
|
stmt = "XA END '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
self._execute_command(stmt)
|
||||||
|
try:
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
raise
|
||||||
|
|
||||||
|
stmt = "XA ROLLBACK '%s','%s',%s" % (xid[1], xid[2], xid[0])
|
||||||
|
try:
|
||||||
|
self._execute_command(stmt)
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
self._xid = None
|
||||||
|
self.tpc_state = TPC_STATE.NONE
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.tpc_state = TPC_STATE.PREPARE
|
||||||
|
|
||||||
|
def tpc_recover(self):
|
||||||
|
"""
|
||||||
|
Returns a list of pending transaction IDs suitable for use with
|
||||||
|
tpc_commit(xid) or .tpc_rollback(xid).
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
cursor = self.cursor()
|
||||||
|
cursor.execute("XA RECOVER")
|
||||||
|
result = cursor.fetchall()
|
||||||
|
del cursor
|
||||||
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tls_peer_cert_info(self):
|
||||||
|
"""Get peer certificate information."""
|
||||||
|
|
||||||
|
if version.Version(mariadb.mariadbapi_version) <\
|
||||||
|
version.Version('3.4.2'):
|
||||||
|
return None
|
||||||
|
self._check_closed()
|
||||||
|
if self._tls:
|
||||||
|
return self._mariadb_get_info(INFO.TLS_PEER_CERT_INFO)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database(self):
|
||||||
|
"""Get the current database of the connection."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.SCHEMA)
|
||||||
|
|
||||||
|
@database.setter
|
||||||
|
def database(self, schema):
|
||||||
|
"""Set default database."""
|
||||||
|
self._check_closed()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._execute_command("USE %s" % str(schema))
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
raise
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
"""
|
||||||
|
Returns the username for the current connection or empty
|
||||||
|
string if it can't be determined, e.g., when using socket
|
||||||
|
authentication.
|
||||||
|
"""
|
||||||
|
self._check_closed()
|
||||||
|
|
||||||
|
return self._mariadb_get_info(INFO.USER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def character_set(self):
|
||||||
|
"""
|
||||||
|
Client character set.
|
||||||
|
|
||||||
|
For MariaDB Connector/Python, it is always utf8mb4.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return _DEFAULT_CHARSET
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_capabilities(self):
|
||||||
|
"""Client capability flags."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.CLIENT_CAPABILITIES)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_capabilities(self):
|
||||||
|
"""Server capability flags."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.SERVER_CAPABILITIES)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extended_server_capabilities(self):
|
||||||
|
"""
|
||||||
|
Extended server capability flags (only for MariaDB
|
||||||
|
database servers).
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.EXTENDED_SERVER_CAPABILITIES)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_port(self):
|
||||||
|
"""
|
||||||
|
Database server TCP/IP port. This value will be 0 in case of an unix
|
||||||
|
socket connection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.PORT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unix_socket(self):
|
||||||
|
"""Unix socket name."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.UNIX_SOCKET)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_name(self):
|
||||||
|
"""Name or IP address of database server."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.HOST)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collation(self):
|
||||||
|
"""Client character set collation"""
|
||||||
|
|
||||||
|
return _DEFAULT_COLLATION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_info(self):
|
||||||
|
"""Server version in alphanumerical format (str)"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.SERVER_VERSION)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tls_cipher(self):
|
||||||
|
"""TLS cipher suite if a secure connection is used."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self._tls:
|
||||||
|
return self._mariadb_get_info(INFO.SSL_CIPHER)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tls_version(self):
|
||||||
|
"""TLS protocol version if a secure connection is used."""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self._tls:
|
||||||
|
return self._mariadb_get_info(INFO.TLS_VERSION)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _tls_verify_status(self):
|
||||||
|
"""Returns the result of the peer certificate verification."""
|
||||||
|
|
||||||
|
if version.Version(mariadb.mariadbapi_version) <\
|
||||||
|
version.Version('3.4.2'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
if self._tls:
|
||||||
|
return self._mariadb_get_info(INFO.TLS_VERIFY_STATUS)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_status(self):
|
||||||
|
"""
|
||||||
|
Return server status flags
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.SERVER_STATUS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_version(self):
|
||||||
|
"""
|
||||||
|
Server version in numerical format.
|
||||||
|
|
||||||
|
The form of the version number is
|
||||||
|
VERSION_MAJOR * 10000 + VERSION_MINOR * 100 + VERSION_PATCH
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self._mariadb_get_info(INFO.SERVER_VERSION_ID)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_version_info(self):
|
||||||
|
"""
|
||||||
|
Returns numeric version of connected database server in tuple format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
version = self.server_version
|
||||||
|
return (int(version / 10000),
|
||||||
|
int((version % 10000) / 100),
|
||||||
|
version % 100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def autocommit(self):
|
||||||
|
"""
|
||||||
|
Toggles autocommit mode on or off for the current database connection.
|
||||||
|
|
||||||
|
Autocommit mode only affects operations on transactional table types.
|
||||||
|
Be aware that rollback() will not work if autocommit mode was switched
|
||||||
|
on.
|
||||||
|
|
||||||
|
By default, autocommit mode is set to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return bool(self.server_status & STATUS.AUTOCOMMIT)
|
||||||
|
|
||||||
|
@autocommit.setter
|
||||||
|
def autocommit(self, mode):
|
||||||
|
self._check_closed()
|
||||||
|
if bool(mode) == self.autocommit:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._execute_command("SET AUTOCOMMIT=%s" % int(mode))
|
||||||
|
self._read_response()
|
||||||
|
except mariadb.Error:
|
||||||
|
raise
|
||||||
|
|
||||||
|
@property
|
||||||
|
def socket(self):
|
||||||
|
"""Returns the socket used for database connection"""
|
||||||
|
|
||||||
|
fno = self._get_socket()
|
||||||
|
if not self._socket:
|
||||||
|
self._socket = socket.socket(fileno=fno)
|
||||||
|
# in case of a possible reconnect, file descriptor has changed
|
||||||
|
elif fno != self._socket.fileno():
|
||||||
|
self._socket = socket.socket(fileno=fno)
|
||||||
|
return self._socket
|
||||||
|
|
||||||
|
@property
|
||||||
|
def open(self):
|
||||||
|
"""
|
||||||
|
Returns true if the connection is alive.
|
||||||
|
|
||||||
|
A ping command will be sent to the server for this purpose,
|
||||||
|
which means this function might fail if there are still
|
||||||
|
non-processed pending result sets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
try:
|
||||||
|
self.ping()
|
||||||
|
except mariadb.Error:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Aliases
|
||||||
|
character_set_name = character_set
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thread_id(self):
|
||||||
|
"""
|
||||||
|
Alias for connection_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_closed()
|
||||||
|
return self.connection_id
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'''
|
||||||
|
MariaDB capability flags.
|
||||||
|
|
||||||
|
These flags are used to check the capabilities both of a MariaDB server
|
||||||
|
or the client applicaion.
|
||||||
|
|
||||||
|
Capability flags are defined in module *mariadb.constants.CAPABILIY*
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
MYSQL = 1 # MariaDB
|
||||||
|
LONG_PASSWORD = 1 # MySQL
|
||||||
|
FOUND_ROWS = 2
|
||||||
|
LONG_FLAG = 4
|
||||||
|
CONNECT_WITH_DB = 8
|
||||||
|
NO_SCHEMA = 16
|
||||||
|
COMPRESS = 32
|
||||||
|
LOCAL_FILES = 128
|
||||||
|
IGNORE_SPACE = 256
|
||||||
|
INTERACTIVE = 1024
|
||||||
|
SSL = 2048
|
||||||
|
TRANSACTIONS = 8192
|
||||||
|
SECURE_CONNECTION = 32768
|
||||||
|
MULTI_STATEMENTS = 1 << 16
|
||||||
|
MULTI_RESULTS = 1 << 17
|
||||||
|
PS_MULTI_RESULTS = 1 << 18
|
||||||
|
PLUGIN_AUTH = 1 << 19
|
||||||
|
CONNECT_ATTRS = 1 << 20
|
||||||
|
CAN_HANDLE_EXPIRED_PASSWORDS = 1 < 22
|
||||||
|
SESSION_TRACKING = 1 << 23
|
||||||
|
SSL_VERIFY_SERVER_CERT = 1 << 30
|
||||||
|
REMEMBER_OPTIONS = 1 << 31
|
||||||
|
|
||||||
|
# MariaDB specific capabilities
|
||||||
|
PROGRESS = 1 << 32
|
||||||
|
BULK_OPERATIONS = 1 << 34
|
||||||
|
EXTENDED_METADATA = 1 << 35
|
||||||
|
CACHE_METDATA = 1 << 36
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'''
|
||||||
|
MariaDB capability flags.
|
||||||
|
|
||||||
|
These flags are used to check the capabilities both of a MariaDB server
|
||||||
|
or the client applicaion.
|
||||||
|
|
||||||
|
Capability flags are defined in module *mariadb.constants.CLIENT*
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
MYSQL = 1 # MariaDB
|
||||||
|
LONG_PASSWORD = 1 # MySQL
|
||||||
|
FOUND_ROWS = 2
|
||||||
|
LONG_FLAG = 4
|
||||||
|
CONNECT_WITH_DB = 8
|
||||||
|
NO_SCHEMA = 16
|
||||||
|
COMPRESS = 32
|
||||||
|
LOCAL_FILES = 128
|
||||||
|
IGNORE_SPACE = 256
|
||||||
|
INTERACTIVE = 1024
|
||||||
|
SSL = 2048
|
||||||
|
TRANSACTIONS = 8192
|
||||||
|
SECURE_CONNECTION = 32768
|
||||||
|
MULTI_STATEMENTS = 1 << 16
|
||||||
|
MULTI_RESULTS = 1 << 17
|
||||||
|
PS_MULTI_RESULTS = 1 << 18
|
||||||
|
PLUGIN_AUTH = 1 << 19
|
||||||
|
CONNECT_ATTRS = 1 << 20
|
||||||
|
CAN_HANDLE_EXPIRED_PASSWORDS = 1 < 22
|
||||||
|
SESSION_TRACKING = 1 << 23
|
||||||
|
SSL_VERIFY_SERVER_CERT = 1 << 30
|
||||||
|
REMEMBER_OPTIONS = 1 << 31
|
||||||
|
|
||||||
|
# MariaDB specific capabilities
|
||||||
|
PROGRESS = 1 << 32
|
||||||
|
BULK_OPERATIONS = 1 << 34
|
||||||
|
EXTENDED_METADATA = 1 << 35
|
||||||
|
CACHE_METDATA = 1 << 36
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Cursor constants are used for server side cursors.
|
||||||
|
Currently only read only cursor is supported.
|
||||||
|
|
||||||
|
Cursor constants are defined in module *mariadb.constants.CURSOR*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE = 0
|
||||||
|
READ_ONLY = 1
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
MariaDB EXT_FIELD_TYPE Constants
|
||||||
|
|
||||||
|
These constants represent the extended field types supported by MariaDB.
|
||||||
|
|
||||||
|
Extended field types are defined in module *mariadb.constants.EXT_FIELD_TYPE*
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE =0
|
||||||
|
JSON = 1
|
||||||
|
UUID = 2
|
||||||
|
INET4 = 3
|
||||||
|
INET6 = 4
|
||||||
|
POINT = 5
|
||||||
|
MULTIPOINT = 6
|
||||||
|
LINESTRING = 7
|
||||||
|
MULTILINESTRING = 8
|
||||||
|
POLYGON = 9
|
||||||
|
MULTIPOLYGON = 10
|
||||||
|
GEOMETRYCOLLECTION = 11
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""MariaDB FIELD_FLAG Constants
|
||||||
|
|
||||||
|
These constants represent the various field flags. As an addition
|
||||||
|
to the DBAPI 2.0 standard (PEP-249) these flags are returned as
|
||||||
|
eighth element of the cursor description attribute.
|
||||||
|
|
||||||
|
Field flags are defined in module *mariadb.constants.FIELD_FLAG*
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Source: mariadb_com.h (MariaDB Connector(C)
|
||||||
|
|
||||||
|
NOT_NULL = 1
|
||||||
|
PRIMARY_KEY = 2
|
||||||
|
UNIQUE_KEY = 4
|
||||||
|
MULTIPLE_KEY = 8
|
||||||
|
BLOB = 16
|
||||||
|
UNSIGNED = 32
|
||||||
|
ZEROFILL = 64
|
||||||
|
BINARY = 128
|
||||||
|
ENUM = 256
|
||||||
|
AUTO_INCREMENT = 512
|
||||||
|
TIMESTAMP = 1024
|
||||||
|
SET = 2048
|
||||||
|
NO_DEFAULT = 4096
|
||||||
|
ON_UPDATE_NOW = 8192
|
||||||
|
NUMERIC = 32768
|
||||||
|
PART_OF_KEY = 16384
|
||||||
|
GROUP = 32768
|
||||||
|
UNIQUE = 65536
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
MariaDB FIELD_TYPE Constants
|
||||||
|
|
||||||
|
These constants represent the field types supported by MariaDB.
|
||||||
|
The field type is returned as second element of cursor description attribute.
|
||||||
|
|
||||||
|
Field types are defined in module *mariadb.constants.FIELD_TYPE*
|
||||||
|
"""
|
||||||
|
|
||||||
|
DECIMAL = 0
|
||||||
|
TINY = 1
|
||||||
|
SHORT = 2
|
||||||
|
LONG = 3
|
||||||
|
FLOAT = 4
|
||||||
|
DOUBLE = 5
|
||||||
|
NULL = 6
|
||||||
|
TIMESTAMP = 7
|
||||||
|
LONGLONG = 8
|
||||||
|
INT24 = 9
|
||||||
|
DATE = 10
|
||||||
|
TIME = 11
|
||||||
|
DATETIME = 12
|
||||||
|
YEAR = 13
|
||||||
|
NEWDATE = 14
|
||||||
|
VARCHAR = 15
|
||||||
|
BIT = 16
|
||||||
|
TIMESTAMP2 = 17
|
||||||
|
DATETIME2 = 18
|
||||||
|
TIME2 = 19
|
||||||
|
JSON = 245
|
||||||
|
NEWDECIMAL = 246
|
||||||
|
ENUM = 247
|
||||||
|
SET = 248
|
||||||
|
TINY_BLOB = 249
|
||||||
|
MEDIUM_BLOB = 250
|
||||||
|
LONG_BLOB = 251
|
||||||
|
BLOB = 252
|
||||||
|
VAR_STRING = 253
|
||||||
|
STRING = 254
|
||||||
|
GEOMETRY = 255
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
'''
|
||||||
|
MariaDB indicator variables
|
||||||
|
|
||||||
|
Indicator values are used in executemany() method of cursor class to
|
||||||
|
indicate special values.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class MrdbIndicator():
|
||||||
|
indicator = 0
|
||||||
|
|
||||||
|
def __init__(self, indicator):
|
||||||
|
self.indicator = indicator
|
||||||
|
|
||||||
|
|
||||||
|
NULL = MrdbIndicator(1)
|
||||||
|
DEFAULT = MrdbIndicator(2)
|
||||||
|
IGNORE = MrdbIndicator(3)
|
||||||
|
IGNORE_ROW = MrdbIndicator(4)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Constants for _get_info method of MariadB connection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
CHARSET_ID = 0
|
||||||
|
CHARSET_NAME = 1
|
||||||
|
CLIENT_ERRORS = 2
|
||||||
|
CLIENT_VERSION = 3
|
||||||
|
CLIENT_VERSION_ID = 4
|
||||||
|
ASYNC_TIMEOUT = 5
|
||||||
|
ASYNC_TIMEOUT_MS = 6
|
||||||
|
CHARSET_INFO = 7
|
||||||
|
ERROR = 8
|
||||||
|
ERROR_ID = 9
|
||||||
|
HOST = 10
|
||||||
|
INFO = 11
|
||||||
|
PORT = 12
|
||||||
|
PROTOCOL_VERSION_ID = 13
|
||||||
|
PVIO_TYPE = 14
|
||||||
|
SCHEMA = 15
|
||||||
|
SERVER_TYPE = 16
|
||||||
|
SERVER_VERSION = 17
|
||||||
|
SERVER_VERSION_ID = 18
|
||||||
|
SOCKET = 19
|
||||||
|
SQLSTATE = 20
|
||||||
|
SSL_CIPHER = 21
|
||||||
|
TLS_LIBRARY = 22
|
||||||
|
TLS_VERSION = 23
|
||||||
|
TLS_VERSION_ID = 24
|
||||||
|
TYPE = 25
|
||||||
|
UNIX_SOCKET = 26
|
||||||
|
USER = 27
|
||||||
|
MAX_ALLOWED_PACKET = 28
|
||||||
|
NET_BUFFER_LENGTH = 29
|
||||||
|
SERVER_STATUS = 30
|
||||||
|
SERVER_CAPABILITIES = 31
|
||||||
|
EXTENDED_SERVER_CAPABILITIES = 32
|
||||||
|
CLIENT_CAPABILITIES = 33
|
||||||
|
BYTES_READ = 34
|
||||||
|
BYTES_SENT = 35
|
||||||
|
TLS_PEER_CERT_INFO = 36
|
||||||
|
TLS_VERIFY_STATUS = 37
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'''
|
||||||
|
MariaDB status flags
|
||||||
|
|
||||||
|
These flags describe the current status of the database server.
|
||||||
|
'''
|
||||||
|
|
||||||
|
IN_TRANS = 1
|
||||||
|
AUTOCOMMIT = 2
|
||||||
|
MORE_RESULTS_EXIST = 8
|
||||||
|
QUERY_NO_GOOD_INDEX_USED = 16
|
||||||
|
QUERY_NO_INDEX_USED = 32
|
||||||
|
CURSOR_EXISTS = 64
|
||||||
|
LAST_ROW_SENT = 128
|
||||||
|
DB_DROPPED = 256
|
||||||
|
NO_BACKSLASH_ESCAPES = 512
|
||||||
|
METADATA_CHANGED = 1024
|
||||||
|
QUERY_WAS_SLOW = 2048
|
||||||
|
PS_OUT_PARAMS = 4096
|
||||||
|
IN_TRANS_READONLY = 8192
|
||||||
|
SESSION_STATE_CHANGED = 16384
|
||||||
|
ANSI_QUOTES = 32768
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
NONE = 0
|
||||||
|
XID = 1
|
||||||
|
PREPARE = 2
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
__all__ = ["CLIENT", "CURSOR", "FIELD_TYPE", "FIELD_FLAG",
|
||||||
|
"INDICATOR", 'STATUS', 'ERR', 'CAPABILITY']
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2020-2021 Georg Richter and MariaDB Corporation AB
|
||||||
|
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Library General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Library General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Library General Public
|
||||||
|
# License along with this library; if not see <http://www.gnu.org/licenses>
|
||||||
|
# or write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin St., Fifth Floor, Boston, MA 02110, USA
|
||||||
|
#
|
||||||
|
|
||||||
|
import mariadb
|
||||||
|
import datetime
|
||||||
|
from numbers import Number
|
||||||
|
from mariadb.constants import CURSOR, STATUS, CAPABILITY, INDICATOR
|
||||||
|
from typing import Sequence
|
||||||
|
import decimal
|
||||||
|
|
||||||
|
PARAMSTYLE_QMARK = 1
|
||||||
|
PARAMSTYLE_FORMAT = 2
|
||||||
|
PARAMSTYLE_PYFORMAT = 3
|
||||||
|
|
||||||
|
ROWS_ALL = -1
|
||||||
|
|
||||||
|
RESULT_TUPLE = 0
|
||||||
|
RESULT_NAMEDTUPLE = 1
|
||||||
|
RESULT_DICTIONARY = 2
|
||||||
|
|
||||||
|
# Command types
|
||||||
|
SQL_NONE = 0,
|
||||||
|
SQL_INSERT = 1
|
||||||
|
SQL_UPDATE = 2
|
||||||
|
SQL_REPLACE = 3
|
||||||
|
SQL_DELETE = 4
|
||||||
|
SQL_CALL = 5
|
||||||
|
SQL_DO = 6
|
||||||
|
SQL_SELECT = 7
|
||||||
|
SQL_OTHER = 255
|
||||||
|
|
||||||
|
ROWS_EOF = -1
|
||||||
|
|
||||||
|
|
||||||
|
class Cursor(mariadb._mariadb.cursor):
|
||||||
|
"""
|
||||||
|
MariaDB Connector/Python Cursor Object
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check_closed(self):
|
||||||
|
if self._thread_id != self.connection.thread_id:
|
||||||
|
raise mariadb.ProgrammingError(f"Cursor cannot be used anymore (the connection aborted and reconnected).")
|
||||||
|
if self._closed:
|
||||||
|
raise mariadb.ProgrammingError("Cursor cannot be used anymore (it was already closed before).")
|
||||||
|
self._connection._check_closed()
|
||||||
|
|
||||||
|
def __init__(self, connection, **kwargs):
|
||||||
|
"""
|
||||||
|
initialization
|
||||||
|
"""
|
||||||
|
self._bulk = False
|
||||||
|
self._dictionary = False
|
||||||
|
self._named_tuple = False
|
||||||
|
self._connection = connection
|
||||||
|
self._resulttype = RESULT_TUPLE
|
||||||
|
self._description = None
|
||||||
|
self._transformed_statement = None
|
||||||
|
self._prepared = False
|
||||||
|
self._prev_stmt = None
|
||||||
|
self._force_binary = None
|
||||||
|
self._rowcount = 0
|
||||||
|
self.buffered = True
|
||||||
|
self._parseinfo = None
|
||||||
|
self._data = None
|
||||||
|
self._thread_id = connection.thread_id
|
||||||
|
self._closed= None
|
||||||
|
|
||||||
|
if not connection:
|
||||||
|
raise mariadb.ProgrammingError("Invalid or no connection provided")
|
||||||
|
|
||||||
|
# parse keywords
|
||||||
|
if kwargs:
|
||||||
|
rtype = kwargs.pop("named_tuple", False)
|
||||||
|
if rtype:
|
||||||
|
self._resulttype = RESULT_NAMEDTUPLE
|
||||||
|
else:
|
||||||
|
rtype = kwargs.pop("dictionary", False)
|
||||||
|
if rtype:
|
||||||
|
self._resulttype = RESULT_DICTIONARY
|
||||||
|
buffered = kwargs.pop("buffered", True)
|
||||||
|
self.buffered = buffered
|
||||||
|
self._prepared = kwargs.pop("prepared", False)
|
||||||
|
self._force_binary = kwargs.pop("binary", False)
|
||||||
|
self._cursor_type = kwargs.pop("cursor_type", 0)
|
||||||
|
|
||||||
|
# call initialization of main class
|
||||||
|
super().__init__(connection, **kwargs)
|
||||||
|
|
||||||
|
def _substitute_parameters(self):
|
||||||
|
"""
|
||||||
|
Internal use only.
|
||||||
|
|
||||||
|
When running in text protocol, this method will replace placeholders
|
||||||
|
by supplied values.
|
||||||
|
|
||||||
|
For values which aren't numbers, strings or bytes string representation
|
||||||
|
will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_stmt = self.statement.encode("utf8")
|
||||||
|
replace_diff = 0
|
||||||
|
if self._paramlist:
|
||||||
|
for i in range(0, len(self._paramlist)):
|
||||||
|
extra_bytes = 0
|
||||||
|
if self._paramstyle == PARAMSTYLE_PYFORMAT:
|
||||||
|
val = self._data[self._keys[i]]
|
||||||
|
else:
|
||||||
|
val = self._data[i]
|
||||||
|
if val is None:
|
||||||
|
replace = "NULL"
|
||||||
|
else:
|
||||||
|
if isinstance(val, INDICATOR.MrdbIndicator):
|
||||||
|
if val == INDICATOR.NULL:
|
||||||
|
replace = "NULL"
|
||||||
|
if val == INDICATOR.DEFAULT:
|
||||||
|
replace = "DEFAULT"
|
||||||
|
elif isinstance(val, Number):
|
||||||
|
replace = val.__str__()
|
||||||
|
else:
|
||||||
|
if isinstance(val, (bytes, bytearray)):
|
||||||
|
replace = "'%s'" % self.connection.escape_string(
|
||||||
|
val.decode(encoding='latin1'))
|
||||||
|
else:
|
||||||
|
replace = "'%s'" % self.connection.escape_string(
|
||||||
|
val.__str__())
|
||||||
|
extra_bytes = len(replace.encode("utf-8")) -\
|
||||||
|
len(replace)
|
||||||
|
ofs = self._paramlist[i] + replace_diff
|
||||||
|
|
||||||
|
new_stmt = new_stmt[:ofs] + replace.__str__().encode("utf8") +\
|
||||||
|
new_stmt[ofs+1:]
|
||||||
|
replace_diff += len(replace) - 1 + extra_bytes
|
||||||
|
return new_stmt
|
||||||
|
|
||||||
|
def _check_decimal_parameter(self, val):
|
||||||
|
"""
|
||||||
|
Internal use only
|
||||||
|
|
||||||
|
Checks for unsupported parameters.
|
||||||
|
The following parameters are not supported by MariaDB/MySQL (but also
|
||||||
|
not part of the SQL Standard)
|
||||||
|
|
||||||
|
- float("nan" | "inf" | "-inf")
|
||||||
|
- Decimal("NaN" | "sNaN" | "Infinity" | "-Infinity")
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(val, float) and repr(val) in ("nan", "inf", "-inf"):
|
||||||
|
raise mariadb.NotSupportedError(f"Float value '{repr(val)}' is not supported.")
|
||||||
|
elif isinstance(val, decimal.Decimal) and val.__str__() in ("NaN", "sNaN", "Infinity", "-Infinity"):
|
||||||
|
raise mariadb.NotSupportedError(f"Decimal value '{val.__str__()}' is not supported.")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _check_execute_params(self):
|
||||||
|
# check data format
|
||||||
|
values = ()
|
||||||
|
if self._paramstyle in (PARAMSTYLE_QMARK, PARAMSTYLE_FORMAT):
|
||||||
|
if not isinstance(self._data, (tuple, list)):
|
||||||
|
raise mariadb.ProgrammingError("Data argument must be "
|
||||||
|
"Tuple or List")
|
||||||
|
values = self._data
|
||||||
|
|
||||||
|
if self._paramstyle == PARAMSTYLE_PYFORMAT:
|
||||||
|
if not isinstance(self._data, dict):
|
||||||
|
raise mariadb.ProgrammingError("Data argument must be "
|
||||||
|
"Dictionary")
|
||||||
|
for i in range(0, len(self._keys)):
|
||||||
|
if self._keys[i] not in self._data:
|
||||||
|
raise mariadb.ProgrammingError("Dictionary doesn't contain"
|
||||||
|
" key '%s'" % self._keys[i])
|
||||||
|
values = self._data.values()
|
||||||
|
else:
|
||||||
|
# check if number of place holders matches the number of
|
||||||
|
# supplied elements in data tuple
|
||||||
|
if self._paramlist and (
|
||||||
|
(not self._data and len(self._paramlist) > 0) or
|
||||||
|
(len(self._data) != len(self._paramlist))):
|
||||||
|
raise mariadb.ProgrammingError(
|
||||||
|
"statement (%s) doesn't match the number of data elements"
|
||||||
|
" (%s)." % (len(self._paramlist), len(self._data)))
|
||||||
|
|
||||||
|
# CONPY-313: Check if parameter types are supported
|
||||||
|
if len(values):
|
||||||
|
for val in values:
|
||||||
|
if isinstance(val, float) or isinstance(val, decimal.Decimal):
|
||||||
|
self._check_decimal_parameter(val)
|
||||||
|
|
||||||
|
def callproc(self, sp: str, data: Sequence = ()):
|
||||||
|
"""
|
||||||
|
Executes a stored procedure sp. The data sequence must contain an
|
||||||
|
entry for each parameter the procedure expects.
|
||||||
|
|
||||||
|
Input/Output or Output parameters have to be retrieved by .fetch
|
||||||
|
methods, the .sp_outparams attribute indicates if the result set
|
||||||
|
contains output parameters.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- sp: Name of stored procedure.
|
||||||
|
- data: Optional sequence containing data for placeholder
|
||||||
|
substitution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.connection.auto_reconnect:
|
||||||
|
self._thread_id= self.connection.thread_id
|
||||||
|
self.check_closed()
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
# create statement
|
||||||
|
params = ""
|
||||||
|
if data and len(data):
|
||||||
|
params = ("?," * len(data))[:-1]
|
||||||
|
statement = "CALL %s(%s)" % (sp, params)
|
||||||
|
self._rowcount = 0
|
||||||
|
self.execute(statement, data)
|
||||||
|
|
||||||
|
def _parse_execute(self, statement: str, data=(), is_bulk=False):
|
||||||
|
"""
|
||||||
|
For internal use
|
||||||
|
|
||||||
|
Parses SQL statement and checks parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not statement:
|
||||||
|
raise mariadb.ProgrammingError("empty statement")
|
||||||
|
|
||||||
|
# parse statement
|
||||||
|
if self.statement != statement or is_bulk and not self._bulk:
|
||||||
|
super()._parse(statement)
|
||||||
|
self._prev_stmt = statement
|
||||||
|
self._reprepare = True
|
||||||
|
else:
|
||||||
|
self._reprepare = False
|
||||||
|
|
||||||
|
self._transformed_statement = self.statement
|
||||||
|
|
||||||
|
if self._cursor_type == CURSOR.READ_ONLY:
|
||||||
|
self._text = False
|
||||||
|
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
self._check_execute_params()
|
||||||
|
|
||||||
|
def nextset(self):
|
||||||
|
"""
|
||||||
|
Will make the cursor skip to the next available result set,
|
||||||
|
discarding any remaining rows from the current set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.check_closed()
|
||||||
|
return super()._nextset()
|
||||||
|
|
||||||
|
def execute(self, statement: str, data: Sequence = (), buffered=None):
|
||||||
|
"""
|
||||||
|
Prepare and execute a SQL statement.
|
||||||
|
|
||||||
|
Parameters may be provided as sequence or mapping and will be bound
|
||||||
|
to variables in the operation. Variables are specified as question
|
||||||
|
marks (paramstyle ='qmark'), however for compatibility reasons MariaDB
|
||||||
|
Connector/Python also supports the 'format' and 'pyformat' paramstyles
|
||||||
|
with the restriction, that different paramstyles can't be mixed within
|
||||||
|
a statement.
|
||||||
|
|
||||||
|
A reference to the operation will be retained by the cursor.
|
||||||
|
If the cursor was created with attribute prepared =True the statement
|
||||||
|
string for following execute operations will be ignored.
|
||||||
|
This is most effective for algorithms where the same operation is used,
|
||||||
|
but different parameters are bound to it (many times).
|
||||||
|
|
||||||
|
By default execute() method generates an buffered result unless the
|
||||||
|
optional parameter buffered was set to False or the cursor was
|
||||||
|
generated as an unbuffered cursor.
|
||||||
|
"""
|
||||||
|
if self.connection.auto_reconnect:
|
||||||
|
self._thread_id= self.connection.thread_id
|
||||||
|
|
||||||
|
self.check_closed()
|
||||||
|
if not self._prepared:
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
self.connection._last_executed_statement = statement
|
||||||
|
|
||||||
|
# Parse statement
|
||||||
|
do_parse = True
|
||||||
|
self._rowcount = 0
|
||||||
|
|
||||||
|
if buffered is not None:
|
||||||
|
self.buffered = buffered
|
||||||
|
|
||||||
|
# clear pending result sets
|
||||||
|
if self.field_count:
|
||||||
|
self._clear_result()
|
||||||
|
|
||||||
|
# if we have a prepared cursor, we have to set statement
|
||||||
|
# to previous statement and don't need to parse
|
||||||
|
if self._prepared and self.statement:
|
||||||
|
statement = self.statement
|
||||||
|
do_parse = False
|
||||||
|
|
||||||
|
# parse statement and check param style
|
||||||
|
if do_parse:
|
||||||
|
self._parse_execute(statement, (data))
|
||||||
|
|
||||||
|
self._description = None
|
||||||
|
|
||||||
|
# CONPY-218: Allow None as replacement for empty tuple
|
||||||
|
data = data or ()
|
||||||
|
|
||||||
|
if len(data):
|
||||||
|
self._data = data
|
||||||
|
else:
|
||||||
|
self._data = None
|
||||||
|
# If statement doesn't contain parameters we force to run in text
|
||||||
|
# mode, unless a server side cursor or stored procedure will be
|
||||||
|
# executed.
|
||||||
|
if self._command != SQL_CALL and self._cursor_type == 0:
|
||||||
|
self._text = True
|
||||||
|
|
||||||
|
if self._force_binary:
|
||||||
|
self._text = False
|
||||||
|
|
||||||
|
# if one of the provided parameters has byte or datetime value,
|
||||||
|
# we don't use text protocol
|
||||||
|
if data and self._check_text_types() == True:
|
||||||
|
self._text = False
|
||||||
|
|
||||||
|
if self._text:
|
||||||
|
# in text mode we need to substitute parameters
|
||||||
|
# and store transformed statement
|
||||||
|
if (self.paramcount > 0):
|
||||||
|
self._transformed_statement = self._substitute_parameters()
|
||||||
|
else:
|
||||||
|
self._transformed_statement = self.statement
|
||||||
|
self._execute_text(self._transformed_statement)
|
||||||
|
self._readresponse()
|
||||||
|
else:
|
||||||
|
self._data = data
|
||||||
|
self._execute_binary()
|
||||||
|
|
||||||
|
self._initresult()
|
||||||
|
self._bulk = 0
|
||||||
|
|
||||||
|
def executemany(self, statement, parameters):
|
||||||
|
"""
|
||||||
|
Prepare a database operation (INSERT,UPDATE,REPLACE or DELETE
|
||||||
|
statement) and execute it against all parameter found in sequence.
|
||||||
|
|
||||||
|
Exactly behaves like .execute() but accepts a list of tuples, where
|
||||||
|
each tuple represents data of a row within a table.
|
||||||
|
.executemany() only supports DML (insert, update, delete) statements.
|
||||||
|
|
||||||
|
If the SQL statement contains a RETURNING clause, executemany()
|
||||||
|
returns a result set containing the values for columns listed in the
|
||||||
|
RETURNING clause.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.connection.auto_reconnect:
|
||||||
|
self._thread_id= self.connection.thread_id
|
||||||
|
|
||||||
|
self.check_closed()
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
if not parameters or not len(parameters):
|
||||||
|
raise mariadb.ProgrammingError("No data provided")
|
||||||
|
|
||||||
|
self.connection._last_executed_statement = statement
|
||||||
|
|
||||||
|
# clear pending results
|
||||||
|
if self.field_count:
|
||||||
|
self._clear_result()
|
||||||
|
|
||||||
|
# If the server doesn't support bulk operations, we need to emulate
|
||||||
|
# by looping
|
||||||
|
# TODO: insert/replace statements are not optimized yet
|
||||||
|
# rowcount updating
|
||||||
|
if not (self.connection.extended_server_capabilities &
|
||||||
|
(CAPABILITY.BULK_OPERATIONS >> 32)):
|
||||||
|
count = 0
|
||||||
|
for row in parameters:
|
||||||
|
self.execute(statement, row)
|
||||||
|
count += self.rowcount
|
||||||
|
self._rowcount = count
|
||||||
|
else:
|
||||||
|
# parse statement
|
||||||
|
self._parse_execute(statement, parameters[0], is_bulk=True)
|
||||||
|
self._data = parameters
|
||||||
|
self.is_text = False
|
||||||
|
self._rowcount = 0
|
||||||
|
self._execute_bulk()
|
||||||
|
self._bulk = 1
|
||||||
|
|
||||||
|
def _fetch_row(self):
|
||||||
|
"""
|
||||||
|
Internal use only
|
||||||
|
|
||||||
|
fetches row and converts values, if connection has a converter.
|
||||||
|
"""
|
||||||
|
if not self.buffered:
|
||||||
|
self.check_closed()
|
||||||
|
|
||||||
|
# if there is no result set, PEP-249 requires to raise an
|
||||||
|
# exception
|
||||||
|
if not self.field_count:
|
||||||
|
raise mariadb.ProgrammingError("Cursor doesn't have a result set")
|
||||||
|
return super().fetchone()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Closes the cursor.
|
||||||
|
|
||||||
|
If the cursor has pending or unread results, .close() will cancel them
|
||||||
|
so that further operations using the same connection can be executed.
|
||||||
|
|
||||||
|
The cursor will be unusable from this point forward; an Error
|
||||||
|
(or subclass) exception will be raised if any operation is attempted
|
||||||
|
with the cursor."
|
||||||
|
"""
|
||||||
|
|
||||||
|
# CONPY-231: fix memory leak
|
||||||
|
if self._data:
|
||||||
|
del self._data
|
||||||
|
|
||||||
|
if not self.connection._closed:
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
self._closed= True
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
"""
|
||||||
|
Fetch the next row of a query result set, returning a single sequence,
|
||||||
|
or None if no more data is available.
|
||||||
|
|
||||||
|
An exception will be raised if the previous call to execute() didn't
|
||||||
|
produce a result set or execute() wasn't called before.
|
||||||
|
"""
|
||||||
|
if not (self.buffered and self._text):
|
||||||
|
self.check_closed()
|
||||||
|
|
||||||
|
row = self._fetch_row()
|
||||||
|
return row
|
||||||
|
|
||||||
|
def fetchmany(self, size: int = 0):
|
||||||
|
"""
|
||||||
|
Fetch the next set of rows of a query result, returning a sequence
|
||||||
|
of sequences (e.g. a list of tuples). An empty sequence is returned
|
||||||
|
when no more rows are available.
|
||||||
|
|
||||||
|
The number of rows to fetch per call is specified by the parameter.
|
||||||
|
If it is not given, the cursor's arraysize determines the number
|
||||||
|
of rows to be fetched. The method should try to fetch as many rows
|
||||||
|
as indicated by the size parameter.
|
||||||
|
If this is not possible due to the specified number of rows not being
|
||||||
|
available, fewer rows may be returned.
|
||||||
|
|
||||||
|
An exception will be raised if the previous call to execute() didn't
|
||||||
|
produce a result set or execute() wasn't called before.
|
||||||
|
"""
|
||||||
|
if not (self.buffered and self._text):
|
||||||
|
self.check_closed()
|
||||||
|
|
||||||
|
if size == 0:
|
||||||
|
size = self.arraysize
|
||||||
|
|
||||||
|
return super().fetchrows(size)
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
"""
|
||||||
|
Fetch all remaining rows of a query result, returning them as a
|
||||||
|
sequence of sequences (e.g. a list of tuples).
|
||||||
|
|
||||||
|
An exception will be raised if the previous call to execute() didn't
|
||||||
|
produce a result set or execute() wasn't called before.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (self.buffered and self._text):
|
||||||
|
self.check_closed()
|
||||||
|
return super().fetchrows(ROWS_EOF)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.fetchone, None)
|
||||||
|
|
||||||
|
def scroll(self, value: int, mode="relative"):
|
||||||
|
"""
|
||||||
|
Scroll the cursor in the result set to a new position according to
|
||||||
|
mode.
|
||||||
|
|
||||||
|
If mode is "relative" (default), value is taken as offset to the
|
||||||
|
current position in the result set, if set to absolute, value states
|
||||||
|
an absolute target position.
|
||||||
|
"""
|
||||||
|
if not (self.buffered and self._text):
|
||||||
|
self.check_closed()
|
||||||
|
|
||||||
|
if self.field_count == 0:
|
||||||
|
raise mariadb.ProgrammingError("Cursor doesn't have a result set")
|
||||||
|
|
||||||
|
if not self.buffered:
|
||||||
|
raise mariadb.ProgrammingError("This method is available only "
|
||||||
|
"for cursors with a buffered "
|
||||||
|
"result set.")
|
||||||
|
|
||||||
|
if mode != "absolute" and mode != "relative":
|
||||||
|
raise mariadb.ProgrammingError("Invalid or unknown scroll "
|
||||||
|
"mode specified.")
|
||||||
|
|
||||||
|
if value == 0 and mode != "absolute":
|
||||||
|
raise mariadb.ProgrammingError("Invalid position value 0.")
|
||||||
|
|
||||||
|
if mode == "relative":
|
||||||
|
if self.rownumber + value < 0 or \
|
||||||
|
self.rownumber + value > self.rowcount:
|
||||||
|
raise mariadb.ProgrammingError("Position value "
|
||||||
|
"is out of range.")
|
||||||
|
new_pos = self.rownumber + value
|
||||||
|
else:
|
||||||
|
if value < 0 or value >= self.rowcount:
|
||||||
|
raise mariadb.ProgrammingError("Position value "
|
||||||
|
"is out of range.")
|
||||||
|
new_pos = value
|
||||||
|
|
||||||
|
self._seek(new_pos)
|
||||||
|
self._rownumber = new_pos
|
||||||
|
|
||||||
|
def setinputsizes(self, size: int):
|
||||||
|
"""
|
||||||
|
Required by PEP-249. Does nothing in MariaDB Connector/Python
|
||||||
|
"""
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def setoutputsize(self, size: int):
|
||||||
|
"""
|
||||||
|
Required by PEP-249. Does nothing in MariaDB Connector/Python
|
||||||
|
"""
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Returns a copy of the cursor."""
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Closes cursor."""
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rowcount(self):
|
||||||
|
"""
|
||||||
|
This read-only attribute specifies the number of rows that the last\
|
||||||
|
execute*() produced (for DQL statements like SELECT) or affected
|
||||||
|
(for DML statements like UPDATE or INSERT).
|
||||||
|
The return value is -1 in case no .execute*() has been performed
|
||||||
|
on the cursor or the rowcount of the last operation cannot be
|
||||||
|
determined by the interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Even if PEP-249 permits operations on a closed cursor, we don't
|
||||||
|
# raise an exception if the cursor or the underlying connection
|
||||||
|
# was closed (See CONPY-269), instead we will return -1
|
||||||
|
if not self.buffered:
|
||||||
|
try:
|
||||||
|
self.check_closed()
|
||||||
|
except mariadb.ProgrammingError:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
if self._rowcount > 0:
|
||||||
|
return self._rowcount
|
||||||
|
return super().rowcount
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sp_outparams(self):
|
||||||
|
"""
|
||||||
|
Indicates if the current result set contains in out or out parameter
|
||||||
|
from a previous executed stored procedure
|
||||||
|
"""
|
||||||
|
self.check_closed()
|
||||||
|
|
||||||
|
return bool(self.connection.server_status & STATUS.PS_OUT_PARAMS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lastrowid(self):
|
||||||
|
"""
|
||||||
|
Returns the ID generated by a query on a table with a column having
|
||||||
|
the AUTO_INCREMENT attribute or the value for the last usage of
|
||||||
|
LAST_INSERT_ID().
|
||||||
|
|
||||||
|
If the last query wasn't an INSERT or UPDATE
|
||||||
|
statement or if the modified table does not have a column with the
|
||||||
|
AUTO_INCREMENT attribute and LAST_INSERT_ID was not used, the returned
|
||||||
|
value will be None
|
||||||
|
"""
|
||||||
|
self.check_closed()
|
||||||
|
|
||||||
|
id = self.insert_id
|
||||||
|
if id > 0:
|
||||||
|
return id
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection(self):
|
||||||
|
"""
|
||||||
|
Read-Only attribute which returns the reference to the connection
|
||||||
|
object on which the cursor was created.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._connection
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
from mariadb.constants import FIELD_TYPE
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
apilevel = '2.0'
|
||||||
|
|
||||||
|
paramstyle = 'qmark'
|
||||||
|
|
||||||
|
threadsafety = True
|
||||||
|
|
||||||
|
|
||||||
|
class DbApiType(frozenset):
|
||||||
|
"""
|
||||||
|
Immutable set for type checking
|
||||||
|
|
||||||
|
By default the following sets are defined:
|
||||||
|
|
||||||
|
- BINARY: for binary field types
|
||||||
|
- NUMBER: for numeric field types
|
||||||
|
- STRING: for character based (string) field types
|
||||||
|
- DATE: for date field type(s)
|
||||||
|
- DATETIME: for datetime and timestamp field type(s)
|
||||||
|
- TIME: for time field type(s)
|
||||||
|
- TIMESTAMP: for datetime and timestamp field type(s)
|
||||||
|
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> FIELD_TYPE.GEOMETRY == mariadb.BINARY
|
||||||
|
True
|
||||||
|
>>> FIELD_TYPE.FLOAT == mariadb.BINARY
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __eq__(self, field_type):
|
||||||
|
if (isinstance(field_type, DbApiType)):
|
||||||
|
return not self.difference(field_type)
|
||||||
|
return field_type in self
|
||||||
|
|
||||||
|
|
||||||
|
BINARY = DbApiType([FIELD_TYPE.GEOMETRY,
|
||||||
|
FIELD_TYPE.LONG_BLOB,
|
||||||
|
FIELD_TYPE.MEDIUM_BLOB,
|
||||||
|
FIELD_TYPE.TINY_BLOB,
|
||||||
|
FIELD_TYPE.BLOB])
|
||||||
|
|
||||||
|
STRING = DbApiType([FIELD_TYPE.ENUM,
|
||||||
|
FIELD_TYPE.JSON,
|
||||||
|
FIELD_TYPE.STRING,
|
||||||
|
FIELD_TYPE.VARCHAR,
|
||||||
|
FIELD_TYPE.VAR_STRING])
|
||||||
|
|
||||||
|
NUMBER = DbApiType([FIELD_TYPE.DECIMAL,
|
||||||
|
FIELD_TYPE.DOUBLE,
|
||||||
|
FIELD_TYPE.FLOAT,
|
||||||
|
FIELD_TYPE.INT24,
|
||||||
|
FIELD_TYPE.LONG,
|
||||||
|
FIELD_TYPE.LONGLONG,
|
||||||
|
FIELD_TYPE.NEWDECIMAL,
|
||||||
|
FIELD_TYPE.SHORT,
|
||||||
|
FIELD_TYPE.TINY,
|
||||||
|
FIELD_TYPE.YEAR])
|
||||||
|
|
||||||
|
DATE = DbApiType([FIELD_TYPE.DATE])
|
||||||
|
TIME = DbApiType([FIELD_TYPE.TIME])
|
||||||
|
DATETIME = TIMESTAMP = DbApiType([FIELD_TYPE.DATETIME,
|
||||||
|
FIELD_TYPE.TIMESTAMP])
|
||||||
|
ROWID = DbApiType()
|
||||||
|
|
||||||
|
|
||||||
|
def Binary(object):
|
||||||
|
"""Constructs an object capable of holding a binary value."""
|
||||||
|
return bytes(object)
|
||||||
|
|
||||||
|
|
||||||
|
def Date(year, month, day):
|
||||||
|
"""Constructs an object holding a date value."""
|
||||||
|
return datetime.date(year, month, day)
|
||||||
|
|
||||||
|
|
||||||
|
def Time(hour, minute, second):
|
||||||
|
"""Constructs an object holding a time value."""
|
||||||
|
return datetime.time(hour, minute, second)
|
||||||
|
|
||||||
|
|
||||||
|
def Timestamp(year, month, day, hour, minute, second):
|
||||||
|
"""Constructs an object holding a datetime value."""
|
||||||
|
return datetime.datetime(year, month, day, hour, minute, second)
|
||||||
|
|
||||||
|
|
||||||
|
def DateFromTicks(ticks):
|
||||||
|
"""Constructs an object holding a date value from the given ticks value
|
||||||
|
(number of seconds since the epoch).
|
||||||
|
For more information see the documentation of the standard Python
|
||||||
|
time module."""
|
||||||
|
return Date(*time.localtime(ticks)[:3])
|
||||||
|
|
||||||
|
|
||||||
|
def TimeFromTicks(ticks):
|
||||||
|
"""Constructs an object holding a time value from the given ticks value
|
||||||
|
(number of seconds since the epoch).
|
||||||
|
For more information see the documentation of the standard Python
|
||||||
|
time module."""
|
||||||
|
return Time(*time.localtime(ticks)[3:6])
|
||||||
|
|
||||||
|
|
||||||
|
def TimestampFromTicks(ticks):
|
||||||
|
"""Constructs an object holding a datetime value from the given ticks value
|
||||||
|
(number of seconds since the epoch).
|
||||||
|
For more information see the documentation of the standard Python
|
||||||
|
time module."""
|
||||||
|
return datetime.datetime(*time.localtime(ticks)[:6])
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from mariadb.constants import FIELD_TYPE, FIELD_FLAG
|
||||||
|
|
||||||
|
field_types = {FIELD_TYPE.DECIMAL: "DECIMAL",
|
||||||
|
FIELD_TYPE.TINY: "TINY",
|
||||||
|
FIELD_TYPE.SHORT: "SHORT",
|
||||||
|
FIELD_TYPE.LONG: "LONG",
|
||||||
|
FIELD_TYPE.FLOAT: "FLOAT",
|
||||||
|
FIELD_TYPE.DOUBLE: "DOUBLE",
|
||||||
|
FIELD_TYPE.NULL: "NULL",
|
||||||
|
FIELD_TYPE.TIMESTAMP: "TIMESTAMP",
|
||||||
|
FIELD_TYPE.LONGLONG: "LONGLONG",
|
||||||
|
FIELD_TYPE.INT24: "INT24",
|
||||||
|
FIELD_TYPE.DATE: "DATE",
|
||||||
|
FIELD_TYPE.TIME: "TIME",
|
||||||
|
FIELD_TYPE.DATETIME: "DATETIME",
|
||||||
|
FIELD_TYPE.YEAR: "YEAR",
|
||||||
|
FIELD_TYPE.NEWDATE: "NEWDATE",
|
||||||
|
FIELD_TYPE.VARCHAR: "VARCHAR",
|
||||||
|
FIELD_TYPE.BIT: "BIT",
|
||||||
|
FIELD_TYPE.JSON: "JSON",
|
||||||
|
FIELD_TYPE.NEWDECIMAL: "NEWDECIMAL",
|
||||||
|
FIELD_TYPE.ENUM: "ENUM",
|
||||||
|
FIELD_TYPE.SET: "SET",
|
||||||
|
FIELD_TYPE.TINY_BLOB: "TINY_BLOB",
|
||||||
|
FIELD_TYPE.MEDIUM_BLOB: "MEDIUM_BLOB",
|
||||||
|
FIELD_TYPE.LONG_BLOB: "LONG_BLOB",
|
||||||
|
FIELD_TYPE.BLOB: "BLOB",
|
||||||
|
FIELD_TYPE.VAR_STRING: "VAR_STRING",
|
||||||
|
FIELD_TYPE.STRING: "STRING",
|
||||||
|
FIELD_TYPE.GEOMETRY: "GEOMETRY"}
|
||||||
|
|
||||||
|
field_flags = {FIELD_FLAG.NOT_NULL: "NOT_NULL",
|
||||||
|
FIELD_FLAG.PRIMARY_KEY: "PRIMARY_KEY",
|
||||||
|
FIELD_FLAG.UNIQUE_KEY: "UNIQUE_KEY",
|
||||||
|
FIELD_FLAG.MULTIPLE_KEY: "PART_KEY",
|
||||||
|
FIELD_FLAG.BLOB: "BLOB",
|
||||||
|
FIELD_FLAG.UNSIGNED: "UNSIGNED",
|
||||||
|
FIELD_FLAG.ZEROFILL: "ZEROFILL",
|
||||||
|
FIELD_FLAG.BINARY: "BINARY",
|
||||||
|
FIELD_FLAG.ENUM: "NUMERIC",
|
||||||
|
FIELD_FLAG.AUTO_INCREMENT: "AUTO_INCREMENT",
|
||||||
|
FIELD_FLAG.TIMESTAMP: "TIMESTAMP",
|
||||||
|
FIELD_FLAG.SET: "SET",
|
||||||
|
FIELD_FLAG.NO_DEFAULT: "NO_DEFAULT",
|
||||||
|
FIELD_FLAG.ON_UPDATE_NOW: "UPDATE_TIMESTAMP",
|
||||||
|
FIELD_FLAG.NUMERIC: "NUMERIC"}
|
||||||
|
|
||||||
|
|
||||||
|
class fieldinfo():
|
||||||
|
|
||||||
|
def type(self, description):
|
||||||
|
if description[1] in field_types:
|
||||||
|
return field_types[description[1]]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def flag(self, description):
|
||||||
|
flags = [field_flags[f] for f in field_flags.keys()
|
||||||
|
if description[7] & f]
|
||||||
|
return " | ".join(flags)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__author__ = 'Georg Richter'
|
||||||
|
__version__ = '1.1.14'
|
||||||
|
__version_info__ = (1, 1, 14)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: packaging
|
||||||
|
Version: 26.3
|
||||||
|
Summary: Core utilities for Python packages
|
||||||
|
Author-email: Donald Stufft <donald@stufft.io>
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-Expression: Apache-2.0 OR BSD-2-Clause
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Classifier: Programming Language :: Python :: 3.15
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
|
||||||
|
Classifier: Typing :: Typed
|
||||||
|
License-File: LICENSE
|
||||||
|
License-File: LICENSE.APACHE
|
||||||
|
License-File: LICENSE.BSD
|
||||||
|
Project-URL: Documentation, https://packaging.pypa.io/
|
||||||
|
Project-URL: Source, https://github.com/pypa/packaging
|
||||||
|
|
||||||
|
packaging
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. start-intro
|
||||||
|
|
||||||
|
Reusable core utilities for various Python Packaging
|
||||||
|
`interoperability specifications <https://packaging.python.org/specifications/>`_.
|
||||||
|
|
||||||
|
This library provides utilities that implement the interoperability
|
||||||
|
specifications which have clearly one correct behaviour (eg: :pep:`440`)
|
||||||
|
or benefit greatly from having a single shared implementation (eg: :pep:`425`).
|
||||||
|
|
||||||
|
.. end-intro
|
||||||
|
|
||||||
|
The ``packaging`` project includes the following: version handling, specifiers,
|
||||||
|
markers, requirements, tags, metadata, lockfiles, utilities.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The `documentation`_ provides information and the API for the following:
|
||||||
|
|
||||||
|
- Version Handling
|
||||||
|
- Specifiers
|
||||||
|
- Markers
|
||||||
|
- Licenses
|
||||||
|
- Requirements
|
||||||
|
- Metadata
|
||||||
|
- Tags
|
||||||
|
- Lockfiles (pylock)
|
||||||
|
- Direct URL helpers
|
||||||
|
- Dependency groups
|
||||||
|
- Errors
|
||||||
|
- Utilities
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Use ``pip`` to install these utilities::
|
||||||
|
|
||||||
|
pip install packaging
|
||||||
|
|
||||||
|
The ``packaging`` library uses calendar-based versioning (``YY.N``).
|
||||||
|
|
||||||
|
Discussion
|
||||||
|
----------
|
||||||
|
|
||||||
|
If you run into bugs, you can file them in our `issue tracker`_.
|
||||||
|
|
||||||
|
You can also join discussions on `GitHub Discussions`_ to ask questions or get involved.
|
||||||
|
|
||||||
|
.. _`documentation`: https://packaging.pypa.io/
|
||||||
|
.. _`issue tracker`: https://github.com/pypa/packaging/issues
|
||||||
|
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
|
||||||
|
|
||||||
|
|
||||||
|
Code of Conduct
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Everyone interacting in the packaging project's codebases, issue trackers, chat
|
||||||
|
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
|
||||||
|
|
||||||
|
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
|
||||||
|
well as how to report a potential security issue. The documentation for this
|
||||||
|
project also covers information about `project development`_ and `security`_.
|
||||||
|
|
||||||
|
.. _`project development`: https://packaging.pypa.io/en/latest/development/
|
||||||
|
.. _`security`: https://packaging.pypa.io/en/latest/security/
|
||||||
|
|
||||||
|
Project History
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
|
||||||
|
recent changes and project history.
|
||||||
|
|
||||||
|
.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
packaging-26.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
packaging-26.3.dist-info/METADATA,sha256=cP24n8TUqaBDv3NyuJcrzIg_3f80q1Xpz4DXOHU4R2M,3544
|
||||||
|
packaging-26.3.dist-info/RECORD,,
|
||||||
|
packaging-26.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||||
|
packaging-26.3.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
||||||
|
packaging-26.3.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
||||||
|
packaging-26.3.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
||||||
|
packaging/__init__.py,sha256=bnWM3QAossrXTHObmtycnwbwVJMjBigLh4v-WdOSGbU,494
|
||||||
|
packaging/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_elffile.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_manylinux.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_musllinux.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_parser.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_ranges.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_structures.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/_tokenizer.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/dependency_groups.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/direct_url.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/errors.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/markers.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/metadata.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/pylock.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/ranges.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/requirements.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/specifiers.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/tags.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
packaging/__pycache__/version.cpython-312.pyc,,
|
||||||
|
packaging/_elffile.py,sha256=HwJfVMVz8ahXQMT72QjMHR-wwXfBQ3fr9fCdbNU2h4A,3236
|
||||||
|
packaging/_manylinux.py,sha256=W26EMkxCU4q0zbFI4kMP26NvBEJ7Dv2lW-oIOjX6YTY,10000
|
||||||
|
packaging/_musllinux.py,sha256=Ayj7gnsRcMd99MEH1Jvo3403JiEDSQph6Kytii-crf8,2774
|
||||||
|
packaging/_parser.py,sha256=iKNIEVIJMnSX5FO2NeXbCZ_VkvkzXXEMcxOynNswxQw,12639
|
||||||
|
packaging/_ranges.py,sha256=t7WV8uaSrrNpfFRbhcJMu8tFUvKtxtkFmqgtCHfoFrc,30850
|
||||||
|
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
|
||||||
|
packaging/_tokenizer.py,sha256=zCXlfA1WTInhUHNHHW4cJkKZypRvsXwOHkr1TvdxPuY,5573
|
||||||
|
packaging/dependency_groups.py,sha256=UwnMVkyjAi37WUgW6dWTuwnN80IIwfuMXsmvtsZwJ4w,11224
|
||||||
|
packaging/direct_url.py,sha256=hDuqldBMu0kojenZ_XfulG6GLLYDEj5-RqaeFx-n0Gs,11645
|
||||||
|
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
|
||||||
|
packaging/licenses/__init__.py,sha256=WQ0S7uc92-xTtOnuW_xk2cs6rLSUDo68uiYHV85WdAs,7859
|
||||||
|
packaging/licenses/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
packaging/licenses/__pycache__/_spdx.cpython-312.pyc,,
|
||||||
|
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
|
||||||
|
packaging/markers.py,sha256=bL6GDTXZjStZnn8Oxz_qsSTgFXEbML6QReZ4ySlA2cw,20554
|
||||||
|
packaging/metadata.py,sha256=k0FJ9CClRJ5vN2qmHW8moII-uCzHxqHIzeWqe-Q9D2M,42059
|
||||||
|
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
packaging/pylock.py,sha256=G84Kn-Y-Qrh-jO0F2icrrxamiqcEs3GEBeJzWmESRnw,35232
|
||||||
|
packaging/ranges.py,sha256=tHgc6RPl3_ngeQbyBYXFTDAo4ff_VjQYKOedUi3b-rY,83054
|
||||||
|
packaging/requirements.py,sha256=RZR26yEH_jDmsTEAUaW9tEx9O3944Oh7raJoKCy6YvU,7040
|
||||||
|
packaging/specifiers.py,sha256=ZhrTHd9F_7hh0mJnNPTj1mlmApuP23lYa7EFgl2NnVk,52798
|
||||||
|
packaging/tags.py,sha256=DnF6CCp5Yg15zwMhtCDfFVk8SRb82gPCfLnYLgfvXW4,38302
|
||||||
|
packaging/utils.py,sha256=oTdCzZmU7spa37AknRJkCMR4Am6x9Iy5evvy_p8o7MY,12215
|
||||||
|
packaging/version.py,sha256=VYsvtQ_RmMZgpMCC-WuJGcVlkRtrS24H80trYMB0vpc,39040
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: flit 3.12.0
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
This software is made available under the terms of *either* of the licenses
|
||||||
|
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
|
||||||
|
under the terms of *both* these licenses.
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Copyright (c) Donald Stufft and individual contributors.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# This file is dual licensed under the terms of the Apache License, Version
|
||||||
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
|
# for complete details.
|
||||||
|
|
||||||
|
__title__ = "packaging"
|
||||||
|
__summary__ = "Core utilities for Python packages"
|
||||||
|
__uri__ = "https://github.com/pypa/packaging"
|
||||||
|
|
||||||
|
__version__ = "26.3"
|
||||||
|
|
||||||
|
__author__ = "Donald Stufft and individual contributors"
|
||||||
|
__email__ = "donald@stufft.io"
|
||||||
|
|
||||||
|
__license__ = "BSD-2-Clause or Apache-2.0"
|
||||||
|
__copyright__ = f"2014 {__author__}"
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
ELF file parser.
|
||||||
|
|
||||||
|
This provides a class ``ELFFile`` that parses an ELF executable in a similar
|
||||||
|
interface to ``ZipFile``. Only the read interface is implemented.
|
||||||
|
|
||||||
|
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
|
||||||
|
class ELFInvalid(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EIClass(enum.IntEnum):
|
||||||
|
C32 = 1
|
||||||
|
C64 = 2
|
||||||
|
|
||||||
|
|
||||||
|
class EIData(enum.IntEnum):
|
||||||
|
Lsb = 1
|
||||||
|
Msb = 2
|
||||||
|
|
||||||
|
|
||||||
|
class EMachine(enum.IntEnum):
|
||||||
|
I386 = 3
|
||||||
|
S390 = 22
|
||||||
|
Arm = 40
|
||||||
|
X8664 = 62
|
||||||
|
AArch64 = 183
|
||||||
|
|
||||||
|
|
||||||
|
class ELFFile:
|
||||||
|
"""
|
||||||
|
Representation of an ELF executable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, f: IO[bytes]) -> None:
|
||||||
|
self._f = f
|
||||||
|
|
||||||
|
try:
|
||||||
|
ident = self._read("16B")
|
||||||
|
except struct.error as e:
|
||||||
|
raise ELFInvalid("unable to parse identification") from e
|
||||||
|
magic = bytes(ident[:4])
|
||||||
|
if magic != b"\x7fELF":
|
||||||
|
raise ELFInvalid(f"invalid magic: {magic!r}")
|
||||||
|
|
||||||
|
self.capacity = ident[4] # Format for program header (bitness).
|
||||||
|
self.encoding = ident[5] # Data structure encoding (endianness).
|
||||||
|
|
||||||
|
try:
|
||||||
|
# e_fmt: Format for the ELF header.
|
||||||
|
# p_fmt: Format for a program header.
|
||||||
|
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
|
||||||
|
e_fmt, self._p_fmt, self._p_idx = {
|
||||||
|
(1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB.
|
||||||
|
(1, 2): (">HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB.
|
||||||
|
(2, 1): ("<HHIQQQIHHH", "<IIQQQQQQ", (0, 2, 5)), # 64-bit LSB.
|
||||||
|
(2, 2): (">HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB.
|
||||||
|
}[(self.capacity, self.encoding)]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ELFInvalid(
|
||||||
|
f"unrecognized capacity ({self.capacity}) or encoding ({self.encoding})"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
self.machine, # Architecture type.
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
self._e_phoff, # Offset of program header.
|
||||||
|
_,
|
||||||
|
self.flags, # Processor-specific flags.
|
||||||
|
_,
|
||||||
|
self._e_phentsize, # Size of a program header entry.
|
||||||
|
self._e_phnum, # Number of program headers.
|
||||||
|
) = self._read(e_fmt)
|
||||||
|
except struct.error as e:
|
||||||
|
raise ELFInvalid("unable to parse machine and section information") from e
|
||||||
|
|
||||||
|
def _read(self, fmt: str) -> tuple[int, ...]:
|
||||||
|
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interpreter(self) -> str | None:
|
||||||
|
"""
|
||||||
|
The path recorded in the ``PT_INTERP`` section header.
|
||||||
|
"""
|
||||||
|
for index in range(self._e_phnum):
|
||||||
|
self._f.seek(self._e_phoff + self._e_phentsize * index)
|
||||||
|
try:
|
||||||
|
data = self._read(self._p_fmt)
|
||||||
|
except struct.error:
|
||||||
|
continue
|
||||||
|
if data[self._p_idx[0]] != 3: # Not PT_INTERP.
|
||||||
|
continue
|
||||||
|
self._f.seek(data[self._p_idx[1]])
|
||||||
|
return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0")
|
||||||
|
return None
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
|
from ._elffile import EIClass, EIData, ELFFile, EMachine
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import types
|
||||||
|
from collections.abc import Generator, Iterator, Sequence
|
||||||
|
|
||||||
|
EF_ARM_ABIMASK = 0xFF000000
|
||||||
|
EF_ARM_ABI_VER5 = 0x05000000
|
||||||
|
EF_ARM_ABI_FLOAT_HARD = 0x00000400
|
||||||
|
|
||||||
|
_ALLOWED_ARCHS = {
|
||||||
|
"x86_64",
|
||||||
|
"aarch64",
|
||||||
|
"ppc64",
|
||||||
|
"ppc64le",
|
||||||
|
"s390x",
|
||||||
|
"loongarch64",
|
||||||
|
"riscv64",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
yield ELFFile(f)
|
||||||
|
except (OSError, TypeError, ValueError):
|
||||||
|
yield None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_linux_armhf(executable: str) -> bool:
|
||||||
|
# hard-float ABI can be detected from the ELF header of the running
|
||||||
|
# process
|
||||||
|
# https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
|
||||||
|
with _parse_elf(executable) as f:
|
||||||
|
return (
|
||||||
|
f is not None
|
||||||
|
and f.capacity == EIClass.C32
|
||||||
|
and f.encoding == EIData.Lsb
|
||||||
|
and f.machine == EMachine.Arm
|
||||||
|
and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5
|
||||||
|
and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_linux_i686(executable: str) -> bool:
|
||||||
|
with _parse_elf(executable) as f:
|
||||||
|
return (
|
||||||
|
f is not None
|
||||||
|
and f.capacity == EIClass.C32
|
||||||
|
and f.encoding == EIData.Lsb
|
||||||
|
and f.machine == EMachine.I386
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
|
||||||
|
if "armv7l" in archs:
|
||||||
|
return _is_linux_armhf(executable)
|
||||||
|
if "i686" in archs:
|
||||||
|
return _is_linux_i686(executable)
|
||||||
|
return any(arch in _ALLOWED_ARCHS for arch in archs)
|
||||||
|
|
||||||
|
|
||||||
|
# If glibc ever changes its major version, we need to know what the last
|
||||||
|
# minor version was, so we can build the complete list of all versions.
|
||||||
|
# For now, guess what the highest minor version might be, assume it will
|
||||||
|
# be 50 for testing. Once this actually happens, update the dictionary
|
||||||
|
# with the actual value.
|
||||||
|
_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
|
||||||
|
|
||||||
|
|
||||||
|
class _GLibCVersion(NamedTuple):
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
|
||||||
|
|
||||||
|
def _glibc_version_string_confstr() -> str | None:
|
||||||
|
"""
|
||||||
|
Primary implementation of glibc_version_string using os.confstr.
|
||||||
|
"""
|
||||||
|
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
|
||||||
|
# to be broken or missing. This strategy is used in the standard library
|
||||||
|
# platform module.
|
||||||
|
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
|
||||||
|
try:
|
||||||
|
# Should be a string like "glibc 2.17".
|
||||||
|
version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
|
||||||
|
assert version_string is not None
|
||||||
|
_, version = version_string.rsplit()
|
||||||
|
except (AssertionError, AttributeError, OSError, ValueError):
|
||||||
|
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
|
||||||
|
return None
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def _glibc_version_string_ctypes() -> str | None:
|
||||||
|
"""
|
||||||
|
Fallback implementation of glibc_version_string using ctypes.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import ctypes # noqa: PLC0415
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
|
||||||
|
# manpage says, "If filename is NULL, then the returned handle is for the
|
||||||
|
# main program". This way we can let the linker do the work to figure out
|
||||||
|
# which libc our process is actually using.
|
||||||
|
#
|
||||||
|
# We must also handle the special case where the executable is not a
|
||||||
|
# dynamically linked executable. This can occur when using musl libc,
|
||||||
|
# for example. In this situation, dlopen() will error, leading to an
|
||||||
|
# OSError. Interestingly, at least in the case of musl, there is no
|
||||||
|
# errno set on the OSError. The single string argument used to construct
|
||||||
|
# OSError comes from libc itself and is therefore not portable to
|
||||||
|
# hard code here. In any case, failure to call dlopen() means we
|
||||||
|
# can proceed, so we bail on our attempt.
|
||||||
|
try:
|
||||||
|
process_namespace = ctypes.CDLL(None)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
gnu_get_libc_version = process_namespace.gnu_get_libc_version
|
||||||
|
except AttributeError:
|
||||||
|
# Symbol doesn't exist -> therefore, we are not linked to
|
||||||
|
# glibc.
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Call gnu_get_libc_version, which returns a string like "2.5".
|
||||||
|
gnu_get_libc_version.restype = ctypes.c_char_p
|
||||||
|
# A c_char_p restype comes back as bytes, so decode to text.
|
||||||
|
version_str: str | bytes = gnu_get_libc_version()
|
||||||
|
if isinstance(version_str, bytes):
|
||||||
|
version_str = version_str.decode("ascii")
|
||||||
|
|
||||||
|
return version_str
|
||||||
|
|
||||||
|
|
||||||
|
def _glibc_version_string() -> str | None:
|
||||||
|
"""Returns glibc version string, or None if not using glibc."""
|
||||||
|
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
|
||||||
|
"""Parse glibc version.
|
||||||
|
|
||||||
|
We use a regexp instead of str.split because we want to discard any
|
||||||
|
random junk that might come after the minor version -- this might happen
|
||||||
|
in patched/forked versions of glibc (e.g. Linaro's version of glibc
|
||||||
|
uses version strings like "2.20-2014.11"). See gh-3588.
|
||||||
|
"""
|
||||||
|
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
|
||||||
|
if not m:
|
||||||
|
warnings.warn(
|
||||||
|
f"Expected glibc version with 2 components major.minor, got: {version_str}",
|
||||||
|
RuntimeWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _GLibCVersion(-1, -1)
|
||||||
|
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def _get_glibc_version() -> _GLibCVersion:
|
||||||
|
version_str = _glibc_version_string()
|
||||||
|
if version_str is None:
|
||||||
|
return _GLibCVersion(-1, -1)
|
||||||
|
return _parse_glibc_version(version_str)
|
||||||
|
|
||||||
|
|
||||||
|
# From PEP 513, PEP 600
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def _get_manylinux_module() -> types.ModuleType | None:
|
||||||
|
"""Return the ``_manylinux`` C extension module, or None if unavailable.
|
||||||
|
|
||||||
|
The result is cached for the lifetime of the process, since the presence
|
||||||
|
of the module does not change while running.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return __import__("_manylinux")
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
|
||||||
|
sys_glibc = _get_glibc_version()
|
||||||
|
if sys_glibc < version:
|
||||||
|
return False
|
||||||
|
# Check for presence of _manylinux module.
|
||||||
|
manylinux_mod = _get_manylinux_module()
|
||||||
|
if manylinux_mod is None:
|
||||||
|
return True
|
||||||
|
if hasattr(manylinux_mod, "manylinux_compatible"):
|
||||||
|
result = manylinux_mod.manylinux_compatible(version[0], version[1], arch)
|
||||||
|
if result is not None:
|
||||||
|
return bool(result)
|
||||||
|
return True
|
||||||
|
if version == _GLibCVersion(2, 5) and hasattr(
|
||||||
|
manylinux_mod, "manylinux1_compatible"
|
||||||
|
):
|
||||||
|
return bool(manylinux_mod.manylinux1_compatible)
|
||||||
|
if version == _GLibCVersion(2, 12) and hasattr(
|
||||||
|
manylinux_mod, "manylinux2010_compatible"
|
||||||
|
):
|
||||||
|
return bool(manylinux_mod.manylinux2010_compatible)
|
||||||
|
if version == _GLibCVersion(2, 17) and hasattr(
|
||||||
|
manylinux_mod, "manylinux2014_compatible"
|
||||||
|
):
|
||||||
|
return bool(manylinux_mod.manylinux2014_compatible)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
|
||||||
|
# CentOS 7 w/ glibc 2.17 (PEP 599)
|
||||||
|
_GLibCVersion(2, 17): "manylinux2014",
|
||||||
|
# CentOS 6 w/ glibc 2.12 (PEP 571)
|
||||||
|
_GLibCVersion(2, 12): "manylinux2010",
|
||||||
|
# CentOS 5 w/ glibc 2.5 (PEP 513)
|
||||||
|
_GLibCVersion(2, 5): "manylinux1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
|
||||||
|
"""Generate manylinux tags compatible to the current platform.
|
||||||
|
|
||||||
|
:param archs: Sequence of compatible architectures.
|
||||||
|
The first one shall be the closest to the actual architecture and be the part of
|
||||||
|
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
|
||||||
|
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
|
||||||
|
be manylinux-compatible.
|
||||||
|
|
||||||
|
:returns: An iterator of compatible manylinux tags.
|
||||||
|
"""
|
||||||
|
if not _have_compatible_abi(sys.executable, archs):
|
||||||
|
return
|
||||||
|
# Oldest glibc to be supported regardless of architecture is (2, 17).
|
||||||
|
too_old_glibc2 = _GLibCVersion(2, 16)
|
||||||
|
if set(archs) & {"x86_64", "i686"}:
|
||||||
|
# On x86/i686 also oldest glibc to be supported is (2, 5).
|
||||||
|
too_old_glibc2 = _GLibCVersion(2, 4)
|
||||||
|
current_glibc = _GLibCVersion(*_get_glibc_version())
|
||||||
|
glibc_max_list = [current_glibc]
|
||||||
|
# We can assume compatibility across glibc major versions.
|
||||||
|
# https://sourceware.org/bugzilla/show_bug.cgi?id=24636
|
||||||
|
#
|
||||||
|
# Build a list of maximum glibc versions so that we can
|
||||||
|
# output the canonical list of all glibc from current_glibc
|
||||||
|
# down to too_old_glibc2, including all intermediary versions.
|
||||||
|
for glibc_major in range(current_glibc.major - 1, 1, -1):
|
||||||
|
glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
|
||||||
|
glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
|
||||||
|
for arch in archs:
|
||||||
|
for glibc_max in glibc_max_list:
|
||||||
|
if glibc_max.major == too_old_glibc2.major:
|
||||||
|
min_minor = too_old_glibc2.minor
|
||||||
|
else:
|
||||||
|
# For other glibc major versions oldest supported is (x, 0).
|
||||||
|
min_minor = -1
|
||||||
|
for glibc_minor in range(glibc_max.minor, min_minor, -1):
|
||||||
|
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
|
||||||
|
if _is_compatible(arch, glibc_version):
|
||||||
|
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
|
||||||
|
|
||||||
|
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
|
||||||
|
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
|
||||||
|
yield f"{legacy_tag}_{arch}"
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""PEP 656 support.
|
||||||
|
|
||||||
|
This module implements logic to detect if the currently running Python is
|
||||||
|
linked against musl, and what musl version is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
|
from ._elffile import ELFFile
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator, Sequence
|
||||||
|
|
||||||
|
|
||||||
|
class _MuslVersion(NamedTuple):
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_musl_version(output: str) -> _MuslVersion | None:
|
||||||
|
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
|
||||||
|
if len(lines) < 2 or lines[0][:4] != "musl":
|
||||||
|
return None
|
||||||
|
m = re.match(r"Version (\d+)\.(\d+)", lines[1])
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def _get_musl_version(executable: str) -> _MuslVersion | None:
|
||||||
|
"""Detect currently-running musl runtime version.
|
||||||
|
|
||||||
|
This is done by checking the specified executable's dynamic linking
|
||||||
|
information, and invoking the loader to parse its output for a version
|
||||||
|
string. If the loader is musl, the output would be something like::
|
||||||
|
|
||||||
|
musl libc (x86_64)
|
||||||
|
Version 1.2.2
|
||||||
|
Dynamic Program Loader
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(executable, "rb") as f:
|
||||||
|
ld = ELFFile(f).interpreter
|
||||||
|
except (OSError, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if ld is None or "musl" not in ld:
|
||||||
|
return None
|
||||||
|
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
|
||||||
|
return _parse_musl_version(proc.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
|
||||||
|
"""Generate musllinux tags compatible to the current platform.
|
||||||
|
|
||||||
|
:param archs: Sequence of compatible architectures.
|
||||||
|
The first one shall be the closest to the actual architecture and be the part of
|
||||||
|
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
|
||||||
|
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
|
||||||
|
be musllinux-compatible.
|
||||||
|
|
||||||
|
:returns: An iterator of compatible musllinux tags.
|
||||||
|
"""
|
||||||
|
sys_musl = _get_musl_version(sys.executable)
|
||||||
|
if sys_musl is None: # Python not dynamically linked against musl.
|
||||||
|
return
|
||||||
|
for arch in archs:
|
||||||
|
for minor in range(sys_musl.minor, -1, -1):
|
||||||
|
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
|
plat = sysconfig.get_platform()
|
||||||
|
assert plat.startswith("linux-"), "not linux"
|
||||||
|
|
||||||
|
print("plat:", plat)
|
||||||
|
print("musl:", _get_musl_version(sys.executable))
|
||||||
|
print("tags:", end=" ")
|
||||||
|
for t in platform_tags([re.sub(r"[.-]", "_", plat.split("-", 1)[-1])]):
|
||||||
|
print(t, end="\n ")
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
"""Handwritten parser of dependency specifiers.
|
||||||
|
|
||||||
|
The docstring for each __parse_* function contains EBNF-inspired grammar representing
|
||||||
|
the implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Literal, NamedTuple, Union
|
||||||
|
|
||||||
|
from ._tokenizer import DEFAULT_RULES, ParserSyntaxError, Tokenizer
|
||||||
|
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
__slots__ = ("value",)
|
||||||
|
|
||||||
|
def __init__(self, value: str) -> None:
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__}({self.value!r})>"
|
||||||
|
|
||||||
|
def serialize(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __getstate__(self) -> str:
|
||||||
|
# Return just the value string for compactness and stability.
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def _restore_value(self, value: object) -> None:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(
|
||||||
|
f"Cannot restore {self.__class__.__name__} value from {value!r}"
|
||||||
|
)
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __setstate__(self, state: object) -> None:
|
||||||
|
if isinstance(state, str):
|
||||||
|
# New format (26.2+): just the value string.
|
||||||
|
self._restore_value(state)
|
||||||
|
return
|
||||||
|
if isinstance(state, tuple) and len(state) == 2:
|
||||||
|
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
|
||||||
|
_, slot_dict = state
|
||||||
|
if isinstance(slot_dict, dict) and "value" in slot_dict:
|
||||||
|
self._restore_value(slot_dict["value"])
|
||||||
|
return
|
||||||
|
if isinstance(state, dict) and "value" in state:
|
||||||
|
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
|
||||||
|
self._restore_value(state["value"])
|
||||||
|
return
|
||||||
|
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class Variable(Node):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def serialize(self) -> str:
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class Value(Node):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def serialize(self) -> str:
|
||||||
|
value = str(self)
|
||||||
|
if '"' not in value:
|
||||||
|
return f'"{value}"'
|
||||||
|
if "'" not in value:
|
||||||
|
return f"'{value}'"
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot serialize marker value containing both quote characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Op(Node):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def serialize(self) -> str:
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
|
MarkerLogical = Literal["and", "or"]
|
||||||
|
MarkerVar = Union[Variable, Value]
|
||||||
|
MarkerItem = tuple[MarkerVar, Op, MarkerVar]
|
||||||
|
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
|
||||||
|
MarkerList = list[Union["MarkerList", MarkerAtom, MarkerLogical]]
|
||||||
|
|
||||||
|
|
||||||
|
class ParsedRequirement(NamedTuple):
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
extras: list[str]
|
||||||
|
specifier: str
|
||||||
|
marker: MarkerList | None
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------
|
||||||
|
# Recursive descent parser for dependency specifier
|
||||||
|
# --------------------------------------------------------------------------------------
|
||||||
|
def parse_requirement(source: str) -> ParsedRequirement:
|
||||||
|
return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
|
||||||
|
"""
|
||||||
|
requirement = WS? IDENTIFIER WS? extras WS? requirement_details
|
||||||
|
"""
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
name_token = tokenizer.expect(
|
||||||
|
"IDENTIFIER", expected="package name at the start of dependency specifier"
|
||||||
|
)
|
||||||
|
name = name_token.text
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
extras = _parse_extras(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
url, specifier, marker = _parse_requirement_details(tokenizer)
|
||||||
|
tokenizer.expect("END", expected="end of dependency specifier")
|
||||||
|
|
||||||
|
return ParsedRequirement(name, url, extras, specifier, marker)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_requirement_details(
|
||||||
|
tokenizer: Tokenizer,
|
||||||
|
) -> tuple[str, str, MarkerList | None]:
|
||||||
|
"""
|
||||||
|
requirement_details = AT URL (WS requirement_marker?)?
|
||||||
|
| specifier WS? (requirement_marker)?
|
||||||
|
"""
|
||||||
|
|
||||||
|
specifier = ""
|
||||||
|
url = ""
|
||||||
|
marker = None
|
||||||
|
|
||||||
|
if tokenizer.check("AT"):
|
||||||
|
tokenizer.read()
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
url_start = tokenizer.position
|
||||||
|
url = tokenizer.expect("URL", expected="URL after @").text
|
||||||
|
if tokenizer.check("END", peek=True):
|
||||||
|
return (url, specifier, marker)
|
||||||
|
|
||||||
|
tokenizer.expect("WS", expected="whitespace after URL")
|
||||||
|
|
||||||
|
# The input might end after whitespace.
|
||||||
|
if tokenizer.check("END", peek=True):
|
||||||
|
return (url, specifier, marker)
|
||||||
|
|
||||||
|
marker = _parse_requirement_marker(
|
||||||
|
tokenizer,
|
||||||
|
span_start=url_start,
|
||||||
|
expected="semicolon (after URL and whitespace)",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
specifier_start = tokenizer.position
|
||||||
|
specifier = _parse_specifier(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
if tokenizer.check("END", peek=True):
|
||||||
|
return (url, specifier, marker)
|
||||||
|
|
||||||
|
marker = _parse_requirement_marker(
|
||||||
|
tokenizer,
|
||||||
|
span_start=specifier_start,
|
||||||
|
expected=(
|
||||||
|
"comma (within version specifier), semicolon (after version specifier)"
|
||||||
|
if specifier
|
||||||
|
else "semicolon (after name with no version specifier)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (url, specifier, marker)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_requirement_marker(
|
||||||
|
tokenizer: Tokenizer, *, span_start: int, expected: str
|
||||||
|
) -> MarkerList:
|
||||||
|
"""
|
||||||
|
requirement_marker = SEMICOLON marker WS?
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not tokenizer.check("SEMICOLON"):
|
||||||
|
tokenizer.raise_syntax_error(
|
||||||
|
f"Expected {expected} or end",
|
||||||
|
span_start=span_start,
|
||||||
|
span_end=None,
|
||||||
|
)
|
||||||
|
tokenizer.read()
|
||||||
|
|
||||||
|
marker = _parse_marker(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
return marker
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_extras(tokenizer: Tokenizer) -> list[str]:
|
||||||
|
"""
|
||||||
|
extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
|
||||||
|
"""
|
||||||
|
if not tokenizer.check("LEFT_BRACKET", peek=True):
|
||||||
|
return []
|
||||||
|
|
||||||
|
with tokenizer.enclosing_tokens(
|
||||||
|
"LEFT_BRACKET",
|
||||||
|
"RIGHT_BRACKET",
|
||||||
|
around="extras",
|
||||||
|
):
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
extras = _parse_extras_list(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
return extras
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
|
||||||
|
"""
|
||||||
|
extras_list = identifier (wsp* ',' wsp* identifier)*
|
||||||
|
"""
|
||||||
|
extras: list[str] = []
|
||||||
|
|
||||||
|
if not tokenizer.check("IDENTIFIER"):
|
||||||
|
return extras
|
||||||
|
|
||||||
|
extras.append(tokenizer.read().text)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
if tokenizer.check("IDENTIFIER", peek=True):
|
||||||
|
tokenizer.raise_syntax_error("Expected comma between extra names")
|
||||||
|
elif not tokenizer.check("COMMA"):
|
||||||
|
break
|
||||||
|
|
||||||
|
tokenizer.read()
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")
|
||||||
|
extras.append(extra_token.text)
|
||||||
|
|
||||||
|
return extras
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_specifier(tokenizer: Tokenizer) -> str:
|
||||||
|
"""
|
||||||
|
specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
|
||||||
|
| WS? version_many WS?
|
||||||
|
"""
|
||||||
|
with tokenizer.enclosing_tokens(
|
||||||
|
"LEFT_PARENTHESIS",
|
||||||
|
"RIGHT_PARENTHESIS",
|
||||||
|
around="version specifier",
|
||||||
|
):
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
parsed_specifiers = _parse_version_many(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
return parsed_specifiers
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version_many(tokenizer: Tokenizer) -> str:
|
||||||
|
"""
|
||||||
|
version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?
|
||||||
|
"""
|
||||||
|
parsed_specifiers = ""
|
||||||
|
while tokenizer.check("SPECIFIER"):
|
||||||
|
span_start = tokenizer.position
|
||||||
|
specifier = tokenizer.read().text
|
||||||
|
parsed_specifiers += specifier
|
||||||
|
if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
|
||||||
|
message = ".* suffix can only be used with `==` or `!=` operators"
|
||||||
|
if specifier.startswith("!=") or (
|
||||||
|
specifier.startswith("==") and not specifier.startswith("===")
|
||||||
|
):
|
||||||
|
message = (
|
||||||
|
".* suffix cannot be used with pre-release, post-release, "
|
||||||
|
"dev or local versions"
|
||||||
|
)
|
||||||
|
tokenizer.raise_syntax_error(
|
||||||
|
message,
|
||||||
|
span_start=span_start,
|
||||||
|
span_end=tokenizer.position + 1,
|
||||||
|
)
|
||||||
|
if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
|
||||||
|
tokenizer.raise_syntax_error(
|
||||||
|
"Local version label can only be used with `==` or `!=` operators",
|
||||||
|
span_start=span_start,
|
||||||
|
span_end=tokenizer.position,
|
||||||
|
)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
if not tokenizer.check("COMMA"):
|
||||||
|
break
|
||||||
|
parsed_specifiers += tokenizer.read().text
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
|
||||||
|
return parsed_specifiers
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------
|
||||||
|
# Recursive descent parser for marker expression
|
||||||
|
# --------------------------------------------------------------------------------------
|
||||||
|
def parse_marker(source: str) -> MarkerList:
|
||||||
|
return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
|
||||||
|
retval = _parse_marker(tokenizer)
|
||||||
|
tokenizer.expect("END", expected="end of marker expression")
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
|
||||||
|
"""
|
||||||
|
marker = marker_atom (BOOLOP marker_atom)+
|
||||||
|
"""
|
||||||
|
expression = [_parse_marker_atom(tokenizer)]
|
||||||
|
while tokenizer.check("BOOLOP"):
|
||||||
|
token = tokenizer.read()
|
||||||
|
expr_right = _parse_marker_atom(tokenizer)
|
||||||
|
expression.extend((token.text, expr_right))
|
||||||
|
return expression
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
|
||||||
|
"""
|
||||||
|
marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?
|
||||||
|
| WS? marker_item WS?
|
||||||
|
"""
|
||||||
|
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
if tokenizer.check("LEFT_PARENTHESIS", peek=True):
|
||||||
|
with tokenizer.enclosing_tokens(
|
||||||
|
"LEFT_PARENTHESIS",
|
||||||
|
"RIGHT_PARENTHESIS",
|
||||||
|
around="marker expression",
|
||||||
|
):
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
marker: MarkerAtom = _parse_marker(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
else:
|
||||||
|
marker = _parse_marker_item(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
return marker
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
|
||||||
|
"""
|
||||||
|
marker_item = WS? marker_var WS? marker_op WS? marker_var WS?
|
||||||
|
"""
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
marker_var_left = _parse_marker_var(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
marker_op = _parse_marker_op(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
marker_var_right = _parse_marker_var(tokenizer)
|
||||||
|
tokenizer.consume("WS")
|
||||||
|
return (marker_var_left, marker_op, marker_var_right)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
|
||||||
|
"""
|
||||||
|
marker_var = VARIABLE | QUOTED_STRING
|
||||||
|
"""
|
||||||
|
if tokenizer.check("VARIABLE"):
|
||||||
|
return process_env_var(tokenizer.read().text.replace(".", "_"))
|
||||||
|
elif tokenizer.check("QUOTED_STRING"):
|
||||||
|
token = tokenizer.read()
|
||||||
|
try:
|
||||||
|
return process_python_str(token.text)
|
||||||
|
except (SyntaxError, ValueError) as exc:
|
||||||
|
raise ParserSyntaxError(
|
||||||
|
"Invalid quoted string",
|
||||||
|
source=tokenizer.source,
|
||||||
|
span=(token.position, token.position + len(token.text)),
|
||||||
|
) from exc
|
||||||
|
else:
|
||||||
|
tokenizer.raise_syntax_error(
|
||||||
|
message="Expected a marker variable or quoted string"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_env_var(env_var: str) -> Variable:
|
||||||
|
if env_var in ("platform_python_implementation", "python_implementation"):
|
||||||
|
return Variable("platform_python_implementation")
|
||||||
|
else:
|
||||||
|
return Variable(env_var)
|
||||||
|
|
||||||
|
|
||||||
|
def process_python_str(python_str: str) -> Value:
|
||||||
|
value = ast.literal_eval(python_str)
|
||||||
|
return Value(str(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_marker_op(tokenizer: Tokenizer) -> Op:
|
||||||
|
"""
|
||||||
|
marker_op = IN | NOT IN | OP
|
||||||
|
"""
|
||||||
|
if tokenizer.check("IN"):
|
||||||
|
tokenizer.read()
|
||||||
|
return Op("in")
|
||||||
|
elif tokenizer.check("NOT"):
|
||||||
|
tokenizer.read()
|
||||||
|
tokenizer.expect("WS", expected="whitespace after 'not'")
|
||||||
|
tokenizer.expect("IN", expected="'in' after 'not'")
|
||||||
|
return Op("not in")
|
||||||
|
elif tokenizer.check("OP"):
|
||||||
|
return Op(tokenizer.read().text)
|
||||||
|
else:
|
||||||
|
return tokenizer.raise_syntax_error(
|
||||||
|
"Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in"
|
||||||
|
)
|
||||||
@@ -0,0 +1,836 @@
|
|||||||
|
# This file is dual licensed under the terms of the Apache License, Version
|
||||||
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
|
# for complete details.
|
||||||
|
"""Private version-range helpers used by :mod:`packaging.specifiers`."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import functools
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Final,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .version import InvalidVersion, Version
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
# Total-order key for comparing two boundaries (boundary-vs-boundary only).
|
||||||
|
# The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
|
||||||
|
_BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int]
|
||||||
|
_BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"FULL_RANGE",
|
||||||
|
"bounds_for_spec",
|
||||||
|
"coerce_version",
|
||||||
|
"filter_by_ranges",
|
||||||
|
"intersect_ranges",
|
||||||
|
"intersect_specifier_bounds",
|
||||||
|
"least_version_above",
|
||||||
|
"matches_bounds_only",
|
||||||
|
"range_is_empty",
|
||||||
|
"ranges_are_prerelease_only",
|
||||||
|
"resolve_prereleases",
|
||||||
|
"standard_ranges",
|
||||||
|
"wildcard_ranges",
|
||||||
|
]
|
||||||
|
|
||||||
|
#: The smallest possible PEP 440 version. No valid version is less than this.
|
||||||
|
MIN_VERSION: Final[Version] = Version("0.dev0")
|
||||||
|
|
||||||
|
#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
|
||||||
|
#: above the ``-inf`` floor.
|
||||||
|
MIN_RELEASE: Final[Version] = Version("0")
|
||||||
|
|
||||||
|
#: Sorts above any real post number and any local label, so a boundary can be
|
||||||
|
#: ordered above the version family it covers when two boundaries are compared.
|
||||||
|
_BOUNDARY_INF: Final[float] = float("inf")
|
||||||
|
|
||||||
|
|
||||||
|
class BoundaryKind(enum.Enum):
|
||||||
|
"""Where a boundary marker sits in the version ordering."""
|
||||||
|
|
||||||
|
AFTER_LOCALS = enum.auto() # after V+local, before V.post0
|
||||||
|
AFTER_POSTS = enum.auto() # after V.postN, before next release
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
|
class BoundaryVersion:
|
||||||
|
"""A point on the version line between two real PEP 440 versions.
|
||||||
|
|
||||||
|
Relative to a base version V::
|
||||||
|
|
||||||
|
V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
|
||||||
|
|
||||||
|
AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
|
||||||
|
local), and the lower bound of the upper-side range of ``!=V``.
|
||||||
|
AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
|
||||||
|
excluding V's post-releases per PEP 440.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_cached_dev",
|
||||||
|
"_cached_epoch",
|
||||||
|
"_cached_post",
|
||||||
|
"_cached_pre",
|
||||||
|
"_cached_trimmed_release",
|
||||||
|
"kind",
|
||||||
|
"version",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, version: Version, kind: BoundaryKind) -> None:
|
||||||
|
self.version = version
|
||||||
|
self.kind = kind
|
||||||
|
self._cached_trimmed_release = trim_release(version.release)
|
||||||
|
self._cached_epoch = version.epoch
|
||||||
|
self._cached_pre = version.pre
|
||||||
|
self._cached_post = version.post
|
||||||
|
self._cached_dev = version.dev
|
||||||
|
|
||||||
|
def _is_family(self, other: Version) -> bool:
|
||||||
|
"""Is ``other`` a version that this boundary sorts above?"""
|
||||||
|
if other.epoch != self._cached_epoch:
|
||||||
|
return False
|
||||||
|
# Inline release-trim comparison: other.release matches the
|
||||||
|
# trimmed release iff its leading slice is equal and any extra
|
||||||
|
# components are zero. Avoids trim_release's tuple allocation.
|
||||||
|
other_release = other.release
|
||||||
|
trimmed_release = self._cached_trimmed_release
|
||||||
|
trimmed_length = len(trimmed_release)
|
||||||
|
if len(other_release) < trimmed_length:
|
||||||
|
return False
|
||||||
|
if other_release[:trimmed_length] != trimmed_release:
|
||||||
|
return False
|
||||||
|
for i in range(trimmed_length, len(other_release)):
|
||||||
|
if other_release[i] != 0:
|
||||||
|
return False
|
||||||
|
if other.pre != self._cached_pre:
|
||||||
|
return False
|
||||||
|
if self.kind == BoundaryKind.AFTER_LOCALS:
|
||||||
|
# Local family: same public version, any local label.
|
||||||
|
return other.post == self._cached_post and other.dev == self._cached_dev
|
||||||
|
# Post family: V itself + any post-release of V.
|
||||||
|
return other.dev == self._cached_dev or other.post is not None
|
||||||
|
|
||||||
|
def _order_key(self) -> _BoundaryOrderKey:
|
||||||
|
"""Sort key placing this boundary just above the versions it covers.
|
||||||
|
|
||||||
|
It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
|
||||||
|
a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
|
||||||
|
``V`` and every ``V+local`` (whose keys carry a real, finite local
|
||||||
|
segment). ``suffix`` is the 6-int comparison suffix
|
||||||
|
``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
|
||||||
|
|
||||||
|
For an AFTER_POSTS boundary the suffix is replaced with one whose
|
||||||
|
post number is ``_BOUNDARY_INF``, so the key also sorts after every
|
||||||
|
``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
|
||||||
|
"""
|
||||||
|
version_key = self.version._key
|
||||||
|
suffix: _BoundaryOrderSuffix = version_key[2]
|
||||||
|
|
||||||
|
if self.kind == BoundaryKind.AFTER_POSTS:
|
||||||
|
suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
|
||||||
|
|
||||||
|
return version_key[0], version_key[1], suffix, _BOUNDARY_INF
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
# Key off the order key so equality matches the ``<`` / ``>`` order:
|
||||||
|
# ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
|
||||||
|
if isinstance(other, BoundaryVersion):
|
||||||
|
return self._order_key() == other._order_key()
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other: BoundaryVersion | Version) -> bool:
|
||||||
|
if isinstance(other, BoundaryVersion):
|
||||||
|
return self._order_key() < other._order_key()
|
||||||
|
# boundary < other_version iff V < other AND other not in family.
|
||||||
|
# The cheap V >= other path short-circuits before the family check.
|
||||||
|
if not (self.version < other):
|
||||||
|
return False
|
||||||
|
return not self._is_family(other)
|
||||||
|
|
||||||
|
def __gt__(self, other: BoundaryVersion | Version) -> bool:
|
||||||
|
# Defined directly to bypass functools.total_ordering's
|
||||||
|
# NotImplemented round-trip on reflected ``Version < boundary``.
|
||||||
|
if isinstance(other, BoundaryVersion):
|
||||||
|
return self._order_key() > other._order_key()
|
||||||
|
if self.version >= other:
|
||||||
|
return True
|
||||||
|
return self._is_family(other)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
# Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
|
||||||
|
return hash(self._order_key())
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_VersionOrBoundary = Union[Version, BoundaryVersion, None]
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
|
class LowerBound:
|
||||||
|
"""Lower bound of a version range.
|
||||||
|
|
||||||
|
A version *v* of ``None`` means unbounded below (-inf).
|
||||||
|
At equal versions, ``[v`` sorts before ``(v`` because an inclusive
|
||||||
|
bound starts earlier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_above", "inclusive", "version")
|
||||||
|
|
||||||
|
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
||||||
|
self.version = version
|
||||||
|
self.inclusive = inclusive
|
||||||
|
# Pre-bind a predicate "is parsed at or above this lower
|
||||||
|
# bound?" for the hot filter / contains loops. One direct
|
||||||
|
# call per check, no operator-dispatch chain.
|
||||||
|
if version is None:
|
||||||
|
self._above: Callable[[Version], bool] | None = None
|
||||||
|
elif isinstance(version, BoundaryVersion):
|
||||||
|
# >V produces an AFTER_POSTS lower bound; the upper-side
|
||||||
|
# range of !=V produces an AFTER_LOCALS lower bound.
|
||||||
|
if version.kind == BoundaryKind.AFTER_POSTS:
|
||||||
|
self._above = _make_above_after_posts(version.version)
|
||||||
|
else:
|
||||||
|
self._above = _make_above_after_locals(version.version)
|
||||||
|
elif inclusive:
|
||||||
|
self._above = version.__le__
|
||||||
|
else:
|
||||||
|
self._above = version.__lt__
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, LowerBound):
|
||||||
|
return NotImplemented
|
||||||
|
return self.version == other.version and self.inclusive == other.inclusive
|
||||||
|
|
||||||
|
def __lt__(self, other: LowerBound) -> bool:
|
||||||
|
if not isinstance(other, LowerBound):
|
||||||
|
return NotImplemented
|
||||||
|
# -inf < anything (except -inf itself).
|
||||||
|
if self.version is None:
|
||||||
|
return other.version is not None
|
||||||
|
if other.version is None:
|
||||||
|
return False
|
||||||
|
if self.version != other.version:
|
||||||
|
return self.version < other.version
|
||||||
|
# [v < (v: inclusive starts earlier.
|
||||||
|
return self.inclusive and not other.inclusive
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.version, self.inclusive))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
bracket = "[" if self.inclusive else "("
|
||||||
|
return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
|
class UpperBound:
|
||||||
|
"""Upper bound of a version range.
|
||||||
|
|
||||||
|
A version *v* of ``None`` means unbounded above (+inf).
|
||||||
|
At equal versions, ``v)`` sorts before ``v]`` because an exclusive
|
||||||
|
bound ends earlier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_below", "inclusive", "version")
|
||||||
|
|
||||||
|
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
||||||
|
self.version = version
|
||||||
|
self.inclusive = inclusive
|
||||||
|
# Pre-bind a predicate "is parsed at or below this upper
|
||||||
|
# bound?". See LowerBound for the rationale.
|
||||||
|
if version is None:
|
||||||
|
self._below: Callable[[Version], bool] | None = None
|
||||||
|
elif isinstance(version, BoundaryVersion):
|
||||||
|
# Standard specifiers only ever produce AFTER_LOCALS upper
|
||||||
|
# bounds (from <=V / ==V / !=V with no local).
|
||||||
|
if version.kind == BoundaryKind.AFTER_LOCALS:
|
||||||
|
self._below = _make_below_after_locals(version.version)
|
||||||
|
else:
|
||||||
|
# An AFTER_POSTS upper is not produced by any specifier, but
|
||||||
|
# range algebra reaches it: complementing ``>V`` flips the
|
||||||
|
# ``AFTER_POSTS(V)`` lower into this upper bound.
|
||||||
|
self._below = version.__ge__
|
||||||
|
elif inclusive:
|
||||||
|
self._below = version.__ge__
|
||||||
|
else:
|
||||||
|
self._below = version.__gt__
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, UpperBound):
|
||||||
|
return NotImplemented
|
||||||
|
return self.version == other.version and self.inclusive == other.inclusive
|
||||||
|
|
||||||
|
def __lt__(self, other: UpperBound) -> bool:
|
||||||
|
if not isinstance(other, UpperBound):
|
||||||
|
return NotImplemented
|
||||||
|
# Nothing < +inf (except +inf itself).
|
||||||
|
if self.version is None:
|
||||||
|
return False
|
||||||
|
if other.version is None:
|
||||||
|
return True
|
||||||
|
if self.version != other.version:
|
||||||
|
return self.version < other.version
|
||||||
|
# v) < v]: exclusive ends earlier.
|
||||||
|
return not self.inclusive and other.inclusive
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.version, self.inclusive))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
bracket = "]" if self.inclusive else ")"
|
||||||
|
return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
#: A single contiguous interval as a (lower, upper) bound pair.
|
||||||
|
Interval = tuple[LowerBound, UpperBound]
|
||||||
|
|
||||||
|
|
||||||
|
NEG_INF: Final[LowerBound] = LowerBound(None, False)
|
||||||
|
POS_INF: Final[UpperBound] = UpperBound(None, False)
|
||||||
|
FULL_RANGE: Final[tuple[Interval]] = ((NEG_INF, POS_INF),)
|
||||||
|
|
||||||
|
|
||||||
|
def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
|
||||||
|
"""Strip trailing zeros from a release tuple for normalized comparison."""
|
||||||
|
end = len(release)
|
||||||
|
while end > 1 and release[end - 1] == 0:
|
||||||
|
end -= 1
|
||||||
|
return release if end == len(release) else release[:end]
|
||||||
|
|
||||||
|
|
||||||
|
def _next_prefix_dev0(version: Version) -> Version:
|
||||||
|
"""Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
|
||||||
|
release = (*version.release[:-1], version.release[-1] + 1)
|
||||||
|
return Version.from_parts(epoch=version.epoch, release=release, dev=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _base_dev0(version: Version) -> Version:
|
||||||
|
"""The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
|
||||||
|
return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
|
||||||
|
|
||||||
|
|
||||||
|
def coerce_version(version: Version | str) -> Version | None:
|
||||||
|
if not isinstance(version, Version):
|
||||||
|
try:
|
||||||
|
version = Version(version)
|
||||||
|
except InvalidVersion:
|
||||||
|
return None
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
|
||||||
|
"""Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
|
||||||
|
|
||||||
|
Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
|
||||||
|
a post-release. AFTER_POSTS sits above V and every V.postN (with
|
||||||
|
or without local), and just below the next release.
|
||||||
|
"""
|
||||||
|
version_ge = version.__ge__
|
||||||
|
version_epoch = version.epoch
|
||||||
|
version_pre = version.pre
|
||||||
|
version_release_trimmed = trim_release(version.release)
|
||||||
|
trimmed_length = len(version_release_trimmed)
|
||||||
|
|
||||||
|
def above(parsed: Version) -> bool:
|
||||||
|
if version_ge(parsed):
|
||||||
|
return False
|
||||||
|
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
|
||||||
|
# post family.
|
||||||
|
if parsed.epoch != version_epoch:
|
||||||
|
return True
|
||||||
|
parsed_release = parsed.release
|
||||||
|
if len(parsed_release) < trimmed_length:
|
||||||
|
return True
|
||||||
|
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||||
|
return True
|
||||||
|
for i in range(trimmed_length, len(parsed_release)):
|
||||||
|
if parsed_release[i] != 0:
|
||||||
|
return True
|
||||||
|
if parsed.pre != version_pre:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Same release and pre as V: parsed is in V's post family (V itself,
|
||||||
|
# V+local, or V.postN), which the boundary sits above. A V.devN
|
||||||
|
# (different dev, no post) sorts before V and was already caught by
|
||||||
|
# ``version_ge`` above, so the answer here is always "not above".
|
||||||
|
return False
|
||||||
|
|
||||||
|
return above
|
||||||
|
|
||||||
|
|
||||||
|
def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
|
||||||
|
"""Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
|
||||||
|
|
||||||
|
Used by the upper-side range of ``!=V`` (when V has no local
|
||||||
|
segment). AFTER_LOCALS sits above V and every ``V+local`` but
|
||||||
|
just below ``V.post0``.
|
||||||
|
"""
|
||||||
|
version_ge = version.__ge__
|
||||||
|
version_epoch = version.epoch
|
||||||
|
version_pre = version.pre
|
||||||
|
version_post = version.post
|
||||||
|
version_dev = version.dev
|
||||||
|
version_release_trimmed = trim_release(version.release)
|
||||||
|
trimmed_length = len(version_release_trimmed)
|
||||||
|
|
||||||
|
def above(parsed: Version) -> bool:
|
||||||
|
if version_ge(parsed):
|
||||||
|
return False
|
||||||
|
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
|
||||||
|
# local family (same public version, any local segment).
|
||||||
|
if parsed.epoch != version_epoch:
|
||||||
|
return True
|
||||||
|
parsed_release = parsed.release
|
||||||
|
if len(parsed_release) < trimmed_length:
|
||||||
|
return True
|
||||||
|
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||||
|
return True
|
||||||
|
for i in range(trimmed_length, len(parsed_release)):
|
||||||
|
if parsed_release[i] != 0:
|
||||||
|
return True
|
||||||
|
if parsed.pre != version_pre:
|
||||||
|
return True
|
||||||
|
if parsed.post != version_post:
|
||||||
|
return True
|
||||||
|
return parsed.dev != version_dev
|
||||||
|
|
||||||
|
return above
|
||||||
|
|
||||||
|
|
||||||
|
def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
|
||||||
|
"""Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
|
||||||
|
|
||||||
|
Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
|
||||||
|
below the boundary when it is at or below V cmpkey-wise, or when
|
||||||
|
it is in V's local family.
|
||||||
|
"""
|
||||||
|
version_ge = version.__ge__
|
||||||
|
version_epoch = version.epoch
|
||||||
|
version_pre = version.pre
|
||||||
|
version_post = version.post
|
||||||
|
version_dev = version.dev
|
||||||
|
version_release_trimmed = trim_release(version.release)
|
||||||
|
trimmed_length = len(version_release_trimmed)
|
||||||
|
|
||||||
|
def below(parsed: Version) -> bool:
|
||||||
|
if version_ge(parsed):
|
||||||
|
return True
|
||||||
|
# parsed > V cmpkey-wise: below the boundary iff in V's local
|
||||||
|
# family.
|
||||||
|
if parsed.epoch != version_epoch:
|
||||||
|
return False
|
||||||
|
parsed_release = parsed.release
|
||||||
|
if len(parsed_release) < trimmed_length:
|
||||||
|
return False
|
||||||
|
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||||
|
return False
|
||||||
|
for i in range(trimmed_length, len(parsed_release)):
|
||||||
|
if parsed_release[i] != 0:
|
||||||
|
return False
|
||||||
|
if parsed.pre != version_pre:
|
||||||
|
return False
|
||||||
|
if parsed.post != version_post:
|
||||||
|
return False
|
||||||
|
return parsed.dev == version_dev
|
||||||
|
|
||||||
|
return below
|
||||||
|
|
||||||
|
|
||||||
|
def least_version_above(boundary: BoundaryVersion) -> Version | None:
|
||||||
|
"""Smallest real version strictly above *boundary*, or ``None`` if none exists."""
|
||||||
|
base = boundary.version
|
||||||
|
|
||||||
|
if boundary.kind == BoundaryKind.AFTER_LOCALS:
|
||||||
|
# AFTER_LOCALS(V) sits just below V.post0, so its least successor is
|
||||||
|
# V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
|
||||||
|
if base.dev is not None:
|
||||||
|
return base.__replace__(dev=base.dev + 1, local=None)
|
||||||
|
next_post = (base.post + 1) if base.post is not None else 0
|
||||||
|
return base.__replace__(post=next_post, dev=0, local=None)
|
||||||
|
|
||||||
|
# AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
|
||||||
|
# a final-release AFTER_POSTS has no least successor.
|
||||||
|
if base.pre is not None:
|
||||||
|
kind, number = base.pre
|
||||||
|
return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
|
||||||
|
"""True when the range defined by *lower* and *upper* contains no versions.
|
||||||
|
|
||||||
|
A boundary lower sits just below the next real version, so an ordered pair
|
||||||
|
is still empty when the upper excludes that least successor:
|
||||||
|
``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
|
||||||
|
"""
|
||||||
|
if upper.version is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if lower.version is None:
|
||||||
|
# Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
|
||||||
|
# leaves an empty floor interval such as ``(-inf, 0.dev0)``.
|
||||||
|
return (
|
||||||
|
not upper.inclusive
|
||||||
|
and isinstance(upper.version, Version)
|
||||||
|
and upper.version <= MIN_VERSION
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(lower.version, BoundaryVersion):
|
||||||
|
successor = least_version_above(lower.version)
|
||||||
|
if successor is not None:
|
||||||
|
if upper.version == successor:
|
||||||
|
return not upper.inclusive
|
||||||
|
return upper.version < successor
|
||||||
|
|
||||||
|
if lower.version == upper.version:
|
||||||
|
return not (lower.inclusive and upper.inclusive)
|
||||||
|
|
||||||
|
return lower.version > upper.version
|
||||||
|
|
||||||
|
|
||||||
|
def intersect_ranges(
|
||||||
|
left: Sequence[Interval],
|
||||||
|
right: Sequence[Interval],
|
||||||
|
) -> list[Interval]:
|
||||||
|
"""Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
|
||||||
|
result: list[Interval] = []
|
||||||
|
left_index = right_index = 0
|
||||||
|
while left_index < len(left) and right_index < len(right):
|
||||||
|
left_lower, left_upper = left[left_index]
|
||||||
|
right_lower, right_upper = right[right_index]
|
||||||
|
|
||||||
|
lower = max(left_lower, right_lower)
|
||||||
|
upper = min(left_upper, right_upper)
|
||||||
|
|
||||||
|
if not range_is_empty(lower, upper):
|
||||||
|
result.append((lower, upper))
|
||||||
|
|
||||||
|
# Advance whichever side has the smaller upper bound.
|
||||||
|
if left_upper < right_upper:
|
||||||
|
left_index += 1
|
||||||
|
else:
|
||||||
|
right_index += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_ranges(
|
||||||
|
ranges: Sequence[Interval],
|
||||||
|
iterable: Iterable[Any],
|
||||||
|
key: Callable[[Any], Version | str] | None,
|
||||||
|
prereleases: bool | None,
|
||||||
|
region: Sequence[Interval] = (),
|
||||||
|
) -> Iterator[Any]:
|
||||||
|
"""Filter *iterable* against precomputed version *ranges*.
|
||||||
|
|
||||||
|
With ``prereleases=None``, the PEP 440 default applies: pre-releases are
|
||||||
|
excluded unless no final matches, in which case buffered pre-releases come
|
||||||
|
out at the end. A pre-release inside the opt-in ``region`` is the exception:
|
||||||
|
it is force-admitted in place, as ``prereleases=True`` would yield it. A
|
||||||
|
force-admitted pre-release is not a final, so it never suppresses the buffer.
|
||||||
|
"""
|
||||||
|
if prereleases is None:
|
||||||
|
prerelease_buffer: list[Any] = []
|
||||||
|
found_final = False
|
||||||
|
|
||||||
|
if len(ranges) == 1:
|
||||||
|
# Hot path: most specifiers and small SpecifierSets reduce to
|
||||||
|
# a single contiguous range.
|
||||||
|
lower, upper = ranges[0]
|
||||||
|
above = lower._above
|
||||||
|
below = upper._below
|
||||||
|
for item in iterable:
|
||||||
|
parsed = coerce_version(item if key is None else key(item))
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
if above is not None and not above(parsed):
|
||||||
|
continue
|
||||||
|
if below is not None and not below(parsed):
|
||||||
|
continue
|
||||||
|
if not parsed.is_prerelease:
|
||||||
|
found_final = True
|
||||||
|
yield item
|
||||||
|
elif region and matches_bounds_only(region, parsed):
|
||||||
|
yield item
|
||||||
|
elif not found_final:
|
||||||
|
prerelease_buffer.append(item)
|
||||||
|
if not found_final:
|
||||||
|
yield from prerelease_buffer
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in iterable:
|
||||||
|
parsed = coerce_version(item if key is None else key(item))
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
for lower, upper in ranges:
|
||||||
|
above = lower._above
|
||||||
|
if above is not None and not above(parsed):
|
||||||
|
break
|
||||||
|
below = upper._below
|
||||||
|
if below is None or below(parsed):
|
||||||
|
if not parsed.is_prerelease:
|
||||||
|
found_final = True
|
||||||
|
yield item
|
||||||
|
elif region and matches_bounds_only(region, parsed):
|
||||||
|
yield item
|
||||||
|
elif not found_final:
|
||||||
|
prerelease_buffer.append(item)
|
||||||
|
break
|
||||||
|
if not found_final:
|
||||||
|
yield from prerelease_buffer
|
||||||
|
return
|
||||||
|
|
||||||
|
exclude_prereleases = prereleases is False
|
||||||
|
|
||||||
|
if len(ranges) == 1:
|
||||||
|
# Hot path: most specifiers and small SpecifierSets reduce to
|
||||||
|
# a single contiguous range.
|
||||||
|
lower, upper = ranges[0]
|
||||||
|
above = lower._above
|
||||||
|
below = upper._below
|
||||||
|
for item in iterable:
|
||||||
|
parsed = coerce_version(item if key is None else key(item))
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
if exclude_prereleases and parsed.is_prerelease:
|
||||||
|
continue
|
||||||
|
if above is not None and not above(parsed):
|
||||||
|
continue
|
||||||
|
if below is None or below(parsed):
|
||||||
|
yield item
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in iterable:
|
||||||
|
parsed = coerce_version(item if key is None else key(item))
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
if exclude_prereleases and parsed.is_prerelease:
|
||||||
|
continue
|
||||||
|
for lower, upper in ranges:
|
||||||
|
above = lower._above
|
||||||
|
if above is not None and not above(parsed):
|
||||||
|
break
|
||||||
|
below = upper._below
|
||||||
|
if below is None or below(parsed):
|
||||||
|
yield item
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _nearest_release_above_prerelease(version: Version) -> Version:
|
||||||
|
"""Smallest non-pre-release at or above a pre-release *version*."""
|
||||||
|
if version.pre is not None:
|
||||||
|
# An a/b/rc pre-release drops to its final release, which outranks
|
||||||
|
# every post-release of that pre-release (1.0a1.post0 -> 1.0).
|
||||||
|
return version.__replace__(pre=None, post=None, dev=None, local=None)
|
||||||
|
|
||||||
|
# A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
|
||||||
|
# whose final 1.0 sorts below it).
|
||||||
|
return version.__replace__(dev=None, local=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
|
||||||
|
"""Smallest non-pre-release version at or above *value*.
|
||||||
|
|
||||||
|
``None`` is the ``-inf`` floor, whose nearest non-pre-release is
|
||||||
|
:data:`MIN_RELEASE`.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return MIN_RELEASE
|
||||||
|
if isinstance(value, BoundaryVersion):
|
||||||
|
inner_version = value.version
|
||||||
|
if inner_version.is_prerelease:
|
||||||
|
return _nearest_release_above_prerelease(inner_version)
|
||||||
|
# AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
|
||||||
|
# AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
|
||||||
|
next_post = (inner_version.post + 1) if inner_version.post is not None else 0
|
||||||
|
return inner_version.__replace__(post=next_post, local=None)
|
||||||
|
|
||||||
|
if not value.is_prerelease:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return _nearest_release_above_prerelease(value)
|
||||||
|
|
||||||
|
|
||||||
|
def ranges_are_prerelease_only(ranges: Sequence[Interval]) -> bool:
|
||||||
|
"""True when every range in *ranges* contains only pre-releases.
|
||||||
|
|
||||||
|
Used to detect unsatisfiable specifier sets when ``prereleases=False``:
|
||||||
|
if every range is pre-release-only, every contained version is excluded.
|
||||||
|
"""
|
||||||
|
for lower, upper in ranges:
|
||||||
|
nearest = _lowest_release_at_or_above(lower.version)
|
||||||
|
if upper.version is None or nearest < upper.version:
|
||||||
|
return False
|
||||||
|
if nearest == upper.version and upper.inclusive:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def wildcard_ranges(op: str, base: Version) -> list[Interval]:
|
||||||
|
"""Ranges for ==V.* and !=V.*.
|
||||||
|
|
||||||
|
==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
|
||||||
|
"""
|
||||||
|
lower = _base_dev0(base)
|
||||||
|
upper = _next_prefix_dev0(base)
|
||||||
|
if op == "==":
|
||||||
|
return [(LowerBound(lower, True), UpperBound(upper, False))]
|
||||||
|
# !=
|
||||||
|
return [
|
||||||
|
(NEG_INF, UpperBound(lower, False)),
|
||||||
|
(LowerBound(upper, True), POS_INF),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def standard_ranges(op: str, version: Version, has_local: bool) -> list[Interval]:
|
||||||
|
"""Ranges for the standard PEP 440 operators (no wildcard, no ===).
|
||||||
|
|
||||||
|
*has_local* indicates whether the spec string included a ``+local``
|
||||||
|
segment; relevant only for ``==`` / ``!=`` to decide whether the
|
||||||
|
upper bound includes V's local family.
|
||||||
|
"""
|
||||||
|
if op == ">=":
|
||||||
|
return [(LowerBound(version, True), POS_INF)]
|
||||||
|
|
||||||
|
if op == "<=":
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
NEG_INF,
|
||||||
|
UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if op == ">":
|
||||||
|
if version.dev is not None:
|
||||||
|
# >V.devN: dev versions have no post-releases, so the
|
||||||
|
# next real version is V.dev(N+1).
|
||||||
|
lower_bound = version.__replace__(dev=version.dev + 1, local=None)
|
||||||
|
return [(LowerBound(lower_bound, True), POS_INF)]
|
||||||
|
if version.post is not None:
|
||||||
|
# >V.postN: next real version is V.post(N+1).dev0.
|
||||||
|
lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
|
||||||
|
return [(LowerBound(lower_bound, True), POS_INF)]
|
||||||
|
# >V (final or pre-release V): exclude V itself, V+local, and
|
||||||
|
# every V.postN per PEP 440.
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
|
||||||
|
POS_INF,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if op == "<":
|
||||||
|
# <V excludes pre-releases of V when V is not a pre-release.
|
||||||
|
# V.dev0 is the earliest pre-release of V.
|
||||||
|
bound = (
|
||||||
|
version if version.is_prerelease else version.__replace__(dev=0, local=None)
|
||||||
|
)
|
||||||
|
if bound <= MIN_VERSION:
|
||||||
|
return []
|
||||||
|
return [(NEG_INF, UpperBound(bound, False))]
|
||||||
|
|
||||||
|
# ==, !=: local versions of V match when the spec has no local segment.
|
||||||
|
after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
|
||||||
|
upper = version if has_local else after_locals
|
||||||
|
|
||||||
|
if op == "==":
|
||||||
|
return [(LowerBound(version, True), UpperBound(upper, True))]
|
||||||
|
|
||||||
|
if op == "!=":
|
||||||
|
return [
|
||||||
|
(NEG_INF, UpperBound(version, False)),
|
||||||
|
(LowerBound(upper, False), POS_INF),
|
||||||
|
]
|
||||||
|
|
||||||
|
if op == "~=":
|
||||||
|
prefix = version.__replace__(release=version.release[:-1])
|
||||||
|
return [
|
||||||
|
(LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
|
||||||
|
]
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
def bounds_for_spec(op: str, version_str: str, version: Version) -> list[Interval]:
|
||||||
|
"""Ranges for one specifier's ``(op, version_str)``.
|
||||||
|
|
||||||
|
Dispatches between the wildcard and standard builders. ``version`` is the
|
||||||
|
parsed ``version_str`` (its base, without the trailing ``.*``, for
|
||||||
|
wildcards). ``===`` is not handled here; its match is a literal string
|
||||||
|
compared in :mod:`packaging.specifiers`.
|
||||||
|
"""
|
||||||
|
if version_str.endswith(".*"):
|
||||||
|
return wildcard_ranges(op, version)
|
||||||
|
|
||||||
|
return standard_ranges(op, version, "+" in version_str)
|
||||||
|
|
||||||
|
|
||||||
|
def intersect_specifier_bounds(
|
||||||
|
per_specifier_ranges: Iterable[Sequence[Interval]],
|
||||||
|
) -> Sequence[Interval]:
|
||||||
|
"""Intersect each specifier's ranges into a single sequence.
|
||||||
|
|
||||||
|
Short-circuits once the running intersection is empty, since no later
|
||||||
|
specifier can revive it. Callers must pass at least one specifier.
|
||||||
|
"""
|
||||||
|
result: Sequence[Interval] | None = None
|
||||||
|
for sub in per_specifier_ranges:
|
||||||
|
if result is None:
|
||||||
|
result = sub
|
||||||
|
else:
|
||||||
|
result = intersect_ranges(result, sub)
|
||||||
|
if not result:
|
||||||
|
break
|
||||||
|
|
||||||
|
if result is None: # pragma: no cover - callers guard non-empty input
|
||||||
|
raise RuntimeError("intersect_specifier_bounds called with no specifiers")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def matches_bounds_only(ranges: Sequence[Interval], version: Version) -> bool:
|
||||||
|
"""Whether ``version`` falls within any of ``ranges``.
|
||||||
|
|
||||||
|
The pure bounds membership test, for a single already-parsed version with
|
||||||
|
no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
|
||||||
|
so a version below one range's lower bound is below every later range too.
|
||||||
|
"""
|
||||||
|
for lower, upper in ranges:
|
||||||
|
above = lower._above
|
||||||
|
if above is not None and not above(version):
|
||||||
|
return False
|
||||||
|
|
||||||
|
below = upper._below
|
||||||
|
if below is None or below(version):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_prereleases(
|
||||||
|
configured: bool | None, autodetected: bool | None
|
||||||
|
) -> bool | None:
|
||||||
|
"""Resolve a specifier's effective default pre-release policy.
|
||||||
|
|
||||||
|
An explicit ``configured`` value wins; otherwise an autodetected ``True``
|
||||||
|
propagates and anything else falls back to the PEP 440 default (``None``).
|
||||||
|
"""
|
||||||
|
if configured is not None:
|
||||||
|
return configured
|
||||||
|
|
||||||
|
if autodetected:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This file is dual licensed under the terms of the Apache License, Version
|
||||||
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
|
# for complete details.
|
||||||
|
|
||||||
|
"""Backward-compatibility shim for unpickling Version objects serialized before
|
||||||
|
packaging 26.1.
|
||||||
|
|
||||||
|
Old pickles reference ``packaging._structures.InfinityType`` and
|
||||||
|
``packaging._structures.NegativeInfinityType``. This module provides minimal
|
||||||
|
stand-in classes so that ``pickle.loads()`` can resolve those references.
|
||||||
|
The deserialized objects are not used for comparisons — ``Version.__setstate__``
|
||||||
|
discards the stale ``_key`` cache and recomputes it from the core version fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class InfinityType:
|
||||||
|
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "Infinity"
|
||||||
|
|
||||||
|
|
||||||
|
class NegativeInfinityType:
|
||||||
|
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "-Infinity"
|
||||||
|
|
||||||
|
|
||||||
|
Infinity = InfinityType()
|
||||||
|
NegativeInfinity = NegativeInfinityType()
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, NoReturn
|
||||||
|
|
||||||
|
from .specifiers import Specifier
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Generator, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Token:
|
||||||
|
__slots__ = ("name", "position", "text")
|
||||||
|
|
||||||
|
name: str
|
||||||
|
text: str
|
||||||
|
position: int
|
||||||
|
|
||||||
|
|
||||||
|
class ParserSyntaxError(Exception):
|
||||||
|
"""The provided source text could not be parsed correctly."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
span: tuple[int, int],
|
||||||
|
) -> None:
|
||||||
|
self.span = span
|
||||||
|
self.message = message
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
|
||||||
|
return f"{self.message}\n {self.source}\n {marker}"
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
|
||||||
|
"LEFT_PARENTHESIS": re.compile(r"\("),
|
||||||
|
"RIGHT_PARENTHESIS": re.compile(r"\)"),
|
||||||
|
"LEFT_BRACKET": re.compile(r"\["),
|
||||||
|
"RIGHT_BRACKET": re.compile(r"\]"),
|
||||||
|
"SEMICOLON": re.compile(r";"),
|
||||||
|
"COMMA": re.compile(r","),
|
||||||
|
"QUOTED_STRING": re.compile(
|
||||||
|
r"""
|
||||||
|
(
|
||||||
|
('[^']*')
|
||||||
|
|
|
||||||
|
("[^"]*")
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
),
|
||||||
|
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
|
||||||
|
"BOOLOP": re.compile(r"\b(or|and)\b"),
|
||||||
|
"IN": re.compile(r"\bin\b"),
|
||||||
|
"NOT": re.compile(r"\bnot\b"),
|
||||||
|
"VARIABLE": re.compile(
|
||||||
|
r"""
|
||||||
|
\b(
|
||||||
|
python_version
|
||||||
|
|python_full_version
|
||||||
|
|os[._]name
|
||||||
|
|sys[._]platform
|
||||||
|
|platform_(release|system)
|
||||||
|
|platform[._](version|machine|python_implementation)
|
||||||
|
|python_implementation
|
||||||
|
|implementation_(name|version)
|
||||||
|
|extras?
|
||||||
|
|dependency_groups
|
||||||
|
)\b
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
),
|
||||||
|
"SPECIFIER": re.compile(
|
||||||
|
Specifier._specifier_regex_str,
|
||||||
|
re.VERBOSE | re.IGNORECASE,
|
||||||
|
),
|
||||||
|
"AT": re.compile(r"\@"),
|
||||||
|
"URL": re.compile(r"[^ \t]+"),
|
||||||
|
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
|
||||||
|
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
|
||||||
|
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
|
||||||
|
"WS": re.compile(r"[ \t]+"),
|
||||||
|
"END": re.compile(r"\Z"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Tokenizer:
|
||||||
|
"""Context-sensitive token parsing.
|
||||||
|
|
||||||
|
Provides methods to examine the input stream to check whether the next token
|
||||||
|
matches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("next_token", "position", "rules", "source")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
*,
|
||||||
|
rules: Mapping[str, re.Pattern[str]],
|
||||||
|
) -> None:
|
||||||
|
self.source = source
|
||||||
|
self.rules = rules
|
||||||
|
self.next_token: Token | None = None
|
||||||
|
self.position = 0
|
||||||
|
|
||||||
|
def consume(self, name: str) -> None:
|
||||||
|
"""Move beyond provided token name, if at current position."""
|
||||||
|
if self.check(name):
|
||||||
|
self.read()
|
||||||
|
|
||||||
|
def check(self, name: str, *, peek: bool = False) -> bool:
|
||||||
|
"""Check whether the next token has the provided name.
|
||||||
|
|
||||||
|
By default, if the check succeeds, the token *must* be read before
|
||||||
|
another check. If `peek` is set to `True`, the token is not loaded and
|
||||||
|
would need to be checked again.
|
||||||
|
"""
|
||||||
|
assert self.next_token is None, (
|
||||||
|
f"Cannot check for {name!r}, already have {self.next_token!r}"
|
||||||
|
)
|
||||||
|
assert name in self.rules, f"Unknown token name: {name!r}"
|
||||||
|
|
||||||
|
expression = self.rules[name]
|
||||||
|
|
||||||
|
match = expression.match(self.source, self.position)
|
||||||
|
if match is None:
|
||||||
|
return False
|
||||||
|
if not peek:
|
||||||
|
self.next_token = Token(name, match[0], self.position)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def expect(self, name: str, *, expected: str) -> Token:
|
||||||
|
"""Expect a certain token name next, failing with a syntax error otherwise.
|
||||||
|
|
||||||
|
The token is read and returned.
|
||||||
|
"""
|
||||||
|
if not self.check(name):
|
||||||
|
raise self.raise_syntax_error(f"Expected {expected}")
|
||||||
|
return self.read()
|
||||||
|
|
||||||
|
def read(self) -> Token:
|
||||||
|
"""Consume the next token and return it."""
|
||||||
|
token = self.next_token
|
||||||
|
assert token is not None
|
||||||
|
|
||||||
|
self.position += len(token.text)
|
||||||
|
self.next_token = None
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
def raise_syntax_error(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
span_start: int | None = None,
|
||||||
|
span_end: int | None = None,
|
||||||
|
) -> NoReturn:
|
||||||
|
"""Raise ParserSyntaxError at the given position."""
|
||||||
|
span = (
|
||||||
|
self.position if span_start is None else span_start,
|
||||||
|
self.position if span_end is None else span_end,
|
||||||
|
)
|
||||||
|
raise ParserSyntaxError(
|
||||||
|
message,
|
||||||
|
source=self.source,
|
||||||
|
span=span,
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def enclosing_tokens(
|
||||||
|
self, open_token: str, close_token: str, *, around: str
|
||||||
|
) -> Generator[None, None, None]:
|
||||||
|
if self.check(open_token):
|
||||||
|
open_position = self.position
|
||||||
|
self.read()
|
||||||
|
else:
|
||||||
|
open_position = None
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
if open_position is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.check(close_token):
|
||||||
|
self.raise_syntax_error(
|
||||||
|
f"Expected matching {close_token} for {open_token}, after {around}",
|
||||||
|
span_start=open_position,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.read()
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
|
||||||
|
from .errors import _ErrorCollector
|
||||||
|
from .requirements import InvalidRequirement, Requirement
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CyclicDependencyGroup",
|
||||||
|
"DependencyGroupInclude",
|
||||||
|
"DependencyGroupResolver",
|
||||||
|
"DuplicateGroupNames",
|
||||||
|
"InvalidDependencyGroupObject",
|
||||||
|
"resolve_dependency_groups",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
# -----------
|
||||||
|
# Error Types
|
||||||
|
# -----------
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateGroupNames(ValueError):
|
||||||
|
"""
|
||||||
|
The same dependency groups were defined twice, with different non-normalized names.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CyclicDependencyGroup(ValueError):
|
||||||
|
"""
|
||||||
|
The dependency group includes form a cycle.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
|
||||||
|
self.requested_group = requested_group
|
||||||
|
self.group = group
|
||||||
|
self.include_group = include_group
|
||||||
|
|
||||||
|
if include_group == group:
|
||||||
|
reason = f"{group} includes itself"
|
||||||
|
else:
|
||||||
|
reason = f"{include_group} -> {group}, {group} -> {include_group}"
|
||||||
|
super().__init__(
|
||||||
|
"Cyclic dependency group include while resolving "
|
||||||
|
f"{requested_group}: {reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Support pickling; ``args`` does not match ``__init__``'s signature.
|
||||||
|
def __reduce__(self) -> tuple[type[CyclicDependencyGroup], tuple[str, str, str]]:
|
||||||
|
return (self.__class__, (self.requested_group, self.group, self.include_group))
|
||||||
|
|
||||||
|
|
||||||
|
# in the PEP 735 spec, the tables in dependency group lists were described as
|
||||||
|
# "Dependency Object Specifiers", but the only defined type of object was a
|
||||||
|
# "Dependency Group Include" -- hence the naming of this error as "Object"
|
||||||
|
class InvalidDependencyGroupObject(ValueError):
|
||||||
|
"""
|
||||||
|
A member of a dependency group was identified as a dict, but was not in a valid
|
||||||
|
format.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Object Model & Interface
|
||||||
|
# ------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyGroupInclude:
|
||||||
|
"""
|
||||||
|
A reference to another dependency group by name.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("include_group",)
|
||||||
|
|
||||||
|
def __init__(self, include_group: str) -> None:
|
||||||
|
"""
|
||||||
|
Initialize a DependencyGroupInclude.
|
||||||
|
|
||||||
|
:param include_group: The name of the group referred to by this include.
|
||||||
|
"""
|
||||||
|
self.include_group = include_group
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({self.include_group!r})"
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyGroupResolver:
|
||||||
|
"""
|
||||||
|
A resolver for Dependency Group data.
|
||||||
|
|
||||||
|
This class handles caching, name normalization, cycle detection, and other
|
||||||
|
parsing requirements. There are only two public methods for exploring the data:
|
||||||
|
``lookup()`` and ``resolve()``.
|
||||||
|
|
||||||
|
:param dependency_groups: A mapping, as provided via pyproject
|
||||||
|
``[dependency-groups]``.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
|
||||||
|
) -> None:
|
||||||
|
errors = _ErrorCollector()
|
||||||
|
|
||||||
|
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
|
||||||
|
|
||||||
|
# a map of group names to parsed data
|
||||||
|
self._parsed_groups: dict[
|
||||||
|
str, tuple[Requirement | DependencyGroupInclude, ...]
|
||||||
|
] = {}
|
||||||
|
# a map of group names to their ancestors, used for cycle detection
|
||||||
|
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
|
||||||
|
# a cache of completed resolutions to Requirement lists
|
||||||
|
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
|
||||||
|
|
||||||
|
errors.finalize("[dependency-groups] data was invalid")
|
||||||
|
|
||||||
|
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
|
||||||
|
"""
|
||||||
|
Lookup a group name, returning the parsed dependency data for that group.
|
||||||
|
This will not resolve includes.
|
||||||
|
|
||||||
|
:param group: the name of the group to lookup
|
||||||
|
"""
|
||||||
|
group = _normalize_name(group)
|
||||||
|
|
||||||
|
with _ErrorCollector().on_exit(
|
||||||
|
f"[dependency-groups] data for {group!r} was malformed"
|
||||||
|
) as errors:
|
||||||
|
return self._parse_group(group, errors)
|
||||||
|
|
||||||
|
def resolve(self, group: str) -> tuple[Requirement, ...]:
|
||||||
|
"""
|
||||||
|
Resolve a dependency group to a list of requirements.
|
||||||
|
|
||||||
|
:param group: the name of the group to resolve
|
||||||
|
"""
|
||||||
|
group = _normalize_name(group)
|
||||||
|
|
||||||
|
with _ErrorCollector().on_exit(
|
||||||
|
f"[dependency-groups] data for {group!r} was malformed"
|
||||||
|
) as errors:
|
||||||
|
return self._resolve(group, group, errors)
|
||||||
|
|
||||||
|
def _resolve(
|
||||||
|
self, group: str, requested_group: str, errors: _ErrorCollector
|
||||||
|
) -> tuple[Requirement, ...]:
|
||||||
|
"""
|
||||||
|
This is a helper for cached resolution to strings. It preserves the name of the
|
||||||
|
group which the user initially requested in order to present a clearer error in
|
||||||
|
the event that a cycle is detected.
|
||||||
|
|
||||||
|
:param group: The normalized name of the group to resolve.
|
||||||
|
:param requested_group: The group which was used in the original, user-facing
|
||||||
|
request.
|
||||||
|
"""
|
||||||
|
if group in self._resolve_cache:
|
||||||
|
return self._resolve_cache[group]
|
||||||
|
|
||||||
|
parsed = self._parse_group(group, errors)
|
||||||
|
|
||||||
|
resolved_group = []
|
||||||
|
|
||||||
|
for item in parsed:
|
||||||
|
if isinstance(item, Requirement):
|
||||||
|
resolved_group.append(item)
|
||||||
|
elif isinstance(item, DependencyGroupInclude):
|
||||||
|
include_group = _normalize_name(item.include_group)
|
||||||
|
|
||||||
|
# if a group is cyclic, record the error
|
||||||
|
# otherwise, follow the include_group reference
|
||||||
|
#
|
||||||
|
# this allows us to examine all includes in a group, even in the
|
||||||
|
# presence of errors
|
||||||
|
if include_group in self._include_graph_ancestors.get(group, ()):
|
||||||
|
errors.error(
|
||||||
|
CyclicDependencyGroup(
|
||||||
|
requested_group, group, item.include_group
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._include_graph_ancestors[include_group] = (
|
||||||
|
*self._include_graph_ancestors.get(group, ()),
|
||||||
|
group,
|
||||||
|
)
|
||||||
|
resolved_group.extend(
|
||||||
|
self._resolve(include_group, requested_group, errors)
|
||||||
|
)
|
||||||
|
else: # pragma: no cover
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Invalid dependency group item after parse: {item}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# in the event that errors were detected, present the group as empty and do not
|
||||||
|
# cache the result
|
||||||
|
# this ensures that repeated access to a cyclic group will raise multiple errors
|
||||||
|
if errors.errors:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
self._resolve_cache[group] = tuple(resolved_group)
|
||||||
|
return self._resolve_cache[group]
|
||||||
|
|
||||||
|
def _parse_group(
|
||||||
|
self, group: str, errors: _ErrorCollector
|
||||||
|
) -> tuple[Requirement | DependencyGroupInclude, ...]:
|
||||||
|
# short circuit -- never do the work twice
|
||||||
|
if group in self._parsed_groups:
|
||||||
|
return self._parsed_groups[group]
|
||||||
|
|
||||||
|
if group not in self.dependency_groups:
|
||||||
|
errors.error(LookupError(f"Dependency group '{group}' not found"))
|
||||||
|
return ()
|
||||||
|
|
||||||
|
raw_group = self.dependency_groups[group]
|
||||||
|
if isinstance(raw_group, str):
|
||||||
|
errors.error(
|
||||||
|
TypeError(
|
||||||
|
f"Dependency group {group!r} contained a string rather than a list."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
if not isinstance(raw_group, Sequence):
|
||||||
|
errors.error(
|
||||||
|
TypeError(f"Dependency group {group!r} is not a sequence type.")
|
||||||
|
)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
elements: list[Requirement | DependencyGroupInclude] = []
|
||||||
|
for item in raw_group:
|
||||||
|
if isinstance(item, str):
|
||||||
|
# packaging.requirements.Requirement parsing ensures that this is a
|
||||||
|
# valid PEP 508 Dependency Specifier. Collect InvalidRequirement
|
||||||
|
# if it throws that.
|
||||||
|
with errors.collect(InvalidRequirement):
|
||||||
|
elements.append(Requirement(item))
|
||||||
|
elif isinstance(item, Mapping):
|
||||||
|
if tuple(item.keys()) != ("include-group",):
|
||||||
|
errors.error(
|
||||||
|
InvalidDependencyGroupObject(
|
||||||
|
f"Invalid dependency group item: {item!r}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
include_group = item["include-group"]
|
||||||
|
if not isinstance(include_group, str):
|
||||||
|
msg = (
|
||||||
|
"Dependency group include-group value is not a string: "
|
||||||
|
f"{item!r}"
|
||||||
|
)
|
||||||
|
errors.error(TypeError(msg))
|
||||||
|
else:
|
||||||
|
elements.append(
|
||||||
|
DependencyGroupInclude(include_group=include_group)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
|
||||||
|
|
||||||
|
if errors.errors:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
self._parsed_groups[group] = tuple(elements)
|
||||||
|
return self._parsed_groups[group]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# Functional Interface
|
||||||
|
# --------------------
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dependency_groups(
|
||||||
|
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
"""
|
||||||
|
Resolve a dependency group to a tuple of requirements, as strings.
|
||||||
|
|
||||||
|
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
|
||||||
|
from ``pyproject.toml``
|
||||||
|
:param groups: the name of the group(s) to resolve
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
resolver = DependencyGroupResolver(dependency_groups)
|
||||||
|
return tuple(str(r) for group in groups for r in resolver.resolve(group))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------
|
||||||
|
# internal helpers
|
||||||
|
# ----------------
|
||||||
|
|
||||||
|
|
||||||
|
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_name(name: str) -> str:
|
||||||
|
return _NORMALIZE_PATTERN.sub("-", name).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_group_names(
|
||||||
|
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
|
||||||
|
errors: _ErrorCollector,
|
||||||
|
) -> dict[str, Sequence[str | Mapping[str, str]]]:
|
||||||
|
original_names: dict[str, list[str]] = {}
|
||||||
|
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
|
||||||
|
|
||||||
|
for group_name, value in dependency_groups.items():
|
||||||
|
normed_group_name = _normalize_name(group_name)
|
||||||
|
original_names.setdefault(normed_group_name, []).append(group_name)
|
||||||
|
normalized_groups[normed_group_name] = value
|
||||||
|
|
||||||
|
for normed_name, names in original_names.items():
|
||||||
|
if len(names) > 1:
|
||||||
|
errors.error(
|
||||||
|
DuplicateGroupNames(
|
||||||
|
"Duplicate dependency group names: "
|
||||||
|
f"{normed_name} ({', '.join(names)})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return normalized_groups
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
import sys
|
||||||
|
from collections.abc import Collection
|
||||||
|
from urllib.parse import SplitResult
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
from typing import Self
|
||||||
|
else:
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ArchiveInfo",
|
||||||
|
"DirInfo",
|
||||||
|
"DirectUrl",
|
||||||
|
"DirectUrlValidationError",
|
||||||
|
"VcsInfo",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
class _FromMappingProtocol(Protocol): # pragma: no cover
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
|
||||||
|
|
||||||
|
|
||||||
|
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
|
||||||
|
|
||||||
|
|
||||||
|
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||||
|
return {key: value for key, value in data if value is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
|
||||||
|
"""Get a value from the dictionary and verify it's the expected type."""
|
||||||
|
if (value := d.get(key)) is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(value, expected_type):
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
f"Unexpected type {type(value).__name__} "
|
||||||
|
f"(expected {expected_type.__name__})",
|
||||||
|
context=key,
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
|
||||||
|
"""Get a required value from the dictionary and verify it's the expected type."""
|
||||||
|
if (value := _get(d, expected_type, key)) is None:
|
||||||
|
raise _DirectUrlRequiredKeyError(key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_object(
|
||||||
|
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
|
||||||
|
) -> _FromMappingProtocolT | None:
|
||||||
|
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
|
||||||
|
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return target_type._from_dict(value)
|
||||||
|
except Exception as e:
|
||||||
|
raise DirectUrlValidationError(e, context=key) from e
|
||||||
|
|
||||||
|
|
||||||
|
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
|
||||||
|
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
|
||||||
|
if "@" not in netloc:
|
||||||
|
return netloc
|
||||||
|
user_pass, netloc_no_user_pass = netloc.rsplit("@", 1)
|
||||||
|
if user_pass in safe_user_passwords:
|
||||||
|
return netloc
|
||||||
|
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
|
||||||
|
return netloc
|
||||||
|
return netloc_no_user_pass
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
|
||||||
|
"""url with user:password part removed unless it is formed with
|
||||||
|
environment variables as specified in PEP 610, or it is a safe user:password
|
||||||
|
such as `git`.
|
||||||
|
"""
|
||||||
|
parsed_url = urllib.parse.urlsplit(url)
|
||||||
|
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
|
||||||
|
return urllib.parse.urlunsplit(
|
||||||
|
(
|
||||||
|
parsed_url.scheme,
|
||||||
|
netloc,
|
||||||
|
parsed_url.path,
|
||||||
|
parsed_url.query,
|
||||||
|
parsed_url.fragment,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _file_url_has_absolute_path(parsed_url: SplitResult) -> bool:
|
||||||
|
return parsed_url.path.startswith("/")
|
||||||
|
|
||||||
|
|
||||||
|
class DirectUrlValidationError(Exception):
|
||||||
|
"""Raised when when input data is not spec-compliant.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
context: str | None = None
|
||||||
|
message: str
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cause: str | Exception,
|
||||||
|
*,
|
||||||
|
context: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
if isinstance(cause, DirectUrlValidationError):
|
||||||
|
if cause.context:
|
||||||
|
self.context = (
|
||||||
|
f"{context}.{cause.context}" if context else cause.context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.context = context # pragma: no cover
|
||||||
|
self.message = cause.message
|
||||||
|
else:
|
||||||
|
self.context = context
|
||||||
|
self.message = str(cause)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.context:
|
||||||
|
return f"{self.message} in {self.context!r}"
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
|
||||||
|
def __init__(self, key: str) -> None:
|
||||||
|
super().__init__("Missing required value", context=key)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True, init=False)
|
||||||
|
class VcsInfo:
|
||||||
|
"""The version control information of a :class:`DirectUrl`."""
|
||||||
|
|
||||||
|
vcs: str
|
||||||
|
commit_id: str
|
||||||
|
requested_revision: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
vcs: str,
|
||||||
|
commit_id: str,
|
||||||
|
requested_revision: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
object.__setattr__(self, "vcs", vcs)
|
||||||
|
object.__setattr__(self, "commit_id", commit_id)
|
||||||
|
object.__setattr__(self, "requested_revision", requested_revision)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
# We can't validate vcs value because is not closed.
|
||||||
|
return cls(
|
||||||
|
vcs=_get_required(d, str, "vcs"),
|
||||||
|
requested_revision=_get(d, str, "requested_revision"),
|
||||||
|
commit_id=_get_required(d, str, "commit_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True, init=False)
|
||||||
|
class ArchiveInfo:
|
||||||
|
"""The archive information of a :class:`DirectUrl`."""
|
||||||
|
|
||||||
|
hashes: Mapping[str, str] | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
hashes: Mapping[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
|
||||||
|
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
"Hash values must be strings", context="hashes"
|
||||||
|
)
|
||||||
|
legacy_hash = _get(d, str, "hash")
|
||||||
|
if legacy_hash is not None:
|
||||||
|
if "=" not in legacy_hash:
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
"Invalid hash format (expected '<algorithm>=<hash>')",
|
||||||
|
context="hash",
|
||||||
|
)
|
||||||
|
hash_algorithm, hash_value = legacy_hash.split("=", 1)
|
||||||
|
if hashes is None:
|
||||||
|
# if `hashes` are not present, we can derive it from the legacy `hash`
|
||||||
|
hashes = {hash_algorithm: hash_value}
|
||||||
|
else:
|
||||||
|
# if `hashes` are present, the legacy `hash` must match one of them
|
||||||
|
if hash_algorithm not in hashes:
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
f"Algorithm {hash_algorithm!r} used in hash field "
|
||||||
|
f"is not present in hashes field",
|
||||||
|
context="hashes",
|
||||||
|
)
|
||||||
|
if hashes[hash_algorithm] != hash_value:
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
f"Algorithm {hash_algorithm!r} used in hash field "
|
||||||
|
f"has different value in hashes field",
|
||||||
|
context="hash",
|
||||||
|
)
|
||||||
|
return cls(hashes=hashes)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True, init=False)
|
||||||
|
class DirInfo:
|
||||||
|
"""The local directory information of a :class:`DirectUrl`."""
|
||||||
|
|
||||||
|
editable: bool | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
editable: bool | None = None,
|
||||||
|
) -> None:
|
||||||
|
object.__setattr__(self, "editable", editable)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
editable=_get(d, bool, "editable"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True, init=False)
|
||||||
|
class DirectUrl:
|
||||||
|
"""A class representing a direct URL.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
url: str
|
||||||
|
archive_info: ArchiveInfo | None = None
|
||||||
|
vcs_info: VcsInfo | None = None
|
||||||
|
dir_info: DirInfo | None = None
|
||||||
|
subdirectory: str | None = None # XXX Path or str?
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
url: str,
|
||||||
|
archive_info: ArchiveInfo | None = None,
|
||||||
|
vcs_info: VcsInfo | None = None,
|
||||||
|
dir_info: DirInfo | None = None,
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "archive_info", archive_info)
|
||||||
|
object.__setattr__(self, "vcs_info", vcs_info)
|
||||||
|
object.__setattr__(self, "dir_info", dir_info)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
direct_url = cls(
|
||||||
|
url=_get_required(d, str, "url"),
|
||||||
|
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
|
||||||
|
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
|
||||||
|
dir_info=_get_object(d, DirInfo, "dir_info"),
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
bool(direct_url.vcs_info)
|
||||||
|
+ bool(direct_url.archive_info)
|
||||||
|
+ bool(direct_url.dir_info)
|
||||||
|
) != 1:
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
"Exactly one of vcs_info, archive_info, dir_info must be present"
|
||||||
|
)
|
||||||
|
if direct_url.dir_info is not None:
|
||||||
|
parsed_url = urllib.parse.urlsplit(direct_url.url)
|
||||||
|
if parsed_url.scheme != "file":
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
"URL scheme must be file:// when dir_info is present",
|
||||||
|
context="url",
|
||||||
|
)
|
||||||
|
if not _file_url_has_absolute_path(parsed_url):
|
||||||
|
raise DirectUrlValidationError(
|
||||||
|
"File URL must be absolute when dir_info is present",
|
||||||
|
context="url",
|
||||||
|
)
|
||||||
|
# XXX subdirectory must be relative, can we, should we validate that here?
|
||||||
|
return direct_url
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
|
||||||
|
"""Create and validate a DirectUrl instance from a JSON dictionary."""
|
||||||
|
return cls._from_dict(d)
|
||||||
|
|
||||||
|
def to_dict(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
generate_legacy_hash: bool = False,
|
||||||
|
strip_user_password: bool = True,
|
||||||
|
safe_user_passwords: Collection[str] = ("git",),
|
||||||
|
) -> Mapping[str, Any]:
|
||||||
|
"""Convert the DirectUrl instance to a JSON dictionary.
|
||||||
|
|
||||||
|
:param generate_legacy_hash: If True, include a legacy `hash` field in
|
||||||
|
`archive_info` for backward compatibility with tools that don't
|
||||||
|
support the `hashes` field.
|
||||||
|
:param strip_user_password: If True, strip user:password from the URL
|
||||||
|
unless it is formed with environment variables as specified in PEP
|
||||||
|
610, or it is a safe user:password such as `git`.
|
||||||
|
:param safe_user_passwords: A collection of user:password strings that
|
||||||
|
should not be stripped from the URL even if `strip_user_password` is
|
||||||
|
True.
|
||||||
|
"""
|
||||||
|
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
|
||||||
|
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
|
||||||
|
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
|
||||||
|
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
|
||||||
|
if strip_user_password:
|
||||||
|
res["url"] = _strip_url(self.url, safe_user_passwords)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
"""Validate the DirectUrl instance against the specification.
|
||||||
|
|
||||||
|
Raises :class:`DirectUrlValidationError` if invalid.
|
||||||
|
"""
|
||||||
|
self.from_dict(self.to_dict())
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import dataclasses
|
||||||
|
import sys
|
||||||
|
import typing
|
||||||
|
|
||||||
|
__all__ = ["ExceptionGroup"]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11): # pragma: no cover
|
||||||
|
from builtins import ExceptionGroup
|
||||||
|
else: # pragma: no cover
|
||||||
|
|
||||||
|
class ExceptionGroup(Exception):
|
||||||
|
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
|
||||||
|
|
||||||
|
If :external:exc:`ExceptionGroup` is already defined by Python itself,
|
||||||
|
that version is used instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
message: str
|
||||||
|
exceptions: list[Exception]
|
||||||
|
|
||||||
|
def __init__(self, message: str, exceptions: list[Exception]) -> None:
|
||||||
|
self.message = message
|
||||||
|
self.exceptions = exceptions
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class _ErrorCollector:
|
||||||
|
"""
|
||||||
|
Collect errors into ExceptionGroups.
|
||||||
|
|
||||||
|
Used like this:
|
||||||
|
|
||||||
|
collector = _ErrorCollector()
|
||||||
|
# Add a single exception
|
||||||
|
collector.error(ValueError("one"))
|
||||||
|
|
||||||
|
# Supports nesting, including combining ExceptionGroups
|
||||||
|
with collector.collect():
|
||||||
|
raise ValueError("two")
|
||||||
|
collector.finalize("Found some errors")
|
||||||
|
|
||||||
|
Since making a collector and then calling finalize later is a common pattern,
|
||||||
|
a convenience method ``on_exit`` is provided.
|
||||||
|
"""
|
||||||
|
|
||||||
|
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
|
||||||
|
|
||||||
|
def finalize(self, msg: str) -> None:
|
||||||
|
"""Raise a group exception if there are any errors."""
|
||||||
|
if self.errors:
|
||||||
|
raise ExceptionGroup(msg, self.errors)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
|
||||||
|
"""
|
||||||
|
Calls finalize if no uncollected errors were present.
|
||||||
|
|
||||||
|
Uncollected errors are raised normally.
|
||||||
|
"""
|
||||||
|
yield self
|
||||||
|
self.finalize(msg)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
|
||||||
|
"""
|
||||||
|
Context manager to collect errors into the error list.
|
||||||
|
|
||||||
|
Must be inside loops, as only one error can be collected at a time.
|
||||||
|
"""
|
||||||
|
error_classes = err_cls or (Exception,)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except ExceptionGroup as error:
|
||||||
|
self.errors.extend(error.exceptions)
|
||||||
|
except error_classes as error:
|
||||||
|
self.errors.append(error)
|
||||||
|
|
||||||
|
def error(
|
||||||
|
self,
|
||||||
|
error: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Add an error to the list."""
|
||||||
|
self.errors.append(error)
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#######################################################################################
|
||||||
|
#
|
||||||
|
# Adapted from:
|
||||||
|
# https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py
|
||||||
|
#
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
# software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
# without restriction, including without limitation the rights to use, copy, modify,
|
||||||
|
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to the following
|
||||||
|
# conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all copies
|
||||||
|
# or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
|
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# With additional allowance of arbitrary `LicenseRef-` identifiers, not just
|
||||||
|
# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.
|
||||||
|
#
|
||||||
|
#######################################################################################
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import NewType, cast
|
||||||
|
|
||||||
|
from ._spdx import EXCEPTIONS, LICENSES
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"InvalidLicenseExpression",
|
||||||
|
"NormalizedLicenseExpression",
|
||||||
|
"canonicalize_license_expression",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Simple __dir__ implementation since there are no public submodules
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
license_ref_allowed = re.compile("^[A-Za-z0-9.-]+$")
|
||||||
|
|
||||||
|
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
|
||||||
|
"""
|
||||||
|
A :class:`typing.NewType` of :class:`str`, representing a normalized
|
||||||
|
License-Expression.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidLicenseExpression(ValueError):
|
||||||
|
"""Raised when a license-expression string is invalid
|
||||||
|
|
||||||
|
>>> from packaging.licenses import canonicalize_license_expression
|
||||||
|
>>> canonicalize_license_expression("invalid")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
packaging.licenses.InvalidLicenseExpression: Unknown license: 'invalid'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_license_expression(
|
||||||
|
raw_license_expression: str,
|
||||||
|
) -> NormalizedLicenseExpression:
|
||||||
|
"""
|
||||||
|
This function takes a valid License-Expression, and returns the normalized
|
||||||
|
form of it.
|
||||||
|
|
||||||
|
The return type is typed as :class:`NormalizedLicenseExpression`. This
|
||||||
|
allows type checkers to help require that a string has passed through this
|
||||||
|
function before use.
|
||||||
|
|
||||||
|
:param str raw_license_expression: The License-Expression to canonicalize.
|
||||||
|
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
|
||||||
|
invalid/unknown license identifier or invalid syntax.
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from packaging.licenses import canonicalize_license_expression
|
||||||
|
>>> canonicalize_license_expression("mit")
|
||||||
|
'MIT'
|
||||||
|
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
|
||||||
|
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
|
||||||
|
>>> canonicalize_license_expression("(mit")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
InvalidLicenseExpression: Invalid license expression: '(mit'
|
||||||
|
>>> canonicalize_license_expression("Use-it-after-midnight")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
|
||||||
|
"""
|
||||||
|
if not raw_license_expression:
|
||||||
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
|
raise InvalidLicenseExpression(message)
|
||||||
|
|
||||||
|
# Pad any parentheses so tokenization can be achieved by merely splitting on
|
||||||
|
# whitespace.
|
||||||
|
license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")
|
||||||
|
licenseref_prefix = "LicenseRef-"
|
||||||
|
license_refs = {
|
||||||
|
ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]
|
||||||
|
for ref in license_expression.split()
|
||||||
|
if ref.lower().startswith(licenseref_prefix.lower())
|
||||||
|
}
|
||||||
|
|
||||||
|
# Normalize to lower case so we can look up licenses/exceptions
|
||||||
|
# and so boolean operators are Python-compatible.
|
||||||
|
license_expression = license_expression.lower()
|
||||||
|
|
||||||
|
tokens = license_expression.split()
|
||||||
|
|
||||||
|
# Rather than implementing a parenthesis/boolean logic parser, create an
|
||||||
|
# expression that Python can parse. Everything that is not involved with the
|
||||||
|
# grammar itself is replaced with the placeholder `False` and the resultant
|
||||||
|
# expression should become a valid Python expression.
|
||||||
|
python_tokens = []
|
||||||
|
for token in tokens:
|
||||||
|
if token not in {"or", "and", "with", "(", ")"}:
|
||||||
|
python_tokens.append("False")
|
||||||
|
elif token == "with":
|
||||||
|
python_tokens.append("or")
|
||||||
|
elif (
|
||||||
|
token == "("
|
||||||
|
and python_tokens
|
||||||
|
and python_tokens[-1] not in {"or", "and", "("}
|
||||||
|
) or (token == ")" and python_tokens and python_tokens[-1] == "("):
|
||||||
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
|
raise InvalidLicenseExpression(message)
|
||||||
|
else:
|
||||||
|
python_tokens.append(token)
|
||||||
|
|
||||||
|
python_expression = " ".join(python_tokens)
|
||||||
|
try:
|
||||||
|
compile(python_expression, "", "eval")
|
||||||
|
except SyntaxError:
|
||||||
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
|
raise InvalidLicenseExpression(message) from None
|
||||||
|
|
||||||
|
# Take a final pass to check for unknown licenses/exceptions.
|
||||||
|
normalized_tokens = []
|
||||||
|
last_license_start = False
|
||||||
|
for index, token in enumerate(tokens):
|
||||||
|
if token in {"or", "and", "with", "(", ")"}:
|
||||||
|
if token == "with" and (
|
||||||
|
not last_license_start
|
||||||
|
or index + 1 == len(tokens)
|
||||||
|
or tokens[index + 1] in {"or", "and", "with", "(", ")"}
|
||||||
|
):
|
||||||
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
|
raise InvalidLicenseExpression(message)
|
||||||
|
normalized_tokens.append(token.upper())
|
||||||
|
last_license_start = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if normalized_tokens and normalized_tokens[-1] == "WITH":
|
||||||
|
if token not in EXCEPTIONS:
|
||||||
|
message = f"Unknown license exception: {token!r}"
|
||||||
|
raise InvalidLicenseExpression(message)
|
||||||
|
|
||||||
|
normalized_tokens.append(EXCEPTIONS[token]["id"])
|
||||||
|
last_license_start = False
|
||||||
|
else:
|
||||||
|
if token.endswith("+"):
|
||||||
|
final_token = token[:-1]
|
||||||
|
suffix = "+"
|
||||||
|
else:
|
||||||
|
final_token = token
|
||||||
|
suffix = ""
|
||||||
|
|
||||||
|
if final_token.startswith("licenseref-"):
|
||||||
|
license_ref_id = final_token[len("licenseref-") :]
|
||||||
|
if suffix or not license_ref_allowed.match(license_ref_id):
|
||||||
|
message = f"Invalid licenseref: {token!r}"
|
||||||
|
raise InvalidLicenseExpression(message)
|
||||||
|
normalized_tokens.append(license_refs[final_token])
|
||||||
|
else:
|
||||||
|
if final_token not in LICENSES:
|
||||||
|
message = f"Unknown license: {final_token!r}"
|
||||||
|
raise InvalidLicenseExpression(message)
|
||||||
|
normalized_tokens.append(LICENSES[final_token]["id"] + suffix)
|
||||||
|
last_license_start = True
|
||||||
|
|
||||||
|
normalized_expression = " ".join(normalized_tokens)
|
||||||
|
|
||||||
|
return cast(
|
||||||
|
"NormalizedLicenseExpression",
|
||||||
|
normalized_expression.replace("( ", "(").replace(" )", ")"),
|
||||||
|
)
|
||||||
@@ -0,0 +1,799 @@
|
|||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class SPDXLicense(TypedDict):
|
||||||
|
id: str
|
||||||
|
deprecated: bool
|
||||||
|
|
||||||
|
class SPDXException(TypedDict):
|
||||||
|
id: str
|
||||||
|
deprecated: bool
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = '3.27.0'
|
||||||
|
|
||||||
|
LICENSES: dict[str, SPDXLicense] = {
|
||||||
|
'0bsd': {'id': '0BSD', 'deprecated': False},
|
||||||
|
'3d-slicer-1.0': {'id': '3D-Slicer-1.0', 'deprecated': False},
|
||||||
|
'aal': {'id': 'AAL', 'deprecated': False},
|
||||||
|
'abstyles': {'id': 'Abstyles', 'deprecated': False},
|
||||||
|
'adacore-doc': {'id': 'AdaCore-doc', 'deprecated': False},
|
||||||
|
'adobe-2006': {'id': 'Adobe-2006', 'deprecated': False},
|
||||||
|
'adobe-display-postscript': {'id': 'Adobe-Display-PostScript', 'deprecated': False},
|
||||||
|
'adobe-glyph': {'id': 'Adobe-Glyph', 'deprecated': False},
|
||||||
|
'adobe-utopia': {'id': 'Adobe-Utopia', 'deprecated': False},
|
||||||
|
'adsl': {'id': 'ADSL', 'deprecated': False},
|
||||||
|
'afl-1.1': {'id': 'AFL-1.1', 'deprecated': False},
|
||||||
|
'afl-1.2': {'id': 'AFL-1.2', 'deprecated': False},
|
||||||
|
'afl-2.0': {'id': 'AFL-2.0', 'deprecated': False},
|
||||||
|
'afl-2.1': {'id': 'AFL-2.1', 'deprecated': False},
|
||||||
|
'afl-3.0': {'id': 'AFL-3.0', 'deprecated': False},
|
||||||
|
'afmparse': {'id': 'Afmparse', 'deprecated': False},
|
||||||
|
'agpl-1.0': {'id': 'AGPL-1.0', 'deprecated': True},
|
||||||
|
'agpl-1.0-only': {'id': 'AGPL-1.0-only', 'deprecated': False},
|
||||||
|
'agpl-1.0-or-later': {'id': 'AGPL-1.0-or-later', 'deprecated': False},
|
||||||
|
'agpl-3.0': {'id': 'AGPL-3.0', 'deprecated': True},
|
||||||
|
'agpl-3.0-only': {'id': 'AGPL-3.0-only', 'deprecated': False},
|
||||||
|
'agpl-3.0-or-later': {'id': 'AGPL-3.0-or-later', 'deprecated': False},
|
||||||
|
'aladdin': {'id': 'Aladdin', 'deprecated': False},
|
||||||
|
'amd-newlib': {'id': 'AMD-newlib', 'deprecated': False},
|
||||||
|
'amdplpa': {'id': 'AMDPLPA', 'deprecated': False},
|
||||||
|
'aml': {'id': 'AML', 'deprecated': False},
|
||||||
|
'aml-glslang': {'id': 'AML-glslang', 'deprecated': False},
|
||||||
|
'ampas': {'id': 'AMPAS', 'deprecated': False},
|
||||||
|
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
|
||||||
|
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
|
||||||
|
'any-osi': {'id': 'any-OSI', 'deprecated': False},
|
||||||
|
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
|
||||||
|
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
|
||||||
|
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
|
||||||
|
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
|
||||||
|
'apafml': {'id': 'APAFML', 'deprecated': False},
|
||||||
|
'apl-1.0': {'id': 'APL-1.0', 'deprecated': False},
|
||||||
|
'app-s2p': {'id': 'App-s2p', 'deprecated': False},
|
||||||
|
'apsl-1.0': {'id': 'APSL-1.0', 'deprecated': False},
|
||||||
|
'apsl-1.1': {'id': 'APSL-1.1', 'deprecated': False},
|
||||||
|
'apsl-1.2': {'id': 'APSL-1.2', 'deprecated': False},
|
||||||
|
'apsl-2.0': {'id': 'APSL-2.0', 'deprecated': False},
|
||||||
|
'arphic-1999': {'id': 'Arphic-1999', 'deprecated': False},
|
||||||
|
'artistic-1.0': {'id': 'Artistic-1.0', 'deprecated': False},
|
||||||
|
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
|
||||||
|
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
|
||||||
|
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
|
||||||
|
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
|
||||||
|
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
|
||||||
|
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
|
||||||
|
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
|
||||||
|
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
|
||||||
|
'bahyph': {'id': 'Bahyph', 'deprecated': False},
|
||||||
|
'barr': {'id': 'Barr', 'deprecated': False},
|
||||||
|
'bcrypt-solar-designer': {'id': 'bcrypt-Solar-Designer', 'deprecated': False},
|
||||||
|
'beerware': {'id': 'Beerware', 'deprecated': False},
|
||||||
|
'bitstream-charter': {'id': 'Bitstream-Charter', 'deprecated': False},
|
||||||
|
'bitstream-vera': {'id': 'Bitstream-Vera', 'deprecated': False},
|
||||||
|
'bittorrent-1.0': {'id': 'BitTorrent-1.0', 'deprecated': False},
|
||||||
|
'bittorrent-1.1': {'id': 'BitTorrent-1.1', 'deprecated': False},
|
||||||
|
'blessing': {'id': 'blessing', 'deprecated': False},
|
||||||
|
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
|
||||||
|
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
|
||||||
|
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
|
||||||
|
'borceux': {'id': 'Borceux', 'deprecated': False},
|
||||||
|
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
|
||||||
|
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
|
||||||
|
'bsd-1-clause': {'id': 'BSD-1-Clause', 'deprecated': False},
|
||||||
|
'bsd-2-clause': {'id': 'BSD-2-Clause', 'deprecated': False},
|
||||||
|
'bsd-2-clause-darwin': {'id': 'BSD-2-Clause-Darwin', 'deprecated': False},
|
||||||
|
'bsd-2-clause-first-lines': {'id': 'BSD-2-Clause-first-lines', 'deprecated': False},
|
||||||
|
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
|
||||||
|
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
|
||||||
|
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
|
||||||
|
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
|
||||||
|
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
|
||||||
|
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
|
||||||
|
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
|
||||||
|
'bsd-3-clause-attribution': {'id': 'BSD-3-Clause-Attribution', 'deprecated': False},
|
||||||
|
'bsd-3-clause-clear': {'id': 'BSD-3-Clause-Clear', 'deprecated': False},
|
||||||
|
'bsd-3-clause-flex': {'id': 'BSD-3-Clause-flex', 'deprecated': False},
|
||||||
|
'bsd-3-clause-hp': {'id': 'BSD-3-Clause-HP', 'deprecated': False},
|
||||||
|
'bsd-3-clause-lbnl': {'id': 'BSD-3-Clause-LBNL', 'deprecated': False},
|
||||||
|
'bsd-3-clause-modification': {'id': 'BSD-3-Clause-Modification', 'deprecated': False},
|
||||||
|
'bsd-3-clause-no-military-license': {'id': 'BSD-3-Clause-No-Military-License', 'deprecated': False},
|
||||||
|
'bsd-3-clause-no-nuclear-license': {'id': 'BSD-3-Clause-No-Nuclear-License', 'deprecated': False},
|
||||||
|
'bsd-3-clause-no-nuclear-license-2014': {'id': 'BSD-3-Clause-No-Nuclear-License-2014', 'deprecated': False},
|
||||||
|
'bsd-3-clause-no-nuclear-warranty': {'id': 'BSD-3-Clause-No-Nuclear-Warranty', 'deprecated': False},
|
||||||
|
'bsd-3-clause-open-mpi': {'id': 'BSD-3-Clause-Open-MPI', 'deprecated': False},
|
||||||
|
'bsd-3-clause-sun': {'id': 'BSD-3-Clause-Sun', 'deprecated': False},
|
||||||
|
'bsd-4-clause': {'id': 'BSD-4-Clause', 'deprecated': False},
|
||||||
|
'bsd-4-clause-shortened': {'id': 'BSD-4-Clause-Shortened', 'deprecated': False},
|
||||||
|
'bsd-4-clause-uc': {'id': 'BSD-4-Clause-UC', 'deprecated': False},
|
||||||
|
'bsd-4.3reno': {'id': 'BSD-4.3RENO', 'deprecated': False},
|
||||||
|
'bsd-4.3tahoe': {'id': 'BSD-4.3TAHOE', 'deprecated': False},
|
||||||
|
'bsd-advertising-acknowledgement': {'id': 'BSD-Advertising-Acknowledgement', 'deprecated': False},
|
||||||
|
'bsd-attribution-hpnd-disclaimer': {'id': 'BSD-Attribution-HPND-disclaimer', 'deprecated': False},
|
||||||
|
'bsd-inferno-nettverk': {'id': 'BSD-Inferno-Nettverk', 'deprecated': False},
|
||||||
|
'bsd-protection': {'id': 'BSD-Protection', 'deprecated': False},
|
||||||
|
'bsd-source-beginning-file': {'id': 'BSD-Source-beginning-file', 'deprecated': False},
|
||||||
|
'bsd-source-code': {'id': 'BSD-Source-Code', 'deprecated': False},
|
||||||
|
'bsd-systemics': {'id': 'BSD-Systemics', 'deprecated': False},
|
||||||
|
'bsd-systemics-w3works': {'id': 'BSD-Systemics-W3Works', 'deprecated': False},
|
||||||
|
'bsl-1.0': {'id': 'BSL-1.0', 'deprecated': False},
|
||||||
|
'busl-1.1': {'id': 'BUSL-1.1', 'deprecated': False},
|
||||||
|
'bzip2-1.0.5': {'id': 'bzip2-1.0.5', 'deprecated': True},
|
||||||
|
'bzip2-1.0.6': {'id': 'bzip2-1.0.6', 'deprecated': False},
|
||||||
|
'c-uda-1.0': {'id': 'C-UDA-1.0', 'deprecated': False},
|
||||||
|
'cal-1.0': {'id': 'CAL-1.0', 'deprecated': False},
|
||||||
|
'cal-1.0-combined-work-exception': {'id': 'CAL-1.0-Combined-Work-Exception', 'deprecated': False},
|
||||||
|
'caldera': {'id': 'Caldera', 'deprecated': False},
|
||||||
|
'caldera-no-preamble': {'id': 'Caldera-no-preamble', 'deprecated': False},
|
||||||
|
'catharon': {'id': 'Catharon', 'deprecated': False},
|
||||||
|
'catosl-1.1': {'id': 'CATOSL-1.1', 'deprecated': False},
|
||||||
|
'cc-by-1.0': {'id': 'CC-BY-1.0', 'deprecated': False},
|
||||||
|
'cc-by-2.0': {'id': 'CC-BY-2.0', 'deprecated': False},
|
||||||
|
'cc-by-2.5': {'id': 'CC-BY-2.5', 'deprecated': False},
|
||||||
|
'cc-by-2.5-au': {'id': 'CC-BY-2.5-AU', 'deprecated': False},
|
||||||
|
'cc-by-3.0': {'id': 'CC-BY-3.0', 'deprecated': False},
|
||||||
|
'cc-by-3.0-at': {'id': 'CC-BY-3.0-AT', 'deprecated': False},
|
||||||
|
'cc-by-3.0-au': {'id': 'CC-BY-3.0-AU', 'deprecated': False},
|
||||||
|
'cc-by-3.0-de': {'id': 'CC-BY-3.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-3.0-igo': {'id': 'CC-BY-3.0-IGO', 'deprecated': False},
|
||||||
|
'cc-by-3.0-nl': {'id': 'CC-BY-3.0-NL', 'deprecated': False},
|
||||||
|
'cc-by-3.0-us': {'id': 'CC-BY-3.0-US', 'deprecated': False},
|
||||||
|
'cc-by-4.0': {'id': 'CC-BY-4.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-1.0': {'id': 'CC-BY-NC-1.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-2.0': {'id': 'CC-BY-NC-2.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-2.5': {'id': 'CC-BY-NC-2.5', 'deprecated': False},
|
||||||
|
'cc-by-nc-3.0': {'id': 'CC-BY-NC-3.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-3.0-de': {'id': 'CC-BY-NC-3.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-nc-4.0': {'id': 'CC-BY-NC-4.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-1.0': {'id': 'CC-BY-NC-ND-1.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-2.0': {'id': 'CC-BY-NC-ND-2.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-2.5': {'id': 'CC-BY-NC-ND-2.5', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-3.0': {'id': 'CC-BY-NC-ND-3.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-3.0-de': {'id': 'CC-BY-NC-ND-3.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-3.0-igo': {'id': 'CC-BY-NC-ND-3.0-IGO', 'deprecated': False},
|
||||||
|
'cc-by-nc-nd-4.0': {'id': 'CC-BY-NC-ND-4.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-1.0': {'id': 'CC-BY-NC-SA-1.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-2.0': {'id': 'CC-BY-NC-SA-2.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-2.0-de': {'id': 'CC-BY-NC-SA-2.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-2.0-fr': {'id': 'CC-BY-NC-SA-2.0-FR', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-2.0-uk': {'id': 'CC-BY-NC-SA-2.0-UK', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-2.5': {'id': 'CC-BY-NC-SA-2.5', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-3.0': {'id': 'CC-BY-NC-SA-3.0', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-3.0-de': {'id': 'CC-BY-NC-SA-3.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-3.0-igo': {'id': 'CC-BY-NC-SA-3.0-IGO', 'deprecated': False},
|
||||||
|
'cc-by-nc-sa-4.0': {'id': 'CC-BY-NC-SA-4.0', 'deprecated': False},
|
||||||
|
'cc-by-nd-1.0': {'id': 'CC-BY-ND-1.0', 'deprecated': False},
|
||||||
|
'cc-by-nd-2.0': {'id': 'CC-BY-ND-2.0', 'deprecated': False},
|
||||||
|
'cc-by-nd-2.5': {'id': 'CC-BY-ND-2.5', 'deprecated': False},
|
||||||
|
'cc-by-nd-3.0': {'id': 'CC-BY-ND-3.0', 'deprecated': False},
|
||||||
|
'cc-by-nd-3.0-de': {'id': 'CC-BY-ND-3.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-nd-4.0': {'id': 'CC-BY-ND-4.0', 'deprecated': False},
|
||||||
|
'cc-by-sa-1.0': {'id': 'CC-BY-SA-1.0', 'deprecated': False},
|
||||||
|
'cc-by-sa-2.0': {'id': 'CC-BY-SA-2.0', 'deprecated': False},
|
||||||
|
'cc-by-sa-2.0-uk': {'id': 'CC-BY-SA-2.0-UK', 'deprecated': False},
|
||||||
|
'cc-by-sa-2.1-jp': {'id': 'CC-BY-SA-2.1-JP', 'deprecated': False},
|
||||||
|
'cc-by-sa-2.5': {'id': 'CC-BY-SA-2.5', 'deprecated': False},
|
||||||
|
'cc-by-sa-3.0': {'id': 'CC-BY-SA-3.0', 'deprecated': False},
|
||||||
|
'cc-by-sa-3.0-at': {'id': 'CC-BY-SA-3.0-AT', 'deprecated': False},
|
||||||
|
'cc-by-sa-3.0-de': {'id': 'CC-BY-SA-3.0-DE', 'deprecated': False},
|
||||||
|
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
|
||||||
|
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
|
||||||
|
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
|
||||||
|
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
|
||||||
|
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
|
||||||
|
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
|
||||||
|
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
|
||||||
|
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
|
||||||
|
'cdl-1.0': {'id': 'CDL-1.0', 'deprecated': False},
|
||||||
|
'cdla-permissive-1.0': {'id': 'CDLA-Permissive-1.0', 'deprecated': False},
|
||||||
|
'cdla-permissive-2.0': {'id': 'CDLA-Permissive-2.0', 'deprecated': False},
|
||||||
|
'cdla-sharing-1.0': {'id': 'CDLA-Sharing-1.0', 'deprecated': False},
|
||||||
|
'cecill-1.0': {'id': 'CECILL-1.0', 'deprecated': False},
|
||||||
|
'cecill-1.1': {'id': 'CECILL-1.1', 'deprecated': False},
|
||||||
|
'cecill-2.0': {'id': 'CECILL-2.0', 'deprecated': False},
|
||||||
|
'cecill-2.1': {'id': 'CECILL-2.1', 'deprecated': False},
|
||||||
|
'cecill-b': {'id': 'CECILL-B', 'deprecated': False},
|
||||||
|
'cecill-c': {'id': 'CECILL-C', 'deprecated': False},
|
||||||
|
'cern-ohl-1.1': {'id': 'CERN-OHL-1.1', 'deprecated': False},
|
||||||
|
'cern-ohl-1.2': {'id': 'CERN-OHL-1.2', 'deprecated': False},
|
||||||
|
'cern-ohl-p-2.0': {'id': 'CERN-OHL-P-2.0', 'deprecated': False},
|
||||||
|
'cern-ohl-s-2.0': {'id': 'CERN-OHL-S-2.0', 'deprecated': False},
|
||||||
|
'cern-ohl-w-2.0': {'id': 'CERN-OHL-W-2.0', 'deprecated': False},
|
||||||
|
'cfitsio': {'id': 'CFITSIO', 'deprecated': False},
|
||||||
|
'check-cvs': {'id': 'check-cvs', 'deprecated': False},
|
||||||
|
'checkmk': {'id': 'checkmk', 'deprecated': False},
|
||||||
|
'clartistic': {'id': 'ClArtistic', 'deprecated': False},
|
||||||
|
'clips': {'id': 'Clips', 'deprecated': False},
|
||||||
|
'cmu-mach': {'id': 'CMU-Mach', 'deprecated': False},
|
||||||
|
'cmu-mach-nodoc': {'id': 'CMU-Mach-nodoc', 'deprecated': False},
|
||||||
|
'cnri-jython': {'id': 'CNRI-Jython', 'deprecated': False},
|
||||||
|
'cnri-python': {'id': 'CNRI-Python', 'deprecated': False},
|
||||||
|
'cnri-python-gpl-compatible': {'id': 'CNRI-Python-GPL-Compatible', 'deprecated': False},
|
||||||
|
'coil-1.0': {'id': 'COIL-1.0', 'deprecated': False},
|
||||||
|
'community-spec-1.0': {'id': 'Community-Spec-1.0', 'deprecated': False},
|
||||||
|
'condor-1.1': {'id': 'Condor-1.1', 'deprecated': False},
|
||||||
|
'copyleft-next-0.3.0': {'id': 'copyleft-next-0.3.0', 'deprecated': False},
|
||||||
|
'copyleft-next-0.3.1': {'id': 'copyleft-next-0.3.1', 'deprecated': False},
|
||||||
|
'cornell-lossless-jpeg': {'id': 'Cornell-Lossless-JPEG', 'deprecated': False},
|
||||||
|
'cpal-1.0': {'id': 'CPAL-1.0', 'deprecated': False},
|
||||||
|
'cpl-1.0': {'id': 'CPL-1.0', 'deprecated': False},
|
||||||
|
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
|
||||||
|
'cronyx': {'id': 'Cronyx', 'deprecated': False},
|
||||||
|
'crossword': {'id': 'Crossword', 'deprecated': False},
|
||||||
|
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
|
||||||
|
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
|
||||||
|
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
|
||||||
|
'cube': {'id': 'Cube', 'deprecated': False},
|
||||||
|
'curl': {'id': 'curl', 'deprecated': False},
|
||||||
|
'cve-tou': {'id': 'cve-tou', 'deprecated': False},
|
||||||
|
'd-fsl-1.0': {'id': 'D-FSL-1.0', 'deprecated': False},
|
||||||
|
'dec-3-clause': {'id': 'DEC-3-Clause', 'deprecated': False},
|
||||||
|
'diffmark': {'id': 'diffmark', 'deprecated': False},
|
||||||
|
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
|
||||||
|
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
|
||||||
|
'doc': {'id': 'DOC', 'deprecated': False},
|
||||||
|
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
|
||||||
|
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
|
||||||
|
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
|
||||||
|
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
|
||||||
|
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
|
||||||
|
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
|
||||||
|
'drl-1.1': {'id': 'DRL-1.1', 'deprecated': False},
|
||||||
|
'dsdp': {'id': 'DSDP', 'deprecated': False},
|
||||||
|
'dtoa': {'id': 'dtoa', 'deprecated': False},
|
||||||
|
'dvipdfm': {'id': 'dvipdfm', 'deprecated': False},
|
||||||
|
'ecl-1.0': {'id': 'ECL-1.0', 'deprecated': False},
|
||||||
|
'ecl-2.0': {'id': 'ECL-2.0', 'deprecated': False},
|
||||||
|
'ecos-2.0': {'id': 'eCos-2.0', 'deprecated': True},
|
||||||
|
'efl-1.0': {'id': 'EFL-1.0', 'deprecated': False},
|
||||||
|
'efl-2.0': {'id': 'EFL-2.0', 'deprecated': False},
|
||||||
|
'egenix': {'id': 'eGenix', 'deprecated': False},
|
||||||
|
'elastic-2.0': {'id': 'Elastic-2.0', 'deprecated': False},
|
||||||
|
'entessa': {'id': 'Entessa', 'deprecated': False},
|
||||||
|
'epics': {'id': 'EPICS', 'deprecated': False},
|
||||||
|
'epl-1.0': {'id': 'EPL-1.0', 'deprecated': False},
|
||||||
|
'epl-2.0': {'id': 'EPL-2.0', 'deprecated': False},
|
||||||
|
'erlpl-1.1': {'id': 'ErlPL-1.1', 'deprecated': False},
|
||||||
|
'etalab-2.0': {'id': 'etalab-2.0', 'deprecated': False},
|
||||||
|
'eudatagrid': {'id': 'EUDatagrid', 'deprecated': False},
|
||||||
|
'eupl-1.0': {'id': 'EUPL-1.0', 'deprecated': False},
|
||||||
|
'eupl-1.1': {'id': 'EUPL-1.1', 'deprecated': False},
|
||||||
|
'eupl-1.2': {'id': 'EUPL-1.2', 'deprecated': False},
|
||||||
|
'eurosym': {'id': 'Eurosym', 'deprecated': False},
|
||||||
|
'fair': {'id': 'Fair', 'deprecated': False},
|
||||||
|
'fbm': {'id': 'FBM', 'deprecated': False},
|
||||||
|
'fdk-aac': {'id': 'FDK-AAC', 'deprecated': False},
|
||||||
|
'ferguson-twofish': {'id': 'Ferguson-Twofish', 'deprecated': False},
|
||||||
|
'frameworx-1.0': {'id': 'Frameworx-1.0', 'deprecated': False},
|
||||||
|
'freebsd-doc': {'id': 'FreeBSD-DOC', 'deprecated': False},
|
||||||
|
'freeimage': {'id': 'FreeImage', 'deprecated': False},
|
||||||
|
'fsfap': {'id': 'FSFAP', 'deprecated': False},
|
||||||
|
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
|
||||||
|
'fsful': {'id': 'FSFUL', 'deprecated': False},
|
||||||
|
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
|
||||||
|
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
|
||||||
|
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
|
||||||
|
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
|
||||||
|
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
|
||||||
|
'ftl': {'id': 'FTL', 'deprecated': False},
|
||||||
|
'furuseth': {'id': 'Furuseth', 'deprecated': False},
|
||||||
|
'fwlw': {'id': 'fwlw', 'deprecated': False},
|
||||||
|
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
|
||||||
|
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
|
||||||
|
'gd': {'id': 'GD', 'deprecated': False},
|
||||||
|
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
|
||||||
|
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
|
||||||
|
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
|
||||||
|
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.1-no-invariants-only': {'id': 'GFDL-1.1-no-invariants-only', 'deprecated': False},
|
||||||
|
'gfdl-1.1-no-invariants-or-later': {'id': 'GFDL-1.1-no-invariants-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.1-only': {'id': 'GFDL-1.1-only', 'deprecated': False},
|
||||||
|
'gfdl-1.1-or-later': {'id': 'GFDL-1.1-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.2': {'id': 'GFDL-1.2', 'deprecated': True},
|
||||||
|
'gfdl-1.2-invariants-only': {'id': 'GFDL-1.2-invariants-only', 'deprecated': False},
|
||||||
|
'gfdl-1.2-invariants-or-later': {'id': 'GFDL-1.2-invariants-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.2-no-invariants-only': {'id': 'GFDL-1.2-no-invariants-only', 'deprecated': False},
|
||||||
|
'gfdl-1.2-no-invariants-or-later': {'id': 'GFDL-1.2-no-invariants-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.2-only': {'id': 'GFDL-1.2-only', 'deprecated': False},
|
||||||
|
'gfdl-1.2-or-later': {'id': 'GFDL-1.2-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.3': {'id': 'GFDL-1.3', 'deprecated': True},
|
||||||
|
'gfdl-1.3-invariants-only': {'id': 'GFDL-1.3-invariants-only', 'deprecated': False},
|
||||||
|
'gfdl-1.3-invariants-or-later': {'id': 'GFDL-1.3-invariants-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.3-no-invariants-only': {'id': 'GFDL-1.3-no-invariants-only', 'deprecated': False},
|
||||||
|
'gfdl-1.3-no-invariants-or-later': {'id': 'GFDL-1.3-no-invariants-or-later', 'deprecated': False},
|
||||||
|
'gfdl-1.3-only': {'id': 'GFDL-1.3-only', 'deprecated': False},
|
||||||
|
'gfdl-1.3-or-later': {'id': 'GFDL-1.3-or-later', 'deprecated': False},
|
||||||
|
'giftware': {'id': 'Giftware', 'deprecated': False},
|
||||||
|
'gl2ps': {'id': 'GL2PS', 'deprecated': False},
|
||||||
|
'glide': {'id': 'Glide', 'deprecated': False},
|
||||||
|
'glulxe': {'id': 'Glulxe', 'deprecated': False},
|
||||||
|
'glwtpl': {'id': 'GLWTPL', 'deprecated': False},
|
||||||
|
'gnuplot': {'id': 'gnuplot', 'deprecated': False},
|
||||||
|
'gpl-1.0': {'id': 'GPL-1.0', 'deprecated': True},
|
||||||
|
'gpl-1.0+': {'id': 'GPL-1.0+', 'deprecated': True},
|
||||||
|
'gpl-1.0-only': {'id': 'GPL-1.0-only', 'deprecated': False},
|
||||||
|
'gpl-1.0-or-later': {'id': 'GPL-1.0-or-later', 'deprecated': False},
|
||||||
|
'gpl-2.0': {'id': 'GPL-2.0', 'deprecated': True},
|
||||||
|
'gpl-2.0+': {'id': 'GPL-2.0+', 'deprecated': True},
|
||||||
|
'gpl-2.0-only': {'id': 'GPL-2.0-only', 'deprecated': False},
|
||||||
|
'gpl-2.0-or-later': {'id': 'GPL-2.0-or-later', 'deprecated': False},
|
||||||
|
'gpl-2.0-with-autoconf-exception': {'id': 'GPL-2.0-with-autoconf-exception', 'deprecated': True},
|
||||||
|
'gpl-2.0-with-bison-exception': {'id': 'GPL-2.0-with-bison-exception', 'deprecated': True},
|
||||||
|
'gpl-2.0-with-classpath-exception': {'id': 'GPL-2.0-with-classpath-exception', 'deprecated': True},
|
||||||
|
'gpl-2.0-with-font-exception': {'id': 'GPL-2.0-with-font-exception', 'deprecated': True},
|
||||||
|
'gpl-2.0-with-gcc-exception': {'id': 'GPL-2.0-with-GCC-exception', 'deprecated': True},
|
||||||
|
'gpl-3.0': {'id': 'GPL-3.0', 'deprecated': True},
|
||||||
|
'gpl-3.0+': {'id': 'GPL-3.0+', 'deprecated': True},
|
||||||
|
'gpl-3.0-only': {'id': 'GPL-3.0-only', 'deprecated': False},
|
||||||
|
'gpl-3.0-or-later': {'id': 'GPL-3.0-or-later', 'deprecated': False},
|
||||||
|
'gpl-3.0-with-autoconf-exception': {'id': 'GPL-3.0-with-autoconf-exception', 'deprecated': True},
|
||||||
|
'gpl-3.0-with-gcc-exception': {'id': 'GPL-3.0-with-GCC-exception', 'deprecated': True},
|
||||||
|
'graphics-gems': {'id': 'Graphics-Gems', 'deprecated': False},
|
||||||
|
'gsoap-1.3b': {'id': 'gSOAP-1.3b', 'deprecated': False},
|
||||||
|
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
|
||||||
|
'gutmann': {'id': 'Gutmann', 'deprecated': False},
|
||||||
|
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
|
||||||
|
'hdf5': {'id': 'HDF5', 'deprecated': False},
|
||||||
|
'hdparm': {'id': 'hdparm', 'deprecated': False},
|
||||||
|
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
|
||||||
|
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
|
||||||
|
'hp-1986': {'id': 'HP-1986', 'deprecated': False},
|
||||||
|
'hp-1989': {'id': 'HP-1989', 'deprecated': False},
|
||||||
|
'hpnd': {'id': 'HPND', 'deprecated': False},
|
||||||
|
'hpnd-dec': {'id': 'HPND-DEC', 'deprecated': False},
|
||||||
|
'hpnd-doc': {'id': 'HPND-doc', 'deprecated': False},
|
||||||
|
'hpnd-doc-sell': {'id': 'HPND-doc-sell', 'deprecated': False},
|
||||||
|
'hpnd-export-us': {'id': 'HPND-export-US', 'deprecated': False},
|
||||||
|
'hpnd-export-us-acknowledgement': {'id': 'HPND-export-US-acknowledgement', 'deprecated': False},
|
||||||
|
'hpnd-export-us-modify': {'id': 'HPND-export-US-modify', 'deprecated': False},
|
||||||
|
'hpnd-export2-us': {'id': 'HPND-export2-US', 'deprecated': False},
|
||||||
|
'hpnd-fenneberg-livingston': {'id': 'HPND-Fenneberg-Livingston', 'deprecated': False},
|
||||||
|
'hpnd-inria-imag': {'id': 'HPND-INRIA-IMAG', 'deprecated': False},
|
||||||
|
'hpnd-intel': {'id': 'HPND-Intel', 'deprecated': False},
|
||||||
|
'hpnd-kevlin-henney': {'id': 'HPND-Kevlin-Henney', 'deprecated': False},
|
||||||
|
'hpnd-markus-kuhn': {'id': 'HPND-Markus-Kuhn', 'deprecated': False},
|
||||||
|
'hpnd-merchantability-variant': {'id': 'HPND-merchantability-variant', 'deprecated': False},
|
||||||
|
'hpnd-mit-disclaimer': {'id': 'HPND-MIT-disclaimer', 'deprecated': False},
|
||||||
|
'hpnd-netrek': {'id': 'HPND-Netrek', 'deprecated': False},
|
||||||
|
'hpnd-pbmplus': {'id': 'HPND-Pbmplus', 'deprecated': False},
|
||||||
|
'hpnd-sell-mit-disclaimer-xserver': {'id': 'HPND-sell-MIT-disclaimer-xserver', 'deprecated': False},
|
||||||
|
'hpnd-sell-regexpr': {'id': 'HPND-sell-regexpr', 'deprecated': False},
|
||||||
|
'hpnd-sell-variant': {'id': 'HPND-sell-variant', 'deprecated': False},
|
||||||
|
'hpnd-sell-variant-mit-disclaimer': {'id': 'HPND-sell-variant-MIT-disclaimer', 'deprecated': False},
|
||||||
|
'hpnd-sell-variant-mit-disclaimer-rev': {'id': 'HPND-sell-variant-MIT-disclaimer-rev', 'deprecated': False},
|
||||||
|
'hpnd-uc': {'id': 'HPND-UC', 'deprecated': False},
|
||||||
|
'hpnd-uc-export-us': {'id': 'HPND-UC-export-US', 'deprecated': False},
|
||||||
|
'htmltidy': {'id': 'HTMLTIDY', 'deprecated': False},
|
||||||
|
'ibm-pibs': {'id': 'IBM-pibs', 'deprecated': False},
|
||||||
|
'icu': {'id': 'ICU', 'deprecated': False},
|
||||||
|
'iec-code-components-eula': {'id': 'IEC-Code-Components-EULA', 'deprecated': False},
|
||||||
|
'ijg': {'id': 'IJG', 'deprecated': False},
|
||||||
|
'ijg-short': {'id': 'IJG-short', 'deprecated': False},
|
||||||
|
'imagemagick': {'id': 'ImageMagick', 'deprecated': False},
|
||||||
|
'imatix': {'id': 'iMatix', 'deprecated': False},
|
||||||
|
'imlib2': {'id': 'Imlib2', 'deprecated': False},
|
||||||
|
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
|
||||||
|
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
|
||||||
|
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
|
||||||
|
'intel': {'id': 'Intel', 'deprecated': False},
|
||||||
|
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
|
||||||
|
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
|
||||||
|
'ipa': {'id': 'IPA', 'deprecated': False},
|
||||||
|
'ipl-1.0': {'id': 'IPL-1.0', 'deprecated': False},
|
||||||
|
'isc': {'id': 'ISC', 'deprecated': False},
|
||||||
|
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
|
||||||
|
'jam': {'id': 'Jam', 'deprecated': False},
|
||||||
|
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
|
||||||
|
'jove': {'id': 'jove', 'deprecated': False},
|
||||||
|
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
|
||||||
|
'jpnic': {'id': 'JPNIC', 'deprecated': False},
|
||||||
|
'json': {'id': 'JSON', 'deprecated': False},
|
||||||
|
'kastrup': {'id': 'Kastrup', 'deprecated': False},
|
||||||
|
'kazlib': {'id': 'Kazlib', 'deprecated': False},
|
||||||
|
'knuth-ctan': {'id': 'Knuth-CTAN', 'deprecated': False},
|
||||||
|
'lal-1.2': {'id': 'LAL-1.2', 'deprecated': False},
|
||||||
|
'lal-1.3': {'id': 'LAL-1.3', 'deprecated': False},
|
||||||
|
'latex2e': {'id': 'Latex2e', 'deprecated': False},
|
||||||
|
'latex2e-translated-notice': {'id': 'Latex2e-translated-notice', 'deprecated': False},
|
||||||
|
'leptonica': {'id': 'Leptonica', 'deprecated': False},
|
||||||
|
'lgpl-2.0': {'id': 'LGPL-2.0', 'deprecated': True},
|
||||||
|
'lgpl-2.0+': {'id': 'LGPL-2.0+', 'deprecated': True},
|
||||||
|
'lgpl-2.0-only': {'id': 'LGPL-2.0-only', 'deprecated': False},
|
||||||
|
'lgpl-2.0-or-later': {'id': 'LGPL-2.0-or-later', 'deprecated': False},
|
||||||
|
'lgpl-2.1': {'id': 'LGPL-2.1', 'deprecated': True},
|
||||||
|
'lgpl-2.1+': {'id': 'LGPL-2.1+', 'deprecated': True},
|
||||||
|
'lgpl-2.1-only': {'id': 'LGPL-2.1-only', 'deprecated': False},
|
||||||
|
'lgpl-2.1-or-later': {'id': 'LGPL-2.1-or-later', 'deprecated': False},
|
||||||
|
'lgpl-3.0': {'id': 'LGPL-3.0', 'deprecated': True},
|
||||||
|
'lgpl-3.0+': {'id': 'LGPL-3.0+', 'deprecated': True},
|
||||||
|
'lgpl-3.0-only': {'id': 'LGPL-3.0-only', 'deprecated': False},
|
||||||
|
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
|
||||||
|
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
|
||||||
|
'libpng': {'id': 'Libpng', 'deprecated': False},
|
||||||
|
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
|
||||||
|
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
|
||||||
|
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
|
||||||
|
'libtiff': {'id': 'libtiff', 'deprecated': False},
|
||||||
|
'libutil-david-nugent': {'id': 'libutil-David-Nugent', 'deprecated': False},
|
||||||
|
'liliq-p-1.1': {'id': 'LiLiQ-P-1.1', 'deprecated': False},
|
||||||
|
'liliq-r-1.1': {'id': 'LiLiQ-R-1.1', 'deprecated': False},
|
||||||
|
'liliq-rplus-1.1': {'id': 'LiLiQ-Rplus-1.1', 'deprecated': False},
|
||||||
|
'linux-man-pages-1-para': {'id': 'Linux-man-pages-1-para', 'deprecated': False},
|
||||||
|
'linux-man-pages-copyleft': {'id': 'Linux-man-pages-copyleft', 'deprecated': False},
|
||||||
|
'linux-man-pages-copyleft-2-para': {'id': 'Linux-man-pages-copyleft-2-para', 'deprecated': False},
|
||||||
|
'linux-man-pages-copyleft-var': {'id': 'Linux-man-pages-copyleft-var', 'deprecated': False},
|
||||||
|
'linux-openib': {'id': 'Linux-OpenIB', 'deprecated': False},
|
||||||
|
'loop': {'id': 'LOOP', 'deprecated': False},
|
||||||
|
'lpd-document': {'id': 'LPD-document', 'deprecated': False},
|
||||||
|
'lpl-1.0': {'id': 'LPL-1.0', 'deprecated': False},
|
||||||
|
'lpl-1.02': {'id': 'LPL-1.02', 'deprecated': False},
|
||||||
|
'lppl-1.0': {'id': 'LPPL-1.0', 'deprecated': False},
|
||||||
|
'lppl-1.1': {'id': 'LPPL-1.1', 'deprecated': False},
|
||||||
|
'lppl-1.2': {'id': 'LPPL-1.2', 'deprecated': False},
|
||||||
|
'lppl-1.3a': {'id': 'LPPL-1.3a', 'deprecated': False},
|
||||||
|
'lppl-1.3c': {'id': 'LPPL-1.3c', 'deprecated': False},
|
||||||
|
'lsof': {'id': 'lsof', 'deprecated': False},
|
||||||
|
'lucida-bitmap-fonts': {'id': 'Lucida-Bitmap-Fonts', 'deprecated': False},
|
||||||
|
'lzma-sdk-9.11-to-9.20': {'id': 'LZMA-SDK-9.11-to-9.20', 'deprecated': False},
|
||||||
|
'lzma-sdk-9.22': {'id': 'LZMA-SDK-9.22', 'deprecated': False},
|
||||||
|
'mackerras-3-clause': {'id': 'Mackerras-3-Clause', 'deprecated': False},
|
||||||
|
'mackerras-3-clause-acknowledgment': {'id': 'Mackerras-3-Clause-acknowledgment', 'deprecated': False},
|
||||||
|
'magaz': {'id': 'magaz', 'deprecated': False},
|
||||||
|
'mailprio': {'id': 'mailprio', 'deprecated': False},
|
||||||
|
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
|
||||||
|
'man2html': {'id': 'man2html', 'deprecated': False},
|
||||||
|
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
|
||||||
|
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
|
||||||
|
'metamail': {'id': 'metamail', 'deprecated': False},
|
||||||
|
'minpack': {'id': 'Minpack', 'deprecated': False},
|
||||||
|
'mips': {'id': 'MIPS', 'deprecated': False},
|
||||||
|
'miros': {'id': 'MirOS', 'deprecated': False},
|
||||||
|
'mit': {'id': 'MIT', 'deprecated': False},
|
||||||
|
'mit-0': {'id': 'MIT-0', 'deprecated': False},
|
||||||
|
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
|
||||||
|
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
|
||||||
|
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
|
||||||
|
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
|
||||||
|
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
|
||||||
|
'mit-festival': {'id': 'MIT-Festival', 'deprecated': False},
|
||||||
|
'mit-khronos-old': {'id': 'MIT-Khronos-old', 'deprecated': False},
|
||||||
|
'mit-modern-variant': {'id': 'MIT-Modern-Variant', 'deprecated': False},
|
||||||
|
'mit-open-group': {'id': 'MIT-open-group', 'deprecated': False},
|
||||||
|
'mit-testregex': {'id': 'MIT-testregex', 'deprecated': False},
|
||||||
|
'mit-wu': {'id': 'MIT-Wu', 'deprecated': False},
|
||||||
|
'mitnfa': {'id': 'MITNFA', 'deprecated': False},
|
||||||
|
'mmixware': {'id': 'MMIXware', 'deprecated': False},
|
||||||
|
'motosoto': {'id': 'Motosoto', 'deprecated': False},
|
||||||
|
'mpeg-ssg': {'id': 'MPEG-SSG', 'deprecated': False},
|
||||||
|
'mpi-permissive': {'id': 'mpi-permissive', 'deprecated': False},
|
||||||
|
'mpich2': {'id': 'mpich2', 'deprecated': False},
|
||||||
|
'mpl-1.0': {'id': 'MPL-1.0', 'deprecated': False},
|
||||||
|
'mpl-1.1': {'id': 'MPL-1.1', 'deprecated': False},
|
||||||
|
'mpl-2.0': {'id': 'MPL-2.0', 'deprecated': False},
|
||||||
|
'mpl-2.0-no-copyleft-exception': {'id': 'MPL-2.0-no-copyleft-exception', 'deprecated': False},
|
||||||
|
'mplus': {'id': 'mplus', 'deprecated': False},
|
||||||
|
'ms-lpl': {'id': 'MS-LPL', 'deprecated': False},
|
||||||
|
'ms-pl': {'id': 'MS-PL', 'deprecated': False},
|
||||||
|
'ms-rl': {'id': 'MS-RL', 'deprecated': False},
|
||||||
|
'mtll': {'id': 'MTLL', 'deprecated': False},
|
||||||
|
'mulanpsl-1.0': {'id': 'MulanPSL-1.0', 'deprecated': False},
|
||||||
|
'mulanpsl-2.0': {'id': 'MulanPSL-2.0', 'deprecated': False},
|
||||||
|
'multics': {'id': 'Multics', 'deprecated': False},
|
||||||
|
'mup': {'id': 'Mup', 'deprecated': False},
|
||||||
|
'naist-2003': {'id': 'NAIST-2003', 'deprecated': False},
|
||||||
|
'nasa-1.3': {'id': 'NASA-1.3', 'deprecated': False},
|
||||||
|
'naumen': {'id': 'Naumen', 'deprecated': False},
|
||||||
|
'nbpl-1.0': {'id': 'NBPL-1.0', 'deprecated': False},
|
||||||
|
'ncbi-pd': {'id': 'NCBI-PD', 'deprecated': False},
|
||||||
|
'ncgl-uk-2.0': {'id': 'NCGL-UK-2.0', 'deprecated': False},
|
||||||
|
'ncl': {'id': 'NCL', 'deprecated': False},
|
||||||
|
'ncsa': {'id': 'NCSA', 'deprecated': False},
|
||||||
|
'net-snmp': {'id': 'Net-SNMP', 'deprecated': True},
|
||||||
|
'netcdf': {'id': 'NetCDF', 'deprecated': False},
|
||||||
|
'newsletr': {'id': 'Newsletr', 'deprecated': False},
|
||||||
|
'ngpl': {'id': 'NGPL', 'deprecated': False},
|
||||||
|
'ngrep': {'id': 'ngrep', 'deprecated': False},
|
||||||
|
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
|
||||||
|
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
|
||||||
|
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
|
||||||
|
'nist-software': {'id': 'NIST-Software', 'deprecated': False},
|
||||||
|
'nlod-1.0': {'id': 'NLOD-1.0', 'deprecated': False},
|
||||||
|
'nlod-2.0': {'id': 'NLOD-2.0', 'deprecated': False},
|
||||||
|
'nlpl': {'id': 'NLPL', 'deprecated': False},
|
||||||
|
'nokia': {'id': 'Nokia', 'deprecated': False},
|
||||||
|
'nosl': {'id': 'NOSL', 'deprecated': False},
|
||||||
|
'noweb': {'id': 'Noweb', 'deprecated': False},
|
||||||
|
'npl-1.0': {'id': 'NPL-1.0', 'deprecated': False},
|
||||||
|
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
|
||||||
|
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
|
||||||
|
'nrl': {'id': 'NRL', 'deprecated': False},
|
||||||
|
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
|
||||||
|
'ntp': {'id': 'NTP', 'deprecated': False},
|
||||||
|
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
|
||||||
|
'nunit': {'id': 'Nunit', 'deprecated': True},
|
||||||
|
'o-uda-1.0': {'id': 'O-UDA-1.0', 'deprecated': False},
|
||||||
|
'oar': {'id': 'OAR', 'deprecated': False},
|
||||||
|
'occt-pl': {'id': 'OCCT-PL', 'deprecated': False},
|
||||||
|
'oclc-2.0': {'id': 'OCLC-2.0', 'deprecated': False},
|
||||||
|
'odbl-1.0': {'id': 'ODbL-1.0', 'deprecated': False},
|
||||||
|
'odc-by-1.0': {'id': 'ODC-By-1.0', 'deprecated': False},
|
||||||
|
'offis': {'id': 'OFFIS', 'deprecated': False},
|
||||||
|
'ofl-1.0': {'id': 'OFL-1.0', 'deprecated': False},
|
||||||
|
'ofl-1.0-no-rfn': {'id': 'OFL-1.0-no-RFN', 'deprecated': False},
|
||||||
|
'ofl-1.0-rfn': {'id': 'OFL-1.0-RFN', 'deprecated': False},
|
||||||
|
'ofl-1.1': {'id': 'OFL-1.1', 'deprecated': False},
|
||||||
|
'ofl-1.1-no-rfn': {'id': 'OFL-1.1-no-RFN', 'deprecated': False},
|
||||||
|
'ofl-1.1-rfn': {'id': 'OFL-1.1-RFN', 'deprecated': False},
|
||||||
|
'ogc-1.0': {'id': 'OGC-1.0', 'deprecated': False},
|
||||||
|
'ogdl-taiwan-1.0': {'id': 'OGDL-Taiwan-1.0', 'deprecated': False},
|
||||||
|
'ogl-canada-2.0': {'id': 'OGL-Canada-2.0', 'deprecated': False},
|
||||||
|
'ogl-uk-1.0': {'id': 'OGL-UK-1.0', 'deprecated': False},
|
||||||
|
'ogl-uk-2.0': {'id': 'OGL-UK-2.0', 'deprecated': False},
|
||||||
|
'ogl-uk-3.0': {'id': 'OGL-UK-3.0', 'deprecated': False},
|
||||||
|
'ogtsl': {'id': 'OGTSL', 'deprecated': False},
|
||||||
|
'oldap-1.1': {'id': 'OLDAP-1.1', 'deprecated': False},
|
||||||
|
'oldap-1.2': {'id': 'OLDAP-1.2', 'deprecated': False},
|
||||||
|
'oldap-1.3': {'id': 'OLDAP-1.3', 'deprecated': False},
|
||||||
|
'oldap-1.4': {'id': 'OLDAP-1.4', 'deprecated': False},
|
||||||
|
'oldap-2.0': {'id': 'OLDAP-2.0', 'deprecated': False},
|
||||||
|
'oldap-2.0.1': {'id': 'OLDAP-2.0.1', 'deprecated': False},
|
||||||
|
'oldap-2.1': {'id': 'OLDAP-2.1', 'deprecated': False},
|
||||||
|
'oldap-2.2': {'id': 'OLDAP-2.2', 'deprecated': False},
|
||||||
|
'oldap-2.2.1': {'id': 'OLDAP-2.2.1', 'deprecated': False},
|
||||||
|
'oldap-2.2.2': {'id': 'OLDAP-2.2.2', 'deprecated': False},
|
||||||
|
'oldap-2.3': {'id': 'OLDAP-2.3', 'deprecated': False},
|
||||||
|
'oldap-2.4': {'id': 'OLDAP-2.4', 'deprecated': False},
|
||||||
|
'oldap-2.5': {'id': 'OLDAP-2.5', 'deprecated': False},
|
||||||
|
'oldap-2.6': {'id': 'OLDAP-2.6', 'deprecated': False},
|
||||||
|
'oldap-2.7': {'id': 'OLDAP-2.7', 'deprecated': False},
|
||||||
|
'oldap-2.8': {'id': 'OLDAP-2.8', 'deprecated': False},
|
||||||
|
'olfl-1.3': {'id': 'OLFL-1.3', 'deprecated': False},
|
||||||
|
'oml': {'id': 'OML', 'deprecated': False},
|
||||||
|
'openpbs-2.3': {'id': 'OpenPBS-2.3', 'deprecated': False},
|
||||||
|
'openssl': {'id': 'OpenSSL', 'deprecated': False},
|
||||||
|
'openssl-standalone': {'id': 'OpenSSL-standalone', 'deprecated': False},
|
||||||
|
'openvision': {'id': 'OpenVision', 'deprecated': False},
|
||||||
|
'opl-1.0': {'id': 'OPL-1.0', 'deprecated': False},
|
||||||
|
'opl-uk-3.0': {'id': 'OPL-UK-3.0', 'deprecated': False},
|
||||||
|
'opubl-1.0': {'id': 'OPUBL-1.0', 'deprecated': False},
|
||||||
|
'oset-pl-2.1': {'id': 'OSET-PL-2.1', 'deprecated': False},
|
||||||
|
'osl-1.0': {'id': 'OSL-1.0', 'deprecated': False},
|
||||||
|
'osl-1.1': {'id': 'OSL-1.1', 'deprecated': False},
|
||||||
|
'osl-2.0': {'id': 'OSL-2.0', 'deprecated': False},
|
||||||
|
'osl-2.1': {'id': 'OSL-2.1', 'deprecated': False},
|
||||||
|
'osl-3.0': {'id': 'OSL-3.0', 'deprecated': False},
|
||||||
|
'padl': {'id': 'PADL', 'deprecated': False},
|
||||||
|
'parity-6.0.0': {'id': 'Parity-6.0.0', 'deprecated': False},
|
||||||
|
'parity-7.0.0': {'id': 'Parity-7.0.0', 'deprecated': False},
|
||||||
|
'pddl-1.0': {'id': 'PDDL-1.0', 'deprecated': False},
|
||||||
|
'php-3.0': {'id': 'PHP-3.0', 'deprecated': False},
|
||||||
|
'php-3.01': {'id': 'PHP-3.01', 'deprecated': False},
|
||||||
|
'pixar': {'id': 'Pixar', 'deprecated': False},
|
||||||
|
'pkgconf': {'id': 'pkgconf', 'deprecated': False},
|
||||||
|
'plexus': {'id': 'Plexus', 'deprecated': False},
|
||||||
|
'pnmstitch': {'id': 'pnmstitch', 'deprecated': False},
|
||||||
|
'polyform-noncommercial-1.0.0': {'id': 'PolyForm-Noncommercial-1.0.0', 'deprecated': False},
|
||||||
|
'polyform-small-business-1.0.0': {'id': 'PolyForm-Small-Business-1.0.0', 'deprecated': False},
|
||||||
|
'postgresql': {'id': 'PostgreSQL', 'deprecated': False},
|
||||||
|
'ppl': {'id': 'PPL', 'deprecated': False},
|
||||||
|
'psf-2.0': {'id': 'PSF-2.0', 'deprecated': False},
|
||||||
|
'psfrag': {'id': 'psfrag', 'deprecated': False},
|
||||||
|
'psutils': {'id': 'psutils', 'deprecated': False},
|
||||||
|
'python-2.0': {'id': 'Python-2.0', 'deprecated': False},
|
||||||
|
'python-2.0.1': {'id': 'Python-2.0.1', 'deprecated': False},
|
||||||
|
'python-ldap': {'id': 'python-ldap', 'deprecated': False},
|
||||||
|
'qhull': {'id': 'Qhull', 'deprecated': False},
|
||||||
|
'qpl-1.0': {'id': 'QPL-1.0', 'deprecated': False},
|
||||||
|
'qpl-1.0-inria-2004': {'id': 'QPL-1.0-INRIA-2004', 'deprecated': False},
|
||||||
|
'radvd': {'id': 'radvd', 'deprecated': False},
|
||||||
|
'rdisc': {'id': 'Rdisc', 'deprecated': False},
|
||||||
|
'rhecos-1.1': {'id': 'RHeCos-1.1', 'deprecated': False},
|
||||||
|
'rpl-1.1': {'id': 'RPL-1.1', 'deprecated': False},
|
||||||
|
'rpl-1.5': {'id': 'RPL-1.5', 'deprecated': False},
|
||||||
|
'rpsl-1.0': {'id': 'RPSL-1.0', 'deprecated': False},
|
||||||
|
'rsa-md': {'id': 'RSA-MD', 'deprecated': False},
|
||||||
|
'rscpl': {'id': 'RSCPL', 'deprecated': False},
|
||||||
|
'ruby': {'id': 'Ruby', 'deprecated': False},
|
||||||
|
'ruby-pty': {'id': 'Ruby-pty', 'deprecated': False},
|
||||||
|
'sax-pd': {'id': 'SAX-PD', 'deprecated': False},
|
||||||
|
'sax-pd-2.0': {'id': 'SAX-PD-2.0', 'deprecated': False},
|
||||||
|
'saxpath': {'id': 'Saxpath', 'deprecated': False},
|
||||||
|
'scea': {'id': 'SCEA', 'deprecated': False},
|
||||||
|
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
|
||||||
|
'sendmail': {'id': 'Sendmail', 'deprecated': False},
|
||||||
|
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
|
||||||
|
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
|
||||||
|
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
|
||||||
|
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
|
||||||
|
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
|
||||||
|
'sgi-opengl': {'id': 'SGI-OpenGL', 'deprecated': False},
|
||||||
|
'sgp4': {'id': 'SGP4', 'deprecated': False},
|
||||||
|
'shl-0.5': {'id': 'SHL-0.5', 'deprecated': False},
|
||||||
|
'shl-0.51': {'id': 'SHL-0.51', 'deprecated': False},
|
||||||
|
'simpl-2.0': {'id': 'SimPL-2.0', 'deprecated': False},
|
||||||
|
'sissl': {'id': 'SISSL', 'deprecated': False},
|
||||||
|
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
|
||||||
|
'sl': {'id': 'SL', 'deprecated': False},
|
||||||
|
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
|
||||||
|
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
|
||||||
|
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
|
||||||
|
'smppl': {'id': 'SMPPL', 'deprecated': False},
|
||||||
|
'snia': {'id': 'SNIA', 'deprecated': False},
|
||||||
|
'snprintf': {'id': 'snprintf', 'deprecated': False},
|
||||||
|
'sofa': {'id': 'SOFA', 'deprecated': False},
|
||||||
|
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
|
||||||
|
'soundex': {'id': 'Soundex', 'deprecated': False},
|
||||||
|
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
|
||||||
|
'spencer-94': {'id': 'Spencer-94', 'deprecated': False},
|
||||||
|
'spencer-99': {'id': 'Spencer-99', 'deprecated': False},
|
||||||
|
'spl-1.0': {'id': 'SPL-1.0', 'deprecated': False},
|
||||||
|
'ssh-keyscan': {'id': 'ssh-keyscan', 'deprecated': False},
|
||||||
|
'ssh-openssh': {'id': 'SSH-OpenSSH', 'deprecated': False},
|
||||||
|
'ssh-short': {'id': 'SSH-short', 'deprecated': False},
|
||||||
|
'ssleay-standalone': {'id': 'SSLeay-standalone', 'deprecated': False},
|
||||||
|
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
|
||||||
|
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
|
||||||
|
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
|
||||||
|
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
|
||||||
|
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
|
||||||
|
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
|
||||||
|
'sunpro': {'id': 'SunPro', 'deprecated': False},
|
||||||
|
'swl': {'id': 'SWL', 'deprecated': False},
|
||||||
|
'swrule': {'id': 'swrule', 'deprecated': False},
|
||||||
|
'symlinks': {'id': 'Symlinks', 'deprecated': False},
|
||||||
|
'tapr-ohl-1.0': {'id': 'TAPR-OHL-1.0', 'deprecated': False},
|
||||||
|
'tcl': {'id': 'TCL', 'deprecated': False},
|
||||||
|
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
|
||||||
|
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
|
||||||
|
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
|
||||||
|
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
|
||||||
|
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
|
||||||
|
'tmate': {'id': 'TMate', 'deprecated': False},
|
||||||
|
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
|
||||||
|
'tosl': {'id': 'TOSL', 'deprecated': False},
|
||||||
|
'tpdl': {'id': 'TPDL', 'deprecated': False},
|
||||||
|
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
|
||||||
|
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
|
||||||
|
'ttwl': {'id': 'TTWL', 'deprecated': False},
|
||||||
|
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
|
||||||
|
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
|
||||||
|
'tu-berlin-2.0': {'id': 'TU-Berlin-2.0', 'deprecated': False},
|
||||||
|
'ubuntu-font-1.0': {'id': 'Ubuntu-font-1.0', 'deprecated': False},
|
||||||
|
'ucar': {'id': 'UCAR', 'deprecated': False},
|
||||||
|
'ucl-1.0': {'id': 'UCL-1.0', 'deprecated': False},
|
||||||
|
'ulem': {'id': 'ulem', 'deprecated': False},
|
||||||
|
'umich-merit': {'id': 'UMich-Merit', 'deprecated': False},
|
||||||
|
'unicode-3.0': {'id': 'Unicode-3.0', 'deprecated': False},
|
||||||
|
'unicode-dfs-2015': {'id': 'Unicode-DFS-2015', 'deprecated': False},
|
||||||
|
'unicode-dfs-2016': {'id': 'Unicode-DFS-2016', 'deprecated': False},
|
||||||
|
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
|
||||||
|
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
|
||||||
|
'unlicense': {'id': 'Unlicense', 'deprecated': False},
|
||||||
|
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
|
||||||
|
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
|
||||||
|
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
|
||||||
|
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
|
||||||
|
'vim': {'id': 'Vim', 'deprecated': False},
|
||||||
|
'vostrom': {'id': 'VOSTROM', 'deprecated': False},
|
||||||
|
'vsl-1.0': {'id': 'VSL-1.0', 'deprecated': False},
|
||||||
|
'w3c': {'id': 'W3C', 'deprecated': False},
|
||||||
|
'w3c-19980720': {'id': 'W3C-19980720', 'deprecated': False},
|
||||||
|
'w3c-20150513': {'id': 'W3C-20150513', 'deprecated': False},
|
||||||
|
'w3m': {'id': 'w3m', 'deprecated': False},
|
||||||
|
'watcom-1.0': {'id': 'Watcom-1.0', 'deprecated': False},
|
||||||
|
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
|
||||||
|
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
|
||||||
|
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
|
||||||
|
'wwl': {'id': 'wwl', 'deprecated': False},
|
||||||
|
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
|
||||||
|
'x11': {'id': 'X11', 'deprecated': False},
|
||||||
|
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
|
||||||
|
'x11-swapped': {'id': 'X11-swapped', 'deprecated': False},
|
||||||
|
'xdebug-1.03': {'id': 'Xdebug-1.03', 'deprecated': False},
|
||||||
|
'xerox': {'id': 'Xerox', 'deprecated': False},
|
||||||
|
'xfig': {'id': 'Xfig', 'deprecated': False},
|
||||||
|
'xfree86-1.1': {'id': 'XFree86-1.1', 'deprecated': False},
|
||||||
|
'xinetd': {'id': 'xinetd', 'deprecated': False},
|
||||||
|
'xkeyboard-config-zinoviev': {'id': 'xkeyboard-config-Zinoviev', 'deprecated': False},
|
||||||
|
'xlock': {'id': 'xlock', 'deprecated': False},
|
||||||
|
'xnet': {'id': 'Xnet', 'deprecated': False},
|
||||||
|
'xpp': {'id': 'xpp', 'deprecated': False},
|
||||||
|
'xskat': {'id': 'XSkat', 'deprecated': False},
|
||||||
|
'xzoom': {'id': 'xzoom', 'deprecated': False},
|
||||||
|
'ypl-1.0': {'id': 'YPL-1.0', 'deprecated': False},
|
||||||
|
'ypl-1.1': {'id': 'YPL-1.1', 'deprecated': False},
|
||||||
|
'zed': {'id': 'Zed', 'deprecated': False},
|
||||||
|
'zeeff': {'id': 'Zeeff', 'deprecated': False},
|
||||||
|
'zend-2.0': {'id': 'Zend-2.0', 'deprecated': False},
|
||||||
|
'zimbra-1.3': {'id': 'Zimbra-1.3', 'deprecated': False},
|
||||||
|
'zimbra-1.4': {'id': 'Zimbra-1.4', 'deprecated': False},
|
||||||
|
'zlib': {'id': 'Zlib', 'deprecated': False},
|
||||||
|
'zlib-acknowledgement': {'id': 'zlib-acknowledgement', 'deprecated': False},
|
||||||
|
'zpl-1.1': {'id': 'ZPL-1.1', 'deprecated': False},
|
||||||
|
'zpl-2.0': {'id': 'ZPL-2.0', 'deprecated': False},
|
||||||
|
'zpl-2.1': {'id': 'ZPL-2.1', 'deprecated': False},
|
||||||
|
}
|
||||||
|
|
||||||
|
EXCEPTIONS: dict[str, SPDXException] = {
|
||||||
|
'389-exception': {'id': '389-exception', 'deprecated': False},
|
||||||
|
'asterisk-exception': {'id': 'Asterisk-exception', 'deprecated': False},
|
||||||
|
'asterisk-linking-protocols-exception': {'id': 'Asterisk-linking-protocols-exception', 'deprecated': False},
|
||||||
|
'autoconf-exception-2.0': {'id': 'Autoconf-exception-2.0', 'deprecated': False},
|
||||||
|
'autoconf-exception-3.0': {'id': 'Autoconf-exception-3.0', 'deprecated': False},
|
||||||
|
'autoconf-exception-generic': {'id': 'Autoconf-exception-generic', 'deprecated': False},
|
||||||
|
'autoconf-exception-generic-3.0': {'id': 'Autoconf-exception-generic-3.0', 'deprecated': False},
|
||||||
|
'autoconf-exception-macro': {'id': 'Autoconf-exception-macro', 'deprecated': False},
|
||||||
|
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
|
||||||
|
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
|
||||||
|
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
|
||||||
|
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
|
||||||
|
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
|
||||||
|
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
|
||||||
|
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
|
||||||
|
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
|
||||||
|
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
|
||||||
|
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
|
||||||
|
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
|
||||||
|
'fawkes-runtime-exception': {'id': 'Fawkes-Runtime-exception', 'deprecated': False},
|
||||||
|
'fltk-exception': {'id': 'FLTK-exception', 'deprecated': False},
|
||||||
|
'fmt-exception': {'id': 'fmt-exception', 'deprecated': False},
|
||||||
|
'font-exception-2.0': {'id': 'Font-exception-2.0', 'deprecated': False},
|
||||||
|
'freertos-exception-2.0': {'id': 'freertos-exception-2.0', 'deprecated': False},
|
||||||
|
'gcc-exception-2.0': {'id': 'GCC-exception-2.0', 'deprecated': False},
|
||||||
|
'gcc-exception-2.0-note': {'id': 'GCC-exception-2.0-note', 'deprecated': False},
|
||||||
|
'gcc-exception-3.1': {'id': 'GCC-exception-3.1', 'deprecated': False},
|
||||||
|
'gmsh-exception': {'id': 'Gmsh-exception', 'deprecated': False},
|
||||||
|
'gnat-exception': {'id': 'GNAT-exception', 'deprecated': False},
|
||||||
|
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
|
||||||
|
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
|
||||||
|
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
|
||||||
|
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
|
||||||
|
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
|
||||||
|
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
|
||||||
|
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
|
||||||
|
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
|
||||||
|
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
|
||||||
|
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
|
||||||
|
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
|
||||||
|
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
|
||||||
|
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
|
||||||
|
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
|
||||||
|
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
|
||||||
|
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
|
||||||
|
'libtool-exception': {'id': 'Libtool-exception', 'deprecated': False},
|
||||||
|
'linux-syscall-note': {'id': 'Linux-syscall-note', 'deprecated': False},
|
||||||
|
'llgpl': {'id': 'LLGPL', 'deprecated': False},
|
||||||
|
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
|
||||||
|
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
|
||||||
|
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
|
||||||
|
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
|
||||||
|
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
|
||||||
|
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
|
||||||
|
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
|
||||||
|
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
|
||||||
|
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
|
||||||
|
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
|
||||||
|
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
|
||||||
|
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
|
||||||
|
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
|
||||||
|
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
|
||||||
|
'qt-lgpl-exception-1.1': {'id': 'Qt-LGPL-exception-1.1', 'deprecated': False},
|
||||||
|
'qwt-exception-1.0': {'id': 'Qwt-exception-1.0', 'deprecated': False},
|
||||||
|
'romic-exception': {'id': 'romic-exception', 'deprecated': False},
|
||||||
|
'rrdtool-floss-exception-2.0': {'id': 'RRDtool-FLOSS-exception-2.0', 'deprecated': False},
|
||||||
|
'sane-exception': {'id': 'SANE-exception', 'deprecated': False},
|
||||||
|
'shl-2.0': {'id': 'SHL-2.0', 'deprecated': False},
|
||||||
|
'shl-2.1': {'id': 'SHL-2.1', 'deprecated': False},
|
||||||
|
'stunnel-exception': {'id': 'stunnel-exception', 'deprecated': False},
|
||||||
|
'swi-exception': {'id': 'SWI-exception', 'deprecated': False},
|
||||||
|
'swift-exception': {'id': 'Swift-exception', 'deprecated': False},
|
||||||
|
'texinfo-exception': {'id': 'Texinfo-exception', 'deprecated': False},
|
||||||
|
'u-boot-exception-2.0': {'id': 'u-boot-exception-2.0', 'deprecated': False},
|
||||||
|
'ubdl-exception': {'id': 'UBDL-exception', 'deprecated': False},
|
||||||
|
'universal-foss-exception-1.0': {'id': 'Universal-FOSS-exception-1.0', 'deprecated': False},
|
||||||
|
'vsftpd-openssl-exception': {'id': 'vsftpd-openssl-exception', 'deprecated': False},
|
||||||
|
'wxwindows-exception-3.1': {'id': 'WxWindows-exception-3.1', 'deprecated': False},
|
||||||
|
'x11vnc-openssl-exception': {'id': 'x11vnc-openssl-exception', 'deprecated': False},
|
||||||
|
}
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
# This file is dual licensed under the terms of the Apache License, Version
|
||||||
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
|
# for complete details.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from collections.abc import Set as AbstractSet
|
||||||
|
from typing import TYPE_CHECKING, Callable, Literal, TypedDict, Union, cast
|
||||||
|
|
||||||
|
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
|
||||||
|
from ._parser import parse_marker as _parse_marker
|
||||||
|
from ._tokenizer import ParserSyntaxError
|
||||||
|
from .specifiers import InvalidSpecifier, Specifier
|
||||||
|
from .utils import canonicalize_name
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Environment",
|
||||||
|
"EvaluateContext",
|
||||||
|
"InvalidMarker",
|
||||||
|
"Marker",
|
||||||
|
"UndefinedComparison",
|
||||||
|
"UndefinedEnvironmentName",
|
||||||
|
"default_environment",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
|
||||||
|
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
|
||||||
|
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
|
||||||
|
|
||||||
|
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
|
||||||
|
|
||||||
|
* ``"metadata"`` (for core metadata; default)
|
||||||
|
* ``"lock_file"`` (for lock files)
|
||||||
|
* ``"requirement"`` (i.e. all other situations)
|
||||||
|
|
||||||
|
.. versionadded:: 25.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
||||||
|
MARKERS_REQUIRING_VERSION = {
|
||||||
|
"implementation_version",
|
||||||
|
"platform_release",
|
||||||
|
"python_full_version",
|
||||||
|
"python_version",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMarker(ValueError):
|
||||||
|
"""Raised when attempting to create a :class:`Marker` from invalid input.
|
||||||
|
|
||||||
|
This error indicates that the given marker string does not conform to the
|
||||||
|
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UndefinedComparison(ValueError):
|
||||||
|
"""Raised when evaluating an unsupported marker comparison.
|
||||||
|
|
||||||
|
This can happen when marker values are compared as versions but do not
|
||||||
|
conform to the :ref:`specification of version specifiers
|
||||||
|
<pypug:version-specifiers>`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UndefinedEnvironmentName(KeyError):
|
||||||
|
"""Raised when evaluating a marker that references a missing environment key.
|
||||||
|
|
||||||
|
Subclasses :class:`KeyError` so that code catching the bare ``KeyError`` that
|
||||||
|
a missing environment lookup historically produced keeps working.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.3
|
||||||
|
Now subclasses :class:`KeyError` (was :class:`ValueError`) and is raised by
|
||||||
|
:meth:`Marker.evaluate` for missing environment keys, where a bare
|
||||||
|
``KeyError`` was raised before.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Environment(TypedDict):
|
||||||
|
"""
|
||||||
|
A dictionary that represents a Python environment as captured by
|
||||||
|
:func:`default_environment`. All fields are required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
implementation_name: str
|
||||||
|
"""The implementation's identifier, e.g. ``'cpython'``."""
|
||||||
|
|
||||||
|
implementation_version: str
|
||||||
|
"""
|
||||||
|
The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or
|
||||||
|
``'7.3.13'`` for PyPy3.10 v7.3.13.
|
||||||
|
"""
|
||||||
|
|
||||||
|
os_name: str
|
||||||
|
"""
|
||||||
|
The value of :py:data:`os.name`. The name of the operating system dependent module
|
||||||
|
imported, e.g. ``'posix'``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_machine: str
|
||||||
|
"""
|
||||||
|
Returns the machine type, e.g. ``'i386'``.
|
||||||
|
|
||||||
|
An empty string if the value cannot be determined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_release: str
|
||||||
|
"""
|
||||||
|
The system's release, e.g. ``'2.2.0'`` or ``'NT'``.
|
||||||
|
|
||||||
|
An empty string if the value cannot be determined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_system: str
|
||||||
|
"""
|
||||||
|
The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.
|
||||||
|
|
||||||
|
An empty string if the value cannot be determined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_version: str
|
||||||
|
"""
|
||||||
|
The system's release version, e.g. ``'#3 on degas'``.
|
||||||
|
|
||||||
|
An empty string if the value cannot be determined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
python_full_version: str
|
||||||
|
"""
|
||||||
|
The Python version as string ``'major.minor.patchlevel'``.
|
||||||
|
|
||||||
|
Note that unlike the Python :py:data:`sys.version`, this value will always include
|
||||||
|
the patchlevel (it defaults to 0).
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_python_implementation: str
|
||||||
|
"""
|
||||||
|
A string identifying the Python implementation, e.g. ``'CPython'``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
python_version: str
|
||||||
|
"""The Python version as string ``'major.minor'``."""
|
||||||
|
|
||||||
|
sys_platform: str
|
||||||
|
"""
|
||||||
|
This string contains a platform identifier that can be used to append
|
||||||
|
platform-specific components to :py:data:`sys.path`, for instance.
|
||||||
|
|
||||||
|
For Unix systems, except on Linux and AIX, this is the lowercased OS name as
|
||||||
|
returned by ``uname -s`` with the first part of the version as returned by
|
||||||
|
``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python
|
||||||
|
was built.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_extras(
|
||||||
|
result: MarkerList | MarkerAtom | str,
|
||||||
|
) -> MarkerList | MarkerAtom | str:
|
||||||
|
if isinstance(result, list):
|
||||||
|
return [_normalize_extras(r) for r in result]
|
||||||
|
if not isinstance(result, tuple):
|
||||||
|
return result
|
||||||
|
|
||||||
|
lhs, op, rhs = result
|
||||||
|
if isinstance(lhs, Variable) and lhs.value == "extra" and isinstance(rhs, Value):
|
||||||
|
normalized_extra = canonicalize_name(rhs.value)
|
||||||
|
rhs = Value(normalized_extra)
|
||||||
|
elif isinstance(rhs, Variable) and rhs.value == "extra" and isinstance(lhs, Value):
|
||||||
|
normalized_extra = canonicalize_name(lhs.value)
|
||||||
|
lhs = Value(normalized_extra)
|
||||||
|
elif (
|
||||||
|
isinstance(rhs, Variable)
|
||||||
|
and rhs.value in MARKERS_ALLOWING_SET
|
||||||
|
and isinstance(lhs, Value)
|
||||||
|
):
|
||||||
|
# PEP 685 (extras) / PEP 735 (dependency_groups): the set-valued membership
|
||||||
|
# literal must also be normalized. evaluate() already canonicalizes both
|
||||||
|
# operands for these keys (see _normalize), so normalizing the literal at
|
||||||
|
# parse time keeps __str__/__eq__/__hash__ consistent with evaluate() -- e.g.
|
||||||
|
# Marker('"Foo" in extras') and Marker('"foo" in extras') must compare and
|
||||||
|
# hash equal (the membership variable is always the right-hand operand).
|
||||||
|
lhs = Value(canonicalize_name(lhs.value))
|
||||||
|
return lhs, op, rhs
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_extra_values(results: MarkerList) -> MarkerList:
|
||||||
|
"""
|
||||||
|
Normalize extra values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [_normalize_extras(r) for r in results]
|
||||||
|
|
||||||
|
|
||||||
|
def _format_marker(
|
||||||
|
marker: list[str] | MarkerAtom | str, first: bool | None = True
|
||||||
|
) -> str:
|
||||||
|
assert isinstance(marker, (list, tuple, str))
|
||||||
|
|
||||||
|
# Unwrap a redundant [[...]] wrapper, but keep the nesting context so a
|
||||||
|
# nested group keeps the parentheses its and/or precedence needs.
|
||||||
|
if (
|
||||||
|
isinstance(marker, list)
|
||||||
|
and len(marker) == 1
|
||||||
|
and isinstance(marker[0], (list, tuple))
|
||||||
|
):
|
||||||
|
return _format_marker(marker[0], first=first)
|
||||||
|
|
||||||
|
if isinstance(marker, list):
|
||||||
|
inner = (_format_marker(m, first=False) for m in marker)
|
||||||
|
if first:
|
||||||
|
return " ".join(inner)
|
||||||
|
else:
|
||||||
|
return "(" + " ".join(inner) + ")"
|
||||||
|
elif isinstance(marker, tuple):
|
||||||
|
return " ".join([m.serialize() for m in marker])
|
||||||
|
else:
|
||||||
|
return marker
|
||||||
|
|
||||||
|
|
||||||
|
_operators: dict[str, Operator] = {
|
||||||
|
"in": lambda lhs, rhs: lhs in rhs,
|
||||||
|
"not in": lambda lhs, rhs: lhs not in rhs,
|
||||||
|
"<": lambda _lhs, _rhs: False,
|
||||||
|
"<=": operator.eq,
|
||||||
|
"==": operator.eq,
|
||||||
|
"!=": operator.ne,
|
||||||
|
">=": operator.eq,
|
||||||
|
">": lambda _lhs, _rhs: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
|
||||||
|
op_str = op.serialize()
|
||||||
|
if key in MARKERS_REQUIRING_VERSION:
|
||||||
|
try:
|
||||||
|
spec = Specifier(f"{op_str}{rhs}")
|
||||||
|
except InvalidSpecifier:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return spec.contains(lhs, prereleases=True)
|
||||||
|
|
||||||
|
oper: Operator | None = _operators.get(op_str)
|
||||||
|
if oper is None:
|
||||||
|
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
|
||||||
|
|
||||||
|
return oper(lhs, rhs)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(
|
||||||
|
lhs: str, rhs: str | AbstractSet[str], key: str
|
||||||
|
) -> tuple[str, str | AbstractSet[str]]:
|
||||||
|
# PEP 685 - Comparison of extra names for optional distribution dependencies
|
||||||
|
# https://peps.python.org/pep-0685/
|
||||||
|
# > When comparing extra names, tools MUST normalize the names being
|
||||||
|
# > compared using the semantics outlined in PEP 503 for names
|
||||||
|
if key == "extra":
|
||||||
|
assert isinstance(rhs, str), "extra value must be a string"
|
||||||
|
# Both sides are normalized at this point already
|
||||||
|
return (lhs, rhs)
|
||||||
|
if key in MARKERS_ALLOWING_SET:
|
||||||
|
if isinstance(rhs, str): # pragma: no cover
|
||||||
|
return (canonicalize_name(lhs), canonicalize_name(rhs))
|
||||||
|
else:
|
||||||
|
return (canonicalize_name(lhs), {canonicalize_name(v) for v in rhs})
|
||||||
|
|
||||||
|
# other environment markers don't have such standards
|
||||||
|
return lhs, rhs
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_environment(
|
||||||
|
environment: dict[str, str | AbstractSet[str]], key: str
|
||||||
|
) -> str | AbstractSet[str]:
|
||||||
|
try:
|
||||||
|
return environment[key]
|
||||||
|
except KeyError:
|
||||||
|
raise UndefinedEnvironmentName(key) from None
|
||||||
|
|
||||||
|
|
||||||
|
def _evaluate_markers(
|
||||||
|
markers: MarkerList, environment: dict[str, str | AbstractSet[str]]
|
||||||
|
) -> bool:
|
||||||
|
groups: list[list[bool]] = [[]]
|
||||||
|
|
||||||
|
for marker in markers:
|
||||||
|
if isinstance(marker, list):
|
||||||
|
groups[-1].append(_evaluate_markers(marker, environment))
|
||||||
|
elif isinstance(marker, tuple):
|
||||||
|
lhs, op, rhs = marker
|
||||||
|
|
||||||
|
if isinstance(lhs, Variable):
|
||||||
|
environment_key = lhs.value
|
||||||
|
lhs_value = _lookup_environment(environment, environment_key)
|
||||||
|
rhs_value = rhs.value
|
||||||
|
else:
|
||||||
|
lhs_value = lhs.value
|
||||||
|
environment_key = rhs.value
|
||||||
|
rhs_value = _lookup_environment(environment, environment_key)
|
||||||
|
|
||||||
|
if not isinstance(lhs_value, str):
|
||||||
|
raise UndefinedComparison(
|
||||||
|
f"Set-valued marker {environment_key!r} can only be used "
|
||||||
|
f'with the membership form (e.g. "<name>" in '
|
||||||
|
f"{environment_key}); it cannot appear on the left-hand "
|
||||||
|
f"side of {op.serialize()!r}."
|
||||||
|
)
|
||||||
|
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
|
||||||
|
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
|
||||||
|
elif marker == "or":
|
||||||
|
groups.append([])
|
||||||
|
elif marker == "and":
|
||||||
|
pass
|
||||||
|
else: # pragma: nocover
|
||||||
|
raise TypeError(f"Unexpected marker {marker!r}")
|
||||||
|
|
||||||
|
return any(all(item) for item in groups)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_full_version(info: sys._version_info) -> str:
|
||||||
|
version = f"{info.major}.{info.minor}.{info.micro}"
|
||||||
|
kind = info.releaselevel
|
||||||
|
if kind != "final":
|
||||||
|
version += kind[0] + str(info.serial)
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def _cached_default_environment() -> Environment:
|
||||||
|
"""Build the default marker environment for the current Python process.
|
||||||
|
|
||||||
|
The values are derived from process-constant data (the running interpreter
|
||||||
|
and the host platform), so this is cached and built only once. The result is
|
||||||
|
shared between callers and must never be mutated; :func:`default_environment`
|
||||||
|
returns a fresh copy.
|
||||||
|
"""
|
||||||
|
iver = _format_full_version(sys.implementation.version)
|
||||||
|
implementation_name = sys.implementation.name
|
||||||
|
return {
|
||||||
|
"implementation_name": implementation_name,
|
||||||
|
"implementation_version": iver,
|
||||||
|
"os_name": os.name,
|
||||||
|
"platform_machine": platform.machine(),
|
||||||
|
"platform_release": platform.release(),
|
||||||
|
"platform_system": platform.system(),
|
||||||
|
"platform_version": platform.version(),
|
||||||
|
"python_full_version": platform.python_version(),
|
||||||
|
"platform_python_implementation": platform.python_implementation(),
|
||||||
|
"python_version": ".".join(platform.python_version_tuple()[:2]),
|
||||||
|
"sys_platform": sys.platform,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def default_environment() -> Environment:
|
||||||
|
"""Return the default marker environment for the current Python process.
|
||||||
|
|
||||||
|
This is the base environment used by :meth:`Marker.evaluate`. A fresh copy
|
||||||
|
is returned on every call so callers may freely mutate the result; a shallow
|
||||||
|
copy suffices because all values are immutable strings.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.3
|
||||||
|
The environment is computed once per process and cached, since it is
|
||||||
|
derived from process-constant data. Patching ``platform``/``sys``/``os``
|
||||||
|
after the first call has no effect; pass an explicit ``environment`` to
|
||||||
|
:meth:`Marker.evaluate` to evaluate against different values.
|
||||||
|
"""
|
||||||
|
return cast("Environment", dict(_cached_default_environment()))
|
||||||
|
|
||||||
|
|
||||||
|
class Marker:
|
||||||
|
"""Represents a parsed dependency marker expression.
|
||||||
|
|
||||||
|
Marker expressions are parsed according to the
|
||||||
|
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||||
|
|
||||||
|
:param marker: The string representation of a marker expression.
|
||||||
|
:raises InvalidMarker: If ``marker`` cannot be parsed.
|
||||||
|
|
||||||
|
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||||
|
format so the same pickle can be loaded in future packaging releases.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.2
|
||||||
|
|
||||||
|
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||||
|
be unpickled with future releases. Backward compatibility with pickles
|
||||||
|
from packaging < 26.2 is supported but may be removed in a future
|
||||||
|
release.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_markers",)
|
||||||
|
|
||||||
|
def __init__(self, marker: str) -> None:
|
||||||
|
# Note: We create a Marker object without calling this constructor in
|
||||||
|
# packaging.requirements.Requirement. If any additional logic is
|
||||||
|
# added here, make sure to mirror/adapt Requirement.
|
||||||
|
|
||||||
|
# If this fails and throws an error, the repr still expects _markers to
|
||||||
|
# be defined.
|
||||||
|
self._markers: MarkerList = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._markers = _normalize_extra_values(_parse_marker(marker))
|
||||||
|
# The attribute `_markers` can be described in terms of a recursive type:
|
||||||
|
# MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
|
||||||
|
#
|
||||||
|
# For example, the following expression:
|
||||||
|
# python_version > "3.6" or (python_version == "3.6" and os_name == "unix")
|
||||||
|
#
|
||||||
|
# is parsed into:
|
||||||
|
# [
|
||||||
|
# (<Variable('python_version')>, <Op('>')>, <Value('3.6')>),
|
||||||
|
# 'and',
|
||||||
|
# [
|
||||||
|
# (<Variable('python_version')>, <Op('==')>, <Value('3.6')>),
|
||||||
|
# 'or',
|
||||||
|
# (<Variable('os_name')>, <Op('==')>, <Value('unix')>)
|
||||||
|
# ]
|
||||||
|
# ]
|
||||||
|
except ParserSyntaxError as e:
|
||||||
|
raise InvalidMarker(str(e)) from e
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_markers(cls, markers: MarkerList) -> Marker:
|
||||||
|
"""Create a Marker instance from a pre-parsed marker tree.
|
||||||
|
|
||||||
|
This avoids re-parsing serialised marker strings when combining markers.
|
||||||
|
"""
|
||||||
|
new = cls.__new__(cls)
|
||||||
|
new._markers = markers
|
||||||
|
return new
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return _format_marker(self._markers)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(str(self))
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Marker):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return str(self) == str(other)
|
||||||
|
|
||||||
|
def __getstate__(self) -> str:
|
||||||
|
# Return the marker expression string for compactness and stability.
|
||||||
|
# Internal Node objects are excluded; the string is re-parsed on load.
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def __setstate__(self, state: object) -> None:
|
||||||
|
if isinstance(state, str):
|
||||||
|
# New format (26.2+): just the marker expression string.
|
||||||
|
try:
|
||||||
|
self._markers = _normalize_extra_values(_parse_marker(state))
|
||||||
|
except ParserSyntaxError as exc:
|
||||||
|
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
|
||||||
|
return
|
||||||
|
if isinstance(state, dict) and "_markers" in state:
|
||||||
|
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||||
|
markers = state["_markers"]
|
||||||
|
if isinstance(markers, list):
|
||||||
|
self._markers = markers
|
||||||
|
return
|
||||||
|
if isinstance(state, tuple) and len(state) == 2:
|
||||||
|
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||||
|
_, slot_dict = state
|
||||||
|
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
|
||||||
|
markers = slot_dict["_markers"]
|
||||||
|
if isinstance(markers, list):
|
||||||
|
self._markers = markers
|
||||||
|
return
|
||||||
|
raise TypeError(f"Cannot restore Marker from {state!r}")
|
||||||
|
|
||||||
|
def __and__(self, other: Marker) -> Marker:
|
||||||
|
"""Combine this marker with another using ``and``.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
if not isinstance(other, Marker):
|
||||||
|
return NotImplemented
|
||||||
|
return self._from_markers([self._markers, "and", other._markers])
|
||||||
|
|
||||||
|
def __or__(self, other: Marker) -> Marker:
|
||||||
|
"""Combine this marker with another using ``or``.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
if not isinstance(other, Marker):
|
||||||
|
return NotImplemented
|
||||||
|
return self._from_markers([self._markers, "or", other._markers])
|
||||||
|
|
||||||
|
def evaluate(
|
||||||
|
self,
|
||||||
|
environment: Mapping[str, str | AbstractSet[str]] | None = None,
|
||||||
|
context: EvaluateContext = "metadata",
|
||||||
|
) -> bool:
|
||||||
|
"""Evaluate a marker.
|
||||||
|
|
||||||
|
Return the boolean from evaluating this marker against the environment.
|
||||||
|
The environment is determined from the current Python process unless
|
||||||
|
passed in explicitly.
|
||||||
|
|
||||||
|
:param environment: Mapping containing keys and values to override the
|
||||||
|
detected environment.
|
||||||
|
:param EvaluateContext context: The context in which the marker is
|
||||||
|
evaluated, which influences what marker names are considered valid.
|
||||||
|
Accepted values are ``"metadata"`` (for core metadata; default),
|
||||||
|
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
|
||||||
|
:raises UndefinedComparison: If the marker uses a comparison on values
|
||||||
|
that are not valid versions per the :ref:`specification of version
|
||||||
|
specifiers <pypug:version-specifiers>`.
|
||||||
|
:raises UndefinedEnvironmentName: If the marker references a value that
|
||||||
|
is missing from the evaluation environment.
|
||||||
|
:returns: ``True`` if the marker matches, otherwise ``False``.
|
||||||
|
|
||||||
|
.. versionchanged:: 25.0
|
||||||
|
Added the ``context`` parameter, which influences which marker names
|
||||||
|
are considered valid.
|
||||||
|
"""
|
||||||
|
current_environment = cast(
|
||||||
|
"dict[str, str | AbstractSet[str]]", default_environment()
|
||||||
|
)
|
||||||
|
if context == "lock_file":
|
||||||
|
current_environment |= {
|
||||||
|
"extras": frozenset(),
|
||||||
|
"dependency_groups": frozenset(),
|
||||||
|
}
|
||||||
|
elif context == "metadata":
|
||||||
|
current_environment["extra"] = ""
|
||||||
|
|
||||||
|
if environment is not None:
|
||||||
|
current_environment |= environment
|
||||||
|
if "extra" in current_environment:
|
||||||
|
# The API used to allow setting extra to None. We need to handle
|
||||||
|
# this case for backwards compatibility. Also skip running
|
||||||
|
# normalize name if extra is empty.
|
||||||
|
extra = cast("str | None", current_environment["extra"])
|
||||||
|
current_environment["extra"] = canonicalize_name(extra) if extra else ""
|
||||||
|
|
||||||
|
return _evaluate_markers(
|
||||||
|
self._markers, _repair_python_full_version(current_environment)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pep440_python_full_version(python_full_version: str) -> str:
|
||||||
|
"""
|
||||||
|
Work around platform.python_version() returning something that is not PEP 440
|
||||||
|
compliant for non-tagged Python builds.
|
||||||
|
"""
|
||||||
|
if python_full_version.endswith("+"):
|
||||||
|
return f"{python_full_version}local"
|
||||||
|
return python_full_version
|
||||||
|
|
||||||
|
|
||||||
|
def _repair_python_full_version(
|
||||||
|
env: dict[str, str | AbstractSet[str]],
|
||||||
|
) -> dict[str, str | AbstractSet[str]]:
|
||||||
|
"""
|
||||||
|
Work around platform.python_version() returning something that is not PEP 440
|
||||||
|
compliant for non-tagged Python builds.
|
||||||
|
"""
|
||||||
|
python_full_version = cast("str", env["python_full_version"])
|
||||||
|
env["python_full_version"] = _pep440_python_full_version(python_full_version)
|
||||||
|
return env
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,943 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Protocol,
|
||||||
|
TypeVar,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
from urllib.parse import unquote, urlparse
|
||||||
|
|
||||||
|
from .markers import (
|
||||||
|
Environment,
|
||||||
|
Marker,
|
||||||
|
_pep440_python_full_version,
|
||||||
|
default_environment,
|
||||||
|
)
|
||||||
|
from .specifiers import SpecifierSet
|
||||||
|
from .tags import create_compatible_tags_selector, sys_tags
|
||||||
|
from .utils import (
|
||||||
|
NormalizedName,
|
||||||
|
is_normalized_name,
|
||||||
|
parse_sdist_filename,
|
||||||
|
parse_wheel_filename,
|
||||||
|
)
|
||||||
|
from .version import Version
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from collections.abc import Collection, Iterator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from .tags import Tag
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Package",
|
||||||
|
"PackageArchive",
|
||||||
|
"PackageDirectory",
|
||||||
|
"PackageSdist",
|
||||||
|
"PackageVcs",
|
||||||
|
"PackageWheel",
|
||||||
|
"Pylock",
|
||||||
|
"PylockSelectError",
|
||||||
|
"PylockUnsupportedVersionError",
|
||||||
|
"PylockValidationError",
|
||||||
|
"is_valid_pylock_path",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
_T2 = TypeVar("_T2")
|
||||||
|
|
||||||
|
|
||||||
|
class _FromMappingProtocol(Protocol): # pragma: no cover
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
|
||||||
|
|
||||||
|
|
||||||
|
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
|
||||||
|
|
||||||
|
|
||||||
|
_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_pylock_path(path: Path) -> bool:
|
||||||
|
"""Check if the given path is a valid pylock file path."""
|
||||||
|
return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_key(key: str) -> str:
|
||||||
|
return key.replace("_", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
|
||||||
|
if isinstance(value, (Version, Marker, SpecifierSet)):
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, Sequence) and key == "environments":
|
||||||
|
return [str(v) for v in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
_toml_key(key): _toml_value(key, value)
|
||||||
|
for key, value in data
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
|
||||||
|
"""Get a value from the dictionary and verify it's the expected type."""
|
||||||
|
if (value := d.get(key)) is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(value, expected_type) or (
|
||||||
|
# Special case: bool is a subclass of int, but TOML distinguishes the two
|
||||||
|
expected_type is int and isinstance(value, bool)
|
||||||
|
):
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Unexpected type {type(value).__name__} "
|
||||||
|
f"(expected {expected_type.__name__})",
|
||||||
|
context=key,
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
|
||||||
|
"""Get a required value from the dictionary and verify it's the expected type."""
|
||||||
|
if (value := _get(d, expected_type, key)) is None:
|
||||||
|
raise _PylockRequiredKeyError(key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sequence(
|
||||||
|
d: Mapping[str, Any], expected_item_type: type[_T], key: str
|
||||||
|
) -> Sequence[_T] | None:
|
||||||
|
"""Get a list value from the dictionary and verify it's the expected items type."""
|
||||||
|
if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
|
||||||
|
return None
|
||||||
|
if isinstance(value, (str, bytes)):
|
||||||
|
# special case: str and bytes are Sequences, but we want to reject it
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Unexpected type {type(value).__name__} (expected Sequence)",
|
||||||
|
context=key,
|
||||||
|
)
|
||||||
|
for i, item in enumerate(value):
|
||||||
|
if not isinstance(item, expected_item_type):
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Unexpected type {type(item).__name__} "
|
||||||
|
f"(expected {expected_item_type.__name__})",
|
||||||
|
context=f"{key}[{i}]",
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_as(
|
||||||
|
d: Mapping[str, Any],
|
||||||
|
expected_type: type[_T],
|
||||||
|
target_type: Callable[[_T], _T2],
|
||||||
|
key: str,
|
||||||
|
) -> _T2 | None:
|
||||||
|
"""Get a value from the dictionary, verify it's the expected type,
|
||||||
|
and convert to the target type.
|
||||||
|
|
||||||
|
This assumes the target_type constructor accepts the value.
|
||||||
|
"""
|
||||||
|
if (value := _get(d, expected_type, key)) is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return target_type(value)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=key) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_as(
|
||||||
|
d: Mapping[str, Any],
|
||||||
|
expected_type: type[_T],
|
||||||
|
target_type: Callable[[_T], _T2],
|
||||||
|
key: str,
|
||||||
|
) -> _T2:
|
||||||
|
"""Get a required value from the dict, verify it's the expected type,
|
||||||
|
and convert to the target type."""
|
||||||
|
if (value := _get_as(d, expected_type, target_type, key)) is None:
|
||||||
|
raise _PylockRequiredKeyError(key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sequence_as(
|
||||||
|
d: Mapping[str, Any],
|
||||||
|
expected_item_type: type[_T],
|
||||||
|
target_item_type: Callable[[_T], _T2],
|
||||||
|
key: str,
|
||||||
|
) -> list[_T2] | None:
|
||||||
|
"""Get list value from dictionary and verify expected items type."""
|
||||||
|
if (value := _get_sequence(d, expected_item_type, key)) is None:
|
||||||
|
return None
|
||||||
|
result = []
|
||||||
|
try:
|
||||||
|
for item in value:
|
||||||
|
typed_item = target_item_type(item)
|
||||||
|
result.append(typed_item)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_object(
|
||||||
|
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
|
||||||
|
) -> _FromMappingProtocolT | None:
|
||||||
|
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
|
||||||
|
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return target_type._from_dict(value)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=key) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sequence_of_objects(
|
||||||
|
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
|
||||||
|
) -> list[_FromMappingProtocolT] | None:
|
||||||
|
"""Get a list value from the dictionary and convert its items to a dataclass."""
|
||||||
|
if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||||
|
return None
|
||||||
|
result: list[_FromMappingProtocolT] = []
|
||||||
|
try:
|
||||||
|
for item in value:
|
||||||
|
typed_item = target_item_type._from_dict(item)
|
||||||
|
result.append(typed_item)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_sequence_of_objects(
|
||||||
|
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
|
||||||
|
) -> Sequence[_FromMappingProtocolT]:
|
||||||
|
"""Get a required list value from the dictionary and convert its items to a
|
||||||
|
dataclass."""
|
||||||
|
if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
|
||||||
|
raise _PylockRequiredKeyError(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_normalized_name(name: str) -> NormalizedName:
|
||||||
|
"""Validate that a string is a NormalizedName."""
|
||||||
|
if not is_normalized_name(name):
|
||||||
|
raise PylockValidationError(f"Name {name!r} is not normalized")
|
||||||
|
return NormalizedName(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_path_url(path: str | None, url: str | None) -> None:
|
||||||
|
if not path and not url:
|
||||||
|
raise PylockValidationError("path or url must be provided")
|
||||||
|
|
||||||
|
|
||||||
|
def _path_name(path: str | None) -> str | None:
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
# If the path is relative it MAY use POSIX-style path separators explicitly
|
||||||
|
# for portability
|
||||||
|
if "/" in path:
|
||||||
|
return path.rsplit("/", 1)[-1]
|
||||||
|
elif "\\" in path:
|
||||||
|
return path.rsplit("\\", 1)[-1]
|
||||||
|
else:
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _url_name(url: str | None) -> str | None:
|
||||||
|
if not url:
|
||||||
|
return None
|
||||||
|
url_path = urlparse(url).path
|
||||||
|
# The last path component is percent-encoded, so decode it to the file name
|
||||||
|
return unquote(url_path.rsplit("/", 1)[-1])
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||||
|
if not hashes:
|
||||||
|
raise PylockValidationError("At least one hash must be provided")
|
||||||
|
if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
|
||||||
|
raise PylockValidationError("Hash values must be strings")
|
||||||
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
class PylockValidationError(Exception):
|
||||||
|
"""Raised when when input data is not spec-compliant."""
|
||||||
|
|
||||||
|
context: str | None = None
|
||||||
|
message: str
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cause: str | Exception,
|
||||||
|
*,
|
||||||
|
context: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
if isinstance(cause, PylockValidationError):
|
||||||
|
if cause.context:
|
||||||
|
self.context = (
|
||||||
|
f"{context}.{cause.context}" if context else cause.context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.context = context
|
||||||
|
self.message = cause.message
|
||||||
|
else:
|
||||||
|
self.context = context
|
||||||
|
self.message = str(cause)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.context:
|
||||||
|
return f"{self.message} in {self.context!r}"
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class _PylockRequiredKeyError(PylockValidationError):
|
||||||
|
def __init__(self, key: str) -> None:
|
||||||
|
super().__init__("Missing required value", context=key)
|
||||||
|
|
||||||
|
|
||||||
|
class PylockUnsupportedVersionError(PylockValidationError):
|
||||||
|
"""Raised when encountering an unsupported `lock_version`."""
|
||||||
|
|
||||||
|
|
||||||
|
class PylockSelectError(Exception):
|
||||||
|
"""Base exception for errors raised by :meth:`Pylock.select`.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageVcs:
|
||||||
|
type: str
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
requested_revision: str | None = None
|
||||||
|
commit_id: str # type: ignore[misc]
|
||||||
|
subdirectory: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
type: str,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
requested_revision: str | None = None,
|
||||||
|
commit_id: str,
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "type", type)
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "requested_revision", requested_revision)
|
||||||
|
object.__setattr__(self, "commit_id", commit_id)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_vcs = cls(
|
||||||
|
type=_get_required(d, str, "type"),
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
requested_revision=_get(d, str, "requested-revision"),
|
||||||
|
commit_id=_get_required(d, str, "commit-id"),
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
_validate_path_url(package_vcs.path, package_vcs.url)
|
||||||
|
return package_vcs
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageDirectory:
|
||||||
|
path: str
|
||||||
|
editable: bool | None = None
|
||||||
|
subdirectory: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
editable: bool | None = None,
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "editable", editable)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
path=_get_required(d, str, "path"),
|
||||||
|
editable=_get(d, bool, "editable"),
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageArchive:
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
upload_time: datetime | None = None
|
||||||
|
hashes: Mapping[str, str] # type: ignore[misc]
|
||||||
|
subdirectory: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
upload_time: datetime | None = None,
|
||||||
|
hashes: Mapping[str, str],
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "size", size)
|
||||||
|
object.__setattr__(self, "upload_time", upload_time)
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_archive = cls(
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
size=_get(d, int, "size"),
|
||||||
|
upload_time=_get(d, datetime, "upload-time"),
|
||||||
|
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
_validate_path_url(package_archive.path, package_archive.url)
|
||||||
|
return package_archive
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageSdist:
|
||||||
|
name: str | None = None
|
||||||
|
upload_time: datetime | None = None
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
hashes: Mapping[str, str] # type: ignore[misc]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
upload_time: datetime | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
hashes: Mapping[str, str],
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "upload_time", upload_time)
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "size", size)
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_sdist = cls(
|
||||||
|
name=_get(d, str, "name"),
|
||||||
|
upload_time=_get(d, datetime, "upload-time"),
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
size=_get(d, int, "size"),
|
||||||
|
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
_validate_path_url(package_sdist.path, package_sdist.url)
|
||||||
|
return package_sdist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
"""Get the filename of the sdist.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
"""
|
||||||
|
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||||
|
if not filename:
|
||||||
|
raise PylockValidationError("Cannot determine sdist filename")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageWheel:
|
||||||
|
name: str | None = None
|
||||||
|
upload_time: datetime | None = None
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
hashes: Mapping[str, str] # type: ignore[misc]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
upload_time: datetime | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
hashes: Mapping[str, str],
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "upload_time", upload_time)
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "size", size)
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_wheel = cls(
|
||||||
|
name=_get(d, str, "name"),
|
||||||
|
upload_time=_get(d, datetime, "upload-time"),
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
size=_get(d, int, "size"),
|
||||||
|
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
_validate_path_url(package_wheel.path, package_wheel.url)
|
||||||
|
return package_wheel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
"""Get the filename of the wheel."""
|
||||||
|
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||||
|
if not filename:
|
||||||
|
raise PylockValidationError("Cannot determine wheel filename")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class Package:
|
||||||
|
name: NormalizedName
|
||||||
|
version: Version | None = None
|
||||||
|
marker: Marker | None = None
|
||||||
|
requires_python: SpecifierSet | None = None
|
||||||
|
dependencies: Sequence[Mapping[str, Any]] | None = None
|
||||||
|
vcs: PackageVcs | None = None
|
||||||
|
directory: PackageDirectory | None = None
|
||||||
|
archive: PackageArchive | None = None
|
||||||
|
index: str | None = None
|
||||||
|
sdist: PackageSdist | None = None
|
||||||
|
wheels: Sequence[PackageWheel] | None = None
|
||||||
|
attestation_identities: Sequence[Mapping[str, Any]] | None = None
|
||||||
|
tool: Mapping[str, Any] | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: NormalizedName,
|
||||||
|
version: Version | None = None,
|
||||||
|
marker: Marker | None = None,
|
||||||
|
requires_python: SpecifierSet | None = None,
|
||||||
|
dependencies: Sequence[Mapping[str, Any]] | None = None,
|
||||||
|
vcs: PackageVcs | None = None,
|
||||||
|
directory: PackageDirectory | None = None,
|
||||||
|
archive: PackageArchive | None = None,
|
||||||
|
index: str | None = None,
|
||||||
|
sdist: PackageSdist | None = None,
|
||||||
|
wheels: Sequence[PackageWheel] | None = None,
|
||||||
|
attestation_identities: Sequence[Mapping[str, Any]] | None = None,
|
||||||
|
tool: Mapping[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "version", version)
|
||||||
|
object.__setattr__(self, "marker", marker)
|
||||||
|
object.__setattr__(self, "requires_python", requires_python)
|
||||||
|
object.__setattr__(self, "dependencies", dependencies)
|
||||||
|
object.__setattr__(self, "vcs", vcs)
|
||||||
|
object.__setattr__(self, "directory", directory)
|
||||||
|
object.__setattr__(self, "archive", archive)
|
||||||
|
object.__setattr__(self, "index", index)
|
||||||
|
object.__setattr__(self, "sdist", sdist)
|
||||||
|
object.__setattr__(self, "wheels", wheels)
|
||||||
|
object.__setattr__(self, "attestation_identities", attestation_identities)
|
||||||
|
object.__setattr__(self, "tool", tool)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package = cls(
|
||||||
|
name=_get_required_as(d, str, _validate_normalized_name, "name"),
|
||||||
|
version=_get_as(d, str, Version, "version"),
|
||||||
|
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
|
||||||
|
dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
|
||||||
|
marker=_get_as(d, str, Marker, "marker"),
|
||||||
|
vcs=_get_object(d, PackageVcs, "vcs"),
|
||||||
|
directory=_get_object(d, PackageDirectory, "directory"),
|
||||||
|
archive=_get_object(d, PackageArchive, "archive"),
|
||||||
|
index=_get(d, str, "index"),
|
||||||
|
sdist=_get_object(d, PackageSdist, "sdist"),
|
||||||
|
wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
|
||||||
|
attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
|
||||||
|
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
distributions = bool(package.sdist) + len(package.wheels or [])
|
||||||
|
direct_urls = (
|
||||||
|
bool(package.vcs) + bool(package.directory) + bool(package.archive)
|
||||||
|
)
|
||||||
|
if distributions > 0 and direct_urls > 0:
|
||||||
|
raise PylockValidationError(
|
||||||
|
"None of vcs, directory, archive must be set if sdist or wheels are set"
|
||||||
|
)
|
||||||
|
if distributions == 0 and direct_urls != 1:
|
||||||
|
raise PylockValidationError(
|
||||||
|
"Exactly one of vcs, directory, archive must be set "
|
||||||
|
"if sdist and wheels are not set"
|
||||||
|
)
|
||||||
|
for i, wheel in enumerate(package.wheels or []):
|
||||||
|
try:
|
||||||
|
(name, version, _, _) = parse_wheel_filename(wheel.filename)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Invalid wheel filename {wheel.filename!r}",
|
||||||
|
context=f"wheels[{i}]",
|
||||||
|
) from e
|
||||||
|
if name != package.name:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Name in {wheel.filename!r} is not consistent with "
|
||||||
|
f"package name {package.name!r}",
|
||||||
|
context=f"wheels[{i}]",
|
||||||
|
)
|
||||||
|
if package.version and version != package.version:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Version in {wheel.filename!r} is not consistent with "
|
||||||
|
f"package version {str(package.version)!r}",
|
||||||
|
context=f"wheels[{i}]",
|
||||||
|
)
|
||||||
|
if package.sdist:
|
||||||
|
try:
|
||||||
|
name, version = parse_sdist_filename(package.sdist.filename)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Invalid sdist filename {package.sdist.filename!r}",
|
||||||
|
context="sdist",
|
||||||
|
) from e
|
||||||
|
if name != package.name:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Name in {package.sdist.filename!r} is not consistent with "
|
||||||
|
f"package name {package.name!r}",
|
||||||
|
context="sdist",
|
||||||
|
)
|
||||||
|
if package.version and version != package.version:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Version in {package.sdist.filename!r} is not consistent with "
|
||||||
|
f"package version {str(package.version)!r}",
|
||||||
|
context="sdist",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
for i, attestation_identity in enumerate( # noqa: B007
|
||||||
|
package.attestation_identities or []
|
||||||
|
):
|
||||||
|
_get_required(attestation_identity, str, "kind")
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(
|
||||||
|
e, context=f"attestation-identities[{i}]"
|
||||||
|
) from e
|
||||||
|
return package
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_direct(self) -> bool:
|
||||||
|
return not (self.sdist or self.wheels)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class Pylock:
|
||||||
|
"""A class representing a pylock file."""
|
||||||
|
|
||||||
|
lock_version: Version
|
||||||
|
environments: Sequence[Marker] | None = None
|
||||||
|
requires_python: SpecifierSet | None = None
|
||||||
|
extras: Sequence[NormalizedName] | None = None
|
||||||
|
dependency_groups: Sequence[str] | None = None
|
||||||
|
default_groups: Sequence[str] | None = None
|
||||||
|
created_by: str # type: ignore[misc]
|
||||||
|
packages: Sequence[Package] # type: ignore[misc]
|
||||||
|
tool: Mapping[str, Any] | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
lock_version: Version,
|
||||||
|
environments: Sequence[Marker] | None = None,
|
||||||
|
requires_python: SpecifierSet | None = None,
|
||||||
|
extras: Sequence[NormalizedName] | None = None,
|
||||||
|
dependency_groups: Sequence[str] | None = None,
|
||||||
|
default_groups: Sequence[str] | None = None,
|
||||||
|
created_by: str,
|
||||||
|
packages: Sequence[Package],
|
||||||
|
tool: Mapping[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "lock_version", lock_version)
|
||||||
|
object.__setattr__(self, "environments", environments)
|
||||||
|
object.__setattr__(self, "requires_python", requires_python)
|
||||||
|
object.__setattr__(self, "extras", extras)
|
||||||
|
object.__setattr__(self, "dependency_groups", dependency_groups)
|
||||||
|
object.__setattr__(self, "default_groups", default_groups)
|
||||||
|
object.__setattr__(self, "created_by", created_by)
|
||||||
|
object.__setattr__(self, "packages", packages)
|
||||||
|
object.__setattr__(self, "tool", tool)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
pylock = cls(
|
||||||
|
lock_version=_get_required_as(d, str, Version, "lock-version"),
|
||||||
|
environments=_get_sequence_as(d, str, Marker, "environments"),
|
||||||
|
extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
|
||||||
|
dependency_groups=_get_sequence(d, str, "dependency-groups"),
|
||||||
|
default_groups=_get_sequence(d, str, "default-groups"),
|
||||||
|
created_by=_get_required(d, str, "created-by"),
|
||||||
|
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
|
||||||
|
packages=_get_required_sequence_of_objects(d, Package, "packages"),
|
||||||
|
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
if not Version("1") <= pylock.lock_version < Version("2"):
|
||||||
|
raise PylockUnsupportedVersionError(
|
||||||
|
f"pylock version {pylock.lock_version} is not supported"
|
||||||
|
)
|
||||||
|
if pylock.lock_version > Version("1.0"):
|
||||||
|
_logger.warning(
|
||||||
|
"pylock minor version %s is not supported", pylock.lock_version
|
||||||
|
)
|
||||||
|
return pylock
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
|
||||||
|
"""Create and validate a Pylock instance from a TOML dictionary.
|
||||||
|
|
||||||
|
Raises :class:`PylockValidationError` if the input data is not
|
||||||
|
spec-compliant.
|
||||||
|
"""
|
||||||
|
return cls._from_dict(d)
|
||||||
|
|
||||||
|
def to_dict(self) -> Mapping[str, Any]:
|
||||||
|
"""Convert the Pylock instance to a TOML dictionary."""
|
||||||
|
return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
"""Validate the Pylock instance against the specification.
|
||||||
|
|
||||||
|
Raises :class:`PylockValidationError` otherwise."""
|
||||||
|
self.from_dict(self.to_dict())
|
||||||
|
|
||||||
|
def select(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
environment: Environment | None = None,
|
||||||
|
tags: Sequence[Tag] | None = None,
|
||||||
|
extras: Collection[str] | None = None,
|
||||||
|
dependency_groups: Collection[str] | None = None,
|
||||||
|
prefer_sdist_predicate: Callable[[NormalizedName], bool] | None = None,
|
||||||
|
) -> Iterator[
|
||||||
|
tuple[
|
||||||
|
Package,
|
||||||
|
PackageVcs
|
||||||
|
| PackageDirectory
|
||||||
|
| PackageArchive
|
||||||
|
| PackageWheel
|
||||||
|
| PackageSdist,
|
||||||
|
]
|
||||||
|
]:
|
||||||
|
"""Select what to install from the lock file.
|
||||||
|
|
||||||
|
The *environment* and *tags* parameters represent the environment being
|
||||||
|
selected for. If unspecified, ``packaging.markers.default_environment()`` and
|
||||||
|
``packaging.tags.sys_tags()`` are used.
|
||||||
|
|
||||||
|
The *extras* parameter represents the extras to install.
|
||||||
|
|
||||||
|
The *dependency_groups* parameter represents the groups to install. If
|
||||||
|
unspecified, the default groups are used.
|
||||||
|
|
||||||
|
The *prefer_sdist_predicate* parameter is called for packages with a source
|
||||||
|
distribution. If it returns ``True``, the source distribution is selected
|
||||||
|
before attempting wheel compatibility. If no source distribution is
|
||||||
|
available, wheel selection proceeds as usual without calling the predicate.
|
||||||
|
|
||||||
|
This method must be used on valid Pylock instances (i.e. one obtained
|
||||||
|
from :meth:`Pylock.from_dict` or if constructed manually, after calling
|
||||||
|
:meth:`Pylock.validate`).
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
|
||||||
|
.. versionchanged:: 26.3
|
||||||
|
Added the *prefer_sdist_predicate* parameter.
|
||||||
|
"""
|
||||||
|
compatible_tags_selector = create_compatible_tags_selector(
|
||||||
|
tags if tags is not None else sys_tags()
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Gather the extras and dependency groups to install and set ``extras`` and
|
||||||
|
# ``dependency_groups`` for marker evaluation, respectively.
|
||||||
|
#
|
||||||
|
# #. ``extras`` SHOULD be set to the empty set by default.
|
||||||
|
# #. ``dependency_groups`` SHOULD be the set created from
|
||||||
|
# :ref:`pylock-default-groups` by default.
|
||||||
|
env = cast(
|
||||||
|
"dict[str, str | frozenset[str]]",
|
||||||
|
dict(
|
||||||
|
environment or {}, # Marker.evaluate will fill-up
|
||||||
|
extras=frozenset(extras or []),
|
||||||
|
dependency_groups=frozenset(
|
||||||
|
(self.default_groups or [])
|
||||||
|
if dependency_groups is None # to allow selecting no group
|
||||||
|
else dependency_groups
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
env_python_full_version = _pep440_python_full_version(
|
||||||
|
environment["python_full_version"]
|
||||||
|
if environment
|
||||||
|
else default_environment()["python_full_version"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
|
||||||
|
# supported; an error or warning MUST be raised as appropriate.
|
||||||
|
# Covered by lock.validate() which is a precondition for this method.
|
||||||
|
|
||||||
|
# #. If :ref:`pylock-requires-python` is specified, check that the environment
|
||||||
|
# being installed for meets the requirement; an error MUST be raised if it is
|
||||||
|
# not met.
|
||||||
|
if self.requires_python and not self.requires_python.contains(
|
||||||
|
env_python_full_version,
|
||||||
|
):
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"python_full_version {env_python_full_version!r} "
|
||||||
|
f"in provided environment does not satisfy the Python version "
|
||||||
|
f"requirement {str(self.requires_python)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. If :ref:`pylock-environments` is specified, check that at least one of the
|
||||||
|
# environment marker expressions is satisfied; an error MUST be raised if no
|
||||||
|
# expression is satisfied.
|
||||||
|
if self.environments:
|
||||||
|
for env_marker in self.environments:
|
||||||
|
if env_marker.evaluate(
|
||||||
|
cast("dict[str, str]", environment or {}), context="requirement"
|
||||||
|
):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise PylockSelectError(
|
||||||
|
"Provided environment does not satisfy any of the "
|
||||||
|
"environments specified in the lock file"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. For each package listed in :ref:`pylock-packages`:
|
||||||
|
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
|
||||||
|
for package_index, package in enumerate(self.packages):
|
||||||
|
# #. If :ref:`pylock-packages-marker` is specified, check if it is
|
||||||
|
# satisfied;if it isn't, skip to the next package.
|
||||||
|
if package.marker and not package.marker.evaluate(env, context="lock_file"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
|
||||||
|
# satisfied; an error MUST be raised if it isn't.
|
||||||
|
if package.requires_python and not package.requires_python.contains(
|
||||||
|
env_python_full_version,
|
||||||
|
):
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"python_full_version {env_python_full_version!r} "
|
||||||
|
f"in provided environment does not satisfy the Python version "
|
||||||
|
f"requirement {str(package.requires_python)!r} for package "
|
||||||
|
f"{package.name!r} at packages[{package_index}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Check that no other conflicting instance of the package has been slated
|
||||||
|
# to be installed; an error about the ambiguity MUST be raised otherwise.
|
||||||
|
if package.name in selected_packages_by_name:
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"Multiple packages with the name {package.name!r} are "
|
||||||
|
f"selected at packages[{package_index}] and "
|
||||||
|
f"packages[{selected_packages_by_name[package.name][0]}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Check that the source of the package is specified appropriately (i.e.
|
||||||
|
# there are no conflicting sources in the package entry);
|
||||||
|
# an error MUST be raised if any issues are found.
|
||||||
|
# Covered by lock.validate() which is a precondition for this method.
|
||||||
|
|
||||||
|
# #. Add the package to the set of packages to install.
|
||||||
|
selected_packages_by_name[package.name] = (package_index, package)
|
||||||
|
|
||||||
|
# #. For each package to be installed:
|
||||||
|
for package_index, package in selected_packages_by_name.values():
|
||||||
|
# - If :ref:`pylock-packages-vcs` is set:
|
||||||
|
if package.vcs is not None:
|
||||||
|
yield package, package.vcs
|
||||||
|
|
||||||
|
# - Else if :ref:`pylock-packages-directory` is set:
|
||||||
|
elif package.directory is not None:
|
||||||
|
yield package, package.directory
|
||||||
|
|
||||||
|
# - Else if :ref:`pylock-packages-archive` is set:
|
||||||
|
elif package.archive is not None:
|
||||||
|
yield package, package.archive
|
||||||
|
|
||||||
|
# - Else if source preference selects an available
|
||||||
|
# :ref:`pylock-packages-sdist`:
|
||||||
|
elif (
|
||||||
|
package.sdist is not None
|
||||||
|
and prefer_sdist_predicate is not None
|
||||||
|
and prefer_sdist_predicate(package.name)
|
||||||
|
):
|
||||||
|
yield package, package.sdist
|
||||||
|
|
||||||
|
# - Else if there are entries for :ref:`pylock-packages-wheels`:
|
||||||
|
elif package.wheels:
|
||||||
|
# #. Look for the appropriate wheel file based on
|
||||||
|
# :ref:`pylock-packages-wheels-name`; if one is not found then move
|
||||||
|
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
|
||||||
|
# a lack of source for the project.
|
||||||
|
best_wheel = next(
|
||||||
|
compatible_tags_selector(
|
||||||
|
(wheel, parse_wheel_filename(wheel.filename)[-1])
|
||||||
|
for wheel in package.wheels
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if best_wheel:
|
||||||
|
yield package, best_wheel
|
||||||
|
elif package.sdist is not None:
|
||||||
|
yield package, package.sdist
|
||||||
|
else:
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"No wheel found matching the provided tags "
|
||||||
|
f"for package {package.name!r} "
|
||||||
|
f"at packages[{package_index}], "
|
||||||
|
f"and no sdist available as a fallback"
|
||||||
|
)
|
||||||
|
|
||||||
|
# - Else if no :ref:`pylock-packages-wheels` file is found or
|
||||||
|
# :ref:`pylock-packages-sdist` is solely set:
|
||||||
|
elif package.sdist is not None:
|
||||||
|
yield package, package.sdist
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Covered by lock.validate() which is a precondition for this method.
|
||||||
|
raise NotImplementedError # pragma: no cover
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,192 @@
|
|||||||
|
# This file is dual licensed under the terms of the Apache License, Version
|
||||||
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
|
# for complete details.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._parser import parse_requirement as _parse_requirement
|
||||||
|
from ._tokenizer import ParserSyntaxError
|
||||||
|
from .markers import Marker, _normalize_extra_values
|
||||||
|
from .specifiers import InvalidSpecifier, SpecifierSet
|
||||||
|
from .utils import canonicalize_name
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"InvalidRequirement",
|
||||||
|
"Requirement",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRequirement(ValueError):
|
||||||
|
"""
|
||||||
|
An invalid requirement was found, users should refer to PEP 508.
|
||||||
|
|
||||||
|
.. versionadded:: 16.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Requirement:
|
||||||
|
"""Parse a requirement.
|
||||||
|
|
||||||
|
Parse a given requirement string into its parts, such as name, specifier,
|
||||||
|
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
|
||||||
|
string.
|
||||||
|
|
||||||
|
.. versionadded:: 16.1
|
||||||
|
|
||||||
|
.. versionchanged:: 22.0
|
||||||
|
Added equality (``__eq__``) and hashing (``__hash__``) so requirements
|
||||||
|
can be compared and stored in sets / dicts.
|
||||||
|
|
||||||
|
.. versionchanged:: 23.2
|
||||||
|
Equality and hashing began canonicalizing requirement names, so
|
||||||
|
requirements whose names differ only by normalization (e.g.
|
||||||
|
``Requirement("Foo")`` vs ``Requirement("foo")``) now compare and hash
|
||||||
|
equal.
|
||||||
|
|
||||||
|
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||||
|
format so the same pickle can be loaded in future packaging releases.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.2
|
||||||
|
|
||||||
|
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||||
|
be unpickled with future releases. Backward compatibility with pickles
|
||||||
|
from packaging < 26.2 is supported but may be removed in a future
|
||||||
|
release.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.3
|
||||||
|
|
||||||
|
The dedicated pickle support introduced in 26.2 did not preserve the
|
||||||
|
specifier's explicit :attr:`~packaging.specifiers.SpecifierSet.prereleases`
|
||||||
|
override; it is now included again.
|
||||||
|
|
||||||
|
Equality and hashing normalize requirement names, extras, and
|
||||||
|
equivalent specifiers. The string representation still preserves the
|
||||||
|
parsed name and extras spelling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Can we test whether something is contained within a requirement?
|
||||||
|
# If so how do we do that? Do we need to test against the _name_ of
|
||||||
|
# the thing as well as the version? What about the markers?
|
||||||
|
# TODO: Can we normalize the name and extra name?
|
||||||
|
|
||||||
|
__slots__ = ("extras", "marker", "name", "specifier", "url")
|
||||||
|
|
||||||
|
def __init__(self, requirement_string: str) -> None:
|
||||||
|
try:
|
||||||
|
parsed = _parse_requirement(requirement_string)
|
||||||
|
except ParserSyntaxError as e:
|
||||||
|
raise InvalidRequirement(str(e)) from e
|
||||||
|
|
||||||
|
self.name: str = parsed.name
|
||||||
|
self.url: str | None = parsed.url or None
|
||||||
|
self.extras: set[str] = set(parsed.extras)
|
||||||
|
try:
|
||||||
|
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
|
||||||
|
except InvalidSpecifier as e:
|
||||||
|
raise InvalidRequirement(str(e)) from e
|
||||||
|
self.marker: Marker | None = None
|
||||||
|
if parsed.marker is not None:
|
||||||
|
self.marker = Marker.__new__(Marker)
|
||||||
|
self.marker._markers = _normalize_extra_values(parsed.marker)
|
||||||
|
|
||||||
|
def _iter_parts(self, name: str) -> Iterator[str]:
|
||||||
|
yield name
|
||||||
|
|
||||||
|
if self.extras:
|
||||||
|
formatted_extras = ",".join(sorted(self.extras))
|
||||||
|
yield f"[{formatted_extras}]"
|
||||||
|
|
||||||
|
if self.specifier:
|
||||||
|
yield str(self.specifier)
|
||||||
|
|
||||||
|
if self.url:
|
||||||
|
yield f" @ {self.url}"
|
||||||
|
if self.marker:
|
||||||
|
yield " "
|
||||||
|
|
||||||
|
if self.marker:
|
||||||
|
yield f"; {self.marker}"
|
||||||
|
|
||||||
|
def __getstate__(self) -> tuple[str, bool | None]:
|
||||||
|
# Return the requirement string for compactness and stability, paired
|
||||||
|
# with the specifier's explicit prereleases override, which is not
|
||||||
|
# captured by the string form. Re-parsed on load to reconstruct all
|
||||||
|
# other fields.
|
||||||
|
return (str(self), self.specifier._prereleases)
|
||||||
|
|
||||||
|
def __setstate__(self, state: object) -> None:
|
||||||
|
if isinstance(state, str):
|
||||||
|
# Format (26.2): just the requirement string.
|
||||||
|
requirement_string: str = state
|
||||||
|
prereleases: bool | None = None
|
||||||
|
elif (
|
||||||
|
isinstance(state, tuple)
|
||||||
|
and len(state) == 2
|
||||||
|
and isinstance(state[0], str)
|
||||||
|
and (state[1] is None or isinstance(state[1], bool))
|
||||||
|
):
|
||||||
|
# New format (26.3+): (requirement string, specifier prereleases).
|
||||||
|
requirement_string, prereleases = state
|
||||||
|
elif isinstance(state, dict) and state.keys() >= set(self.__slots__):
|
||||||
|
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||||
|
for key in self.__slots__:
|
||||||
|
setattr(self, key, state[key])
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Cannot restore Requirement from {state!r}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tmp = Requirement(requirement_string)
|
||||||
|
except InvalidRequirement as exc:
|
||||||
|
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
|
||||||
|
self.name = tmp.name
|
||||||
|
self.url = tmp.url
|
||||||
|
self.extras = tmp.extras
|
||||||
|
self.specifier = tmp.specifier
|
||||||
|
self.specifier._prereleases = prereleases
|
||||||
|
self.marker = tmp.marker
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "".join(self._iter_parts(self.name))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
# Mirror __eq__ by hashing the canonical specifier object rather than
|
||||||
|
# its raw string. ``_iter_parts`` yields ``str(self.specifier)``, which
|
||||||
|
# is non-canonical, so trailing-zero-equivalent requirements such as
|
||||||
|
# ``foo==1.0.0`` and ``foo==1.0.0.0`` (which compare equal) would
|
||||||
|
# otherwise hash differently, breaking the hash/__eq__ invariant.
|
||||||
|
return hash(
|
||||||
|
(
|
||||||
|
canonicalize_name(self.name),
|
||||||
|
frozenset(canonicalize_name(e) for e in self.extras),
|
||||||
|
self.specifier,
|
||||||
|
self.url,
|
||||||
|
self.marker,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Requirement):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
# Extras must be normalized before comparison as per PEP 685.
|
||||||
|
self_extras = frozenset(canonicalize_name(e) for e in self.extras)
|
||||||
|
other_extras = frozenset(canonicalize_name(e) for e in other.extras)
|
||||||
|
return (
|
||||||
|
canonicalize_name(self.name) == canonicalize_name(other.name)
|
||||||
|
and self_extras == other_extras
|
||||||
|
and self.specifier == other.specifier
|
||||||
|
and self.url == other.url
|
||||||
|
and self.marker == other.marker
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,370 @@
|
|||||||
|
# This file is dual licensed under the terms of the Apache License, Version
|
||||||
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
|
# for complete details.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import NewType, Union, cast
|
||||||
|
|
||||||
|
from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag
|
||||||
|
from .version import InvalidVersion, Version, _TrimmedRelease
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BuildTag",
|
||||||
|
"InvalidName",
|
||||||
|
"InvalidSdistFilename",
|
||||||
|
"InvalidWheelFilename",
|
||||||
|
"NormalizedName",
|
||||||
|
"canonicalize_name",
|
||||||
|
"canonicalize_version",
|
||||||
|
"is_normalized_name",
|
||||||
|
"parse_sdist_filename",
|
||||||
|
"parse_wheel_filename",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
BuildTag = Union[tuple[()], tuple[int, str]]
|
||||||
|
"""
|
||||||
|
A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair.
|
||||||
|
|
||||||
|
.. versionadded:: 20.9
|
||||||
|
"""
|
||||||
|
|
||||||
|
NormalizedName = NewType("NormalizedName", str)
|
||||||
|
"""
|
||||||
|
A :class:`typing.NewType` of :class:`str`, representing a normalized name.
|
||||||
|
|
||||||
|
.. versionadded:: 20.4
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidName(ValueError):
|
||||||
|
"""
|
||||||
|
An invalid distribution name; users should refer to the packaging user guide.
|
||||||
|
|
||||||
|
.. versionadded:: 23.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidWheelFilename(ValueError):
|
||||||
|
"""
|
||||||
|
An invalid wheel filename was found, users should refer to PEP 427.
|
||||||
|
|
||||||
|
.. versionadded:: 20.9
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSdistFilename(ValueError):
|
||||||
|
"""
|
||||||
|
An invalid sdist filename was found, users should refer to the packaging user guide.
|
||||||
|
|
||||||
|
.. versionadded:: 20.9
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Core metadata spec for `Name`
|
||||||
|
_validate_regex = re.compile(
|
||||||
|
r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII
|
||||||
|
)
|
||||||
|
_normalized_regex = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*", re.ASCII)
|
||||||
|
# PEP 427: The build number must start with a digit.
|
||||||
|
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
|
||||||
|
# PEP 427: Valid characters for an escaped project name in a wheel filename.
|
||||||
|
# Requires at least one character so an empty project name is rejected.
|
||||||
|
_wheel_name_regex = re.compile(r"^[\w._]+\Z", re.UNICODE)
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
||||||
|
"""
|
||||||
|
This function takes a valid Python package or extra name, and returns the
|
||||||
|
normalized form of it.
|
||||||
|
|
||||||
|
The return type is typed as :class:`NormalizedName`. This allows type
|
||||||
|
checkers to help require that a string has passed through this function
|
||||||
|
before use.
|
||||||
|
|
||||||
|
If **validate** is true, then the function will check if **name** is a valid
|
||||||
|
distribution name before normalizing.
|
||||||
|
|
||||||
|
:param str name: The name to normalize.
|
||||||
|
:param bool validate: Check whether the name is a valid distribution name.
|
||||||
|
:raises InvalidName: If **validate** is true and the name is not an
|
||||||
|
acceptable distribution name.
|
||||||
|
|
||||||
|
>>> from packaging.utils import canonicalize_name
|
||||||
|
>>> canonicalize_name("Django")
|
||||||
|
'django'
|
||||||
|
>>> canonicalize_name("oslo.concurrency")
|
||||||
|
'oslo-concurrency'
|
||||||
|
>>> canonicalize_name("requests")
|
||||||
|
'requests'
|
||||||
|
|
||||||
|
.. versionadded:: 16.2
|
||||||
|
|
||||||
|
.. versionchanged:: 20.4
|
||||||
|
The return type was changed to :class:`NormalizedName`.
|
||||||
|
|
||||||
|
.. versionchanged:: 23.2
|
||||||
|
Added the *validate* keyword parameter.
|
||||||
|
"""
|
||||||
|
if validate and not _validate_regex.fullmatch(name):
|
||||||
|
raise InvalidName(f"name is invalid: {name!r}")
|
||||||
|
# Ensure all ``.`` and ``_`` are ``-``
|
||||||
|
# Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503
|
||||||
|
# Much faster than re, and even faster than str.translate
|
||||||
|
value = name.lower().replace("_", "-").replace(".", "-")
|
||||||
|
# Condense repeats (faster than regex)
|
||||||
|
while "--" in value:
|
||||||
|
value = value.replace("--", "-")
|
||||||
|
return cast("NormalizedName", value)
|
||||||
|
|
||||||
|
|
||||||
|
def is_normalized_name(name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a name is a normalized project name (i.e. a valid name that
|
||||||
|
:func:`canonicalize_name` would roundtrip to the same value).
|
||||||
|
|
||||||
|
The roundtrip only characterizes normalized names for *valid* names. A name
|
||||||
|
must start and end with an ASCII letter or digit, which
|
||||||
|
:func:`canonicalize_name` does not enforce: it leaves a leading or trailing
|
||||||
|
hyphen in place, so such a name roundtrips without being normalized.
|
||||||
|
|
||||||
|
:param str name: The name to check.
|
||||||
|
|
||||||
|
>>> from packaging.utils import canonicalize_name, is_normalized_name
|
||||||
|
>>> is_normalized_name("requests")
|
||||||
|
True
|
||||||
|
>>> is_normalized_name("Django")
|
||||||
|
False
|
||||||
|
>>> canonicalize_name("_not_legal")
|
||||||
|
'-not-legal'
|
||||||
|
>>> is_normalized_name("-not-legal") # roundtrips, but not a valid name
|
||||||
|
False
|
||||||
|
|
||||||
|
.. versionadded:: 23.2
|
||||||
|
"""
|
||||||
|
return _normalized_regex.fullmatch(name) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_version(
|
||||||
|
version: Version | str, *, strip_trailing_zero: bool = True
|
||||||
|
) -> str:
|
||||||
|
"""Return a canonical form of a version as a string.
|
||||||
|
|
||||||
|
This function takes a string representing a package version (or a
|
||||||
|
:class:`~packaging.version.Version` instance), and returns the
|
||||||
|
normalized form of it. By default, it strips trailing zeros from
|
||||||
|
the release segment.
|
||||||
|
|
||||||
|
>>> from packaging.utils import canonicalize_version
|
||||||
|
>>> canonicalize_version('1.0.1')
|
||||||
|
'1.0.1'
|
||||||
|
|
||||||
|
Per PEP 625, versions may have multiple canonical forms, differing
|
||||||
|
only by trailing zeros.
|
||||||
|
|
||||||
|
>>> canonicalize_version('1.0.0')
|
||||||
|
'1'
|
||||||
|
>>> canonicalize_version('1.0.0', strip_trailing_zero=False)
|
||||||
|
'1.0.0'
|
||||||
|
|
||||||
|
Invalid versions are returned unaltered.
|
||||||
|
|
||||||
|
>>> canonicalize_version('foo bar baz')
|
||||||
|
'foo bar baz'
|
||||||
|
|
||||||
|
>>> canonicalize_version('1.4.0.0.0')
|
||||||
|
'1.4'
|
||||||
|
|
||||||
|
.. versionadded:: 17.1
|
||||||
|
|
||||||
|
.. versionchanged:: 21.0
|
||||||
|
The return type was narrowed to :class:`str`.
|
||||||
|
|
||||||
|
.. versionchanged:: 22.0
|
||||||
|
Added the *strip_trailing_zero* keyword parameter.
|
||||||
|
"""
|
||||||
|
if isinstance(version, str):
|
||||||
|
try:
|
||||||
|
version = Version(version)
|
||||||
|
except InvalidVersion:
|
||||||
|
return str(version)
|
||||||
|
return str(_TrimmedRelease(version) if strip_trailing_zero else version)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_wheel_filename(
|
||||||
|
filename: str,
|
||||||
|
*,
|
||||||
|
validate_order: bool = False,
|
||||||
|
) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
|
||||||
|
"""
|
||||||
|
This function takes the filename of a wheel file, and parses it,
|
||||||
|
returning a tuple of name, version, build number, and tags.
|
||||||
|
|
||||||
|
The name part of the tuple is normalized and typed as
|
||||||
|
:class:`NormalizedName`. The version portion is an instance of
|
||||||
|
:class:`~packaging.version.Version`. The build number is ``()`` if
|
||||||
|
there is no build number in the wheel filename, otherwise a
|
||||||
|
two-item tuple of an integer for the leading digits and
|
||||||
|
a string for the rest of the build number. The tags portion is a
|
||||||
|
frozen set of :class:`~packaging.tags.Tag` instances (as the tag
|
||||||
|
string format allows multiple tags to be combined into a single
|
||||||
|
string).
|
||||||
|
|
||||||
|
If **validate_order** is true, compressed tag set components are
|
||||||
|
checked to be in sorted order as required by PEP 425.
|
||||||
|
|
||||||
|
:param str filename: The name of the wheel file.
|
||||||
|
:param bool validate_order: Check whether compressed tag set components
|
||||||
|
are in sorted order.
|
||||||
|
:raises InvalidWheelFilename: If the filename in question
|
||||||
|
does not follow the :ref:`wheel specification
|
||||||
|
<pypug:binary-distribution-format>`.
|
||||||
|
|
||||||
|
>>> from packaging.utils import parse_wheel_filename
|
||||||
|
>>> from packaging.tags import Tag
|
||||||
|
>>> from packaging.version import Version
|
||||||
|
>>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
|
||||||
|
>>> name
|
||||||
|
'foo'
|
||||||
|
>>> ver == Version('1.0')
|
||||||
|
True
|
||||||
|
>>> tags == {Tag("py3", "none", "any")}
|
||||||
|
True
|
||||||
|
>>> not build
|
||||||
|
True
|
||||||
|
|
||||||
|
.. versionadded:: 20.9
|
||||||
|
|
||||||
|
.. versionchanged:: 23.2
|
||||||
|
Raises :class:`InvalidWheelFilename` when the version component is invalid.
|
||||||
|
|
||||||
|
.. versionadded:: 26.1
|
||||||
|
The *validate_order* parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.3
|
||||||
|
Raises :class:`InvalidWheelFilename` when an interpreter component is
|
||||||
|
not an identifier, a tag set component is empty, or the project name is
|
||||||
|
empty.
|
||||||
|
"""
|
||||||
|
if not filename.endswith(".whl"):
|
||||||
|
raise InvalidWheelFilename(
|
||||||
|
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = filename[:-4]
|
||||||
|
dashes = filename.count("-")
|
||||||
|
if dashes not in (4, 5):
|
||||||
|
raise InvalidWheelFilename(
|
||||||
|
f"Invalid wheel filename (wrong number of parts): {filename!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = filename.split("-", dashes - 2)
|
||||||
|
name_part = parts[0]
|
||||||
|
# See PEP 427 for the rules on escaping the project name.
|
||||||
|
if "__" in name_part or _wheel_name_regex.match(name_part) is None:
|
||||||
|
raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
|
||||||
|
name = canonicalize_name(name_part)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = Version(parts[1])
|
||||||
|
except InvalidVersion as e:
|
||||||
|
raise InvalidWheelFilename(
|
||||||
|
f"Invalid wheel filename (invalid version): {filename!r}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
if dashes == 5:
|
||||||
|
build_part = parts[2]
|
||||||
|
build_match = _build_tag_regex.match(build_part)
|
||||||
|
if build_match is None:
|
||||||
|
raise InvalidWheelFilename(
|
||||||
|
f"Invalid build number: {build_part} in {filename!r}"
|
||||||
|
)
|
||||||
|
build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2)))
|
||||||
|
else:
|
||||||
|
build = ()
|
||||||
|
tag_str = parts[-1]
|
||||||
|
try:
|
||||||
|
tags = parse_tag(tag_str, validate_order=validate_order)
|
||||||
|
except UnsortedTagsError:
|
||||||
|
raise InvalidWheelFilename(
|
||||||
|
f"Invalid wheel filename (compressed tag set components must be in "
|
||||||
|
f"sorted order per PEP 425): {filename!r}"
|
||||||
|
) from None
|
||||||
|
except InvalidTag:
|
||||||
|
raise InvalidWheelFilename(
|
||||||
|
f"Invalid wheel filename (invalid tag component): {filename!r}"
|
||||||
|
) from None
|
||||||
|
return (name, version, build, tags)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
|
||||||
|
"""
|
||||||
|
This function takes the filename of a sdist file (as specified
|
||||||
|
in the `Source distribution format`_ documentation), and parses
|
||||||
|
it, returning a tuple of the normalized name and version as
|
||||||
|
represented by an instance of :class:`~packaging.version.Version`.
|
||||||
|
|
||||||
|
:param str filename: The name of the sdist file.
|
||||||
|
:raises InvalidSdistFilename: If the filename does not end
|
||||||
|
with an sdist extension (``.zip`` or ``.tar.gz``), if it does not
|
||||||
|
contain a dash separating the name and the version of the distribution,
|
||||||
|
if the project name is empty, or if the version portion is not a valid
|
||||||
|
version.
|
||||||
|
|
||||||
|
>>> from packaging.utils import parse_sdist_filename
|
||||||
|
>>> from packaging.version import Version
|
||||||
|
>>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
|
||||||
|
>>> name
|
||||||
|
'foo'
|
||||||
|
>>> ver == Version('1.0')
|
||||||
|
True
|
||||||
|
|
||||||
|
.. versionadded:: 20.9
|
||||||
|
|
||||||
|
.. versionchanged:: 21.0
|
||||||
|
Added support for ``.zip`` source distributions.
|
||||||
|
|
||||||
|
.. versionchanged:: 23.2
|
||||||
|
Raises :class:`InvalidSdistFilename` when the version component is invalid.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.3
|
||||||
|
Raises :class:`InvalidSdistFilename` on an empty project name.
|
||||||
|
|
||||||
|
.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
|
||||||
|
"""
|
||||||
|
if filename.endswith(".tar.gz"):
|
||||||
|
file_stem = filename[: -len(".tar.gz")]
|
||||||
|
elif filename.endswith(".zip"):
|
||||||
|
file_stem = filename[: -len(".zip")]
|
||||||
|
else:
|
||||||
|
raise InvalidSdistFilename(
|
||||||
|
f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
|
||||||
|
f" {filename!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# We are requiring a PEP 440 version, which cannot contain dashes,
|
||||||
|
# so we split on the last dash.
|
||||||
|
name_part, sep, version_part = file_stem.rpartition("-")
|
||||||
|
if not sep:
|
||||||
|
raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}")
|
||||||
|
if not name_part:
|
||||||
|
raise InvalidSdistFilename(
|
||||||
|
f"Invalid sdist filename (empty project name): {filename!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
name = canonicalize_name(name_part)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = Version(version_part)
|
||||||
|
except InvalidVersion as e:
|
||||||
|
raise InvalidSdistFilename(
|
||||||
|
f"Invalid sdist filename (invalid version): {filename!r}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
return (name, version)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,760 @@
|
|||||||
|
@Switch01
|
||||||
|
A_Rog
|
||||||
|
Aakanksha Agrawal
|
||||||
|
Abhinav Sagar
|
||||||
|
ABHYUDAY PRATAP SINGH
|
||||||
|
abs51295
|
||||||
|
AceGentile
|
||||||
|
Adam Chainz
|
||||||
|
Adam Tse
|
||||||
|
Adam Wentz
|
||||||
|
admin
|
||||||
|
Adrien Morison
|
||||||
|
ahayrapetyan
|
||||||
|
Ahilya
|
||||||
|
AinsworthK
|
||||||
|
Akash Srivastava
|
||||||
|
Alan Yee
|
||||||
|
Albert Tugushev
|
||||||
|
Albert-Guan
|
||||||
|
albertg
|
||||||
|
Alberto Sottile
|
||||||
|
Aleks Bunin
|
||||||
|
Ales Erjavec
|
||||||
|
Alethea Flowers
|
||||||
|
Alex Gaynor
|
||||||
|
Alex Grönholm
|
||||||
|
Alex Hedges
|
||||||
|
Alex Loosley
|
||||||
|
Alex Morega
|
||||||
|
Alex Stachowiak
|
||||||
|
Alexander Shtyrov
|
||||||
|
Alexandre Conrad
|
||||||
|
Alexey Popravka
|
||||||
|
Aleš Erjavec
|
||||||
|
Alli
|
||||||
|
Ami Fischman
|
||||||
|
Ananya Maiti
|
||||||
|
Anatoly Techtonik
|
||||||
|
Anders Kaseorg
|
||||||
|
Andre Aguiar
|
||||||
|
Andreas Lutro
|
||||||
|
Andrei Geacar
|
||||||
|
Andrew Gaul
|
||||||
|
Andrew Shymanel
|
||||||
|
Andrey Bienkowski
|
||||||
|
Andrey Bulgakov
|
||||||
|
Andrés Delfino
|
||||||
|
Andy Freeland
|
||||||
|
Andy Kluger
|
||||||
|
Ani Hayrapetyan
|
||||||
|
Aniruddha Basak
|
||||||
|
Anish Tambe
|
||||||
|
Anrs Hu
|
||||||
|
Anthony Sottile
|
||||||
|
Antoine Musso
|
||||||
|
Anton Ovchinnikov
|
||||||
|
Anton Patrushev
|
||||||
|
Antonio Alvarado Hernandez
|
||||||
|
Antony Lee
|
||||||
|
Antti Kaihola
|
||||||
|
Anubhav Patel
|
||||||
|
Anudit Nagar
|
||||||
|
Anuj Godase
|
||||||
|
AQNOUCH Mohammed
|
||||||
|
AraHaan
|
||||||
|
Arindam Choudhury
|
||||||
|
Armin Ronacher
|
||||||
|
Artem
|
||||||
|
Arun Babu Neelicattu
|
||||||
|
Ashley Manton
|
||||||
|
Ashwin Ramaswami
|
||||||
|
atse
|
||||||
|
Atsushi Odagiri
|
||||||
|
Avinash Karhana
|
||||||
|
Avner Cohen
|
||||||
|
Awit (Ah-Wit) Ghirmai
|
||||||
|
Baptiste Mispelon
|
||||||
|
Barney Gale
|
||||||
|
barneygale
|
||||||
|
Bartek Ogryczak
|
||||||
|
Bastian Venthur
|
||||||
|
Ben Bodenmiller
|
||||||
|
Ben Darnell
|
||||||
|
Ben Hoyt
|
||||||
|
Ben Mares
|
||||||
|
Ben Rosser
|
||||||
|
Bence Nagy
|
||||||
|
Benjamin Peterson
|
||||||
|
Benjamin VanEvery
|
||||||
|
Benoit Pierre
|
||||||
|
Berker Peksag
|
||||||
|
Bernard
|
||||||
|
Bernard Tyers
|
||||||
|
Bernardo B. Marques
|
||||||
|
Bernhard M. Wiedemann
|
||||||
|
Bertil Hatt
|
||||||
|
Bhavam Vidyarthi
|
||||||
|
Blazej Michalik
|
||||||
|
Bogdan Opanchuk
|
||||||
|
BorisZZZ
|
||||||
|
Brad Erickson
|
||||||
|
Bradley Ayers
|
||||||
|
Brandon L. Reiss
|
||||||
|
Brandt Bucher
|
||||||
|
Brett Randall
|
||||||
|
Brett Rosen
|
||||||
|
Brian Cristante
|
||||||
|
Brian Rosner
|
||||||
|
briantracy
|
||||||
|
BrownTruck
|
||||||
|
Bruno Oliveira
|
||||||
|
Bruno Renié
|
||||||
|
Bruno S
|
||||||
|
Bstrdsmkr
|
||||||
|
Buck Golemon
|
||||||
|
burrows
|
||||||
|
Bussonnier Matthias
|
||||||
|
bwoodsend
|
||||||
|
c22
|
||||||
|
Caleb Martinez
|
||||||
|
Calvin Smith
|
||||||
|
Carl Meyer
|
||||||
|
Carlos Liam
|
||||||
|
Carol Willing
|
||||||
|
Carter Thayer
|
||||||
|
Cass
|
||||||
|
Chandrasekhar Atina
|
||||||
|
Chih-Hsuan Yen
|
||||||
|
Chris Brinker
|
||||||
|
Chris Hunt
|
||||||
|
Chris Jerdonek
|
||||||
|
Chris Kuehl
|
||||||
|
Chris McDonough
|
||||||
|
Chris Pawley
|
||||||
|
Chris Pryer
|
||||||
|
Chris Wolfe
|
||||||
|
Christian Clauss
|
||||||
|
Christian Heimes
|
||||||
|
Christian Oudard
|
||||||
|
Christoph Reiter
|
||||||
|
Christopher Hunt
|
||||||
|
Christopher Snyder
|
||||||
|
cjc7373
|
||||||
|
Clark Boylan
|
||||||
|
Claudio Jolowicz
|
||||||
|
Clay McClure
|
||||||
|
Cody
|
||||||
|
Cody Soyland
|
||||||
|
Colin Watson
|
||||||
|
Collin Anderson
|
||||||
|
Connor Osborn
|
||||||
|
Cooper Lees
|
||||||
|
Cooper Ry Lees
|
||||||
|
Cory Benfield
|
||||||
|
Cory Wright
|
||||||
|
Craig Kerstiens
|
||||||
|
Cristian Sorinel
|
||||||
|
Cristina
|
||||||
|
Cristina Muñoz
|
||||||
|
Curtis Doty
|
||||||
|
cytolentino
|
||||||
|
Daan De Meyer
|
||||||
|
Dale
|
||||||
|
Damian
|
||||||
|
Damian Quiroga
|
||||||
|
Damian Shaw
|
||||||
|
Dan Black
|
||||||
|
Dan Savilonis
|
||||||
|
Dan Sully
|
||||||
|
Dane Hillard
|
||||||
|
daniel
|
||||||
|
Daniel Collins
|
||||||
|
Daniel Hahler
|
||||||
|
Daniel Holth
|
||||||
|
Daniel Jost
|
||||||
|
Daniel Katz
|
||||||
|
Daniel Shaulov
|
||||||
|
Daniele Esposti
|
||||||
|
Daniele Nicolodi
|
||||||
|
Daniele Procida
|
||||||
|
Daniil Konovalenko
|
||||||
|
Danny Hermes
|
||||||
|
Danny McClanahan
|
||||||
|
Darren Kavanagh
|
||||||
|
Dav Clark
|
||||||
|
Dave Abrahams
|
||||||
|
Dave Jones
|
||||||
|
David Aguilar
|
||||||
|
David Black
|
||||||
|
David Bordeynik
|
||||||
|
David Caro
|
||||||
|
David D Lowe
|
||||||
|
David Evans
|
||||||
|
David Hewitt
|
||||||
|
David Linke
|
||||||
|
David Poggi
|
||||||
|
David Pursehouse
|
||||||
|
David Runge
|
||||||
|
David Tucker
|
||||||
|
David Wales
|
||||||
|
Davidovich
|
||||||
|
ddelange
|
||||||
|
Deepak Sharma
|
||||||
|
Deepyaman Datta
|
||||||
|
Denise Yu
|
||||||
|
dependabot[bot]
|
||||||
|
derwolfe
|
||||||
|
Desetude
|
||||||
|
Devesh Kumar Singh
|
||||||
|
Diego Caraballo
|
||||||
|
Diego Ramirez
|
||||||
|
DiegoCaraballo
|
||||||
|
Dimitri Merejkowsky
|
||||||
|
Dimitri Papadopoulos
|
||||||
|
Dirk Stolle
|
||||||
|
Dmitry Gladkov
|
||||||
|
Dmitry Volodin
|
||||||
|
Domen Kožar
|
||||||
|
Dominic Davis-Foster
|
||||||
|
Donald Stufft
|
||||||
|
Dongweiming
|
||||||
|
doron zarhi
|
||||||
|
Dos Moonen
|
||||||
|
Douglas Thor
|
||||||
|
DrFeathers
|
||||||
|
Dustin Ingram
|
||||||
|
Dwayne Bailey
|
||||||
|
Ed Morley
|
||||||
|
Edgar Ramírez
|
||||||
|
Edgar Ramírez Mondragón
|
||||||
|
Ee Durbin
|
||||||
|
Efflam Lemaillet
|
||||||
|
efflamlemaillet
|
||||||
|
Eitan Adler
|
||||||
|
ekristina
|
||||||
|
elainechan
|
||||||
|
Eli Schwartz
|
||||||
|
Elisha Hollander
|
||||||
|
Ellen Marie Dash
|
||||||
|
Emil Burzo
|
||||||
|
Emil Styrke
|
||||||
|
Emmanuel Arias
|
||||||
|
Endoh Takanao
|
||||||
|
enoch
|
||||||
|
Erdinc Mutlu
|
||||||
|
Eric Cousineau
|
||||||
|
Eric Gillingham
|
||||||
|
Eric Hanchrow
|
||||||
|
Eric Hopper
|
||||||
|
Erik M. Bray
|
||||||
|
Erik Rose
|
||||||
|
Erwin Janssen
|
||||||
|
Eugene Vereshchagin
|
||||||
|
everdimension
|
||||||
|
Federico
|
||||||
|
Felipe Peter
|
||||||
|
Felix Yan
|
||||||
|
fiber-space
|
||||||
|
Filip Kokosiński
|
||||||
|
Filipe Laíns
|
||||||
|
Finn Womack
|
||||||
|
finnagin
|
||||||
|
Flavio Amurrio
|
||||||
|
Florian Briand
|
||||||
|
Florian Rathgeber
|
||||||
|
Francesco
|
||||||
|
Francesco Montesano
|
||||||
|
Frost Ming
|
||||||
|
Gabriel Curio
|
||||||
|
Gabriel de Perthuis
|
||||||
|
Garry Polley
|
||||||
|
gavin
|
||||||
|
gdanielson
|
||||||
|
Geoffrey Sneddon
|
||||||
|
George Song
|
||||||
|
Georgi Valkov
|
||||||
|
Georgy Pchelkin
|
||||||
|
ghost
|
||||||
|
Giftlin Rajaiah
|
||||||
|
gizmoguy1
|
||||||
|
gkdoc
|
||||||
|
Godefroid Chapelle
|
||||||
|
Gopinath M
|
||||||
|
GOTO Hayato
|
||||||
|
gousaiyang
|
||||||
|
gpiks
|
||||||
|
Greg Roodt
|
||||||
|
Greg Ward
|
||||||
|
Guilherme Espada
|
||||||
|
Guillaume Seguin
|
||||||
|
gutsytechster
|
||||||
|
Guy Rozendorn
|
||||||
|
Guy Tuval
|
||||||
|
gzpan123
|
||||||
|
Hanjun Kim
|
||||||
|
Hari Charan
|
||||||
|
Harsh Vardhan
|
||||||
|
harupy
|
||||||
|
Harutaka Kawamura
|
||||||
|
hauntsaninja
|
||||||
|
Henrich Hartzer
|
||||||
|
Henry Schreiner
|
||||||
|
Herbert Pfennig
|
||||||
|
Holly Stotelmyer
|
||||||
|
Honnix
|
||||||
|
Hsiaoming Yang
|
||||||
|
Hugo Lopes Tavares
|
||||||
|
Hugo van Kemenade
|
||||||
|
Hugues Bruant
|
||||||
|
Hynek Schlawack
|
||||||
|
Ian Bicking
|
||||||
|
Ian Cordasco
|
||||||
|
Ian Lee
|
||||||
|
Ian Stapleton Cordasco
|
||||||
|
Ian Wienand
|
||||||
|
Igor Kuzmitshov
|
||||||
|
Igor Sobreira
|
||||||
|
Ilan Schnell
|
||||||
|
Illia Volochii
|
||||||
|
Ilya Baryshev
|
||||||
|
Inada Naoki
|
||||||
|
Ionel Cristian Mărieș
|
||||||
|
Ionel Maries Cristian
|
||||||
|
Itamar Turner-Trauring
|
||||||
|
Ivan Pozdeev
|
||||||
|
J. Nick Koston
|
||||||
|
Jacob Kim
|
||||||
|
Jacob Walls
|
||||||
|
Jaime Sanz
|
||||||
|
jakirkham
|
||||||
|
Jakub Kuczys
|
||||||
|
Jakub Stasiak
|
||||||
|
Jakub Vysoky
|
||||||
|
Jakub Wilk
|
||||||
|
James Cleveland
|
||||||
|
James Curtin
|
||||||
|
James Firth
|
||||||
|
James Gerity
|
||||||
|
James Polley
|
||||||
|
Jan Pokorný
|
||||||
|
Jannis Leidel
|
||||||
|
Jarek Potiuk
|
||||||
|
jarondl
|
||||||
|
Jason Curtis
|
||||||
|
Jason R. Coombs
|
||||||
|
JasonMo
|
||||||
|
JasonMo1
|
||||||
|
Jay Graves
|
||||||
|
Jean Abou Samra
|
||||||
|
Jean-Christophe Fillion-Robin
|
||||||
|
Jeff Barber
|
||||||
|
Jeff Dairiki
|
||||||
|
Jeff Widman
|
||||||
|
Jelmer Vernooij
|
||||||
|
jenix21
|
||||||
|
Jeremy Stanley
|
||||||
|
Jeremy Zafran
|
||||||
|
Jesse Rittner
|
||||||
|
Jiashuo Li
|
||||||
|
Jim Fisher
|
||||||
|
Jim Garrison
|
||||||
|
Jiun Bae
|
||||||
|
Jivan Amara
|
||||||
|
Joe Bylund
|
||||||
|
Joe Michelini
|
||||||
|
John Paton
|
||||||
|
John T. Wodder II
|
||||||
|
John-Scott Atlakson
|
||||||
|
johnthagen
|
||||||
|
Jon Banafato
|
||||||
|
Jon Dufresne
|
||||||
|
Jon Parise
|
||||||
|
Jonas Nockert
|
||||||
|
Jonathan Herbert
|
||||||
|
Joonatan Partanen
|
||||||
|
Joost Molenaar
|
||||||
|
Jorge Niedbalski
|
||||||
|
Joseph Bylund
|
||||||
|
Joseph Long
|
||||||
|
Josh Bronson
|
||||||
|
Josh Hansen
|
||||||
|
Josh Schneier
|
||||||
|
Joshua
|
||||||
|
Juan Luis Cano Rodríguez
|
||||||
|
Juanjo Bazán
|
||||||
|
Judah Rand
|
||||||
|
Julian Berman
|
||||||
|
Julian Gethmann
|
||||||
|
Julien Demoor
|
||||||
|
Jussi Kukkonen
|
||||||
|
jwg4
|
||||||
|
Jyrki Pulliainen
|
||||||
|
Kai Chen
|
||||||
|
Kai Mueller
|
||||||
|
Kamal Bin Mustafa
|
||||||
|
kasium
|
||||||
|
kaustav haldar
|
||||||
|
keanemind
|
||||||
|
Keith Maxwell
|
||||||
|
Kelsey Hightower
|
||||||
|
Kenneth Belitzky
|
||||||
|
Kenneth Reitz
|
||||||
|
Kevin Burke
|
||||||
|
Kevin Carter
|
||||||
|
Kevin Frommelt
|
||||||
|
Kevin R Patterson
|
||||||
|
Kexuan Sun
|
||||||
|
Kit Randel
|
||||||
|
Klaas van Schelven
|
||||||
|
KOLANICH
|
||||||
|
kpinc
|
||||||
|
Krishna Oza
|
||||||
|
Kumar McMillan
|
||||||
|
Kurt McKee
|
||||||
|
Kyle Persohn
|
||||||
|
lakshmanaram
|
||||||
|
Laszlo Kiss-Kollar
|
||||||
|
Laurent Bristiel
|
||||||
|
Laurent LAPORTE
|
||||||
|
Laurie O
|
||||||
|
Laurie Opperman
|
||||||
|
layday
|
||||||
|
Leon Sasson
|
||||||
|
Lev Givon
|
||||||
|
Lincoln de Sousa
|
||||||
|
Lipis
|
||||||
|
lorddavidiii
|
||||||
|
Loren Carvalho
|
||||||
|
Lucas Cimon
|
||||||
|
Ludovic Gasc
|
||||||
|
Lukas Geiger
|
||||||
|
Lukas Juhrich
|
||||||
|
Luke Macken
|
||||||
|
Luo Jiebin
|
||||||
|
luojiebin
|
||||||
|
luz.paz
|
||||||
|
László Kiss Kollár
|
||||||
|
M00nL1ght
|
||||||
|
Marc Abramowitz
|
||||||
|
Marc Tamlyn
|
||||||
|
Marcus Smith
|
||||||
|
Mariatta
|
||||||
|
Mark Kohler
|
||||||
|
Mark Williams
|
||||||
|
Markus Hametner
|
||||||
|
Martey Dodoo
|
||||||
|
Martin Fischer
|
||||||
|
Martin Häcker
|
||||||
|
Martin Pavlasek
|
||||||
|
Masaki
|
||||||
|
Masklinn
|
||||||
|
Matej Stuchlik
|
||||||
|
Mathew Jennings
|
||||||
|
Mathieu Bridon
|
||||||
|
Mathieu Kniewallner
|
||||||
|
Matt Bacchi
|
||||||
|
Matt Good
|
||||||
|
Matt Maker
|
||||||
|
Matt Robenolt
|
||||||
|
matthew
|
||||||
|
Matthew Einhorn
|
||||||
|
Matthew Feickert
|
||||||
|
Matthew Gilliard
|
||||||
|
Matthew Iversen
|
||||||
|
Matthew Treinish
|
||||||
|
Matthew Trumbell
|
||||||
|
Matthew Willson
|
||||||
|
Matthias Bussonnier
|
||||||
|
mattip
|
||||||
|
Maurits van Rees
|
||||||
|
Max W Chase
|
||||||
|
Maxim Kurnikov
|
||||||
|
Maxime Rouyrre
|
||||||
|
mayeut
|
||||||
|
mbaluna
|
||||||
|
mdebi
|
||||||
|
memoselyk
|
||||||
|
meowmeowcat
|
||||||
|
Michael
|
||||||
|
Michael Aquilina
|
||||||
|
Michael E. Karpeles
|
||||||
|
Michael Klich
|
||||||
|
Michael Mintz
|
||||||
|
Michael Williamson
|
||||||
|
michaelpacer
|
||||||
|
Michał Górny
|
||||||
|
Mickaël Schoentgen
|
||||||
|
Miguel Araujo Perez
|
||||||
|
Mihir Singh
|
||||||
|
Mike
|
||||||
|
Mike Hendricks
|
||||||
|
Min RK
|
||||||
|
MinRK
|
||||||
|
Miro Hrončok
|
||||||
|
Monica Baluna
|
||||||
|
montefra
|
||||||
|
Monty Taylor
|
||||||
|
Muha Ajjan
|
||||||
|
Nadav Wexler
|
||||||
|
Nahuel Ambrosini
|
||||||
|
Nate Coraor
|
||||||
|
Nate Prewitt
|
||||||
|
Nathan Houghton
|
||||||
|
Nathaniel J. Smith
|
||||||
|
Nehal J Wani
|
||||||
|
Neil Botelho
|
||||||
|
Nguyễn Gia Phong
|
||||||
|
Nicholas Serra
|
||||||
|
Nick Coghlan
|
||||||
|
Nick Stenning
|
||||||
|
Nick Timkovich
|
||||||
|
Nicolas Bock
|
||||||
|
Nicole Harris
|
||||||
|
Nikhil Benesch
|
||||||
|
Nikhil Ladha
|
||||||
|
Nikita Chepanov
|
||||||
|
Nikolay Korolev
|
||||||
|
Nipunn Koorapati
|
||||||
|
Nitesh Sharma
|
||||||
|
Niyas Sait
|
||||||
|
Noah
|
||||||
|
Noah Gorny
|
||||||
|
Nowell Strite
|
||||||
|
NtaleGrey
|
||||||
|
nvdv
|
||||||
|
OBITORASU
|
||||||
|
Ofek Lev
|
||||||
|
ofrinevo
|
||||||
|
Oliver Freund
|
||||||
|
Oliver Jeeves
|
||||||
|
Oliver Mannion
|
||||||
|
Oliver Tonnhofer
|
||||||
|
Olivier Girardot
|
||||||
|
Olivier Grisel
|
||||||
|
Ollie Rutherfurd
|
||||||
|
OMOTO Kenji
|
||||||
|
Omry Yadan
|
||||||
|
onlinejudge95
|
||||||
|
Oren Held
|
||||||
|
Oscar Benjamin
|
||||||
|
Oz N Tiram
|
||||||
|
Pachwenko
|
||||||
|
Patrick Dubroy
|
||||||
|
Patrick Jenkins
|
||||||
|
Patrick Lawson
|
||||||
|
patricktokeeffe
|
||||||
|
Patrik Kopkan
|
||||||
|
Paul Ganssle
|
||||||
|
Paul Kehrer
|
||||||
|
Paul Moore
|
||||||
|
Paul Nasrat
|
||||||
|
Paul Oswald
|
||||||
|
Paul van der Linden
|
||||||
|
Paulus Schoutsen
|
||||||
|
Pavel Safronov
|
||||||
|
Pavithra Eswaramoorthy
|
||||||
|
Pawel Jasinski
|
||||||
|
Paweł Szramowski
|
||||||
|
Pekka Klärck
|
||||||
|
Peter Gessler
|
||||||
|
Peter Lisák
|
||||||
|
Peter Waller
|
||||||
|
petr-tik
|
||||||
|
Phaneendra Chiruvella
|
||||||
|
Phil Elson
|
||||||
|
Phil Freo
|
||||||
|
Phil Pennock
|
||||||
|
Phil Whelan
|
||||||
|
Philip Jägenstedt
|
||||||
|
Philip Molloy
|
||||||
|
Philippe Ombredanne
|
||||||
|
Pi Delport
|
||||||
|
Pierre-Yves Rofes
|
||||||
|
Pieter Degroote
|
||||||
|
pip
|
||||||
|
Prabakaran Kumaresshan
|
||||||
|
Prabhjyotsing Surjit Singh Sodhi
|
||||||
|
Prabhu Marappan
|
||||||
|
Pradyun Gedam
|
||||||
|
Prashant Sharma
|
||||||
|
Pratik Mallya
|
||||||
|
pre-commit-ci[bot]
|
||||||
|
Preet Thakkar
|
||||||
|
Preston Holmes
|
||||||
|
Przemek Wrzos
|
||||||
|
Pulkit Goyal
|
||||||
|
q0w
|
||||||
|
Qiangning Hong
|
||||||
|
Qiming Xu
|
||||||
|
Quentin Lee
|
||||||
|
Quentin Pradet
|
||||||
|
R. David Murray
|
||||||
|
Rafael Caricio
|
||||||
|
Ralf Schmitt
|
||||||
|
Razzi Abuissa
|
||||||
|
rdb
|
||||||
|
Reece Dunham
|
||||||
|
Remi Rampin
|
||||||
|
Rene Dudfield
|
||||||
|
Riccardo Magliocchetti
|
||||||
|
Riccardo Schirone
|
||||||
|
Richard Jones
|
||||||
|
Richard Si
|
||||||
|
Ricky Ng-Adam
|
||||||
|
Rishi
|
||||||
|
RobberPhex
|
||||||
|
Robert Collins
|
||||||
|
Robert McGibbon
|
||||||
|
Robert Pollak
|
||||||
|
Robert T. McGibbon
|
||||||
|
robin elisha robinson
|
||||||
|
Roey Berman
|
||||||
|
Rohan Jain
|
||||||
|
Roman Bogorodskiy
|
||||||
|
Roman Donchenko
|
||||||
|
Romuald Brunet
|
||||||
|
ronaudinho
|
||||||
|
Ronny Pfannschmidt
|
||||||
|
Rory McCann
|
||||||
|
Ross Brattain
|
||||||
|
Roy Wellington Ⅳ
|
||||||
|
Ruairidh MacLeod
|
||||||
|
Russell Keith-Magee
|
||||||
|
Ryan Shepherd
|
||||||
|
Ryan Wooden
|
||||||
|
ryneeverett
|
||||||
|
Sachi King
|
||||||
|
Salvatore Rinchiera
|
||||||
|
sandeepkiran-js
|
||||||
|
Sander Van Balen
|
||||||
|
Savio Jomton
|
||||||
|
schlamar
|
||||||
|
Scott Kitterman
|
||||||
|
Sean
|
||||||
|
seanj
|
||||||
|
Sebastian Jordan
|
||||||
|
Sebastian Schaetz
|
||||||
|
Segev Finer
|
||||||
|
SeongSoo Cho
|
||||||
|
Sergey Vasilyev
|
||||||
|
Seth Michael Larson
|
||||||
|
Seth Woodworth
|
||||||
|
Shahar Epstein
|
||||||
|
Shantanu
|
||||||
|
shireenrao
|
||||||
|
Shivansh-007
|
||||||
|
Shlomi Fish
|
||||||
|
Shovan Maity
|
||||||
|
Simeon Visser
|
||||||
|
Simon Cross
|
||||||
|
Simon Pichugin
|
||||||
|
sinoroc
|
||||||
|
sinscary
|
||||||
|
snook92
|
||||||
|
socketubs
|
||||||
|
Sorin Sbarnea
|
||||||
|
Srinivas Nyayapati
|
||||||
|
Stavros Korokithakis
|
||||||
|
Stefan Scherfke
|
||||||
|
Stefano Rivera
|
||||||
|
Stephan Erb
|
||||||
|
Stephen Rosen
|
||||||
|
stepshal
|
||||||
|
Steve (Gadget) Barnes
|
||||||
|
Steve Barnes
|
||||||
|
Steve Dower
|
||||||
|
Steve Kowalik
|
||||||
|
Steven Myint
|
||||||
|
Steven Silvester
|
||||||
|
stonebig
|
||||||
|
studioj
|
||||||
|
Stéphane Bidoul
|
||||||
|
Stéphane Bidoul (ACSONE)
|
||||||
|
Stéphane Klein
|
||||||
|
Sumana Harihareswara
|
||||||
|
Surbhi Sharma
|
||||||
|
Sviatoslav Sydorenko
|
||||||
|
Swat009
|
||||||
|
Sylvain
|
||||||
|
Takayuki SHIMIZUKAWA
|
||||||
|
Taneli Hukkinen
|
||||||
|
tbeswick
|
||||||
|
Thiago
|
||||||
|
Thijs Triemstra
|
||||||
|
Thomas Fenzl
|
||||||
|
Thomas Grainger
|
||||||
|
Thomas Guettler
|
||||||
|
Thomas Johansson
|
||||||
|
Thomas Kluyver
|
||||||
|
Thomas Smith
|
||||||
|
Thomas VINCENT
|
||||||
|
Tim D. Smith
|
||||||
|
Tim Gates
|
||||||
|
Tim Harder
|
||||||
|
Tim Heap
|
||||||
|
tim smith
|
||||||
|
tinruufu
|
||||||
|
Tobias Hermann
|
||||||
|
Tom Forbes
|
||||||
|
Tom Freudenheim
|
||||||
|
Tom V
|
||||||
|
Tomas Hrnciar
|
||||||
|
Tomas Orsava
|
||||||
|
Tomer Chachamu
|
||||||
|
Tommi Enenkel | AnB
|
||||||
|
Tomáš Hrnčiar
|
||||||
|
Tony Beswick
|
||||||
|
Tony Narlock
|
||||||
|
Tony Zhaocheng Tan
|
||||||
|
TonyBeswick
|
||||||
|
toonarmycaptain
|
||||||
|
Toshio Kuratomi
|
||||||
|
toxinu
|
||||||
|
Travis Swicegood
|
||||||
|
Tushar Sadhwani
|
||||||
|
Tzu-ping Chung
|
||||||
|
Valentin Haenel
|
||||||
|
Victor Stinner
|
||||||
|
victorvpaulo
|
||||||
|
Vikram - Google
|
||||||
|
Viktor Szépe
|
||||||
|
Ville Skyttä
|
||||||
|
Vinay Sajip
|
||||||
|
Vincent Philippon
|
||||||
|
Vinicyus Macedo
|
||||||
|
Vipul Kumar
|
||||||
|
Vitaly Babiy
|
||||||
|
Vladimir Fokow
|
||||||
|
Vladimir Rutsky
|
||||||
|
W. Trevor King
|
||||||
|
Wil Tan
|
||||||
|
Wilfred Hughes
|
||||||
|
William Edwards
|
||||||
|
William ML Leslie
|
||||||
|
William T Olson
|
||||||
|
William Woodruff
|
||||||
|
Wilson Mo
|
||||||
|
wim glenn
|
||||||
|
Winson Luk
|
||||||
|
Wolfgang Maier
|
||||||
|
Wu Zhenyu
|
||||||
|
XAMES3
|
||||||
|
Xavier Fernandez
|
||||||
|
xoviat
|
||||||
|
xtreak
|
||||||
|
YAMAMOTO Takashi
|
||||||
|
Yen Chi Hsuan
|
||||||
|
Yeray Diaz Diaz
|
||||||
|
Yoval P
|
||||||
|
Yu Jian
|
||||||
|
Yuan Jing Vincent Yan
|
||||||
|
Yusuke Hayashi
|
||||||
|
Zearin
|
||||||
|
Zhiping Deng
|
||||||
|
ziebam
|
||||||
|
Zvezdan Petkovic
|
||||||
|
Łukasz Langa
|
||||||
|
Роман Донченко
|
||||||
|
Семён Марьясин
|
||||||
|
rekcäH nitraM
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2008-present The pip developers (see AUTHORS.txt file)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: pip
|
||||||
|
Version: 24.0
|
||||||
|
Summary: The PyPA recommended tool for installing Python packages.
|
||||||
|
Author-email: The pip developers <distutils-sig@python.org>
|
||||||
|
License: MIT
|
||||||
|
Project-URL: Homepage, https://pip.pypa.io/
|
||||||
|
Project-URL: Documentation, https://pip.pypa.io
|
||||||
|
Project-URL: Source, https://github.com/pypa/pip
|
||||||
|
Project-URL: Changelog, https://pip.pypa.io/en/stable/news/
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Topic :: Software Development :: Build Tools
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Requires-Python: >=3.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.txt
|
||||||
|
License-File: AUTHORS.txt
|
||||||
|
|
||||||
|
pip - The Python Package Installer
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/pip.svg
|
||||||
|
:target: https://pypi.org/project/pip/
|
||||||
|
:alt: PyPI
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/pip
|
||||||
|
:target: https://pypi.org/project/pip
|
||||||
|
:alt: PyPI - Python Version
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/pip/badge/?version=latest
|
||||||
|
:target: https://pip.pypa.io/en/latest
|
||||||
|
:alt: Documentation
|
||||||
|
|
||||||
|
pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes.
|
||||||
|
|
||||||
|
Please take a look at our documentation for how to install and use pip:
|
||||||
|
|
||||||
|
* `Installation`_
|
||||||
|
* `Usage`_
|
||||||
|
|
||||||
|
We release updates regularly, with a new version every 3 months. Find more details in our documentation:
|
||||||
|
|
||||||
|
* `Release notes`_
|
||||||
|
* `Release process`_
|
||||||
|
|
||||||
|
If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms:
|
||||||
|
|
||||||
|
* `Issue tracking`_
|
||||||
|
* `Discourse channel`_
|
||||||
|
* `User IRC`_
|
||||||
|
|
||||||
|
If you want to get involved head over to GitHub to get the source code, look at our development documentation and feel free to jump on the developer mailing lists and chat rooms:
|
||||||
|
|
||||||
|
* `GitHub page`_
|
||||||
|
* `Development documentation`_
|
||||||
|
* `Development IRC`_
|
||||||
|
|
||||||
|
Code of Conduct
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Everyone interacting in the pip project's codebases, issue trackers, chat
|
||||||
|
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
|
||||||
|
|
||||||
|
.. _package installer: https://packaging.python.org/guides/tool-recommendations/
|
||||||
|
.. _Python Package Index: https://pypi.org
|
||||||
|
.. _Installation: https://pip.pypa.io/en/stable/installation/
|
||||||
|
.. _Usage: https://pip.pypa.io/en/stable/
|
||||||
|
.. _Release notes: https://pip.pypa.io/en/stable/news.html
|
||||||
|
.. _Release process: https://pip.pypa.io/en/latest/development/release-process/
|
||||||
|
.. _GitHub page: https://github.com/pypa/pip
|
||||||
|
.. _Development documentation: https://pip.pypa.io/en/latest/development
|
||||||
|
.. _Issue tracking: https://github.com/pypa/pip/issues
|
||||||
|
.. _Discourse channel: https://discuss.python.org/c/packaging
|
||||||
|
.. _User IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa
|
||||||
|
.. _Development IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa-dev
|
||||||
|
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.42.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[console_scripts]
|
||||||
|
pip = pip._internal.cli.main:main
|
||||||
|
pip3 = pip._internal.cli.main:main
|
||||||
|
pip3.12 = pip._internal.cli.main:main
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
__version__ = "24.0"
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: Optional[List[str]] = None) -> int:
|
||||||
|
"""This is an internal API only meant for use by pip's own console scripts.
|
||||||
|
|
||||||
|
For additional details, see https://github.com/pypa/pip/issues/7498.
|
||||||
|
"""
|
||||||
|
from pip._internal.utils.entrypoints import _wrapper
|
||||||
|
|
||||||
|
return _wrapper(args)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Remove '' and current working directory from the first entry
|
||||||
|
# of sys.path, if present to avoid using current directory
|
||||||
|
# in pip commands check, freeze, install, list and show,
|
||||||
|
# when invoked as python -m pip <command>
|
||||||
|
if sys.path[0] in ("", os.getcwd()):
|
||||||
|
sys.path.pop(0)
|
||||||
|
|
||||||
|
# If we are running from a wheel, add the wheel to sys.path
|
||||||
|
# This allows the usage python pip-*.whl/pip install pip-*.whl
|
||||||
|
if __package__ == "":
|
||||||
|
# __file__ is pip-*.whl/pip/__main__.py
|
||||||
|
# first dirname call strips of '/__main__.py', second strips off '/pip'
|
||||||
|
# Resulting path is the name of the wheel itself
|
||||||
|
# Add that to sys.path so we can import pip
|
||||||
|
path = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
sys.path.insert(0, path)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from pip._internal.cli.main import main as _main
|
||||||
|
|
||||||
|
sys.exit(_main())
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Execute exactly this copy of pip, within a different environment.
|
||||||
|
|
||||||
|
This file is named as it is, to ensure that this module can't be imported via
|
||||||
|
an import statement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# /!\ This version compatibility check section must be Python 2 compatible. /!\
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Copied from setup.py
|
||||||
|
PYTHON_REQUIRES = (3, 7)
|
||||||
|
|
||||||
|
|
||||||
|
def version_str(version): # type: ignore
|
||||||
|
return ".".join(str(v) for v in version)
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[:2] < PYTHON_REQUIRES:
|
||||||
|
raise SystemExit(
|
||||||
|
"This version of pip does not support python {} (requires >={}).".format(
|
||||||
|
version_str(sys.version_info[:2]), version_str(PYTHON_REQUIRES)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# From here on, we can use Python 3 features, but the syntax must remain
|
||||||
|
# Python 2 compatible.
|
||||||
|
|
||||||
|
import runpy # noqa: E402
|
||||||
|
from importlib.machinery import PathFinder # noqa: E402
|
||||||
|
from os.path import dirname # noqa: E402
|
||||||
|
|
||||||
|
PIP_SOURCES_ROOT = dirname(dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class PipImportRedirectingFinder:
|
||||||
|
@classmethod
|
||||||
|
def find_spec(self, fullname, path=None, target=None): # type: ignore
|
||||||
|
if fullname != "pip":
|
||||||
|
return None
|
||||||
|
|
||||||
|
spec = PathFinder.find_spec(fullname, [PIP_SOURCES_ROOT], target)
|
||||||
|
assert spec, (PIP_SOURCES_ROOT, fullname)
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
sys.meta_path.insert(0, PipImportRedirectingFinder())
|
||||||
|
|
||||||
|
assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"
|
||||||
|
runpy.run_module("pip", run_name="__main__", alter_sys=True)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pip._internal.utils import _log
|
||||||
|
|
||||||
|
# init_logging() must be called before any call to logging.getLogger()
|
||||||
|
# which happens at import of most modules.
|
||||||
|
_log.init_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: (Optional[List[str]]) = None) -> int:
|
||||||
|
"""This is preserved for old console scripts that may still be referencing
|
||||||
|
it.
|
||||||
|
|
||||||
|
For additional details, see https://github.com/pypa/pip/issues/7498.
|
||||||
|
"""
|
||||||
|
from pip._internal.utils.entrypoints import _wrapper
|
||||||
|
|
||||||
|
return _wrapper(args)
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
"""Build Environment used for isolation during sdist building
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import site
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from collections import OrderedDict
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
|
||||||
|
|
||||||
|
from pip._vendor.certifi import where
|
||||||
|
from pip._vendor.packaging.requirements import Requirement
|
||||||
|
from pip._vendor.packaging.version import Version
|
||||||
|
|
||||||
|
from pip import __file__ as pip_location
|
||||||
|
from pip._internal.cli.spinners import open_spinner
|
||||||
|
from pip._internal.locations import get_platlib, get_purelib, get_scheme
|
||||||
|
from pip._internal.metadata import get_default_environment, get_environment
|
||||||
|
from pip._internal.utils.subprocess import call_subprocess
|
||||||
|
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pip._internal.index.package_finder import PackageFinder
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]:
|
||||||
|
return (a, b) if a != b else (a,)
|
||||||
|
|
||||||
|
|
||||||
|
class _Prefix:
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
self.path = path
|
||||||
|
self.setup = False
|
||||||
|
scheme = get_scheme("", prefix=path)
|
||||||
|
self.bin_dir = scheme.scripts
|
||||||
|
self.lib_dirs = _dedup(scheme.purelib, scheme.platlib)
|
||||||
|
|
||||||
|
|
||||||
|
def get_runnable_pip() -> str:
|
||||||
|
"""Get a file to pass to a Python executable, to run the currently-running pip.
|
||||||
|
|
||||||
|
This is used to run a pip subprocess, for installing requirements into the build
|
||||||
|
environment.
|
||||||
|
"""
|
||||||
|
source = pathlib.Path(pip_location).resolve().parent
|
||||||
|
|
||||||
|
if not source.is_dir():
|
||||||
|
# This would happen if someone is using pip from inside a zip file. In that
|
||||||
|
# case, we can use that directly.
|
||||||
|
return str(source)
|
||||||
|
|
||||||
|
return os.fsdecode(source / "__pip-runner__.py")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_system_sitepackages() -> Set[str]:
|
||||||
|
"""Get system site packages
|
||||||
|
|
||||||
|
Usually from site.getsitepackages,
|
||||||
|
but fallback on `get_purelib()/get_platlib()` if unavailable
|
||||||
|
(e.g. in a virtualenv created by virtualenv<20)
|
||||||
|
|
||||||
|
Returns normalized set of strings.
|
||||||
|
"""
|
||||||
|
if hasattr(site, "getsitepackages"):
|
||||||
|
system_sites = site.getsitepackages()
|
||||||
|
else:
|
||||||
|
# virtualenv < 20 overwrites site.py without getsitepackages
|
||||||
|
# fallback on get_purelib/get_platlib.
|
||||||
|
# this is known to miss things, but shouldn't in the cases
|
||||||
|
# where getsitepackages() has been removed (inside a virtualenv)
|
||||||
|
system_sites = [get_purelib(), get_platlib()]
|
||||||
|
return {os.path.normcase(path) for path in system_sites}
|
||||||
|
|
||||||
|
|
||||||
|
class BuildEnvironment:
|
||||||
|
"""Creates and manages an isolated environment to install build deps"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
|
||||||
|
|
||||||
|
self._prefixes = OrderedDict(
|
||||||
|
(name, _Prefix(os.path.join(temp_dir.path, name)))
|
||||||
|
for name in ("normal", "overlay")
|
||||||
|
)
|
||||||
|
|
||||||
|
self._bin_dirs: List[str] = []
|
||||||
|
self._lib_dirs: List[str] = []
|
||||||
|
for prefix in reversed(list(self._prefixes.values())):
|
||||||
|
self._bin_dirs.append(prefix.bin_dir)
|
||||||
|
self._lib_dirs.extend(prefix.lib_dirs)
|
||||||
|
|
||||||
|
# Customize site to:
|
||||||
|
# - ensure .pth files are honored
|
||||||
|
# - prevent access to system site packages
|
||||||
|
system_sites = _get_system_sitepackages()
|
||||||
|
|
||||||
|
self._site_dir = os.path.join(temp_dir.path, "site")
|
||||||
|
if not os.path.exists(self._site_dir):
|
||||||
|
os.mkdir(self._site_dir)
|
||||||
|
with open(
|
||||||
|
os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
|
||||||
|
) as fp:
|
||||||
|
fp.write(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""
|
||||||
|
import os, site, sys
|
||||||
|
|
||||||
|
# First, drop system-sites related paths.
|
||||||
|
original_sys_path = sys.path[:]
|
||||||
|
known_paths = set()
|
||||||
|
for path in {system_sites!r}:
|
||||||
|
site.addsitedir(path, known_paths=known_paths)
|
||||||
|
system_paths = set(
|
||||||
|
os.path.normcase(path)
|
||||||
|
for path in sys.path[len(original_sys_path):]
|
||||||
|
)
|
||||||
|
original_sys_path = [
|
||||||
|
path for path in original_sys_path
|
||||||
|
if os.path.normcase(path) not in system_paths
|
||||||
|
]
|
||||||
|
sys.path = original_sys_path
|
||||||
|
|
||||||
|
# Second, add lib directories.
|
||||||
|
# ensuring .pth file are processed.
|
||||||
|
for path in {lib_dirs!r}:
|
||||||
|
assert not path in sys.path
|
||||||
|
site.addsitedir(path)
|
||||||
|
"""
|
||||||
|
).format(system_sites=system_sites, lib_dirs=self._lib_dirs)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self) -> None:
|
||||||
|
self._save_env = {
|
||||||
|
name: os.environ.get(name, None)
|
||||||
|
for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
path = self._bin_dirs[:]
|
||||||
|
old_path = self._save_env["PATH"]
|
||||||
|
if old_path:
|
||||||
|
path.extend(old_path.split(os.pathsep))
|
||||||
|
|
||||||
|
pythonpath = [self._site_dir]
|
||||||
|
|
||||||
|
os.environ.update(
|
||||||
|
{
|
||||||
|
"PATH": os.pathsep.join(path),
|
||||||
|
"PYTHONNOUSERSITE": "1",
|
||||||
|
"PYTHONPATH": os.pathsep.join(pythonpath),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException],
|
||||||
|
exc_tb: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
for varname, old_value in self._save_env.items():
|
||||||
|
if old_value is None:
|
||||||
|
os.environ.pop(varname, None)
|
||||||
|
else:
|
||||||
|
os.environ[varname] = old_value
|
||||||
|
|
||||||
|
def check_requirements(
|
||||||
|
self, reqs: Iterable[str]
|
||||||
|
) -> Tuple[Set[Tuple[str, str]], Set[str]]:
|
||||||
|
"""Return 2 sets:
|
||||||
|
- conflicting requirements: set of (installed, wanted) reqs tuples
|
||||||
|
- missing requirements: set of reqs
|
||||||
|
"""
|
||||||
|
missing = set()
|
||||||
|
conflicting = set()
|
||||||
|
if reqs:
|
||||||
|
env = (
|
||||||
|
get_environment(self._lib_dirs)
|
||||||
|
if hasattr(self, "_lib_dirs")
|
||||||
|
else get_default_environment()
|
||||||
|
)
|
||||||
|
for req_str in reqs:
|
||||||
|
req = Requirement(req_str)
|
||||||
|
# We're explicitly evaluating with an empty extra value, since build
|
||||||
|
# environments are not provided any mechanism to select specific extras.
|
||||||
|
if req.marker is not None and not req.marker.evaluate({"extra": ""}):
|
||||||
|
continue
|
||||||
|
dist = env.get_distribution(req.name)
|
||||||
|
if not dist:
|
||||||
|
missing.add(req_str)
|
||||||
|
continue
|
||||||
|
if isinstance(dist.version, Version):
|
||||||
|
installed_req_str = f"{req.name}=={dist.version}"
|
||||||
|
else:
|
||||||
|
installed_req_str = f"{req.name}==={dist.version}"
|
||||||
|
if not req.specifier.contains(dist.version, prereleases=True):
|
||||||
|
conflicting.add((installed_req_str, req_str))
|
||||||
|
# FIXME: Consider direct URL?
|
||||||
|
return conflicting, missing
|
||||||
|
|
||||||
|
def install_requirements(
|
||||||
|
self,
|
||||||
|
finder: "PackageFinder",
|
||||||
|
requirements: Iterable[str],
|
||||||
|
prefix_as_string: str,
|
||||||
|
*,
|
||||||
|
kind: str,
|
||||||
|
) -> None:
|
||||||
|
prefix = self._prefixes[prefix_as_string]
|
||||||
|
assert not prefix.setup
|
||||||
|
prefix.setup = True
|
||||||
|
if not requirements:
|
||||||
|
return
|
||||||
|
self._install_requirements(
|
||||||
|
get_runnable_pip(),
|
||||||
|
finder,
|
||||||
|
requirements,
|
||||||
|
prefix,
|
||||||
|
kind=kind,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _install_requirements(
|
||||||
|
pip_runnable: str,
|
||||||
|
finder: "PackageFinder",
|
||||||
|
requirements: Iterable[str],
|
||||||
|
prefix: _Prefix,
|
||||||
|
*,
|
||||||
|
kind: str,
|
||||||
|
) -> None:
|
||||||
|
args: List[str] = [
|
||||||
|
sys.executable,
|
||||||
|
pip_runnable,
|
||||||
|
"install",
|
||||||
|
"--ignore-installed",
|
||||||
|
"--no-user",
|
||||||
|
"--prefix",
|
||||||
|
prefix.path,
|
||||||
|
"--no-warn-script-location",
|
||||||
|
]
|
||||||
|
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||||
|
args.append("-v")
|
||||||
|
for format_control in ("no_binary", "only_binary"):
|
||||||
|
formats = getattr(finder.format_control, format_control)
|
||||||
|
args.extend(
|
||||||
|
(
|
||||||
|
"--" + format_control.replace("_", "-"),
|
||||||
|
",".join(sorted(formats or {":none:"})),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
index_urls = finder.index_urls
|
||||||
|
if index_urls:
|
||||||
|
args.extend(["-i", index_urls[0]])
|
||||||
|
for extra_index in index_urls[1:]:
|
||||||
|
args.extend(["--extra-index-url", extra_index])
|
||||||
|
else:
|
||||||
|
args.append("--no-index")
|
||||||
|
for link in finder.find_links:
|
||||||
|
args.extend(["--find-links", link])
|
||||||
|
|
||||||
|
for host in finder.trusted_hosts:
|
||||||
|
args.extend(["--trusted-host", host])
|
||||||
|
if finder.allow_all_prereleases:
|
||||||
|
args.append("--pre")
|
||||||
|
if finder.prefer_binary:
|
||||||
|
args.append("--prefer-binary")
|
||||||
|
args.append("--")
|
||||||
|
args.extend(requirements)
|
||||||
|
extra_environ = {"_PIP_STANDALONE_CERT": where()}
|
||||||
|
with open_spinner(f"Installing {kind}") as spinner:
|
||||||
|
call_subprocess(
|
||||||
|
args,
|
||||||
|
command_desc=f"pip subprocess to install {kind}",
|
||||||
|
spinner=spinner,
|
||||||
|
extra_environ=extra_environ,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NoOpBuildEnvironment(BuildEnvironment):
|
||||||
|
"""A no-op drop-in replacement for BuildEnvironment"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException],
|
||||||
|
exc_tb: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def install_requirements(
|
||||||
|
self,
|
||||||
|
finder: "PackageFinder",
|
||||||
|
requirements: Iterable[str],
|
||||||
|
prefix_as_string: str,
|
||||||
|
*,
|
||||||
|
kind: str,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
"""Cache Management
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
|
||||||
|
from pip._vendor.packaging.utils import canonicalize_name
|
||||||
|
|
||||||
|
from pip._internal.exceptions import InvalidWheelFilename
|
||||||
|
from pip._internal.models.direct_url import DirectUrl
|
||||||
|
from pip._internal.models.link import Link
|
||||||
|
from pip._internal.models.wheel import Wheel
|
||||||
|
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
||||||
|
from pip._internal.utils.urls import path_to_url
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ORIGIN_JSON_NAME = "origin.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_dict(d: Dict[str, str]) -> str:
|
||||||
|
"""Return a stable sha224 of a dictionary."""
|
||||||
|
s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||||
|
return hashlib.sha224(s.encode("ascii")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
"""An abstract class - provides cache directories for data from links
|
||||||
|
|
||||||
|
:param cache_dir: The root of the cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cache_dir: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
assert not cache_dir or os.path.isabs(cache_dir)
|
||||||
|
self.cache_dir = cache_dir or None
|
||||||
|
|
||||||
|
def _get_cache_path_parts(self, link: Link) -> List[str]:
|
||||||
|
"""Get parts of part that must be os.path.joined with cache_dir"""
|
||||||
|
|
||||||
|
# We want to generate an url to use as our cache key, we don't want to
|
||||||
|
# just re-use the URL because it might have other items in the fragment
|
||||||
|
# and we don't care about those.
|
||||||
|
key_parts = {"url": link.url_without_fragment}
|
||||||
|
if link.hash_name is not None and link.hash is not None:
|
||||||
|
key_parts[link.hash_name] = link.hash
|
||||||
|
if link.subdirectory_fragment:
|
||||||
|
key_parts["subdirectory"] = link.subdirectory_fragment
|
||||||
|
|
||||||
|
# Include interpreter name, major and minor version in cache key
|
||||||
|
# to cope with ill-behaved sdists that build a different wheel
|
||||||
|
# depending on the python version their setup.py is being run on,
|
||||||
|
# and don't encode the difference in compatibility tags.
|
||||||
|
# https://github.com/pypa/pip/issues/7296
|
||||||
|
key_parts["interpreter_name"] = interpreter_name()
|
||||||
|
key_parts["interpreter_version"] = interpreter_version()
|
||||||
|
|
||||||
|
# Encode our key url with sha224, we'll use this because it has similar
|
||||||
|
# security properties to sha256, but with a shorter total output (and
|
||||||
|
# thus less secure). However the differences don't make a lot of
|
||||||
|
# difference for our use case here.
|
||||||
|
hashed = _hash_dict(key_parts)
|
||||||
|
|
||||||
|
# We want to nest the directories some to prevent having a ton of top
|
||||||
|
# level directories where we might run out of sub directories on some
|
||||||
|
# FS.
|
||||||
|
parts = [hashed[:2], hashed[2:4], hashed[4:6], hashed[6:]]
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:
|
||||||
|
can_not_cache = not self.cache_dir or not canonical_package_name or not link
|
||||||
|
if can_not_cache:
|
||||||
|
return []
|
||||||
|
|
||||||
|
path = self.get_path_for_link(link)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
return [(candidate, path) for candidate in os.listdir(path)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_path_for_link(self, link: Link) -> str:
|
||||||
|
"""Return a directory to store cached items in for link."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
link: Link,
|
||||||
|
package_name: Optional[str],
|
||||||
|
supported_tags: List[Tag],
|
||||||
|
) -> Link:
|
||||||
|
"""Returns a link to a cached item if it exists, otherwise returns the
|
||||||
|
passed link.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleWheelCache(Cache):
|
||||||
|
"""A cache of wheels for future installs."""
|
||||||
|
|
||||||
|
def __init__(self, cache_dir: str) -> None:
|
||||||
|
super().__init__(cache_dir)
|
||||||
|
|
||||||
|
def get_path_for_link(self, link: Link) -> str:
|
||||||
|
"""Return a directory to store cached wheels for link
|
||||||
|
|
||||||
|
Because there are M wheels for any one sdist, we provide a directory
|
||||||
|
to cache them in, and then consult that directory when looking up
|
||||||
|
cache hits.
|
||||||
|
|
||||||
|
We only insert things into the cache if they have plausible version
|
||||||
|
numbers, so that we don't contaminate the cache with things that were
|
||||||
|
not unique. E.g. ./package might have dozens of installs done for it
|
||||||
|
and build a version of 0.0...and if we built and cached a wheel, we'd
|
||||||
|
end up using the same wheel even if the source has been edited.
|
||||||
|
|
||||||
|
:param link: The link of the sdist for which this will cache wheels.
|
||||||
|
"""
|
||||||
|
parts = self._get_cache_path_parts(link)
|
||||||
|
assert self.cache_dir
|
||||||
|
# Store wheels within the root cache_dir
|
||||||
|
return os.path.join(self.cache_dir, "wheels", *parts)
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
link: Link,
|
||||||
|
package_name: Optional[str],
|
||||||
|
supported_tags: List[Tag],
|
||||||
|
) -> Link:
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
if not package_name:
|
||||||
|
return link
|
||||||
|
|
||||||
|
canonical_package_name = canonicalize_name(package_name)
|
||||||
|
for wheel_name, wheel_dir in self._get_candidates(link, canonical_package_name):
|
||||||
|
try:
|
||||||
|
wheel = Wheel(wheel_name)
|
||||||
|
except InvalidWheelFilename:
|
||||||
|
continue
|
||||||
|
if canonicalize_name(wheel.name) != canonical_package_name:
|
||||||
|
logger.debug(
|
||||||
|
"Ignoring cached wheel %s for %s as it "
|
||||||
|
"does not match the expected distribution name %s.",
|
||||||
|
wheel_name,
|
||||||
|
link,
|
||||||
|
package_name,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if not wheel.supported(supported_tags):
|
||||||
|
# Built for a different python/arch/etc
|
||||||
|
continue
|
||||||
|
candidates.append(
|
||||||
|
(
|
||||||
|
wheel.support_index_min(supported_tags),
|
||||||
|
wheel_name,
|
||||||
|
wheel_dir,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return link
|
||||||
|
|
||||||
|
_, wheel_name, wheel_dir = min(candidates)
|
||||||
|
return Link(path_to_url(os.path.join(wheel_dir, wheel_name)))
|
||||||
|
|
||||||
|
|
||||||
|
class EphemWheelCache(SimpleWheelCache):
|
||||||
|
"""A SimpleWheelCache that creates it's own temporary cache directory"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._temp_dir = TempDirectory(
|
||||||
|
kind=tempdir_kinds.EPHEM_WHEEL_CACHE,
|
||||||
|
globally_managed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(self._temp_dir.path)
|
||||||
|
|
||||||
|
|
||||||
|
class CacheEntry:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
link: Link,
|
||||||
|
persistent: bool,
|
||||||
|
):
|
||||||
|
self.link = link
|
||||||
|
self.persistent = persistent
|
||||||
|
self.origin: Optional[DirectUrl] = None
|
||||||
|
origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME
|
||||||
|
if origin_direct_url_path.exists():
|
||||||
|
try:
|
||||||
|
self.origin = DirectUrl.from_json(
|
||||||
|
origin_direct_url_path.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
"Ignoring invalid cache entry origin file %s for %s (%s)",
|
||||||
|
origin_direct_url_path,
|
||||||
|
link.filename,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WheelCache(Cache):
|
||||||
|
"""Wraps EphemWheelCache and SimpleWheelCache into a single Cache
|
||||||
|
|
||||||
|
This Cache allows for gracefully degradation, using the ephem wheel cache
|
||||||
|
when a certain link is not found in the simple wheel cache first.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cache_dir: str) -> None:
|
||||||
|
super().__init__(cache_dir)
|
||||||
|
self._wheel_cache = SimpleWheelCache(cache_dir)
|
||||||
|
self._ephem_cache = EphemWheelCache()
|
||||||
|
|
||||||
|
def get_path_for_link(self, link: Link) -> str:
|
||||||
|
return self._wheel_cache.get_path_for_link(link)
|
||||||
|
|
||||||
|
def get_ephem_path_for_link(self, link: Link) -> str:
|
||||||
|
return self._ephem_cache.get_path_for_link(link)
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
link: Link,
|
||||||
|
package_name: Optional[str],
|
||||||
|
supported_tags: List[Tag],
|
||||||
|
) -> Link:
|
||||||
|
cache_entry = self.get_cache_entry(link, package_name, supported_tags)
|
||||||
|
if cache_entry is None:
|
||||||
|
return link
|
||||||
|
return cache_entry.link
|
||||||
|
|
||||||
|
def get_cache_entry(
|
||||||
|
self,
|
||||||
|
link: Link,
|
||||||
|
package_name: Optional[str],
|
||||||
|
supported_tags: List[Tag],
|
||||||
|
) -> Optional[CacheEntry]:
|
||||||
|
"""Returns a CacheEntry with a link to a cached item if it exists or
|
||||||
|
None. The cache entry indicates if the item was found in the persistent
|
||||||
|
or ephemeral cache.
|
||||||
|
"""
|
||||||
|
retval = self._wheel_cache.get(
|
||||||
|
link=link,
|
||||||
|
package_name=package_name,
|
||||||
|
supported_tags=supported_tags,
|
||||||
|
)
|
||||||
|
if retval is not link:
|
||||||
|
return CacheEntry(retval, persistent=True)
|
||||||
|
|
||||||
|
retval = self._ephem_cache.get(
|
||||||
|
link=link,
|
||||||
|
package_name=package_name,
|
||||||
|
supported_tags=supported_tags,
|
||||||
|
)
|
||||||
|
if retval is not link:
|
||||||
|
return CacheEntry(retval, persistent=False)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def record_download_origin(cache_dir: str, download_info: DirectUrl) -> None:
|
||||||
|
origin_path = Path(cache_dir) / ORIGIN_JSON_NAME
|
||||||
|
if origin_path.exists():
|
||||||
|
try:
|
||||||
|
origin = DirectUrl.from_json(origin_path.read_text(encoding="utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
"Could not read origin file %s in cache entry (%s). "
|
||||||
|
"Will attempt to overwrite it.",
|
||||||
|
origin_path,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# TODO: use DirectUrl.equivalent when
|
||||||
|
# https://github.com/pypa/pip/pull/10564 is merged.
|
||||||
|
if origin.url != download_info.url:
|
||||||
|
logger.warning(
|
||||||
|
"Origin URL %s in cache entry %s does not match download URL "
|
||||||
|
"%s. This is likely a pip bug or a cache corruption issue. "
|
||||||
|
"Will overwrite it with the new value.",
|
||||||
|
origin.url,
|
||||||
|
cache_dir,
|
||||||
|
download_info.url,
|
||||||
|
)
|
||||||
|
origin_path.write_text(download_info.to_json(), encoding="utf-8")
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""Subpackage containing all of pip's command line interface related code
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This file intentionally does not import submodules
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""Logic that powers autocompletion installed by ``pip completion``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from itertools import chain
|
||||||
|
from typing import Any, Iterable, List, Optional
|
||||||
|
|
||||||
|
from pip._internal.cli.main_parser import create_main_parser
|
||||||
|
from pip._internal.commands import commands_dict, create_command
|
||||||
|
from pip._internal.metadata import get_default_environment
|
||||||
|
|
||||||
|
|
||||||
|
def autocomplete() -> None:
|
||||||
|
"""Entry Point for completion of main and subcommand options."""
|
||||||
|
# Don't complete if user hasn't sourced bash_completion file.
|
||||||
|
if "PIP_AUTO_COMPLETE" not in os.environ:
|
||||||
|
return
|
||||||
|
cwords = os.environ["COMP_WORDS"].split()[1:]
|
||||||
|
cword = int(os.environ["COMP_CWORD"])
|
||||||
|
try:
|
||||||
|
current = cwords[cword - 1]
|
||||||
|
except IndexError:
|
||||||
|
current = ""
|
||||||
|
|
||||||
|
parser = create_main_parser()
|
||||||
|
subcommands = list(commands_dict)
|
||||||
|
options = []
|
||||||
|
|
||||||
|
# subcommand
|
||||||
|
subcommand_name: Optional[str] = None
|
||||||
|
for word in cwords:
|
||||||
|
if word in subcommands:
|
||||||
|
subcommand_name = word
|
||||||
|
break
|
||||||
|
# subcommand options
|
||||||
|
if subcommand_name is not None:
|
||||||
|
# special case: 'help' subcommand has no options
|
||||||
|
if subcommand_name == "help":
|
||||||
|
sys.exit(1)
|
||||||
|
# special case: list locally installed dists for show and uninstall
|
||||||
|
should_list_installed = not current.startswith("-") and subcommand_name in [
|
||||||
|
"show",
|
||||||
|
"uninstall",
|
||||||
|
]
|
||||||
|
if should_list_installed:
|
||||||
|
env = get_default_environment()
|
||||||
|
lc = current.lower()
|
||||||
|
installed = [
|
||||||
|
dist.canonical_name
|
||||||
|
for dist in env.iter_installed_distributions(local_only=True)
|
||||||
|
if dist.canonical_name.startswith(lc)
|
||||||
|
and dist.canonical_name not in cwords[1:]
|
||||||
|
]
|
||||||
|
# if there are no dists installed, fall back to option completion
|
||||||
|
if installed:
|
||||||
|
for dist in installed:
|
||||||
|
print(dist)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
should_list_installables = (
|
||||||
|
not current.startswith("-") and subcommand_name == "install"
|
||||||
|
)
|
||||||
|
if should_list_installables:
|
||||||
|
for path in auto_complete_paths(current, "path"):
|
||||||
|
print(path)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
subcommand = create_command(subcommand_name)
|
||||||
|
|
||||||
|
for opt in subcommand.parser.option_list_all:
|
||||||
|
if opt.help != optparse.SUPPRESS_HELP:
|
||||||
|
options += [
|
||||||
|
(opt_str, opt.nargs) for opt_str in opt._long_opts + opt._short_opts
|
||||||
|
]
|
||||||
|
|
||||||
|
# filter out previously specified options from available options
|
||||||
|
prev_opts = [x.split("=")[0] for x in cwords[1 : cword - 1]]
|
||||||
|
options = [(x, v) for (x, v) in options if x not in prev_opts]
|
||||||
|
# filter options by current input
|
||||||
|
options = [(k, v) for k, v in options if k.startswith(current)]
|
||||||
|
# get completion type given cwords and available subcommand options
|
||||||
|
completion_type = get_path_completion_type(
|
||||||
|
cwords,
|
||||||
|
cword,
|
||||||
|
subcommand.parser.option_list_all,
|
||||||
|
)
|
||||||
|
# get completion files and directories if ``completion_type`` is
|
||||||
|
# ``<file>``, ``<dir>`` or ``<path>``
|
||||||
|
if completion_type:
|
||||||
|
paths = auto_complete_paths(current, completion_type)
|
||||||
|
options = [(path, 0) for path in paths]
|
||||||
|
for option in options:
|
||||||
|
opt_label = option[0]
|
||||||
|
# append '=' to options which require args
|
||||||
|
if option[1] and option[0][:2] == "--":
|
||||||
|
opt_label += "="
|
||||||
|
print(opt_label)
|
||||||
|
else:
|
||||||
|
# show main parser options only when necessary
|
||||||
|
|
||||||
|
opts = [i.option_list for i in parser.option_groups]
|
||||||
|
opts.append(parser.option_list)
|
||||||
|
flattened_opts = chain.from_iterable(opts)
|
||||||
|
if current.startswith("-"):
|
||||||
|
for opt in flattened_opts:
|
||||||
|
if opt.help != optparse.SUPPRESS_HELP:
|
||||||
|
subcommands += opt._long_opts + opt._short_opts
|
||||||
|
else:
|
||||||
|
# get completion type given cwords and all available options
|
||||||
|
completion_type = get_path_completion_type(cwords, cword, flattened_opts)
|
||||||
|
if completion_type:
|
||||||
|
subcommands = list(auto_complete_paths(current, completion_type))
|
||||||
|
|
||||||
|
print(" ".join([x for x in subcommands if x.startswith(current)]))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_path_completion_type(
|
||||||
|
cwords: List[str], cword: int, opts: Iterable[Any]
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Get the type of path completion (``file``, ``dir``, ``path`` or None)
|
||||||
|
|
||||||
|
:param cwords: same as the environmental variable ``COMP_WORDS``
|
||||||
|
:param cword: same as the environmental variable ``COMP_CWORD``
|
||||||
|
:param opts: The available options to check
|
||||||
|
:return: path completion type (``file``, ``dir``, ``path`` or None)
|
||||||
|
"""
|
||||||
|
if cword < 2 or not cwords[cword - 2].startswith("-"):
|
||||||
|
return None
|
||||||
|
for opt in opts:
|
||||||
|
if opt.help == optparse.SUPPRESS_HELP:
|
||||||
|
continue
|
||||||
|
for o in str(opt).split("/"):
|
||||||
|
if cwords[cword - 2].split("=")[0] == o:
|
||||||
|
if not opt.metavar or any(
|
||||||
|
x in ("path", "file", "dir") for x in opt.metavar.split("/")
|
||||||
|
):
|
||||||
|
return opt.metavar
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def auto_complete_paths(current: str, completion_type: str) -> Iterable[str]:
|
||||||
|
"""If ``completion_type`` is ``file`` or ``path``, list all regular files
|
||||||
|
and directories starting with ``current``; otherwise only list directories
|
||||||
|
starting with ``current``.
|
||||||
|
|
||||||
|
:param current: The word to be completed
|
||||||
|
:param completion_type: path completion type(``file``, ``path`` or ``dir``)
|
||||||
|
:return: A generator of regular files and/or directories
|
||||||
|
"""
|
||||||
|
directory, filename = os.path.split(current)
|
||||||
|
current_path = os.path.abspath(directory)
|
||||||
|
# Don't complete paths if they can't be accessed
|
||||||
|
if not os.access(current_path, os.R_OK):
|
||||||
|
return
|
||||||
|
filename = os.path.normcase(filename)
|
||||||
|
# list all files that start with ``filename``
|
||||||
|
file_list = (
|
||||||
|
x for x in os.listdir(current_path) if os.path.normcase(x).startswith(filename)
|
||||||
|
)
|
||||||
|
for f in file_list:
|
||||||
|
opt = os.path.join(current_path, f)
|
||||||
|
comp_file = os.path.normcase(os.path.join(directory, f))
|
||||||
|
# complete regular files when there is not ``<dir>`` after option
|
||||||
|
# complete directories when there is ``<file>``, ``<path>`` or
|
||||||
|
# ``<dir>``after option
|
||||||
|
if completion_type != "dir" and os.path.isfile(opt):
|
||||||
|
yield comp_file
|
||||||
|
elif os.path.isdir(opt):
|
||||||
|
yield os.path.join(comp_file, "")
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
"""Base Command class, and related routines"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from optparse import Values
|
||||||
|
from typing import Any, Callable, List, Optional, Tuple
|
||||||
|
|
||||||
|
from pip._vendor.rich import traceback as rich_traceback
|
||||||
|
|
||||||
|
from pip._internal.cli import cmdoptions
|
||||||
|
from pip._internal.cli.command_context import CommandContextMixIn
|
||||||
|
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
|
||||||
|
from pip._internal.cli.status_codes import (
|
||||||
|
ERROR,
|
||||||
|
PREVIOUS_BUILD_DIR_ERROR,
|
||||||
|
UNKNOWN_ERROR,
|
||||||
|
VIRTUALENV_NOT_FOUND,
|
||||||
|
)
|
||||||
|
from pip._internal.exceptions import (
|
||||||
|
BadCommand,
|
||||||
|
CommandError,
|
||||||
|
DiagnosticPipError,
|
||||||
|
InstallationError,
|
||||||
|
NetworkConnectionError,
|
||||||
|
PreviousBuildDirError,
|
||||||
|
UninstallationError,
|
||||||
|
)
|
||||||
|
from pip._internal.utils.filesystem import check_path_owner
|
||||||
|
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
|
||||||
|
from pip._internal.utils.misc import get_prog, normalize_path
|
||||||
|
from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry as TempDirRegistry
|
||||||
|
from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry
|
||||||
|
from pip._internal.utils.virtualenv import running_under_virtualenv
|
||||||
|
|
||||||
|
__all__ = ["Command"]
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(CommandContextMixIn):
|
||||||
|
usage: str = ""
|
||||||
|
ignore_require_venv: bool = False
|
||||||
|
|
||||||
|
def __init__(self, name: str, summary: str, isolated: bool = False) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.summary = summary
|
||||||
|
self.parser = ConfigOptionParser(
|
||||||
|
usage=self.usage,
|
||||||
|
prog=f"{get_prog()} {name}",
|
||||||
|
formatter=UpdatingDefaultsHelpFormatter(),
|
||||||
|
add_help_option=False,
|
||||||
|
name=name,
|
||||||
|
description=self.__doc__,
|
||||||
|
isolated=isolated,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tempdir_registry: Optional[TempDirRegistry] = None
|
||||||
|
|
||||||
|
# Commands should add options to this option group
|
||||||
|
optgroup_name = f"{self.name.capitalize()} Options"
|
||||||
|
self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
|
||||||
|
|
||||||
|
# Add the general options
|
||||||
|
gen_opts = cmdoptions.make_option_group(
|
||||||
|
cmdoptions.general_group,
|
||||||
|
self.parser,
|
||||||
|
)
|
||||||
|
self.parser.add_option_group(gen_opts)
|
||||||
|
|
||||||
|
self.add_options()
|
||||||
|
|
||||||
|
def add_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_pip_version_check(self, options: Values) -> None:
|
||||||
|
"""
|
||||||
|
This is a no-op so that commands by default do not do the pip version
|
||||||
|
check.
|
||||||
|
"""
|
||||||
|
# Make sure we do the pip version check if the index_group options
|
||||||
|
# are present.
|
||||||
|
assert not hasattr(options, "no_index")
|
||||||
|
|
||||||
|
def run(self, options: Values, args: List[str]) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
|
||||||
|
# factored out for testability
|
||||||
|
return self.parser.parse_args(args)
|
||||||
|
|
||||||
|
def main(self, args: List[str]) -> int:
|
||||||
|
try:
|
||||||
|
with self.main_context():
|
||||||
|
return self._main(args)
|
||||||
|
finally:
|
||||||
|
logging.shutdown()
|
||||||
|
|
||||||
|
def _main(self, args: List[str]) -> int:
|
||||||
|
# We must initialize this before the tempdir manager, otherwise the
|
||||||
|
# configuration would not be accessible by the time we clean up the
|
||||||
|
# tempdir manager.
|
||||||
|
self.tempdir_registry = self.enter_context(tempdir_registry())
|
||||||
|
# Intentionally set as early as possible so globally-managed temporary
|
||||||
|
# directories are available to the rest of the code.
|
||||||
|
self.enter_context(global_tempdir_manager())
|
||||||
|
|
||||||
|
options, args = self.parse_args(args)
|
||||||
|
|
||||||
|
# Set verbosity so that it can be used elsewhere.
|
||||||
|
self.verbosity = options.verbose - options.quiet
|
||||||
|
|
||||||
|
level_number = setup_logging(
|
||||||
|
verbosity=self.verbosity,
|
||||||
|
no_color=options.no_color,
|
||||||
|
user_log_file=options.log,
|
||||||
|
)
|
||||||
|
|
||||||
|
always_enabled_features = set(options.features_enabled) & set(
|
||||||
|
cmdoptions.ALWAYS_ENABLED_FEATURES
|
||||||
|
)
|
||||||
|
if always_enabled_features:
|
||||||
|
logger.warning(
|
||||||
|
"The following features are always enabled: %s. ",
|
||||||
|
", ".join(sorted(always_enabled_features)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure that the --python argument isn't specified after the
|
||||||
|
# subcommand. We can tell, because if --python was specified,
|
||||||
|
# we should only reach this point if we're running in the created
|
||||||
|
# subprocess, which has the _PIP_RUNNING_IN_SUBPROCESS environment
|
||||||
|
# variable set.
|
||||||
|
if options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
|
||||||
|
logger.critical(
|
||||||
|
"The --python option must be placed before the pip subcommand name"
|
||||||
|
)
|
||||||
|
sys.exit(ERROR)
|
||||||
|
|
||||||
|
# TODO: Try to get these passing down from the command?
|
||||||
|
# without resorting to os.environ to hold these.
|
||||||
|
# This also affects isolated builds and it should.
|
||||||
|
|
||||||
|
if options.no_input:
|
||||||
|
os.environ["PIP_NO_INPUT"] = "1"
|
||||||
|
|
||||||
|
if options.exists_action:
|
||||||
|
os.environ["PIP_EXISTS_ACTION"] = " ".join(options.exists_action)
|
||||||
|
|
||||||
|
if options.require_venv and not self.ignore_require_venv:
|
||||||
|
# If a venv is required check if it can really be found
|
||||||
|
if not running_under_virtualenv():
|
||||||
|
logger.critical("Could not find an activated virtualenv (required).")
|
||||||
|
sys.exit(VIRTUALENV_NOT_FOUND)
|
||||||
|
|
||||||
|
if options.cache_dir:
|
||||||
|
options.cache_dir = normalize_path(options.cache_dir)
|
||||||
|
if not check_path_owner(options.cache_dir):
|
||||||
|
logger.warning(
|
||||||
|
"The directory '%s' or its parent directory is not owned "
|
||||||
|
"or is not writable by the current user. The cache "
|
||||||
|
"has been disabled. Check the permissions and owner of "
|
||||||
|
"that directory. If executing pip with sudo, you should "
|
||||||
|
"use sudo's -H flag.",
|
||||||
|
options.cache_dir,
|
||||||
|
)
|
||||||
|
options.cache_dir = None
|
||||||
|
|
||||||
|
def intercepts_unhandled_exc(
|
||||||
|
run_func: Callable[..., int]
|
||||||
|
) -> Callable[..., int]:
|
||||||
|
@functools.wraps(run_func)
|
||||||
|
def exc_logging_wrapper(*args: Any) -> int:
|
||||||
|
try:
|
||||||
|
status = run_func(*args)
|
||||||
|
assert isinstance(status, int)
|
||||||
|
return status
|
||||||
|
except DiagnosticPipError as exc:
|
||||||
|
logger.error("%s", exc, extra={"rich": True})
|
||||||
|
logger.debug("Exception information:", exc_info=True)
|
||||||
|
|
||||||
|
return ERROR
|
||||||
|
except PreviousBuildDirError as exc:
|
||||||
|
logger.critical(str(exc))
|
||||||
|
logger.debug("Exception information:", exc_info=True)
|
||||||
|
|
||||||
|
return PREVIOUS_BUILD_DIR_ERROR
|
||||||
|
except (
|
||||||
|
InstallationError,
|
||||||
|
UninstallationError,
|
||||||
|
BadCommand,
|
||||||
|
NetworkConnectionError,
|
||||||
|
) as exc:
|
||||||
|
logger.critical(str(exc))
|
||||||
|
logger.debug("Exception information:", exc_info=True)
|
||||||
|
|
||||||
|
return ERROR
|
||||||
|
except CommandError as exc:
|
||||||
|
logger.critical("%s", exc)
|
||||||
|
logger.debug("Exception information:", exc_info=True)
|
||||||
|
|
||||||
|
return ERROR
|
||||||
|
except BrokenStdoutLoggingError:
|
||||||
|
# Bypass our logger and write any remaining messages to
|
||||||
|
# stderr because stdout no longer works.
|
||||||
|
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
|
||||||
|
if level_number <= logging.DEBUG:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
|
||||||
|
return ERROR
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.critical("Operation cancelled by user")
|
||||||
|
logger.debug("Exception information:", exc_info=True)
|
||||||
|
|
||||||
|
return ERROR
|
||||||
|
except BaseException:
|
||||||
|
logger.critical("Exception:", exc_info=True)
|
||||||
|
|
||||||
|
return UNKNOWN_ERROR
|
||||||
|
|
||||||
|
return exc_logging_wrapper
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not options.debug_mode:
|
||||||
|
run = intercepts_unhandled_exc(self.run)
|
||||||
|
else:
|
||||||
|
run = self.run
|
||||||
|
rich_traceback.install(show_locals=True)
|
||||||
|
return run(options, args)
|
||||||
|
finally:
|
||||||
|
self.handle_pip_version_check(options)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
from contextlib import ExitStack, contextmanager
|
||||||
|
from typing import ContextManager, Generator, TypeVar
|
||||||
|
|
||||||
|
_T = TypeVar("_T", covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandContextMixIn:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._in_main_context = False
|
||||||
|
self._main_context = ExitStack()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def main_context(self) -> Generator[None, None, None]:
|
||||||
|
assert not self._in_main_context
|
||||||
|
|
||||||
|
self._in_main_context = True
|
||||||
|
try:
|
||||||
|
with self._main_context:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self._in_main_context = False
|
||||||
|
|
||||||
|
def enter_context(self, context_provider: ContextManager[_T]) -> _T:
|
||||||
|
assert self._in_main_context
|
||||||
|
|
||||||
|
return self._main_context.enter_context(context_provider)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Primary application entrypoint.
|
||||||
|
"""
|
||||||
|
import locale
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pip._internal.cli.autocompletion import autocomplete
|
||||||
|
from pip._internal.cli.main_parser import parse_command
|
||||||
|
from pip._internal.commands import create_command
|
||||||
|
from pip._internal.exceptions import PipError
|
||||||
|
from pip._internal.utils import deprecation
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Do not import and use main() directly! Using it directly is actively
|
||||||
|
# discouraged by pip's maintainers. The name, location and behavior of
|
||||||
|
# this function is subject to change, so calling it directly is not
|
||||||
|
# portable across different pip versions.
|
||||||
|
|
||||||
|
# In addition, running pip in-process is unsupported and unsafe. This is
|
||||||
|
# elaborated in detail at
|
||||||
|
# https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program.
|
||||||
|
# That document also provides suggestions that should work for nearly
|
||||||
|
# all users that are considering importing and using main() directly.
|
||||||
|
|
||||||
|
# However, we know that certain users will still want to invoke pip
|
||||||
|
# in-process. If you understand and accept the implications of using pip
|
||||||
|
# in an unsupported manner, the best approach is to use runpy to avoid
|
||||||
|
# depending on the exact location of this entry point.
|
||||||
|
|
||||||
|
# The following example shows how to use runpy to invoke pip in that
|
||||||
|
# case:
|
||||||
|
#
|
||||||
|
# sys.argv = ["pip", your, args, here]
|
||||||
|
# runpy.run_module("pip", run_name="__main__")
|
||||||
|
#
|
||||||
|
# Note that this will exit the process after running, unlike a direct
|
||||||
|
# call to main. As it is not safe to do any processing after calling
|
||||||
|
# main, this should not be an issue in practice.
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: Optional[List[str]] = None) -> int:
|
||||||
|
if args is None:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
# Suppress the pkg_resources deprecation warning
|
||||||
|
# Note - we use a module of .*pkg_resources to cover
|
||||||
|
# the normal case (pip._vendor.pkg_resources) and the
|
||||||
|
# devendored case (a bare pkg_resources)
|
||||||
|
warnings.filterwarnings(
|
||||||
|
action="ignore", category=DeprecationWarning, module=".*pkg_resources"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure our deprecation warnings to be sent through loggers
|
||||||
|
deprecation.install_warning_logger()
|
||||||
|
|
||||||
|
autocomplete()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd_name, cmd_args = parse_command(args)
|
||||||
|
except PipError as exc:
|
||||||
|
sys.stderr.write(f"ERROR: {exc}")
|
||||||
|
sys.stderr.write(os.linesep)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Needed for locale.getpreferredencoding(False) to work
|
||||||
|
# in pip._internal.utils.encoding.auto_decode
|
||||||
|
try:
|
||||||
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
|
except locale.Error as e:
|
||||||
|
# setlocale can apparently crash if locale are uninitialized
|
||||||
|
logger.debug("Ignoring error %s when setting locale", e)
|
||||||
|
command = create_command(cmd_name, isolated=("--isolated" in cmd_args))
|
||||||
|
|
||||||
|
return command.main(cmd_args)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""A single place for constructing and exposing the main parser
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from pip._internal.build_env import get_runnable_pip
|
||||||
|
from pip._internal.cli import cmdoptions
|
||||||
|
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
|
||||||
|
from pip._internal.commands import commands_dict, get_similar_commands
|
||||||
|
from pip._internal.exceptions import CommandError
|
||||||
|
from pip._internal.utils.misc import get_pip_version, get_prog
|
||||||
|
|
||||||
|
__all__ = ["create_main_parser", "parse_command"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_main_parser() -> ConfigOptionParser:
|
||||||
|
"""Creates and returns the main parser for pip's CLI"""
|
||||||
|
|
||||||
|
parser = ConfigOptionParser(
|
||||||
|
usage="\n%prog <command> [options]",
|
||||||
|
add_help_option=False,
|
||||||
|
formatter=UpdatingDefaultsHelpFormatter(),
|
||||||
|
name="global",
|
||||||
|
prog=get_prog(),
|
||||||
|
)
|
||||||
|
parser.disable_interspersed_args()
|
||||||
|
|
||||||
|
parser.version = get_pip_version()
|
||||||
|
|
||||||
|
# add the general options
|
||||||
|
gen_opts = cmdoptions.make_option_group(cmdoptions.general_group, parser)
|
||||||
|
parser.add_option_group(gen_opts)
|
||||||
|
|
||||||
|
# so the help formatter knows
|
||||||
|
parser.main = True # type: ignore
|
||||||
|
|
||||||
|
# create command listing for description
|
||||||
|
description = [""] + [
|
||||||
|
f"{name:27} {command_info.summary}"
|
||||||
|
for name, command_info in commands_dict.items()
|
||||||
|
]
|
||||||
|
parser.description = "\n".join(description)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def identify_python_interpreter(python: str) -> Optional[str]:
|
||||||
|
# If the named file exists, use it.
|
||||||
|
# If it's a directory, assume it's a virtual environment and
|
||||||
|
# look for the environment's Python executable.
|
||||||
|
if os.path.exists(python):
|
||||||
|
if os.path.isdir(python):
|
||||||
|
# bin/python for Unix, Scripts/python.exe for Windows
|
||||||
|
# Try both in case of odd cases like cygwin.
|
||||||
|
for exe in ("bin/python", "Scripts/python.exe"):
|
||||||
|
py = os.path.join(python, exe)
|
||||||
|
if os.path.exists(py):
|
||||||
|
return py
|
||||||
|
else:
|
||||||
|
return python
|
||||||
|
|
||||||
|
# Could not find the interpreter specified
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
|
||||||
|
parser = create_main_parser()
|
||||||
|
|
||||||
|
# Note: parser calls disable_interspersed_args(), so the result of this
|
||||||
|
# call is to split the initial args into the general options before the
|
||||||
|
# subcommand and everything else.
|
||||||
|
# For example:
|
||||||
|
# args: ['--timeout=5', 'install', '--user', 'INITools']
|
||||||
|
# general_options: ['--timeout==5']
|
||||||
|
# args_else: ['install', '--user', 'INITools']
|
||||||
|
general_options, args_else = parser.parse_args(args)
|
||||||
|
|
||||||
|
# --python
|
||||||
|
if general_options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
|
||||||
|
# Re-invoke pip using the specified Python interpreter
|
||||||
|
interpreter = identify_python_interpreter(general_options.python)
|
||||||
|
if interpreter is None:
|
||||||
|
raise CommandError(
|
||||||
|
f"Could not locate Python interpreter {general_options.python}"
|
||||||
|
)
|
||||||
|
|
||||||
|
pip_cmd = [
|
||||||
|
interpreter,
|
||||||
|
get_runnable_pip(),
|
||||||
|
]
|
||||||
|
pip_cmd.extend(args)
|
||||||
|
|
||||||
|
# Set a flag so the child doesn't re-invoke itself, causing
|
||||||
|
# an infinite loop.
|
||||||
|
os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
|
||||||
|
returncode = 0
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(pip_cmd)
|
||||||
|
returncode = proc.returncode
|
||||||
|
except (subprocess.SubprocessError, OSError) as exc:
|
||||||
|
raise CommandError(f"Failed to run pip under {interpreter}: {exc}")
|
||||||
|
sys.exit(returncode)
|
||||||
|
|
||||||
|
# --version
|
||||||
|
if general_options.version:
|
||||||
|
sys.stdout.write(parser.version)
|
||||||
|
sys.stdout.write(os.linesep)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# pip || pip help -> print_help()
|
||||||
|
if not args_else or (args_else[0] == "help" and len(args_else) == 1):
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# the subcommand name
|
||||||
|
cmd_name = args_else[0]
|
||||||
|
|
||||||
|
if cmd_name not in commands_dict:
|
||||||
|
guess = get_similar_commands(cmd_name)
|
||||||
|
|
||||||
|
msg = [f'unknown command "{cmd_name}"']
|
||||||
|
if guess:
|
||||||
|
msg.append(f'maybe you meant "{guess}"')
|
||||||
|
|
||||||
|
raise CommandError(" - ".join(msg))
|
||||||
|
|
||||||
|
# all the args without the subcommand
|
||||||
|
cmd_args = args[:]
|
||||||
|
cmd_args.remove(cmd_name)
|
||||||
|
|
||||||
|
return cmd_name, cmd_args
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
"""Base option parser setup"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import optparse
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from contextlib import suppress
|
||||||
|
from typing import Any, Dict, Generator, List, Tuple
|
||||||
|
|
||||||
|
from pip._internal.cli.status_codes import UNKNOWN_ERROR
|
||||||
|
from pip._internal.configuration import Configuration, ConfigurationError
|
||||||
|
from pip._internal.utils.misc import redact_auth_from_url, strtobool
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
|
||||||
|
"""A prettier/less verbose help formatter for optparse."""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
# help position must be aligned with __init__.parseopts.description
|
||||||
|
kwargs["max_help_position"] = 30
|
||||||
|
kwargs["indent_increment"] = 1
|
||||||
|
kwargs["width"] = shutil.get_terminal_size()[0] - 2
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def format_option_strings(self, option: optparse.Option) -> str:
|
||||||
|
return self._format_option_strings(option)
|
||||||
|
|
||||||
|
def _format_option_strings(
|
||||||
|
self, option: optparse.Option, mvarfmt: str = " <{}>", optsep: str = ", "
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Return a comma-separated list of option strings and metavars.
|
||||||
|
|
||||||
|
:param option: tuple of (short opt, long opt), e.g: ('-f', '--format')
|
||||||
|
:param mvarfmt: metavar format string
|
||||||
|
:param optsep: separator
|
||||||
|
"""
|
||||||
|
opts = []
|
||||||
|
|
||||||
|
if option._short_opts:
|
||||||
|
opts.append(option._short_opts[0])
|
||||||
|
if option._long_opts:
|
||||||
|
opts.append(option._long_opts[0])
|
||||||
|
if len(opts) > 1:
|
||||||
|
opts.insert(1, optsep)
|
||||||
|
|
||||||
|
if option.takes_value():
|
||||||
|
assert option.dest is not None
|
||||||
|
metavar = option.metavar or option.dest.lower()
|
||||||
|
opts.append(mvarfmt.format(metavar.lower()))
|
||||||
|
|
||||||
|
return "".join(opts)
|
||||||
|
|
||||||
|
def format_heading(self, heading: str) -> str:
|
||||||
|
if heading == "Options":
|
||||||
|
return ""
|
||||||
|
return heading + ":\n"
|
||||||
|
|
||||||
|
def format_usage(self, usage: str) -> str:
|
||||||
|
"""
|
||||||
|
Ensure there is only one newline between usage and the first heading
|
||||||
|
if there is no description.
|
||||||
|
"""
|
||||||
|
msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " "))
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def format_description(self, description: str) -> str:
|
||||||
|
# leave full control over description to us
|
||||||
|
if description:
|
||||||
|
if hasattr(self.parser, "main"):
|
||||||
|
label = "Commands"
|
||||||
|
else:
|
||||||
|
label = "Description"
|
||||||
|
# some doc strings have initial newlines, some don't
|
||||||
|
description = description.lstrip("\n")
|
||||||
|
# some doc strings have final newlines and spaces, some don't
|
||||||
|
description = description.rstrip()
|
||||||
|
# dedent, then reindent
|
||||||
|
description = self.indent_lines(textwrap.dedent(description), " ")
|
||||||
|
description = f"{label}:\n{description}\n"
|
||||||
|
return description
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def format_epilog(self, epilog: str) -> str:
|
||||||
|
# leave full control over epilog to us
|
||||||
|
if epilog:
|
||||||
|
return epilog
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def indent_lines(self, text: str, indent: str) -> str:
|
||||||
|
new_lines = [indent + line for line in text.split("\n")]
|
||||||
|
return "\n".join(new_lines)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
|
||||||
|
"""Custom help formatter for use in ConfigOptionParser.
|
||||||
|
|
||||||
|
This is updates the defaults before expanding them, allowing
|
||||||
|
them to show up correctly in the help listing.
|
||||||
|
|
||||||
|
Also redact auth from url type options
|
||||||
|
"""
|
||||||
|
|
||||||
|
def expand_default(self, option: optparse.Option) -> str:
|
||||||
|
default_values = None
|
||||||
|
if self.parser is not None:
|
||||||
|
assert isinstance(self.parser, ConfigOptionParser)
|
||||||
|
self.parser._update_defaults(self.parser.defaults)
|
||||||
|
assert option.dest is not None
|
||||||
|
default_values = self.parser.defaults.get(option.dest)
|
||||||
|
help_text = super().expand_default(option)
|
||||||
|
|
||||||
|
if default_values and option.metavar == "URL":
|
||||||
|
if isinstance(default_values, str):
|
||||||
|
default_values = [default_values]
|
||||||
|
|
||||||
|
# If its not a list, we should abort and just return the help text
|
||||||
|
if not isinstance(default_values, list):
|
||||||
|
default_values = []
|
||||||
|
|
||||||
|
for val in default_values:
|
||||||
|
help_text = help_text.replace(val, redact_auth_from_url(val))
|
||||||
|
|
||||||
|
return help_text
|
||||||
|
|
||||||
|
|
||||||
|
class CustomOptionParser(optparse.OptionParser):
|
||||||
|
def insert_option_group(
|
||||||
|
self, idx: int, *args: Any, **kwargs: Any
|
||||||
|
) -> optparse.OptionGroup:
|
||||||
|
"""Insert an OptionGroup at a given position."""
|
||||||
|
group = self.add_option_group(*args, **kwargs)
|
||||||
|
|
||||||
|
self.option_groups.pop()
|
||||||
|
self.option_groups.insert(idx, group)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def option_list_all(self) -> List[optparse.Option]:
|
||||||
|
"""Get a list of all options, including those in option groups."""
|
||||||
|
res = self.option_list[:]
|
||||||
|
for i in self.option_groups:
|
||||||
|
res.extend(i.option_list)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigOptionParser(CustomOptionParser):
|
||||||
|
"""Custom option parser which updates its defaults by checking the
|
||||||
|
configuration files and environmental variables"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args: Any,
|
||||||
|
name: str,
|
||||||
|
isolated: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.config = Configuration(isolated)
|
||||||
|
|
||||||
|
assert self.name
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def check_default(self, option: optparse.Option, key: str, val: Any) -> Any:
|
||||||
|
try:
|
||||||
|
return option.check_value(key, val)
|
||||||
|
except optparse.OptionValueError as exc:
|
||||||
|
print(f"An error occurred during configuration: {exc}")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
def _get_ordered_configuration_items(
|
||||||
|
self,
|
||||||
|
) -> Generator[Tuple[str, Any], None, None]:
|
||||||
|
# Configuration gives keys in an unordered manner. Order them.
|
||||||
|
override_order = ["global", self.name, ":env:"]
|
||||||
|
|
||||||
|
# Pool the options into different groups
|
||||||
|
section_items: Dict[str, List[Tuple[str, Any]]] = {
|
||||||
|
name: [] for name in override_order
|
||||||
|
}
|
||||||
|
for section_key, val in self.config.items():
|
||||||
|
# ignore empty values
|
||||||
|
if not val:
|
||||||
|
logger.debug(
|
||||||
|
"Ignoring configuration key '%s' as it's value is empty.",
|
||||||
|
section_key,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
section, key = section_key.split(".", 1)
|
||||||
|
if section in override_order:
|
||||||
|
section_items[section].append((key, val))
|
||||||
|
|
||||||
|
# Yield each group in their override order
|
||||||
|
for section in override_order:
|
||||||
|
for key, val in section_items[section]:
|
||||||
|
yield key, val
|
||||||
|
|
||||||
|
def _update_defaults(self, defaults: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Updates the given defaults with values from the config files and
|
||||||
|
the environ. Does a little special handling for certain types of
|
||||||
|
options (lists)."""
|
||||||
|
|
||||||
|
# Accumulate complex default state.
|
||||||
|
self.values = optparse.Values(self.defaults)
|
||||||
|
late_eval = set()
|
||||||
|
# Then set the options with those values
|
||||||
|
for key, val in self._get_ordered_configuration_items():
|
||||||
|
# '--' because configuration supports only long names
|
||||||
|
option = self.get_option("--" + key)
|
||||||
|
|
||||||
|
# Ignore options not present in this parser. E.g. non-globals put
|
||||||
|
# in [global] by users that want them to apply to all applicable
|
||||||
|
# commands.
|
||||||
|
if option is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert option.dest is not None
|
||||||
|
|
||||||
|
if option.action in ("store_true", "store_false"):
|
||||||
|
try:
|
||||||
|
val = strtobool(val)
|
||||||
|
except ValueError:
|
||||||
|
self.error(
|
||||||
|
f"{val} is not a valid value for {key} option, "
|
||||||
|
"please specify a boolean value like yes/no, "
|
||||||
|
"true/false or 1/0 instead."
|
||||||
|
)
|
||||||
|
elif option.action == "count":
|
||||||
|
with suppress(ValueError):
|
||||||
|
val = strtobool(val)
|
||||||
|
with suppress(ValueError):
|
||||||
|
val = int(val)
|
||||||
|
if not isinstance(val, int) or val < 0:
|
||||||
|
self.error(
|
||||||
|
f"{val} is not a valid value for {key} option, "
|
||||||
|
"please instead specify either a non-negative integer "
|
||||||
|
"or a boolean value like yes/no or false/true "
|
||||||
|
"which is equivalent to 1/0."
|
||||||
|
)
|
||||||
|
elif option.action == "append":
|
||||||
|
val = val.split()
|
||||||
|
val = [self.check_default(option, key, v) for v in val]
|
||||||
|
elif option.action == "callback":
|
||||||
|
assert option.callback is not None
|
||||||
|
late_eval.add(option.dest)
|
||||||
|
opt_str = option.get_opt_string()
|
||||||
|
val = option.convert_value(opt_str, val)
|
||||||
|
# From take_action
|
||||||
|
args = option.callback_args or ()
|
||||||
|
kwargs = option.callback_kwargs or {}
|
||||||
|
option.callback(option, opt_str, val, self, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
val = self.check_default(option, key, val)
|
||||||
|
|
||||||
|
defaults[option.dest] = val
|
||||||
|
|
||||||
|
for key in late_eval:
|
||||||
|
defaults[key] = getattr(self.values, key)
|
||||||
|
self.values = None
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
def get_default_values(self) -> optparse.Values:
|
||||||
|
"""Overriding to make updating the defaults after instantiation of
|
||||||
|
the option parser possible, _update_defaults() does the dirty work."""
|
||||||
|
if not self.process_default_values:
|
||||||
|
# Old, pre-Optik 1.5 behaviour.
|
||||||
|
return optparse.Values(self.defaults)
|
||||||
|
|
||||||
|
# Load the configuration, or error out in case of an error
|
||||||
|
try:
|
||||||
|
self.config.load()
|
||||||
|
except ConfigurationError as err:
|
||||||
|
self.exit(UNKNOWN_ERROR, str(err))
|
||||||
|
|
||||||
|
defaults = self._update_defaults(self.defaults.copy()) # ours
|
||||||
|
for option in self._get_all_options():
|
||||||
|
assert option.dest is not None
|
||||||
|
default = defaults.get(option.dest)
|
||||||
|
if isinstance(default, str):
|
||||||
|
opt_str = option.get_opt_string()
|
||||||
|
defaults[option.dest] = option.check_value(opt_str, default)
|
||||||
|
return optparse.Values(defaults)
|
||||||
|
|
||||||
|
def error(self, msg: str) -> None:
|
||||||
|
self.print_usage(sys.stderr)
|
||||||
|
self.exit(UNKNOWN_ERROR, f"{msg}\n")
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import functools
|
||||||
|
from typing import Callable, Generator, Iterable, Iterator, Optional, Tuple
|
||||||
|
|
||||||
|
from pip._vendor.rich.progress import (
|
||||||
|
BarColumn,
|
||||||
|
DownloadColumn,
|
||||||
|
FileSizeColumn,
|
||||||
|
Progress,
|
||||||
|
ProgressColumn,
|
||||||
|
SpinnerColumn,
|
||||||
|
TextColumn,
|
||||||
|
TimeElapsedColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
TransferSpeedColumn,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pip._internal.utils.logging import get_indentation
|
||||||
|
|
||||||
|
DownloadProgressRenderer = Callable[[Iterable[bytes]], Iterator[bytes]]
|
||||||
|
|
||||||
|
|
||||||
|
def _rich_progress_bar(
|
||||||
|
iterable: Iterable[bytes],
|
||||||
|
*,
|
||||||
|
bar_type: str,
|
||||||
|
size: int,
|
||||||
|
) -> Generator[bytes, None, None]:
|
||||||
|
assert bar_type == "on", "This should only be used in the default mode."
|
||||||
|
|
||||||
|
if not size:
|
||||||
|
total = float("inf")
|
||||||
|
columns: Tuple[ProgressColumn, ...] = (
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
SpinnerColumn("line", speed=1.5),
|
||||||
|
FileSizeColumn(),
|
||||||
|
TransferSpeedColumn(),
|
||||||
|
TimeElapsedColumn(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
total = size
|
||||||
|
columns = (
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
DownloadColumn(),
|
||||||
|
TransferSpeedColumn(),
|
||||||
|
TextColumn("eta"),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
)
|
||||||
|
|
||||||
|
progress = Progress(*columns, refresh_per_second=30)
|
||||||
|
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
|
||||||
|
with progress:
|
||||||
|
for chunk in iterable:
|
||||||
|
yield chunk
|
||||||
|
progress.update(task_id, advance=len(chunk))
|
||||||
|
|
||||||
|
|
||||||
|
def get_download_progress_renderer(
|
||||||
|
*, bar_type: str, size: Optional[int] = None
|
||||||
|
) -> DownloadProgressRenderer:
|
||||||
|
"""Get an object that can be used to render the download progress.
|
||||||
|
|
||||||
|
Returns a callable, that takes an iterable to "wrap".
|
||||||
|
"""
|
||||||
|
if bar_type == "on":
|
||||||
|
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
|
||||||
|
else:
|
||||||
|
return iter # no-op, when passed an iterator
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
"""Contains the Command base classes that depend on PipSession.
|
||||||
|
|
||||||
|
The classes in this module are in a separate module so the commands not
|
||||||
|
needing download / PackageFinder capability don't unnecessarily import the
|
||||||
|
PackageFinder machinery and all its vendored dependencies, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from functools import partial
|
||||||
|
from optparse import Values
|
||||||
|
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
|
||||||
|
|
||||||
|
from pip._internal.cache import WheelCache
|
||||||
|
from pip._internal.cli import cmdoptions
|
||||||
|
from pip._internal.cli.base_command import Command
|
||||||
|
from pip._internal.cli.command_context import CommandContextMixIn
|
||||||
|
from pip._internal.exceptions import CommandError, PreviousBuildDirError
|
||||||
|
from pip._internal.index.collector import LinkCollector
|
||||||
|
from pip._internal.index.package_finder import PackageFinder
|
||||||
|
from pip._internal.models.selection_prefs import SelectionPreferences
|
||||||
|
from pip._internal.models.target_python import TargetPython
|
||||||
|
from pip._internal.network.session import PipSession
|
||||||
|
from pip._internal.operations.build.build_tracker import BuildTracker
|
||||||
|
from pip._internal.operations.prepare import RequirementPreparer
|
||||||
|
from pip._internal.req.constructors import (
|
||||||
|
install_req_from_editable,
|
||||||
|
install_req_from_line,
|
||||||
|
install_req_from_parsed_requirement,
|
||||||
|
install_req_from_req_string,
|
||||||
|
)
|
||||||
|
from pip._internal.req.req_file import parse_requirements
|
||||||
|
from pip._internal.req.req_install import InstallRequirement
|
||||||
|
from pip._internal.resolution.base import BaseResolver
|
||||||
|
from pip._internal.self_outdated_check import pip_self_version_check
|
||||||
|
from pip._internal.utils.temp_dir import (
|
||||||
|
TempDirectory,
|
||||||
|
TempDirectoryTypeRegistry,
|
||||||
|
tempdir_kinds,
|
||||||
|
)
|
||||||
|
from pip._internal.utils.virtualenv import running_under_virtualenv
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ssl import SSLContext
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
raise CommandError("The truststore feature is only available for Python 3.10+")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("Disabling truststore since ssl support is missing")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pip._vendor import truststore
|
||||||
|
except ImportError as e:
|
||||||
|
raise CommandError(f"The truststore feature is unavailable: {e}")
|
||||||
|
|
||||||
|
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionCommandMixin(CommandContextMixIn):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A class mixin for command classes needing _build_session().
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._session: Optional[PipSession] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
|
||||||
|
"""Return a list of index urls from user-provided options."""
|
||||||
|
index_urls = []
|
||||||
|
if not getattr(options, "no_index", False):
|
||||||
|
url = getattr(options, "index_url", None)
|
||||||
|
if url:
|
||||||
|
index_urls.append(url)
|
||||||
|
urls = getattr(options, "extra_index_urls", None)
|
||||||
|
if urls:
|
||||||
|
index_urls.extend(urls)
|
||||||
|
# Return None rather than an empty list
|
||||||
|
return index_urls or None
|
||||||
|
|
||||||
|
def get_default_session(self, options: Values) -> PipSession:
|
||||||
|
"""Get a default-managed session."""
|
||||||
|
if self._session is None:
|
||||||
|
self._session = self.enter_context(self._build_session(options))
|
||||||
|
# there's no type annotation on requests.Session, so it's
|
||||||
|
# automatically ContextManager[Any] and self._session becomes Any,
|
||||||
|
# then https://github.com/python/mypy/issues/7696 kicks in
|
||||||
|
assert self._session is not None
|
||||||
|
return self._session
|
||||||
|
|
||||||
|
def _build_session(
|
||||||
|
self,
|
||||||
|
options: Values,
|
||||||
|
retries: Optional[int] = None,
|
||||||
|
timeout: Optional[int] = None,
|
||||||
|
fallback_to_certifi: bool = False,
|
||||||
|
) -> PipSession:
|
||||||
|
cache_dir = options.cache_dir
|
||||||
|
assert not cache_dir or os.path.isabs(cache_dir)
|
||||||
|
|
||||||
|
if "truststore" in options.features_enabled:
|
||||||
|
try:
|
||||||
|
ssl_context = _create_truststore_ssl_context()
|
||||||
|
except Exception:
|
||||||
|
if not fallback_to_certifi:
|
||||||
|
raise
|
||||||
|
ssl_context = None
|
||||||
|
else:
|
||||||
|
ssl_context = None
|
||||||
|
|
||||||
|
session = PipSession(
|
||||||
|
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
|
||||||
|
retries=retries if retries is not None else options.retries,
|
||||||
|
trusted_hosts=options.trusted_hosts,
|
||||||
|
index_urls=self._get_index_urls(options),
|
||||||
|
ssl_context=ssl_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle custom ca-bundles from the user
|
||||||
|
if options.cert:
|
||||||
|
session.verify = options.cert
|
||||||
|
|
||||||
|
# Handle SSL client certificate
|
||||||
|
if options.client_cert:
|
||||||
|
session.cert = options.client_cert
|
||||||
|
|
||||||
|
# Handle timeouts
|
||||||
|
if options.timeout or timeout:
|
||||||
|
session.timeout = timeout if timeout is not None else options.timeout
|
||||||
|
|
||||||
|
# Handle configured proxies
|
||||||
|
if options.proxy:
|
||||||
|
session.proxies = {
|
||||||
|
"http": options.proxy,
|
||||||
|
"https": options.proxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine if we can prompt the user for authentication or not
|
||||||
|
session.auth.prompting = not options.no_input
|
||||||
|
session.auth.keyring_provider = options.keyring_provider
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
class IndexGroupCommand(Command, SessionCommandMixin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Abstract base class for commands with the index_group options.
|
||||||
|
|
||||||
|
This also corresponds to the commands that permit the pip version check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle_pip_version_check(self, options: Values) -> None:
|
||||||
|
"""
|
||||||
|
Do the pip version check if not disabled.
|
||||||
|
|
||||||
|
This overrides the default behavior of not doing the check.
|
||||||
|
"""
|
||||||
|
# Make sure the index_group options are present.
|
||||||
|
assert hasattr(options, "no_index")
|
||||||
|
|
||||||
|
if options.disable_pip_version_check or options.no_index:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise, check if we're using the latest version of pip available.
|
||||||
|
session = self._build_session(
|
||||||
|
options,
|
||||||
|
retries=0,
|
||||||
|
timeout=min(5, options.timeout),
|
||||||
|
# This is set to ensure the function does not fail when truststore is
|
||||||
|
# specified in use-feature but cannot be loaded. This usually raises a
|
||||||
|
# CommandError and shows a nice user-facing error, but this function is not
|
||||||
|
# called in that try-except block.
|
||||||
|
fallback_to_certifi=True,
|
||||||
|
)
|
||||||
|
with session:
|
||||||
|
pip_self_version_check(session, options)
|
||||||
|
|
||||||
|
|
||||||
|
KEEPABLE_TEMPDIR_TYPES = [
|
||||||
|
tempdir_kinds.BUILD_ENV,
|
||||||
|
tempdir_kinds.EPHEM_WHEEL_CACHE,
|
||||||
|
tempdir_kinds.REQ_BUILD,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def warn_if_run_as_root() -> None:
|
||||||
|
"""Output a warning for sudo users on Unix.
|
||||||
|
|
||||||
|
In a virtual environment, sudo pip still writes to virtualenv.
|
||||||
|
On Windows, users may run pip as Administrator without issues.
|
||||||
|
This warning only applies to Unix root users outside of virtualenv.
|
||||||
|
"""
|
||||||
|
if running_under_virtualenv():
|
||||||
|
return
|
||||||
|
if not hasattr(os, "getuid"):
|
||||||
|
return
|
||||||
|
# On Windows, there are no "system managed" Python packages. Installing as
|
||||||
|
# Administrator via pip is the correct way of updating system environments.
|
||||||
|
#
|
||||||
|
# We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
|
||||||
|
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
|
||||||
|
if sys.platform == "win32" or sys.platform == "cygwin":
|
||||||
|
return
|
||||||
|
|
||||||
|
if os.getuid() != 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"Running pip as the 'root' user can result in broken permissions and "
|
||||||
|
"conflicting behaviour with the system package manager. "
|
||||||
|
"It is recommended to use a virtual environment instead: "
|
||||||
|
"https://pip.pypa.io/warnings/venv"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def with_cleanup(func: Any) -> Any:
|
||||||
|
"""Decorator for common logic related to managing temporary
|
||||||
|
directories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def configure_tempdir_registry(registry: TempDirectoryTypeRegistry) -> None:
|
||||||
|
for t in KEEPABLE_TEMPDIR_TYPES:
|
||||||
|
registry.set_delete(t, False)
|
||||||
|
|
||||||
|
def wrapper(
|
||||||
|
self: RequirementCommand, options: Values, args: List[Any]
|
||||||
|
) -> Optional[int]:
|
||||||
|
assert self.tempdir_registry is not None
|
||||||
|
if options.no_clean:
|
||||||
|
configure_tempdir_registry(self.tempdir_registry)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return func(self, options, args)
|
||||||
|
except PreviousBuildDirError:
|
||||||
|
# This kind of conflict can occur when the user passes an explicit
|
||||||
|
# build directory with a pre-existing folder. In that case we do
|
||||||
|
# not want to accidentally remove it.
|
||||||
|
configure_tempdir_registry(self.tempdir_registry)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementCommand(IndexGroupCommand):
|
||||||
|
def __init__(self, *args: Any, **kw: Any) -> None:
|
||||||
|
super().__init__(*args, **kw)
|
||||||
|
|
||||||
|
self.cmd_opts.add_option(cmdoptions.no_clean())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def determine_resolver_variant(options: Values) -> str:
|
||||||
|
"""Determines which resolver should be used, based on the given options."""
|
||||||
|
if "legacy-resolver" in options.deprecated_features_enabled:
|
||||||
|
return "legacy"
|
||||||
|
|
||||||
|
return "resolvelib"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_requirement_preparer(
|
||||||
|
cls,
|
||||||
|
temp_build_dir: TempDirectory,
|
||||||
|
options: Values,
|
||||||
|
build_tracker: BuildTracker,
|
||||||
|
session: PipSession,
|
||||||
|
finder: PackageFinder,
|
||||||
|
use_user_site: bool,
|
||||||
|
download_dir: Optional[str] = None,
|
||||||
|
verbosity: int = 0,
|
||||||
|
) -> RequirementPreparer:
|
||||||
|
"""
|
||||||
|
Create a RequirementPreparer instance for the given parameters.
|
||||||
|
"""
|
||||||
|
temp_build_dir_path = temp_build_dir.path
|
||||||
|
assert temp_build_dir_path is not None
|
||||||
|
legacy_resolver = False
|
||||||
|
|
||||||
|
resolver_variant = cls.determine_resolver_variant(options)
|
||||||
|
if resolver_variant == "resolvelib":
|
||||||
|
lazy_wheel = "fast-deps" in options.features_enabled
|
||||||
|
if lazy_wheel:
|
||||||
|
logger.warning(
|
||||||
|
"pip is using lazily downloaded wheels using HTTP "
|
||||||
|
"range requests to obtain dependency information. "
|
||||||
|
"This experimental feature is enabled through "
|
||||||
|
"--use-feature=fast-deps and it is not ready for "
|
||||||
|
"production."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
legacy_resolver = True
|
||||||
|
lazy_wheel = False
|
||||||
|
if "fast-deps" in options.features_enabled:
|
||||||
|
logger.warning(
|
||||||
|
"fast-deps has no effect when used with the legacy resolver."
|
||||||
|
)
|
||||||
|
|
||||||
|
return RequirementPreparer(
|
||||||
|
build_dir=temp_build_dir_path,
|
||||||
|
src_dir=options.src_dir,
|
||||||
|
download_dir=download_dir,
|
||||||
|
build_isolation=options.build_isolation,
|
||||||
|
check_build_deps=options.check_build_deps,
|
||||||
|
build_tracker=build_tracker,
|
||||||
|
session=session,
|
||||||
|
progress_bar=options.progress_bar,
|
||||||
|
finder=finder,
|
||||||
|
require_hashes=options.require_hashes,
|
||||||
|
use_user_site=use_user_site,
|
||||||
|
lazy_wheel=lazy_wheel,
|
||||||
|
verbosity=verbosity,
|
||||||
|
legacy_resolver=legacy_resolver,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_resolver(
|
||||||
|
cls,
|
||||||
|
preparer: RequirementPreparer,
|
||||||
|
finder: PackageFinder,
|
||||||
|
options: Values,
|
||||||
|
wheel_cache: Optional[WheelCache] = None,
|
||||||
|
use_user_site: bool = False,
|
||||||
|
ignore_installed: bool = True,
|
||||||
|
ignore_requires_python: bool = False,
|
||||||
|
force_reinstall: bool = False,
|
||||||
|
upgrade_strategy: str = "to-satisfy-only",
|
||||||
|
use_pep517: Optional[bool] = None,
|
||||||
|
py_version_info: Optional[Tuple[int, ...]] = None,
|
||||||
|
) -> BaseResolver:
|
||||||
|
"""
|
||||||
|
Create a Resolver instance for the given parameters.
|
||||||
|
"""
|
||||||
|
make_install_req = partial(
|
||||||
|
install_req_from_req_string,
|
||||||
|
isolated=options.isolated_mode,
|
||||||
|
use_pep517=use_pep517,
|
||||||
|
)
|
||||||
|
resolver_variant = cls.determine_resolver_variant(options)
|
||||||
|
# The long import name and duplicated invocation is needed to convince
|
||||||
|
# Mypy into correctly typechecking. Otherwise it would complain the
|
||||||
|
# "Resolver" class being redefined.
|
||||||
|
if resolver_variant == "resolvelib":
|
||||||
|
import pip._internal.resolution.resolvelib.resolver
|
||||||
|
|
||||||
|
return pip._internal.resolution.resolvelib.resolver.Resolver(
|
||||||
|
preparer=preparer,
|
||||||
|
finder=finder,
|
||||||
|
wheel_cache=wheel_cache,
|
||||||
|
make_install_req=make_install_req,
|
||||||
|
use_user_site=use_user_site,
|
||||||
|
ignore_dependencies=options.ignore_dependencies,
|
||||||
|
ignore_installed=ignore_installed,
|
||||||
|
ignore_requires_python=ignore_requires_python,
|
||||||
|
force_reinstall=force_reinstall,
|
||||||
|
upgrade_strategy=upgrade_strategy,
|
||||||
|
py_version_info=py_version_info,
|
||||||
|
)
|
||||||
|
import pip._internal.resolution.legacy.resolver
|
||||||
|
|
||||||
|
return pip._internal.resolution.legacy.resolver.Resolver(
|
||||||
|
preparer=preparer,
|
||||||
|
finder=finder,
|
||||||
|
wheel_cache=wheel_cache,
|
||||||
|
make_install_req=make_install_req,
|
||||||
|
use_user_site=use_user_site,
|
||||||
|
ignore_dependencies=options.ignore_dependencies,
|
||||||
|
ignore_installed=ignore_installed,
|
||||||
|
ignore_requires_python=ignore_requires_python,
|
||||||
|
force_reinstall=force_reinstall,
|
||||||
|
upgrade_strategy=upgrade_strategy,
|
||||||
|
py_version_info=py_version_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_requirements(
|
||||||
|
self,
|
||||||
|
args: List[str],
|
||||||
|
options: Values,
|
||||||
|
finder: PackageFinder,
|
||||||
|
session: PipSession,
|
||||||
|
) -> List[InstallRequirement]:
|
||||||
|
"""
|
||||||
|
Parse command-line arguments into the corresponding requirements.
|
||||||
|
"""
|
||||||
|
requirements: List[InstallRequirement] = []
|
||||||
|
for filename in options.constraints:
|
||||||
|
for parsed_req in parse_requirements(
|
||||||
|
filename,
|
||||||
|
constraint=True,
|
||||||
|
finder=finder,
|
||||||
|
options=options,
|
||||||
|
session=session,
|
||||||
|
):
|
||||||
|
req_to_add = install_req_from_parsed_requirement(
|
||||||
|
parsed_req,
|
||||||
|
isolated=options.isolated_mode,
|
||||||
|
user_supplied=False,
|
||||||
|
)
|
||||||
|
requirements.append(req_to_add)
|
||||||
|
|
||||||
|
for req in args:
|
||||||
|
req_to_add = install_req_from_line(
|
||||||
|
req,
|
||||||
|
comes_from=None,
|
||||||
|
isolated=options.isolated_mode,
|
||||||
|
use_pep517=options.use_pep517,
|
||||||
|
user_supplied=True,
|
||||||
|
config_settings=getattr(options, "config_settings", None),
|
||||||
|
)
|
||||||
|
requirements.append(req_to_add)
|
||||||
|
|
||||||
|
for req in options.editables:
|
||||||
|
req_to_add = install_req_from_editable(
|
||||||
|
req,
|
||||||
|
user_supplied=True,
|
||||||
|
isolated=options.isolated_mode,
|
||||||
|
use_pep517=options.use_pep517,
|
||||||
|
config_settings=getattr(options, "config_settings", None),
|
||||||
|
)
|
||||||
|
requirements.append(req_to_add)
|
||||||
|
|
||||||
|
# NOTE: options.require_hashes may be set if --require-hashes is True
|
||||||
|
for filename in options.requirements:
|
||||||
|
for parsed_req in parse_requirements(
|
||||||
|
filename, finder=finder, options=options, session=session
|
||||||
|
):
|
||||||
|
req_to_add = install_req_from_parsed_requirement(
|
||||||
|
parsed_req,
|
||||||
|
isolated=options.isolated_mode,
|
||||||
|
use_pep517=options.use_pep517,
|
||||||
|
user_supplied=True,
|
||||||
|
config_settings=parsed_req.options.get("config_settings")
|
||||||
|
if parsed_req.options
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
requirements.append(req_to_add)
|
||||||
|
|
||||||
|
# If any requirement has hash options, enable hash checking.
|
||||||
|
if any(req.has_hash_options for req in requirements):
|
||||||
|
options.require_hashes = True
|
||||||
|
|
||||||
|
if not (args or options.editables or options.requirements):
|
||||||
|
opts = {"name": self.name}
|
||||||
|
if options.find_links:
|
||||||
|
raise CommandError(
|
||||||
|
"You must give at least one requirement to {name} "
|
||||||
|
'(maybe you meant "pip {name} {links}"?)'.format(
|
||||||
|
**dict(opts, links=" ".join(options.find_links))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise CommandError(
|
||||||
|
"You must give at least one requirement to {name} "
|
||||||
|
'(see "pip help {name}")'.format(**opts)
|
||||||
|
)
|
||||||
|
|
||||||
|
return requirements
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def trace_basic_info(finder: PackageFinder) -> None:
|
||||||
|
"""
|
||||||
|
Trace basic information about the provided objects.
|
||||||
|
"""
|
||||||
|
# Display where finder is looking for packages
|
||||||
|
search_scope = finder.search_scope
|
||||||
|
locations = search_scope.get_formatted_locations()
|
||||||
|
if locations:
|
||||||
|
logger.info(locations)
|
||||||
|
|
||||||
|
def _build_package_finder(
|
||||||
|
self,
|
||||||
|
options: Values,
|
||||||
|
session: PipSession,
|
||||||
|
target_python: Optional[TargetPython] = None,
|
||||||
|
ignore_requires_python: Optional[bool] = None,
|
||||||
|
) -> PackageFinder:
|
||||||
|
"""
|
||||||
|
Create a package finder appropriate to this requirement command.
|
||||||
|
|
||||||
|
:param ignore_requires_python: Whether to ignore incompatible
|
||||||
|
"Requires-Python" values in links. Defaults to False.
|
||||||
|
"""
|
||||||
|
link_collector = LinkCollector.create(session, options=options)
|
||||||
|
selection_prefs = SelectionPreferences(
|
||||||
|
allow_yanked=True,
|
||||||
|
format_control=options.format_control,
|
||||||
|
allow_all_prereleases=options.pre,
|
||||||
|
prefer_binary=options.prefer_binary,
|
||||||
|
ignore_requires_python=ignore_requires_python,
|
||||||
|
)
|
||||||
|
|
||||||
|
return PackageFinder.create(
|
||||||
|
link_collector=link_collector,
|
||||||
|
selection_prefs=selection_prefs,
|
||||||
|
target_python=target_python,
|
||||||
|
)
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import contextlib
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import IO, Generator, Optional
|
||||||
|
|
||||||
|
from pip._internal.utils.compat import WINDOWS
|
||||||
|
from pip._internal.utils.logging import get_indentation
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SpinnerInterface:
|
||||||
|
def spin(self) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def finish(self, final_status: str) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveSpinner(SpinnerInterface):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
file: Optional[IO[str]] = None,
|
||||||
|
spin_chars: str = "-\\|/",
|
||||||
|
# Empirically, 8 updates/second looks nice
|
||||||
|
min_update_interval_seconds: float = 0.125,
|
||||||
|
):
|
||||||
|
self._message = message
|
||||||
|
if file is None:
|
||||||
|
file = sys.stdout
|
||||||
|
self._file = file
|
||||||
|
self._rate_limiter = RateLimiter(min_update_interval_seconds)
|
||||||
|
self._finished = False
|
||||||
|
|
||||||
|
self._spin_cycle = itertools.cycle(spin_chars)
|
||||||
|
|
||||||
|
self._file.write(" " * get_indentation() + self._message + " ... ")
|
||||||
|
self._width = 0
|
||||||
|
|
||||||
|
def _write(self, status: str) -> None:
|
||||||
|
assert not self._finished
|
||||||
|
# Erase what we wrote before by backspacing to the beginning, writing
|
||||||
|
# spaces to overwrite the old text, and then backspacing again
|
||||||
|
backup = "\b" * self._width
|
||||||
|
self._file.write(backup + " " * self._width + backup)
|
||||||
|
# Now we have a blank slate to add our status
|
||||||
|
self._file.write(status)
|
||||||
|
self._width = len(status)
|
||||||
|
self._file.flush()
|
||||||
|
self._rate_limiter.reset()
|
||||||
|
|
||||||
|
def spin(self) -> None:
|
||||||
|
if self._finished:
|
||||||
|
return
|
||||||
|
if not self._rate_limiter.ready():
|
||||||
|
return
|
||||||
|
self._write(next(self._spin_cycle))
|
||||||
|
|
||||||
|
def finish(self, final_status: str) -> None:
|
||||||
|
if self._finished:
|
||||||
|
return
|
||||||
|
self._write(final_status)
|
||||||
|
self._file.write("\n")
|
||||||
|
self._file.flush()
|
||||||
|
self._finished = True
|
||||||
|
|
||||||
|
|
||||||
|
# Used for dumb terminals, non-interactive installs (no tty), etc.
|
||||||
|
# We still print updates occasionally (once every 60 seconds by default) to
|
||||||
|
# act as a keep-alive for systems like Travis-CI that take lack-of-output as
|
||||||
|
# an indication that a task has frozen.
|
||||||
|
class NonInteractiveSpinner(SpinnerInterface):
|
||||||
|
def __init__(self, message: str, min_update_interval_seconds: float = 60.0) -> None:
|
||||||
|
self._message = message
|
||||||
|
self._finished = False
|
||||||
|
self._rate_limiter = RateLimiter(min_update_interval_seconds)
|
||||||
|
self._update("started")
|
||||||
|
|
||||||
|
def _update(self, status: str) -> None:
|
||||||
|
assert not self._finished
|
||||||
|
self._rate_limiter.reset()
|
||||||
|
logger.info("%s: %s", self._message, status)
|
||||||
|
|
||||||
|
def spin(self) -> None:
|
||||||
|
if self._finished:
|
||||||
|
return
|
||||||
|
if not self._rate_limiter.ready():
|
||||||
|
return
|
||||||
|
self._update("still running...")
|
||||||
|
|
||||||
|
def finish(self, final_status: str) -> None:
|
||||||
|
if self._finished:
|
||||||
|
return
|
||||||
|
self._update(f"finished with status '{final_status}'")
|
||||||
|
self._finished = True
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
def __init__(self, min_update_interval_seconds: float) -> None:
|
||||||
|
self._min_update_interval_seconds = min_update_interval_seconds
|
||||||
|
self._last_update: float = 0
|
||||||
|
|
||||||
|
def ready(self) -> bool:
|
||||||
|
now = time.time()
|
||||||
|
delta = now - self._last_update
|
||||||
|
return delta >= self._min_update_interval_seconds
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._last_update = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]:
|
||||||
|
# Interactive spinner goes directly to sys.stdout rather than being routed
|
||||||
|
# through the logging system, but it acts like it has level INFO,
|
||||||
|
# i.e. it's only displayed if we're at level INFO or better.
|
||||||
|
# Non-interactive spinner goes through the logging system, so it is always
|
||||||
|
# in sync with logging configuration.
|
||||||
|
if sys.stdout.isatty() and logger.getEffectiveLevel() <= logging.INFO:
|
||||||
|
spinner: SpinnerInterface = InteractiveSpinner(message)
|
||||||
|
else:
|
||||||
|
spinner = NonInteractiveSpinner(message)
|
||||||
|
try:
|
||||||
|
with hidden_cursor(sys.stdout):
|
||||||
|
yield spinner
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
spinner.finish("canceled")
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
spinner.finish("error")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
spinner.finish("done")
|
||||||
|
|
||||||
|
|
||||||
|
HIDE_CURSOR = "\x1b[?25l"
|
||||||
|
SHOW_CURSOR = "\x1b[?25h"
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def hidden_cursor(file: IO[str]) -> Generator[None, None, None]:
|
||||||
|
# The Windows terminal does not support the hide/show cursor ANSI codes,
|
||||||
|
# even via colorama. So don't even try.
|
||||||
|
if WINDOWS:
|
||||||
|
yield
|
||||||
|
# We don't want to clutter the output with control characters if we're
|
||||||
|
# writing to a file, or if the user is running with --quiet.
|
||||||
|
# See https://github.com/pypa/pip/issues/3418
|
||||||
|
elif not file.isatty() or logger.getEffectiveLevel() > logging.INFO:
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
file.write(HIDE_CURSOR)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
file.write(SHOW_CURSOR)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
SUCCESS = 0
|
||||||
|
ERROR = 1
|
||||||
|
UNKNOWN_ERROR = 2
|
||||||
|
VIRTUALENV_NOT_FOUND = 3
|
||||||
|
PREVIOUS_BUILD_DIR_ERROR = 4
|
||||||
|
NO_MATCHES_FOUND = 23
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
Package containing all pip commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from pip._internal.cli.base_command import Command
|
||||||
|
|
||||||
|
CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
|
||||||
|
|
||||||
|
# This dictionary does a bunch of heavy lifting for help output:
|
||||||
|
# - Enables avoiding additional (costly) imports for presenting `--help`.
|
||||||
|
# - The ordering matters for help display.
|
||||||
|
#
|
||||||
|
# Even though the module path starts with the same "pip._internal.commands"
|
||||||
|
# prefix, the full path makes testing easier (specifically when modifying
|
||||||
|
# `commands_dict` in test setup / teardown).
|
||||||
|
commands_dict: Dict[str, CommandInfo] = {
|
||||||
|
"install": CommandInfo(
|
||||||
|
"pip._internal.commands.install",
|
||||||
|
"InstallCommand",
|
||||||
|
"Install packages.",
|
||||||
|
),
|
||||||
|
"download": CommandInfo(
|
||||||
|
"pip._internal.commands.download",
|
||||||
|
"DownloadCommand",
|
||||||
|
"Download packages.",
|
||||||
|
),
|
||||||
|
"uninstall": CommandInfo(
|
||||||
|
"pip._internal.commands.uninstall",
|
||||||
|
"UninstallCommand",
|
||||||
|
"Uninstall packages.",
|
||||||
|
),
|
||||||
|
"freeze": CommandInfo(
|
||||||
|
"pip._internal.commands.freeze",
|
||||||
|
"FreezeCommand",
|
||||||
|
"Output installed packages in requirements format.",
|
||||||
|
),
|
||||||
|
"inspect": CommandInfo(
|
||||||
|
"pip._internal.commands.inspect",
|
||||||
|
"InspectCommand",
|
||||||
|
"Inspect the python environment.",
|
||||||
|
),
|
||||||
|
"list": CommandInfo(
|
||||||
|
"pip._internal.commands.list",
|
||||||
|
"ListCommand",
|
||||||
|
"List installed packages.",
|
||||||
|
),
|
||||||
|
"show": CommandInfo(
|
||||||
|
"pip._internal.commands.show",
|
||||||
|
"ShowCommand",
|
||||||
|
"Show information about installed packages.",
|
||||||
|
),
|
||||||
|
"check": CommandInfo(
|
||||||
|
"pip._internal.commands.check",
|
||||||
|
"CheckCommand",
|
||||||
|
"Verify installed packages have compatible dependencies.",
|
||||||
|
),
|
||||||
|
"config": CommandInfo(
|
||||||
|
"pip._internal.commands.configuration",
|
||||||
|
"ConfigurationCommand",
|
||||||
|
"Manage local and global configuration.",
|
||||||
|
),
|
||||||
|
"search": CommandInfo(
|
||||||
|
"pip._internal.commands.search",
|
||||||
|
"SearchCommand",
|
||||||
|
"Search PyPI for packages.",
|
||||||
|
),
|
||||||
|
"cache": CommandInfo(
|
||||||
|
"pip._internal.commands.cache",
|
||||||
|
"CacheCommand",
|
||||||
|
"Inspect and manage pip's wheel cache.",
|
||||||
|
),
|
||||||
|
"index": CommandInfo(
|
||||||
|
"pip._internal.commands.index",
|
||||||
|
"IndexCommand",
|
||||||
|
"Inspect information available from package indexes.",
|
||||||
|
),
|
||||||
|
"wheel": CommandInfo(
|
||||||
|
"pip._internal.commands.wheel",
|
||||||
|
"WheelCommand",
|
||||||
|
"Build wheels from your requirements.",
|
||||||
|
),
|
||||||
|
"hash": CommandInfo(
|
||||||
|
"pip._internal.commands.hash",
|
||||||
|
"HashCommand",
|
||||||
|
"Compute hashes of package archives.",
|
||||||
|
),
|
||||||
|
"completion": CommandInfo(
|
||||||
|
"pip._internal.commands.completion",
|
||||||
|
"CompletionCommand",
|
||||||
|
"A helper command used for command completion.",
|
||||||
|
),
|
||||||
|
"debug": CommandInfo(
|
||||||
|
"pip._internal.commands.debug",
|
||||||
|
"DebugCommand",
|
||||||
|
"Show information useful for debugging.",
|
||||||
|
),
|
||||||
|
"help": CommandInfo(
|
||||||
|
"pip._internal.commands.help",
|
||||||
|
"HelpCommand",
|
||||||
|
"Show help for commands.",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_command(name: str, **kwargs: Any) -> Command:
|
||||||
|
"""
|
||||||
|
Create an instance of the Command class with the given name.
|
||||||
|
"""
|
||||||
|
module_path, class_name, summary = commands_dict[name]
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
command_class = getattr(module, class_name)
|
||||||
|
command = command_class(name=name, summary=summary, **kwargs)
|
||||||
|
|
||||||
|
return command
|
||||||
|
|
||||||
|
|
||||||
|
def get_similar_commands(name: str) -> Optional[str]:
|
||||||
|
"""Command name auto-correct."""
|
||||||
|
from difflib import get_close_matches
|
||||||
|
|
||||||
|
name = name.lower()
|
||||||
|
|
||||||
|
close_commands = get_close_matches(name, commands_dict.keys())
|
||||||
|
|
||||||
|
if close_commands:
|
||||||
|
return close_commands[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
from optparse import Values
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
from pip._internal.cli.base_command import Command
|
||||||
|
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
||||||
|
from pip._internal.exceptions import CommandError, PipError
|
||||||
|
from pip._internal.utils import filesystem
|
||||||
|
from pip._internal.utils.logging import getLogger
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CacheCommand(Command):
|
||||||
|
"""
|
||||||
|
Inspect and manage pip's wheel cache.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
|
||||||
|
- dir: Show the cache directory.
|
||||||
|
- info: Show information about the cache.
|
||||||
|
- list: List filenames of packages stored in the cache.
|
||||||
|
- remove: Remove one or more package from the cache.
|
||||||
|
- purge: Remove all items from the cache.
|
||||||
|
|
||||||
|
``<pattern>`` can be a glob expression or a package name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ignore_require_venv = True
|
||||||
|
usage = """
|
||||||
|
%prog dir
|
||||||
|
%prog info
|
||||||
|
%prog list [<pattern>] [--format=[human, abspath]]
|
||||||
|
%prog remove <pattern>
|
||||||
|
%prog purge
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_options(self) -> None:
|
||||||
|
self.cmd_opts.add_option(
|
||||||
|
"--format",
|
||||||
|
action="store",
|
||||||
|
dest="list_format",
|
||||||
|
default="human",
|
||||||
|
choices=("human", "abspath"),
|
||||||
|
help="Select the output format among: human (default) or abspath",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.insert_option_group(0, self.cmd_opts)
|
||||||
|
|
||||||
|
def run(self, options: Values, args: List[str]) -> int:
|
||||||
|
handlers = {
|
||||||
|
"dir": self.get_cache_dir,
|
||||||
|
"info": self.get_cache_info,
|
||||||
|
"list": self.list_cache_items,
|
||||||
|
"remove": self.remove_cache_items,
|
||||||
|
"purge": self.purge_cache,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not options.cache_dir:
|
||||||
|
logger.error("pip cache commands can not function since cache is disabled.")
|
||||||
|
return ERROR
|
||||||
|
|
||||||
|
# Determine action
|
||||||
|
if not args or args[0] not in handlers:
|
||||||
|
logger.error(
|
||||||
|
"Need an action (%s) to perform.",
|
||||||
|
", ".join(sorted(handlers)),
|
||||||
|
)
|
||||||
|
return ERROR
|
||||||
|
|
||||||
|
action = args[0]
|
||||||
|
|
||||||
|
# Error handling happens here, not in the action-handlers.
|
||||||
|
try:
|
||||||
|
handlers[action](options, args[1:])
|
||||||
|
except PipError as e:
|
||||||
|
logger.error(e.args[0])
|
||||||
|
return ERROR
|
||||||
|
|
||||||
|
return SUCCESS
|
||||||
|
|
||||||
|
def get_cache_dir(self, options: Values, args: List[Any]) -> None:
|
||||||
|
if args:
|
||||||
|
raise CommandError("Too many arguments")
|
||||||
|
|
||||||
|
logger.info(options.cache_dir)
|
||||||
|
|
||||||
|
def get_cache_info(self, options: Values, args: List[Any]) -> None:
|
||||||
|
if args:
|
||||||
|
raise CommandError("Too many arguments")
|
||||||
|
|
||||||
|
num_http_files = len(self._find_http_files(options))
|
||||||
|
num_packages = len(self._find_wheels(options, "*"))
|
||||||
|
|
||||||
|
http_cache_location = self._cache_dir(options, "http-v2")
|
||||||
|
old_http_cache_location = self._cache_dir(options, "http")
|
||||||
|
wheels_cache_location = self._cache_dir(options, "wheels")
|
||||||
|
http_cache_size = filesystem.format_size(
|
||||||
|
filesystem.directory_size(http_cache_location)
|
||||||
|
+ filesystem.directory_size(old_http_cache_location)
|
||||||
|
)
|
||||||
|
wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
|
||||||
|
|
||||||
|
message = (
|
||||||
|
textwrap.dedent(
|
||||||
|
"""
|
||||||
|
Package index page cache location (pip v23.3+): {http_cache_location}
|
||||||
|
Package index page cache location (older pips): {old_http_cache_location}
|
||||||
|
Package index page cache size: {http_cache_size}
|
||||||
|
Number of HTTP files: {num_http_files}
|
||||||
|
Locally built wheels location: {wheels_cache_location}
|
||||||
|
Locally built wheels size: {wheels_cache_size}
|
||||||
|
Number of locally built wheels: {package_count}
|
||||||
|
""" # noqa: E501
|
||||||
|
)
|
||||||
|
.format(
|
||||||
|
http_cache_location=http_cache_location,
|
||||||
|
old_http_cache_location=old_http_cache_location,
|
||||||
|
http_cache_size=http_cache_size,
|
||||||
|
num_http_files=num_http_files,
|
||||||
|
wheels_cache_location=wheels_cache_location,
|
||||||
|
package_count=num_packages,
|
||||||
|
wheels_cache_size=wheels_cache_size,
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
def list_cache_items(self, options: Values, args: List[Any]) -> None:
|
||||||
|
if len(args) > 1:
|
||||||
|
raise CommandError("Too many arguments")
|
||||||
|
|
||||||
|
if args:
|
||||||
|
pattern = args[0]
|
||||||
|
else:
|
||||||
|
pattern = "*"
|
||||||
|
|
||||||
|
files = self._find_wheels(options, pattern)
|
||||||
|
if options.list_format == "human":
|
||||||
|
self.format_for_human(files)
|
||||||
|
else:
|
||||||
|
self.format_for_abspath(files)
|
||||||
|
|
||||||
|
def format_for_human(self, files: List[str]) -> None:
|
||||||
|
if not files:
|
||||||
|
logger.info("No locally built wheels cached.")
|
||||||
|
return
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for filename in files:
|
||||||
|
wheel = os.path.basename(filename)
|
||||||
|
size = filesystem.format_file_size(filename)
|
||||||
|
results.append(f" - {wheel} ({size})")
|
||||||
|
logger.info("Cache contents:\n")
|
||||||
|
logger.info("\n".join(sorted(results)))
|
||||||
|
|
||||||
|
def format_for_abspath(self, files: List[str]) -> None:
|
||||||
|
if files:
|
||||||
|
logger.info("\n".join(sorted(files)))
|
||||||
|
|
||||||
|
def remove_cache_items(self, options: Values, args: List[Any]) -> None:
|
||||||
|
if len(args) > 1:
|
||||||
|
raise CommandError("Too many arguments")
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
raise CommandError("Please provide a pattern")
|
||||||
|
|
||||||
|
files = self._find_wheels(options, args[0])
|
||||||
|
|
||||||
|
no_matching_msg = "No matching packages"
|
||||||
|
if args[0] == "*":
|
||||||
|
# Only fetch http files if no specific pattern given
|
||||||
|
files += self._find_http_files(options)
|
||||||
|
else:
|
||||||
|
# Add the pattern to the log message
|
||||||
|
no_matching_msg += f' for pattern "{args[0]}"'
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
logger.warning(no_matching_msg)
|
||||||
|
|
||||||
|
for filename in files:
|
||||||
|
os.unlink(filename)
|
||||||
|
logger.verbose("Removed %s", filename)
|
||||||
|
logger.info("Files removed: %s", len(files))
|
||||||
|
|
||||||
|
def purge_cache(self, options: Values, args: List[Any]) -> None:
|
||||||
|
if args:
|
||||||
|
raise CommandError("Too many arguments")
|
||||||
|
|
||||||
|
return self.remove_cache_items(options, ["*"])
|
||||||
|
|
||||||
|
def _cache_dir(self, options: Values, subdir: str) -> str:
|
||||||
|
return os.path.join(options.cache_dir, subdir)
|
||||||
|
|
||||||
|
def _find_http_files(self, options: Values) -> List[str]:
|
||||||
|
old_http_dir = self._cache_dir(options, "http")
|
||||||
|
new_http_dir = self._cache_dir(options, "http-v2")
|
||||||
|
return filesystem.find_files(old_http_dir, "*") + filesystem.find_files(
|
||||||
|
new_http_dir, "*"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_wheels(self, options: Values, pattern: str) -> List[str]:
|
||||||
|
wheel_dir = self._cache_dir(options, "wheels")
|
||||||
|
|
||||||
|
# The wheel filename format, as specified in PEP 427, is:
|
||||||
|
# {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
|
||||||
|
#
|
||||||
|
# Additionally, non-alphanumeric values in the distribution are
|
||||||
|
# normalized to underscores (_), meaning hyphens can never occur
|
||||||
|
# before `-{version}`.
|
||||||
|
#
|
||||||
|
# Given that information:
|
||||||
|
# - If the pattern we're given contains a hyphen (-), the user is
|
||||||
|
# providing at least the version. Thus, we can just append `*.whl`
|
||||||
|
# to match the rest of it.
|
||||||
|
# - If the pattern we're given doesn't contain a hyphen (-), the
|
||||||
|
# user is only providing the name. Thus, we append `-*.whl` to
|
||||||
|
# match the hyphen before the version, followed by anything else.
|
||||||
|
#
|
||||||
|
# PEP 427: https://www.python.org/dev/peps/pep-0427/
|
||||||
|
pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl")
|
||||||
|
|
||||||
|
return filesystem.find_files(wheel_dir, pattern)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import logging
|
||||||
|
from optparse import Values
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pip._internal.cli.base_command import Command
|
||||||
|
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
||||||
|
from pip._internal.operations.check import (
|
||||||
|
check_package_set,
|
||||||
|
create_package_set_from_installed,
|
||||||
|
warn_legacy_versions_and_specifiers,
|
||||||
|
)
|
||||||
|
from pip._internal.utils.misc import write_output
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckCommand(Command):
|
||||||
|
"""Verify installed packages have compatible dependencies."""
|
||||||
|
|
||||||
|
usage = """
|
||||||
|
%prog [options]"""
|
||||||
|
|
||||||
|
def run(self, options: Values, args: List[str]) -> int:
|
||||||
|
package_set, parsing_probs = create_package_set_from_installed()
|
||||||
|
warn_legacy_versions_and_specifiers(package_set)
|
||||||
|
missing, conflicting = check_package_set(package_set)
|
||||||
|
|
||||||
|
for project_name in missing:
|
||||||
|
version = package_set[project_name].version
|
||||||
|
for dependency in missing[project_name]:
|
||||||
|
write_output(
|
||||||
|
"%s %s requires %s, which is not installed.",
|
||||||
|
project_name,
|
||||||
|
version,
|
||||||
|
dependency[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
for project_name in conflicting:
|
||||||
|
version = package_set[project_name].version
|
||||||
|
for dep_name, dep_version, req in conflicting[project_name]:
|
||||||
|
write_output(
|
||||||
|
"%s %s has requirement %s, but you have %s %s.",
|
||||||
|
project_name,
|
||||||
|
version,
|
||||||
|
req,
|
||||||
|
dep_name,
|
||||||
|
dep_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing or conflicting or parsing_probs:
|
||||||
|
return ERROR
|
||||||
|
else:
|
||||||
|
write_output("No broken requirements found.")
|
||||||
|
return SUCCESS
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from optparse import Values
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pip._internal.cli.base_command import Command
|
||||||
|
from pip._internal.cli.status_codes import SUCCESS
|
||||||
|
from pip._internal.utils.misc import get_prog
|
||||||
|
|
||||||
|
BASE_COMPLETION = """
|
||||||
|
# pip {shell} completion start{script}# pip {shell} completion end
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPLETION_SCRIPTS = {
|
||||||
|
"bash": """
|
||||||
|
_pip_completion()
|
||||||
|
{{
|
||||||
|
COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\
|
||||||
|
COMP_CWORD=$COMP_CWORD \\
|
||||||
|
PIP_AUTO_COMPLETE=1 $1 2>/dev/null ) )
|
||||||
|
}}
|
||||||
|
complete -o default -F _pip_completion {prog}
|
||||||
|
""",
|
||||||
|
"zsh": """
|
||||||
|
#compdef -P pip[0-9.]#
|
||||||
|
__pip() {{
|
||||||
|
compadd $( COMP_WORDS="$words[*]" \\
|
||||||
|
COMP_CWORD=$((CURRENT-1)) \\
|
||||||
|
PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null )
|
||||||
|
}}
|
||||||
|
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
|
||||||
|
# autoload from fpath, call function directly
|
||||||
|
__pip "$@"
|
||||||
|
else
|
||||||
|
# eval/source/. command, register function for later
|
||||||
|
compdef __pip -P 'pip[0-9.]#'
|
||||||
|
fi
|
||||||
|
""",
|
||||||
|
"fish": """
|
||||||
|
function __fish_complete_pip
|
||||||
|
set -lx COMP_WORDS (commandline -o) ""
|
||||||
|
set -lx COMP_CWORD ( \\
|
||||||
|
math (contains -i -- (commandline -t) $COMP_WORDS)-1 \\
|
||||||
|
)
|
||||||
|
set -lx PIP_AUTO_COMPLETE 1
|
||||||
|
string split \\ -- (eval $COMP_WORDS[1])
|
||||||
|
end
|
||||||
|
complete -fa "(__fish_complete_pip)" -c {prog}
|
||||||
|
""",
|
||||||
|
"powershell": """
|
||||||
|
if ((Test-Path Function:\\TabExpansion) -and -not `
|
||||||
|
(Test-Path Function:\\_pip_completeBackup)) {{
|
||||||
|
Rename-Item Function:\\TabExpansion _pip_completeBackup
|
||||||
|
}}
|
||||||
|
function TabExpansion($line, $lastWord) {{
|
||||||
|
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
|
||||||
|
if ($lastBlock.StartsWith("{prog} ")) {{
|
||||||
|
$Env:COMP_WORDS=$lastBlock
|
||||||
|
$Env:COMP_CWORD=$lastBlock.Split().Length - 1
|
||||||
|
$Env:PIP_AUTO_COMPLETE=1
|
||||||
|
(& {prog}).Split()
|
||||||
|
Remove-Item Env:COMP_WORDS
|
||||||
|
Remove-Item Env:COMP_CWORD
|
||||||
|
Remove-Item Env:PIP_AUTO_COMPLETE
|
||||||
|
}}
|
||||||
|
elseif (Test-Path Function:\\_pip_completeBackup) {{
|
||||||
|
# Fall back on existing tab expansion
|
||||||
|
_pip_completeBackup $line $lastWord
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionCommand(Command):
|
||||||
|
"""A helper command to be used for command completion."""
|
||||||
|
|
||||||
|
ignore_require_venv = True
|
||||||
|
|
||||||
|
def add_options(self) -> None:
|
||||||
|
self.cmd_opts.add_option(
|
||||||
|
"--bash",
|
||||||
|
"-b",
|
||||||
|
action="store_const",
|
||||||
|
const="bash",
|
||||||
|
dest="shell",
|
||||||
|
help="Emit completion code for bash",
|
||||||
|
)
|
||||||
|
self.cmd_opts.add_option(
|
||||||
|
"--zsh",
|
||||||
|
"-z",
|
||||||
|
action="store_const",
|
||||||
|
const="zsh",
|
||||||
|
dest="shell",
|
||||||
|
help="Emit completion code for zsh",
|
||||||
|
)
|
||||||
|
self.cmd_opts.add_option(
|
||||||
|
"--fish",
|
||||||
|
"-f",
|
||||||
|
action="store_const",
|
||||||
|
const="fish",
|
||||||
|
dest="shell",
|
||||||
|
help="Emit completion code for fish",
|
||||||
|
)
|
||||||
|
self.cmd_opts.add_option(
|
||||||
|
"--powershell",
|
||||||
|
"-p",
|
||||||
|
action="store_const",
|
||||||
|
const="powershell",
|
||||||
|
dest="shell",
|
||||||
|
help="Emit completion code for powershell",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.insert_option_group(0, self.cmd_opts)
|
||||||
|
|
||||||
|
def run(self, options: Values, args: List[str]) -> int:
|
||||||
|
"""Prints the completion code of the given shell"""
|
||||||
|
shells = COMPLETION_SCRIPTS.keys()
|
||||||
|
shell_options = ["--" + shell for shell in sorted(shells)]
|
||||||
|
if options.shell in shells:
|
||||||
|
script = textwrap.dedent(
|
||||||
|
COMPLETION_SCRIPTS.get(options.shell, "").format(prog=get_prog())
|
||||||
|
)
|
||||||
|
print(BASE_COMPLETION.format(script=script, shell=options.shell))
|
||||||
|
return SUCCESS
|
||||||
|
else:
|
||||||
|
sys.stderr.write(
|
||||||
|
"ERROR: You must pass {}\n".format(" or ".join(shell_options))
|
||||||
|
)
|
||||||
|
return SUCCESS
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user