build: Initialize Python virtual environment and install project dependencies.
Some checks failed
WLED CI / wled_build (push) Has been cancelled
Some checks failed
WLED CI / wled_build (push) Has been cancelled
This commit is contained in:
247
.venv/bin/Activate.ps1
Normal file
247
.venv/bin/Activate.ps1
Normal file
@@ -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"
|
||||
69
.venv/bin/activate
Normal file
69
.venv/bin/activate
Normal file
@@ -0,0 +1,69 @@
|
||||
# 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
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
VIRTUAL_ENV=/home/wartana/myApp/WLED/.venv
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_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
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
26
.venv/bin/activate.csh
Normal file
26
.venv/bin/activate.csh
Normal file
@@ -0,0 +1,26 @@
|
||||
# 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/wartana/myApp/WLED/.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
|
||||
69
.venv/bin/activate.fish
Normal file
69
.venv/bin/activate.fish
Normal file
@@ -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/wartana/myApp/WLED/.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
|
||||
8
.venv/bin/async-json-rpc-server
Executable file
8
.venv/bin/async-json-rpc-server
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from ajsonrpc.scripts.server import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
.venv/bin/bottle
Executable file
8
.venv/bin/bottle
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from bottle import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
4681
.venv/bin/bottle.py
Executable file
4681
.venv/bin/bottle.py
Executable file
File diff suppressed because it is too large
Load Diff
8
.venv/bin/normalizer
Executable file
8
.venv/bin/normalizer
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from charset_normalizer.cli import cli_detect
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cli_detect())
|
||||
8
.venv/bin/pio
Executable file
8
.venv/bin/pio
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from platformio.__main__ import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
.venv/bin/piodebuggdb
Executable file
8
.venv/bin/piodebuggdb
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from platformio.__main__ import debug_gdb_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(debug_gdb_main())
|
||||
8
.venv/bin/pip
Executable file
8
.venv/bin/pip
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.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())
|
||||
8
.venv/bin/pip3
Executable file
8
.venv/bin/pip3
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.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())
|
||||
8
.venv/bin/pip3.11
Executable file
8
.venv/bin/pip3.11
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.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())
|
||||
8
.venv/bin/platformio
Executable file
8
.venv/bin/platformio
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from platformio.__main__ import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
.venv/bin/pyserial-miniterm
Executable file
8
.venv/bin/pyserial-miniterm
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from serial.tools.miniterm import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
.venv/bin/pyserial-ports
Executable file
8
.venv/bin/pyserial-ports
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from serial.tools.list_ports import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
1
.venv/bin/python
Symbolic link
1
.venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
1
.venv/bin/python3
Symbolic link
1
.venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
.venv/bin/python3.11
Symbolic link
1
.venv/bin/python3.11
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
1996
.venv/bin/readelf.py
Executable file
1996
.venv/bin/readelf.py
Executable file
File diff suppressed because it is too large
Load Diff
8
.venv/bin/tabulate
Executable file
8
.venv/bin/tabulate
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from tabulate import _main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(_main())
|
||||
8
.venv/bin/uvicorn
Executable file
8
.venv/bin/uvicorn
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from uvicorn.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
.venv/bin/wheel
Executable file
8
.venv/bin/wheel
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/wartana/myApp/WLED/.venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from wheel._commands import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
222
.venv/lib/python3.11/site-packages/_distutils_hack/__init__.py
Normal file
222
.venv/lib/python3.11/site-packages/_distutils_hack/__init__.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# don't import any costly modules
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
is_pypy = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
if is_pypy and sys.version_info < (3, 7):
|
||||
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
|
||||
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils."
|
||||
)
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn("Setuptools is replacing distutils.")
|
||||
mods = [
|
||||
name
|
||||
for name in sys.modules
|
||||
if name == "distutils" or name.startswith("distutils.")
|
||||
]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
import importlib
|
||||
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
with shim():
|
||||
importlib.import_module('distutils')
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
assert 'setuptools._distutils.log' not in sys.modules
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class _TrivialRe:
|
||||
def __init__(self, *patterns):
|
||||
self._patterns = patterns
|
||||
|
||||
def match(self, string):
|
||||
return all(pat in string for pat in self._patterns)
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# optimization: only consider top level modules and those
|
||||
# found in the CPython test suite.
|
||||
if path is not None and not fullname.startswith('test.'):
|
||||
return
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
if self.is_cpython():
|
||||
return
|
||||
|
||||
import importlib
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
mod = importlib.import_module('setuptools._distutils')
|
||||
except Exception:
|
||||
# There are a couple of cases where setuptools._distutils
|
||||
# may not be present:
|
||||
# - An older Setuptools without a local distutils is
|
||||
# taking precedence. Ref #2957.
|
||||
# - Path manipulation during sitecustomize removes
|
||||
# setuptools from the path but only after the hook
|
||||
# has been loaded. Ref #2980.
|
||||
# In either case, fall back to stdlib behavior.
|
||||
return
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mod.__name__ = 'distutils'
|
||||
return mod
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader(
|
||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_cpython():
|
||||
"""
|
||||
Suppress supplying distutils for CPython (build and tests).
|
||||
Ref #2965 and #3007.
|
||||
"""
|
||||
return os.path.isfile('pybuilddir.txt')
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@classmethod
|
||||
def pip_imported_during_build(cls):
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
return any(
|
||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def frame_file_is_setup(frame):
|
||||
"""
|
||||
Return True if the indicated frame suggests a setup.py file.
|
||||
"""
|
||||
# some frames may not have __file__ (#2940)
|
||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||
|
||||
def spec_for_sensitive_tests(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running select tests under CPython.
|
||||
|
||||
python/cpython#91169
|
||||
"""
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
sensitive_tests = (
|
||||
[
|
||||
'test.test_distutils',
|
||||
'test.test_peg_generator',
|
||||
'test.test_importlib',
|
||||
]
|
||||
if sys.version_info < (3, 10)
|
||||
else [
|
||||
'test.test_distutils',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
for name in DistutilsMetaFinder.sensitive_tests:
|
||||
setattr(
|
||||
DistutilsMetaFinder,
|
||||
f'spec_for_{name}',
|
||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||
|
||||
|
||||
class shim:
|
||||
def __enter__(self):
|
||||
insert_shim()
|
||||
|
||||
def __exit__(self, exc, value, tb):
|
||||
remove_shim()
|
||||
|
||||
|
||||
def insert_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
33
.venv/lib/python3.11/site-packages/_yaml/__init__.py
Normal file
33
.venv/lib/python3.11/site-packages/_yaml/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# This is a stub package designed to roughly emulate the _yaml
|
||||
# extension module, which previously existed as a standalone module
|
||||
# and has been moved into the `yaml` package namespace.
|
||||
# It does not perfectly mimic its old counterpart, but should get
|
||||
# close enough for anyone who's relying on it even when they shouldn't.
|
||||
import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
raise exc("No module named '_yaml'")
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Kirill Pavlov <k@p99.io>
|
||||
|
||||
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,163 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: ajsonrpc
|
||||
Version: 1.2.0
|
||||
Summary: Async JSON-RPC 2.0 protocol + server powered by asyncio
|
||||
Home-page: https://github.com/pavlov99/ajsonrpc
|
||||
Author: Kirill Pavlov
|
||||
Author-email: k@p99.io
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://ajsonrpc.readthedocs.io
|
||||
Project-URL: Code, https://github.com/pavlov99/ajsonrpc
|
||||
Project-URL: Issue tracker, https://github.com/pavlov99/ajsonrpc/issues
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Framework :: AsyncIO
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.5
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# Async JSON-RPC 2.0 protocol + asyncio server
|
||||
|
||||
[](https://github.com/pavlov99/ajsonrpc/actions?query=workflow%3A%22Python+package%22)
|
||||
[](https://github.com/pavlov99/ajsonrpc/actions?query=workflow%3ACodeQL)
|
||||
[](https://pypi.org/project/ajsonrpc/)
|
||||
|
||||
Lightweight JSON-RPC 2.0 protocol implementation and asynchronous server powered by asyncio. This library is a successor of json-rpc and written by the same team.
|
||||
|
||||
Features:
|
||||
* Full JSON-RPC 2.0 Implementation.
|
||||
* Async request manager that handles the protocol.
|
||||
* Vanilla Python, no dependencies.
|
||||
* API server setup in 1 min.
|
||||
* Same development team as `json-rpc`, largely compatible code.
|
||||
|
||||
## Installing
|
||||
```
|
||||
$ pip install ajsonrpc
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
This package contains core JSON-RPC 2.0 primitives (request, response, etc.) and convenient backend-independent abstractions on top of them: dispatcher and request manager. These modules mirror implementation in the original json-rpc package with minor changes and improvements. Below is a summary of each module.
|
||||
|
||||
#### Core Module
|
||||
Consists of JSON-RPC 2.0 primitives: request, batch request, response, batch response, error. It also defines base classes for custom errors and exceptions.
|
||||
|
||||
Development principles:
|
||||
* If python object is created or modified without exceptions, it contains valid data.
|
||||
* Private state `<object>._body` contains the single source of truth. It is accessible and modifiable via getters (properties) and setters that ensure validation.
|
||||
* `body` is always a dictionary with primitive keys and values (the only exception is `response.result` that could hold any value defined by the application).
|
||||
* Constructor, getters and setters operate with JSON-RPC defined types, e.g. `response.error` always has `JSONRPC20Error` type. Most of other types are strings and numbers.
|
||||
|
||||
Unlike json-rpc package, core module does not deal with serialization/de-serialization, this logic was moved to manager.
|
||||
|
||||
#### Dispatcher
|
||||
Dispatcher is a dict-like object that maps method names to executables. One can think of it as an inproved dictionary, in fact it is inherited from `MutableMapping`. Some of the ways to add methods to dispatcher:
|
||||
|
||||
```python
|
||||
# init
|
||||
d = Dispatcher({"sum": lambda a, b: a + b})
|
||||
|
||||
# set item
|
||||
d["max"] = lambda a, b: max(a, b)
|
||||
|
||||
# function decorator
|
||||
@d.add_function
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
# Add class or object
|
||||
class Math:
|
||||
def sum(self, a, b):
|
||||
return a + b
|
||||
|
||||
def diff(self, a, b):
|
||||
return a - b
|
||||
|
||||
d.add_class(Math)
|
||||
d.add_object(Math())
|
||||
d.add_dict({"min": lambda a, b: min(a, b)})
|
||||
|
||||
# rename function
|
||||
d.add_function(add, name="my_add")
|
||||
|
||||
# prefix methos
|
||||
d.add_class(Math, prefix="get_")
|
||||
```
|
||||
|
||||
#### Manager
|
||||
Manager generates a response for a request. It handles common routines: request parsing, exception handling and error generation, parallel request execution for batch requests, serialization/de-serialization. Manager is asynchronous and dackend agnostic, it exposes following common methods:
|
||||
|
||||
```python
|
||||
# Get a response object for a single request. Used by other methods.
|
||||
async def get_response_for_request(
|
||||
self, request: JSONRPC20Request
|
||||
) -> Optional[JSONRPC20Response]
|
||||
|
||||
# Get (batch) response for a string payload. Handles de-serialization and parse errors.
|
||||
async def get_response_for_payload(
|
||||
self, payload: str
|
||||
) -> Optional[Union[JSONRPC20Response, JSONRPC20BatchResponse]]
|
||||
|
||||
# Most high-level method, returns string json for a string payload.
|
||||
async def get_payload_for_payload(self, payload: str) -> str
|
||||
```
|
||||
|
||||
#### Vanilla Server (Demo)
|
||||
This package comes with an asyncio [Protocol-based](https://docs.python.org/3/library/asyncio-protocol.html) minimalistic server script `async-json-rpc-server`. One could think of it as a bottle-py of API servers.
|
||||
|
||||
This was an experiment turned prototype: unlike json-rpc that requires some "shell" like Django or Flask to work, this package relies on asyncio and therefore could build on top of its [TCP server](https://docs.python.org/3/library/asyncio-protocol.html#tcp-echo-server). Indeed, JSON-RPC 2.0 is intentionally simple: server does not require views, has only one endpoint (routing is not required), only deals with json. Hence, vanilla code would be not only sufficient but likely faster than any framework.
|
||||
|
||||
This idea of self-sufficient server was extended further: what would be the minimum interface that allows to plug application code? What if zero integration is required? Likely, this was possible with runtime method introspection: `async-json-rpc-server` parses given file with methods and exposes all of them. Let's consider an example:
|
||||
|
||||
```python
|
||||
# examples/methods.py
|
||||
import asyncio
|
||||
|
||||
def echo(s='pong'):
|
||||
return s
|
||||
|
||||
def mul2(a, b):
|
||||
return a * b
|
||||
|
||||
async def say_after(delay, what):
|
||||
await asyncio.sleep(delay)
|
||||
return what
|
||||
```
|
||||
|
||||
To launch a server based on above methods, simply run:
|
||||
|
||||
```
|
||||
$ async-json-rpc-server examples/methods.py --port=8888
|
||||
```
|
||||
(Ctrl+C stops the server).
|
||||
|
||||
Single request example:
|
||||
```
|
||||
$ curl -H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc": "2.0", "method": "echo", "id": 0}' \
|
||||
http://127.0.0.1:8888
|
||||
|
||||
{"jsonrpc": "2.0", "id": 0, "result": "pong"}
|
||||
```
|
||||
|
||||
Batch request example:
|
||||

|
||||
|
||||
#### Backends
|
||||
Backend support is a syntactic sugar that wraps dispatcher and manager under one api class and provides convenient boilerplate, such as handler generation. Currently supported frameworks:
|
||||
* Tornado
|
||||
* Sanic
|
||||
* Quart
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
../../../bin/async-json-rpc-server,sha256=aueST28q1CHzFFr2qAQFUh8FNpmqkVXbM-XjZmkIz_I,248
|
||||
ajsonrpc-1.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
ajsonrpc-1.2.0.dist-info/LICENSE.txt,sha256=MuaJyO94t2dskQe7cepWSoxDQHVCCkDidxWa023h4JY,1091
|
||||
ajsonrpc-1.2.0.dist-info/METADATA,sha256=InynyYtpF6YHYg3wHu3dvumt6WcNIEKDU4Lq1JrJnTs,6923
|
||||
ajsonrpc-1.2.0.dist-info/RECORD,,
|
||||
ajsonrpc-1.2.0.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92
|
||||
ajsonrpc-1.2.0.dist-info/entry_points.txt,sha256=9bYFirevIbWWbjUBkNhMn7DgmPfVEv6JzmKlbNo8iGs,72
|
||||
ajsonrpc-1.2.0.dist-info/top_level.txt,sha256=Q4qXeQSl9d5m-GJuRxO0cGS12q9umn9uLcM1lcDM15k,9
|
||||
ajsonrpc/__init__.py,sha256=N_KB00-L04arTx7TMB25rgqun4RFCFkXj0sqO3Jn6aU,175
|
||||
ajsonrpc/__pycache__/__init__.cpython-311.pyc,,
|
||||
ajsonrpc/__pycache__/core.cpython-311.pyc,,
|
||||
ajsonrpc/__pycache__/dispatcher.cpython-311.pyc,,
|
||||
ajsonrpc/__pycache__/manager.cpython-311.pyc,,
|
||||
ajsonrpc/__pycache__/utils.cpython-311.pyc,,
|
||||
ajsonrpc/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
ajsonrpc/backend/__pycache__/__init__.cpython-311.pyc,,
|
||||
ajsonrpc/backend/__pycache__/common.cpython-311.pyc,,
|
||||
ajsonrpc/backend/__pycache__/quart.cpython-311.pyc,,
|
||||
ajsonrpc/backend/__pycache__/sanic.cpython-311.pyc,,
|
||||
ajsonrpc/backend/__pycache__/tornado.cpython-311.pyc,,
|
||||
ajsonrpc/backend/common.py,sha256=Xvm0EhXlrCr6iTg685_IfI8G5uClesV8CY31geaZIlo,686
|
||||
ajsonrpc/backend/quart.py,sha256=GI_uLZrDPSPjjKYjCD9SiQYbseyMqpvClRL3DXXmwNE,452
|
||||
ajsonrpc/backend/sanic.py,sha256=2DEqhI8KwbRsOc7mfOSaLoY0um02CCKcWFxlc-5MKbY,409
|
||||
ajsonrpc/backend/tornado.py,sha256=owbRiQLrd2xGNjnDxQ2OiOROzYs4ppdeQJS8ZcBT1Ak,528
|
||||
ajsonrpc/core.py,sha256=MZou79fakZucUNxjt3syalybPuYUZwbaeINtwsgf-V0,19868
|
||||
ajsonrpc/dispatcher.py,sha256=AVL4oqqiaZisZqhJ-9j8lSgaa3vp9mkN1aV6P-TRGuw,4995
|
||||
ajsonrpc/manager.py,sha256=dBZf3M9b5PMChmo4JpAPpnr1uBZYJuWnsLBA5r1DBrw,4737
|
||||
ajsonrpc/scripts/__pycache__/server.cpython-311.pyc,,
|
||||
ajsonrpc/scripts/server.py,sha256=oHCZ-aPMaGXwBCVWhsbh_Jrct9NGYqOgoLO5SgzqUP0,3346
|
||||
ajsonrpc/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
ajsonrpc/tests/__pycache__/__init__.cpython-311.pyc,,
|
||||
ajsonrpc/tests/__pycache__/test_core.cpython-311.pyc,,
|
||||
ajsonrpc/tests/__pycache__/test_dispatcher.cpython-311.pyc,,
|
||||
ajsonrpc/tests/__pycache__/test_manager.cpython-311.pyc,,
|
||||
ajsonrpc/tests/test_core.py,sha256=FrTlFNbvwzIfKnqH-xj9iTziEIK0K3wFU7wr7MSLUYQ,13556
|
||||
ajsonrpc/tests/test_dispatcher.py,sha256=s-a10uHgHvL5VkTG4zQV20Vq92t2AjLey-3b51bTEQw,3381
|
||||
ajsonrpc/tests/test_manager.py,sha256=zU-TqSLjYnGEwGd5DOfJbNFMUk5k3iTXOumNKrVmFEQ,11117
|
||||
ajsonrpc/utils.py,sha256=nc3ImNF9BnNewJz0Dmy_g__Q5IEqd65gAQSJ5gOkCwY,1129
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.36.2)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[console_scripts]
|
||||
async-json-rpc-server = ajsonrpc.scripts.server:main
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ajsonrpc
|
||||
5
.venv/lib/python3.11/site-packages/ajsonrpc/__init__.py
Normal file
5
.venv/lib/python3.11/site-packages/ajsonrpc/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .dispatcher import Dispatcher
|
||||
from .manager import AsyncJSONRPCResponseManager
|
||||
|
||||
__version__ = "0.0.0" # replaced with release tag in GitHub action
|
||||
__version__ = "1.2.0"
|
||||
@@ -0,0 +1,20 @@
|
||||
import json
|
||||
from ..dispatcher import Dispatcher
|
||||
from ..manager import AsyncJSONRPCResponseManager
|
||||
|
||||
class CommonBackend:
|
||||
def __init__(self, serialize=json.dumps, deserialize=json.loads):
|
||||
self.manager = AsyncJSONRPCResponseManager(
|
||||
Dispatcher(),
|
||||
serialize=serialize,
|
||||
deserialize=deserialize
|
||||
)
|
||||
|
||||
def add_class(self, *args, **kwargs):
|
||||
return self.manager.dispatcher.add_class(*args, **kwargs)
|
||||
|
||||
def add_object(self, *args, **kwargs):
|
||||
return self.manager.dispatcher.add_object(*args, **kwargs)
|
||||
|
||||
def add_function(self, *args, **kwargs):
|
||||
return self.manager.dispatcher.add_function(*args, **kwargs)
|
||||
17
.venv/lib/python3.11/site-packages/ajsonrpc/backend/quart.py
Normal file
17
.venv/lib/python3.11/site-packages/ajsonrpc/backend/quart.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import json
|
||||
|
||||
from quart import Response, request
|
||||
from .common import CommonBackend
|
||||
|
||||
|
||||
class JSONRPCQuart(CommonBackend):
|
||||
@property
|
||||
def handler(self):
|
||||
"""Get Quart Handler"""
|
||||
|
||||
async def handle():
|
||||
request_body = await request.body
|
||||
response = await self.manager.get_response_for_payload(request_body)
|
||||
return Response(json.dumps(response.body), mimetype="application/json")
|
||||
|
||||
return handle
|
||||
12
.venv/lib/python3.11/site-packages/ajsonrpc/backend/sanic.py
Normal file
12
.venv/lib/python3.11/site-packages/ajsonrpc/backend/sanic.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from sanic.response import json as json_response
|
||||
from .common import CommonBackend
|
||||
|
||||
class JSONRPCSanic(CommonBackend):
|
||||
@property
|
||||
def handler(self):
|
||||
"""Get Sanic Handler"""
|
||||
async def handle(request):
|
||||
response = await self.manager.get_response_for_payload(request.body)
|
||||
return json_response(response.body, dumps=self.manager.serialize)
|
||||
|
||||
return handle
|
||||
@@ -0,0 +1,18 @@
|
||||
import tornado.web
|
||||
from .common import CommonBackend
|
||||
|
||||
|
||||
class JSONRPCTornado(CommonBackend):
|
||||
@property
|
||||
def handler(self):
|
||||
"""Get Tornado Handler"""
|
||||
manager = self.manager
|
||||
|
||||
class JSONRPCTornadoHandler(tornado.web.RequestHandler):
|
||||
async def post(self):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
payload = await manager.get_payload_for_payload(self.request.body)
|
||||
self.write(payload)
|
||||
|
||||
return JSONRPCTornadoHandler
|
||||
|
||||
609
.venv/lib/python3.11/site-packages/ajsonrpc/core.py
Normal file
609
.venv/lib/python3.11/site-packages/ajsonrpc/core.py
Normal file
@@ -0,0 +1,609 @@
|
||||
from typing import Union, Optional, Any, Iterable, Mapping, List, Dict
|
||||
from numbers import Number
|
||||
import warnings
|
||||
import collections.abc
|
||||
|
||||
|
||||
class JSONRPC20RequestIdWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class JSONRPC20ResponseIdWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class JSONRPC20Request:
|
||||
"""JSON-RPC 2.0 Request object.
|
||||
|
||||
A rpc call is represented by sending a Request object to a Server.
|
||||
The Request object has the following members:
|
||||
|
||||
jsonrpc
|
||||
A String specifying the version of the JSON-RPC protocol. MUST be
|
||||
exactly "2.0".
|
||||
method
|
||||
A String containing the name of the method to be invoked. Method names
|
||||
that begin with the word rpc followed by a period character (U+002E or
|
||||
ASCII 46) are reserved for rpc-internal methods and extensions and MUST
|
||||
NOT be used for anything else.
|
||||
params
|
||||
A Structured value that holds the parameter values to be used during
|
||||
the invocation of the method. This member MAY be omitted.
|
||||
id
|
||||
An identifier established by the Client that MUST contain a String,
|
||||
Number, or NULL value if included. If it is not included it is assumed
|
||||
to be a notification. The value SHOULD normally not be Null [1] and
|
||||
Numbers SHOULD NOT contain fractional parts [2].
|
||||
|
||||
The Server MUST reply with the same value in the Response object if
|
||||
included. This member is used to correlate the context between the two
|
||||
objects.
|
||||
|
||||
[1] The use of Null as a value for the id member in a Request object
|
||||
is discouraged, because this specification uses a value of Null for
|
||||
Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id
|
||||
value of Null for Notifications this could cause confusion in handling.
|
||||
|
||||
[2] Fractional parts may be problematic, since many decimal fractions
|
||||
cannot be represented exactly as binary fractions.
|
||||
|
||||
Notification
|
||||
A Notification is a Request object without an "id" member. A Request
|
||||
object that is a Notification signifies the Client's lack of interest
|
||||
in the corresponding Response object, and as such no Response object
|
||||
needs to be returned to the client. The Server MUST NOT reply to a
|
||||
Notification, including those that are within a batch request.
|
||||
|
||||
Notifications are not confirmable by definition, since they do not have
|
||||
a Response object to be returned. As such, the Client would not be
|
||||
aware of any errors (like e.g. "Invalid params","Internal error").
|
||||
|
||||
Parameter Structures
|
||||
If present, parameters for the rpc call MUST be provided as a
|
||||
Structured value. Either by-position through an Array or by-name
|
||||
through an Object.
|
||||
|
||||
by-position: params MUST be an Array, containing the values in the
|
||||
Server expected order.
|
||||
by-name: params MUST be an Object, with member names that match the
|
||||
Server expected parameter names. The absence of expected names MAY
|
||||
result in an error being generated. The names MUST match exactly,
|
||||
including case, to the method's expected parameters.
|
||||
|
||||
Note:
|
||||
Design principles:
|
||||
* if an object is created or modified without exceptions, its state is
|
||||
valid.
|
||||
* There is a signle source of truth (see Attributes), modification should
|
||||
be done via setters.
|
||||
|
||||
Args:
|
||||
method (str):
|
||||
method to call.
|
||||
params (:obj:dict, optional):
|
||||
a mapping of method argument values or a list of positional arguments.
|
||||
id:
|
||||
an id of the request. By default equals to None and raises warning.
|
||||
For notifications set is_notification=True so id would not be
|
||||
included in the body.
|
||||
is_notification:
|
||||
a boolean flag indicating whether to include id in the body or not.
|
||||
|
||||
Attributes:
|
||||
_body (dict): body of the request. It should always contain valid data and
|
||||
should be modified via setters to ensure validity.
|
||||
|
||||
Examples:
|
||||
modification via self.body["method"] vs self.method
|
||||
modifications via self._body
|
||||
"""
|
||||
def __init__(self,
|
||||
method: str,
|
||||
params: Optional[Union[Mapping[str, Any], Iterable[Any]]] = None,
|
||||
id: Optional[Union[str, int]] = None,
|
||||
is_notification: bool = False
|
||||
) -> None:
|
||||
request_body = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
}
|
||||
|
||||
if params is not None:
|
||||
# If params are not present, they should not be in body
|
||||
request_body["params"] = params
|
||||
|
||||
if not is_notification:
|
||||
# For non-notifications "id" has to be in body, even if null
|
||||
request_body["id"] = id
|
||||
|
||||
self._body = {} # init body
|
||||
self.body = request_body
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value: Mapping[str, Any]) -> None:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("request body has to be of type Mapping")
|
||||
|
||||
extra_keys = set(value.keys()) - {"jsonrpc", "method", "params", "id"}
|
||||
if len(extra_keys) > 0:
|
||||
raise ValueError("unexpected keys {}".format(extra_keys))
|
||||
|
||||
if value.get("jsonrpc") != "2.0":
|
||||
raise ValueError("value of key 'jsonrpc' has to be '2.0'")
|
||||
|
||||
self.validate_method(value.get("method"))
|
||||
if "params" in value:
|
||||
self.validate_params(value["params"])
|
||||
|
||||
# Validate id for non-notification
|
||||
if "id" in value:
|
||||
self.validate_id(value["id"])
|
||||
|
||||
self._body = value
|
||||
|
||||
@property
|
||||
def method(self) -> str:
|
||||
return self.body["method"]
|
||||
|
||||
@staticmethod
|
||||
def validate_method(value: str) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Method should be string")
|
||||
|
||||
if value.startswith("rpc."):
|
||||
raise ValueError(
|
||||
"Method names that begin with the word rpc followed by a " +
|
||||
"period character (U+002E or ASCII 46) are reserved for " +
|
||||
"rpc-internal methods and extensions and MUST NOT be used " +
|
||||
"for anything else.")
|
||||
|
||||
@method.setter
|
||||
def method(self, value: str) -> None:
|
||||
self.validate_method(value)
|
||||
self._body["method"] = value
|
||||
|
||||
@property
|
||||
def params(self) -> Optional[Union[Mapping[str, Any], Iterable[Any]]]:
|
||||
return self.body.get("params")
|
||||
|
||||
@staticmethod
|
||||
def validate_params(value: Optional[Union[Mapping[str, Any], Iterable[Any]]]) -> None:
|
||||
"""
|
||||
Note: params has to be None, dict or iterable. In the latter case it would be
|
||||
converted to a list. It is possible to set param as tuple or even string as they
|
||||
are iterables, they would be converted to lists, e.g. ["h", "e", "l", "l", "o"]
|
||||
"""
|
||||
if not isinstance(value, (Mapping, Iterable)):
|
||||
raise ValueError("Incorrect params {0}".format(value))
|
||||
|
||||
@params.setter
|
||||
def params(self, value: Optional[Union[Mapping[str, Any], Iterable[Any]]]) -> None:
|
||||
self.validate_params(value)
|
||||
self._body["params"] = value
|
||||
|
||||
@params.deleter
|
||||
def params(self):
|
||||
del self._body["params"]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.body["id"]
|
||||
|
||||
@staticmethod
|
||||
def validate_id(value: Optional[Union[str, Number]]) -> None:
|
||||
if value is None:
|
||||
warnings.warn(
|
||||
"The use of Null as a value for the id member in a Request "
|
||||
"object is discouraged, because this specification uses a "
|
||||
"value of Null for Responses with an unknown id. Also, because"
|
||||
" JSON-RPC 1.0 uses an id value of Null for Notifications this"
|
||||
" could cause confusion in handling.",
|
||||
JSONRPC20RequestIdWarning
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(value, (str, Number)):
|
||||
raise ValueError("id MUST contain a String, Number, or NULL value")
|
||||
|
||||
if isinstance(value, Number) and not isinstance(value, int):
|
||||
warnings.warn(
|
||||
"Fractional parts may be problematic, since many decimal "
|
||||
"fractions cannot be represented exactly as binary fractions.",
|
||||
JSONRPC20RequestIdWarning
|
||||
)
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[Union[str, Number]]) -> None:
|
||||
self.validate_id(value)
|
||||
self._body["id"] = value
|
||||
|
||||
@id.deleter
|
||||
def id(self):
|
||||
del self._body["id"]
|
||||
|
||||
@property
|
||||
def is_notification(self):
|
||||
"""Check if request is a notification.
|
||||
There is no API to make a request notification as this has to remove
|
||||
"id" from body and might cause confusion. To make a request
|
||||
notification delete "id" explicitly.
|
||||
"""
|
||||
return "id" not in self.body
|
||||
|
||||
@property
|
||||
def args(self) -> List:
|
||||
""" Method position arguments.
|
||||
:return list args: method position arguments.
|
||||
note: dict is also iterable, so exclude it from args.
|
||||
"""
|
||||
# if not none and not mapping
|
||||
return list(self.params) if isinstance(self.params, Iterable) and not isinstance(self.params, Mapping) else []
|
||||
|
||||
@property
|
||||
def kwargs(self) -> Dict:
|
||||
""" Method named arguments.
|
||||
:return dict kwargs: method named arguments.
|
||||
"""
|
||||
# if mapping
|
||||
return dict(self.params) if isinstance(self.params, Mapping) else {}
|
||||
|
||||
@staticmethod
|
||||
def from_body(body: Mapping):
|
||||
request = JSONRPC20Request(method="", id=0)
|
||||
request.body = body
|
||||
return request
|
||||
|
||||
|
||||
class JSONRPC20BatchRequest(collections.abc.MutableSequence):
|
||||
def __init__(self, requests: List[JSONRPC20Request] = None):
|
||||
self.requests = requests or []
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.requests[index]
|
||||
|
||||
def __setitem__(self, index, value: JSONRPC20Request):
|
||||
self.requests[index] = value
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self.requests[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.requests)
|
||||
|
||||
def insert(self, index, value: JSONRPC20Request):
|
||||
self.requests.insert(index, value)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return [request.body for request in self]
|
||||
|
||||
|
||||
class JSONRPC20Error:
|
||||
|
||||
"""Error object.
|
||||
|
||||
When a rpc call encounters an error, the Response Object MUST contain the
|
||||
error member with a value that is a Object with the following members:
|
||||
|
||||
code
|
||||
A Number that indicates the error type that occurred.
|
||||
This MUST be an integer.
|
||||
message
|
||||
A String providing a short description of the error.
|
||||
The message SHOULD be limited to a concise single sentence.
|
||||
data
|
||||
A Primitive or Structured value that contains additional information
|
||||
about the error.
|
||||
This may be omitted.
|
||||
The value of this member is defined by the Server (e.g. detailed error
|
||||
information, nested errors etc.).
|
||||
|
||||
The error codes from and including -32768 to -32000 are reserved for
|
||||
pre-defined errors. Any code within this range, but not defined explicitly
|
||||
below is reserved for future use. The error codes are nearly the same as
|
||||
those suggested for XML-RPC at the following url:
|
||||
http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
|
||||
|
||||
code | message | meaning
|
||||
-----------------|------------------|-------------------------------------
|
||||
-32700 | Parse error | Invalid JSON was received by the server.
|
||||
| An error occurred on the server while parsing the JSON text.
|
||||
-32600 | Invalid Request | The JSON sent is not a valid Request object.
|
||||
-32601 | Method not found | The method does not exist / is not available.
|
||||
-32602 | Invalid params | Invalid method parameter(s).
|
||||
-32603 | Internal error | Internal JSON-RPC error.
|
||||
-32000 to -32099 | Server error | Reserved for implementation-defined server-errors.
|
||||
|
||||
The remainder of the space is available for application defined errors.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, message: str, data: Any = None):
|
||||
error_body = {
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
if data is not None:
|
||||
# NOTE: if not set in constructor, do not add 'data' to payload.
|
||||
# If data = null is requred, set it after object initialization.
|
||||
error_body["data"] = data
|
||||
|
||||
self._body = {} # init body
|
||||
self.body = error_body
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.code == other.code \
|
||||
and self.message == other.message \
|
||||
and self.data == other.data
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("value has to be of type dict")
|
||||
|
||||
self.validate_code(value["code"])
|
||||
self.validate_message(value["message"])
|
||||
self._body = value
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self.body["code"]
|
||||
|
||||
@staticmethod
|
||||
def validate_code(value: int) -> None:
|
||||
if not isinstance(value, int):
|
||||
raise ValueError("Error code MUST be an integer")
|
||||
|
||||
@code.setter
|
||||
def code(self, value: int) -> None:
|
||||
self.validate_code(value)
|
||||
self._body["code"] = value
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.body["message"]
|
||||
|
||||
@staticmethod
|
||||
def validate_message(value: str) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Error message should be string")
|
||||
|
||||
@message.setter
|
||||
def message(self, value: str):
|
||||
self.validate_message(value)
|
||||
self._body["message"] = value
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._body.get("data")
|
||||
|
||||
@data.setter
|
||||
def data(self, value):
|
||||
self._body["data"] = value
|
||||
|
||||
@data.deleter
|
||||
def data(self):
|
||||
del self._body["data"]
|
||||
|
||||
@staticmethod
|
||||
def validate_body(value: dict) -> None:
|
||||
if not (set(value.keys()) <= {"code", "message", "data"}):
|
||||
raise ValueError("Error body could have only 'code', 'message' and 'data' keys")
|
||||
|
||||
JSONRPC20Error.validate_code(value.get("code"))
|
||||
JSONRPC20Error.validate_message(value.get("message"))
|
||||
|
||||
|
||||
class JSONRPC20SpecificError(JSONRPC20Error):
|
||||
|
||||
"""Base class for errors with fixed code and message.
|
||||
|
||||
Keep only data in constructor and forbid code and message modifications.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, data: Any = None):
|
||||
super(JSONRPC20SpecificError, self).__init__(getattr(self.__class__, "CODE"), getattr(self.__class__, "MESSAGE"), data)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr == "code":
|
||||
raise NotImplementedError("Code modification is forbidden")
|
||||
elif attr == "message":
|
||||
raise NotImplementedError("Message modification is forbidden")
|
||||
else:
|
||||
super(JSONRPC20SpecificError, self).__setattr__(attr, value)
|
||||
|
||||
|
||||
class JSONRPC20ParseError(JSONRPC20SpecificError):
|
||||
|
||||
"""Parse Error.
|
||||
Invalid JSON was received by the server.
|
||||
An error occurred on the server while parsing the JSON text.
|
||||
"""
|
||||
|
||||
CODE = -32700
|
||||
MESSAGE = "Parse error"
|
||||
|
||||
|
||||
class JSONRPC20InvalidRequest(JSONRPC20SpecificError):
|
||||
|
||||
"""Invalid Request.
|
||||
The JSON sent is not a valid Request object.
|
||||
"""
|
||||
|
||||
CODE = -32600
|
||||
MESSAGE = "Invalid Request"
|
||||
|
||||
|
||||
class JSONRPC20MethodNotFound(JSONRPC20SpecificError):
|
||||
|
||||
"""Method not found.
|
||||
The method does not exist / is not available.
|
||||
"""
|
||||
|
||||
CODE = -32601
|
||||
MESSAGE = "Method not found"
|
||||
|
||||
|
||||
class JSONRPC20InvalidParams(JSONRPC20SpecificError):
|
||||
|
||||
"""Invalid params.
|
||||
Invalid method parameter(s).
|
||||
"""
|
||||
|
||||
CODE = -32602
|
||||
MESSAGE = "Invalid params"
|
||||
|
||||
|
||||
class JSONRPC20InternalError(JSONRPC20SpecificError):
|
||||
|
||||
"""Internal error.
|
||||
Internal JSON-RPC error.
|
||||
"""
|
||||
|
||||
CODE = -32603
|
||||
MESSAGE = "Internal error"
|
||||
|
||||
|
||||
class JSONRPC20ServerError(JSONRPC20SpecificError):
|
||||
|
||||
"""Server error.
|
||||
Reserved for implementation-defined server-errors.
|
||||
"""
|
||||
|
||||
CODE = -32000
|
||||
MESSAGE = "Server error"
|
||||
|
||||
|
||||
class JSONRPC20Response:
|
||||
def __init__(self,
|
||||
result: Optional[Any] = None,
|
||||
error: Optional[JSONRPC20Error] = None,
|
||||
id: Optional[Union[str, int]] = None,
|
||||
) -> None:
|
||||
response_body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
}
|
||||
|
||||
if result is not None:
|
||||
response_body["result"] = result
|
||||
|
||||
if error is not None:
|
||||
response_body["error"] = error.body
|
||||
|
||||
self._body = {} # init body
|
||||
self.body = response_body
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value: Mapping[str, Any]) -> None:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("value has to be of type dict")
|
||||
|
||||
if value.get("jsonrpc") != "2.0":
|
||||
raise ValueError("value['jsonrpc'] has to be '2.0'")
|
||||
|
||||
if "result" not in value and "error" not in value:
|
||||
raise ValueError("Either result or error should exist")
|
||||
|
||||
if "result" in value and "error" in value:
|
||||
raise ValueError("Only one result or error should exist")
|
||||
|
||||
if "error" in value:
|
||||
self.validate_error(value["error"])
|
||||
|
||||
self.validate_id(value["id"])
|
||||
|
||||
self._body = {
|
||||
k: v for (k, v) in value.items()
|
||||
if k in ["jsonrpc", "result", "error", "id"]
|
||||
}
|
||||
|
||||
@property
|
||||
def result(self) -> Optional[Any]:
|
||||
return self.body.get("result")
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[JSONRPC20Error]:
|
||||
if "error" in self.body:
|
||||
return JSONRPC20Error(**self.body["error"])
|
||||
|
||||
@staticmethod
|
||||
def validate_error(error_body: dict) -> None:
|
||||
JSONRPC20Error.validate_body(error_body)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.body["id"]
|
||||
|
||||
@staticmethod
|
||||
def validate_id(value: Optional[Union[str, Number]]) -> None:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if not isinstance(value, (str, Number)):
|
||||
raise ValueError("id MUST contain a String, Number, or NULL value")
|
||||
|
||||
if isinstance(value, Number) and not isinstance(value, int):
|
||||
warnings.warn(
|
||||
"Fractional parts may be problematic, since many decimal "
|
||||
"fractions cannot be represented exactly as binary fractions.",
|
||||
JSONRPC20ResponseIdWarning
|
||||
)
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[Union[str, Number]]) -> None:
|
||||
self.validate_id(value)
|
||||
self._body["id"] = value
|
||||
|
||||
|
||||
class JSONRPC20BatchResponse(collections.abc.MutableSequence):
|
||||
def __init__(self, requests: List[JSONRPC20Response] = None):
|
||||
self.requests = requests or []
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.requests[index]
|
||||
|
||||
def __setitem__(self, index, value: JSONRPC20Response):
|
||||
self.requests[index] = value
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self.requests[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.requests)
|
||||
|
||||
def insert(self, index, value: JSONRPC20Response):
|
||||
self.requests.insert(index, value)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return [request.body for request in self]
|
||||
|
||||
|
||||
class JSONRPC20Exception(Exception):
|
||||
|
||||
"""JSON-RPC Exception class."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JSONRPC20DispatchException(JSONRPC20Exception):
|
||||
|
||||
"""JSON-RPC Base Exception for dispatcher methods."""
|
||||
|
||||
def __init__(self, code=None, message=None, data=None, *args, **kwargs):
|
||||
super(JSONRPC20DispatchException, self).__init__(args, kwargs)
|
||||
self.error = JSONRPC20Error(code=code, data=data, message=message)
|
||||
164
.venv/lib/python3.11/site-packages/ajsonrpc/dispatcher.py
Normal file
164
.venv/lib/python3.11/site-packages/ajsonrpc/dispatcher.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Method name to method mapper.
|
||||
|
||||
Dispatcher is a dict-like object which maps method_name to method.
|
||||
For usage examples see :meth:`~Dispatcher.add_function`
|
||||
|
||||
"""
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from typing import Any, Optional, Mapping
|
||||
from collections.abc import Mapping as CollectionsMapping, MutableMapping, Callable
|
||||
|
||||
|
||||
class Dispatcher(MutableMapping):
|
||||
|
||||
"""Dictionary-like object which maps method_name to method."""
|
||||
|
||||
def __init__(self, prototype: Any = None, prefix: Optional[str] = None) -> None:
|
||||
""" Build method dispatcher.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prototype : object or dict, optional
|
||||
Initial method mapping.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Init object with method dictionary.
|
||||
|
||||
>>> Dispatcher({"sum": lambda a, b: a + b})
|
||||
None
|
||||
|
||||
"""
|
||||
self.method_map: Mapping[str, Callable] = dict()
|
||||
|
||||
if prototype is not None:
|
||||
self.add_prototype(prototype, prefix=prefix)
|
||||
|
||||
def __getitem__(self, key: str) -> Callable:
|
||||
return self.method_map[key]
|
||||
|
||||
def __setitem__(self, key: str, value: Callable) -> None:
|
||||
self.method_map[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self.method_map[key]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.method_map)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.method_map)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.method_map)
|
||||
|
||||
@staticmethod
|
||||
def _getattr_function(prototype: Any, attr: str) -> Callable:
|
||||
"""Fix the issue of accessing instance method of a class.
|
||||
|
||||
Class.method(self, *args **kwargs) requires the first argument to be
|
||||
instance, but it was not given. Substitute method with a partial
|
||||
function where the first argument is an empty class constructor.
|
||||
|
||||
"""
|
||||
|
||||
method = getattr(prototype, attr)
|
||||
if inspect.isclass(prototype) and isinstance(prototype.__dict__[attr], types.FunctionType):
|
||||
return functools.partial(method, prototype())
|
||||
return method
|
||||
|
||||
@staticmethod
|
||||
def _extract_methods(prototype: Any, prefix: str = "") -> Mapping[str, Callable]:
|
||||
return {
|
||||
prefix + attr: Dispatcher._getattr_function(prototype, attr)
|
||||
for attr in dir(prototype)
|
||||
if not attr.startswith("_")
|
||||
}
|
||||
|
||||
def add_class(self, cls: Any, prefix: Optional[str] = None) -> None:
|
||||
"""Add class to dispatcher.
|
||||
|
||||
Adds all of the public methods to dispatcher.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If class has instance methods (e.g. no @classmethod decorator),
|
||||
they likely would not work. Use :meth:`~add_object` instead.
|
||||
At the moment, dispatcher creates an object with empty constructor
|
||||
for instance methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cls : type
|
||||
class with methods to be added to dispatcher
|
||||
prefix : str, optional
|
||||
Method prefix. If not present, lowercased class name is used.
|
||||
|
||||
"""
|
||||
if prefix is None:
|
||||
prefix = cls.__name__.lower() + '.'
|
||||
|
||||
self.update(Dispatcher._extract_methods(cls, prefix=prefix))
|
||||
|
||||
def add_object(self, obj: Any, prefix: Optional[str] = None) -> None:
|
||||
if prefix is None:
|
||||
prefix = obj.__class__.__name__.lower() + '.'
|
||||
|
||||
self.update(Dispatcher._extract_methods(obj, prefix=prefix))
|
||||
|
||||
def add_prototype(self, prototype: Any, prefix: Optional[str] = None) -> None:
|
||||
if isinstance(prototype, CollectionsMapping):
|
||||
self.update({
|
||||
(prefix or "") + key: value
|
||||
for key, value in prototype.items()
|
||||
})
|
||||
elif inspect.isclass(prototype):
|
||||
self.add_class(prototype, prefix=prefix)
|
||||
else:
|
||||
self.add_object(prototype, prefix=prefix)
|
||||
|
||||
def add_function(self, f: Callable = None, name: Optional[str] = None) -> Callable:
|
||||
""" Add a method to the dispatcher.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Callable to be added.
|
||||
name : str, optional
|
||||
Name to register (the default is function **f** name)
|
||||
|
||||
Notes
|
||||
-----
|
||||
When used as a decorator keeps callable object unmodified.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Use as method
|
||||
|
||||
>>> d = Dispatcher()
|
||||
>>> d.add_function(lambda a, b: a + b, name="sum")
|
||||
<function __main__.<lambda>>
|
||||
|
||||
Or use as decorator
|
||||
|
||||
>>> d = Dispatcher()
|
||||
>>> @d.add_function
|
||||
def mymethod(*args, **kwargs):
|
||||
print(args, kwargs)
|
||||
|
||||
Or use as a decorator with a different function name
|
||||
>>> d = Dispatcher()
|
||||
>>> @d.add_function(name="my.method")
|
||||
def mymethod(*args, **kwargs):
|
||||
print(args, kwargs)
|
||||
|
||||
"""
|
||||
if name and not f:
|
||||
return functools.partial(self.add_function, name=name)
|
||||
|
||||
self[name or f.__name__] = f
|
||||
return f
|
||||
118
.venv/lib/python3.11/site-packages/ajsonrpc/manager.py
Normal file
118
.venv/lib/python3.11/site-packages/ajsonrpc/manager.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import json
|
||||
import inspect
|
||||
import asyncio
|
||||
from typing import Optional, Union, Iterable, Mapping
|
||||
|
||||
from .core import (
|
||||
JSONRPC20Request, JSONRPC20BatchRequest, JSONRPC20Response,
|
||||
JSONRPC20BatchResponse, JSONRPC20MethodNotFound, JSONRPC20InvalidParams,
|
||||
JSONRPC20ServerError, JSONRPC20ParseError, JSONRPC20InvalidRequest,
|
||||
JSONRPC20DispatchException,
|
||||
)
|
||||
from .dispatcher import Dispatcher
|
||||
from .utils import is_invalid_params
|
||||
|
||||
|
||||
class AsyncJSONRPCResponseManager:
|
||||
|
||||
"""Async JSON-RPC Response manager."""
|
||||
|
||||
def __init__(self, dispatcher: Dispatcher, serialize=json.dumps, deserialize=json.loads, is_server_error_verbose=False):
|
||||
self.dispatcher = dispatcher
|
||||
self.serialize = serialize
|
||||
self.deserialize = deserialize
|
||||
self.is_server_error_verbose = is_server_error_verbose
|
||||
|
||||
async def get_response_for_request(self, request: JSONRPC20Request) -> Optional[JSONRPC20Response]:
|
||||
"""Get response for an individual request."""
|
||||
output = None
|
||||
response_id = request.id if not request.is_notification else None
|
||||
try:
|
||||
method = self.dispatcher[request.method]
|
||||
except KeyError:
|
||||
# method not found
|
||||
output = JSONRPC20Response(
|
||||
error=JSONRPC20MethodNotFound(),
|
||||
id=response_id
|
||||
)
|
||||
else:
|
||||
try:
|
||||
result = await method(*request.args, **request.kwargs) \
|
||||
if inspect.iscoroutinefunction(method) \
|
||||
else method(*request.args, **request.kwargs)
|
||||
except JSONRPC20DispatchException as dispatch_error:
|
||||
# Dispatcher method raised exception with controlled "data"
|
||||
output = JSONRPC20Response(
|
||||
error=dispatch_error.error,
|
||||
id=response_id
|
||||
)
|
||||
except Exception as e:
|
||||
if is_invalid_params(method, *request.args, **request.kwargs):
|
||||
# Method's parameters are incorrect
|
||||
output = JSONRPC20Response(
|
||||
error=JSONRPC20InvalidParams(),
|
||||
id=response_id
|
||||
)
|
||||
else:
|
||||
# Dispatcher method raised exception
|
||||
output = JSONRPC20Response(
|
||||
error=JSONRPC20ServerError(
|
||||
data={
|
||||
"type": e.__class__.__name__,
|
||||
"args": e.args,
|
||||
"message": str(e),
|
||||
} if self.is_server_error_verbose else None
|
||||
),
|
||||
id=response_id
|
||||
)
|
||||
else:
|
||||
output = JSONRPC20Response(result=result, id=response_id)
|
||||
|
||||
if not request.is_notification:
|
||||
return output
|
||||
|
||||
async def get_response_for_request_body(self, request_body) -> Optional[JSONRPC20Response]:
|
||||
"""Catch parse error as well"""
|
||||
try:
|
||||
request = JSONRPC20Request.from_body(request_body)
|
||||
except ValueError:
|
||||
return JSONRPC20Response(error=JSONRPC20InvalidRequest())
|
||||
else:
|
||||
return await self.get_response_for_request(request)
|
||||
|
||||
async def get_response_for_payload(self, payload: str) -> Optional[Union[JSONRPC20Response, JSONRPC20BatchResponse]]:
|
||||
"""Top level handler
|
||||
|
||||
NOTE: top level handler, accepts string payload.
|
||||
|
||||
"""
|
||||
try:
|
||||
request_data = self.deserialize(payload)
|
||||
except (TypeError, ValueError):
|
||||
return JSONRPC20Response(error=JSONRPC20ParseError())
|
||||
|
||||
# check if iterable, and determine what request to instantiate.
|
||||
is_batch_request = isinstance(request_data, Iterable) \
|
||||
and not isinstance(request_data, Mapping)
|
||||
if is_batch_request and len(request_data) == 0:
|
||||
return JSONRPC20Response(error=JSONRPC20InvalidRequest())
|
||||
|
||||
requests_bodies = request_data if is_batch_request else [request_data]
|
||||
responses = await asyncio.gather(*[
|
||||
self.get_response_for_request_body(request_body)
|
||||
for request_body in requests_bodies
|
||||
])
|
||||
nonempty_responses = [r for r in responses if r is not None]
|
||||
if is_batch_request:
|
||||
if len(nonempty_responses) > 0:
|
||||
return JSONRPC20BatchResponse(nonempty_responses)
|
||||
elif len(nonempty_responses) > 0:
|
||||
return nonempty_responses[0]
|
||||
|
||||
async def get_payload_for_payload(self, payload: str) -> str:
|
||||
response = await self.get_response_for_payload(payload)
|
||||
|
||||
if response is None:
|
||||
return ""
|
||||
|
||||
return self.serialize(response.body)
|
||||
107
.venv/lib/python3.11/site-packages/ajsonrpc/scripts/server.py
Normal file
107
.venv/lib/python3.11/site-packages/ajsonrpc/scripts/server.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import importlib.util
|
||||
import sys
|
||||
from inspect import getmembers, isfunction
|
||||
from ajsonrpc import __version__
|
||||
from ajsonrpc.dispatcher import Dispatcher
|
||||
from ajsonrpc.manager import AsyncJSONRPCResponseManager
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Helper funciont to create asyncio task
|
||||
# see: https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
||||
if sys.version_info >= (3, 7):
|
||||
create_task = asyncio.create_task
|
||||
else:
|
||||
create_task = asyncio.ensure_future
|
||||
|
||||
|
||||
class JSONRPCProtocol(asyncio.Protocol):
|
||||
def __init__(self, json_rpc_manager):
|
||||
self.json_rpc_manager = json_rpc_manager
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
def data_received(self, data):
|
||||
message = data.decode()
|
||||
request_method, request_message = message.split('\r\n', 1)
|
||||
if not request_method.startswith('POST'):
|
||||
logger.warning('Incorrect HTTP method, should be POST')
|
||||
|
||||
_, payload = request_message.split('\r\n\r\n', 1)
|
||||
task = create_task(self.json_rpc_manager.get_payload_for_payload(payload))
|
||||
task.add_done_callback(self.handle_task_result)
|
||||
|
||||
def handle_task_result(self, task):
|
||||
res = task.result()
|
||||
self.transport.write((
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"\r\n"
|
||||
+ str(res)
|
||||
).encode("utf-8"))
|
||||
|
||||
logger.info('Close the client socket')
|
||||
self.transport.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Usage: % examples.methods"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=True,
|
||||
description="Start async JSON-RPC 2.0 server")
|
||||
parser.add_argument(
|
||||
'--version', action='version',
|
||||
version='%(prog)s {version}'.format(version=__version__))
|
||||
parser.add_argument("--host", dest="host", default="127.0.0.1")
|
||||
parser.add_argument("--port", dest="port")
|
||||
parser.add_argument('module')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
spec = importlib.util.spec_from_file_location("module", args.module)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# get functions from the module
|
||||
methods = getmembers(module, isfunction)
|
||||
logger.info('Extracted methods: {}'.format(methods))
|
||||
dispatcher = Dispatcher(dict(methods))
|
||||
|
||||
json_rpc_manager = AsyncJSONRPCResponseManager(dispatcher=dispatcher)
|
||||
loop = asyncio.get_event_loop()
|
||||
# Each client connection will create a new protocol instance
|
||||
coro = loop.create_server(
|
||||
lambda: JSONRPCProtocol(json_rpc_manager),
|
||||
host=args.host,
|
||||
port=args.port
|
||||
)
|
||||
server = loop.run_until_complete(coro)
|
||||
|
||||
# Serve requests until Ctrl+C is pressed
|
||||
logger.info('Serving on {}'.format(server.sockets[0].getsockname()))
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# Close the server
|
||||
server.close()
|
||||
loop.run_until_complete(server.wait_closed())
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# setup console logging
|
||||
logger.setLevel(logging.DEBUG)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter("%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s")
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
main()
|
||||
407
.venv/lib/python3.11/site-packages/ajsonrpc/tests/test_core.py
Normal file
407
.venv/lib/python3.11/site-packages/ajsonrpc/tests/test_core.py
Normal file
@@ -0,0 +1,407 @@
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from ..core import (JSONRPC20BatchRequest, JSONRPC20BatchResponse,
|
||||
JSONRPC20Error, JSONRPC20InternalError,
|
||||
JSONRPC20InvalidParams, JSONRPC20InvalidRequest,
|
||||
JSONRPC20MethodNotFound, JSONRPC20ParseError,
|
||||
JSONRPC20Request, JSONRPC20RequestIdWarning,
|
||||
JSONRPC20Response, JSONRPC20ServerError)
|
||||
|
||||
|
||||
class TestJSONRPC20Request(unittest.TestCase):
|
||||
|
||||
"""Test JSONRPC20Request.
|
||||
|
||||
On creation and after modification the request object has to be valid. As
|
||||
these scenarios are almost identical, test both in test cases.
|
||||
|
||||
"""
|
||||
|
||||
#############################################
|
||||
# "method" tests
|
||||
#############################################
|
||||
|
||||
def test_method_validation_correct(self):
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
self.assertEqual(r.method, "valid")
|
||||
r.method = "also_valid"
|
||||
self.assertEqual(r.method, "also_valid")
|
||||
|
||||
def test_method_validation_not_str(self):
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method=[], id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = []
|
||||
|
||||
# invalid setters should not modify the object
|
||||
self.assertEqual(r.method, "valid")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method={}, id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = {}
|
||||
|
||||
def test_method_validation_invalid_rpc_prefix(self):
|
||||
""" Test method SHOULD NOT starts with rpc."""
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method="rpc.", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = "rpc."
|
||||
|
||||
# invalid setters should not modify the object
|
||||
self.assertEqual(r.method, "valid")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method="rpc.test", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = "rpc.test"
|
||||
|
||||
JSONRPC20Request(method="rpcvalid", id=0)
|
||||
JSONRPC20Request(method="rpc", id=0)
|
||||
|
||||
#############################################
|
||||
# "params" tests
|
||||
#############################################
|
||||
|
||||
def test_params_validation_none(self):
|
||||
r1 = JSONRPC20Request("null_params", params=None, id=1)
|
||||
self.assertFalse("params" in r1.body)
|
||||
|
||||
# Remove params
|
||||
r2 = JSONRPC20Request("null_params", params=[], id=2)
|
||||
self.assertTrue("params" in r2.body)
|
||||
del r2.params
|
||||
self.assertFalse("params" in r2.body)
|
||||
|
||||
def test_params_validation_list(self):
|
||||
r = JSONRPC20Request("list", params=[], id=0)
|
||||
self.assertEqual(r.params, [])
|
||||
r.params = [0, 1]
|
||||
self.assertEqual(r.params, [0, 1])
|
||||
|
||||
def test_params_validation_tuple(self):
|
||||
r = JSONRPC20Request("tuple", params=(), id=0)
|
||||
self.assertEqual(r.params, ()) # keep the same iterable
|
||||
r.params = (0, 1)
|
||||
self.assertEqual(r.params, (0, 1))
|
||||
|
||||
def test_params_validation_iterable(self):
|
||||
r1 = JSONRPC20Request("string_params", params="string", id=1)
|
||||
self.assertEqual(r1.params, "string")
|
||||
r1.params = "another string"
|
||||
self.assertEqual(r1.params, "another string")
|
||||
|
||||
r2 = JSONRPC20Request("range_params", params=range(1), id=2)
|
||||
self.assertEqual(r2.params, range(1))
|
||||
r2.params = range(2)
|
||||
self.assertEqual(r2.params, range(2))
|
||||
|
||||
def test_params_validation_dict(self):
|
||||
r1 = JSONRPC20Request("dict_params", params={}, id=1)
|
||||
self.assertEqual(r1.params, {})
|
||||
r1.params = {"a": 0}
|
||||
self.assertEqual(r1.params, {"a": 0})
|
||||
|
||||
r2 = JSONRPC20Request("dict_params", params={"a": 0}, id=2)
|
||||
self.assertEqual(r2.params, {"a": 0})
|
||||
r2.params = {"a": {}}
|
||||
self.assertEqual(r2.params, {"a": {}})
|
||||
|
||||
def test_params_validation_invalid(self):
|
||||
r = JSONRPC20Request("list", params=[], id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request("invalid_params", params=0, id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.params = 0
|
||||
|
||||
self.assertEqual(r.params, [])
|
||||
|
||||
#############################################
|
||||
# "id" tests
|
||||
#############################################
|
||||
|
||||
def test_id_validation_valid(self):
|
||||
r1 = JSONRPC20Request("string_id", id="id")
|
||||
self.assertEqual(r1.id, "id")
|
||||
r1.id = "another_id"
|
||||
self.assertEqual(r1.id, "another_id")
|
||||
|
||||
r2 = JSONRPC20Request("int_id", id=0)
|
||||
self.assertEqual(r2.id, 0)
|
||||
r2.id = 1
|
||||
self.assertEqual(r2.id, 1)
|
||||
|
||||
# Null ids are possible but discouraged. Omit id for notifications.
|
||||
with warnings.catch_warnings(record=True) as _warnings:
|
||||
warnings.simplefilter("always")
|
||||
JSONRPC20Request("null_id", id=None)
|
||||
assert len(_warnings) == 1
|
||||
assert issubclass(_warnings[-1].category, JSONRPC20RequestIdWarning)
|
||||
assert "Null as a value" in str(_warnings[-1].message)
|
||||
|
||||
# Float ids are possible but discouraged
|
||||
with warnings.catch_warnings(record=True) as _warnings:
|
||||
warnings.simplefilter("always")
|
||||
JSONRPC20Request("float_id", id=0.1)
|
||||
|
||||
assert len(_warnings) == 1
|
||||
assert issubclass(_warnings[-1].category, JSONRPC20RequestIdWarning)
|
||||
assert "Fractional parts" in str(_warnings[-1].message)
|
||||
|
||||
|
||||
def test_id_validation_invalid(self):
|
||||
r = JSONRPC20Request("valid_id", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request("list_id", id=[])
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.id = []
|
||||
|
||||
self.assertEqual(r.id, 0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request("dict_id", id={})
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.id = {}
|
||||
|
||||
#############################################
|
||||
# Notification tests
|
||||
#############################################
|
||||
|
||||
def test_notification_init(self):
|
||||
r = JSONRPC20Request("notification", is_notification=True)
|
||||
self.assertTrue(r.is_notification)
|
||||
with self.assertRaises(KeyError):
|
||||
r.id
|
||||
|
||||
def test_notification_conversion(self):
|
||||
r = JSONRPC20Request("notification", id=0)
|
||||
self.assertFalse(r.is_notification)
|
||||
del r.id
|
||||
self.assertTrue(r.is_notification)
|
||||
|
||||
#############################################
|
||||
# Auxiliary methods tests
|
||||
#############################################
|
||||
|
||||
def test_request_args(self):
|
||||
self.assertEqual(JSONRPC20Request("add", id=0).args, [])
|
||||
self.assertEqual(JSONRPC20Request("add", [], id=0).args, [])
|
||||
self.assertEqual(JSONRPC20Request("add", "str", id=0).args, ["s", "t", "r"])
|
||||
self.assertEqual(JSONRPC20Request("add", {"a": 1}, id=0).args, [])
|
||||
self.assertEqual(JSONRPC20Request("add", [1, 2], id=0).args, [1, 2])
|
||||
|
||||
def test_request_kwargs(self):
|
||||
self.assertEqual(JSONRPC20Request("add", id=0).kwargs, {})
|
||||
self.assertEqual(JSONRPC20Request("add", [1, 2], id=0).kwargs, {})
|
||||
self.assertEqual(JSONRPC20Request("add", {}, id=0).kwargs, {})
|
||||
self.assertEqual(JSONRPC20Request("add", {"a": 1}, id=0).kwargs, {"a": 1})
|
||||
|
||||
#############################################
|
||||
# body methods tests
|
||||
#############################################
|
||||
def test_body_validation(self):
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
self.assertEqual(
|
||||
r.body,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "valid",
|
||||
"id": 0,
|
||||
}
|
||||
)
|
||||
r.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "new",
|
||||
}
|
||||
self.assertEqual(r.id, 1)
|
||||
self.assertEqual(r.method, "new")
|
||||
|
||||
# body has to have "jsonrpc" in it
|
||||
with self.assertRaises(ValueError):
|
||||
r.body = {"id": 1, "method": "new"}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": [],
|
||||
"method": 0,
|
||||
"params": 0,
|
||||
}
|
||||
|
||||
self.assertEqual(r.id, 1)
|
||||
self.assertEqual(r.method, "new")
|
||||
|
||||
|
||||
class TestJSONRPC20BatchRequest(unittest.TestCase):
|
||||
def test_init(self):
|
||||
br = JSONRPC20BatchRequest()
|
||||
self.assertEqual(len(br), 0)
|
||||
|
||||
br.append(JSONRPC20Request("first", id=1))
|
||||
br.extend([JSONRPC20Request("second", id=2)])
|
||||
self.assertEqual(len(br), 2)
|
||||
self.assertEqual(br[-1].method, "second")
|
||||
|
||||
|
||||
class TestJSONRPC20Error(unittest.TestCase):
|
||||
|
||||
"""Test JSONRPC20Error.
|
||||
|
||||
On creation and after modification the request object has to be valid. As
|
||||
these scenarios are almost identical, test both in test cases.
|
||||
|
||||
"""
|
||||
|
||||
#############################################
|
||||
# "code" tests
|
||||
#############################################
|
||||
|
||||
def test_code_validation_valid_numeric(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
self.assertEqual(e.code, 0)
|
||||
# Allow numeric codes. Though, prefer using integers
|
||||
e.code = 1
|
||||
self.assertEqual(e.code, 1)
|
||||
|
||||
def test_code_validation_not_number(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Error(code="0", message="error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
e.code = "0"
|
||||
|
||||
#############################################
|
||||
# "message" tests
|
||||
#############################################
|
||||
|
||||
def test_message_validation_valid_str(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
self.assertEqual(e.message, "error")
|
||||
e.message = "specific error"
|
||||
self.assertEqual(e.message, "specific error")
|
||||
|
||||
def test_message_validation_not_str(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Error(code=0, message=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
e.message = 0
|
||||
|
||||
#############################################
|
||||
# "data" tests
|
||||
#############################################
|
||||
def test_data_validation_valid(self):
|
||||
e = JSONRPC20Error(code=0, message="error", data=0)
|
||||
self.assertEqual(e.data, 0)
|
||||
e.data = {"timestamp": 0}
|
||||
self.assertEqual(e.data, {"timestamp": 0})
|
||||
|
||||
def test_could_not_change_code_message_predefined_errors(self):
|
||||
errors = [
|
||||
JSONRPC20ParseError(),
|
||||
JSONRPC20InvalidRequest(),
|
||||
JSONRPC20MethodNotFound(),
|
||||
JSONRPC20InvalidParams(),
|
||||
JSONRPC20InternalError(),
|
||||
JSONRPC20ServerError(),
|
||||
]
|
||||
|
||||
for error in errors:
|
||||
with self.assertRaises(NotImplementedError):
|
||||
error.code = 0
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
error.message = ""
|
||||
|
||||
|
||||
class TestJSONRPC20Response(unittest.TestCase):
|
||||
def test_valid_result(self):
|
||||
response = JSONRPC20Response(result="valid")
|
||||
self.assertEqual(response.result, "valid")
|
||||
self.assertIsNone(response.error)
|
||||
self.assertEqual(
|
||||
response.body,
|
||||
{"jsonrpc": "2.0", "id": None, "result": "valid"}
|
||||
)
|
||||
|
||||
def test_valid_error(self):
|
||||
error = JSONRPC20MethodNotFound()
|
||||
response = JSONRPC20Response(error=error)
|
||||
self.assertIsNone(response.result)
|
||||
self.assertEqual(response.error, error)
|
||||
self.assertEqual(
|
||||
response.body,
|
||||
{"jsonrpc": "2.0", "id": None, "error": error.body}
|
||||
)
|
||||
|
||||
def test_set_valid_body(self):
|
||||
response = JSONRPC20Response(result="")
|
||||
response.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "",
|
||||
}
|
||||
}
|
||||
self.assertIsInstance(response.error, JSONRPC20Error)
|
||||
|
||||
def test_set_body_result_and_error(self):
|
||||
response = JSONRPC20Response(result="")
|
||||
with self.assertRaises(ValueError):
|
||||
response.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"result": "",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "",
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Response(
|
||||
result="",
|
||||
error=JSONRPC20Error(code=0, message="")
|
||||
)
|
||||
|
||||
@unittest.skip("TODO: Implement later")
|
||||
def test_set_body_error_correct_error_class(self):
|
||||
"""Return error class matching pre-defined error codes."""
|
||||
response = JSONRPC20Response(result="")
|
||||
response.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"error": JSONRPC20MethodNotFound().body,
|
||||
}
|
||||
self.assertIsInstance(response.error, JSONRPC20MethodNotFound)
|
||||
|
||||
|
||||
class TestJSONRPC20BatchResponse(unittest.TestCase):
|
||||
def test_init(self):
|
||||
batch = JSONRPC20BatchResponse()
|
||||
self.assertEqual(len(batch), 0)
|
||||
|
||||
batch.append(JSONRPC20Response(result="first", id=1))
|
||||
batch.extend([JSONRPC20Response(result="second", id=2)])
|
||||
self.assertEqual(len(batch), 2)
|
||||
self.assertEqual(batch[-1].result, "second")
|
||||
@@ -0,0 +1,122 @@
|
||||
import unittest
|
||||
from ..dispatcher import Dispatcher
|
||||
|
||||
|
||||
class Math:
|
||||
@staticmethod
|
||||
def sum(a, b):
|
||||
return a + b
|
||||
|
||||
@classmethod
|
||||
def diff(cls, a, b):
|
||||
return a - b
|
||||
|
||||
def mul(self, a, b):
|
||||
return a * b
|
||||
|
||||
|
||||
class TestDispatcher(unittest.TestCase):
|
||||
def test_empty(self):
|
||||
self.assertEqual(len(Dispatcher()), 0)
|
||||
|
||||
def test_add_function(self):
|
||||
d = Dispatcher()
|
||||
|
||||
@d.add_function
|
||||
def one():
|
||||
return 1
|
||||
|
||||
def two():
|
||||
return 2
|
||||
|
||||
d.add_function(two)
|
||||
d.add_function(two, name="two_alias")
|
||||
|
||||
self.assertIn("one", d)
|
||||
self.assertEqual(d["one"](), 1)
|
||||
self.assertIsNotNone(one) # do not remove function from the scope
|
||||
self.assertIn("two", d)
|
||||
self.assertIn("two_alias", d)
|
||||
|
||||
def test_class(self):
|
||||
d1 = Dispatcher()
|
||||
d1.add_class(Math)
|
||||
self.assertIn("math.sum", d1)
|
||||
self.assertIn("math.diff", d1)
|
||||
self.assertIn("math.mul", d1)
|
||||
self.assertEqual(d1["math.sum"](3, 8), 11)
|
||||
self.assertEqual(d1["math.diff"](6, 9), -3)
|
||||
self.assertEqual(d1["math.mul"](2, 3), 6)
|
||||
|
||||
d2 = Dispatcher(Math)
|
||||
self.assertNotIn("__class__", d2)
|
||||
self.assertEqual(d1.keys(), d2.keys())
|
||||
for method in ["math.sum", "math.diff"]:
|
||||
self.assertEqual(d1[method], d2[method])
|
||||
|
||||
def test_class_prefix(self):
|
||||
d = Dispatcher(Math, prefix="")
|
||||
self.assertIn("sum", d)
|
||||
self.assertNotIn("math.sum", d)
|
||||
|
||||
def test_object(self):
|
||||
math = Math()
|
||||
d1 = Dispatcher()
|
||||
d1.add_object(math)
|
||||
self.assertIn("math.sum", d1)
|
||||
self.assertIn("math.diff", d1)
|
||||
self.assertEqual(d1["math.sum"](3, 8), 11)
|
||||
self.assertEqual(d1["math.diff"](6, 9), -3)
|
||||
|
||||
d2 = Dispatcher(math)
|
||||
self.assertNotIn("__class__", d2)
|
||||
self.assertEqual(d1, d2)
|
||||
|
||||
def test_object_prefix(self):
|
||||
d = Dispatcher(Math(), prefix="")
|
||||
self.assertIn("sum", d)
|
||||
self.assertNotIn("math.sum", d)
|
||||
|
||||
def test_add_dict(self):
|
||||
d = Dispatcher()
|
||||
d.add_prototype({"sum": lambda *args: sum(args)}, "util.")
|
||||
|
||||
self.assertIn("util.sum", d)
|
||||
self.assertEqual(d["util.sum"](13, -2), 11)
|
||||
|
||||
def test_init_from_dict(self):
|
||||
d = Dispatcher({
|
||||
"one": lambda: 1,
|
||||
"two": lambda: 2,
|
||||
})
|
||||
|
||||
self.assertIn("one", d)
|
||||
self.assertIn("two", d)
|
||||
|
||||
def test_del_method(self):
|
||||
d = Dispatcher()
|
||||
d["method"] = lambda: ""
|
||||
self.assertIn("method", d)
|
||||
|
||||
del d["method"]
|
||||
self.assertNotIn("method", d)
|
||||
|
||||
def test_to_dict(self):
|
||||
d = Dispatcher()
|
||||
|
||||
def func():
|
||||
return ""
|
||||
|
||||
d["method"] = func
|
||||
self.assertEqual(dict(d), {"method": func})
|
||||
|
||||
def test__getattr_function(self):
|
||||
# class
|
||||
self.assertEqual(Dispatcher._getattr_function(Math, "sum")(3, 2), 5)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math, "diff")(3, 2), 1)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math, "mul")(3, 2), 6)
|
||||
|
||||
# object
|
||||
self.assertEqual(Dispatcher._getattr_function(Math(), "sum")(3, 2), 5)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math(), "diff")(3, 2), 1)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math(), "mul")(3, 2), 6)
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Test Async JSON-RPC Response manager."""
|
||||
import unittest
|
||||
import json
|
||||
|
||||
from ..core import JSONRPC20Request, JSONRPC20Response, JSONRPC20MethodNotFound, JSONRPC20InvalidParams, JSONRPC20ServerError, JSONRPC20DispatchException
|
||||
from ..manager import AsyncJSONRPCResponseManager
|
||||
|
||||
|
||||
class TestAsyncJSONRPCResponseManager(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
def subtract(minuend, subtrahend):
|
||||
return minuend - subtrahend
|
||||
|
||||
def raise_(e: Exception):
|
||||
raise e
|
||||
|
||||
async def async_sum(*args):
|
||||
return sum(args)
|
||||
|
||||
self.dispatcher = {
|
||||
"subtract": subtract,
|
||||
"async_sum": async_sum,
|
||||
"dispatch_exception": lambda: raise_(
|
||||
JSONRPC20DispatchException(
|
||||
code=4000, message="error", data={"param": 1}
|
||||
)
|
||||
),
|
||||
"unexpected_exception": lambda: raise_(ValueError("Unexpected")),
|
||||
}
|
||||
|
||||
self.manager = AsyncJSONRPCResponseManager(dispatcher=self.dispatcher)
|
||||
|
||||
async def test_get_response(self):
|
||||
req = JSONRPC20Request("subtract", params=[5, 3], id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.result, 2)
|
||||
|
||||
async def test_get_response_notification(self):
|
||||
req = JSONRPC20Request("subtract", params=[5, 3], is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_async_response(self):
|
||||
req = JSONRPC20Request("async_sum", params=[1, 2, 3], id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.result, 6)
|
||||
|
||||
async def test_get_response_method_not_found(self):
|
||||
req = JSONRPC20Request("does_not_exist", id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error, JSONRPC20MethodNotFound())
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_method_not_found_notification(self):
|
||||
req = JSONRPC20Request("does_not_exist", is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_incorrect_arguments(self):
|
||||
req = JSONRPC20Request("subtract", params=[0], id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error, JSONRPC20InvalidParams())
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_incorrect_arguments_notification(self):
|
||||
req = JSONRPC20Request("subtract", params=[0], is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_method_expected_error(self):
|
||||
req = JSONRPC20Request("dispatch_exception", id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error.body, dict(code=4000, message="error", data={"param": 1}))
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_method_expected_error_notification(self):
|
||||
req = JSONRPC20Request("dispatch_exception", is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_method_unexpected_error(self):
|
||||
req = JSONRPC20Request("unexpected_exception", id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error, JSONRPC20ServerError())
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_method_unexpected_error_notification(self):
|
||||
req = JSONRPC20Request("unexpected_exception", is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_for_payload_batch(self):
|
||||
response = await self.manager.get_response_for_payload(json.dumps([
|
||||
{"jsonrpc": "2.0", "method": "subtract", "params": [3, 4], "id": 1},
|
||||
{"jsonrpc": "2.0"}
|
||||
]))
|
||||
self.assertEqual(
|
||||
response.body,
|
||||
[
|
||||
{"jsonrpc": "2.0", "result": -1, "id": 1},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {"code": -32600, "message": "Invalid Request"},
|
||||
"id": None
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
async def test_verbose_error(self):
|
||||
manager = AsyncJSONRPCResponseManager(
|
||||
dispatcher=self.dispatcher, is_server_error_verbose=True)
|
||||
req = JSONRPC20Request("unexpected_exception", id=0)
|
||||
res = await manager.get_response_for_request(req)
|
||||
self.assertEqual(
|
||||
res.error.data,
|
||||
{'type': 'ValueError', 'args': ('Unexpected',), 'message': 'Unexpected'}
|
||||
)
|
||||
|
||||
manager.is_server_error_verbose = False
|
||||
res = await manager.get_response_for_request(req)
|
||||
self.assertIsNone(res.error.data)
|
||||
|
||||
#############################################
|
||||
# Test examples from https://www.jsonrpc.org/specification
|
||||
#############################################
|
||||
|
||||
async def test_examples_positional_parameters(self):
|
||||
response1 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}'
|
||||
)
|
||||
self.assertEqual(response1.body, {"jsonrpc": "2.0", "result": 19, "id": 1})
|
||||
|
||||
response2 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}'
|
||||
)
|
||||
self.assertEqual(response2.body, {"jsonrpc": "2.0", "result": -19, "id": 2})
|
||||
|
||||
async def test_examples_named_parameters(self):
|
||||
response1 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}'
|
||||
)
|
||||
self.assertEqual(response1.body, {"jsonrpc": "2.0", "result": 19, "id": 3})
|
||||
|
||||
response2 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}'
|
||||
)
|
||||
self.assertEqual(response2.body, {"jsonrpc": "2.0", "result": 19, "id": 4})
|
||||
|
||||
async def test_examples_notification(self):
|
||||
response1 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}'
|
||||
)
|
||||
self.assertIsNone(response1)
|
||||
|
||||
response2 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "foobar"}'
|
||||
)
|
||||
self.assertIsNone(response2)
|
||||
|
||||
async def test_examples_rpc_call_of_nonexistent_method(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "foobar", "id": "1"}'
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"})
|
||||
|
||||
async def test_exampels_rpc_call_with_invalid_json(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]'
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_with_invalid_request_object(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": 1, "params": "bar"}'
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_batch_invalid_json(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
"""[
|
||||
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
|
||||
{"jsonrpc": "2.0", "method"
|
||||
]"""
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_with_an_empty_array(self):
|
||||
response = await self.manager.get_response_for_payload('[]')
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_with_an_invalid_batch_but_not_empty(self):
|
||||
response = await self.manager.get_response_for_payload('[1]')
|
||||
self.assertEqual(response.body, [{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}])
|
||||
|
||||
async def test_examples_rpc_call_with_invalid_batch(self):
|
||||
response = await self.manager.get_response_for_payload('[1,2,3]')
|
||||
self.assertEqual(response.body, [
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}
|
||||
])
|
||||
|
||||
async def test_examples_rpc_call_batch(self):
|
||||
dispatcher = {
|
||||
"sum": lambda *values: sum(values),
|
||||
"subtract": lambda a, b: a - b,
|
||||
"get_data": lambda: ["hello", 5],
|
||||
}
|
||||
manager = AsyncJSONRPCResponseManager(dispatcher=dispatcher)
|
||||
|
||||
response = await manager.get_response_for_payload(json.dumps([
|
||||
{"jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1"},
|
||||
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
|
||||
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"},
|
||||
{"foo": "boo"},
|
||||
{"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
|
||||
{"jsonrpc": "2.0", "method": "get_data", "id": "9"},
|
||||
]))
|
||||
|
||||
self.assertEqual(response.body, [
|
||||
{"jsonrpc": "2.0", "result": 7, "id": "1"},
|
||||
{"jsonrpc": "2.0", "result": 19, "id": "2"},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
|
||||
{"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
|
||||
])
|
||||
|
||||
async def test_examples_rpc_call_batch_all_notifications(self):
|
||||
response = await self.manager.get_response_for_payload(json.dumps([
|
||||
{"jsonrpc": "2.0", "method": "notify_sum", "params": [1, 2, 4]},
|
||||
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
|
||||
]))
|
||||
self.assertIsNone(response)
|
||||
35
.venv/lib/python3.11/site-packages/ajsonrpc/utils.py
Normal file
35
.venv/lib/python3.11/site-packages/ajsonrpc/utils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import inspect
|
||||
|
||||
|
||||
def is_invalid_params(func, *args, **kwargs):
|
||||
"""
|
||||
Method:
|
||||
Validate pre-defined criteria, if any is True - function is invalid
|
||||
0. func should be callable
|
||||
1. kwargs should not have unexpected keywords
|
||||
2. remove kwargs.keys from func.parameters
|
||||
3. number of args should be <= remaining func.parameters
|
||||
4. number of args should be >= remaining func.parameters less default
|
||||
"""
|
||||
# For builtin functions inspect.getargspec(funct) return error. If builtin
|
||||
# function generates TypeError, it is because of wrong parameters.
|
||||
if not inspect.isfunction(func):
|
||||
return True
|
||||
|
||||
signature = inspect.signature(func)
|
||||
parameters = signature.parameters
|
||||
|
||||
unexpected = set(kwargs.keys()) - set(parameters.keys())
|
||||
if len(unexpected) > 0:
|
||||
return True
|
||||
|
||||
params = [
|
||||
parameter for name, parameter in parameters.items()
|
||||
if name not in kwargs
|
||||
]
|
||||
params_required = [
|
||||
param for param in params
|
||||
if param.default is param.empty
|
||||
]
|
||||
|
||||
return not (len(params_required) <= len(args) <= len(params))
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,96 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: anyio
|
||||
Version: 4.12.1
|
||||
Summary: High-level concurrency and networking framework on top of asyncio or Trio
|
||||
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
||||
License-Expression: MIT
|
||||
Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
|
||||
Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
|
||||
Project-URL: Source code, https://github.com/agronholm/anyio
|
||||
Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Framework :: AnyIO
|
||||
Classifier: Typing :: Typed
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
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
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11"
|
||||
Requires-Dist: idna>=2.8
|
||||
Requires-Dist: typing_extensions>=4.5; python_version < "3.13"
|
||||
Provides-Extra: trio
|
||||
Requires-Dist: trio>=0.32.0; python_version >= "3.10" and extra == "trio"
|
||||
Requires-Dist: trio>=0.31.0; python_version < "3.10" and extra == "trio"
|
||||
Dynamic: license-file
|
||||
|
||||
.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
|
||||
:target: https://github.com/agronholm/anyio/actions/workflows/test.yml
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/agronholm/anyio?branch=master
|
||||
:alt: Code Coverage
|
||||
.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
|
||||
:target: https://anyio.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation
|
||||
.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
|
||||
:target: https://gitter.im/python-trio/AnyIO
|
||||
:alt: Gitter chat
|
||||
|
||||
AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
|
||||
Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
|
||||
with the native SC of Trio itself.
|
||||
|
||||
Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
|
||||
Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full
|
||||
refactoring necessary. It will blend in with the native libraries of your chosen backend.
|
||||
|
||||
To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it
|
||||
`here <https://anyio.readthedocs.io/en/stable/why.html>`_.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
View full documentation at: https://anyio.readthedocs.io/
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
AnyIO offers the following functionality:
|
||||
|
||||
* Task groups (nurseries_ in trio terminology)
|
||||
* High-level networking (TCP, UDP and UNIX sockets)
|
||||
|
||||
* `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
|
||||
3.8)
|
||||
* async/await style UDP sockets (unlike asyncio where you still have to use Transports and
|
||||
Protocols)
|
||||
|
||||
* A versatile API for byte streams and object streams
|
||||
* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
|
||||
streams)
|
||||
* Worker threads
|
||||
* Subprocesses
|
||||
* Subinterpreter support for code parallelization (on Python 3.13 and later)
|
||||
* Asynchronous file I/O (using worker threads)
|
||||
* Signal handling
|
||||
* Asynchronous version of the functools_ module
|
||||
|
||||
AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
|
||||
It even works with the popular Hypothesis_ library.
|
||||
|
||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||
.. _Trio: https://github.com/python-trio/trio
|
||||
.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
|
||||
.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
|
||||
.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
|
||||
.. _pytest: https://docs.pytest.org/en/latest/
|
||||
.. _functools: https://docs.python.org/3/library/functools.html
|
||||
.. _Hypothesis: https://hypothesis.works/
|
||||
@@ -0,0 +1,92 @@
|
||||
anyio-4.12.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
anyio-4.12.1.dist-info/METADATA,sha256=DfiDab9Tmmcfy802lOLTMEHJQShkOSbopCwqCYbLuJk,4277
|
||||
anyio-4.12.1.dist-info/RECORD,,
|
||||
anyio-4.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
||||
anyio-4.12.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
|
||||
anyio-4.12.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
|
||||
anyio-4.12.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
|
||||
anyio/__init__.py,sha256=7iDVqMUprUuKNY91FuoKqayAhR-OY136YDPI6P78HHk,6170
|
||||
anyio/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/__pycache__/from_thread.cpython-311.pyc,,
|
||||
anyio/__pycache__/functools.cpython-311.pyc,,
|
||||
anyio/__pycache__/lowlevel.cpython-311.pyc,,
|
||||
anyio/__pycache__/pytest_plugin.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_interpreter.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_process.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_thread.cpython-311.pyc,,
|
||||
anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_backends/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/_backends/__pycache__/_asyncio.cpython-311.pyc,,
|
||||
anyio/_backends/__pycache__/_trio.cpython-311.pyc,,
|
||||
anyio/_backends/_asyncio.py,sha256=xG6qv60mgGnL0mK82dxjH2b8hlkMlJ-x2BqIq3qv70Y,98863
|
||||
anyio/_backends/_trio.py,sha256=30Rctb7lm8g63ZHljVPVnj5aH-uK6oQvphjwUBoAzuI,41456
|
||||
anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_core/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_eventloop.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_exceptions.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_fileio.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_resources.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_signals.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_sockets.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_streams.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_subprocesses.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_synchronization.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_tasks.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_tempfile.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_testing.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_typedattr.cpython-311.pyc,,
|
||||
anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626
|
||||
anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215
|
||||
anyio/_core/_eventloop.py,sha256=c2EdcBX-xnKwxPcC4Pjn3_qG9I-x4IWFO2R9RqCGjM4,6448
|
||||
anyio/_core/_exceptions.py,sha256=Y3aq-Wxd7Q2HqwSg7nZPvRsHEuGazv_qeet6gqEBdPk,4407
|
||||
anyio/_core/_fileio.py,sha256=uc7t10Vb-If7GbdWM_zFf-ajUe6uek63fSt7IBLlZW0,25731
|
||||
anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435
|
||||
anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016
|
||||
anyio/_core/_sockets.py,sha256=RBXHcUqZt5gg_-OOfgHVv8uq2FSKk1uVUzTdpjBoI1o,34977
|
||||
anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806
|
||||
anyio/_core/_subprocesses.py,sha256=EXm5igL7dj55iYkPlbYVAqtbqxJxjU-6OndSTIx9SRg,8047
|
||||
anyio/_core/_synchronization.py,sha256=MgVVqFzvt580tHC31LiOcq1G6aryut--xRG4Ff8KwxQ,20869
|
||||
anyio/_core/_tasks.py,sha256=pVB7K6AAulzUM8YgXAeqNZG44nSyZ1bYJjH8GznC00I,5435
|
||||
anyio/_core/_tempfile.py,sha256=lHb7CW4FyIlpkf5ADAf4VmLHCKwEHF9nxqNyBCFFUiA,19697
|
||||
anyio/_core/_testing.py,sha256=u7MPqGXwpTxqI7hclSdNA30z2GH1Nw258uwKvy_RfBg,2340
|
||||
anyio/_core/_typedattr.py,sha256=P4ozZikn3-DbpoYcvyghS_FOYAgbmUxeoU8-L_07pZM,2508
|
||||
anyio/abc/__init__.py,sha256=6mWhcl_pGXhrgZVHP_TCfMvIXIOp9mroEFM90fYCU_U,2869
|
||||
anyio/abc/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_eventloop.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_resources.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_sockets.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_streams.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_subprocesses.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_tasks.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_testing.cpython-311.pyc,,
|
||||
anyio/abc/_eventloop.py,sha256=GlzgB3UJGgG6Kr7olpjOZ-o00PghecXuofVDQ_5611Q,10749
|
||||
anyio/abc/_resources.py,sha256=DrYvkNN1hH6Uvv5_5uKySvDsnknGVDe8FCKfko0VtN8,783
|
||||
anyio/abc/_sockets.py,sha256=ECTY0jLEF18gryANHR3vFzXzGdZ-xPwELq1QdgOb0Jo,13258
|
||||
anyio/abc/_streams.py,sha256=005GKSCXGprxnhucILboSqc2JFovECZk9m3p-qqxXVc,7640
|
||||
anyio/abc/_subprocesses.py,sha256=cumAPJTktOQtw63IqG0lDpyZqu_l1EElvQHMiwJgL08,2067
|
||||
anyio/abc/_tasks.py,sha256=KC7wrciE48AINOI-AhPutnFhe1ewfP7QnamFlDzqesQ,3721
|
||||
anyio/abc/_testing.py,sha256=tBJUzkSfOXJw23fe8qSJ03kJlShOYjjaEyFB6k6MYT8,1821
|
||||
anyio/from_thread.py,sha256=L-0w1HxJ6BSb-KuVi57k5Tkc3yzQrx3QK5tAxMPcY-0,19141
|
||||
anyio/functools.py,sha256=HWj7GBEmc0Z-mZg3uok7Z7ZJn0rEC_0Pzbt0nYUDaTQ,10973
|
||||
anyio/lowlevel.py,sha256=AyKLVK3LaWSoK39LkCKxE4_GDMLKZBNqTrLUgk63y80,5158
|
||||
anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/pytest_plugin.py,sha256=3jAFQn0jv_pyoWE2GBBlHaj9sqXj4e8vob0_hgrsXE8,10244
|
||||
anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/streams/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/buffered.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/file.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/memory.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/stapled.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/text.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/tls.cpython-311.pyc,,
|
||||
anyio/streams/buffered.py,sha256=2R3PeJhe4EXrdYqz44Y6-Eg9R6DrmlsYrP36Ir43-po,6263
|
||||
anyio/streams/file.py,sha256=4WZ7XGz5WNu39FQHvqbe__TQ0HDP9OOhgO1mk9iVpVU,4470
|
||||
anyio/streams/memory.py,sha256=F0zwzvFJKAhX_LRZGoKzzqDC2oMM-f-yyTBrEYEGOaU,10740
|
||||
anyio/streams/stapled.py,sha256=T8Xqwf8K6EgURPxbt1N4i7A8BAk-gScv-GRhjLXIf_o,4390
|
||||
anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765
|
||||
anyio/streams/tls.py,sha256=Jpxy0Mfbcp1BxHCwE-YjSSFaLnIBbnnwur-excYThs4,15368
|
||||
anyio/to_interpreter.py,sha256=_mLngrMy97TMR6VbW4Y6YzDUk9ZuPcQMPlkuyRh3C9k,7100
|
||||
anyio/to_process.py,sha256=J7gAA_YOuoHqnpDAf5fm1Qu6kOmTzdFbiDNvnV755vk,9798
|
||||
anyio/to_thread.py,sha256=menEgXYmUV7Fjg_9WqCV95P9MAtQS8BzPGGcWB_QnfQ,2687
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (80.9.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest11]
|
||||
anyio = anyio.pytest_plugin
|
||||
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Alex Grönholm
|
||||
|
||||
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 @@
|
||||
anyio
|
||||
111
.venv/lib/python3.11/site-packages/anyio/__init__.py
Normal file
111
.venv/lib/python3.11/site-packages/anyio/__init__.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._core._contextmanagers import AsyncContextManagerMixin as AsyncContextManagerMixin
|
||||
from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin
|
||||
from ._core._eventloop import current_time as current_time
|
||||
from ._core._eventloop import get_all_backends as get_all_backends
|
||||
from ._core._eventloop import get_available_backends as get_available_backends
|
||||
from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class
|
||||
from ._core._eventloop import run as run
|
||||
from ._core._eventloop import sleep as sleep
|
||||
from ._core._eventloop import sleep_forever as sleep_forever
|
||||
from ._core._eventloop import sleep_until as sleep_until
|
||||
from ._core._exceptions import BrokenResourceError as BrokenResourceError
|
||||
from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter
|
||||
from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess
|
||||
from ._core._exceptions import BusyResourceError as BusyResourceError
|
||||
from ._core._exceptions import ClosedResourceError as ClosedResourceError
|
||||
from ._core._exceptions import ConnectionFailed as ConnectionFailed
|
||||
from ._core._exceptions import DelimiterNotFound as DelimiterNotFound
|
||||
from ._core._exceptions import EndOfStream as EndOfStream
|
||||
from ._core._exceptions import IncompleteRead as IncompleteRead
|
||||
from ._core._exceptions import NoEventLoopError as NoEventLoopError
|
||||
from ._core._exceptions import RunFinishedError as RunFinishedError
|
||||
from ._core._exceptions import TypedAttributeLookupError as TypedAttributeLookupError
|
||||
from ._core._exceptions import WouldBlock as WouldBlock
|
||||
from ._core._fileio import AsyncFile as AsyncFile
|
||||
from ._core._fileio import Path as Path
|
||||
from ._core._fileio import open_file as open_file
|
||||
from ._core._fileio import wrap_file as wrap_file
|
||||
from ._core._resources import aclose_forcefully as aclose_forcefully
|
||||
from ._core._signals import open_signal_receiver as open_signal_receiver
|
||||
from ._core._sockets import TCPConnectable as TCPConnectable
|
||||
from ._core._sockets import UNIXConnectable as UNIXConnectable
|
||||
from ._core._sockets import as_connectable as as_connectable
|
||||
from ._core._sockets import connect_tcp as connect_tcp
|
||||
from ._core._sockets import connect_unix as connect_unix
|
||||
from ._core._sockets import create_connected_udp_socket as create_connected_udp_socket
|
||||
from ._core._sockets import (
|
||||
create_connected_unix_datagram_socket as create_connected_unix_datagram_socket,
|
||||
)
|
||||
from ._core._sockets import create_tcp_listener as create_tcp_listener
|
||||
from ._core._sockets import create_udp_socket as create_udp_socket
|
||||
from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_socket
|
||||
from ._core._sockets import create_unix_listener as create_unix_listener
|
||||
from ._core._sockets import getaddrinfo as getaddrinfo
|
||||
from ._core._sockets import getnameinfo as getnameinfo
|
||||
from ._core._sockets import notify_closing as notify_closing
|
||||
from ._core._sockets import wait_readable as wait_readable
|
||||
from ._core._sockets import wait_socket_readable as wait_socket_readable
|
||||
from ._core._sockets import wait_socket_writable as wait_socket_writable
|
||||
from ._core._sockets import wait_writable as wait_writable
|
||||
from ._core._streams import create_memory_object_stream as create_memory_object_stream
|
||||
from ._core._subprocesses import open_process as open_process
|
||||
from ._core._subprocesses import run_process as run_process
|
||||
from ._core._synchronization import CapacityLimiter as CapacityLimiter
|
||||
from ._core._synchronization import (
|
||||
CapacityLimiterStatistics as CapacityLimiterStatistics,
|
||||
)
|
||||
from ._core._synchronization import Condition as Condition
|
||||
from ._core._synchronization import ConditionStatistics as ConditionStatistics
|
||||
from ._core._synchronization import Event as Event
|
||||
from ._core._synchronization import EventStatistics as EventStatistics
|
||||
from ._core._synchronization import Lock as Lock
|
||||
from ._core._synchronization import LockStatistics as LockStatistics
|
||||
from ._core._synchronization import ResourceGuard as ResourceGuard
|
||||
from ._core._synchronization import Semaphore as Semaphore
|
||||
from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics
|
||||
from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED
|
||||
from ._core._tasks import CancelScope as CancelScope
|
||||
from ._core._tasks import create_task_group as create_task_group
|
||||
from ._core._tasks import current_effective_deadline as current_effective_deadline
|
||||
from ._core._tasks import fail_after as fail_after
|
||||
from ._core._tasks import move_on_after as move_on_after
|
||||
from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile
|
||||
from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile
|
||||
from ._core._tempfile import TemporaryDirectory as TemporaryDirectory
|
||||
from ._core._tempfile import TemporaryFile as TemporaryFile
|
||||
from ._core._tempfile import gettempdir as gettempdir
|
||||
from ._core._tempfile import gettempdirb as gettempdirb
|
||||
from ._core._tempfile import mkdtemp as mkdtemp
|
||||
from ._core._tempfile import mkstemp as mkstemp
|
||||
from ._core._testing import TaskInfo as TaskInfo
|
||||
from ._core._testing import get_current_task as get_current_task
|
||||
from ._core._testing import get_running_tasks as get_running_tasks
|
||||
from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked
|
||||
from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider
|
||||
from ._core._typedattr import TypedAttributeSet as TypedAttributeSet
|
||||
from ._core._typedattr import typed_attribute as typed_attribute
|
||||
|
||||
# Re-export imports so they look like they live directly in this package
|
||||
for __value in list(locals().values()):
|
||||
if getattr(__value, "__module__", "").startswith("anyio."):
|
||||
__value.__module__ = __name__
|
||||
|
||||
|
||||
del __value
|
||||
|
||||
|
||||
def __getattr__(attr: str) -> type[BrokenWorkerInterpreter]:
|
||||
"""Support deprecated aliases."""
|
||||
if attr == "BrokenWorkerIntepreter":
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"The 'BrokenWorkerIntepreter' alias is deprecated, use 'BrokenWorkerInterpreter' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return BrokenWorkerInterpreter
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
|
||||
2980
.venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py
Normal file
2980
.venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py
Normal file
File diff suppressed because it is too large
Load Diff
1346
.venv/lib/python3.11/site-packages/anyio/_backends/_trio.py
Normal file
1346
.venv/lib/python3.11/site-packages/anyio/_backends/_trio.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import FileDescriptorLike
|
||||
|
||||
_selector_lock = threading.Lock()
|
||||
_selector: Selector | None = None
|
||||
|
||||
|
||||
class Selector:
|
||||
def __init__(self) -> None:
|
||||
self._thread = threading.Thread(target=self.run, name="AnyIO socket selector")
|
||||
self._selector = DefaultSelector()
|
||||
self._send, self._receive = socket.socketpair()
|
||||
self._send.setblocking(False)
|
||||
self._receive.setblocking(False)
|
||||
# This somewhat reduces the amount of memory wasted queueing up data
|
||||
# for wakeups. With these settings, maximum number of 1-byte sends
|
||||
# before getting BlockingIOError:
|
||||
# Linux 4.8: 6
|
||||
# macOS (darwin 15.5): 1
|
||||
# Windows 10: 525347
|
||||
# Windows you're weird. (And on Windows setting SNDBUF to 0 makes send
|
||||
# blocking, even on non-blocking sockets, so don't do that.)
|
||||
self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
|
||||
self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1)
|
||||
# On Windows this is a TCP socket so this might matter. On other
|
||||
# platforms this fails b/c AF_UNIX sockets aren't actually TCP.
|
||||
try:
|
||||
self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._selector.register(self._receive, EVENT_READ)
|
||||
self._closed = False
|
||||
|
||||
def start(self) -> None:
|
||||
self._thread.start()
|
||||
threading._register_atexit(self._stop) # type: ignore[attr-defined]
|
||||
|
||||
def _stop(self) -> None:
|
||||
global _selector
|
||||
self._closed = True
|
||||
self._notify_self()
|
||||
self._send.close()
|
||||
self._thread.join()
|
||||
self._selector.unregister(self._receive)
|
||||
self._receive.close()
|
||||
self._selector.close()
|
||||
_selector = None
|
||||
assert not self._selector.get_map(), (
|
||||
"selector still has registered file descriptors after shutdown"
|
||||
)
|
||||
|
||||
def _notify_self(self) -> None:
|
||||
try:
|
||||
self._send.send(b"\x00")
|
||||
except BlockingIOError:
|
||||
pass
|
||||
|
||||
def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)})
|
||||
else:
|
||||
if EVENT_READ in key.data:
|
||||
raise ValueError(
|
||||
"this file descriptor is already registered for reading"
|
||||
)
|
||||
|
||||
key.data[EVENT_READ] = loop, callback
|
||||
self._selector.modify(fd, key.events | EVENT_READ, key.data)
|
||||
|
||||
self._notify_self()
|
||||
|
||||
def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)})
|
||||
else:
|
||||
if EVENT_WRITE in key.data:
|
||||
raise ValueError(
|
||||
"this file descriptor is already registered for writing"
|
||||
)
|
||||
|
||||
key.data[EVENT_WRITE] = loop, callback
|
||||
self._selector.modify(fd, key.events | EVENT_WRITE, key.data)
|
||||
|
||||
self._notify_self()
|
||||
|
||||
def remove_reader(self, fd: FileDescriptorLike) -> bool:
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if new_events := key.events ^ EVENT_READ:
|
||||
del key.data[EVENT_READ]
|
||||
self._selector.modify(fd, new_events, key.data)
|
||||
else:
|
||||
self._selector.unregister(fd)
|
||||
|
||||
return True
|
||||
|
||||
def remove_writer(self, fd: FileDescriptorLike) -> bool:
|
||||
try:
|
||||
key = self._selector.get_key(fd)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if new_events := key.events ^ EVENT_WRITE:
|
||||
del key.data[EVENT_WRITE]
|
||||
self._selector.modify(fd, new_events, key.data)
|
||||
else:
|
||||
self._selector.unregister(fd)
|
||||
|
||||
return True
|
||||
|
||||
def run(self) -> None:
|
||||
while not self._closed:
|
||||
for key, events in self._selector.select():
|
||||
if key.fileobj is self._receive:
|
||||
try:
|
||||
while self._receive.recv(4096):
|
||||
pass
|
||||
except BlockingIOError:
|
||||
pass
|
||||
|
||||
continue
|
||||
|
||||
if events & EVENT_READ:
|
||||
loop, callback = key.data[EVENT_READ]
|
||||
self.remove_reader(key.fd)
|
||||
try:
|
||||
loop.call_soon_threadsafe(callback)
|
||||
except RuntimeError:
|
||||
pass # the loop was already closed
|
||||
|
||||
if events & EVENT_WRITE:
|
||||
loop, callback = key.data[EVENT_WRITE]
|
||||
self.remove_writer(key.fd)
|
||||
try:
|
||||
loop.call_soon_threadsafe(callback)
|
||||
except RuntimeError:
|
||||
pass # the loop was already closed
|
||||
|
||||
|
||||
def get_selector() -> Selector:
|
||||
global _selector
|
||||
|
||||
with _selector_lock:
|
||||
if _selector is None:
|
||||
_selector = Selector()
|
||||
_selector.start()
|
||||
|
||||
return _selector
|
||||
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
||||
from inspect import isasyncgen, iscoroutine, isgenerator
|
||||
from types import TracebackType
|
||||
from typing import Protocol, TypeVar, cast, final
|
||||
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound="bool | None")
|
||||
|
||||
|
||||
class _SupportsCtxMgr(Protocol[_T_co, _ExitT_co]):
|
||||
def __contextmanager__(self) -> AbstractContextManager[_T_co, _ExitT_co]: ...
|
||||
|
||||
|
||||
class _SupportsAsyncCtxMgr(Protocol[_T_co, _ExitT_co]):
|
||||
def __asynccontextmanager__(
|
||||
self,
|
||||
) -> AbstractAsyncContextManager[_T_co, _ExitT_co]: ...
|
||||
|
||||
|
||||
class ContextManagerMixin:
|
||||
"""
|
||||
Mixin class providing context manager functionality via a generator-based
|
||||
implementation.
|
||||
|
||||
This class allows you to implement a context manager via :meth:`__contextmanager__`
|
||||
which should return a generator. The mechanics are meant to mirror those of
|
||||
:func:`@contextmanager <contextlib.contextmanager>`.
|
||||
|
||||
.. note:: Classes using this mix-in are not reentrant as context managers, meaning
|
||||
that once you enter it, you can't re-enter before first exiting it.
|
||||
|
||||
.. seealso:: :doc:`contextmanagers`
|
||||
"""
|
||||
|
||||
__cm: AbstractContextManager[object, bool | None] | None = None
|
||||
|
||||
@final
|
||||
def __enter__(self: _SupportsCtxMgr[_T_co, bool | None]) -> _T_co:
|
||||
# Needed for mypy to assume self still has the __cm member
|
||||
assert isinstance(self, ContextManagerMixin)
|
||||
if self.__cm is not None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has already been entered"
|
||||
)
|
||||
|
||||
cm = self.__contextmanager__()
|
||||
if not isinstance(cm, AbstractContextManager):
|
||||
if isgenerator(cm):
|
||||
raise TypeError(
|
||||
"__contextmanager__() returned a generator object instead of "
|
||||
"a context manager. Did you forget to add the @contextmanager "
|
||||
"decorator?"
|
||||
)
|
||||
|
||||
raise TypeError(
|
||||
f"__contextmanager__() did not return a context manager object, "
|
||||
f"but {cm.__class__!r}"
|
||||
)
|
||||
|
||||
if cm is self:
|
||||
raise TypeError(
|
||||
f"{self.__class__.__qualname__}.__contextmanager__() returned "
|
||||
f"self. Did you forget to add the @contextmanager decorator and a "
|
||||
f"'yield' statement?"
|
||||
)
|
||||
|
||||
value = cm.__enter__()
|
||||
self.__cm = cm
|
||||
return value
|
||||
|
||||
@final
|
||||
def __exit__(
|
||||
self: _SupportsCtxMgr[object, _ExitT_co],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> _ExitT_co:
|
||||
# Needed for mypy to assume self still has the __cm member
|
||||
assert isinstance(self, ContextManagerMixin)
|
||||
if self.__cm is None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has not been entered yet"
|
||||
)
|
||||
|
||||
# Prevent circular references
|
||||
cm = self.__cm
|
||||
del self.__cm
|
||||
|
||||
return cast(_ExitT_co, cm.__exit__(exc_type, exc_val, exc_tb))
|
||||
|
||||
@abstractmethod
|
||||
def __contextmanager__(self) -> AbstractContextManager[object, bool | None]:
|
||||
"""
|
||||
Implement your context manager logic here.
|
||||
|
||||
This method **must** be decorated with
|
||||
:func:`@contextmanager <contextlib.contextmanager>`.
|
||||
|
||||
.. note:: Remember that the ``yield`` will raise any exception raised in the
|
||||
enclosed context block, so use a ``finally:`` block to clean up resources!
|
||||
|
||||
:return: a context manager object
|
||||
"""
|
||||
|
||||
|
||||
class AsyncContextManagerMixin:
|
||||
"""
|
||||
Mixin class providing async context manager functionality via a generator-based
|
||||
implementation.
|
||||
|
||||
This class allows you to implement a context manager via
|
||||
:meth:`__asynccontextmanager__`. The mechanics are meant to mirror those of
|
||||
:func:`@asynccontextmanager <contextlib.asynccontextmanager>`.
|
||||
|
||||
.. note:: Classes using this mix-in are not reentrant as context managers, meaning
|
||||
that once you enter it, you can't re-enter before first exiting it.
|
||||
|
||||
.. seealso:: :doc:`contextmanagers`
|
||||
"""
|
||||
|
||||
__cm: AbstractAsyncContextManager[object, bool | None] | None = None
|
||||
|
||||
@final
|
||||
async def __aenter__(self: _SupportsAsyncCtxMgr[_T_co, bool | None]) -> _T_co:
|
||||
# Needed for mypy to assume self still has the __cm member
|
||||
assert isinstance(self, AsyncContextManagerMixin)
|
||||
if self.__cm is not None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has already been entered"
|
||||
)
|
||||
|
||||
cm = self.__asynccontextmanager__()
|
||||
if not isinstance(cm, AbstractAsyncContextManager):
|
||||
if isasyncgen(cm):
|
||||
raise TypeError(
|
||||
"__asynccontextmanager__() returned an async generator instead of "
|
||||
"an async context manager. Did you forget to add the "
|
||||
"@asynccontextmanager decorator?"
|
||||
)
|
||||
elif iscoroutine(cm):
|
||||
cm.close()
|
||||
raise TypeError(
|
||||
"__asynccontextmanager__() returned a coroutine object instead of "
|
||||
"an async context manager. Did you forget to add the "
|
||||
"@asynccontextmanager decorator and a 'yield' statement?"
|
||||
)
|
||||
|
||||
raise TypeError(
|
||||
f"__asynccontextmanager__() did not return an async context manager, "
|
||||
f"but {cm.__class__!r}"
|
||||
)
|
||||
|
||||
if cm is self:
|
||||
raise TypeError(
|
||||
f"{self.__class__.__qualname__}.__asynccontextmanager__() returned "
|
||||
f"self. Did you forget to add the @asynccontextmanager decorator and a "
|
||||
f"'yield' statement?"
|
||||
)
|
||||
|
||||
value = await cm.__aenter__()
|
||||
self.__cm = cm
|
||||
return value
|
||||
|
||||
@final
|
||||
async def __aexit__(
|
||||
self: _SupportsAsyncCtxMgr[object, _ExitT_co],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> _ExitT_co:
|
||||
assert isinstance(self, AsyncContextManagerMixin)
|
||||
if self.__cm is None:
|
||||
raise RuntimeError(
|
||||
f"this {self.__class__.__qualname__} has not been entered yet"
|
||||
)
|
||||
|
||||
# Prevent circular references
|
||||
cm = self.__cm
|
||||
del self.__cm
|
||||
|
||||
return cast(_ExitT_co, await cm.__aexit__(exc_type, exc_val, exc_tb))
|
||||
|
||||
@abstractmethod
|
||||
def __asynccontextmanager__(
|
||||
self,
|
||||
) -> AbstractAsyncContextManager[object, bool | None]:
|
||||
"""
|
||||
Implement your async context manager logic here.
|
||||
|
||||
This method **must** be decorated with
|
||||
:func:`@asynccontextmanager <contextlib.asynccontextmanager>`.
|
||||
|
||||
.. note:: Remember that the ``yield`` will raise any exception raised in the
|
||||
enclosed context block, so use a ``finally:`` block to clean up resources!
|
||||
|
||||
:return: an async context manager object
|
||||
"""
|
||||
234
.venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py
Normal file
234
.venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import Token
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from ._exceptions import NoEventLoopError
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
sniffio: Any
|
||||
try:
|
||||
import sniffio
|
||||
except ModuleNotFoundError:
|
||||
sniffio = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..abc import AsyncBackend
|
||||
|
||||
# This must be updated when new backends are introduced
|
||||
BACKENDS = "asyncio", "trio"
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
threadlocals = threading.local()
|
||||
loaded_backends: dict[str, type[AsyncBackend]] = {}
|
||||
|
||||
|
||||
def run(
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
*args: Unpack[PosArgsT],
|
||||
backend: str = "asyncio",
|
||||
backend_options: dict[str, Any] | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Run the given coroutine function in an asynchronous event loop.
|
||||
|
||||
The current thread must not be already running an event loop.
|
||||
|
||||
:param func: a coroutine function
|
||||
:param args: positional arguments to ``func``
|
||||
:param backend: name of the asynchronous event loop implementation – currently
|
||||
either ``asyncio`` or ``trio``
|
||||
:param backend_options: keyword arguments to call the backend ``run()``
|
||||
implementation with (documented :ref:`here <backend options>`)
|
||||
:return: the return value of the coroutine function
|
||||
:raises RuntimeError: if an asynchronous event loop is already running in this
|
||||
thread
|
||||
:raises LookupError: if the named backend is not found
|
||||
|
||||
"""
|
||||
if asynclib_name := current_async_library():
|
||||
raise RuntimeError(f"Already running {asynclib_name} in this thread")
|
||||
|
||||
try:
|
||||
async_backend = get_async_backend(backend)
|
||||
except ImportError as exc:
|
||||
raise LookupError(f"No such backend: {backend}") from exc
|
||||
|
||||
token = None
|
||||
if asynclib_name is None:
|
||||
# Since we're in control of the event loop, we can cache the name of the async
|
||||
# library
|
||||
token = set_current_async_library(backend)
|
||||
|
||||
try:
|
||||
backend_options = backend_options or {}
|
||||
return async_backend.run(func, args, {}, backend_options)
|
||||
finally:
|
||||
reset_current_async_library(token)
|
||||
|
||||
|
||||
async def sleep(delay: float) -> None:
|
||||
"""
|
||||
Pause the current task for the specified duration.
|
||||
|
||||
:param delay: the duration, in seconds
|
||||
|
||||
"""
|
||||
return await get_async_backend().sleep(delay)
|
||||
|
||||
|
||||
async def sleep_forever() -> None:
|
||||
"""
|
||||
Pause the current task until it's cancelled.
|
||||
|
||||
This is a shortcut for ``sleep(math.inf)``.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
"""
|
||||
await sleep(math.inf)
|
||||
|
||||
|
||||
async def sleep_until(deadline: float) -> None:
|
||||
"""
|
||||
Pause the current task until the given time.
|
||||
|
||||
:param deadline: the absolute time to wake up at (according to the internal
|
||||
monotonic clock of the event loop)
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
"""
|
||||
now = current_time()
|
||||
await sleep(max(deadline - now, 0))
|
||||
|
||||
|
||||
def current_time() -> float:
|
||||
"""
|
||||
Return the current value of the event loop's internal clock.
|
||||
|
||||
:return: the clock value (seconds)
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().current_time()
|
||||
|
||||
|
||||
def get_all_backends() -> tuple[str, ...]:
|
||||
"""Return a tuple of the names of all built-in backends."""
|
||||
return BACKENDS
|
||||
|
||||
|
||||
def get_available_backends() -> tuple[str, ...]:
|
||||
"""
|
||||
Test for the availability of built-in backends.
|
||||
|
||||
:return a tuple of the built-in backend names that were successfully imported
|
||||
|
||||
.. versionadded:: 4.12
|
||||
|
||||
"""
|
||||
available_backends: list[str] = []
|
||||
for backend_name in get_all_backends():
|
||||
try:
|
||||
get_async_backend(backend_name)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
available_backends.append(backend_name)
|
||||
|
||||
return tuple(available_backends)
|
||||
|
||||
|
||||
def get_cancelled_exc_class() -> type[BaseException]:
|
||||
"""
|
||||
Return the current async library's cancellation exception class.
|
||||
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().cancelled_exception_class()
|
||||
|
||||
|
||||
#
|
||||
# Private API
|
||||
#
|
||||
|
||||
|
||||
@contextmanager
|
||||
def claim_worker_thread(
|
||||
backend_class: type[AsyncBackend], token: object
|
||||
) -> Generator[Any, None, None]:
|
||||
from ..lowlevel import EventLoopToken
|
||||
|
||||
threadlocals.current_token = EventLoopToken(backend_class, token)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
del threadlocals.current_token
|
||||
|
||||
|
||||
def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]:
|
||||
if asynclib_name is None:
|
||||
asynclib_name = current_async_library()
|
||||
if not asynclib_name:
|
||||
raise NoEventLoopError(
|
||||
f"Not currently running on any asynchronous event loop. "
|
||||
f"Available async backends: {', '.join(get_all_backends())}"
|
||||
)
|
||||
|
||||
# We use our own dict instead of sys.modules to get the already imported back-end
|
||||
# class because the appropriate modules in sys.modules could potentially be only
|
||||
# partially initialized
|
||||
try:
|
||||
return loaded_backends[asynclib_name]
|
||||
except KeyError:
|
||||
module = import_module(f"anyio._backends._{asynclib_name}")
|
||||
loaded_backends[asynclib_name] = module.backend_class
|
||||
return module.backend_class
|
||||
|
||||
|
||||
def current_async_library() -> str | None:
|
||||
if sniffio is None:
|
||||
# If sniffio is not installed, we assume we're either running asyncio or nothing
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
return "asyncio"
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
return sniffio.current_async_library()
|
||||
except sniffio.AsyncLibraryNotFoundError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def set_current_async_library(asynclib_name: str | None) -> Token | None:
|
||||
# no-op if sniffio is not installed
|
||||
if sniffio is None:
|
||||
return None
|
||||
|
||||
return sniffio.current_async_library_cvar.set(asynclib_name)
|
||||
|
||||
|
||||
def reset_current_async_library(token: Token | None) -> None:
|
||||
if token is not None:
|
||||
sniffio.current_async_library_cvar.reset(token)
|
||||
156
.venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py
Normal file
156
.venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Generator
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
||||
class BrokenResourceError(Exception):
|
||||
"""
|
||||
Raised when trying to use a resource that has been rendered unusable due to external
|
||||
causes (e.g. a send stream whose peer has disconnected).
|
||||
"""
|
||||
|
||||
|
||||
class BrokenWorkerProcess(Exception):
|
||||
"""
|
||||
Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or
|
||||
otherwise misbehaves.
|
||||
"""
|
||||
|
||||
|
||||
class BrokenWorkerInterpreter(Exception):
|
||||
"""
|
||||
Raised by :meth:`~anyio.to_interpreter.run_sync` if an unexpected exception is
|
||||
raised in the subinterpreter.
|
||||
"""
|
||||
|
||||
def __init__(self, excinfo: Any):
|
||||
# This was adapted from concurrent.futures.interpreter.ExecutionFailed
|
||||
msg = excinfo.formatted
|
||||
if not msg:
|
||||
if excinfo.type and excinfo.msg:
|
||||
msg = f"{excinfo.type.__name__}: {excinfo.msg}"
|
||||
else:
|
||||
msg = excinfo.type.__name__ or excinfo.msg
|
||||
|
||||
super().__init__(msg)
|
||||
self.excinfo = excinfo
|
||||
|
||||
def __str__(self) -> str:
|
||||
try:
|
||||
formatted = self.excinfo.errdisplay
|
||||
except Exception:
|
||||
return super().__str__()
|
||||
else:
|
||||
return dedent(
|
||||
f"""
|
||||
{super().__str__()}
|
||||
|
||||
Uncaught in the interpreter:
|
||||
|
||||
{formatted}
|
||||
""".strip()
|
||||
)
|
||||
|
||||
|
||||
class BusyResourceError(Exception):
|
||||
"""
|
||||
Raised when two tasks are trying to read from or write to the same resource
|
||||
concurrently.
|
||||
"""
|
||||
|
||||
def __init__(self, action: str):
|
||||
super().__init__(f"Another task is already {action} this resource")
|
||||
|
||||
|
||||
class ClosedResourceError(Exception):
|
||||
"""Raised when trying to use a resource that has been closed."""
|
||||
|
||||
|
||||
class ConnectionFailed(OSError):
|
||||
"""
|
||||
Raised when a connection attempt fails.
|
||||
|
||||
.. note:: This class inherits from :exc:`OSError` for backwards compatibility.
|
||||
"""
|
||||
|
||||
|
||||
def iterate_exceptions(
|
||||
exception: BaseException,
|
||||
) -> Generator[BaseException, None, None]:
|
||||
if isinstance(exception, BaseExceptionGroup):
|
||||
for exc in exception.exceptions:
|
||||
yield from iterate_exceptions(exc)
|
||||
else:
|
||||
yield exception
|
||||
|
||||
|
||||
class DelimiterNotFound(Exception):
|
||||
"""
|
||||
Raised during
|
||||
:meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the
|
||||
maximum number of bytes has been read without the delimiter being found.
|
||||
"""
|
||||
|
||||
def __init__(self, max_bytes: int) -> None:
|
||||
super().__init__(
|
||||
f"The delimiter was not found among the first {max_bytes} bytes"
|
||||
)
|
||||
|
||||
|
||||
class EndOfStream(Exception):
|
||||
"""
|
||||
Raised when trying to read from a stream that has been closed from the other end.
|
||||
"""
|
||||
|
||||
|
||||
class IncompleteRead(Exception):
|
||||
"""
|
||||
Raised during
|
||||
:meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or
|
||||
:meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the
|
||||
connection is closed before the requested amount of bytes has been read.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"The stream was closed before the read operation could be completed"
|
||||
)
|
||||
|
||||
|
||||
class TypedAttributeLookupError(LookupError):
|
||||
"""
|
||||
Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute
|
||||
is not found and no default value has been given.
|
||||
"""
|
||||
|
||||
|
||||
class WouldBlock(Exception):
|
||||
"""Raised by ``X_nowait`` functions if ``X()`` would block."""
|
||||
|
||||
|
||||
class NoEventLoopError(RuntimeError):
|
||||
"""
|
||||
Raised by several functions that require an event loop to be running in the current
|
||||
thread when there is no running event loop.
|
||||
|
||||
This is also raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync`
|
||||
if not calling from an AnyIO worker thread, and no ``token`` was passed.
|
||||
"""
|
||||
|
||||
|
||||
class RunFinishedError(RuntimeError):
|
||||
"""
|
||||
Raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` if the event
|
||||
loop associated with the explicitly passed token has already finished.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"The event loop associated with the given token has already finished"
|
||||
)
|
||||
797
.venv/lib/python3.11/site-packages/anyio/_core/_fileio.py
Normal file
797
.venv/lib/python3.11/site-packages/anyio/_core/_fileio.py
Normal file
@@ -0,0 +1,797 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from os import PathLike
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AnyStr,
|
||||
ClassVar,
|
||||
Final,
|
||||
Generic,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .. import to_thread
|
||||
from ..abc import AsyncResource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
|
||||
else:
|
||||
ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object
|
||||
|
||||
|
||||
class AsyncFile(AsyncResource, Generic[AnyStr]):
|
||||
"""
|
||||
An asynchronous file object.
|
||||
|
||||
This class wraps a standard file object and provides async friendly versions of the
|
||||
following blocking methods (where available on the original file object):
|
||||
|
||||
* read
|
||||
* read1
|
||||
* readline
|
||||
* readlines
|
||||
* readinto
|
||||
* readinto1
|
||||
* write
|
||||
* writelines
|
||||
* truncate
|
||||
* seek
|
||||
* tell
|
||||
* flush
|
||||
|
||||
All other methods are directly passed through.
|
||||
|
||||
This class supports the asynchronous context manager protocol which closes the
|
||||
underlying file at the end of the context block.
|
||||
|
||||
This class also supports asynchronous iteration::
|
||||
|
||||
async with await open_file(...) as f:
|
||||
async for line in f:
|
||||
print(line)
|
||||
"""
|
||||
|
||||
def __init__(self, fp: IO[AnyStr]) -> None:
|
||||
self._fp: Any = fp
|
||||
|
||||
def __getattr__(self, name: str) -> object:
|
||||
return getattr(self._fp, name)
|
||||
|
||||
@property
|
||||
def wrapped(self) -> IO[AnyStr]:
|
||||
"""The wrapped file object."""
|
||||
return self._fp
|
||||
|
||||
async def __aiter__(self) -> AsyncIterator[AnyStr]:
|
||||
while True:
|
||||
line = await self.readline()
|
||||
if line:
|
||||
yield line
|
||||
else:
|
||||
break
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return await to_thread.run_sync(self._fp.close)
|
||||
|
||||
async def read(self, size: int = -1) -> AnyStr:
|
||||
return await to_thread.run_sync(self._fp.read, size)
|
||||
|
||||
async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes:
|
||||
return await to_thread.run_sync(self._fp.read1, size)
|
||||
|
||||
async def readline(self) -> AnyStr:
|
||||
return await to_thread.run_sync(self._fp.readline)
|
||||
|
||||
async def readlines(self) -> list[AnyStr]:
|
||||
return await to_thread.run_sync(self._fp.readlines)
|
||||
|
||||
async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int:
|
||||
return await to_thread.run_sync(self._fp.readinto, b)
|
||||
|
||||
async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int:
|
||||
return await to_thread.run_sync(self._fp.readinto1, b)
|
||||
|
||||
@overload
|
||||
async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ...
|
||||
|
||||
@overload
|
||||
async def write(self: AsyncFile[str], b: str) -> int: ...
|
||||
|
||||
async def write(self, b: ReadableBuffer | str) -> int:
|
||||
return await to_thread.run_sync(self._fp.write, b)
|
||||
|
||||
@overload
|
||||
async def writelines(
|
||||
self: AsyncFile[bytes], lines: Iterable[ReadableBuffer]
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ...
|
||||
|
||||
async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None:
|
||||
return await to_thread.run_sync(self._fp.writelines, lines)
|
||||
|
||||
async def truncate(self, size: int | None = None) -> int:
|
||||
return await to_thread.run_sync(self._fp.truncate, size)
|
||||
|
||||
async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
|
||||
return await to_thread.run_sync(self._fp.seek, offset, whence)
|
||||
|
||||
async def tell(self) -> int:
|
||||
return await to_thread.run_sync(self._fp.tell)
|
||||
|
||||
async def flush(self) -> None:
|
||||
return await to_thread.run_sync(self._fp.flush)
|
||||
|
||||
|
||||
@overload
|
||||
async def open_file(
|
||||
file: str | PathLike[str] | int,
|
||||
mode: OpenBinaryMode,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
closefd: bool = ...,
|
||||
opener: Callable[[str, int], int] | None = ...,
|
||||
) -> AsyncFile[bytes]: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def open_file(
|
||||
file: str | PathLike[str] | int,
|
||||
mode: OpenTextMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
closefd: bool = ...,
|
||||
opener: Callable[[str, int], int] | None = ...,
|
||||
) -> AsyncFile[str]: ...
|
||||
|
||||
|
||||
async def open_file(
|
||||
file: str | PathLike[str] | int,
|
||||
mode: str = "r",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
closefd: bool = True,
|
||||
opener: Callable[[str, int], int] | None = None,
|
||||
) -> AsyncFile[Any]:
|
||||
"""
|
||||
Open a file asynchronously.
|
||||
|
||||
The arguments are exactly the same as for the builtin :func:`open`.
|
||||
|
||||
:return: an asynchronous file object
|
||||
|
||||
"""
|
||||
fp = await to_thread.run_sync(
|
||||
open, file, mode, buffering, encoding, errors, newline, closefd, opener
|
||||
)
|
||||
return AsyncFile(fp)
|
||||
|
||||
|
||||
def wrap_file(file: IO[AnyStr]) -> AsyncFile[AnyStr]:
|
||||
"""
|
||||
Wrap an existing file as an asynchronous file.
|
||||
|
||||
:param file: an existing file-like object
|
||||
:return: an asynchronous file object
|
||||
|
||||
"""
|
||||
return AsyncFile(file)
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class _PathIterator(AsyncIterator["Path"]):
|
||||
iterator: Iterator[PathLike[str]]
|
||||
|
||||
async def __anext__(self) -> Path:
|
||||
nextval = await to_thread.run_sync(
|
||||
next, self.iterator, None, abandon_on_cancel=True
|
||||
)
|
||||
if nextval is None:
|
||||
raise StopAsyncIteration from None
|
||||
|
||||
return Path(nextval)
|
||||
|
||||
|
||||
class Path:
|
||||
"""
|
||||
An asynchronous version of :class:`pathlib.Path`.
|
||||
|
||||
This class cannot be substituted for :class:`pathlib.Path` or
|
||||
:class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike`
|
||||
interface.
|
||||
|
||||
It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for
|
||||
the deprecated :meth:`~pathlib.Path.link_to` method.
|
||||
|
||||
Some methods may be unavailable or have limited functionality, based on the Python
|
||||
version:
|
||||
|
||||
* :meth:`~pathlib.Path.copy` (available on Python 3.14 or later)
|
||||
* :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later)
|
||||
* :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later)
|
||||
* :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later)
|
||||
* :attr:`~pathlib.Path.info` (available on Python 3.14 or later)
|
||||
* :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later)
|
||||
* :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only
|
||||
available on Python 3.13 or later)
|
||||
* :meth:`~pathlib.Path.move` (available on Python 3.14 or later)
|
||||
* :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later)
|
||||
* :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available
|
||||
on Python 3.12 or later)
|
||||
* :meth:`~pathlib.Path.walk` (available on Python 3.12 or later)
|
||||
|
||||
Any methods that do disk I/O need to be awaited on. These methods are:
|
||||
|
||||
* :meth:`~pathlib.Path.absolute`
|
||||
* :meth:`~pathlib.Path.chmod`
|
||||
* :meth:`~pathlib.Path.cwd`
|
||||
* :meth:`~pathlib.Path.exists`
|
||||
* :meth:`~pathlib.Path.expanduser`
|
||||
* :meth:`~pathlib.Path.group`
|
||||
* :meth:`~pathlib.Path.hardlink_to`
|
||||
* :meth:`~pathlib.Path.home`
|
||||
* :meth:`~pathlib.Path.is_block_device`
|
||||
* :meth:`~pathlib.Path.is_char_device`
|
||||
* :meth:`~pathlib.Path.is_dir`
|
||||
* :meth:`~pathlib.Path.is_fifo`
|
||||
* :meth:`~pathlib.Path.is_file`
|
||||
* :meth:`~pathlib.Path.is_junction`
|
||||
* :meth:`~pathlib.Path.is_mount`
|
||||
* :meth:`~pathlib.Path.is_socket`
|
||||
* :meth:`~pathlib.Path.is_symlink`
|
||||
* :meth:`~pathlib.Path.lchmod`
|
||||
* :meth:`~pathlib.Path.lstat`
|
||||
* :meth:`~pathlib.Path.mkdir`
|
||||
* :meth:`~pathlib.Path.open`
|
||||
* :meth:`~pathlib.Path.owner`
|
||||
* :meth:`~pathlib.Path.read_bytes`
|
||||
* :meth:`~pathlib.Path.read_text`
|
||||
* :meth:`~pathlib.Path.readlink`
|
||||
* :meth:`~pathlib.Path.rename`
|
||||
* :meth:`~pathlib.Path.replace`
|
||||
* :meth:`~pathlib.Path.resolve`
|
||||
* :meth:`~pathlib.Path.rmdir`
|
||||
* :meth:`~pathlib.Path.samefile`
|
||||
* :meth:`~pathlib.Path.stat`
|
||||
* :meth:`~pathlib.Path.symlink_to`
|
||||
* :meth:`~pathlib.Path.touch`
|
||||
* :meth:`~pathlib.Path.unlink`
|
||||
* :meth:`~pathlib.Path.walk`
|
||||
* :meth:`~pathlib.Path.write_bytes`
|
||||
* :meth:`~pathlib.Path.write_text`
|
||||
|
||||
Additionally, the following methods return an async iterator yielding
|
||||
:class:`~.Path` objects:
|
||||
|
||||
* :meth:`~pathlib.Path.glob`
|
||||
* :meth:`~pathlib.Path.iterdir`
|
||||
* :meth:`~pathlib.Path.rglob`
|
||||
"""
|
||||
|
||||
__slots__ = "_path", "__weakref__"
|
||||
|
||||
__weakref__: Any
|
||||
|
||||
def __init__(self, *args: str | PathLike[str]) -> None:
|
||||
self._path: Final[pathlib.Path] = pathlib.Path(*args)
|
||||
|
||||
def __fspath__(self) -> str:
|
||||
return self._path.__fspath__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._path.__str__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.as_posix()!r})"
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return self._path.__bytes__()
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self._path.__hash__()
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__eq__(target)
|
||||
|
||||
def __lt__(self, other: pathlib.PurePath | Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__lt__(target)
|
||||
|
||||
def __le__(self, other: pathlib.PurePath | Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__le__(target)
|
||||
|
||||
def __gt__(self, other: pathlib.PurePath | Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__gt__(target)
|
||||
|
||||
def __ge__(self, other: pathlib.PurePath | Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__ge__(target)
|
||||
|
||||
def __truediv__(self, other: str | PathLike[str]) -> Path:
|
||||
return Path(self._path / other)
|
||||
|
||||
def __rtruediv__(self, other: str | PathLike[str]) -> Path:
|
||||
return Path(other) / self
|
||||
|
||||
@property
|
||||
def parts(self) -> tuple[str, ...]:
|
||||
return self._path.parts
|
||||
|
||||
@property
|
||||
def drive(self) -> str:
|
||||
return self._path.drive
|
||||
|
||||
@property
|
||||
def root(self) -> str:
|
||||
return self._path.root
|
||||
|
||||
@property
|
||||
def anchor(self) -> str:
|
||||
return self._path.anchor
|
||||
|
||||
@property
|
||||
def parents(self) -> Sequence[Path]:
|
||||
return tuple(Path(p) for p in self._path.parents)
|
||||
|
||||
@property
|
||||
def parent(self) -> Path:
|
||||
return Path(self._path.parent)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._path.name
|
||||
|
||||
@property
|
||||
def suffix(self) -> str:
|
||||
return self._path.suffix
|
||||
|
||||
@property
|
||||
def suffixes(self) -> list[str]:
|
||||
return self._path.suffixes
|
||||
|
||||
@property
|
||||
def stem(self) -> str:
|
||||
return self._path.stem
|
||||
|
||||
async def absolute(self) -> Path:
|
||||
path = await to_thread.run_sync(self._path.absolute)
|
||||
return Path(path)
|
||||
|
||||
def as_posix(self) -> str:
|
||||
return self._path.as_posix()
|
||||
|
||||
def as_uri(self) -> str:
|
||||
return self._path.as_uri()
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
parser: ClassVar[ModuleType] = pathlib.Path.parser
|
||||
|
||||
@classmethod
|
||||
def from_uri(cls, uri: str) -> Path:
|
||||
return Path(pathlib.Path.from_uri(uri))
|
||||
|
||||
def full_match(
|
||||
self, path_pattern: str, *, case_sensitive: bool | None = None
|
||||
) -> bool:
|
||||
return self._path.full_match(path_pattern, case_sensitive=case_sensitive)
|
||||
|
||||
def match(
|
||||
self, path_pattern: str, *, case_sensitive: bool | None = None
|
||||
) -> bool:
|
||||
return self._path.match(path_pattern, case_sensitive=case_sensitive)
|
||||
else:
|
||||
|
||||
def match(self, path_pattern: str) -> bool:
|
||||
return self._path.match(path_pattern)
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
|
||||
@property
|
||||
def info(self) -> Any: # TODO: add return type annotation when Typeshed gets it
|
||||
return self._path.info
|
||||
|
||||
async def copy(
|
||||
self,
|
||||
target: str | os.PathLike[str],
|
||||
*,
|
||||
follow_symlinks: bool = True,
|
||||
preserve_metadata: bool = False,
|
||||
) -> Path:
|
||||
func = partial(
|
||||
self._path.copy,
|
||||
follow_symlinks=follow_symlinks,
|
||||
preserve_metadata=preserve_metadata,
|
||||
)
|
||||
return Path(await to_thread.run_sync(func, pathlib.Path(target)))
|
||||
|
||||
async def copy_into(
|
||||
self,
|
||||
target_dir: str | os.PathLike[str],
|
||||
*,
|
||||
follow_symlinks: bool = True,
|
||||
preserve_metadata: bool = False,
|
||||
) -> Path:
|
||||
func = partial(
|
||||
self._path.copy_into,
|
||||
follow_symlinks=follow_symlinks,
|
||||
preserve_metadata=preserve_metadata,
|
||||
)
|
||||
return Path(await to_thread.run_sync(func, pathlib.Path(target_dir)))
|
||||
|
||||
async def move(self, target: str | os.PathLike[str]) -> Path:
|
||||
# Upstream does not handle anyio.Path properly as a PathLike
|
||||
target = pathlib.Path(target)
|
||||
return Path(await to_thread.run_sync(self._path.move, target))
|
||||
|
||||
async def move_into(
|
||||
self,
|
||||
target_dir: str | os.PathLike[str],
|
||||
) -> Path:
|
||||
return Path(await to_thread.run_sync(self._path.move_into, target_dir))
|
||||
|
||||
def is_relative_to(self, other: str | PathLike[str]) -> bool:
|
||||
try:
|
||||
self.relative_to(other)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None:
|
||||
func = partial(os.chmod, follow_symlinks=follow_symlinks)
|
||||
return await to_thread.run_sync(func, self._path, mode)
|
||||
|
||||
@classmethod
|
||||
async def cwd(cls) -> Path:
|
||||
path = await to_thread.run_sync(pathlib.Path.cwd)
|
||||
return cls(path)
|
||||
|
||||
async def exists(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.exists, abandon_on_cancel=True)
|
||||
|
||||
async def expanduser(self) -> Path:
|
||||
return Path(
|
||||
await to_thread.run_sync(self._path.expanduser, abandon_on_cancel=True)
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# Python 3.11 and earlier
|
||||
def glob(self, pattern: str) -> AsyncIterator[Path]:
|
||||
gen = self._path.glob(pattern)
|
||||
return _PathIterator(gen)
|
||||
elif (3, 12) <= sys.version_info < (3, 13):
|
||||
# changed in Python 3.12:
|
||||
# - The case_sensitive parameter was added.
|
||||
def glob(
|
||||
self,
|
||||
pattern: str,
|
||||
*,
|
||||
case_sensitive: bool | None = None,
|
||||
) -> AsyncIterator[Path]:
|
||||
gen = self._path.glob(pattern, case_sensitive=case_sensitive)
|
||||
return _PathIterator(gen)
|
||||
elif sys.version_info >= (3, 13):
|
||||
# Changed in Python 3.13:
|
||||
# - The recurse_symlinks parameter was added.
|
||||
# - The pattern parameter accepts a path-like object.
|
||||
def glob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block
|
||||
self,
|
||||
pattern: str | PathLike[str],
|
||||
*,
|
||||
case_sensitive: bool | None = None,
|
||||
recurse_symlinks: bool = False,
|
||||
) -> AsyncIterator[Path]:
|
||||
gen = self._path.glob(
|
||||
pattern, # type: ignore[arg-type]
|
||||
case_sensitive=case_sensitive,
|
||||
recurse_symlinks=recurse_symlinks,
|
||||
)
|
||||
return _PathIterator(gen)
|
||||
|
||||
async def group(self) -> str:
|
||||
return await to_thread.run_sync(self._path.group, abandon_on_cancel=True)
|
||||
|
||||
async def hardlink_to(
|
||||
self, target: str | bytes | PathLike[str] | PathLike[bytes]
|
||||
) -> None:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(os.link, target, self)
|
||||
|
||||
@classmethod
|
||||
async def home(cls) -> Path:
|
||||
home_path = await to_thread.run_sync(pathlib.Path.home)
|
||||
return cls(home_path)
|
||||
|
||||
def is_absolute(self) -> bool:
|
||||
return self._path.is_absolute()
|
||||
|
||||
async def is_block_device(self) -> bool:
|
||||
return await to_thread.run_sync(
|
||||
self._path.is_block_device, abandon_on_cancel=True
|
||||
)
|
||||
|
||||
async def is_char_device(self) -> bool:
|
||||
return await to_thread.run_sync(
|
||||
self._path.is_char_device, abandon_on_cancel=True
|
||||
)
|
||||
|
||||
async def is_dir(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_dir, abandon_on_cancel=True)
|
||||
|
||||
async def is_fifo(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_fifo, abandon_on_cancel=True)
|
||||
|
||||
async def is_file(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_file, abandon_on_cancel=True)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
async def is_junction(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_junction)
|
||||
|
||||
async def is_mount(self) -> bool:
|
||||
return await to_thread.run_sync(
|
||||
os.path.ismount, self._path, abandon_on_cancel=True
|
||||
)
|
||||
|
||||
def is_reserved(self) -> bool:
|
||||
return self._path.is_reserved()
|
||||
|
||||
async def is_socket(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_socket, abandon_on_cancel=True)
|
||||
|
||||
async def is_symlink(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_symlink, abandon_on_cancel=True)
|
||||
|
||||
async def iterdir(self) -> AsyncIterator[Path]:
|
||||
gen = (
|
||||
self._path.iterdir()
|
||||
if sys.version_info < (3, 13)
|
||||
else await to_thread.run_sync(self._path.iterdir, abandon_on_cancel=True)
|
||||
)
|
||||
async for path in _PathIterator(gen):
|
||||
yield path
|
||||
|
||||
def joinpath(self, *args: str | PathLike[str]) -> Path:
|
||||
return Path(self._path.joinpath(*args))
|
||||
|
||||
async def lchmod(self, mode: int) -> None:
|
||||
await to_thread.run_sync(self._path.lchmod, mode)
|
||||
|
||||
async def lstat(self) -> os.stat_result:
|
||||
return await to_thread.run_sync(self._path.lstat, abandon_on_cancel=True)
|
||||
|
||||
async def mkdir(
|
||||
self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False
|
||||
) -> None:
|
||||
await to_thread.run_sync(self._path.mkdir, mode, parents, exist_ok)
|
||||
|
||||
@overload
|
||||
async def open(
|
||||
self,
|
||||
mode: OpenBinaryMode,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
) -> AsyncFile[bytes]: ...
|
||||
|
||||
@overload
|
||||
async def open(
|
||||
self,
|
||||
mode: OpenTextMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
) -> AsyncFile[str]: ...
|
||||
|
||||
async def open(
|
||||
self,
|
||||
mode: str = "r",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
) -> AsyncFile[Any]:
|
||||
fp = await to_thread.run_sync(
|
||||
self._path.open, mode, buffering, encoding, errors, newline
|
||||
)
|
||||
return AsyncFile(fp)
|
||||
|
||||
async def owner(self) -> str:
|
||||
return await to_thread.run_sync(self._path.owner, abandon_on_cancel=True)
|
||||
|
||||
async def read_bytes(self) -> bytes:
|
||||
return await to_thread.run_sync(self._path.read_bytes)
|
||||
|
||||
async def read_text(
|
||||
self, encoding: str | None = None, errors: str | None = None
|
||||
) -> str:
|
||||
return await to_thread.run_sync(self._path.read_text, encoding, errors)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
def relative_to(
|
||||
self, *other: str | PathLike[str], walk_up: bool = False
|
||||
) -> Path:
|
||||
# relative_to() should work with any PathLike but it doesn't
|
||||
others = [pathlib.Path(other) for other in other]
|
||||
return Path(self._path.relative_to(*others, walk_up=walk_up))
|
||||
|
||||
else:
|
||||
|
||||
def relative_to(self, *other: str | PathLike[str]) -> Path:
|
||||
return Path(self._path.relative_to(*other))
|
||||
|
||||
async def readlink(self) -> Path:
|
||||
target = await to_thread.run_sync(os.readlink, self._path)
|
||||
return Path(target)
|
||||
|
||||
async def rename(self, target: str | pathlib.PurePath | Path) -> Path:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(self._path.rename, target)
|
||||
return Path(target)
|
||||
|
||||
async def replace(self, target: str | pathlib.PurePath | Path) -> Path:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(self._path.replace, target)
|
||||
return Path(target)
|
||||
|
||||
async def resolve(self, strict: bool = False) -> Path:
|
||||
func = partial(self._path.resolve, strict=strict)
|
||||
return Path(await to_thread.run_sync(func, abandon_on_cancel=True))
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# Pre Python 3.12
|
||||
def rglob(self, pattern: str) -> AsyncIterator[Path]:
|
||||
gen = self._path.rglob(pattern)
|
||||
return _PathIterator(gen)
|
||||
elif (3, 12) <= sys.version_info < (3, 13):
|
||||
# Changed in Python 3.12:
|
||||
# - The case_sensitive parameter was added.
|
||||
def rglob(
|
||||
self, pattern: str, *, case_sensitive: bool | None = None
|
||||
) -> AsyncIterator[Path]:
|
||||
gen = self._path.rglob(pattern, case_sensitive=case_sensitive)
|
||||
return _PathIterator(gen)
|
||||
elif sys.version_info >= (3, 13):
|
||||
# Changed in Python 3.13:
|
||||
# - The recurse_symlinks parameter was added.
|
||||
# - The pattern parameter accepts a path-like object.
|
||||
def rglob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block
|
||||
self,
|
||||
pattern: str | PathLike[str],
|
||||
*,
|
||||
case_sensitive: bool | None = None,
|
||||
recurse_symlinks: bool = False,
|
||||
) -> AsyncIterator[Path]:
|
||||
gen = self._path.rglob(
|
||||
pattern, # type: ignore[arg-type]
|
||||
case_sensitive=case_sensitive,
|
||||
recurse_symlinks=recurse_symlinks,
|
||||
)
|
||||
return _PathIterator(gen)
|
||||
|
||||
async def rmdir(self) -> None:
|
||||
await to_thread.run_sync(self._path.rmdir)
|
||||
|
||||
async def samefile(self, other_path: str | PathLike[str]) -> bool:
|
||||
if isinstance(other_path, Path):
|
||||
other_path = other_path._path
|
||||
|
||||
return await to_thread.run_sync(
|
||||
self._path.samefile, other_path, abandon_on_cancel=True
|
||||
)
|
||||
|
||||
async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result:
|
||||
func = partial(os.stat, follow_symlinks=follow_symlinks)
|
||||
return await to_thread.run_sync(func, self._path, abandon_on_cancel=True)
|
||||
|
||||
async def symlink_to(
|
||||
self,
|
||||
target: str | bytes | PathLike[str] | PathLike[bytes],
|
||||
target_is_directory: bool = False,
|
||||
) -> None:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(self._path.symlink_to, target, target_is_directory)
|
||||
|
||||
async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
|
||||
await to_thread.run_sync(self._path.touch, mode, exist_ok)
|
||||
|
||||
async def unlink(self, missing_ok: bool = False) -> None:
|
||||
try:
|
||||
await to_thread.run_sync(self._path.unlink)
|
||||
except FileNotFoundError:
|
||||
if not missing_ok:
|
||||
raise
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
async def walk(
|
||||
self,
|
||||
top_down: bool = True,
|
||||
on_error: Callable[[OSError], object] | None = None,
|
||||
follow_symlinks: bool = False,
|
||||
) -> AsyncIterator[tuple[Path, list[str], list[str]]]:
|
||||
def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None:
|
||||
try:
|
||||
return next(gen)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
gen = self._path.walk(top_down, on_error, follow_symlinks)
|
||||
while True:
|
||||
value = await to_thread.run_sync(get_next_value)
|
||||
if value is None:
|
||||
return
|
||||
|
||||
root, dirs, paths = value
|
||||
yield Path(root), dirs, paths
|
||||
|
||||
def with_name(self, name: str) -> Path:
|
||||
return Path(self._path.with_name(name))
|
||||
|
||||
def with_stem(self, stem: str) -> Path:
|
||||
return Path(self._path.with_name(stem + self._path.suffix))
|
||||
|
||||
def with_suffix(self, suffix: str) -> Path:
|
||||
return Path(self._path.with_suffix(suffix))
|
||||
|
||||
def with_segments(self, *pathsegments: str | PathLike[str]) -> Path:
|
||||
return Path(*pathsegments)
|
||||
|
||||
async def write_bytes(self, data: bytes) -> int:
|
||||
return await to_thread.run_sync(self._path.write_bytes, data)
|
||||
|
||||
async def write_text(
|
||||
self,
|
||||
data: str,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
) -> int:
|
||||
# Path.write_text() does not support the "newline" parameter before Python 3.10
|
||||
def sync_write_text() -> int:
|
||||
with self._path.open(
|
||||
"w", encoding=encoding, errors=errors, newline=newline
|
||||
) as fp:
|
||||
return fp.write(data)
|
||||
|
||||
return await to_thread.run_sync(sync_write_text)
|
||||
|
||||
|
||||
PathLike.register(Path)
|
||||
18
.venv/lib/python3.11/site-packages/anyio/_core/_resources.py
Normal file
18
.venv/lib/python3.11/site-packages/anyio/_core/_resources.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..abc import AsyncResource
|
||||
from ._tasks import CancelScope
|
||||
|
||||
|
||||
async def aclose_forcefully(resource: AsyncResource) -> None:
|
||||
"""
|
||||
Close an asynchronous resource in a cancelled scope.
|
||||
|
||||
Doing this closes the resource without waiting on anything.
|
||||
|
||||
:param resource: the resource to close
|
||||
|
||||
"""
|
||||
with CancelScope() as scope:
|
||||
scope.cancel()
|
||||
await resource.aclose()
|
||||
29
.venv/lib/python3.11/site-packages/anyio/_core/_signals.py
Normal file
29
.venv/lib/python3.11/site-packages/anyio/_core/_signals.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import AbstractContextManager
|
||||
from signal import Signals
|
||||
|
||||
from ._eventloop import get_async_backend
|
||||
|
||||
|
||||
def open_signal_receiver(
|
||||
*signals: Signals,
|
||||
) -> AbstractContextManager[AsyncIterator[Signals]]:
|
||||
"""
|
||||
Start receiving operating system signals.
|
||||
|
||||
:param signals: signals to receive (e.g. ``signal.SIGINT``)
|
||||
:return: an asynchronous context manager for an asynchronous iterator which yields
|
||||
signal numbers
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
.. warning:: Windows does not support signals natively so it is best to avoid
|
||||
relying on this in cross-platform applications.
|
||||
|
||||
.. warning:: On asyncio, this permanently replaces any previous signal handler for
|
||||
the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`.
|
||||
|
||||
"""
|
||||
return get_async_backend().open_signal_receiver(*signals)
|
||||
1003
.venv/lib/python3.11/site-packages/anyio/_core/_sockets.py
Normal file
1003
.venv/lib/python3.11/site-packages/anyio/_core/_sockets.py
Normal file
File diff suppressed because it is too large
Load Diff
52
.venv/lib/python3.11/site-packages/anyio/_core/_streams.py
Normal file
52
.venv/lib/python3.11/site-packages/anyio/_core/_streams.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TypeVar
|
||||
from warnings import warn
|
||||
|
||||
from ..streams.memory import (
|
||||
MemoryObjectReceiveStream,
|
||||
MemoryObjectSendStream,
|
||||
_MemoryObjectStreamState,
|
||||
)
|
||||
|
||||
T_Item = TypeVar("T_Item")
|
||||
|
||||
|
||||
class create_memory_object_stream(
|
||||
tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]],
|
||||
):
|
||||
"""
|
||||
Create a memory object stream.
|
||||
|
||||
The stream's item type can be annotated like
|
||||
:func:`create_memory_object_stream[T_Item]`.
|
||||
|
||||
:param max_buffer_size: number of items held in the buffer until ``send()`` starts
|
||||
blocking
|
||||
:param item_type: old way of marking the streams with the right generic type for
|
||||
static typing (does nothing on AnyIO 4)
|
||||
|
||||
.. deprecated:: 4.0
|
||||
Use ``create_memory_object_stream[YourItemType](...)`` instead.
|
||||
:return: a tuple of (send stream, receive stream)
|
||||
|
||||
"""
|
||||
|
||||
def __new__( # type: ignore[misc]
|
||||
cls, max_buffer_size: float = 0, item_type: object = None
|
||||
) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]:
|
||||
if max_buffer_size != math.inf and not isinstance(max_buffer_size, int):
|
||||
raise ValueError("max_buffer_size must be either an integer or math.inf")
|
||||
if max_buffer_size < 0:
|
||||
raise ValueError("max_buffer_size cannot be negative")
|
||||
if item_type is not None:
|
||||
warn(
|
||||
"The item_type argument has been deprecated in AnyIO 4.0. "
|
||||
"Use create_memory_object_stream[YourItemType](...) instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
state = _MemoryObjectStreamState[T_Item](max_buffer_size)
|
||||
return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state))
|
||||
202
.venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py
Normal file
202
.venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Iterable, Mapping, Sequence
|
||||
from io import BytesIO
|
||||
from os import PathLike
|
||||
from subprocess import PIPE, CalledProcessError, CompletedProcess
|
||||
from typing import IO, Any, Union, cast
|
||||
|
||||
from ..abc import Process
|
||||
from ._eventloop import get_async_backend
|
||||
from ._tasks import create_task_group
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"]
|
||||
|
||||
|
||||
async def run_process(
|
||||
command: StrOrBytesPath | Sequence[StrOrBytesPath],
|
||||
*,
|
||||
input: bytes | None = None,
|
||||
stdin: int | IO[Any] | None = None,
|
||||
stdout: int | IO[Any] | None = PIPE,
|
||||
stderr: int | IO[Any] | None = PIPE,
|
||||
check: bool = True,
|
||||
cwd: StrOrBytesPath | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
startupinfo: Any = None,
|
||||
creationflags: int = 0,
|
||||
start_new_session: bool = False,
|
||||
pass_fds: Sequence[int] = (),
|
||||
user: str | int | None = None,
|
||||
group: str | int | None = None,
|
||||
extra_groups: Iterable[str | int] | None = None,
|
||||
umask: int = -1,
|
||||
) -> CompletedProcess[bytes]:
|
||||
"""
|
||||
Run an external command in a subprocess and wait until it completes.
|
||||
|
||||
.. seealso:: :func:`subprocess.run`
|
||||
|
||||
:param command: either a string to pass to the shell, or an iterable of strings
|
||||
containing the executable name or path and its arguments
|
||||
:param input: bytes passed to the standard input of the subprocess
|
||||
:param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
a file-like object, or `None`; ``input`` overrides this
|
||||
:param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
a file-like object, or `None`
|
||||
:param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
:data:`subprocess.STDOUT`, a file-like object, or `None`
|
||||
:param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the
|
||||
process terminates with a return code other than 0
|
||||
:param cwd: If not ``None``, change the working directory to this before running the
|
||||
command
|
||||
:param env: if not ``None``, this mapping replaces the inherited environment
|
||||
variables from the parent process
|
||||
:param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used
|
||||
to specify process startup parameters (Windows only)
|
||||
:param creationflags: flags that can be used to control the creation of the
|
||||
subprocess (see :class:`subprocess.Popen` for the specifics)
|
||||
:param start_new_session: if ``true`` the setsid() system call will be made in the
|
||||
child process prior to the execution of the subprocess. (POSIX only)
|
||||
:param pass_fds: sequence of file descriptors to keep open between the parent and
|
||||
child processes. (POSIX only)
|
||||
:param user: effective user to run the process as (Python >= 3.9, POSIX only)
|
||||
:param group: effective group to run the process as (Python >= 3.9, POSIX only)
|
||||
:param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9,
|
||||
POSIX only)
|
||||
:param umask: if not negative, this umask is applied in the child process before
|
||||
running the given command (Python >= 3.9, POSIX only)
|
||||
:return: an object representing the completed process
|
||||
:raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process
|
||||
exits with a nonzero return code
|
||||
|
||||
"""
|
||||
|
||||
async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None:
|
||||
buffer = BytesIO()
|
||||
async for chunk in stream:
|
||||
buffer.write(chunk)
|
||||
|
||||
stream_contents[index] = buffer.getvalue()
|
||||
|
||||
if stdin is not None and input is not None:
|
||||
raise ValueError("only one of stdin and input is allowed")
|
||||
|
||||
async with await open_process(
|
||||
command,
|
||||
stdin=PIPE if input else stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
start_new_session=start_new_session,
|
||||
pass_fds=pass_fds,
|
||||
user=user,
|
||||
group=group,
|
||||
extra_groups=extra_groups,
|
||||
umask=umask,
|
||||
) as process:
|
||||
stream_contents: list[bytes | None] = [None, None]
|
||||
async with create_task_group() as tg:
|
||||
if process.stdout:
|
||||
tg.start_soon(drain_stream, process.stdout, 0)
|
||||
|
||||
if process.stderr:
|
||||
tg.start_soon(drain_stream, process.stderr, 1)
|
||||
|
||||
if process.stdin and input:
|
||||
await process.stdin.send(input)
|
||||
await process.stdin.aclose()
|
||||
|
||||
await process.wait()
|
||||
|
||||
output, errors = stream_contents
|
||||
if check and process.returncode != 0:
|
||||
raise CalledProcessError(cast(int, process.returncode), command, output, errors)
|
||||
|
||||
return CompletedProcess(command, cast(int, process.returncode), output, errors)
|
||||
|
||||
|
||||
async def open_process(
|
||||
command: StrOrBytesPath | Sequence[StrOrBytesPath],
|
||||
*,
|
||||
stdin: int | IO[Any] | None = PIPE,
|
||||
stdout: int | IO[Any] | None = PIPE,
|
||||
stderr: int | IO[Any] | None = PIPE,
|
||||
cwd: StrOrBytesPath | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
startupinfo: Any = None,
|
||||
creationflags: int = 0,
|
||||
start_new_session: bool = False,
|
||||
pass_fds: Sequence[int] = (),
|
||||
user: str | int | None = None,
|
||||
group: str | int | None = None,
|
||||
extra_groups: Iterable[str | int] | None = None,
|
||||
umask: int = -1,
|
||||
) -> Process:
|
||||
"""
|
||||
Start an external command in a subprocess.
|
||||
|
||||
.. seealso:: :class:`subprocess.Popen`
|
||||
|
||||
:param command: either a string to pass to the shell, or an iterable of strings
|
||||
containing the executable name or path and its arguments
|
||||
:param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a
|
||||
file-like object, or ``None``
|
||||
:param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
a file-like object, or ``None``
|
||||
:param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
:data:`subprocess.STDOUT`, a file-like object, or ``None``
|
||||
:param cwd: If not ``None``, the working directory is changed before executing
|
||||
:param env: If env is not ``None``, it must be a mapping that defines the
|
||||
environment variables for the new process
|
||||
:param creationflags: flags that can be used to control the creation of the
|
||||
subprocess (see :class:`subprocess.Popen` for the specifics)
|
||||
:param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used
|
||||
to specify process startup parameters (Windows only)
|
||||
:param start_new_session: if ``true`` the setsid() system call will be made in the
|
||||
child process prior to the execution of the subprocess. (POSIX only)
|
||||
:param pass_fds: sequence of file descriptors to keep open between the parent and
|
||||
child processes. (POSIX only)
|
||||
:param user: effective user to run the process as (POSIX only)
|
||||
:param group: effective group to run the process as (POSIX only)
|
||||
:param extra_groups: supplementary groups to set in the subprocess (POSIX only)
|
||||
:param umask: if not negative, this umask is applied in the child process before
|
||||
running the given command (POSIX only)
|
||||
:return: an asynchronous process object
|
||||
|
||||
"""
|
||||
kwargs: dict[str, Any] = {}
|
||||
if user is not None:
|
||||
kwargs["user"] = user
|
||||
|
||||
if group is not None:
|
||||
kwargs["group"] = group
|
||||
|
||||
if extra_groups is not None:
|
||||
kwargs["extra_groups"] = group
|
||||
|
||||
if umask >= 0:
|
||||
kwargs["umask"] = umask
|
||||
|
||||
return await get_async_backend().open_process(
|
||||
command,
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
start_new_session=start_new_session,
|
||||
pass_fds=pass_fds,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,753 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import TypeVar
|
||||
|
||||
from ..lowlevel import checkpoint_if_cancelled
|
||||
from ._eventloop import get_async_backend
|
||||
from ._exceptions import BusyResourceError, NoEventLoopError
|
||||
from ._tasks import CancelScope
|
||||
from ._testing import TaskInfo, get_current_task
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventStatistics:
|
||||
"""
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait`
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapacityLimiterStatistics:
|
||||
"""
|
||||
:ivar int borrowed_tokens: number of tokens currently borrowed by tasks
|
||||
:ivar float total_tokens: total number of available tokens
|
||||
:ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from
|
||||
this limiter
|
||||
:ivar int tasks_waiting: number of tasks waiting on
|
||||
:meth:`~.CapacityLimiter.acquire` or
|
||||
:meth:`~.CapacityLimiter.acquire_on_behalf_of`
|
||||
"""
|
||||
|
||||
borrowed_tokens: int
|
||||
total_tokens: float
|
||||
borrowers: tuple[object, ...]
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LockStatistics:
|
||||
"""
|
||||
:ivar bool locked: flag indicating if this lock is locked or not
|
||||
:ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the
|
||||
lock is not held by any task)
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire`
|
||||
"""
|
||||
|
||||
locked: bool
|
||||
owner: TaskInfo | None
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConditionStatistics:
|
||||
"""
|
||||
:ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait`
|
||||
:ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying
|
||||
:class:`~.Lock`
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
lock_statistics: LockStatistics
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemaphoreStatistics:
|
||||
"""
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire`
|
||||
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
class Event:
|
||||
def __new__(cls) -> Event:
|
||||
try:
|
||||
return get_async_backend().create_event()
|
||||
except NoEventLoopError:
|
||||
return EventAdapter()
|
||||
|
||||
def set(self) -> None:
|
||||
"""Set the flag, notifying all listeners."""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_set(self) -> bool:
|
||||
"""Return ``True`` if the flag is set, ``False`` if not."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def wait(self) -> None:
|
||||
"""
|
||||
Wait until the flag has been set.
|
||||
|
||||
If the flag has already been set when this method is called, it returns
|
||||
immediately.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def statistics(self) -> EventStatistics:
|
||||
"""Return statistics about the current state of this event."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EventAdapter(Event):
|
||||
_internal_event: Event | None = None
|
||||
_is_set: bool = False
|
||||
|
||||
def __new__(cls) -> EventAdapter:
|
||||
return object.__new__(cls)
|
||||
|
||||
@property
|
||||
def _event(self) -> Event:
|
||||
if self._internal_event is None:
|
||||
self._internal_event = get_async_backend().create_event()
|
||||
if self._is_set:
|
||||
self._internal_event.set()
|
||||
|
||||
return self._internal_event
|
||||
|
||||
def set(self) -> None:
|
||||
if self._internal_event is None:
|
||||
self._is_set = True
|
||||
else:
|
||||
self._event.set()
|
||||
|
||||
def is_set(self) -> bool:
|
||||
if self._internal_event is None:
|
||||
return self._is_set
|
||||
|
||||
return self._internal_event.is_set()
|
||||
|
||||
async def wait(self) -> None:
|
||||
await self._event.wait()
|
||||
|
||||
def statistics(self) -> EventStatistics:
|
||||
if self._internal_event is None:
|
||||
return EventStatistics(tasks_waiting=0)
|
||||
|
||||
return self._internal_event.statistics()
|
||||
|
||||
|
||||
class Lock:
|
||||
def __new__(cls, *, fast_acquire: bool = False) -> Lock:
|
||||
try:
|
||||
return get_async_backend().create_lock(fast_acquire=fast_acquire)
|
||||
except NoEventLoopError:
|
||||
return LockAdapter(fast_acquire=fast_acquire)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.acquire()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Acquire the lock."""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the lock."""
|
||||
raise NotImplementedError
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if the lock is currently held."""
|
||||
raise NotImplementedError
|
||||
|
||||
def statistics(self) -> LockStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this lock.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LockAdapter(Lock):
|
||||
_internal_lock: Lock | None = None
|
||||
|
||||
def __new__(cls, *, fast_acquire: bool = False) -> LockAdapter:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, *, fast_acquire: bool = False):
|
||||
self._fast_acquire = fast_acquire
|
||||
|
||||
@property
|
||||
def _lock(self) -> Lock:
|
||||
if self._internal_lock is None:
|
||||
self._internal_lock = get_async_backend().create_lock(
|
||||
fast_acquire=self._fast_acquire
|
||||
)
|
||||
|
||||
return self._internal_lock
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self._lock.acquire()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
if self._internal_lock is not None:
|
||||
self._internal_lock.release()
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Acquire the lock."""
|
||||
await self._lock.acquire()
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
self._lock.acquire_nowait()
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the lock."""
|
||||
self._lock.release()
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if the lock is currently held."""
|
||||
return self._lock.locked()
|
||||
|
||||
def statistics(self) -> LockStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this lock.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
if self._internal_lock is None:
|
||||
return LockStatistics(False, None, 0)
|
||||
|
||||
return self._internal_lock.statistics()
|
||||
|
||||
|
||||
class Condition:
|
||||
_owner_task: TaskInfo | None = None
|
||||
|
||||
def __init__(self, lock: Lock | None = None):
|
||||
self._lock = lock or Lock()
|
||||
self._waiters: deque[Event] = deque()
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.acquire()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
def _check_acquired(self) -> None:
|
||||
if self._owner_task != get_current_task():
|
||||
raise RuntimeError("The current task is not holding the underlying lock")
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Acquire the underlying lock."""
|
||||
await self._lock.acquire()
|
||||
self._owner_task = get_current_task()
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the underlying lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
self._lock.acquire_nowait()
|
||||
self._owner_task = get_current_task()
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the underlying lock."""
|
||||
self._lock.release()
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if the lock is set."""
|
||||
return self._lock.locked()
|
||||
|
||||
def notify(self, n: int = 1) -> None:
|
||||
"""Notify exactly n listeners."""
|
||||
self._check_acquired()
|
||||
for _ in range(n):
|
||||
try:
|
||||
event = self._waiters.popleft()
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
event.set()
|
||||
|
||||
def notify_all(self) -> None:
|
||||
"""Notify all the listeners."""
|
||||
self._check_acquired()
|
||||
for event in self._waiters:
|
||||
event.set()
|
||||
|
||||
self._waiters.clear()
|
||||
|
||||
async def wait(self) -> None:
|
||||
"""Wait for a notification."""
|
||||
await checkpoint_if_cancelled()
|
||||
self._check_acquired()
|
||||
event = Event()
|
||||
self._waiters.append(event)
|
||||
self.release()
|
||||
try:
|
||||
await event.wait()
|
||||
except BaseException:
|
||||
if not event.is_set():
|
||||
self._waiters.remove(event)
|
||||
|
||||
raise
|
||||
finally:
|
||||
with CancelScope(shield=True):
|
||||
await self.acquire()
|
||||
|
||||
async def wait_for(self, predicate: Callable[[], T]) -> T:
|
||||
"""
|
||||
Wait until a predicate becomes true.
|
||||
|
||||
:param predicate: a callable that returns a truthy value when the condition is
|
||||
met
|
||||
:return: the result of the predicate
|
||||
|
||||
.. versionadded:: 4.11.0
|
||||
|
||||
"""
|
||||
while not (result := predicate()):
|
||||
await self.wait()
|
||||
|
||||
return result
|
||||
|
||||
def statistics(self) -> ConditionStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this condition.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return ConditionStatistics(len(self._waiters), self._lock.statistics())
|
||||
|
||||
|
||||
class Semaphore:
|
||||
def __new__(
|
||||
cls,
|
||||
initial_value: int,
|
||||
*,
|
||||
max_value: int | None = None,
|
||||
fast_acquire: bool = False,
|
||||
) -> Semaphore:
|
||||
try:
|
||||
return get_async_backend().create_semaphore(
|
||||
initial_value, max_value=max_value, fast_acquire=fast_acquire
|
||||
)
|
||||
except NoEventLoopError:
|
||||
return SemaphoreAdapter(initial_value, max_value=max_value)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_value: int,
|
||||
*,
|
||||
max_value: int | None = None,
|
||||
fast_acquire: bool = False,
|
||||
):
|
||||
if not isinstance(initial_value, int):
|
||||
raise TypeError("initial_value must be an integer")
|
||||
if initial_value < 0:
|
||||
raise ValueError("initial_value must be >= 0")
|
||||
if max_value is not None:
|
||||
if not isinstance(max_value, int):
|
||||
raise TypeError("max_value must be an integer or None")
|
||||
if max_value < initial_value:
|
||||
raise ValueError(
|
||||
"max_value must be equal to or higher than initial_value"
|
||||
)
|
||||
|
||||
self._fast_acquire = fast_acquire
|
||||
|
||||
async def __aenter__(self) -> Semaphore:
|
||||
await self.acquire()
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Decrement the semaphore value, blocking if necessary."""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the underlying lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def release(self) -> None:
|
||||
"""Increment the semaphore value."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def value(self) -> int:
|
||||
"""The current value of the semaphore."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def max_value(self) -> int | None:
|
||||
"""The maximum value of the semaphore."""
|
||||
raise NotImplementedError
|
||||
|
||||
def statistics(self) -> SemaphoreStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this semaphore.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SemaphoreAdapter(Semaphore):
|
||||
_internal_semaphore: Semaphore | None = None
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
initial_value: int,
|
||||
*,
|
||||
max_value: int | None = None,
|
||||
fast_acquire: bool = False,
|
||||
) -> SemaphoreAdapter:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_value: int,
|
||||
*,
|
||||
max_value: int | None = None,
|
||||
fast_acquire: bool = False,
|
||||
) -> None:
|
||||
super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire)
|
||||
self._initial_value = initial_value
|
||||
self._max_value = max_value
|
||||
|
||||
@property
|
||||
def _semaphore(self) -> Semaphore:
|
||||
if self._internal_semaphore is None:
|
||||
self._internal_semaphore = get_async_backend().create_semaphore(
|
||||
self._initial_value, max_value=self._max_value
|
||||
)
|
||||
|
||||
return self._internal_semaphore
|
||||
|
||||
async def acquire(self) -> None:
|
||||
await self._semaphore.acquire()
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
self._semaphore.acquire_nowait()
|
||||
|
||||
def release(self) -> None:
|
||||
self._semaphore.release()
|
||||
|
||||
@property
|
||||
def value(self) -> int:
|
||||
if self._internal_semaphore is None:
|
||||
return self._initial_value
|
||||
|
||||
return self._semaphore.value
|
||||
|
||||
@property
|
||||
def max_value(self) -> int | None:
|
||||
return self._max_value
|
||||
|
||||
def statistics(self) -> SemaphoreStatistics:
|
||||
if self._internal_semaphore is None:
|
||||
return SemaphoreStatistics(tasks_waiting=0)
|
||||
|
||||
return self._semaphore.statistics()
|
||||
|
||||
|
||||
class CapacityLimiter:
|
||||
def __new__(cls, total_tokens: float) -> CapacityLimiter:
|
||||
try:
|
||||
return get_async_backend().create_capacity_limiter(total_tokens)
|
||||
except NoEventLoopError:
|
||||
return CapacityLimiterAdapter(total_tokens)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def total_tokens(self) -> float:
|
||||
"""
|
||||
The total number of tokens available for borrowing.
|
||||
|
||||
This is a read-write property. If the total number of tokens is increased, the
|
||||
proportionate number of tasks waiting on this limiter will be granted their
|
||||
tokens.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
The property is now writable.
|
||||
.. versionchanged:: 4.12
|
||||
The value can now be set to 0.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@total_tokens.setter
|
||||
def total_tokens(self, value: float) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def borrowed_tokens(self) -> int:
|
||||
"""The number of tokens that have currently been borrowed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def available_tokens(self) -> float:
|
||||
"""The number of tokens currently available to be borrowed"""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire a token for the current task without waiting for one to become
|
||||
available.
|
||||
|
||||
:raises ~anyio.WouldBlock: if there are no tokens available for borrowing
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
|
||||
"""
|
||||
Acquire a token without waiting for one to become available.
|
||||
|
||||
:param borrower: the entity borrowing a token
|
||||
:raises ~anyio.WouldBlock: if there are no tokens available for borrowing
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""
|
||||
Acquire a token for the current task, waiting if necessary for one to become
|
||||
available.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def acquire_on_behalf_of(self, borrower: object) -> None:
|
||||
"""
|
||||
Acquire a token, waiting if necessary for one to become available.
|
||||
|
||||
:param borrower: the entity borrowing a token
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def release(self) -> None:
|
||||
"""
|
||||
Release the token held by the current task.
|
||||
|
||||
:raises RuntimeError: if the current task has not borrowed a token from this
|
||||
limiter.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def release_on_behalf_of(self, borrower: object) -> None:
|
||||
"""
|
||||
Release the token held by the given borrower.
|
||||
|
||||
:raises RuntimeError: if the borrower has not borrowed a token from this
|
||||
limiter.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def statistics(self) -> CapacityLimiterStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this limiter.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CapacityLimiterAdapter(CapacityLimiter):
|
||||
_internal_limiter: CapacityLimiter | None = None
|
||||
|
||||
def __new__(cls, total_tokens: float) -> CapacityLimiterAdapter:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, total_tokens: float) -> None:
|
||||
self.total_tokens = total_tokens
|
||||
|
||||
@property
|
||||
def _limiter(self) -> CapacityLimiter:
|
||||
if self._internal_limiter is None:
|
||||
self._internal_limiter = get_async_backend().create_capacity_limiter(
|
||||
self._total_tokens
|
||||
)
|
||||
|
||||
return self._internal_limiter
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self._limiter.__aenter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
return await self._limiter.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
@property
|
||||
def total_tokens(self) -> float:
|
||||
if self._internal_limiter is None:
|
||||
return self._total_tokens
|
||||
|
||||
return self._internal_limiter.total_tokens
|
||||
|
||||
@total_tokens.setter
|
||||
def total_tokens(self, value: float) -> None:
|
||||
if not isinstance(value, int) and value is not math.inf:
|
||||
raise TypeError("total_tokens must be an int or math.inf")
|
||||
elif value < 1:
|
||||
raise ValueError("total_tokens must be >= 1")
|
||||
|
||||
if self._internal_limiter is None:
|
||||
self._total_tokens = value
|
||||
return
|
||||
|
||||
self._limiter.total_tokens = value
|
||||
|
||||
@property
|
||||
def borrowed_tokens(self) -> int:
|
||||
if self._internal_limiter is None:
|
||||
return 0
|
||||
|
||||
return self._internal_limiter.borrowed_tokens
|
||||
|
||||
@property
|
||||
def available_tokens(self) -> float:
|
||||
if self._internal_limiter is None:
|
||||
return self._total_tokens
|
||||
|
||||
return self._internal_limiter.available_tokens
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
self._limiter.acquire_nowait()
|
||||
|
||||
def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
|
||||
self._limiter.acquire_on_behalf_of_nowait(borrower)
|
||||
|
||||
async def acquire(self) -> None:
|
||||
await self._limiter.acquire()
|
||||
|
||||
async def acquire_on_behalf_of(self, borrower: object) -> None:
|
||||
await self._limiter.acquire_on_behalf_of(borrower)
|
||||
|
||||
def release(self) -> None:
|
||||
self._limiter.release()
|
||||
|
||||
def release_on_behalf_of(self, borrower: object) -> None:
|
||||
self._limiter.release_on_behalf_of(borrower)
|
||||
|
||||
def statistics(self) -> CapacityLimiterStatistics:
|
||||
if self._internal_limiter is None:
|
||||
return CapacityLimiterStatistics(
|
||||
borrowed_tokens=0,
|
||||
total_tokens=self.total_tokens,
|
||||
borrowers=(),
|
||||
tasks_waiting=0,
|
||||
)
|
||||
|
||||
return self._internal_limiter.statistics()
|
||||
|
||||
|
||||
class ResourceGuard:
|
||||
"""
|
||||
A context manager for ensuring that a resource is only used by a single task at a
|
||||
time.
|
||||
|
||||
Entering this context manager while the previous has not exited it yet will trigger
|
||||
:exc:`BusyResourceError`.
|
||||
|
||||
:param action: the action to guard against (visible in the :exc:`BusyResourceError`
|
||||
when triggered, e.g. "Another task is already {action} this resource")
|
||||
|
||||
.. versionadded:: 4.1
|
||||
"""
|
||||
|
||||
__slots__ = "action", "_guarded"
|
||||
|
||||
def __init__(self, action: str = "using"):
|
||||
self.action: str = action
|
||||
self._guarded = False
|
||||
|
||||
def __enter__(self) -> None:
|
||||
if self._guarded:
|
||||
raise BusyResourceError(self.action)
|
||||
|
||||
self._guarded = True
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self._guarded = False
|
||||
173
.venv/lib/python3.11/site-packages/anyio/_core/_tasks.py
Normal file
173
.venv/lib/python3.11/site-packages/anyio/_core/_tasks.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from types import TracebackType
|
||||
|
||||
from ..abc._tasks import TaskGroup, TaskStatus
|
||||
from ._eventloop import get_async_backend
|
||||
|
||||
|
||||
class _IgnoredTaskStatus(TaskStatus[object]):
|
||||
def started(self, value: object = None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
TASK_STATUS_IGNORED = _IgnoredTaskStatus()
|
||||
|
||||
|
||||
class CancelScope:
|
||||
"""
|
||||
Wraps a unit of work that can be made separately cancellable.
|
||||
|
||||
:param deadline: The time (clock value) when this scope is cancelled automatically
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
"""
|
||||
|
||||
def __new__(
|
||||
cls, *, deadline: float = math.inf, shield: bool = False
|
||||
) -> CancelScope:
|
||||
return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline)
|
||||
|
||||
def cancel(self, reason: str | None = None) -> None:
|
||||
"""
|
||||
Cancel this scope immediately.
|
||||
|
||||
:param reason: a message describing the reason for the cancellation
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def deadline(self) -> float:
|
||||
"""
|
||||
The time (clock value) when this scope is cancelled automatically.
|
||||
|
||||
Will be ``float('inf')`` if no timeout has been set.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@deadline.setter
|
||||
def deadline(self, value: float) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def cancel_called(self) -> bool:
|
||||
"""``True`` if :meth:`cancel` has been called."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def cancelled_caught(self) -> bool:
|
||||
"""
|
||||
``True`` if this scope suppressed a cancellation exception it itself raised.
|
||||
|
||||
This is typically used to check if any work was interrupted, or to see if the
|
||||
scope was cancelled due to its deadline being reached. The value will, however,
|
||||
only be ``True`` if the cancellation was triggered by the scope itself (and not
|
||||
an outer scope).
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def shield(self) -> bool:
|
||||
"""
|
||||
``True`` if this scope is shielded from external cancellation.
|
||||
|
||||
While a scope is shielded, it will not receive cancellations from outside.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@shield.setter
|
||||
def shield(self, value: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __enter__(self) -> CancelScope:
|
||||
raise NotImplementedError
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@contextmanager
|
||||
def fail_after(
|
||||
delay: float | None, shield: bool = False
|
||||
) -> Generator[CancelScope, None, None]:
|
||||
"""
|
||||
Create a context manager which raises a :class:`TimeoutError` if does not finish in
|
||||
time.
|
||||
|
||||
:param delay: maximum allowed time (in seconds) before raising the exception, or
|
||||
``None`` to disable the timeout
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
:return: a context manager that yields a cancel scope
|
||||
:rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\]
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
current_time = get_async_backend().current_time
|
||||
deadline = (current_time() + delay) if delay is not None else math.inf
|
||||
with get_async_backend().create_cancel_scope(
|
||||
deadline=deadline, shield=shield
|
||||
) as cancel_scope:
|
||||
yield cancel_scope
|
||||
|
||||
if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline:
|
||||
raise TimeoutError
|
||||
|
||||
|
||||
def move_on_after(delay: float | None, shield: bool = False) -> CancelScope:
|
||||
"""
|
||||
Create a cancel scope with a deadline that expires after the given delay.
|
||||
|
||||
:param delay: maximum allowed time (in seconds) before exiting the context block, or
|
||||
``None`` to disable the timeout
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
:return: a cancel scope
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
deadline = (
|
||||
(get_async_backend().current_time() + delay) if delay is not None else math.inf
|
||||
)
|
||||
return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield)
|
||||
|
||||
|
||||
def current_effective_deadline() -> float:
|
||||
"""
|
||||
Return the nearest deadline among all the cancel scopes effective for the current
|
||||
task.
|
||||
|
||||
:return: a clock value from the event loop's internal clock (or ``float('inf')`` if
|
||||
there is no deadline in effect, or ``float('-inf')`` if the current scope has
|
||||
been cancelled)
|
||||
:rtype: float
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().current_effective_deadline()
|
||||
|
||||
|
||||
def create_task_group() -> TaskGroup:
|
||||
"""
|
||||
Create a task group.
|
||||
|
||||
:return: a task group
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().create_task_group()
|
||||
616
.venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py
Normal file
616
.venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py
Normal file
@@ -0,0 +1,616 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Iterable
|
||||
from io import BytesIO, TextIOWrapper
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AnyStr,
|
||||
Generic,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .. import to_thread
|
||||
from .._core._fileio import AsyncFile
|
||||
from ..lowlevel import checkpoint_if_cancelled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
|
||||
|
||||
|
||||
class TemporaryFile(Generic[AnyStr]):
|
||||
"""
|
||||
An asynchronous temporary file that is automatically created and cleaned up.
|
||||
|
||||
This class provides an asynchronous context manager interface to a temporary file.
|
||||
The file is created using Python's standard `tempfile.TemporaryFile` function in a
|
||||
background thread, and is wrapped as an asynchronous file using `AsyncFile`.
|
||||
|
||||
:param mode: The mode in which the file is opened. Defaults to "w+b".
|
||||
:param buffering: The buffering policy (-1 means the default buffering).
|
||||
:param encoding: The encoding used to decode or encode the file. Only applicable in
|
||||
text mode.
|
||||
:param newline: Controls how universal newlines mode works (only applicable in text
|
||||
mode).
|
||||
:param suffix: The suffix for the temporary file name.
|
||||
:param prefix: The prefix for the temporary file name.
|
||||
:param dir: The directory in which the temporary file is created.
|
||||
:param errors: The error handling scheme used for encoding/decoding errors.
|
||||
"""
|
||||
|
||||
_async_file: AsyncFile[AnyStr]
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: TemporaryFile[bytes],
|
||||
mode: OpenBinaryMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
suffix: str | None = ...,
|
||||
prefix: str | None = ...,
|
||||
dir: str | None = ...,
|
||||
*,
|
||||
errors: str | None = ...,
|
||||
): ...
|
||||
@overload
|
||||
def __init__(
|
||||
self: TemporaryFile[str],
|
||||
mode: OpenTextMode,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
suffix: str | None = ...,
|
||||
prefix: str | None = ...,
|
||||
dir: str | None = ...,
|
||||
*,
|
||||
errors: str | None = ...,
|
||||
): ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode: OpenTextMode | OpenBinaryMode = "w+b",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
newline: str | None = None,
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
*,
|
||||
errors: str | None = None,
|
||||
) -> None:
|
||||
self.mode = mode
|
||||
self.buffering = buffering
|
||||
self.encoding = encoding
|
||||
self.newline = newline
|
||||
self.suffix: str | None = suffix
|
||||
self.prefix: str | None = prefix
|
||||
self.dir: str | None = dir
|
||||
self.errors = errors
|
||||
|
||||
async def __aenter__(self) -> AsyncFile[AnyStr]:
|
||||
fp = await to_thread.run_sync(
|
||||
lambda: tempfile.TemporaryFile(
|
||||
self.mode,
|
||||
self.buffering,
|
||||
self.encoding,
|
||||
self.newline,
|
||||
self.suffix,
|
||||
self.prefix,
|
||||
self.dir,
|
||||
errors=self.errors,
|
||||
)
|
||||
)
|
||||
self._async_file = AsyncFile(fp)
|
||||
return self._async_file
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
await self._async_file.aclose()
|
||||
|
||||
|
||||
class NamedTemporaryFile(Generic[AnyStr]):
|
||||
"""
|
||||
An asynchronous named temporary file that is automatically created and cleaned up.
|
||||
|
||||
This class provides an asynchronous context manager for a temporary file with a
|
||||
visible name in the file system. It uses Python's standard
|
||||
:func:`~tempfile.NamedTemporaryFile` function and wraps the file object with
|
||||
:class:`AsyncFile` for asynchronous operations.
|
||||
|
||||
:param mode: The mode in which the file is opened. Defaults to "w+b".
|
||||
:param buffering: The buffering policy (-1 means the default buffering).
|
||||
:param encoding: The encoding used to decode or encode the file. Only applicable in
|
||||
text mode.
|
||||
:param newline: Controls how universal newlines mode works (only applicable in text
|
||||
mode).
|
||||
:param suffix: The suffix for the temporary file name.
|
||||
:param prefix: The prefix for the temporary file name.
|
||||
:param dir: The directory in which the temporary file is created.
|
||||
:param delete: Whether to delete the file when it is closed.
|
||||
:param errors: The error handling scheme used for encoding/decoding errors.
|
||||
:param delete_on_close: (Python 3.12+) Whether to delete the file on close.
|
||||
"""
|
||||
|
||||
_async_file: AsyncFile[AnyStr]
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: NamedTemporaryFile[bytes],
|
||||
mode: OpenBinaryMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
suffix: str | None = ...,
|
||||
prefix: str | None = ...,
|
||||
dir: str | None = ...,
|
||||
delete: bool = ...,
|
||||
*,
|
||||
errors: str | None = ...,
|
||||
delete_on_close: bool = ...,
|
||||
): ...
|
||||
@overload
|
||||
def __init__(
|
||||
self: NamedTemporaryFile[str],
|
||||
mode: OpenTextMode,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
suffix: str | None = ...,
|
||||
prefix: str | None = ...,
|
||||
dir: str | None = ...,
|
||||
delete: bool = ...,
|
||||
*,
|
||||
errors: str | None = ...,
|
||||
delete_on_close: bool = ...,
|
||||
): ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode: OpenBinaryMode | OpenTextMode = "w+b",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
newline: str | None = None,
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
delete: bool = True,
|
||||
*,
|
||||
errors: str | None = None,
|
||||
delete_on_close: bool = True,
|
||||
) -> None:
|
||||
self._params: dict[str, Any] = {
|
||||
"mode": mode,
|
||||
"buffering": buffering,
|
||||
"encoding": encoding,
|
||||
"newline": newline,
|
||||
"suffix": suffix,
|
||||
"prefix": prefix,
|
||||
"dir": dir,
|
||||
"delete": delete,
|
||||
"errors": errors,
|
||||
}
|
||||
if sys.version_info >= (3, 12):
|
||||
self._params["delete_on_close"] = delete_on_close
|
||||
|
||||
async def __aenter__(self) -> AsyncFile[AnyStr]:
|
||||
fp = await to_thread.run_sync(
|
||||
lambda: tempfile.NamedTemporaryFile(**self._params)
|
||||
)
|
||||
self._async_file = AsyncFile(fp)
|
||||
return self._async_file
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
await self._async_file.aclose()
|
||||
|
||||
|
||||
class SpooledTemporaryFile(AsyncFile[AnyStr]):
|
||||
"""
|
||||
An asynchronous spooled temporary file that starts in memory and is spooled to disk.
|
||||
|
||||
This class provides an asynchronous interface to a spooled temporary file, much like
|
||||
Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous
|
||||
write operations and provides a method to force a rollover to disk.
|
||||
|
||||
:param max_size: Maximum size in bytes before the file is rolled over to disk.
|
||||
:param mode: The mode in which the file is opened. Defaults to "w+b".
|
||||
:param buffering: The buffering policy (-1 means the default buffering).
|
||||
:param encoding: The encoding used to decode or encode the file (text mode only).
|
||||
:param newline: Controls how universal newlines mode works (text mode only).
|
||||
:param suffix: The suffix for the temporary file name.
|
||||
:param prefix: The prefix for the temporary file name.
|
||||
:param dir: The directory in which the temporary file is created.
|
||||
:param errors: The error handling scheme used for encoding/decoding errors.
|
||||
"""
|
||||
|
||||
_rolled: bool = False
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: SpooledTemporaryFile[bytes],
|
||||
max_size: int = ...,
|
||||
mode: OpenBinaryMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
suffix: str | None = ...,
|
||||
prefix: str | None = ...,
|
||||
dir: str | None = ...,
|
||||
*,
|
||||
errors: str | None = ...,
|
||||
): ...
|
||||
@overload
|
||||
def __init__(
|
||||
self: SpooledTemporaryFile[str],
|
||||
max_size: int = ...,
|
||||
mode: OpenTextMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
suffix: str | None = ...,
|
||||
prefix: str | None = ...,
|
||||
dir: str | None = ...,
|
||||
*,
|
||||
errors: str | None = ...,
|
||||
): ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_size: int = 0,
|
||||
mode: OpenBinaryMode | OpenTextMode = "w+b",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
newline: str | None = None,
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
*,
|
||||
errors: str | None = None,
|
||||
) -> None:
|
||||
self._tempfile_params: dict[str, Any] = {
|
||||
"mode": mode,
|
||||
"buffering": buffering,
|
||||
"encoding": encoding,
|
||||
"newline": newline,
|
||||
"suffix": suffix,
|
||||
"prefix": prefix,
|
||||
"dir": dir,
|
||||
"errors": errors,
|
||||
}
|
||||
self._max_size = max_size
|
||||
if "b" in mode:
|
||||
super().__init__(BytesIO()) # type: ignore[arg-type]
|
||||
else:
|
||||
super().__init__(
|
||||
TextIOWrapper( # type: ignore[arg-type]
|
||||
BytesIO(),
|
||||
encoding=encoding,
|
||||
errors=errors,
|
||||
newline=newline,
|
||||
write_through=True,
|
||||
)
|
||||
)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if not self._rolled:
|
||||
self._fp.close()
|
||||
return
|
||||
|
||||
await super().aclose()
|
||||
|
||||
async def _check(self) -> None:
|
||||
if self._rolled or self._fp.tell() <= self._max_size:
|
||||
return
|
||||
|
||||
await self.rollover()
|
||||
|
||||
async def rollover(self) -> None:
|
||||
if self._rolled:
|
||||
return
|
||||
|
||||
self._rolled = True
|
||||
buffer = self._fp
|
||||
buffer.seek(0)
|
||||
self._fp = await to_thread.run_sync(
|
||||
lambda: tempfile.TemporaryFile(**self._tempfile_params)
|
||||
)
|
||||
await self.write(buffer.read())
|
||||
buffer.close()
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
return self._fp.closed
|
||||
|
||||
async def read(self, size: int = -1) -> AnyStr:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.read(size)
|
||||
|
||||
return await super().read(size) # type: ignore[return-value]
|
||||
|
||||
async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.read1(size)
|
||||
|
||||
return await super().read1(size)
|
||||
|
||||
async def readline(self) -> AnyStr:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.readline()
|
||||
|
||||
return await super().readline() # type: ignore[return-value]
|
||||
|
||||
async def readlines(self) -> list[AnyStr]:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.readlines()
|
||||
|
||||
return await super().readlines() # type: ignore[return-value]
|
||||
|
||||
async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
self._fp.readinto(b)
|
||||
|
||||
return await super().readinto(b)
|
||||
|
||||
async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
self._fp.readinto(b)
|
||||
|
||||
return await super().readinto1(b)
|
||||
|
||||
async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.seek(offset, whence)
|
||||
|
||||
return await super().seek(offset, whence)
|
||||
|
||||
async def tell(self) -> int:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.tell()
|
||||
|
||||
return await super().tell()
|
||||
|
||||
async def truncate(self, size: int | None = None) -> int:
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
return self._fp.truncate(size)
|
||||
|
||||
return await super().truncate(size)
|
||||
|
||||
@overload
|
||||
async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ...
|
||||
@overload
|
||||
async def write(self: SpooledTemporaryFile[str], b: str) -> int: ...
|
||||
|
||||
async def write(self, b: ReadableBuffer | str) -> int:
|
||||
"""
|
||||
Asynchronously write data to the spooled temporary file.
|
||||
|
||||
If the file has not yet been rolled over, the data is written synchronously,
|
||||
and a rollover is triggered if the size exceeds the maximum size.
|
||||
|
||||
:param s: The data to write.
|
||||
:return: The number of bytes written.
|
||||
:raises RuntimeError: If the underlying file is not initialized.
|
||||
|
||||
"""
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
result = self._fp.write(b)
|
||||
await self._check()
|
||||
return result
|
||||
|
||||
return await super().write(b) # type: ignore[misc]
|
||||
|
||||
@overload
|
||||
async def writelines(
|
||||
self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer]
|
||||
) -> None: ...
|
||||
@overload
|
||||
async def writelines(
|
||||
self: SpooledTemporaryFile[str], lines: Iterable[str]
|
||||
) -> None: ...
|
||||
|
||||
async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None:
|
||||
"""
|
||||
Asynchronously write a list of lines to the spooled temporary file.
|
||||
|
||||
If the file has not yet been rolled over, the lines are written synchronously,
|
||||
and a rollover is triggered if the size exceeds the maximum size.
|
||||
|
||||
:param lines: An iterable of lines to write.
|
||||
:raises RuntimeError: If the underlying file is not initialized.
|
||||
|
||||
"""
|
||||
if not self._rolled:
|
||||
await checkpoint_if_cancelled()
|
||||
result = self._fp.writelines(lines)
|
||||
await self._check()
|
||||
return result
|
||||
|
||||
return await super().writelines(lines) # type: ignore[misc]
|
||||
|
||||
|
||||
class TemporaryDirectory(Generic[AnyStr]):
|
||||
"""
|
||||
An asynchronous temporary directory that is created and cleaned up automatically.
|
||||
|
||||
This class provides an asynchronous context manager for creating a temporary
|
||||
directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to
|
||||
perform directory creation and cleanup operations in a background thread.
|
||||
|
||||
:param suffix: Suffix to be added to the temporary directory name.
|
||||
:param prefix: Prefix to be added to the temporary directory name.
|
||||
:param dir: The parent directory where the temporary directory is created.
|
||||
:param ignore_cleanup_errors: Whether to ignore errors during cleanup
|
||||
(Python 3.10+).
|
||||
:param delete: Whether to delete the directory upon closing (Python 3.12+).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
suffix: AnyStr | None = None,
|
||||
prefix: AnyStr | None = None,
|
||||
dir: AnyStr | None = None,
|
||||
*,
|
||||
ignore_cleanup_errors: bool = False,
|
||||
delete: bool = True,
|
||||
) -> None:
|
||||
self.suffix: AnyStr | None = suffix
|
||||
self.prefix: AnyStr | None = prefix
|
||||
self.dir: AnyStr | None = dir
|
||||
self.ignore_cleanup_errors = ignore_cleanup_errors
|
||||
self.delete = delete
|
||||
|
||||
self._tempdir: tempfile.TemporaryDirectory | None = None
|
||||
|
||||
async def __aenter__(self) -> str:
|
||||
params: dict[str, Any] = {
|
||||
"suffix": self.suffix,
|
||||
"prefix": self.prefix,
|
||||
"dir": self.dir,
|
||||
}
|
||||
if sys.version_info >= (3, 10):
|
||||
params["ignore_cleanup_errors"] = self.ignore_cleanup_errors
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
params["delete"] = self.delete
|
||||
|
||||
self._tempdir = await to_thread.run_sync(
|
||||
lambda: tempfile.TemporaryDirectory(**params)
|
||||
)
|
||||
return await to_thread.run_sync(self._tempdir.__enter__)
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
if self._tempdir is not None:
|
||||
await to_thread.run_sync(
|
||||
self._tempdir.__exit__, exc_type, exc_value, traceback
|
||||
)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self._tempdir is not None:
|
||||
await to_thread.run_sync(self._tempdir.cleanup)
|
||||
|
||||
|
||||
@overload
|
||||
async def mkstemp(
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
text: bool = False,
|
||||
) -> tuple[int, str]: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def mkstemp(
|
||||
suffix: bytes | None = None,
|
||||
prefix: bytes | None = None,
|
||||
dir: bytes | None = None,
|
||||
text: bool = False,
|
||||
) -> tuple[int, bytes]: ...
|
||||
|
||||
|
||||
async def mkstemp(
|
||||
suffix: AnyStr | None = None,
|
||||
prefix: AnyStr | None = None,
|
||||
dir: AnyStr | None = None,
|
||||
text: bool = False,
|
||||
) -> tuple[int, str | bytes]:
|
||||
"""
|
||||
Asynchronously create a temporary file and return an OS-level handle and the file
|
||||
name.
|
||||
|
||||
This function wraps `tempfile.mkstemp` and executes it in a background thread.
|
||||
|
||||
:param suffix: Suffix to be added to the file name.
|
||||
:param prefix: Prefix to be added to the file name.
|
||||
:param dir: Directory in which the temporary file is created.
|
||||
:param text: Whether the file is opened in text mode.
|
||||
:return: A tuple containing the file descriptor and the file name.
|
||||
|
||||
"""
|
||||
return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text)
|
||||
|
||||
|
||||
@overload
|
||||
async def mkdtemp(
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
) -> str: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def mkdtemp(
|
||||
suffix: bytes | None = None,
|
||||
prefix: bytes | None = None,
|
||||
dir: bytes | None = None,
|
||||
) -> bytes: ...
|
||||
|
||||
|
||||
async def mkdtemp(
|
||||
suffix: AnyStr | None = None,
|
||||
prefix: AnyStr | None = None,
|
||||
dir: AnyStr | None = None,
|
||||
) -> str | bytes:
|
||||
"""
|
||||
Asynchronously create a temporary directory and return its path.
|
||||
|
||||
This function wraps `tempfile.mkdtemp` and executes it in a background thread.
|
||||
|
||||
:param suffix: Suffix to be added to the directory name.
|
||||
:param prefix: Prefix to be added to the directory name.
|
||||
:param dir: Parent directory where the temporary directory is created.
|
||||
:return: The path of the created temporary directory.
|
||||
|
||||
"""
|
||||
return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir)
|
||||
|
||||
|
||||
async def gettempdir() -> str:
|
||||
"""
|
||||
Asynchronously return the name of the directory used for temporary files.
|
||||
|
||||
This function wraps `tempfile.gettempdir` and executes it in a background thread.
|
||||
|
||||
:return: The path of the temporary directory as a string.
|
||||
|
||||
"""
|
||||
return await to_thread.run_sync(tempfile.gettempdir)
|
||||
|
||||
|
||||
async def gettempdirb() -> bytes:
|
||||
"""
|
||||
Asynchronously return the name of the directory used for temporary files in bytes.
|
||||
|
||||
This function wraps `tempfile.gettempdirb` and executes it in a background thread.
|
||||
|
||||
:return: The path of the temporary directory as bytes.
|
||||
|
||||
"""
|
||||
return await to_thread.run_sync(tempfile.gettempdirb)
|
||||
82
.venv/lib/python3.11/site-packages/anyio/_core/_testing.py
Normal file
82
.venv/lib/python3.11/site-packages/anyio/_core/_testing.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Generator
|
||||
from typing import Any, cast
|
||||
|
||||
from ._eventloop import get_async_backend
|
||||
|
||||
|
||||
class TaskInfo:
|
||||
"""
|
||||
Represents an asynchronous task.
|
||||
|
||||
:ivar int id: the unique identifier of the task
|
||||
:ivar parent_id: the identifier of the parent task, if any
|
||||
:vartype parent_id: Optional[int]
|
||||
:ivar str name: the description of the task (if any)
|
||||
:ivar ~collections.abc.Coroutine coro: the coroutine object of the task
|
||||
"""
|
||||
|
||||
__slots__ = "_name", "id", "parent_id", "name", "coro"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
parent_id: int | None,
|
||||
name: str | None,
|
||||
coro: Generator[Any, Any, Any] | Awaitable[Any],
|
||||
):
|
||||
func = get_current_task
|
||||
self._name = f"{func.__module__}.{func.__qualname__}"
|
||||
self.id: int = id
|
||||
self.parent_id: int | None = parent_id
|
||||
self.name: str | None = name
|
||||
self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, TaskInfo):
|
||||
return self.id == other.id
|
||||
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})"
|
||||
|
||||
def has_pending_cancellation(self) -> bool:
|
||||
"""
|
||||
Return ``True`` if the task has a cancellation pending, ``False`` otherwise.
|
||||
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def get_current_task() -> TaskInfo:
|
||||
"""
|
||||
Return the current task.
|
||||
|
||||
:return: a representation of the current task
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().get_current_task()
|
||||
|
||||
|
||||
def get_running_tasks() -> list[TaskInfo]:
|
||||
"""
|
||||
Return a list of running tasks in the current event loop.
|
||||
|
||||
:return: a list of task info objects
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return cast("list[TaskInfo]", get_async_backend().get_running_tasks())
|
||||
|
||||
|
||||
async def wait_all_tasks_blocked() -> None:
|
||||
"""Wait until all other tasks are waiting for something."""
|
||||
await get_async_backend().wait_all_tasks_blocked()
|
||||
81
.venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py
Normal file
81
.venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any, TypeVar, final, overload
|
||||
|
||||
from ._exceptions import TypedAttributeLookupError
|
||||
|
||||
T_Attr = TypeVar("T_Attr")
|
||||
T_Default = TypeVar("T_Default")
|
||||
undefined = object()
|
||||
|
||||
|
||||
def typed_attribute() -> Any:
|
||||
"""Return a unique object, used to mark typed attributes."""
|
||||
return object()
|
||||
|
||||
|
||||
class TypedAttributeSet:
|
||||
"""
|
||||
Superclass for typed attribute collections.
|
||||
|
||||
Checks that every public attribute of every subclass has a type annotation.
|
||||
"""
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
annotations: dict[str, Any] = getattr(cls, "__annotations__", {})
|
||||
for attrname in dir(cls):
|
||||
if not attrname.startswith("_") and attrname not in annotations:
|
||||
raise TypeError(
|
||||
f"Attribute {attrname!r} is missing its type annotation"
|
||||
)
|
||||
|
||||
super().__init_subclass__()
|
||||
|
||||
|
||||
class TypedAttributeProvider:
|
||||
"""Base class for classes that wish to provide typed extra attributes."""
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]:
|
||||
"""
|
||||
A mapping of the extra attributes to callables that return the corresponding
|
||||
values.
|
||||
|
||||
If the provider wraps another provider, the attributes from that wrapper should
|
||||
also be included in the returned mapping (but the wrapper may override the
|
||||
callables from the wrapped instance).
|
||||
|
||||
"""
|
||||
return {}
|
||||
|
||||
@overload
|
||||
def extra(self, attribute: T_Attr) -> T_Attr: ...
|
||||
|
||||
@overload
|
||||
def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ...
|
||||
|
||||
@final
|
||||
def extra(self, attribute: Any, default: object = undefined) -> object:
|
||||
"""
|
||||
extra(attribute, default=undefined)
|
||||
|
||||
Return the value of the given typed extra attribute.
|
||||
|
||||
:param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to
|
||||
look for
|
||||
:param default: the value that should be returned if no value is found for the
|
||||
attribute
|
||||
:raises ~anyio.TypedAttributeLookupError: if the search failed and no default
|
||||
value was given
|
||||
|
||||
"""
|
||||
try:
|
||||
getter = self.extra_attributes[attribute]
|
||||
except KeyError:
|
||||
if default is undefined:
|
||||
raise TypedAttributeLookupError("Attribute not found") from None
|
||||
else:
|
||||
return default
|
||||
|
||||
return getter()
|
||||
58
.venv/lib/python3.11/site-packages/anyio/abc/__init__.py
Normal file
58
.venv/lib/python3.11/site-packages/anyio/abc/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._eventloop import AsyncBackend as AsyncBackend
|
||||
from ._resources import AsyncResource as AsyncResource
|
||||
from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket
|
||||
from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket
|
||||
from ._sockets import IPAddressType as IPAddressType
|
||||
from ._sockets import IPSockAddrType as IPSockAddrType
|
||||
from ._sockets import SocketAttribute as SocketAttribute
|
||||
from ._sockets import SocketListener as SocketListener
|
||||
from ._sockets import SocketStream as SocketStream
|
||||
from ._sockets import UDPPacketType as UDPPacketType
|
||||
from ._sockets import UDPSocket as UDPSocket
|
||||
from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType
|
||||
from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket
|
||||
from ._sockets import UNIXSocketStream as UNIXSocketStream
|
||||
from ._streams import AnyByteReceiveStream as AnyByteReceiveStream
|
||||
from ._streams import AnyByteSendStream as AnyByteSendStream
|
||||
from ._streams import AnyByteStream as AnyByteStream
|
||||
from ._streams import AnyByteStreamConnectable as AnyByteStreamConnectable
|
||||
from ._streams import AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream
|
||||
from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream
|
||||
from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream
|
||||
from ._streams import ByteReceiveStream as ByteReceiveStream
|
||||
from ._streams import ByteSendStream as ByteSendStream
|
||||
from ._streams import ByteStream as ByteStream
|
||||
from ._streams import ByteStreamConnectable as ByteStreamConnectable
|
||||
from ._streams import Listener as Listener
|
||||
from ._streams import ObjectReceiveStream as ObjectReceiveStream
|
||||
from ._streams import ObjectSendStream as ObjectSendStream
|
||||
from ._streams import ObjectStream as ObjectStream
|
||||
from ._streams import ObjectStreamConnectable as ObjectStreamConnectable
|
||||
from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream
|
||||
from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream
|
||||
from ._streams import UnreliableObjectStream as UnreliableObjectStream
|
||||
from ._subprocesses import Process as Process
|
||||
from ._tasks import TaskGroup as TaskGroup
|
||||
from ._tasks import TaskStatus as TaskStatus
|
||||
from ._testing import TestRunner as TestRunner
|
||||
|
||||
# Re-exported here, for backwards compatibility
|
||||
# isort: off
|
||||
from .._core._synchronization import (
|
||||
CapacityLimiter as CapacityLimiter,
|
||||
Condition as Condition,
|
||||
Event as Event,
|
||||
Lock as Lock,
|
||||
Semaphore as Semaphore,
|
||||
)
|
||||
from .._core._tasks import CancelScope as CancelScope
|
||||
from ..from_thread import BlockingPortal as BlockingPortal
|
||||
|
||||
# Re-export imports so they look like they live directly in this package
|
||||
for __value in list(locals().values()):
|
||||
if getattr(__value, "__module__", "").startswith("anyio.abc."):
|
||||
__value.__module__ = __name__
|
||||
|
||||
del __value
|
||||
414
.venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py
Normal file
414
.venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py
Normal file
@@ -0,0 +1,414 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
||||
from contextlib import AbstractContextManager
|
||||
from os import PathLike
|
||||
from signal import Signals
|
||||
from socket import AddressFamily, SocketKind, socket
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import FileDescriptorLike
|
||||
|
||||
from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore
|
||||
from .._core._tasks import CancelScope
|
||||
from .._core._testing import TaskInfo
|
||||
from ._sockets import (
|
||||
ConnectedUDPSocket,
|
||||
ConnectedUNIXDatagramSocket,
|
||||
IPSockAddrType,
|
||||
SocketListener,
|
||||
SocketStream,
|
||||
UDPSocket,
|
||||
UNIXDatagramSocket,
|
||||
UNIXSocketStream,
|
||||
)
|
||||
from ._subprocesses import Process
|
||||
from ._tasks import TaskGroup
|
||||
from ._testing import TestRunner
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"]
|
||||
|
||||
|
||||
class AsyncBackend(metaclass=ABCMeta):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def run(
|
||||
cls,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
args: tuple[Unpack[PosArgsT]],
|
||||
kwargs: dict[str, Any],
|
||||
options: dict[str, Any],
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Run the given coroutine function in an asynchronous event loop.
|
||||
|
||||
The current thread must not be already running an event loop.
|
||||
|
||||
:param func: a coroutine function
|
||||
:param args: positional arguments to ``func``
|
||||
:param kwargs: positional arguments to ``func``
|
||||
:param options: keyword arguments to call the backend ``run()`` implementation
|
||||
with
|
||||
:return: the return value of the coroutine function
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def current_token(cls) -> object:
|
||||
"""
|
||||
Return an object that allows other threads to run code inside the event loop.
|
||||
|
||||
:return: a token object, specific to the event loop running in the current
|
||||
thread
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def current_time(cls) -> float:
|
||||
"""
|
||||
Return the current value of the event loop's internal clock.
|
||||
|
||||
:return: the clock value (seconds)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def cancelled_exception_class(cls) -> type[BaseException]:
|
||||
"""Return the exception class that is raised in a task if it's cancelled."""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def checkpoint(cls) -> None:
|
||||
"""
|
||||
Check if the task has been cancelled, and allow rescheduling of other tasks.
|
||||
|
||||
This is effectively the same as running :meth:`checkpoint_if_cancelled` and then
|
||||
:meth:`cancel_shielded_checkpoint`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def checkpoint_if_cancelled(cls) -> None:
|
||||
"""
|
||||
Check if the current task group has been cancelled.
|
||||
|
||||
This will check if the task has been cancelled, but will not allow other tasks
|
||||
to be scheduled if not.
|
||||
|
||||
"""
|
||||
if cls.current_effective_deadline() == -math.inf:
|
||||
await cls.checkpoint()
|
||||
|
||||
@classmethod
|
||||
async def cancel_shielded_checkpoint(cls) -> None:
|
||||
"""
|
||||
Allow the rescheduling of other tasks.
|
||||
|
||||
This will give other tasks the opportunity to run, but without checking if the
|
||||
current task group has been cancelled, unlike with :meth:`checkpoint`.
|
||||
|
||||
"""
|
||||
with cls.create_cancel_scope(shield=True):
|
||||
await cls.sleep(0)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def sleep(cls, delay: float) -> None:
|
||||
"""
|
||||
Pause the current task for the specified duration.
|
||||
|
||||
:param delay: the duration, in seconds
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_cancel_scope(
|
||||
cls, *, deadline: float = math.inf, shield: bool = False
|
||||
) -> CancelScope:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def current_effective_deadline(cls) -> float:
|
||||
"""
|
||||
Return the nearest deadline among all the cancel scopes effective for the
|
||||
current task.
|
||||
|
||||
:return:
|
||||
- a clock value from the event loop's internal clock
|
||||
- ``inf`` if there is no deadline in effect
|
||||
- ``-inf`` if the current scope has been cancelled
|
||||
:rtype: float
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_task_group(cls) -> TaskGroup:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_event(cls) -> Event:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_lock(cls, *, fast_acquire: bool) -> Lock:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_semaphore(
|
||||
cls,
|
||||
initial_value: int,
|
||||
*,
|
||||
max_value: int | None = None,
|
||||
fast_acquire: bool = False,
|
||||
) -> Semaphore:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def run_sync_in_worker_thread(
|
||||
cls,
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
args: tuple[Unpack[PosArgsT]],
|
||||
abandon_on_cancel: bool = False,
|
||||
limiter: CapacityLimiter | None = None,
|
||||
) -> T_Retval:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def check_cancelled(cls) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def run_async_from_thread(
|
||||
cls,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
args: tuple[Unpack[PosArgsT]],
|
||||
token: object,
|
||||
) -> T_Retval:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def run_sync_from_thread(
|
||||
cls,
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
args: tuple[Unpack[PosArgsT]],
|
||||
token: object,
|
||||
) -> T_Retval:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def open_process(
|
||||
cls,
|
||||
command: StrOrBytesPath | Sequence[StrOrBytesPath],
|
||||
*,
|
||||
stdin: int | IO[Any] | None,
|
||||
stdout: int | IO[Any] | None,
|
||||
stderr: int | IO[Any] | None,
|
||||
**kwargs: Any,
|
||||
) -> Process:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def connect_tcp(
|
||||
cls, host: str, port: int, local_address: IPSockAddrType | None = None
|
||||
) -> SocketStream:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_tcp_listener(cls, sock: socket) -> SocketListener:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_unix_listener(cls, sock: socket) -> SocketListener:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def create_udp_socket(
|
||||
cls,
|
||||
family: AddressFamily,
|
||||
local_address: IPSockAddrType | None,
|
||||
remote_address: IPSockAddrType | None,
|
||||
reuse_port: bool,
|
||||
) -> UDPSocket | ConnectedUDPSocket:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
async def create_unix_datagram_socket(
|
||||
cls, raw_socket: socket, remote_path: None
|
||||
) -> UNIXDatagramSocket: ...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
async def create_unix_datagram_socket(
|
||||
cls, raw_socket: socket, remote_path: str | bytes
|
||||
) -> ConnectedUNIXDatagramSocket: ...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def create_unix_datagram_socket(
|
||||
cls, raw_socket: socket, remote_path: str | bytes | None
|
||||
) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def getaddrinfo(
|
||||
cls,
|
||||
host: bytes | str | None,
|
||||
port: str | int | None,
|
||||
*,
|
||||
family: int | AddressFamily = 0,
|
||||
type: int | SocketKind = 0,
|
||||
proto: int = 0,
|
||||
flags: int = 0,
|
||||
) -> Sequence[
|
||||
tuple[
|
||||
AddressFamily,
|
||||
SocketKind,
|
||||
int,
|
||||
str,
|
||||
tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes],
|
||||
]
|
||||
]:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def getnameinfo(
|
||||
cls, sockaddr: IPSockAddrType, flags: int = 0
|
||||
) -> tuple[str, str]:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wait_readable(cls, obj: FileDescriptorLike) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wait_writable(cls, obj: FileDescriptorLike) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def notify_closing(cls, obj: FileDescriptorLike) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_listener_socket(cls, sock: socket) -> SocketListener:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_stream_socket(cls, sock: socket) -> SocketStream:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_unix_stream_socket(cls, sock: socket) -> UNIXSocketStream:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_udp_socket(cls, sock: socket) -> UDPSocket:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_connected_udp_socket(cls, sock: socket) -> ConnectedUDPSocket:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_unix_datagram_socket(cls, sock: socket) -> UNIXDatagramSocket:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wrap_connected_unix_datagram_socket(
|
||||
cls, sock: socket
|
||||
) -> ConnectedUNIXDatagramSocket:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def current_default_thread_limiter(cls) -> CapacityLimiter:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def open_signal_receiver(
|
||||
cls, *signals: Signals
|
||||
) -> AbstractContextManager[AsyncIterator[Signals]]:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_current_task(cls) -> TaskInfo:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_running_tasks(cls) -> Sequence[TaskInfo]:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
async def wait_all_tasks_blocked(cls) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_test_runner(cls, options: dict[str, Any]) -> TestRunner:
|
||||
pass
|
||||
33
.venv/lib/python3.11/site-packages/anyio/abc/_resources.py
Normal file
33
.venv/lib/python3.11/site-packages/anyio/abc/_resources.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from types import TracebackType
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class AsyncResource(metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract base class for all closeable asynchronous resources.
|
||||
|
||||
Works as an asynchronous context manager which returns the instance itself on enter,
|
||||
and calls :meth:`aclose` on exit.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
async def __aenter__(self: T) -> T:
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
await self.aclose()
|
||||
|
||||
@abstractmethod
|
||||
async def aclose(self) -> None:
|
||||
"""Close the resource."""
|
||||
405
.venv/lib/python3.11/site-packages/anyio/abc/_sockets.py
Normal file
405
.venv/lib/python3.11/site-packages/anyio/abc/_sockets.py
Normal file
@@ -0,0 +1,405 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import socket
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Collection, Mapping
|
||||
from contextlib import AsyncExitStack
|
||||
from io import IOBase
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from socket import AddressFamily
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from .._core._eventloop import get_async_backend
|
||||
from .._core._typedattr import (
|
||||
TypedAttributeProvider,
|
||||
TypedAttributeSet,
|
||||
typed_attribute,
|
||||
)
|
||||
from ._streams import ByteStream, Listener, UnreliableObjectStream
|
||||
from ._tasks import TaskGroup
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
IPAddressType: TypeAlias = Union[str, IPv4Address, IPv6Address]
|
||||
IPSockAddrType: TypeAlias = tuple[str, int]
|
||||
SockAddrType: TypeAlias = Union[IPSockAddrType, str]
|
||||
UDPPacketType: TypeAlias = tuple[bytes, IPSockAddrType]
|
||||
UNIXDatagramPacketType: TypeAlias = tuple[bytes, str]
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
|
||||
|
||||
def _validate_socket(
|
||||
sock_or_fd: socket.socket | int,
|
||||
sock_type: socket.SocketKind,
|
||||
addr_family: socket.AddressFamily = socket.AF_UNSPEC,
|
||||
*,
|
||||
require_connected: bool = False,
|
||||
require_bound: bool = False,
|
||||
) -> socket.socket:
|
||||
if isinstance(sock_or_fd, int):
|
||||
try:
|
||||
sock = socket.socket(fileno=sock_or_fd)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.ENOTSOCK:
|
||||
raise ValueError(
|
||||
"the file descriptor does not refer to a socket"
|
||||
) from exc
|
||||
elif require_connected:
|
||||
raise ValueError("the socket must be connected") from exc
|
||||
elif require_bound:
|
||||
raise ValueError("the socket must be bound to a local address") from exc
|
||||
else:
|
||||
raise
|
||||
elif isinstance(sock_or_fd, socket.socket):
|
||||
sock = sock_or_fd
|
||||
else:
|
||||
raise TypeError(
|
||||
f"expected an int or socket, got {type(sock_or_fd).__qualname__} instead"
|
||||
)
|
||||
|
||||
try:
|
||||
if require_connected:
|
||||
try:
|
||||
sock.getpeername()
|
||||
except OSError as exc:
|
||||
raise ValueError("the socket must be connected") from exc
|
||||
|
||||
if require_bound:
|
||||
try:
|
||||
if sock.family in (socket.AF_INET, socket.AF_INET6):
|
||||
bound_addr = sock.getsockname()[1]
|
||||
else:
|
||||
bound_addr = sock.getsockname()
|
||||
except OSError:
|
||||
bound_addr = None
|
||||
|
||||
if not bound_addr:
|
||||
raise ValueError("the socket must be bound to a local address")
|
||||
|
||||
if addr_family != socket.AF_UNSPEC and sock.family != addr_family:
|
||||
raise ValueError(
|
||||
f"address family mismatch: expected {addr_family.name}, got "
|
||||
f"{sock.family.name}"
|
||||
)
|
||||
|
||||
if sock.type != sock_type:
|
||||
raise ValueError(
|
||||
f"socket type mismatch: expected {sock_type.name}, got {sock.type.name}"
|
||||
)
|
||||
except BaseException:
|
||||
# Avoid ResourceWarning from the locally constructed socket object
|
||||
if isinstance(sock_or_fd, int):
|
||||
sock.detach()
|
||||
|
||||
raise
|
||||
|
||||
sock.setblocking(False)
|
||||
return sock
|
||||
|
||||
|
||||
class SocketAttribute(TypedAttributeSet):
|
||||
"""
|
||||
.. attribute:: family
|
||||
:type: socket.AddressFamily
|
||||
|
||||
the address family of the underlying socket
|
||||
|
||||
.. attribute:: local_address
|
||||
:type: tuple[str, int] | str
|
||||
|
||||
the local address the underlying socket is connected to
|
||||
|
||||
.. attribute:: local_port
|
||||
:type: int
|
||||
|
||||
for IP based sockets, the local port the underlying socket is bound to
|
||||
|
||||
.. attribute:: raw_socket
|
||||
:type: socket.socket
|
||||
|
||||
the underlying stdlib socket object
|
||||
|
||||
.. attribute:: remote_address
|
||||
:type: tuple[str, int] | str
|
||||
|
||||
the remote address the underlying socket is connected to
|
||||
|
||||
.. attribute:: remote_port
|
||||
:type: int
|
||||
|
||||
for IP based sockets, the remote port the underlying socket is connected to
|
||||
"""
|
||||
|
||||
family: AddressFamily = typed_attribute()
|
||||
local_address: SockAddrType = typed_attribute()
|
||||
local_port: int = typed_attribute()
|
||||
raw_socket: socket.socket = typed_attribute()
|
||||
remote_address: SockAddrType = typed_attribute()
|
||||
remote_port: int = typed_attribute()
|
||||
|
||||
|
||||
class _SocketProvider(TypedAttributeProvider):
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
from .._core._sockets import convert_ipv6_sockaddr as convert
|
||||
|
||||
attributes: dict[Any, Callable[[], Any]] = {
|
||||
SocketAttribute.family: lambda: self._raw_socket.family,
|
||||
SocketAttribute.local_address: lambda: convert(
|
||||
self._raw_socket.getsockname()
|
||||
),
|
||||
SocketAttribute.raw_socket: lambda: self._raw_socket,
|
||||
}
|
||||
try:
|
||||
peername: tuple[str, int] | None = convert(self._raw_socket.getpeername())
|
||||
except OSError:
|
||||
peername = None
|
||||
|
||||
# Provide the remote address for connected sockets
|
||||
if peername is not None:
|
||||
attributes[SocketAttribute.remote_address] = lambda: peername
|
||||
|
||||
# Provide local and remote ports for IP based sockets
|
||||
if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6):
|
||||
attributes[SocketAttribute.local_port] = (
|
||||
lambda: self._raw_socket.getsockname()[1]
|
||||
)
|
||||
if peername is not None:
|
||||
remote_port = peername[1]
|
||||
attributes[SocketAttribute.remote_port] = lambda: remote_port
|
||||
|
||||
return attributes
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _raw_socket(self) -> socket.socket:
|
||||
pass
|
||||
|
||||
|
||||
class SocketStream(ByteStream, _SocketProvider):
|
||||
"""
|
||||
Transports bytes over a socket.
|
||||
|
||||
Supports all relevant extra attributes from :class:`~SocketAttribute`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_socket(cls, sock_or_fd: socket.socket | int) -> SocketStream:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a socket stream.
|
||||
|
||||
The newly created socket wrapper takes ownership of the socket being passed in.
|
||||
The existing socket must already be connected.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a socket stream
|
||||
|
||||
"""
|
||||
sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_connected=True)
|
||||
return await get_async_backend().wrap_stream_socket(sock)
|
||||
|
||||
|
||||
class UNIXSocketStream(SocketStream):
|
||||
@classmethod
|
||||
async def from_socket(cls, sock_or_fd: socket.socket | int) -> UNIXSocketStream:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a UNIX socket stream.
|
||||
|
||||
The newly created socket wrapper takes ownership of the socket being passed in.
|
||||
The existing socket must already be connected.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a UNIX socket stream
|
||||
|
||||
"""
|
||||
sock = _validate_socket(
|
||||
sock_or_fd, socket.SOCK_STREAM, socket.AF_UNIX, require_connected=True
|
||||
)
|
||||
return await get_async_backend().wrap_unix_stream_socket(sock)
|
||||
|
||||
@abstractmethod
|
||||
async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None:
|
||||
"""
|
||||
Send file descriptors along with a message to the peer.
|
||||
|
||||
:param message: a non-empty bytestring
|
||||
:param fds: a collection of files (either numeric file descriptors or open file
|
||||
or socket objects)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]:
|
||||
"""
|
||||
Receive file descriptors along with a message from the peer.
|
||||
|
||||
:param msglen: length of the message to expect from the peer
|
||||
:param maxfds: maximum number of file descriptors to expect from the peer
|
||||
:return: a tuple of (message, file descriptors)
|
||||
"""
|
||||
|
||||
|
||||
class SocketListener(Listener[SocketStream], _SocketProvider):
|
||||
"""
|
||||
Listens to incoming socket connections.
|
||||
|
||||
Supports all relevant extra attributes from :class:`~SocketAttribute`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_socket(
|
||||
cls,
|
||||
sock_or_fd: socket.socket | int,
|
||||
) -> SocketListener:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a socket listener.
|
||||
|
||||
The newly created listener takes ownership of the socket being passed in.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a socket listener
|
||||
|
||||
"""
|
||||
sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_bound=True)
|
||||
return await get_async_backend().wrap_listener_socket(sock)
|
||||
|
||||
@abstractmethod
|
||||
async def accept(self) -> SocketStream:
|
||||
"""Accept an incoming connection."""
|
||||
|
||||
async def serve(
|
||||
self,
|
||||
handler: Callable[[SocketStream], Any],
|
||||
task_group: TaskGroup | None = None,
|
||||
) -> None:
|
||||
from .. import create_task_group
|
||||
|
||||
async with AsyncExitStack() as stack:
|
||||
if task_group is None:
|
||||
task_group = await stack.enter_async_context(create_task_group())
|
||||
|
||||
while True:
|
||||
stream = await self.accept()
|
||||
task_group.start_soon(handler, stream)
|
||||
|
||||
|
||||
class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider):
|
||||
"""
|
||||
Represents an unconnected UDP socket.
|
||||
|
||||
Supports all relevant extra attributes from :class:`~SocketAttribute`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_socket(cls, sock_or_fd: socket.socket | int) -> UDPSocket:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a UDP socket.
|
||||
|
||||
The newly created socket wrapper takes ownership of the socket being passed in.
|
||||
The existing socket must be bound to a local address.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a UDP socket
|
||||
|
||||
"""
|
||||
sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, require_bound=True)
|
||||
return await get_async_backend().wrap_udp_socket(sock)
|
||||
|
||||
async def sendto(self, data: bytes, host: str, port: int) -> None:
|
||||
"""
|
||||
Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))).
|
||||
|
||||
"""
|
||||
return await self.send((data, (host, port)))
|
||||
|
||||
|
||||
class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider):
|
||||
"""
|
||||
Represents an connected UDP socket.
|
||||
|
||||
Supports all relevant extra attributes from :class:`~SocketAttribute`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_socket(cls, sock_or_fd: socket.socket | int) -> ConnectedUDPSocket:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a connected UDP socket.
|
||||
|
||||
The newly created socket wrapper takes ownership of the socket being passed in.
|
||||
The existing socket must already be connected.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a connected UDP socket
|
||||
|
||||
"""
|
||||
sock = _validate_socket(
|
||||
sock_or_fd,
|
||||
socket.SOCK_DGRAM,
|
||||
require_connected=True,
|
||||
)
|
||||
return await get_async_backend().wrap_connected_udp_socket(sock)
|
||||
|
||||
|
||||
class UNIXDatagramSocket(
|
||||
UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider
|
||||
):
|
||||
"""
|
||||
Represents an unconnected Unix datagram socket.
|
||||
|
||||
Supports all relevant extra attributes from :class:`~SocketAttribute`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_socket(
|
||||
cls,
|
||||
sock_or_fd: socket.socket | int,
|
||||
) -> UNIXDatagramSocket:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a UNIX datagram
|
||||
socket.
|
||||
|
||||
The newly created socket wrapper takes ownership of the socket being passed in.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a UNIX datagram socket
|
||||
|
||||
"""
|
||||
sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX)
|
||||
return await get_async_backend().wrap_unix_datagram_socket(sock)
|
||||
|
||||
async def sendto(self, data: bytes, path: str) -> None:
|
||||
"""Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path))."""
|
||||
return await self.send((data, path))
|
||||
|
||||
|
||||
class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider):
|
||||
"""
|
||||
Represents a connected Unix datagram socket.
|
||||
|
||||
Supports all relevant extra attributes from :class:`~SocketAttribute`.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_socket(
|
||||
cls,
|
||||
sock_or_fd: socket.socket | int,
|
||||
) -> ConnectedUNIXDatagramSocket:
|
||||
"""
|
||||
Wrap an existing socket object or file descriptor as a connected UNIX datagram
|
||||
socket.
|
||||
|
||||
The newly created socket wrapper takes ownership of the socket being passed in.
|
||||
The existing socket must already be connected.
|
||||
|
||||
:param sock_or_fd: a socket object or file descriptor
|
||||
:return: a connected UNIX datagram socket
|
||||
|
||||
"""
|
||||
sock = _validate_socket(
|
||||
sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX, require_connected=True
|
||||
)
|
||||
return await get_async_backend().wrap_connected_unix_datagram_socket(sock)
|
||||
239
.venv/lib/python3.11/site-packages/anyio/abc/_streams.py
Normal file
239
.venv/lib/python3.11/site-packages/anyio/abc/_streams.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Generic, TypeVar, Union
|
||||
|
||||
from .._core._exceptions import EndOfStream
|
||||
from .._core._typedattr import TypedAttributeProvider
|
||||
from ._resources import AsyncResource
|
||||
from ._tasks import TaskGroup
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
T_Item = TypeVar("T_Item")
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
T_contra = TypeVar("T_contra", contravariant=True)
|
||||
|
||||
|
||||
class UnreliableObjectReceiveStream(
|
||||
Generic[T_co], AsyncResource, TypedAttributeProvider
|
||||
):
|
||||
"""
|
||||
An interface for receiving objects.
|
||||
|
||||
This interface makes no guarantees that the received messages arrive in the order in
|
||||
which they were sent, or that no messages are missed.
|
||||
|
||||
Asynchronously iterating over objects of this type will yield objects matching the
|
||||
given type parameter.
|
||||
"""
|
||||
|
||||
def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> T_co:
|
||||
try:
|
||||
return await self.receive()
|
||||
except EndOfStream:
|
||||
raise StopAsyncIteration from None
|
||||
|
||||
@abstractmethod
|
||||
async def receive(self) -> T_co:
|
||||
"""
|
||||
Receive the next item.
|
||||
|
||||
:raises ~anyio.ClosedResourceError: if the receive stream has been explicitly
|
||||
closed
|
||||
:raises ~anyio.EndOfStream: if this stream has been closed from the other end
|
||||
:raises ~anyio.BrokenResourceError: if this stream has been rendered unusable
|
||||
due to external causes
|
||||
"""
|
||||
|
||||
|
||||
class UnreliableObjectSendStream(
|
||||
Generic[T_contra], AsyncResource, TypedAttributeProvider
|
||||
):
|
||||
"""
|
||||
An interface for sending objects.
|
||||
|
||||
This interface makes no guarantees that the messages sent will reach the
|
||||
recipient(s) in the same order in which they were sent, or at all.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def send(self, item: T_contra) -> None:
|
||||
"""
|
||||
Send an item to the peer(s).
|
||||
|
||||
:param item: the item to send
|
||||
:raises ~anyio.ClosedResourceError: if the send stream has been explicitly
|
||||
closed
|
||||
:raises ~anyio.BrokenResourceError: if this stream has been rendered unusable
|
||||
due to external causes
|
||||
"""
|
||||
|
||||
|
||||
class UnreliableObjectStream(
|
||||
UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item]
|
||||
):
|
||||
"""
|
||||
A bidirectional message stream which does not guarantee the order or reliability of
|
||||
message delivery.
|
||||
"""
|
||||
|
||||
|
||||
class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]):
|
||||
"""
|
||||
A receive message stream which guarantees that messages are received in the same
|
||||
order in which they were sent, and that no messages are missed.
|
||||
"""
|
||||
|
||||
|
||||
class ObjectSendStream(UnreliableObjectSendStream[T_contra]):
|
||||
"""
|
||||
A send message stream which guarantees that messages are delivered in the same order
|
||||
in which they were sent, without missing any messages in the middle.
|
||||
"""
|
||||
|
||||
|
||||
class ObjectStream(
|
||||
ObjectReceiveStream[T_Item],
|
||||
ObjectSendStream[T_Item],
|
||||
UnreliableObjectStream[T_Item],
|
||||
):
|
||||
"""
|
||||
A bidirectional message stream which guarantees the order and reliability of message
|
||||
delivery.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def send_eof(self) -> None:
|
||||
"""
|
||||
Send an end-of-file indication to the peer.
|
||||
|
||||
You should not try to send any further data to this stream after calling this
|
||||
method. This method is idempotent (does nothing on successive calls).
|
||||
"""
|
||||
|
||||
|
||||
class ByteReceiveStream(AsyncResource, TypedAttributeProvider):
|
||||
"""
|
||||
An interface for receiving bytes from a single peer.
|
||||
|
||||
Iterating this byte stream will yield a byte string of arbitrary length, but no more
|
||||
than 65536 bytes.
|
||||
"""
|
||||
|
||||
def __aiter__(self) -> ByteReceiveStream:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> bytes:
|
||||
try:
|
||||
return await self.receive()
|
||||
except EndOfStream:
|
||||
raise StopAsyncIteration from None
|
||||
|
||||
@abstractmethod
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
"""
|
||||
Receive at most ``max_bytes`` bytes from the peer.
|
||||
|
||||
.. note:: Implementers of this interface should not return an empty
|
||||
:class:`bytes` object, and users should ignore them.
|
||||
|
||||
:param max_bytes: maximum number of bytes to receive
|
||||
:return: the received bytes
|
||||
:raises ~anyio.EndOfStream: if this stream has been closed from the other end
|
||||
"""
|
||||
|
||||
|
||||
class ByteSendStream(AsyncResource, TypedAttributeProvider):
|
||||
"""An interface for sending bytes to a single peer."""
|
||||
|
||||
@abstractmethod
|
||||
async def send(self, item: bytes) -> None:
|
||||
"""
|
||||
Send the given bytes to the peer.
|
||||
|
||||
:param item: the bytes to send
|
||||
"""
|
||||
|
||||
|
||||
class ByteStream(ByteReceiveStream, ByteSendStream):
|
||||
"""A bidirectional byte stream."""
|
||||
|
||||
@abstractmethod
|
||||
async def send_eof(self) -> None:
|
||||
"""
|
||||
Send an end-of-file indication to the peer.
|
||||
|
||||
You should not try to send any further data to this stream after calling this
|
||||
method. This method is idempotent (does nothing on successive calls).
|
||||
"""
|
||||
|
||||
|
||||
#: Type alias for all unreliable bytes-oriented receive streams.
|
||||
AnyUnreliableByteReceiveStream: TypeAlias = Union[
|
||||
UnreliableObjectReceiveStream[bytes], ByteReceiveStream
|
||||
]
|
||||
#: Type alias for all unreliable bytes-oriented send streams.
|
||||
AnyUnreliableByteSendStream: TypeAlias = Union[
|
||||
UnreliableObjectSendStream[bytes], ByteSendStream
|
||||
]
|
||||
#: Type alias for all unreliable bytes-oriented streams.
|
||||
AnyUnreliableByteStream: TypeAlias = Union[UnreliableObjectStream[bytes], ByteStream]
|
||||
#: Type alias for all bytes-oriented receive streams.
|
||||
AnyByteReceiveStream: TypeAlias = Union[ObjectReceiveStream[bytes], ByteReceiveStream]
|
||||
#: Type alias for all bytes-oriented send streams.
|
||||
AnyByteSendStream: TypeAlias = Union[ObjectSendStream[bytes], ByteSendStream]
|
||||
#: Type alias for all bytes-oriented streams.
|
||||
AnyByteStream: TypeAlias = Union[ObjectStream[bytes], ByteStream]
|
||||
|
||||
|
||||
class Listener(Generic[T_co], AsyncResource, TypedAttributeProvider):
|
||||
"""An interface for objects that let you accept incoming connections."""
|
||||
|
||||
@abstractmethod
|
||||
async def serve(
|
||||
self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Accept incoming connections as they come in and start tasks to handle them.
|
||||
|
||||
:param handler: a callable that will be used to handle each accepted connection
|
||||
:param task_group: the task group that will be used to start tasks for handling
|
||||
each accepted connection (if omitted, an ad-hoc task group will be created)
|
||||
"""
|
||||
|
||||
|
||||
class ObjectStreamConnectable(Generic[T_co], metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
async def connect(self) -> ObjectStream[T_co]:
|
||||
"""
|
||||
Connect to the remote endpoint.
|
||||
|
||||
:return: an object stream connected to the remote end
|
||||
:raises ConnectionFailed: if the connection fails
|
||||
"""
|
||||
|
||||
|
||||
class ByteStreamConnectable(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
async def connect(self) -> ByteStream:
|
||||
"""
|
||||
Connect to the remote endpoint.
|
||||
|
||||
:return: a bytestream connected to the remote end
|
||||
:raises ConnectionFailed: if the connection fails
|
||||
"""
|
||||
|
||||
|
||||
#: Type alias for all connectables returning bytestreams or bytes-oriented object streams
|
||||
AnyByteStreamConnectable: TypeAlias = Union[
|
||||
ObjectStreamConnectable[bytes], ByteStreamConnectable
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from signal import Signals
|
||||
|
||||
from ._resources import AsyncResource
|
||||
from ._streams import ByteReceiveStream, ByteSendStream
|
||||
|
||||
|
||||
class Process(AsyncResource):
|
||||
"""An asynchronous version of :class:`subprocess.Popen`."""
|
||||
|
||||
@abstractmethod
|
||||
async def wait(self) -> int:
|
||||
"""
|
||||
Wait until the process exits.
|
||||
|
||||
:return: the exit code of the process
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def terminate(self) -> None:
|
||||
"""
|
||||
Terminates the process, gracefully if possible.
|
||||
|
||||
On Windows, this calls ``TerminateProcess()``.
|
||||
On POSIX systems, this sends ``SIGTERM`` to the process.
|
||||
|
||||
.. seealso:: :meth:`subprocess.Popen.terminate`
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def kill(self) -> None:
|
||||
"""
|
||||
Kills the process.
|
||||
|
||||
On Windows, this calls ``TerminateProcess()``.
|
||||
On POSIX systems, this sends ``SIGKILL`` to the process.
|
||||
|
||||
.. seealso:: :meth:`subprocess.Popen.kill`
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def send_signal(self, signal: Signals) -> None:
|
||||
"""
|
||||
Send a signal to the subprocess.
|
||||
|
||||
.. seealso:: :meth:`subprocess.Popen.send_signal`
|
||||
|
||||
:param signal: the signal number (e.g. :data:`signal.SIGHUP`)
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def pid(self) -> int:
|
||||
"""The process ID of the process."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def returncode(self) -> int | None:
|
||||
"""
|
||||
The return code of the process. If the process has not yet terminated, this will
|
||||
be ``None``.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stdin(self) -> ByteSendStream | None:
|
||||
"""The stream for the standard input of the process."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stdout(self) -> ByteReceiveStream | None:
|
||||
"""The stream for the standard output of the process."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stderr(self) -> ByteReceiveStream | None:
|
||||
"""The stream for the standard error output of the process."""
|
||||
117
.venv/lib/python3.11/site-packages/anyio/abc/_tasks.py
Normal file
117
.venv/lib/python3.11/site-packages/anyio/abc/_tasks.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Protocol, overload
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar
|
||||
else:
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core._tasks import CancelScope
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
T_contra = TypeVar("T_contra", contravariant=True, default=None)
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
|
||||
class TaskStatus(Protocol[T_contra]):
|
||||
@overload
|
||||
def started(self: TaskStatus[None]) -> None: ...
|
||||
|
||||
@overload
|
||||
def started(self, value: T_contra) -> None: ...
|
||||
|
||||
def started(self, value: T_contra | None = None) -> None:
|
||||
"""
|
||||
Signal that the task has started.
|
||||
|
||||
:param value: object passed back to the starter of the task
|
||||
"""
|
||||
|
||||
|
||||
class TaskGroup(metaclass=ABCMeta):
|
||||
"""
|
||||
Groups several asynchronous tasks together.
|
||||
|
||||
:ivar cancel_scope: the cancel scope inherited by all child tasks
|
||||
:vartype cancel_scope: CancelScope
|
||||
|
||||
.. note:: On asyncio, support for eager task factories is considered to be
|
||||
**experimental**. In particular, they don't follow the usual semantics of new
|
||||
tasks being scheduled on the next iteration of the event loop, and may thus
|
||||
cause unexpected behavior in code that wasn't written with such semantics in
|
||||
mind.
|
||||
"""
|
||||
|
||||
cancel_scope: CancelScope
|
||||
|
||||
@abstractmethod
|
||||
def start_soon(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
|
||||
*args: Unpack[PosArgsT],
|
||||
name: object = None,
|
||||
) -> None:
|
||||
"""
|
||||
Start a new task in this task group.
|
||||
|
||||
:param func: a coroutine function
|
||||
:param args: positional arguments to call the function with
|
||||
:param name: name of the task, for the purposes of introspection and debugging
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def start(
|
||||
self,
|
||||
func: Callable[..., Awaitable[Any]],
|
||||
*args: object,
|
||||
name: object = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Start a new task and wait until it signals for readiness.
|
||||
|
||||
The target callable must accept a keyword argument ``task_status`` (of type
|
||||
:class:`TaskStatus`). Awaiting on this method will return whatever was passed to
|
||||
``task_status.started()`` (``None`` by default).
|
||||
|
||||
.. note:: The :class:`TaskStatus` class is generic, and the type argument should
|
||||
indicate the type of the value that will be passed to
|
||||
``task_status.started()``.
|
||||
|
||||
:param func: a coroutine function that accepts the ``task_status`` keyword
|
||||
argument
|
||||
:param args: positional arguments to call the function with
|
||||
:param name: an optional name for the task, for introspection and debugging
|
||||
:return: the value passed to ``task_status.started()``
|
||||
:raises RuntimeError: if the task finishes without calling
|
||||
``task_status.started()``
|
||||
|
||||
.. seealso:: :ref:`start_initialize`
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def __aenter__(self) -> TaskGroup:
|
||||
"""Enter the task group context and allow starting new tasks."""
|
||||
|
||||
@abstractmethod
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool:
|
||||
"""Exit the task group context waiting for all tasks to finish."""
|
||||
65
.venv/lib/python3.11/site-packages/anyio/abc/_testing.py
Normal file
65
.venv/lib/python3.11/site-packages/anyio/abc/_testing.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import types
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class TestRunner(metaclass=ABCMeta):
|
||||
"""
|
||||
Encapsulates a running event loop. Every call made through this object will use the
|
||||
same event loop.
|
||||
"""
|
||||
|
||||
def __enter__(self) -> TestRunner:
|
||||
return self
|
||||
|
||||
@abstractmethod
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: types.TracebackType | None,
|
||||
) -> bool | None: ...
|
||||
|
||||
@abstractmethod
|
||||
def run_asyncgen_fixture(
|
||||
self,
|
||||
fixture_func: Callable[..., AsyncGenerator[_T, Any]],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Iterable[_T]:
|
||||
"""
|
||||
Run an async generator fixture.
|
||||
|
||||
:param fixture_func: the fixture function
|
||||
:param kwargs: keyword arguments to call the fixture function with
|
||||
:return: an iterator yielding the value yielded from the async generator
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def run_fixture(
|
||||
self,
|
||||
fixture_func: Callable[..., Coroutine[Any, Any, _T]],
|
||||
kwargs: dict[str, Any],
|
||||
) -> _T:
|
||||
"""
|
||||
Run an async fixture.
|
||||
|
||||
:param fixture_func: the fixture function
|
||||
:param kwargs: keyword arguments to call the fixture function with
|
||||
:return: the return value of the fixture function
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def run_test(
|
||||
self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Run an async test function.
|
||||
|
||||
:param test_func: the test function
|
||||
:param kwargs: keyword arguments to call the test function with
|
||||
"""
|
||||
578
.venv/lib/python3.11/site-packages/anyio/from_thread.py
Normal file
578
.venv/lib/python3.11/site-packages/anyio/from_thread.py
Normal file
@@ -0,0 +1,578 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"BlockingPortal",
|
||||
"BlockingPortalProvider",
|
||||
"check_cancelled",
|
||||
"run",
|
||||
"run_sync",
|
||||
"start_blocking_portal",
|
||||
)
|
||||
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from concurrent.futures import Future
|
||||
from contextlib import (
|
||||
AbstractAsyncContextManager,
|
||||
AbstractContextManager,
|
||||
contextmanager,
|
||||
)
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from threading import Lock, Thread, current_thread, get_ident
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
TypeVar,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from ._core._eventloop import (
|
||||
get_cancelled_exc_class,
|
||||
threadlocals,
|
||||
)
|
||||
from ._core._eventloop import run as run_eventloop
|
||||
from ._core._exceptions import NoEventLoopError
|
||||
from ._core._synchronization import Event
|
||||
from ._core._tasks import CancelScope, create_task_group
|
||||
from .abc._tasks import TaskStatus
|
||||
from .lowlevel import EventLoopToken, current_token
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
|
||||
def _token_or_error(token: EventLoopToken | None) -> EventLoopToken:
|
||||
if token is not None:
|
||||
return token
|
||||
|
||||
try:
|
||||
return threadlocals.current_token
|
||||
except AttributeError:
|
||||
raise NoEventLoopError(
|
||||
"Not running inside an AnyIO worker thread, and no event loop token was "
|
||||
"provided"
|
||||
) from None
|
||||
|
||||
|
||||
def run(
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
*args: Unpack[PosArgsT],
|
||||
token: EventLoopToken | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Call a coroutine function from a worker thread.
|
||||
|
||||
:param func: a coroutine function
|
||||
:param args: positional arguments for the callable
|
||||
:param token: an event loop token to use to get back to the event loop thread
|
||||
(required if calling this function from outside an AnyIO worker thread)
|
||||
:return: the return value of the coroutine function
|
||||
:raises MissingTokenError: if no token was provided and called from outside an
|
||||
AnyIO worker thread
|
||||
:raises RunFinishedError: if the event loop tied to ``token`` is no longer running
|
||||
|
||||
.. versionchanged:: 4.11.0
|
||||
Added the ``token`` parameter.
|
||||
|
||||
"""
|
||||
explicit_token = token is not None
|
||||
token = _token_or_error(token)
|
||||
return token.backend_class.run_async_from_thread(
|
||||
func, args, token=token.native_token if explicit_token else None
|
||||
)
|
||||
|
||||
|
||||
def run_sync(
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
token: EventLoopToken | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Call a function in the event loop thread from a worker thread.
|
||||
|
||||
:param func: a callable
|
||||
:param args: positional arguments for the callable
|
||||
:param token: an event loop token to use to get back to the event loop thread
|
||||
(required if calling this function from outside an AnyIO worker thread)
|
||||
:return: the return value of the callable
|
||||
:raises MissingTokenError: if no token was provided and called from outside an
|
||||
AnyIO worker thread
|
||||
:raises RunFinishedError: if the event loop tied to ``token`` is no longer running
|
||||
|
||||
.. versionchanged:: 4.11.0
|
||||
Added the ``token`` parameter.
|
||||
|
||||
"""
|
||||
explicit_token = token is not None
|
||||
token = _token_or_error(token)
|
||||
return token.backend_class.run_sync_from_thread(
|
||||
func, args, token=token.native_token if explicit_token else None
|
||||
)
|
||||
|
||||
|
||||
class _BlockingAsyncContextManager(Generic[T_co], AbstractContextManager):
|
||||
_enter_future: Future[T_co]
|
||||
_exit_future: Future[bool | None]
|
||||
_exit_event: Event
|
||||
_exit_exc_info: tuple[
|
||||
type[BaseException] | None, BaseException | None, TracebackType | None
|
||||
] = (None, None, None)
|
||||
|
||||
def __init__(
|
||||
self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal
|
||||
):
|
||||
self._async_cm = async_cm
|
||||
self._portal = portal
|
||||
|
||||
async def run_async_cm(self) -> bool | None:
|
||||
try:
|
||||
self._exit_event = Event()
|
||||
value = await self._async_cm.__aenter__()
|
||||
except BaseException as exc:
|
||||
self._enter_future.set_exception(exc)
|
||||
raise
|
||||
else:
|
||||
self._enter_future.set_result(value)
|
||||
|
||||
try:
|
||||
# Wait for the sync context manager to exit.
|
||||
# This next statement can raise `get_cancelled_exc_class()` if
|
||||
# something went wrong in a task group in this async context
|
||||
# manager.
|
||||
await self._exit_event.wait()
|
||||
finally:
|
||||
# In case of cancellation, it could be that we end up here before
|
||||
# `_BlockingAsyncContextManager.__exit__` is called, and an
|
||||
# `_exit_exc_info` has been set.
|
||||
result = await self._async_cm.__aexit__(*self._exit_exc_info)
|
||||
|
||||
return result
|
||||
|
||||
def __enter__(self) -> T_co:
|
||||
self._enter_future = Future()
|
||||
self._exit_future = self._portal.start_task_soon(self.run_async_cm)
|
||||
return self._enter_future.result()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
__exc_type: type[BaseException] | None,
|
||||
__exc_value: BaseException | None,
|
||||
__traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
self._exit_exc_info = __exc_type, __exc_value, __traceback
|
||||
self._portal.call(self._exit_event.set)
|
||||
return self._exit_future.result()
|
||||
|
||||
|
||||
class _BlockingPortalTaskStatus(TaskStatus):
|
||||
def __init__(self, future: Future):
|
||||
self._future = future
|
||||
|
||||
def started(self, value: object = None) -> None:
|
||||
self._future.set_result(value)
|
||||
|
||||
|
||||
class BlockingPortal:
|
||||
"""
|
||||
An object that lets external threads run code in an asynchronous event loop.
|
||||
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._token = current_token()
|
||||
self._event_loop_thread_id: int | None = get_ident()
|
||||
self._stop_event = Event()
|
||||
self._task_group = create_task_group()
|
||||
|
||||
async def __aenter__(self) -> BlockingPortal:
|
||||
await self._task_group.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool:
|
||||
await self.stop()
|
||||
return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
def _check_running(self) -> None:
|
||||
if self._event_loop_thread_id is None:
|
||||
raise RuntimeError("This portal is not running")
|
||||
if self._event_loop_thread_id == get_ident():
|
||||
raise RuntimeError(
|
||||
"This method cannot be called from the event loop thread"
|
||||
)
|
||||
|
||||
async def sleep_until_stopped(self) -> None:
|
||||
"""Sleep until :meth:`stop` is called."""
|
||||
await self._stop_event.wait()
|
||||
|
||||
async def stop(self, cancel_remaining: bool = False) -> None:
|
||||
"""
|
||||
Signal the portal to shut down.
|
||||
|
||||
This marks the portal as no longer accepting new calls and exits from
|
||||
:meth:`sleep_until_stopped`.
|
||||
|
||||
:param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False``
|
||||
to let them finish before returning
|
||||
|
||||
"""
|
||||
self._event_loop_thread_id = None
|
||||
self._stop_event.set()
|
||||
if cancel_remaining:
|
||||
self._task_group.cancel_scope.cancel("the blocking portal is shutting down")
|
||||
|
||||
async def _call_func(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
||||
args: tuple[Unpack[PosArgsT]],
|
||||
kwargs: dict[str, Any],
|
||||
future: Future[T_Retval],
|
||||
) -> None:
|
||||
def callback(f: Future[T_Retval]) -> None:
|
||||
if f.cancelled():
|
||||
if self._event_loop_thread_id == get_ident():
|
||||
scope.cancel("the future was cancelled")
|
||||
elif self._event_loop_thread_id is not None:
|
||||
self.call(scope.cancel, "the future was cancelled")
|
||||
|
||||
try:
|
||||
retval_or_awaitable = func(*args, **kwargs)
|
||||
if isawaitable(retval_or_awaitable):
|
||||
with CancelScope() as scope:
|
||||
future.add_done_callback(callback)
|
||||
retval = await retval_or_awaitable
|
||||
else:
|
||||
retval = retval_or_awaitable
|
||||
except get_cancelled_exc_class():
|
||||
future.cancel()
|
||||
future.set_running_or_notify_cancel()
|
||||
except BaseException as exc:
|
||||
if not future.cancelled():
|
||||
future.set_exception(exc)
|
||||
|
||||
# Let base exceptions fall through
|
||||
if not isinstance(exc, Exception):
|
||||
raise
|
||||
else:
|
||||
if not future.cancelled():
|
||||
future.set_result(retval)
|
||||
finally:
|
||||
scope = None # type: ignore[assignment]
|
||||
|
||||
def _spawn_task_from_thread(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
||||
args: tuple[Unpack[PosArgsT]],
|
||||
kwargs: dict[str, Any],
|
||||
name: object,
|
||||
future: Future[T_Retval],
|
||||
) -> None:
|
||||
"""
|
||||
Spawn a new task using the given callable.
|
||||
|
||||
:param func: a callable
|
||||
:param args: positional arguments to be passed to the callable
|
||||
:param kwargs: keyword arguments to be passed to the callable
|
||||
:param name: name of the task (will be coerced to a string if not ``None``)
|
||||
:param future: a future that will resolve to the return value of the callable,
|
||||
or the exception raised during its execution
|
||||
|
||||
"""
|
||||
run_sync(
|
||||
partial(self._task_group.start_soon, name=name),
|
||||
self._call_func,
|
||||
func,
|
||||
args,
|
||||
kwargs,
|
||||
future,
|
||||
token=self._token,
|
||||
)
|
||||
|
||||
@overload
|
||||
def call(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
*args: Unpack[PosArgsT],
|
||||
) -> T_Retval: ...
|
||||
|
||||
@overload
|
||||
def call(
|
||||
self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT]
|
||||
) -> T_Retval: ...
|
||||
|
||||
def call(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Call the given function in the event loop thread.
|
||||
|
||||
If the callable returns a coroutine object, it is awaited on.
|
||||
|
||||
:param func: any callable
|
||||
:raises RuntimeError: if the portal is not running or if this method is called
|
||||
from within the event loop thread
|
||||
|
||||
"""
|
||||
return cast(T_Retval, self.start_task_soon(func, *args).result())
|
||||
|
||||
@overload
|
||||
def start_task_soon(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
||||
*args: Unpack[PosArgsT],
|
||||
name: object = None,
|
||||
) -> Future[T_Retval]: ...
|
||||
|
||||
@overload
|
||||
def start_task_soon(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
name: object = None,
|
||||
) -> Future[T_Retval]: ...
|
||||
|
||||
def start_task_soon(
|
||||
self,
|
||||
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
name: object = None,
|
||||
) -> Future[T_Retval]:
|
||||
"""
|
||||
Start a task in the portal's task group.
|
||||
|
||||
The task will be run inside a cancel scope which can be cancelled by cancelling
|
||||
the returned future.
|
||||
|
||||
:param func: the target function
|
||||
:param args: positional arguments passed to ``func``
|
||||
:param name: name of the task (will be coerced to a string if not ``None``)
|
||||
:return: a future that resolves with the return value of the callable if the
|
||||
task completes successfully, or with the exception raised in the task
|
||||
:raises RuntimeError: if the portal is not running or if this method is called
|
||||
from within the event loop thread
|
||||
:rtype: concurrent.futures.Future[T_Retval]
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
self._check_running()
|
||||
f: Future[T_Retval] = Future()
|
||||
self._spawn_task_from_thread(func, args, {}, name, f)
|
||||
return f
|
||||
|
||||
def start_task(
|
||||
self,
|
||||
func: Callable[..., Awaitable[T_Retval]],
|
||||
*args: object,
|
||||
name: object = None,
|
||||
) -> tuple[Future[T_Retval], Any]:
|
||||
"""
|
||||
Start a task in the portal's task group and wait until it signals for readiness.
|
||||
|
||||
This method works the same way as :meth:`.abc.TaskGroup.start`.
|
||||
|
||||
:param func: the target function
|
||||
:param args: positional arguments passed to ``func``
|
||||
:param name: name of the task (will be coerced to a string if not ``None``)
|
||||
:return: a tuple of (future, task_status_value) where the ``task_status_value``
|
||||
is the value passed to ``task_status.started()`` from within the target
|
||||
function
|
||||
:rtype: tuple[concurrent.futures.Future[T_Retval], Any]
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
|
||||
def task_done(future: Future[T_Retval]) -> None:
|
||||
if not task_status_future.done():
|
||||
if future.cancelled():
|
||||
task_status_future.cancel()
|
||||
elif future.exception():
|
||||
task_status_future.set_exception(future.exception())
|
||||
else:
|
||||
exc = RuntimeError(
|
||||
"Task exited without calling task_status.started()"
|
||||
)
|
||||
task_status_future.set_exception(exc)
|
||||
|
||||
self._check_running()
|
||||
task_status_future: Future = Future()
|
||||
task_status = _BlockingPortalTaskStatus(task_status_future)
|
||||
f: Future = Future()
|
||||
f.add_done_callback(task_done)
|
||||
self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f)
|
||||
return f, task_status_future.result()
|
||||
|
||||
def wrap_async_context_manager(
|
||||
self, cm: AbstractAsyncContextManager[T_co]
|
||||
) -> AbstractContextManager[T_co]:
|
||||
"""
|
||||
Wrap an async context manager as a synchronous context manager via this portal.
|
||||
|
||||
Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping
|
||||
in the middle until the synchronous context manager exits.
|
||||
|
||||
:param cm: an asynchronous context manager
|
||||
:return: a synchronous context manager
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
"""
|
||||
return _BlockingAsyncContextManager(cm, self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockingPortalProvider:
|
||||
"""
|
||||
A manager for a blocking portal. Used as a context manager. The first thread to
|
||||
enter this context manager causes a blocking portal to be started with the specific
|
||||
parameters, and the last thread to exit causes the portal to be shut down. Thus,
|
||||
there will be exactly one blocking portal running in this context as long as at
|
||||
least one thread has entered this context manager.
|
||||
|
||||
The parameters are the same as for :func:`~anyio.run`.
|
||||
|
||||
:param backend: name of the backend
|
||||
:param backend_options: backend options
|
||||
|
||||
.. versionadded:: 4.4
|
||||
"""
|
||||
|
||||
backend: str = "asyncio"
|
||||
backend_options: dict[str, Any] | None = None
|
||||
_lock: Lock = field(init=False, default_factory=Lock)
|
||||
_leases: int = field(init=False, default=0)
|
||||
_portal: BlockingPortal = field(init=False)
|
||||
_portal_cm: AbstractContextManager[BlockingPortal] | None = field(
|
||||
init=False, default=None
|
||||
)
|
||||
|
||||
def __enter__(self) -> BlockingPortal:
|
||||
with self._lock:
|
||||
if self._portal_cm is None:
|
||||
self._portal_cm = start_blocking_portal(
|
||||
self.backend, self.backend_options
|
||||
)
|
||||
self._portal = self._portal_cm.__enter__()
|
||||
|
||||
self._leases += 1
|
||||
return self._portal
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
portal_cm: AbstractContextManager[BlockingPortal] | None = None
|
||||
with self._lock:
|
||||
assert self._portal_cm
|
||||
assert self._leases > 0
|
||||
self._leases -= 1
|
||||
if not self._leases:
|
||||
portal_cm = self._portal_cm
|
||||
self._portal_cm = None
|
||||
del self._portal
|
||||
|
||||
if portal_cm:
|
||||
portal_cm.__exit__(None, None, None)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def start_blocking_portal(
|
||||
backend: str = "asyncio",
|
||||
backend_options: dict[str, Any] | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
) -> Generator[BlockingPortal, Any, None]:
|
||||
"""
|
||||
Start a new event loop in a new thread and run a blocking portal in its main task.
|
||||
|
||||
The parameters are the same as for :func:`~anyio.run`.
|
||||
|
||||
:param backend: name of the backend
|
||||
:param backend_options: backend options
|
||||
:param name: name of the thread
|
||||
:return: a context manager that yields a blocking portal
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Usage as a context manager is now required.
|
||||
|
||||
"""
|
||||
|
||||
async def run_portal() -> None:
|
||||
async with BlockingPortal() as portal_:
|
||||
if name is None:
|
||||
current_thread().name = f"{backend}-portal-{id(portal_):x}"
|
||||
|
||||
future.set_result(portal_)
|
||||
await portal_.sleep_until_stopped()
|
||||
|
||||
def run_blocking_portal() -> None:
|
||||
if future.set_running_or_notify_cancel():
|
||||
try:
|
||||
run_eventloop(
|
||||
run_portal, backend=backend, backend_options=backend_options
|
||||
)
|
||||
except BaseException as exc:
|
||||
if not future.done():
|
||||
future.set_exception(exc)
|
||||
|
||||
future: Future[BlockingPortal] = Future()
|
||||
thread = Thread(target=run_blocking_portal, daemon=True, name=name)
|
||||
thread.start()
|
||||
try:
|
||||
cancel_remaining_tasks = False
|
||||
portal = future.result()
|
||||
try:
|
||||
yield portal
|
||||
except BaseException:
|
||||
cancel_remaining_tasks = True
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
portal.call(portal.stop, cancel_remaining_tasks)
|
||||
except RuntimeError:
|
||||
pass
|
||||
finally:
|
||||
thread.join()
|
||||
|
||||
|
||||
def check_cancelled() -> None:
|
||||
"""
|
||||
Check if the cancel scope of the host task's running the current worker thread has
|
||||
been cancelled.
|
||||
|
||||
If the host task's current cancel scope has indeed been cancelled, the
|
||||
backend-specific cancellation exception will be raised.
|
||||
|
||||
:raises RuntimeError: if the current thread was not spawned by
|
||||
:func:`.to_thread.run_sync`
|
||||
|
||||
"""
|
||||
try:
|
||||
token: EventLoopToken = threadlocals.current_token
|
||||
except AttributeError:
|
||||
raise NoEventLoopError(
|
||||
"This function can only be called inside an AnyIO worker thread"
|
||||
) from None
|
||||
|
||||
token.backend_class.check_cancelled()
|
||||
375
.venv/lib/python3.11/site-packages/anyio/functools.py
Normal file
375
.venv/lib/python3.11/site-packages/anyio/functools.py
Normal file
@@ -0,0 +1,375 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"AsyncCacheInfo",
|
||||
"AsyncCacheParameters",
|
||||
"AsyncLRUCacheWrapper",
|
||||
"cache",
|
||||
"lru_cache",
|
||||
"reduce",
|
||||
)
|
||||
|
||||
import functools
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from collections.abc import (
|
||||
AsyncIterable,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Hashable,
|
||||
Iterable,
|
||||
)
|
||||
from functools import update_wrapper
|
||||
from inspect import iscoroutinefunction
|
||||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
NamedTuple,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
cast,
|
||||
final,
|
||||
overload,
|
||||
)
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from ._core._synchronization import Lock
|
||||
from .lowlevel import RunVar, checkpoint
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import ParamSpec
|
||||
else:
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
P = ParamSpec("P")
|
||||
lru_cache_items: RunVar[
|
||||
WeakKeyDictionary[
|
||||
AsyncLRUCacheWrapper[Any, Any],
|
||||
OrderedDict[Hashable, tuple[_InitialMissingType, Lock] | tuple[Any, None]],
|
||||
]
|
||||
] = RunVar("lru_cache_items")
|
||||
|
||||
|
||||
class _InitialMissingType:
|
||||
pass
|
||||
|
||||
|
||||
initial_missing: _InitialMissingType = _InitialMissingType()
|
||||
|
||||
|
||||
class AsyncCacheInfo(NamedTuple):
|
||||
hits: int
|
||||
misses: int
|
||||
maxsize: int | None
|
||||
currsize: int
|
||||
|
||||
|
||||
class AsyncCacheParameters(TypedDict):
|
||||
maxsize: int | None
|
||||
typed: bool
|
||||
always_checkpoint: bool
|
||||
|
||||
|
||||
class _LRUMethodWrapper(Generic[T]):
|
||||
def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object):
|
||||
self.__wrapper = wrapper
|
||||
self.__instance = instance
|
||||
|
||||
def cache_info(self) -> AsyncCacheInfo:
|
||||
return self.__wrapper.cache_info()
|
||||
|
||||
def cache_parameters(self) -> AsyncCacheParameters:
|
||||
return self.__wrapper.cache_parameters()
|
||||
|
||||
def cache_clear(self) -> None:
|
||||
self.__wrapper.cache_clear()
|
||||
|
||||
async def __call__(self, *args: Any, **kwargs: Any) -> T:
|
||||
if self.__instance is None:
|
||||
return await self.__wrapper(*args, **kwargs)
|
||||
|
||||
return await self.__wrapper(self.__instance, *args, **kwargs)
|
||||
|
||||
|
||||
@final
|
||||
class AsyncLRUCacheWrapper(Generic[P, T]):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[P, Awaitable[T]],
|
||||
maxsize: int | None,
|
||||
typed: bool,
|
||||
always_checkpoint: bool,
|
||||
):
|
||||
self.__wrapped__ = func
|
||||
self._hits: int = 0
|
||||
self._misses: int = 0
|
||||
self._maxsize = max(maxsize, 0) if maxsize is not None else None
|
||||
self._currsize: int = 0
|
||||
self._typed = typed
|
||||
self._always_checkpoint = always_checkpoint
|
||||
update_wrapper(self, func)
|
||||
|
||||
def cache_info(self) -> AsyncCacheInfo:
|
||||
return AsyncCacheInfo(self._hits, self._misses, self._maxsize, self._currsize)
|
||||
|
||||
def cache_parameters(self) -> AsyncCacheParameters:
|
||||
return {
|
||||
"maxsize": self._maxsize,
|
||||
"typed": self._typed,
|
||||
"always_checkpoint": self._always_checkpoint,
|
||||
}
|
||||
|
||||
def cache_clear(self) -> None:
|
||||
if cache := lru_cache_items.get(None):
|
||||
cache.pop(self, None)
|
||||
self._hits = self._misses = self._currsize = 0
|
||||
|
||||
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||
# Easy case first: if maxsize == 0, no caching is done
|
||||
if self._maxsize == 0:
|
||||
value = await self.__wrapped__(*args, **kwargs)
|
||||
self._misses += 1
|
||||
return value
|
||||
|
||||
# The key is constructed as a flat tuple to avoid memory overhead
|
||||
key: tuple[Any, ...] = args
|
||||
if kwargs:
|
||||
# initial_missing is used as a separator
|
||||
key += (initial_missing,) + sum(kwargs.items(), ())
|
||||
|
||||
if self._typed:
|
||||
key += tuple(type(arg) for arg in args)
|
||||
if kwargs:
|
||||
key += (initial_missing,) + tuple(type(val) for val in kwargs.values())
|
||||
|
||||
try:
|
||||
cache = lru_cache_items.get()
|
||||
except LookupError:
|
||||
cache = WeakKeyDictionary()
|
||||
lru_cache_items.set(cache)
|
||||
|
||||
try:
|
||||
cache_entry = cache[self]
|
||||
except KeyError:
|
||||
cache_entry = cache[self] = OrderedDict()
|
||||
|
||||
cached_value: T | _InitialMissingType
|
||||
try:
|
||||
cached_value, lock = cache_entry[key]
|
||||
except KeyError:
|
||||
# We're the first task to call this function
|
||||
cached_value, lock = (
|
||||
initial_missing,
|
||||
Lock(fast_acquire=not self._always_checkpoint),
|
||||
)
|
||||
cache_entry[key] = cached_value, lock
|
||||
|
||||
if lock is None:
|
||||
# The value was already cached
|
||||
self._hits += 1
|
||||
cache_entry.move_to_end(key)
|
||||
if self._always_checkpoint:
|
||||
await checkpoint()
|
||||
|
||||
return cast(T, cached_value)
|
||||
|
||||
async with lock:
|
||||
# Check if another task filled the cache while we acquired the lock
|
||||
if (cached_value := cache_entry[key][0]) is initial_missing:
|
||||
self._misses += 1
|
||||
if self._maxsize is not None and self._currsize >= self._maxsize:
|
||||
cache_entry.popitem(last=False)
|
||||
else:
|
||||
self._currsize += 1
|
||||
|
||||
value = await self.__wrapped__(*args, **kwargs)
|
||||
cache_entry[key] = value, None
|
||||
else:
|
||||
# Another task filled the cache while we were waiting for the lock
|
||||
self._hits += 1
|
||||
cache_entry.move_to_end(key)
|
||||
value = cast(T, cached_value)
|
||||
|
||||
return value
|
||||
|
||||
def __get__(
|
||||
self, instance: object, owner: type | None = None
|
||||
) -> _LRUMethodWrapper[T]:
|
||||
wrapper = _LRUMethodWrapper(self, instance)
|
||||
update_wrapper(wrapper, self.__wrapped__)
|
||||
return wrapper
|
||||
|
||||
|
||||
class _LRUCacheWrapper(Generic[T]):
|
||||
def __init__(self, maxsize: int | None, typed: bool, always_checkpoint: bool):
|
||||
self._maxsize = maxsize
|
||||
self._typed = typed
|
||||
self._always_checkpoint = always_checkpoint
|
||||
|
||||
@overload
|
||||
def __call__( # type: ignore[overload-overlap]
|
||||
self, func: Callable[P, Coroutine[Any, Any, T]], /
|
||||
) -> AsyncLRUCacheWrapper[P, T]: ...
|
||||
|
||||
@overload
|
||||
def __call__(
|
||||
self, func: Callable[..., T], /
|
||||
) -> functools._lru_cache_wrapper[T]: ...
|
||||
|
||||
def __call__(
|
||||
self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], /
|
||||
) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
|
||||
if iscoroutinefunction(f):
|
||||
return AsyncLRUCacheWrapper(
|
||||
f, self._maxsize, self._typed, self._always_checkpoint
|
||||
)
|
||||
|
||||
return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@overload
|
||||
def cache( # type: ignore[overload-overlap]
|
||||
func: Callable[P, Coroutine[Any, Any, T]], /
|
||||
) -> AsyncLRUCacheWrapper[P, T]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
|
||||
|
||||
|
||||
def cache(
|
||||
func: Callable[..., T] | Callable[P, Coroutine[Any, Any, T]], /
|
||||
) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
|
||||
"""
|
||||
A convenient shortcut for :func:`lru_cache` with ``maxsize=None``.
|
||||
|
||||
This is the asynchronous equivalent to :func:`functools.cache`.
|
||||
|
||||
"""
|
||||
return lru_cache(maxsize=None)(func)
|
||||
|
||||
|
||||
@overload
|
||||
def lru_cache(
|
||||
*, maxsize: int | None = ..., typed: bool = ..., always_checkpoint: bool = ...
|
||||
) -> _LRUCacheWrapper[Any]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def lru_cache( # type: ignore[overload-overlap]
|
||||
func: Callable[P, Coroutine[Any, Any, T]], /
|
||||
) -> AsyncLRUCacheWrapper[P, T]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
|
||||
|
||||
|
||||
def lru_cache(
|
||||
func: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T] | None = None,
|
||||
/,
|
||||
*,
|
||||
maxsize: int | None = 128,
|
||||
typed: bool = False,
|
||||
always_checkpoint: bool = False,
|
||||
) -> (
|
||||
AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T] | _LRUCacheWrapper[Any]
|
||||
):
|
||||
"""
|
||||
An asynchronous version of :func:`functools.lru_cache`.
|
||||
|
||||
If a synchronous function is passed, the standard library
|
||||
:func:`functools.lru_cache` is applied instead.
|
||||
|
||||
:param always_checkpoint: if ``True``, every call to the cached function will be
|
||||
guaranteed to yield control to the event loop at least once
|
||||
|
||||
.. note:: Caches and locks are managed on a per-event loop basis.
|
||||
|
||||
"""
|
||||
if func is None:
|
||||
return _LRUCacheWrapper[Any](maxsize, typed, always_checkpoint)
|
||||
|
||||
if not callable(func):
|
||||
raise TypeError("the first argument must be callable")
|
||||
|
||||
return _LRUCacheWrapper[T](maxsize, typed, always_checkpoint)(func)
|
||||
|
||||
|
||||
@overload
|
||||
async def reduce(
|
||||
function: Callable[[T, S], Awaitable[T]],
|
||||
iterable: Iterable[S] | AsyncIterable[S],
|
||||
/,
|
||||
initial: T,
|
||||
) -> T: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def reduce(
|
||||
function: Callable[[T, T], Awaitable[T]],
|
||||
iterable: Iterable[T] | AsyncIterable[T],
|
||||
/,
|
||||
) -> T: ...
|
||||
|
||||
|
||||
async def reduce( # type: ignore[misc]
|
||||
function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]],
|
||||
iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S],
|
||||
/,
|
||||
initial: T | _InitialMissingType = initial_missing,
|
||||
) -> T:
|
||||
"""
|
||||
Asynchronous version of :func:`functools.reduce`.
|
||||
|
||||
:param function: a coroutine function that takes two arguments: the accumulated
|
||||
value and the next element from the iterable
|
||||
:param iterable: an iterable or async iterable
|
||||
:param initial: the initial value (if missing, the first element of the iterable is
|
||||
used as the initial value)
|
||||
|
||||
"""
|
||||
element: Any
|
||||
function_called = False
|
||||
if isinstance(iterable, AsyncIterable):
|
||||
async_it = iterable.__aiter__()
|
||||
if initial is initial_missing:
|
||||
try:
|
||||
value = cast(T, await async_it.__anext__())
|
||||
except StopAsyncIteration:
|
||||
raise TypeError(
|
||||
"reduce() of empty sequence with no initial value"
|
||||
) from None
|
||||
else:
|
||||
value = cast(T, initial)
|
||||
|
||||
async for element in async_it:
|
||||
value = await function(value, element)
|
||||
function_called = True
|
||||
elif isinstance(iterable, Iterable):
|
||||
it = iter(iterable)
|
||||
if initial is initial_missing:
|
||||
try:
|
||||
value = cast(T, next(it))
|
||||
except StopIteration:
|
||||
raise TypeError(
|
||||
"reduce() of empty sequence with no initial value"
|
||||
) from None
|
||||
else:
|
||||
value = cast(T, initial)
|
||||
|
||||
for element in it:
|
||||
value = await function(value, element)
|
||||
function_called = True
|
||||
else:
|
||||
raise TypeError("reduce() argument 2 must be an iterable or async iterable")
|
||||
|
||||
# Make sure there is at least one checkpoint, even if an empty iterable and an
|
||||
# initial value were given
|
||||
if not function_called:
|
||||
await checkpoint()
|
||||
|
||||
return value
|
||||
196
.venv/lib/python3.11/site-packages/anyio/lowlevel.py
Normal file
196
.venv/lib/python3.11/site-packages/anyio/lowlevel.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"EventLoopToken",
|
||||
"RunvarToken",
|
||||
"RunVar",
|
||||
"checkpoint",
|
||||
"checkpoint_if_cancelled",
|
||||
"cancel_shielded_checkpoint",
|
||||
"current_token",
|
||||
)
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import Any, Generic, Literal, TypeVar, final, overload
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from ._core._eventloop import get_async_backend
|
||||
from .abc import AsyncBackend
|
||||
|
||||
T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
async def checkpoint() -> None:
|
||||
"""
|
||||
Check for cancellation and allow the scheduler to switch to another task.
|
||||
|
||||
Equivalent to (but more efficient than)::
|
||||
|
||||
await checkpoint_if_cancelled()
|
||||
await cancel_shielded_checkpoint()
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
await get_async_backend().checkpoint()
|
||||
|
||||
|
||||
async def checkpoint_if_cancelled() -> None:
|
||||
"""
|
||||
Enter a checkpoint if the enclosing cancel scope has been cancelled.
|
||||
|
||||
This does not allow the scheduler to switch to a different task.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
await get_async_backend().checkpoint_if_cancelled()
|
||||
|
||||
|
||||
async def cancel_shielded_checkpoint() -> None:
|
||||
"""
|
||||
Allow the scheduler to switch to another task but without checking for cancellation.
|
||||
|
||||
Equivalent to (but potentially more efficient than)::
|
||||
|
||||
with CancelScope(shield=True):
|
||||
await checkpoint()
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
await get_async_backend().cancel_shielded_checkpoint()
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, repr=False)
|
||||
class EventLoopToken:
|
||||
"""
|
||||
An opaque object that holds a reference to an event loop.
|
||||
|
||||
.. versionadded:: 4.11.0
|
||||
"""
|
||||
|
||||
backend_class: type[AsyncBackend]
|
||||
native_token: object
|
||||
|
||||
|
||||
def current_token() -> EventLoopToken:
|
||||
"""
|
||||
Return a token object that can be used to call code in the current event loop from
|
||||
another thread.
|
||||
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
.. versionadded:: 4.11.0
|
||||
|
||||
"""
|
||||
backend_class = get_async_backend()
|
||||
raw_token = backend_class.current_token()
|
||||
return EventLoopToken(backend_class, raw_token)
|
||||
|
||||
|
||||
_run_vars: WeakKeyDictionary[object, dict[RunVar[Any], Any]] = WeakKeyDictionary()
|
||||
|
||||
|
||||
class _NoValueSet(enum.Enum):
|
||||
NO_VALUE_SET = enum.auto()
|
||||
|
||||
|
||||
class RunvarToken(Generic[T]):
|
||||
__slots__ = "_var", "_value", "_redeemed"
|
||||
|
||||
def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]):
|
||||
self._var = var
|
||||
self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value
|
||||
self._redeemed = False
|
||||
|
||||
def __enter__(self) -> RunvarToken[T]:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self._var.reset(self)
|
||||
|
||||
|
||||
class RunVar(Generic[T]):
|
||||
"""
|
||||
Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop.
|
||||
|
||||
Can be used as a context manager, Just like :class:`~contextvars.ContextVar`, that
|
||||
will reset the variable to its previous value when the context block is exited.
|
||||
"""
|
||||
|
||||
__slots__ = "_name", "_default"
|
||||
|
||||
NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET
|
||||
|
||||
def __init__(
|
||||
self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET
|
||||
):
|
||||
self._name = name
|
||||
self._default = default
|
||||
|
||||
@property
|
||||
def _current_vars(self) -> dict[RunVar[T], T]:
|
||||
native_token = current_token().native_token
|
||||
try:
|
||||
return _run_vars[native_token]
|
||||
except KeyError:
|
||||
run_vars = _run_vars[native_token] = {}
|
||||
return run_vars
|
||||
|
||||
@overload
|
||||
def get(self, default: D) -> T | D: ...
|
||||
|
||||
@overload
|
||||
def get(self) -> T: ...
|
||||
|
||||
def get(
|
||||
self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET
|
||||
) -> T | D:
|
||||
try:
|
||||
return self._current_vars[self]
|
||||
except KeyError:
|
||||
if default is not RunVar.NO_VALUE_SET:
|
||||
return default
|
||||
elif self._default is not RunVar.NO_VALUE_SET:
|
||||
return self._default
|
||||
|
||||
raise LookupError(
|
||||
f'Run variable "{self._name}" has no value and no default set'
|
||||
)
|
||||
|
||||
def set(self, value: T) -> RunvarToken[T]:
|
||||
current_vars = self._current_vars
|
||||
token = RunvarToken(self, current_vars.get(self, RunVar.NO_VALUE_SET))
|
||||
current_vars[self] = value
|
||||
return token
|
||||
|
||||
def reset(self, token: RunvarToken[T]) -> None:
|
||||
if token._var is not self:
|
||||
raise ValueError("This token does not belong to this RunVar")
|
||||
|
||||
if token._redeemed:
|
||||
raise ValueError("This token has already been used")
|
||||
|
||||
if token._value is _NoValueSet.NO_VALUE_SET:
|
||||
try:
|
||||
del self._current_vars[self]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self._current_vars[self] = token._value
|
||||
|
||||
token._redeemed = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RunVar name={self._name!r}>"
|
||||
0
.venv/lib/python3.11/site-packages/anyio/py.typed
Normal file
0
.venv/lib/python3.11/site-packages/anyio/py.typed
Normal file
302
.venv/lib/python3.11/site-packages/anyio/pytest_plugin.py
Normal file
302
.venv/lib/python3.11/site-packages/anyio/pytest_plugin.py
Normal file
@@ -0,0 +1,302 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Callable, Generator, Iterator
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from inspect import isasyncgenfunction, iscoroutinefunction, ismethod
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import SubRequest
|
||||
from _pytest.outcomes import Exit
|
||||
|
||||
from . import get_available_backends
|
||||
from ._core._eventloop import (
|
||||
current_async_library,
|
||||
get_async_backend,
|
||||
reset_current_async_library,
|
||||
set_current_async_library,
|
||||
)
|
||||
from ._core._exceptions import iterate_exceptions
|
||||
from .abc import TestRunner
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
_current_runner: TestRunner | None = None
|
||||
_runner_stack: ExitStack | None = None
|
||||
_runner_leases = 0
|
||||
|
||||
|
||||
def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]:
|
||||
if isinstance(backend, str):
|
||||
return backend, {}
|
||||
elif isinstance(backend, tuple) and len(backend) == 2:
|
||||
if isinstance(backend[0], str) and isinstance(backend[1], dict):
|
||||
return cast(tuple[str, dict[str, Any]], backend)
|
||||
|
||||
raise TypeError("anyio_backend must be either a string or tuple of (string, dict)")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_runner(
|
||||
backend_name: str, backend_options: dict[str, Any]
|
||||
) -> Iterator[TestRunner]:
|
||||
global _current_runner, _runner_leases, _runner_stack
|
||||
if _current_runner is None:
|
||||
asynclib = get_async_backend(backend_name)
|
||||
_runner_stack = ExitStack()
|
||||
if current_async_library() is None:
|
||||
# Since we're in control of the event loop, we can cache the name of the
|
||||
# async library
|
||||
token = set_current_async_library(backend_name)
|
||||
_runner_stack.callback(reset_current_async_library, token)
|
||||
|
||||
backend_options = backend_options or {}
|
||||
_current_runner = _runner_stack.enter_context(
|
||||
asynclib.create_test_runner(backend_options)
|
||||
)
|
||||
|
||||
_runner_leases += 1
|
||||
try:
|
||||
yield _current_runner
|
||||
finally:
|
||||
_runner_leases -= 1
|
||||
if not _runner_leases:
|
||||
assert _runner_stack is not None
|
||||
_runner_stack.close()
|
||||
_runner_stack = _current_runner = None
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addini(
|
||||
"anyio_mode",
|
||||
default="strict",
|
||||
help='AnyIO plugin mode (either "strict" or "auto")',
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"anyio: mark the (coroutine function) test to be run asynchronously via anyio.",
|
||||
)
|
||||
if (
|
||||
config.getini("anyio_mode") == "auto"
|
||||
and config.pluginmanager.has_plugin("asyncio")
|
||||
and config.getini("asyncio_mode") == "auto"
|
||||
):
|
||||
config.issue_config_time_warning(
|
||||
pytest.PytestConfigWarning(
|
||||
"AnyIO auto mode has been enabled together with pytest-asyncio auto "
|
||||
"mode. This may cause unexpected behavior."
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]:
|
||||
def wrapper(anyio_backend: Any, request: SubRequest, **kwargs: Any) -> Any:
|
||||
# Rebind any fixture methods to the request instance
|
||||
if (
|
||||
request.instance
|
||||
and ismethod(func)
|
||||
and type(func.__self__) is type(request.instance)
|
||||
):
|
||||
local_func = func.__func__.__get__(request.instance)
|
||||
else:
|
||||
local_func = func
|
||||
|
||||
backend_name, backend_options = extract_backend_and_options(anyio_backend)
|
||||
if has_backend_arg:
|
||||
kwargs["anyio_backend"] = anyio_backend
|
||||
|
||||
if has_request_arg:
|
||||
kwargs["request"] = request
|
||||
|
||||
with get_runner(backend_name, backend_options) as runner:
|
||||
if isasyncgenfunction(local_func):
|
||||
yield from runner.run_asyncgen_fixture(local_func, kwargs)
|
||||
else:
|
||||
yield runner.run_fixture(local_func, kwargs)
|
||||
|
||||
# Only apply this to coroutine functions and async generator functions in requests
|
||||
# that involve the anyio_backend fixture
|
||||
func = fixturedef.func
|
||||
if isasyncgenfunction(func) or iscoroutinefunction(func):
|
||||
if "anyio_backend" in request.fixturenames:
|
||||
fixturedef.func = wrapper
|
||||
original_argname = fixturedef.argnames
|
||||
|
||||
if not (has_backend_arg := "anyio_backend" in fixturedef.argnames):
|
||||
fixturedef.argnames += ("anyio_backend",)
|
||||
|
||||
if not (has_request_arg := "request" in fixturedef.argnames):
|
||||
fixturedef.argnames += ("request",)
|
||||
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
fixturedef.func = func
|
||||
fixturedef.argnames = original_argname
|
||||
|
||||
return (yield)
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: pytest.Module | pytest.Class, name: str, obj: object
|
||||
) -> None:
|
||||
if collector.istestfunction(obj, name):
|
||||
inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj
|
||||
if iscoroutinefunction(inner_func):
|
||||
anyio_auto_mode = collector.config.getini("anyio_mode") == "auto"
|
||||
marker = collector.get_closest_marker("anyio")
|
||||
own_markers = getattr(obj, "pytestmark", ())
|
||||
if (
|
||||
anyio_auto_mode
|
||||
or marker
|
||||
or any(marker.name == "anyio" for marker in own_markers)
|
||||
):
|
||||
pytest.mark.usefixtures("anyio_backend")(obj)
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None:
|
||||
def run_with_hypothesis(**kwargs: Any) -> None:
|
||||
with get_runner(backend_name, backend_options) as runner:
|
||||
runner.run_test(original_func, kwargs)
|
||||
|
||||
backend = pyfuncitem.funcargs.get("anyio_backend")
|
||||
if backend:
|
||||
backend_name, backend_options = extract_backend_and_options(backend)
|
||||
|
||||
if hasattr(pyfuncitem.obj, "hypothesis"):
|
||||
# Wrap the inner test function unless it's already wrapped
|
||||
original_func = pyfuncitem.obj.hypothesis.inner_test
|
||||
if original_func.__qualname__ != run_with_hypothesis.__qualname__:
|
||||
if iscoroutinefunction(original_func):
|
||||
pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis
|
||||
|
||||
return None
|
||||
|
||||
if iscoroutinefunction(pyfuncitem.obj):
|
||||
funcargs = pyfuncitem.funcargs
|
||||
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
|
||||
with get_runner(backend_name, backend_options) as runner:
|
||||
try:
|
||||
runner.run_test(pyfuncitem.obj, testargs)
|
||||
except ExceptionGroup as excgrp:
|
||||
for exc in iterate_exceptions(excgrp):
|
||||
if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)):
|
||||
raise exc from excgrp
|
||||
|
||||
raise
|
||||
|
||||
return True
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=get_available_backends())
|
||||
def anyio_backend(request: Any) -> Any:
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend_name(anyio_backend: Any) -> str:
|
||||
if isinstance(anyio_backend, str):
|
||||
return anyio_backend
|
||||
else:
|
||||
return anyio_backend[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]:
|
||||
if isinstance(anyio_backend, str):
|
||||
return {}
|
||||
else:
|
||||
return anyio_backend[1]
|
||||
|
||||
|
||||
class FreePortFactory:
|
||||
"""
|
||||
Manages port generation based on specified socket kind, ensuring no duplicate
|
||||
ports are generated.
|
||||
|
||||
This class provides functionality for generating available free ports on the
|
||||
system. It is initialized with a specific socket kind and can generate ports
|
||||
for given address families while avoiding reuse of previously generated ports.
|
||||
|
||||
Users should not instantiate this class directly, but use the
|
||||
``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple
|
||||
uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead.
|
||||
"""
|
||||
|
||||
def __init__(self, kind: socket.SocketKind) -> None:
|
||||
self._kind = kind
|
||||
self._generated = set[int]()
|
||||
|
||||
@property
|
||||
def kind(self) -> socket.SocketKind:
|
||||
"""
|
||||
The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or
|
||||
:data:`~socket.SOCK_DGRAM`) used to bind for checking port availability
|
||||
|
||||
"""
|
||||
return self._kind
|
||||
|
||||
def __call__(self, family: socket.AddressFamily | None = None) -> int:
|
||||
"""
|
||||
Return an unbound port for the given address family.
|
||||
|
||||
:param family: if omitted, both IPv4 and IPv6 addresses will be tried
|
||||
:return: a port number
|
||||
|
||||
"""
|
||||
if family is not None:
|
||||
families = [family]
|
||||
else:
|
||||
families = [socket.AF_INET]
|
||||
if socket.has_ipv6:
|
||||
families.append(socket.AF_INET6)
|
||||
|
||||
while True:
|
||||
port = 0
|
||||
with ExitStack() as stack:
|
||||
for family in families:
|
||||
sock = stack.enter_context(socket.socket(family, self._kind))
|
||||
addr = "::1" if family == socket.AF_INET6 else "127.0.0.1"
|
||||
try:
|
||||
sock.bind((addr, port))
|
||||
except OSError:
|
||||
break
|
||||
|
||||
if not port:
|
||||
port = sock.getsockname()[1]
|
||||
else:
|
||||
if port not in self._generated:
|
||||
self._generated.add(port)
|
||||
return port
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def free_tcp_port_factory() -> FreePortFactory:
|
||||
return FreePortFactory(socket.SOCK_STREAM)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def free_udp_port_factory() -> FreePortFactory:
|
||||
return FreePortFactory(socket.SOCK_DGRAM)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int:
|
||||
return free_tcp_port_factory()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int:
|
||||
return free_udp_port_factory()
|
||||
188
.venv/lib/python3.11/site-packages/anyio/streams/buffered.py
Normal file
188
.venv/lib/python3.11/site-packages/anyio/streams/buffered.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"BufferedByteReceiveStream",
|
||||
"BufferedByteStream",
|
||||
"BufferedConnectable",
|
||||
)
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, SupportsIndex
|
||||
|
||||
from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead
|
||||
from ..abc import (
|
||||
AnyByteReceiveStream,
|
||||
AnyByteStream,
|
||||
AnyByteStreamConnectable,
|
||||
ByteReceiveStream,
|
||||
ByteStream,
|
||||
ByteStreamConnectable,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class BufferedByteReceiveStream(ByteReceiveStream):
|
||||
"""
|
||||
Wraps any bytes-based receive stream and uses a buffer to provide sophisticated
|
||||
receiving capabilities in the form of a byte stream.
|
||||
"""
|
||||
|
||||
receive_stream: AnyByteReceiveStream
|
||||
_buffer: bytearray = field(init=False, default_factory=bytearray)
|
||||
_closed: bool = field(init=False, default=False)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self.receive_stream.aclose()
|
||||
self._closed = True
|
||||
|
||||
@property
|
||||
def buffer(self) -> bytes:
|
||||
"""The bytes currently in the buffer."""
|
||||
return bytes(self._buffer)
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return self.receive_stream.extra_attributes
|
||||
|
||||
def feed_data(self, data: Iterable[SupportsIndex], /) -> None:
|
||||
"""
|
||||
Append data directly into the buffer.
|
||||
|
||||
Any data in the buffer will be consumed by receive operations before receiving
|
||||
anything from the wrapped stream.
|
||||
|
||||
:param data: the data to append to the buffer (can be bytes or anything else
|
||||
that supports ``__index__()``)
|
||||
|
||||
"""
|
||||
self._buffer.extend(data)
|
||||
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
|
||||
if self._buffer:
|
||||
chunk = bytes(self._buffer[:max_bytes])
|
||||
del self._buffer[:max_bytes]
|
||||
return chunk
|
||||
elif isinstance(self.receive_stream, ByteReceiveStream):
|
||||
return await self.receive_stream.receive(max_bytes)
|
||||
else:
|
||||
# With a bytes-oriented object stream, we need to handle any surplus bytes
|
||||
# we get from the receive() call
|
||||
chunk = await self.receive_stream.receive()
|
||||
if len(chunk) > max_bytes:
|
||||
# Save the surplus bytes in the buffer
|
||||
self._buffer.extend(chunk[max_bytes:])
|
||||
return chunk[:max_bytes]
|
||||
else:
|
||||
return chunk
|
||||
|
||||
async def receive_exactly(self, nbytes: int) -> bytes:
|
||||
"""
|
||||
Read exactly the given amount of bytes from the stream.
|
||||
|
||||
:param nbytes: the number of bytes to read
|
||||
:return: the bytes read
|
||||
:raises ~anyio.IncompleteRead: if the stream was closed before the requested
|
||||
amount of bytes could be read from the stream
|
||||
|
||||
"""
|
||||
while True:
|
||||
remaining = nbytes - len(self._buffer)
|
||||
if remaining <= 0:
|
||||
retval = self._buffer[:nbytes]
|
||||
del self._buffer[:nbytes]
|
||||
return bytes(retval)
|
||||
|
||||
try:
|
||||
if isinstance(self.receive_stream, ByteReceiveStream):
|
||||
chunk = await self.receive_stream.receive(remaining)
|
||||
else:
|
||||
chunk = await self.receive_stream.receive()
|
||||
except EndOfStream as exc:
|
||||
raise IncompleteRead from exc
|
||||
|
||||
self._buffer.extend(chunk)
|
||||
|
||||
async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes:
|
||||
"""
|
||||
Read from the stream until the delimiter is found or max_bytes have been read.
|
||||
|
||||
:param delimiter: the marker to look for in the stream
|
||||
:param max_bytes: maximum number of bytes that will be read before raising
|
||||
:exc:`~anyio.DelimiterNotFound`
|
||||
:return: the bytes read (not including the delimiter)
|
||||
:raises ~anyio.IncompleteRead: if the stream was closed before the delimiter
|
||||
was found
|
||||
:raises ~anyio.DelimiterNotFound: if the delimiter is not found within the
|
||||
bytes read up to the maximum allowed
|
||||
|
||||
"""
|
||||
delimiter_size = len(delimiter)
|
||||
offset = 0
|
||||
while True:
|
||||
# Check if the delimiter can be found in the current buffer
|
||||
index = self._buffer.find(delimiter, offset)
|
||||
if index >= 0:
|
||||
found = self._buffer[:index]
|
||||
del self._buffer[: index + len(delimiter) :]
|
||||
return bytes(found)
|
||||
|
||||
# Check if the buffer is already at or over the limit
|
||||
if len(self._buffer) >= max_bytes:
|
||||
raise DelimiterNotFound(max_bytes)
|
||||
|
||||
# Read more data into the buffer from the socket
|
||||
try:
|
||||
data = await self.receive_stream.receive()
|
||||
except EndOfStream as exc:
|
||||
raise IncompleteRead from exc
|
||||
|
||||
# Move the offset forward and add the new data to the buffer
|
||||
offset = max(len(self._buffer) - delimiter_size + 1, 0)
|
||||
self._buffer.extend(data)
|
||||
|
||||
|
||||
class BufferedByteStream(BufferedByteReceiveStream, ByteStream):
|
||||
"""
|
||||
A full-duplex variant of :class:`BufferedByteReceiveStream`. All writes are passed
|
||||
through to the wrapped stream as-is.
|
||||
"""
|
||||
|
||||
def __init__(self, stream: AnyByteStream):
|
||||
"""
|
||||
:param stream: the stream to be wrapped
|
||||
|
||||
"""
|
||||
super().__init__(stream)
|
||||
self._stream = stream
|
||||
|
||||
@override
|
||||
async def send_eof(self) -> None:
|
||||
await self._stream.send_eof()
|
||||
|
||||
@override
|
||||
async def send(self, item: bytes) -> None:
|
||||
await self._stream.send(item)
|
||||
|
||||
|
||||
class BufferedConnectable(ByteStreamConnectable):
|
||||
def __init__(self, connectable: AnyByteStreamConnectable):
|
||||
"""
|
||||
:param connectable: the connectable to wrap
|
||||
|
||||
"""
|
||||
self.connectable = connectable
|
||||
|
||||
@override
|
||||
async def connect(self) -> BufferedByteStream:
|
||||
stream = await self.connectable.connect()
|
||||
return BufferedByteStream(stream)
|
||||
154
.venv/lib/python3.11/site-packages/anyio/streams/file.py
Normal file
154
.venv/lib/python3.11/site-packages/anyio/streams/file.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"FileReadStream",
|
||||
"FileStreamAttribute",
|
||||
"FileWriteStream",
|
||||
)
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from io import SEEK_SET, UnsupportedOperation
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO, cast
|
||||
|
||||
from .. import (
|
||||
BrokenResourceError,
|
||||
ClosedResourceError,
|
||||
EndOfStream,
|
||||
TypedAttributeSet,
|
||||
to_thread,
|
||||
typed_attribute,
|
||||
)
|
||||
from ..abc import ByteReceiveStream, ByteSendStream
|
||||
|
||||
|
||||
class FileStreamAttribute(TypedAttributeSet):
|
||||
#: the open file descriptor
|
||||
file: BinaryIO = typed_attribute()
|
||||
#: the path of the file on the file system, if available (file must be a real file)
|
||||
path: Path = typed_attribute()
|
||||
#: the file number, if available (file must be a real file or a TTY)
|
||||
fileno: int = typed_attribute()
|
||||
|
||||
|
||||
class _BaseFileStream:
|
||||
def __init__(self, file: BinaryIO):
|
||||
self._file = file
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await to_thread.run_sync(self._file.close)
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
attributes: dict[Any, Callable[[], Any]] = {
|
||||
FileStreamAttribute.file: lambda: self._file,
|
||||
}
|
||||
|
||||
if hasattr(self._file, "name"):
|
||||
attributes[FileStreamAttribute.path] = lambda: Path(self._file.name)
|
||||
|
||||
try:
|
||||
self._file.fileno()
|
||||
except UnsupportedOperation:
|
||||
pass
|
||||
else:
|
||||
attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno()
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
class FileReadStream(_BaseFileStream, ByteReceiveStream):
|
||||
"""
|
||||
A byte stream that reads from a file in the file system.
|
||||
|
||||
:param file: a file that has been opened for reading in binary mode
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_path(cls, path: str | PathLike[str]) -> FileReadStream:
|
||||
"""
|
||||
Create a file read stream by opening the given file.
|
||||
|
||||
:param path: path of the file to read from
|
||||
|
||||
"""
|
||||
file = await to_thread.run_sync(Path(path).open, "rb")
|
||||
return cls(cast(BinaryIO, file))
|
||||
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
try:
|
||||
data = await to_thread.run_sync(self._file.read, max_bytes)
|
||||
except ValueError:
|
||||
raise ClosedResourceError from None
|
||||
except OSError as exc:
|
||||
raise BrokenResourceError from exc
|
||||
|
||||
if data:
|
||||
return data
|
||||
else:
|
||||
raise EndOfStream
|
||||
|
||||
async def seek(self, position: int, whence: int = SEEK_SET) -> int:
|
||||
"""
|
||||
Seek the file to the given position.
|
||||
|
||||
.. seealso:: :meth:`io.IOBase.seek`
|
||||
|
||||
.. note:: Not all file descriptors are seekable.
|
||||
|
||||
:param position: position to seek the file to
|
||||
:param whence: controls how ``position`` is interpreted
|
||||
:return: the new absolute position
|
||||
:raises OSError: if the file is not seekable
|
||||
|
||||
"""
|
||||
return await to_thread.run_sync(self._file.seek, position, whence)
|
||||
|
||||
async def tell(self) -> int:
|
||||
"""
|
||||
Return the current stream position.
|
||||
|
||||
.. note:: Not all file descriptors are seekable.
|
||||
|
||||
:return: the current absolute position
|
||||
:raises OSError: if the file is not seekable
|
||||
|
||||
"""
|
||||
return await to_thread.run_sync(self._file.tell)
|
||||
|
||||
|
||||
class FileWriteStream(_BaseFileStream, ByteSendStream):
|
||||
"""
|
||||
A byte stream that writes to a file in the file system.
|
||||
|
||||
:param file: a file that has been opened for writing in binary mode
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def from_path(
|
||||
cls, path: str | PathLike[str], append: bool = False
|
||||
) -> FileWriteStream:
|
||||
"""
|
||||
Create a file write stream by opening the given file for writing.
|
||||
|
||||
:param path: path of the file to write to
|
||||
:param append: if ``True``, open the file for appending; if ``False``, any
|
||||
existing file at the given path will be truncated
|
||||
|
||||
"""
|
||||
mode = "ab" if append else "wb"
|
||||
file = await to_thread.run_sync(Path(path).open, mode)
|
||||
return cls(cast(BinaryIO, file))
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
try:
|
||||
await to_thread.run_sync(self._file.write, item)
|
||||
except ValueError:
|
||||
raise ClosedResourceError from None
|
||||
except OSError as exc:
|
||||
raise BrokenResourceError from exc
|
||||
325
.venv/lib/python3.11/site-packages/anyio/streams/memory.py
Normal file
325
.venv/lib/python3.11/site-packages/anyio/streams/memory.py
Normal file
@@ -0,0 +1,325 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"MemoryObjectReceiveStream",
|
||||
"MemoryObjectSendStream",
|
||||
"MemoryObjectStreamStatistics",
|
||||
)
|
||||
|
||||
import warnings
|
||||
from collections import OrderedDict, deque
|
||||
from dataclasses import dataclass, field
|
||||
from types import TracebackType
|
||||
from typing import Generic, NamedTuple, TypeVar
|
||||
|
||||
from .. import (
|
||||
BrokenResourceError,
|
||||
ClosedResourceError,
|
||||
EndOfStream,
|
||||
WouldBlock,
|
||||
)
|
||||
from .._core._testing import TaskInfo, get_current_task
|
||||
from ..abc import Event, ObjectReceiveStream, ObjectSendStream
|
||||
from ..lowlevel import checkpoint
|
||||
|
||||
T_Item = TypeVar("T_Item")
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
T_contra = TypeVar("T_contra", contravariant=True)
|
||||
|
||||
|
||||
class MemoryObjectStreamStatistics(NamedTuple):
|
||||
current_buffer_used: int #: number of items stored in the buffer
|
||||
#: maximum number of items that can be stored on this stream (or :data:`math.inf`)
|
||||
max_buffer_size: float
|
||||
open_send_streams: int #: number of unclosed clones of the send stream
|
||||
open_receive_streams: int #: number of unclosed clones of the receive stream
|
||||
#: number of tasks blocked on :meth:`MemoryObjectSendStream.send`
|
||||
tasks_waiting_send: int
|
||||
#: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive`
|
||||
tasks_waiting_receive: int
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class _MemoryObjectItemReceiver(Generic[T_Item]):
|
||||
task_info: TaskInfo = field(init=False, default_factory=get_current_task)
|
||||
item: T_Item = field(init=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# When item is not defined, we get following error with default __repr__:
|
||||
# AttributeError: 'MemoryObjectItemReceiver' object has no attribute 'item'
|
||||
item = getattr(self, "item", None)
|
||||
return f"{self.__class__.__name__}(task_info={self.task_info}, item={item!r})"
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class _MemoryObjectStreamState(Generic[T_Item]):
|
||||
max_buffer_size: float = field()
|
||||
buffer: deque[T_Item] = field(init=False, default_factory=deque)
|
||||
open_send_channels: int = field(init=False, default=0)
|
||||
open_receive_channels: int = field(init=False, default=0)
|
||||
waiting_receivers: OrderedDict[Event, _MemoryObjectItemReceiver[T_Item]] = field(
|
||||
init=False, default_factory=OrderedDict
|
||||
)
|
||||
waiting_senders: OrderedDict[Event, T_Item] = field(
|
||||
init=False, default_factory=OrderedDict
|
||||
)
|
||||
|
||||
def statistics(self) -> MemoryObjectStreamStatistics:
|
||||
return MemoryObjectStreamStatistics(
|
||||
len(self.buffer),
|
||||
self.max_buffer_size,
|
||||
self.open_send_channels,
|
||||
self.open_receive_channels,
|
||||
len(self.waiting_senders),
|
||||
len(self.waiting_receivers),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class MemoryObjectReceiveStream(Generic[T_co], ObjectReceiveStream[T_co]):
|
||||
_state: _MemoryObjectStreamState[T_co]
|
||||
_closed: bool = field(init=False, default=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._state.open_receive_channels += 1
|
||||
|
||||
def receive_nowait(self) -> T_co:
|
||||
"""
|
||||
Receive the next item if it can be done without waiting.
|
||||
|
||||
:return: the received item
|
||||
:raises ~anyio.ClosedResourceError: if this send stream has been closed
|
||||
:raises ~anyio.EndOfStream: if the buffer is empty and this stream has been
|
||||
closed from the sending end
|
||||
:raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks
|
||||
waiting to send
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
|
||||
if self._state.waiting_senders:
|
||||
# Get the item from the next sender
|
||||
send_event, item = self._state.waiting_senders.popitem(last=False)
|
||||
self._state.buffer.append(item)
|
||||
send_event.set()
|
||||
|
||||
if self._state.buffer:
|
||||
return self._state.buffer.popleft()
|
||||
elif not self._state.open_send_channels:
|
||||
raise EndOfStream
|
||||
|
||||
raise WouldBlock
|
||||
|
||||
async def receive(self) -> T_co:
|
||||
await checkpoint()
|
||||
try:
|
||||
return self.receive_nowait()
|
||||
except WouldBlock:
|
||||
# Add ourselves in the queue
|
||||
receive_event = Event()
|
||||
receiver = _MemoryObjectItemReceiver[T_co]()
|
||||
self._state.waiting_receivers[receive_event] = receiver
|
||||
|
||||
try:
|
||||
await receive_event.wait()
|
||||
finally:
|
||||
self._state.waiting_receivers.pop(receive_event, None)
|
||||
|
||||
try:
|
||||
return receiver.item
|
||||
except AttributeError:
|
||||
raise EndOfStream from None
|
||||
|
||||
def clone(self) -> MemoryObjectReceiveStream[T_co]:
|
||||
"""
|
||||
Create a clone of this receive stream.
|
||||
|
||||
Each clone can be closed separately. Only when all clones have been closed will
|
||||
the receiving end of the memory stream be considered closed by the sending ends.
|
||||
|
||||
:return: the cloned stream
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
|
||||
return MemoryObjectReceiveStream(_state=self._state)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the stream.
|
||||
|
||||
This works the exact same way as :meth:`aclose`, but is provided as a special
|
||||
case for the benefit of synchronous callbacks.
|
||||
|
||||
"""
|
||||
if not self._closed:
|
||||
self._closed = True
|
||||
self._state.open_receive_channels -= 1
|
||||
if self._state.open_receive_channels == 0:
|
||||
send_events = list(self._state.waiting_senders.keys())
|
||||
for event in send_events:
|
||||
event.set()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
self.close()
|
||||
|
||||
def statistics(self) -> MemoryObjectStreamStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this stream.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return self._state.statistics()
|
||||
|
||||
def __enter__(self) -> MemoryObjectReceiveStream[T_co]:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.close()
|
||||
|
||||
def __del__(self) -> None:
|
||||
if not self._closed:
|
||||
warnings.warn(
|
||||
f"Unclosed <{self.__class__.__name__} at {id(self):x}>",
|
||||
ResourceWarning,
|
||||
stacklevel=1,
|
||||
source=self,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class MemoryObjectSendStream(Generic[T_contra], ObjectSendStream[T_contra]):
|
||||
_state: _MemoryObjectStreamState[T_contra]
|
||||
_closed: bool = field(init=False, default=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._state.open_send_channels += 1
|
||||
|
||||
def send_nowait(self, item: T_contra) -> None:
|
||||
"""
|
||||
Send an item immediately if it can be done without waiting.
|
||||
|
||||
:param item: the item to send
|
||||
:raises ~anyio.ClosedResourceError: if this send stream has been closed
|
||||
:raises ~anyio.BrokenResourceError: if the stream has been closed from the
|
||||
receiving end
|
||||
:raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting
|
||||
to receive
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
if not self._state.open_receive_channels:
|
||||
raise BrokenResourceError
|
||||
|
||||
while self._state.waiting_receivers:
|
||||
receive_event, receiver = self._state.waiting_receivers.popitem(last=False)
|
||||
if not receiver.task_info.has_pending_cancellation():
|
||||
receiver.item = item
|
||||
receive_event.set()
|
||||
return
|
||||
|
||||
if len(self._state.buffer) < self._state.max_buffer_size:
|
||||
self._state.buffer.append(item)
|
||||
else:
|
||||
raise WouldBlock
|
||||
|
||||
async def send(self, item: T_contra) -> None:
|
||||
"""
|
||||
Send an item to the stream.
|
||||
|
||||
If the buffer is full, this method blocks until there is again room in the
|
||||
buffer or the item can be sent directly to a receiver.
|
||||
|
||||
:param item: the item to send
|
||||
:raises ~anyio.ClosedResourceError: if this send stream has been closed
|
||||
:raises ~anyio.BrokenResourceError: if the stream has been closed from the
|
||||
receiving end
|
||||
|
||||
"""
|
||||
await checkpoint()
|
||||
try:
|
||||
self.send_nowait(item)
|
||||
except WouldBlock:
|
||||
# Wait until there's someone on the receiving end
|
||||
send_event = Event()
|
||||
self._state.waiting_senders[send_event] = item
|
||||
try:
|
||||
await send_event.wait()
|
||||
except BaseException:
|
||||
self._state.waiting_senders.pop(send_event, None)
|
||||
raise
|
||||
|
||||
if send_event in self._state.waiting_senders:
|
||||
del self._state.waiting_senders[send_event]
|
||||
raise BrokenResourceError from None
|
||||
|
||||
def clone(self) -> MemoryObjectSendStream[T_contra]:
|
||||
"""
|
||||
Create a clone of this send stream.
|
||||
|
||||
Each clone can be closed separately. Only when all clones have been closed will
|
||||
the sending end of the memory stream be considered closed by the receiving ends.
|
||||
|
||||
:return: the cloned stream
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
|
||||
return MemoryObjectSendStream(_state=self._state)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the stream.
|
||||
|
||||
This works the exact same way as :meth:`aclose`, but is provided as a special
|
||||
case for the benefit of synchronous callbacks.
|
||||
|
||||
"""
|
||||
if not self._closed:
|
||||
self._closed = True
|
||||
self._state.open_send_channels -= 1
|
||||
if self._state.open_send_channels == 0:
|
||||
receive_events = list(self._state.waiting_receivers.keys())
|
||||
self._state.waiting_receivers.clear()
|
||||
for event in receive_events:
|
||||
event.set()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
self.close()
|
||||
|
||||
def statistics(self) -> MemoryObjectStreamStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this stream.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return self._state.statistics()
|
||||
|
||||
def __enter__(self) -> MemoryObjectSendStream[T_contra]:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.close()
|
||||
|
||||
def __del__(self) -> None:
|
||||
if not self._closed:
|
||||
warnings.warn(
|
||||
f"Unclosed <{self.__class__.__name__} at {id(self):x}>",
|
||||
ResourceWarning,
|
||||
stacklevel=1,
|
||||
source=self,
|
||||
)
|
||||
147
.venv/lib/python3.11/site-packages/anyio/streams/stapled.py
Normal file
147
.venv/lib/python3.11/site-packages/anyio/streams/stapled.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"MultiListener",
|
||||
"StapledByteStream",
|
||||
"StapledObjectStream",
|
||||
)
|
||||
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from ..abc import (
|
||||
ByteReceiveStream,
|
||||
ByteSendStream,
|
||||
ByteStream,
|
||||
Listener,
|
||||
ObjectReceiveStream,
|
||||
ObjectSendStream,
|
||||
ObjectStream,
|
||||
TaskGroup,
|
||||
)
|
||||
|
||||
T_Item = TypeVar("T_Item")
|
||||
T_Stream = TypeVar("T_Stream")
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class StapledByteStream(ByteStream):
|
||||
"""
|
||||
Combines two byte streams into a single, bidirectional byte stream.
|
||||
|
||||
Extra attributes will be provided from both streams, with the receive stream
|
||||
providing the values in case of a conflict.
|
||||
|
||||
:param ByteSendStream send_stream: the sending byte stream
|
||||
:param ByteReceiveStream receive_stream: the receiving byte stream
|
||||
"""
|
||||
|
||||
send_stream: ByteSendStream
|
||||
receive_stream: ByteReceiveStream
|
||||
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
return await self.receive_stream.receive(max_bytes)
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
await self.send_stream.send(item)
|
||||
|
||||
async def send_eof(self) -> None:
|
||||
await self.send_stream.aclose()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self.send_stream.aclose()
|
||||
await self.receive_stream.aclose()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return {
|
||||
**self.send_stream.extra_attributes,
|
||||
**self.receive_stream.extra_attributes,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class StapledObjectStream(Generic[T_Item], ObjectStream[T_Item]):
|
||||
"""
|
||||
Combines two object streams into a single, bidirectional object stream.
|
||||
|
||||
Extra attributes will be provided from both streams, with the receive stream
|
||||
providing the values in case of a conflict.
|
||||
|
||||
:param ObjectSendStream send_stream: the sending object stream
|
||||
:param ObjectReceiveStream receive_stream: the receiving object stream
|
||||
"""
|
||||
|
||||
send_stream: ObjectSendStream[T_Item]
|
||||
receive_stream: ObjectReceiveStream[T_Item]
|
||||
|
||||
async def receive(self) -> T_Item:
|
||||
return await self.receive_stream.receive()
|
||||
|
||||
async def send(self, item: T_Item) -> None:
|
||||
await self.send_stream.send(item)
|
||||
|
||||
async def send_eof(self) -> None:
|
||||
await self.send_stream.aclose()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self.send_stream.aclose()
|
||||
await self.receive_stream.aclose()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return {
|
||||
**self.send_stream.extra_attributes,
|
||||
**self.receive_stream.extra_attributes,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class MultiListener(Generic[T_Stream], Listener[T_Stream]):
|
||||
"""
|
||||
Combines multiple listeners into one, serving connections from all of them at once.
|
||||
|
||||
Any MultiListeners in the given collection of listeners will have their listeners
|
||||
moved into this one.
|
||||
|
||||
Extra attributes are provided from each listener, with each successive listener
|
||||
overriding any conflicting attributes from the previous one.
|
||||
|
||||
:param listeners: listeners to serve
|
||||
:type listeners: Sequence[Listener[T_Stream]]
|
||||
"""
|
||||
|
||||
listeners: Sequence[Listener[T_Stream]]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
listeners: list[Listener[T_Stream]] = []
|
||||
for listener in self.listeners:
|
||||
if isinstance(listener, MultiListener):
|
||||
listeners.extend(listener.listeners)
|
||||
del listener.listeners[:] # type: ignore[attr-defined]
|
||||
else:
|
||||
listeners.append(listener)
|
||||
|
||||
self.listeners = listeners
|
||||
|
||||
async def serve(
|
||||
self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None
|
||||
) -> None:
|
||||
from .. import create_task_group
|
||||
|
||||
async with create_task_group() as tg:
|
||||
for listener in self.listeners:
|
||||
tg.start_soon(listener.serve, handler, task_group)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
for listener in self.listeners:
|
||||
await listener.aclose()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
attributes: dict = {}
|
||||
for listener in self.listeners:
|
||||
attributes.update(listener.extra_attributes)
|
||||
|
||||
return attributes
|
||||
176
.venv/lib/python3.11/site-packages/anyio/streams/text.py
Normal file
176
.venv/lib/python3.11/site-packages/anyio/streams/text.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"TextConnectable",
|
||||
"TextReceiveStream",
|
||||
"TextSendStream",
|
||||
"TextStream",
|
||||
)
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from ..abc import (
|
||||
AnyByteReceiveStream,
|
||||
AnyByteSendStream,
|
||||
AnyByteStream,
|
||||
AnyByteStreamConnectable,
|
||||
ObjectReceiveStream,
|
||||
ObjectSendStream,
|
||||
ObjectStream,
|
||||
ObjectStreamConnectable,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class TextReceiveStream(ObjectReceiveStream[str]):
|
||||
"""
|
||||
Stream wrapper that decodes bytes to strings using the given encoding.
|
||||
|
||||
Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any
|
||||
completely received unicode characters as soon as they come in.
|
||||
|
||||
:param transport_stream: any bytes-based receive stream
|
||||
:param encoding: character encoding to use for decoding bytes to strings (defaults
|
||||
to ``utf-8``)
|
||||
:param errors: handling scheme for decoding errors (defaults to ``strict``; see the
|
||||
`codecs module documentation`_ for a comprehensive list of options)
|
||||
|
||||
.. _codecs module documentation:
|
||||
https://docs.python.org/3/library/codecs.html#codec-objects
|
||||
"""
|
||||
|
||||
transport_stream: AnyByteReceiveStream
|
||||
encoding: InitVar[str] = "utf-8"
|
||||
errors: InitVar[str] = "strict"
|
||||
_decoder: codecs.IncrementalDecoder = field(init=False)
|
||||
|
||||
def __post_init__(self, encoding: str, errors: str) -> None:
|
||||
decoder_class = codecs.getincrementaldecoder(encoding)
|
||||
self._decoder = decoder_class(errors=errors)
|
||||
|
||||
async def receive(self) -> str:
|
||||
while True:
|
||||
chunk = await self.transport_stream.receive()
|
||||
decoded = self._decoder.decode(chunk)
|
||||
if decoded:
|
||||
return decoded
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self.transport_stream.aclose()
|
||||
self._decoder.reset()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return self.transport_stream.extra_attributes
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class TextSendStream(ObjectSendStream[str]):
|
||||
"""
|
||||
Sends strings to the wrapped stream as bytes using the given encoding.
|
||||
|
||||
:param AnyByteSendStream transport_stream: any bytes-based send stream
|
||||
:param str encoding: character encoding to use for encoding strings to bytes
|
||||
(defaults to ``utf-8``)
|
||||
:param str errors: handling scheme for encoding errors (defaults to ``strict``; see
|
||||
the `codecs module documentation`_ for a comprehensive list of options)
|
||||
|
||||
.. _codecs module documentation:
|
||||
https://docs.python.org/3/library/codecs.html#codec-objects
|
||||
"""
|
||||
|
||||
transport_stream: AnyByteSendStream
|
||||
encoding: InitVar[str] = "utf-8"
|
||||
errors: str = "strict"
|
||||
_encoder: Callable[..., tuple[bytes, int]] = field(init=False)
|
||||
|
||||
def __post_init__(self, encoding: str) -> None:
|
||||
self._encoder = codecs.getencoder(encoding)
|
||||
|
||||
async def send(self, item: str) -> None:
|
||||
encoded = self._encoder(item, self.errors)[0]
|
||||
await self.transport_stream.send(encoded)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self.transport_stream.aclose()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return self.transport_stream.extra_attributes
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class TextStream(ObjectStream[str]):
|
||||
"""
|
||||
A bidirectional stream that decodes bytes to strings on receive and encodes strings
|
||||
to bytes on send.
|
||||
|
||||
Extra attributes will be provided from both streams, with the receive stream
|
||||
providing the values in case of a conflict.
|
||||
|
||||
:param AnyByteStream transport_stream: any bytes-based stream
|
||||
:param str encoding: character encoding to use for encoding/decoding strings to/from
|
||||
bytes (defaults to ``utf-8``)
|
||||
:param str errors: handling scheme for encoding errors (defaults to ``strict``; see
|
||||
the `codecs module documentation`_ for a comprehensive list of options)
|
||||
|
||||
.. _codecs module documentation:
|
||||
https://docs.python.org/3/library/codecs.html#codec-objects
|
||||
"""
|
||||
|
||||
transport_stream: AnyByteStream
|
||||
encoding: InitVar[str] = "utf-8"
|
||||
errors: InitVar[str] = "strict"
|
||||
_receive_stream: TextReceiveStream = field(init=False)
|
||||
_send_stream: TextSendStream = field(init=False)
|
||||
|
||||
def __post_init__(self, encoding: str, errors: str) -> None:
|
||||
self._receive_stream = TextReceiveStream(
|
||||
self.transport_stream, encoding=encoding, errors=errors
|
||||
)
|
||||
self._send_stream = TextSendStream(
|
||||
self.transport_stream, encoding=encoding, errors=errors
|
||||
)
|
||||
|
||||
async def receive(self) -> str:
|
||||
return await self._receive_stream.receive()
|
||||
|
||||
async def send(self, item: str) -> None:
|
||||
await self._send_stream.send(item)
|
||||
|
||||
async def send_eof(self) -> None:
|
||||
await self.transport_stream.send_eof()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._send_stream.aclose()
|
||||
await self._receive_stream.aclose()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return {
|
||||
**self._send_stream.extra_attributes,
|
||||
**self._receive_stream.extra_attributes,
|
||||
}
|
||||
|
||||
|
||||
class TextConnectable(ObjectStreamConnectable[str]):
|
||||
def __init__(self, connectable: AnyByteStreamConnectable):
|
||||
"""
|
||||
:param connectable: the bytestream endpoint to wrap
|
||||
|
||||
"""
|
||||
self.connectable = connectable
|
||||
|
||||
@override
|
||||
async def connect(self) -> TextStream:
|
||||
stream = await self.connectable.connect()
|
||||
return TextStream(stream)
|
||||
424
.venv/lib/python3.11/site-packages/anyio/streams/tls.py
Normal file
424
.venv/lib/python3.11/site-packages/anyio/streams/tls.py
Normal file
@@ -0,0 +1,424 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"TLSAttribute",
|
||||
"TLSConnectable",
|
||||
"TLSListener",
|
||||
"TLSStream",
|
||||
)
|
||||
|
||||
import logging
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from ssl import SSLContext
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from .. import (
|
||||
BrokenResourceError,
|
||||
EndOfStream,
|
||||
aclose_forcefully,
|
||||
get_cancelled_exc_class,
|
||||
to_thread,
|
||||
)
|
||||
from .._core._typedattr import TypedAttributeSet, typed_attribute
|
||||
from ..abc import (
|
||||
AnyByteStream,
|
||||
AnyByteStreamConnectable,
|
||||
ByteStream,
|
||||
ByteStreamConnectable,
|
||||
Listener,
|
||||
TaskGroup,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
_PCTRTT: TypeAlias = tuple[tuple[str, str], ...]
|
||||
_PCTRTTT: TypeAlias = tuple[_PCTRTT, ...]
|
||||
|
||||
|
||||
class TLSAttribute(TypedAttributeSet):
|
||||
"""Contains Transport Layer Security related attributes."""
|
||||
|
||||
#: the selected ALPN protocol
|
||||
alpn_protocol: str | None = typed_attribute()
|
||||
#: the channel binding for type ``tls-unique``
|
||||
channel_binding_tls_unique: bytes = typed_attribute()
|
||||
#: the selected cipher
|
||||
cipher: tuple[str, str, int] = typed_attribute()
|
||||
#: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert`
|
||||
# for more information)
|
||||
peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute()
|
||||
#: the peer certificate in binary form
|
||||
peer_certificate_binary: bytes | None = typed_attribute()
|
||||
#: ``True`` if this is the server side of the connection
|
||||
server_side: bool = typed_attribute()
|
||||
#: ciphers shared by the client during the TLS handshake (``None`` if this is the
|
||||
#: client side)
|
||||
shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute()
|
||||
#: the :class:`~ssl.SSLObject` used for encryption
|
||||
ssl_object: ssl.SSLObject = typed_attribute()
|
||||
#: ``True`` if this stream does (and expects) a closing TLS handshake when the
|
||||
#: stream is being closed
|
||||
standard_compatible: bool = typed_attribute()
|
||||
#: the TLS protocol version (e.g. ``TLSv1.2``)
|
||||
tls_version: str = typed_attribute()
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class TLSStream(ByteStream):
|
||||
"""
|
||||
A stream wrapper that encrypts all sent data and decrypts received data.
|
||||
|
||||
This class has no public initializer; use :meth:`wrap` instead.
|
||||
All extra attributes from :class:`~TLSAttribute` are supported.
|
||||
|
||||
:var AnyByteStream transport_stream: the wrapped stream
|
||||
|
||||
"""
|
||||
|
||||
transport_stream: AnyByteStream
|
||||
standard_compatible: bool
|
||||
_ssl_object: ssl.SSLObject
|
||||
_read_bio: ssl.MemoryBIO
|
||||
_write_bio: ssl.MemoryBIO
|
||||
|
||||
@classmethod
|
||||
async def wrap(
|
||||
cls,
|
||||
transport_stream: AnyByteStream,
|
||||
*,
|
||||
server_side: bool | None = None,
|
||||
hostname: str | None = None,
|
||||
ssl_context: ssl.SSLContext | None = None,
|
||||
standard_compatible: bool = True,
|
||||
) -> TLSStream:
|
||||
"""
|
||||
Wrap an existing stream with Transport Layer Security.
|
||||
|
||||
This performs a TLS handshake with the peer.
|
||||
|
||||
:param transport_stream: a bytes-transporting stream to wrap
|
||||
:param server_side: ``True`` if this is the server side of the connection,
|
||||
``False`` if this is the client side (if omitted, will be set to ``False``
|
||||
if ``hostname`` has been provided, ``False`` otherwise). Used only to create
|
||||
a default context when an explicit context has not been provided.
|
||||
:param hostname: host name of the peer (if host name checking is desired)
|
||||
:param ssl_context: the SSLContext object to use (if not provided, a secure
|
||||
default will be created)
|
||||
:param standard_compatible: if ``False``, skip the closing handshake when
|
||||
closing the connection, and don't raise an exception if the peer does the
|
||||
same
|
||||
:raises ~ssl.SSLError: if the TLS handshake fails
|
||||
|
||||
"""
|
||||
if server_side is None:
|
||||
server_side = not hostname
|
||||
|
||||
if not ssl_context:
|
||||
purpose = (
|
||||
ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH
|
||||
)
|
||||
ssl_context = ssl.create_default_context(purpose)
|
||||
|
||||
# Re-enable detection of unexpected EOFs if it was disabled by Python
|
||||
if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"):
|
||||
ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF
|
||||
|
||||
bio_in = ssl.MemoryBIO()
|
||||
bio_out = ssl.MemoryBIO()
|
||||
|
||||
# External SSLContext implementations may do blocking I/O in wrap_bio(),
|
||||
# but the standard library implementation won't
|
||||
if type(ssl_context) is ssl.SSLContext:
|
||||
ssl_object = ssl_context.wrap_bio(
|
||||
bio_in, bio_out, server_side=server_side, server_hostname=hostname
|
||||
)
|
||||
else:
|
||||
ssl_object = await to_thread.run_sync(
|
||||
ssl_context.wrap_bio,
|
||||
bio_in,
|
||||
bio_out,
|
||||
server_side,
|
||||
hostname,
|
||||
None,
|
||||
)
|
||||
|
||||
wrapper = cls(
|
||||
transport_stream=transport_stream,
|
||||
standard_compatible=standard_compatible,
|
||||
_ssl_object=ssl_object,
|
||||
_read_bio=bio_in,
|
||||
_write_bio=bio_out,
|
||||
)
|
||||
await wrapper._call_sslobject_method(ssl_object.do_handshake)
|
||||
return wrapper
|
||||
|
||||
async def _call_sslobject_method(
|
||||
self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT]
|
||||
) -> T_Retval:
|
||||
while True:
|
||||
try:
|
||||
result = func(*args)
|
||||
except ssl.SSLWantReadError:
|
||||
try:
|
||||
# Flush any pending writes first
|
||||
if self._write_bio.pending:
|
||||
await self.transport_stream.send(self._write_bio.read())
|
||||
|
||||
data = await self.transport_stream.receive()
|
||||
except EndOfStream:
|
||||
self._read_bio.write_eof()
|
||||
except OSError as exc:
|
||||
self._read_bio.write_eof()
|
||||
self._write_bio.write_eof()
|
||||
raise BrokenResourceError from exc
|
||||
else:
|
||||
self._read_bio.write(data)
|
||||
except ssl.SSLWantWriteError:
|
||||
await self.transport_stream.send(self._write_bio.read())
|
||||
except ssl.SSLSyscallError as exc:
|
||||
self._read_bio.write_eof()
|
||||
self._write_bio.write_eof()
|
||||
raise BrokenResourceError from exc
|
||||
except ssl.SSLError as exc:
|
||||
self._read_bio.write_eof()
|
||||
self._write_bio.write_eof()
|
||||
if isinstance(exc, ssl.SSLEOFError) or (
|
||||
exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror
|
||||
):
|
||||
if self.standard_compatible:
|
||||
raise BrokenResourceError from exc
|
||||
else:
|
||||
raise EndOfStream from None
|
||||
|
||||
raise
|
||||
else:
|
||||
# Flush any pending writes first
|
||||
if self._write_bio.pending:
|
||||
await self.transport_stream.send(self._write_bio.read())
|
||||
|
||||
return result
|
||||
|
||||
async def unwrap(self) -> tuple[AnyByteStream, bytes]:
|
||||
"""
|
||||
Does the TLS closing handshake.
|
||||
|
||||
:return: a tuple of (wrapped byte stream, bytes left in the read buffer)
|
||||
|
||||
"""
|
||||
await self._call_sslobject_method(self._ssl_object.unwrap)
|
||||
self._read_bio.write_eof()
|
||||
self._write_bio.write_eof()
|
||||
return self.transport_stream, self._read_bio.read()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self.standard_compatible:
|
||||
try:
|
||||
await self.unwrap()
|
||||
except BaseException:
|
||||
await aclose_forcefully(self.transport_stream)
|
||||
raise
|
||||
|
||||
await self.transport_stream.aclose()
|
||||
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
data = await self._call_sslobject_method(self._ssl_object.read, max_bytes)
|
||||
if not data:
|
||||
raise EndOfStream
|
||||
|
||||
return data
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
await self._call_sslobject_method(self._ssl_object.write, item)
|
||||
|
||||
async def send_eof(self) -> None:
|
||||
tls_version = self.extra(TLSAttribute.tls_version)
|
||||
match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version)
|
||||
if match:
|
||||
major, minor = int(match.group(1)), int(match.group(2) or 0)
|
||||
if (major, minor) < (1, 3):
|
||||
raise NotImplementedError(
|
||||
f"send_eof() requires at least TLSv1.3; current "
|
||||
f"session uses {tls_version}"
|
||||
)
|
||||
|
||||
raise NotImplementedError(
|
||||
"send_eof() has not yet been implemented for TLS streams"
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return {
|
||||
**self.transport_stream.extra_attributes,
|
||||
TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol,
|
||||
TLSAttribute.channel_binding_tls_unique: (
|
||||
self._ssl_object.get_channel_binding
|
||||
),
|
||||
TLSAttribute.cipher: self._ssl_object.cipher,
|
||||
TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False),
|
||||
TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert(
|
||||
True
|
||||
),
|
||||
TLSAttribute.server_side: lambda: self._ssl_object.server_side,
|
||||
TLSAttribute.shared_ciphers: lambda: self._ssl_object.shared_ciphers()
|
||||
if self._ssl_object.server_side
|
||||
else None,
|
||||
TLSAttribute.standard_compatible: lambda: self.standard_compatible,
|
||||
TLSAttribute.ssl_object: lambda: self._ssl_object,
|
||||
TLSAttribute.tls_version: self._ssl_object.version,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class TLSListener(Listener[TLSStream]):
|
||||
"""
|
||||
A convenience listener that wraps another listener and auto-negotiates a TLS session
|
||||
on every accepted connection.
|
||||
|
||||
If the TLS handshake times out or raises an exception,
|
||||
:meth:`handle_handshake_error` is called to do whatever post-mortem processing is
|
||||
deemed necessary.
|
||||
|
||||
Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute.
|
||||
|
||||
:param Listener listener: the listener to wrap
|
||||
:param ssl_context: the SSL context object
|
||||
:param standard_compatible: a flag passed through to :meth:`TLSStream.wrap`
|
||||
:param handshake_timeout: time limit for the TLS handshake
|
||||
(passed to :func:`~anyio.fail_after`)
|
||||
"""
|
||||
|
||||
listener: Listener[Any]
|
||||
ssl_context: ssl.SSLContext
|
||||
standard_compatible: bool = True
|
||||
handshake_timeout: float = 30
|
||||
|
||||
@staticmethod
|
||||
async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None:
|
||||
"""
|
||||
Handle an exception raised during the TLS handshake.
|
||||
|
||||
This method does 3 things:
|
||||
|
||||
#. Forcefully closes the original stream
|
||||
#. Logs the exception (unless it was a cancellation exception) using the
|
||||
``anyio.streams.tls`` logger
|
||||
#. Reraises the exception if it was a base exception or a cancellation exception
|
||||
|
||||
:param exc: the exception
|
||||
:param stream: the original stream
|
||||
|
||||
"""
|
||||
await aclose_forcefully(stream)
|
||||
|
||||
# Log all except cancellation exceptions
|
||||
if not isinstance(exc, get_cancelled_exc_class()):
|
||||
# CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using
|
||||
# any asyncio implementation, so we explicitly pass the exception to log
|
||||
# (https://github.com/python/cpython/issues/108668). Trio does not have this
|
||||
# issue because it works around the CPython bug.
|
||||
logging.getLogger(__name__).exception(
|
||||
"Error during TLS handshake", exc_info=exc
|
||||
)
|
||||
|
||||
# Only reraise base exceptions and cancellation exceptions
|
||||
if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()):
|
||||
raise
|
||||
|
||||
async def serve(
|
||||
self,
|
||||
handler: Callable[[TLSStream], Any],
|
||||
task_group: TaskGroup | None = None,
|
||||
) -> None:
|
||||
@wraps(handler)
|
||||
async def handler_wrapper(stream: AnyByteStream) -> None:
|
||||
from .. import fail_after
|
||||
|
||||
try:
|
||||
with fail_after(self.handshake_timeout):
|
||||
wrapped_stream = await TLSStream.wrap(
|
||||
stream,
|
||||
ssl_context=self.ssl_context,
|
||||
standard_compatible=self.standard_compatible,
|
||||
)
|
||||
except BaseException as exc:
|
||||
await self.handle_handshake_error(exc, stream)
|
||||
else:
|
||||
await handler(wrapped_stream)
|
||||
|
||||
await self.listener.serve(handler_wrapper, task_group)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self.listener.aclose()
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||||
return {
|
||||
TLSAttribute.standard_compatible: lambda: self.standard_compatible,
|
||||
}
|
||||
|
||||
|
||||
class TLSConnectable(ByteStreamConnectable):
|
||||
"""
|
||||
Wraps another connectable and does TLS negotiation after a successful connection.
|
||||
|
||||
:param connectable: the connectable to wrap
|
||||
:param hostname: host name of the server (if host name checking is desired)
|
||||
:param ssl_context: the SSLContext object to use (if not provided, a secure default
|
||||
will be created)
|
||||
:param standard_compatible: if ``False``, skip the closing handshake when closing
|
||||
the connection, and don't raise an exception if the server does the same
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connectable: AnyByteStreamConnectable,
|
||||
*,
|
||||
hostname: str | None = None,
|
||||
ssl_context: ssl.SSLContext | None = None,
|
||||
standard_compatible: bool = True,
|
||||
) -> None:
|
||||
self.connectable = connectable
|
||||
self.ssl_context: SSLContext = ssl_context or ssl.create_default_context(
|
||||
ssl.Purpose.SERVER_AUTH
|
||||
)
|
||||
if not isinstance(self.ssl_context, ssl.SSLContext):
|
||||
raise TypeError(
|
||||
"ssl_context must be an instance of ssl.SSLContext, not "
|
||||
f"{type(self.ssl_context).__name__}"
|
||||
)
|
||||
self.hostname = hostname
|
||||
self.standard_compatible = standard_compatible
|
||||
|
||||
@override
|
||||
async def connect(self) -> TLSStream:
|
||||
stream = await self.connectable.connect()
|
||||
try:
|
||||
return await TLSStream.wrap(
|
||||
stream,
|
||||
hostname=self.hostname,
|
||||
ssl_context=self.ssl_context,
|
||||
standard_compatible=self.standard_compatible,
|
||||
)
|
||||
except BaseException:
|
||||
await aclose_forcefully(stream)
|
||||
raise
|
||||
246
.venv/lib/python3.11/site-packages/anyio/to_interpreter.py
Normal file
246
.venv/lib/python3.11/site-packages/anyio/to_interpreter.py
Normal file
@@ -0,0 +1,246 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"run_sync",
|
||||
"current_default_interpreter_limiter",
|
||||
)
|
||||
|
||||
import atexit
|
||||
import os
|
||||
import sys
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Final, TypeVar
|
||||
|
||||
from . import current_time, to_thread
|
||||
from ._core._exceptions import BrokenWorkerInterpreter
|
||||
from ._core._synchronization import CapacityLimiter
|
||||
from .lowlevel import RunVar
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
from concurrent.interpreters import ExecutionFailed, create
|
||||
|
||||
def _interp_call(
|
||||
func: Callable[..., Any], args: tuple[Any, ...]
|
||||
) -> tuple[Any, bool]:
|
||||
try:
|
||||
retval = func(*args)
|
||||
except BaseException as exc:
|
||||
return exc, True
|
||||
else:
|
||||
return retval, False
|
||||
|
||||
class _Worker:
|
||||
last_used: float = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._interpreter = create()
|
||||
|
||||
def destroy(self) -> None:
|
||||
self._interpreter.close()
|
||||
|
||||
def call(
|
||||
self,
|
||||
func: Callable[..., T_Retval],
|
||||
args: tuple[Any, ...],
|
||||
) -> T_Retval:
|
||||
try:
|
||||
res, is_exception = self._interpreter.call(_interp_call, func, args)
|
||||
except ExecutionFailed as exc:
|
||||
raise BrokenWorkerInterpreter(exc.excinfo) from exc
|
||||
|
||||
if is_exception:
|
||||
raise res
|
||||
|
||||
return res
|
||||
elif sys.version_info >= (3, 13):
|
||||
import _interpqueues
|
||||
import _interpreters
|
||||
|
||||
UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib
|
||||
FMT_UNPICKLED: Final = 0
|
||||
FMT_PICKLED: Final = 1
|
||||
QUEUE_PICKLE_ARGS: Final = (FMT_PICKLED, UNBOUND)
|
||||
QUEUE_UNPICKLE_ARGS: Final = (FMT_UNPICKLED, UNBOUND)
|
||||
|
||||
_run_func = compile(
|
||||
"""
|
||||
import _interpqueues
|
||||
from _interpreters import NotShareableError
|
||||
from pickle import loads, dumps, HIGHEST_PROTOCOL
|
||||
|
||||
QUEUE_PICKLE_ARGS = (1, 2)
|
||||
QUEUE_UNPICKLE_ARGS = (0, 2)
|
||||
|
||||
item = _interpqueues.get(queue_id)[0]
|
||||
try:
|
||||
func, args = loads(item)
|
||||
retval = func(*args)
|
||||
except BaseException as exc:
|
||||
is_exception = True
|
||||
retval = exc
|
||||
else:
|
||||
is_exception = False
|
||||
|
||||
try:
|
||||
_interpqueues.put(queue_id, (retval, is_exception), *QUEUE_UNPICKLE_ARGS)
|
||||
except NotShareableError:
|
||||
retval = dumps(retval, HIGHEST_PROTOCOL)
|
||||
_interpqueues.put(queue_id, (retval, is_exception), *QUEUE_PICKLE_ARGS)
|
||||
""",
|
||||
"<string>",
|
||||
"exec",
|
||||
)
|
||||
|
||||
class _Worker:
|
||||
last_used: float = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._interpreter_id = _interpreters.create()
|
||||
self._queue_id = _interpqueues.create(1, *QUEUE_UNPICKLE_ARGS)
|
||||
_interpreters.set___main___attrs(
|
||||
self._interpreter_id, {"queue_id": self._queue_id}
|
||||
)
|
||||
|
||||
def destroy(self) -> None:
|
||||
_interpqueues.destroy(self._queue_id)
|
||||
_interpreters.destroy(self._interpreter_id)
|
||||
|
||||
def call(
|
||||
self,
|
||||
func: Callable[..., T_Retval],
|
||||
args: tuple[Any, ...],
|
||||
) -> T_Retval:
|
||||
import pickle
|
||||
|
||||
item = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL)
|
||||
_interpqueues.put(self._queue_id, item, *QUEUE_PICKLE_ARGS)
|
||||
exc_info = _interpreters.exec(self._interpreter_id, _run_func)
|
||||
if exc_info:
|
||||
raise BrokenWorkerInterpreter(exc_info)
|
||||
|
||||
res = _interpqueues.get(self._queue_id)
|
||||
(res, is_exception), fmt = res[:2]
|
||||
if fmt == FMT_PICKLED:
|
||||
res = pickle.loads(res)
|
||||
|
||||
if is_exception:
|
||||
raise res
|
||||
|
||||
return res
|
||||
else:
|
||||
|
||||
class _Worker:
|
||||
last_used: float = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
raise RuntimeError("subinterpreters require at least Python 3.13")
|
||||
|
||||
def call(
|
||||
self,
|
||||
func: Callable[..., T_Retval],
|
||||
args: tuple[Any, ...],
|
||||
) -> T_Retval:
|
||||
raise NotImplementedError
|
||||
|
||||
def destroy(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value
|
||||
MAX_WORKER_IDLE_TIME = (
|
||||
30 # seconds a subinterpreter can be idle before becoming eligible for pruning
|
||||
)
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
_idle_workers = RunVar[deque[_Worker]]("_available_workers")
|
||||
_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter")
|
||||
|
||||
|
||||
def _stop_workers(workers: deque[_Worker]) -> None:
|
||||
for worker in workers:
|
||||
worker.destroy()
|
||||
|
||||
workers.clear()
|
||||
|
||||
|
||||
async def run_sync(
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
limiter: CapacityLimiter | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Call the given function with the given arguments in a subinterpreter.
|
||||
|
||||
.. warning:: On Python 3.13, the :mod:`concurrent.interpreters` module was not yet
|
||||
available, so the code path for that Python version relies on an undocumented,
|
||||
private API. As such, it is recommended to not rely on this function for anything
|
||||
mission-critical on Python 3.13.
|
||||
|
||||
:param func: a callable
|
||||
:param args: the positional arguments for the callable
|
||||
:param limiter: capacity limiter to use to limit the total number of subinterpreters
|
||||
running (if omitted, the default limiter is used)
|
||||
:return: the result of the call
|
||||
:raises BrokenWorkerInterpreter: if there's an internal error in a subinterpreter
|
||||
|
||||
"""
|
||||
if limiter is None:
|
||||
limiter = current_default_interpreter_limiter()
|
||||
|
||||
try:
|
||||
idle_workers = _idle_workers.get()
|
||||
except LookupError:
|
||||
idle_workers = deque()
|
||||
_idle_workers.set(idle_workers)
|
||||
atexit.register(_stop_workers, idle_workers)
|
||||
|
||||
async with limiter:
|
||||
try:
|
||||
worker = idle_workers.pop()
|
||||
except IndexError:
|
||||
worker = _Worker()
|
||||
|
||||
try:
|
||||
return await to_thread.run_sync(
|
||||
worker.call,
|
||||
func,
|
||||
args,
|
||||
limiter=limiter,
|
||||
)
|
||||
finally:
|
||||
# Prune workers that have been idle for too long
|
||||
now = current_time()
|
||||
while idle_workers:
|
||||
if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME:
|
||||
break
|
||||
|
||||
await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter)
|
||||
|
||||
worker.last_used = current_time()
|
||||
idle_workers.append(worker)
|
||||
|
||||
|
||||
def current_default_interpreter_limiter() -> CapacityLimiter:
|
||||
"""
|
||||
Return the capacity limiter used by default to limit the number of concurrently
|
||||
running subinterpreters.
|
||||
|
||||
Defaults to the number of CPU cores.
|
||||
|
||||
:return: a capacity limiter object
|
||||
|
||||
"""
|
||||
try:
|
||||
return _default_interpreter_limiter.get()
|
||||
except LookupError:
|
||||
limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT)
|
||||
_default_interpreter_limiter.set(limiter)
|
||||
return limiter
|
||||
266
.venv/lib/python3.11/site-packages/anyio/to_process.py
Normal file
266
.venv/lib/python3.11/site-packages/anyio/to_process.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"current_default_process_limiter",
|
||||
"process_worker",
|
||||
"run_sync",
|
||||
)
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from typing import TypeVar, cast
|
||||
|
||||
from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class
|
||||
from ._core._exceptions import BrokenWorkerProcess
|
||||
from ._core._subprocesses import open_process
|
||||
from ._core._synchronization import CapacityLimiter
|
||||
from ._core._tasks import CancelScope, fail_after
|
||||
from .abc import ByteReceiveStream, ByteSendStream, Process
|
||||
from .lowlevel import RunVar, checkpoint_if_cancelled
|
||||
from .streams.buffered import BufferedByteReceiveStream
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
WORKER_MAX_IDLE_TIME = 300 # 5 minutes
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers")
|
||||
_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar(
|
||||
"_process_pool_idle_workers"
|
||||
)
|
||||
_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter")
|
||||
|
||||
|
||||
async def run_sync( # type: ignore[return]
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
cancellable: bool = False,
|
||||
limiter: CapacityLimiter | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Call the given function with the given arguments in a worker process.
|
||||
|
||||
If the ``cancellable`` option is enabled and the task waiting for its completion is
|
||||
cancelled, the worker process running it will be abruptly terminated using SIGKILL
|
||||
(or ``terminateProcess()`` on Windows).
|
||||
|
||||
:param func: a callable
|
||||
:param args: positional arguments for the callable
|
||||
:param cancellable: ``True`` to allow cancellation of the operation while it's
|
||||
running
|
||||
:param limiter: capacity limiter to use to limit the total amount of processes
|
||||
running (if omitted, the default limiter is used)
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
:return: an awaitable that yields the return value of the function.
|
||||
|
||||
"""
|
||||
|
||||
async def send_raw_command(pickled_cmd: bytes) -> object:
|
||||
try:
|
||||
await stdin.send(pickled_cmd)
|
||||
response = await buffered.receive_until(b"\n", 50)
|
||||
status, length = response.split(b" ")
|
||||
if status not in (b"RETURN", b"EXCEPTION"):
|
||||
raise RuntimeError(
|
||||
f"Worker process returned unexpected response: {response!r}"
|
||||
)
|
||||
|
||||
pickled_response = await buffered.receive_exactly(int(length))
|
||||
except BaseException as exc:
|
||||
workers.discard(process)
|
||||
try:
|
||||
process.kill()
|
||||
with CancelScope(shield=True):
|
||||
await process.aclose()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
if isinstance(exc, get_cancelled_exc_class()):
|
||||
raise
|
||||
else:
|
||||
raise BrokenWorkerProcess from exc
|
||||
|
||||
retval = pickle.loads(pickled_response)
|
||||
if status == b"EXCEPTION":
|
||||
assert isinstance(retval, BaseException)
|
||||
raise retval
|
||||
else:
|
||||
return retval
|
||||
|
||||
# First pickle the request before trying to reserve a worker process
|
||||
await checkpoint_if_cancelled()
|
||||
request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
# If this is the first run in this event loop thread, set up the necessary variables
|
||||
try:
|
||||
workers = _process_pool_workers.get()
|
||||
idle_workers = _process_pool_idle_workers.get()
|
||||
except LookupError:
|
||||
workers = set()
|
||||
idle_workers = deque()
|
||||
_process_pool_workers.set(workers)
|
||||
_process_pool_idle_workers.set(idle_workers)
|
||||
get_async_backend().setup_process_pool_exit_at_shutdown(workers)
|
||||
|
||||
async with limiter or current_default_process_limiter():
|
||||
# Pop processes from the pool (starting from the most recently used) until we
|
||||
# find one that hasn't exited yet
|
||||
process: Process
|
||||
while idle_workers:
|
||||
process, idle_since = idle_workers.pop()
|
||||
if process.returncode is None:
|
||||
stdin = cast(ByteSendStream, process.stdin)
|
||||
buffered = BufferedByteReceiveStream(
|
||||
cast(ByteReceiveStream, process.stdout)
|
||||
)
|
||||
|
||||
# Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME
|
||||
# seconds or longer
|
||||
now = current_time()
|
||||
killed_processes: list[Process] = []
|
||||
while idle_workers:
|
||||
if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME:
|
||||
break
|
||||
|
||||
process_to_kill, idle_since = idle_workers.popleft()
|
||||
process_to_kill.kill()
|
||||
workers.remove(process_to_kill)
|
||||
killed_processes.append(process_to_kill)
|
||||
|
||||
with CancelScope(shield=True):
|
||||
for killed_process in killed_processes:
|
||||
await killed_process.aclose()
|
||||
|
||||
break
|
||||
|
||||
workers.remove(process)
|
||||
else:
|
||||
command = [sys.executable, "-u", "-m", __name__]
|
||||
process = await open_process(
|
||||
command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||
)
|
||||
try:
|
||||
stdin = cast(ByteSendStream, process.stdin)
|
||||
buffered = BufferedByteReceiveStream(
|
||||
cast(ByteReceiveStream, process.stdout)
|
||||
)
|
||||
with fail_after(20):
|
||||
message = await buffered.receive(6)
|
||||
|
||||
if message != b"READY\n":
|
||||
raise BrokenWorkerProcess(
|
||||
f"Worker process returned unexpected response: {message!r}"
|
||||
)
|
||||
|
||||
main_module_path = getattr(sys.modules["__main__"], "__file__", None)
|
||||
pickled = pickle.dumps(
|
||||
("init", sys.path, main_module_path),
|
||||
protocol=pickle.HIGHEST_PROTOCOL,
|
||||
)
|
||||
await send_raw_command(pickled)
|
||||
except (BrokenWorkerProcess, get_cancelled_exc_class()):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
process.kill()
|
||||
raise BrokenWorkerProcess(
|
||||
"Error during worker process initialization"
|
||||
) from exc
|
||||
|
||||
workers.add(process)
|
||||
|
||||
with CancelScope(shield=not cancellable):
|
||||
try:
|
||||
return cast(T_Retval, await send_raw_command(request))
|
||||
finally:
|
||||
if process in workers:
|
||||
idle_workers.append((process, current_time()))
|
||||
|
||||
|
||||
def current_default_process_limiter() -> CapacityLimiter:
|
||||
"""
|
||||
Return the capacity limiter that is used by default to limit the number of worker
|
||||
processes.
|
||||
|
||||
:return: a capacity limiter object
|
||||
|
||||
"""
|
||||
try:
|
||||
return _default_process_limiter.get()
|
||||
except LookupError:
|
||||
limiter = CapacityLimiter(os.cpu_count() or 2)
|
||||
_default_process_limiter.set(limiter)
|
||||
return limiter
|
||||
|
||||
|
||||
def process_worker() -> None:
|
||||
# Redirect standard streams to os.devnull so that user code won't interfere with the
|
||||
# parent-worker communication
|
||||
stdin = sys.stdin
|
||||
stdout = sys.stdout
|
||||
sys.stdin = open(os.devnull)
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
stdout.buffer.write(b"READY\n")
|
||||
while True:
|
||||
retval = exception = None
|
||||
try:
|
||||
command, *args = pickle.load(stdin.buffer)
|
||||
except EOFError:
|
||||
return
|
||||
except BaseException as exc:
|
||||
exception = exc
|
||||
else:
|
||||
if command == "run":
|
||||
func, args = args
|
||||
try:
|
||||
retval = func(*args)
|
||||
except BaseException as exc:
|
||||
exception = exc
|
||||
elif command == "init":
|
||||
main_module_path: str | None
|
||||
sys.path, main_module_path = args
|
||||
del sys.modules["__main__"]
|
||||
if main_module_path and os.path.isfile(main_module_path):
|
||||
# Load the parent's main module but as __mp_main__ instead of
|
||||
# __main__ (like multiprocessing does) to avoid infinite recursion
|
||||
try:
|
||||
spec = spec_from_file_location("__mp_main__", main_module_path)
|
||||
if spec and spec.loader:
|
||||
main = module_from_spec(spec)
|
||||
spec.loader.exec_module(main)
|
||||
sys.modules["__main__"] = main
|
||||
except BaseException as exc:
|
||||
exception = exc
|
||||
try:
|
||||
if exception is not None:
|
||||
status = b"EXCEPTION"
|
||||
pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL)
|
||||
else:
|
||||
status = b"RETURN"
|
||||
pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL)
|
||||
except BaseException as exc:
|
||||
exception = exc
|
||||
status = b"EXCEPTION"
|
||||
pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
stdout.buffer.write(b"%s %d\n" % (status, len(pickled)))
|
||||
stdout.buffer.write(pickled)
|
||||
|
||||
# Respect SIGTERM
|
||||
if isinstance(exception, SystemExit):
|
||||
raise exception
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
process_worker()
|
||||
78
.venv/lib/python3.11/site-packages/anyio/to_thread.py
Normal file
78
.venv/lib/python3.11/site-packages/anyio/to_thread.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"run_sync",
|
||||
"current_default_thread_limiter",
|
||||
)
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import TypeVar
|
||||
from warnings import warn
|
||||
|
||||
from ._core._eventloop import get_async_backend
|
||||
from .abc import CapacityLimiter
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypeVarTuple, Unpack
|
||||
else:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
|
||||
async def run_sync(
|
||||
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
||||
*args: Unpack[PosArgsT],
|
||||
abandon_on_cancel: bool = False,
|
||||
cancellable: bool | None = None,
|
||||
limiter: CapacityLimiter | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Call the given function with the given arguments in a worker thread.
|
||||
|
||||
If the ``cancellable`` option is enabled and the task waiting for its completion is
|
||||
cancelled, the thread will still run its course but its return value (or any raised
|
||||
exception) will be ignored.
|
||||
|
||||
:param func: a callable
|
||||
:param args: positional arguments for the callable
|
||||
:param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run
|
||||
unchecked on own) if the host task is cancelled, ``False`` to ignore
|
||||
cancellations in the host task until the operation has completed in the worker
|
||||
thread
|
||||
:param cancellable: deprecated alias of ``abandon_on_cancel``; will override
|
||||
``abandon_on_cancel`` if both parameters are passed
|
||||
:param limiter: capacity limiter to use to limit the total amount of threads running
|
||||
(if omitted, the default limiter is used)
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
:return: an awaitable that yields the return value of the function.
|
||||
|
||||
"""
|
||||
if cancellable is not None:
|
||||
abandon_on_cancel = cancellable
|
||||
warn(
|
||||
"The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is "
|
||||
"deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
return await get_async_backend().run_sync_in_worker_thread(
|
||||
func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter
|
||||
)
|
||||
|
||||
|
||||
def current_default_thread_limiter() -> CapacityLimiter:
|
||||
"""
|
||||
Return the capacity limiter that is used by default to limit the number of
|
||||
concurrent threads.
|
||||
|
||||
:return: a capacity limiter object
|
||||
:raises NoEventLoopError: if no supported asynchronous event loop is running in the
|
||||
current thread
|
||||
|
||||
"""
|
||||
return get_async_backend().current_default_thread_limiter()
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,41 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: bottle
|
||||
Version: 0.13.4
|
||||
Summary: Fast and simple WSGI-framework for small web-applications.
|
||||
Home-page: http://bottlepy.org/
|
||||
Author: Marcel Hellkamp
|
||||
Author-email: marc@gsites.de
|
||||
License: MIT
|
||||
Project-URL: Source, https://github.com/bottlepy/bottle
|
||||
Platform: any
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
|
||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
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 :: 3.13
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Dynamic: author
|
||||
Dynamic: author-email
|
||||
Dynamic: classifier
|
||||
Dynamic: description-content-type
|
||||
Dynamic: home-page
|
||||
Dynamic: license
|
||||
Dynamic: license-file
|
||||
Dynamic: platform
|
||||
Dynamic: project-url
|
||||
Dynamic: summary
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user