configure docker
parent
9adb3696c0
commit
cdfdffc274
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="finance.db" uuid="1ea8b50c-eb10-45e8-b4f2-da82d2ee1c53">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/finance.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,10 @@
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
WORKDIR /python-docker
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,25 @@
|
||||
services:
|
||||
flask:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.finance-web.entrypoints: web
|
||||
traefik.http.routers.finance-web.rule: (Host(`finance.home`) && PathPrefix(`/web`))
|
||||
traefik.http.routers.finance.entrypoints: websecure
|
||||
traefik.http.routers.finance.rule: (Host(`finance.woitschetzki.eu`) && PathPrefix(`/web`))
|
||||
traefik.http.routers.finance.tls: true
|
||||
traefik.http..finance.tls.certresolver: http_resolver
|
||||
traefik.http.routers.finance.service: finance
|
||||
traefik.http.services.finance.loadbalancer.server.port: 5000
|
||||
traefik.docker.network: proxy
|
||||
traefik.http.routers.finance.middlewares: default@file
|
||||
volumes:
|
||||
- .:/app
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
@ -0,0 +1,70 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# You cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath "/mnt/Daten/coding/cs50/psets/9/finance/env")
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV="/mnt/Daten/coding/cs50/psets/9/finance/env"
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1="(env) ${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT="(env) "
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
@ -0,0 +1,27 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/mnt/Daten/coding/cs50/psets/9/finance/env"
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = "(env) $prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT "(env) "
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
@ -0,0 +1,69 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/). You cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/mnt/Daten/coding/cs50/psets/9/finance/env"
|
||||
|
||||
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) "(env) " (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 "(env) "
|
||||
end
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from automat._visualize import tool
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(tool())
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from flask.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- 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())
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- 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())
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- 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())
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- 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())
|
||||
@ -0,0 +1 @@
|
||||
/usr/bin/python
|
||||
@ -0,0 +1 @@
|
||||
python
|
||||
@ -0,0 +1 @@
|
||||
python
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from sqlparse.__main__ import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,8 @@
|
||||
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from wheel.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,164 @@
|
||||
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||
|
||||
/* Greenlet object interface */
|
||||
|
||||
#ifndef Py_GREENLETOBJECT_H
|
||||
#define Py_GREENLETOBJECT_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is deprecated and undocumented. It does not change. */
|
||||
#define GREENLET_VERSION "1.0.0"
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
#define implementation_ptr_t void*
|
||||
#endif
|
||||
|
||||
typedef struct _greenlet {
|
||||
PyObject_HEAD
|
||||
PyObject* weakreflist;
|
||||
PyObject* dict;
|
||||
implementation_ptr_t pimpl;
|
||||
} PyGreenlet;
|
||||
|
||||
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||
|
||||
|
||||
/* C API functions */
|
||||
|
||||
/* Total number of symbols that are exported */
|
||||
#define PyGreenlet_API_pointers 12
|
||||
|
||||
#define PyGreenlet_Type_NUM 0
|
||||
#define PyExc_GreenletError_NUM 1
|
||||
#define PyExc_GreenletExit_NUM 2
|
||||
|
||||
#define PyGreenlet_New_NUM 3
|
||||
#define PyGreenlet_GetCurrent_NUM 4
|
||||
#define PyGreenlet_Throw_NUM 5
|
||||
#define PyGreenlet_Switch_NUM 6
|
||||
#define PyGreenlet_SetParent_NUM 7
|
||||
|
||||
#define PyGreenlet_MAIN_NUM 8
|
||||
#define PyGreenlet_STARTED_NUM 9
|
||||
#define PyGreenlet_ACTIVE_NUM 10
|
||||
#define PyGreenlet_GET_PARENT_NUM 11
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
/* This section is used by modules that uses the greenlet C API */
|
||||
static void** _PyGreenlet_API = NULL;
|
||||
|
||||
# define PyGreenlet_Type \
|
||||
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||
|
||||
# define PyExc_GreenletError \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||
|
||||
# define PyExc_GreenletExit \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_New(PyObject *args)
|
||||
*
|
||||
* greenlet.greenlet(run, parent=None)
|
||||
*/
|
||||
# define PyGreenlet_New \
|
||||
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetCurrent(void)
|
||||
*
|
||||
* greenlet.getcurrent()
|
||||
*/
|
||||
# define PyGreenlet_GetCurrent \
|
||||
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Throw(
|
||||
* PyGreenlet *greenlet,
|
||||
* PyObject *typ,
|
||||
* PyObject *val,
|
||||
* PyObject *tb)
|
||||
*
|
||||
* g.throw(...)
|
||||
*/
|
||||
# define PyGreenlet_Throw \
|
||||
(*(PyObject * (*)(PyGreenlet * self, \
|
||||
PyObject * typ, \
|
||||
PyObject * val, \
|
||||
PyObject * tb)) \
|
||||
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||
*
|
||||
* g.switch(*args, **kwargs)
|
||||
*/
|
||||
# define PyGreenlet_Switch \
|
||||
(*(PyObject * \
|
||||
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||
*
|
||||
* g.parent = new_parent
|
||||
*/
|
||||
# define PyGreenlet_SetParent \
|
||||
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||
*
|
||||
* return greenlet.parent;
|
||||
*
|
||||
* This could return NULL even if there is no exception active.
|
||||
* If it does not return NULL, you are responsible for decrementing the
|
||||
* reference count.
|
||||
*/
|
||||
# define PyGreenlet_GetParent \
|
||||
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||
|
||||
/*
|
||||
* deprecated, undocumented alias.
|
||||
*/
|
||||
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||
|
||||
# define PyGreenlet_MAIN \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||
|
||||
# define PyGreenlet_STARTED \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||
|
||||
# define PyGreenlet_ACTIVE \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||
|
||||
|
||||
|
||||
|
||||
/* Macro that imports greenlet and initializes C API */
|
||||
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||
keep the older definition to be sure older code that might have a copy of
|
||||
the header still works. */
|
||||
# define PyGreenlet_Import() \
|
||||
{ \
|
||||
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||
}
|
||||
|
||||
#endif /* GREENLET_MODULE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_GREENLETOBJECT_H */
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,21 @@
|
||||
Copyright (c) 2014
|
||||
Rackspace
|
||||
|
||||
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,199 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Automat
|
||||
Version: 24.8.1
|
||||
Summary: Self-service finite-state machines for the programmer on the go.
|
||||
Author-email: Glyph <code@glyph.im>
|
||||
License: Copyright (c) 2014
|
||||
Rackspace
|
||||
|
||||
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.
|
||||
|
||||
Project-URL: Documentation, https://automat.readthedocs.io/
|
||||
Project-URL: Source, https://github.com/glyph/automat/
|
||||
Keywords: fsm,state machine,automata
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
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
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: typing-extensions; python_version < "3.10"
|
||||
Provides-Extra: visualize
|
||||
Requires-Dist: graphviz>0.5.1; extra == "visualize"
|
||||
Requires-Dist: Twisted>=16.1.1; extra == "visualize"
|
||||
|
||||
# Automat #
|
||||
|
||||
[](http://automat.readthedocs.io/en/latest/)
|
||||
[](https://github.com/glyph/automat/actions/workflows/ci.yml?query=branch%3Atrunk)
|
||||
[](http://codecov.io/github/glyph/automat?branch=trunk)
|
||||
|
||||
## Self-service finite-state machines for the programmer on the go. ##
|
||||
|
||||
Automat is a library for concise, idiomatic Python expression of finite-state
|
||||
automata (particularly deterministic finite-state transducers).
|
||||
|
||||
Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation
|
||||
|
||||
### Why use state machines? ###
|
||||
|
||||
Sometimes you have to create an object whose behavior varies with its state,
|
||||
but still wishes to present a consistent interface to its callers.
|
||||
|
||||
For example, let's say you're writing the software for a coffee machine. It
|
||||
has a lid that can be opened or closed, a chamber for water, a chamber for
|
||||
coffee beans, and a button for "brew".
|
||||
|
||||
There are a number of possible states for the coffee machine. It might or
|
||||
might not have water. It might or might not have beans. The lid might be open
|
||||
or closed. The "brew" button should only actually attempt to brew coffee in
|
||||
one of these configurations, and the "open lid" button should only work if the
|
||||
coffee is not, in fact, brewing.
|
||||
|
||||
With diligence and attention to detail, you can implement this correctly using
|
||||
a collection of attributes on an object; `hasWater`, `hasBeans`, `isLidOpen`
|
||||
and so on. However, you have to keep all these attributes consistent. As the
|
||||
coffee maker becomes more complex - perhaps you add an additional chamber for
|
||||
flavorings so you can make hazelnut coffee, for example - you have to keep
|
||||
adding more and more checks and more and more reasoning about which
|
||||
combinations of states are allowed.
|
||||
|
||||
Rather than adding tedious `if` checks to every single method to make sure that
|
||||
each of these flags are exactly what you expect, you can use a state machine to
|
||||
ensure that if your code runs at all, it will be run with all the required
|
||||
values initialized, because they have to be called in the order you declare
|
||||
them.
|
||||
|
||||
You can read about state machines and their advantages for Python programmers
|
||||
in more detail [in this excellent article by Jean-Paul
|
||||
Calderone](https://web.archive.org/web/20160507053658/https://clusterhq.com/2013/12/05/what-is-a-state-machine/).
|
||||
|
||||
### What makes Automat different? ###
|
||||
|
||||
There are
|
||||
[dozens of libraries on PyPI implementing state machines](https://pypi.org/search/?q=finite+state+machine).
|
||||
So it behooves me to say why yet another one would be a good idea.
|
||||
|
||||
Automat is designed around this principle: while organizing your code around
|
||||
state machines is a good idea, your callers don't, and shouldn't have to, care
|
||||
that you've done so. In Python, the "input" to a stateful system is a method
|
||||
call; the "output" may be a method call, if you need to invoke a side effect,
|
||||
or a return value, if you are just performing a computation in memory. Most
|
||||
other state-machine libraries require you to explicitly create an input object,
|
||||
provide that object to a generic "input" method, and then receive results,
|
||||
sometimes in terms of that library's interfaces and sometimes in terms of
|
||||
classes you define yourself.
|
||||
|
||||
For example, a snippet of the coffee-machine example above might be implemented
|
||||
as follows in naive Python:
|
||||
|
||||
```python
|
||||
class CoffeeMachine(object):
|
||||
def brewButton(self) -> None:
|
||||
if self.hasWater and self.hasBeans and not self.isLidOpen:
|
||||
self.heatTheHeatingElement()
|
||||
# ...
|
||||
```
|
||||
|
||||
With Automat, you'd begin with a `typing.Protocol` that describes all of your
|
||||
inputs:
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class CoffeeBrewer(Protocol):
|
||||
def brewButton(self) -> None:
|
||||
"The user pressed the 'brew' button."
|
||||
def putInBeans(self) -> None:
|
||||
"The user put in some beans."
|
||||
```
|
||||
|
||||
We'll then need a concrete class to contain the shared core of state shared
|
||||
among the different states:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class BrewerCore:
|
||||
heatingElement: HeatingElement
|
||||
```
|
||||
|
||||
Next, we need to describe our state machine, including all of our states. For
|
||||
simplicity's sake let's say that the only two states are `noBeans` and
|
||||
`haveBeans`:
|
||||
|
||||
```python
|
||||
from automat import TypeMachineBuilder
|
||||
|
||||
builder = TypeMachineBuilder(CoffeeBrewer, BrewerCore)
|
||||
noBeans = builder.state("noBeans")
|
||||
haveBeans = builder.state("haveBeans")
|
||||
```
|
||||
|
||||
Next we can describe a simple transition; when we put in beans, we move to the
|
||||
`haveBeans` state, with no other behavior.
|
||||
|
||||
```python
|
||||
# When we don't have beans, upon putting in beans, we will then have beans
|
||||
noBeans.upon(CoffeeBrewer.putInBeans).to(haveBeans).returns(None)
|
||||
```
|
||||
|
||||
And then another transition that we describe with a decorator, one that *does*
|
||||
have some behavior, that needs to heat up the heating element to brew the
|
||||
coffee:
|
||||
|
||||
```python
|
||||
@haveBeans.upon(CoffeeBrewer.brewButton).to(noBeans)
|
||||
def heatUp(inputs: CoffeeBrewer, core: BrewerCore) -> None:
|
||||
"""
|
||||
When we have beans, upon pressing the brew button, we will then not have
|
||||
beans any more (as they have been entered into the brewing chamber) and
|
||||
our output will be heating the heating element.
|
||||
"""
|
||||
print("Brewing the coffee...")
|
||||
core.heatingElement.turnOn()
|
||||
```
|
||||
|
||||
Then we finalize the state machine by building it, which gives us a callable
|
||||
that takes a `BrewerCore` and returns a synthetic `CoffeeBrewer`
|
||||
|
||||
```python
|
||||
newCoffeeMachine = builder.build()
|
||||
```
|
||||
|
||||
```python
|
||||
>>> coffee = newCoffeeMachine(BrewerCore(HeatingElement()))
|
||||
>>> machine.putInBeans()
|
||||
>>> machine.brewButton()
|
||||
Brewing the coffee...
|
||||
```
|
||||
|
||||
All of the *inputs* are provided by calling them like methods, all of the
|
||||
*output behaviors* are automatically invoked when they are produced according
|
||||
to the outputs specified to `upon` and all of the states are simply opaque
|
||||
tokens.
|
||||
@ -0,0 +1,40 @@
|
||||
../../../bin/automat-visualize,sha256=Gyv_3d6TeKMpzxHJb64voO8imr3m8LPZReWuLMxsYwE,254
|
||||
Automat-24.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
Automat-24.8.1.dist-info/LICENSE,sha256=siATAWeNCpN9k4VDgnyhNgcS6zTiPejuPzv_-9TA43Y,1053
|
||||
Automat-24.8.1.dist-info/METADATA,sha256=XJIxL4Olb15WCoAdVEcORum39__wiCXQR6LNubERZ6M,8396
|
||||
Automat-24.8.1.dist-info/RECORD,,
|
||||
Automat-24.8.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
Automat-24.8.1.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
|
||||
Automat-24.8.1.dist-info/entry_points.txt,sha256=D5Dc6byHpLWAktM443OHbx0ZGl1ZBZtf6rPNlGi7NbQ,62
|
||||
Automat-24.8.1.dist-info/top_level.txt,sha256=vg4zAOyhP_3YCmpKZLNgFw1uMF3lC_b6TKsdz7jBSpI,8
|
||||
automat/__init__.py,sha256=8yHAuqaxK0mdiOEXHbwe6WaNzaY01k2HMWD_RltPB-U,356
|
||||
automat/__pycache__/__init__.cpython-312.pyc,,
|
||||
automat/__pycache__/_core.cpython-312.pyc,,
|
||||
automat/__pycache__/_discover.cpython-312.pyc,,
|
||||
automat/__pycache__/_introspection.cpython-312.pyc,,
|
||||
automat/__pycache__/_methodical.cpython-312.pyc,,
|
||||
automat/__pycache__/_runtimeproto.cpython-312.pyc,,
|
||||
automat/__pycache__/_typed.cpython-312.pyc,,
|
||||
automat/__pycache__/_visualize.cpython-312.pyc,,
|
||||
automat/_core.py,sha256=oe4QNlfvmgsnKe_8fyNiOsHsfz5xPArGuXWle9zePp8,6663
|
||||
automat/_discover.py,sha256=KRbmm7kxpd-WReDQU4qe6hVKGUKmGBHUjYIkRneO4mc,5197
|
||||
automat/_introspection.py,sha256=uF5ymY-GZckyRxvRs7UToPBV_oVV6xHmvlBVey9nv80,1441
|
||||
automat/_methodical.py,sha256=YgZXraDe6dvK6w_Y9xfPa0kXCka4ZeGLfcb4IfK4bnI,17243
|
||||
automat/_runtimeproto.py,sha256=mJ_4VuEGpLc1u7Ptm5cfaLUq7LGD7KfrvmsAa4LsyuU,1654
|
||||
automat/_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
automat/_test/__pycache__/__init__.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_core.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_discover.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_methodical.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_trace.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_type_based.cpython-312.pyc,,
|
||||
automat/_test/__pycache__/test_visualize.cpython-312.pyc,,
|
||||
automat/_test/test_core.py,sha256=PJHNvQ85i8vjH-oF6nPNKB84_noTyl2dQSv_iRl70J8,3481
|
||||
automat/_test/test_discover.py,sha256=ROnW7eSLE4T76FVncY2UK05DIYcHZ3TSGGg7kQnbvtw,22067
|
||||
automat/_test/test_methodical.py,sha256=SKMMbl-6bjH-QZ1hDFRItCOyKF4SstuA4QvExnVhn7w,20267
|
||||
automat/_test/test_trace.py,sha256=tty7P_ctJtk38ZXnpmEy-J9Rn-Hh2AKb_ia6EmtXSQI,3299
|
||||
automat/_test/test_type_based.py,sha256=8oZxz3T7zIvyMKg4THg7M85zaHtE4QU9nAegU_OUvko,17872
|
||||
automat/_test/test_visualize.py,sha256=HXBPgMAD0OTz_l1Yq0lI3d1vJ_l3sHTH67Jauaf-2sk,14631
|
||||
automat/_typed.py,sha256=lMzMgUfX713Xw_W4pr8iyPqcdpRSbu4rEpRlrXAOW2k,24204
|
||||
automat/_visualize.py,sha256=DQYig2mBKX-LquPEEy89Y_qyj21tSutAUaFsF1F64ws,6512
|
||||
automat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (72.2.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
automat-visualize = automat._visualize:tool
|
||||
@ -0,0 +1 @@
|
||||
automat
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,28 @@
|
||||
Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,92 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: MarkupSafe
|
||||
Version: 3.0.2
|
||||
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||
Maintainer-email: Pallets <contact@palletsprojects.com>
|
||||
License: Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||
Project-URL: Source, https://github.com/pallets/markupsafe/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE.txt
|
||||
|
||||
# MarkupSafe
|
||||
|
||||
MarkupSafe implements a text object that escapes characters so it is
|
||||
safe to use in HTML and XML. Characters that have special meanings are
|
||||
replaced so that they display as the actual characters. This mitigates
|
||||
injection attacks, meaning untrusted user input can safely be displayed
|
||||
on a page.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```pycon
|
||||
>>> from markupsafe import Markup, escape
|
||||
|
||||
>>> # escape replaces special characters and wraps in Markup
|
||||
>>> escape("<script>alert(document.cookie);</script>")
|
||||
Markup('<script>alert(document.cookie);</script>')
|
||||
|
||||
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||
>>> Markup("<strong>Hello</strong>")
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> escape(Markup("<strong>Hello</strong>"))
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> # Markup is a str subclass
|
||||
>>> # methods and operators escape their arguments
|
||||
>>> template = Markup("Hello <em>{name}</em>")
|
||||
>>> template.format(name='"World"')
|
||||
Markup('Hello <em>"World"</em>')
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports MarkupSafe and other
|
||||
popular packages. In order to grow the community of contributors and
|
||||
users, and allow the maintainers to devote more time to the projects,
|
||||
[please donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
@ -0,0 +1,14 @@
|
||||
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
|
||||
MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975
|
||||
MarkupSafe-3.0.2.dist-info/RECORD,,
|
||||
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=OVgtqZzfzIXXtylXP90gxCZ6CKBCwKYyHM8PpMEjN1M,151
|
||||
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||
markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214
|
||||
markupsafe/__pycache__/__init__.cpython-312.pyc,,
|
||||
markupsafe/__pycache__/_native.cpython-312.pyc,,
|
||||
markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210
|
||||
markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149
|
||||
markupsafe/_speedups.cpython-312-x86_64-linux-gnu.so,sha256=t1DBZlpsjFA30BOOvXfXfT1wvO_4cS16VbHz1-49q5U,43432
|
||||
markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41
|
||||
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.2.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||
Tag: cp312-cp312-manylinux2014_x86_64
|
||||
|
||||
@ -0,0 +1 @@
|
||||
markupsafe
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,19 @@
|
||||
Copyright 2005-2024 SQLAlchemy authors and contributors <see AUTHORS file>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,243 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: SQLAlchemy
|
||||
Version: 2.0.36
|
||||
Summary: Database Abstraction Library
|
||||
Home-page: https://www.sqlalchemy.org
|
||||
Author: Mike Bayer
|
||||
Author-email: mike_mp@zzzcomputing.com
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://docs.sqlalchemy.org
|
||||
Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Database :: Front-Ends
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: typing-extensions >=4.6.0
|
||||
Requires-Dist: greenlet !=0.4.17 ; python_version < "3.13" and (platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32"))))))
|
||||
Requires-Dist: importlib-metadata ; python_version < "3.8"
|
||||
Provides-Extra: aiomysql
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
|
||||
Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
|
||||
Provides-Extra: aioodbc
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc'
|
||||
Requires-Dist: aioodbc ; extra == 'aioodbc'
|
||||
Provides-Extra: aiosqlite
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
|
||||
Requires-Dist: aiosqlite ; extra == 'aiosqlite'
|
||||
Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
|
||||
Provides-Extra: asyncio
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
|
||||
Provides-Extra: asyncmy
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
|
||||
Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
|
||||
Provides-Extra: mariadb_connector
|
||||
Requires-Dist: mariadb !=1.1.10,!=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
|
||||
Provides-Extra: mssql
|
||||
Requires-Dist: pyodbc ; extra == 'mssql'
|
||||
Provides-Extra: mssql_pymssql
|
||||
Requires-Dist: pymssql ; extra == 'mssql_pymssql'
|
||||
Provides-Extra: mssql_pyodbc
|
||||
Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
|
||||
Provides-Extra: mypy
|
||||
Requires-Dist: mypy >=0.910 ; extra == 'mypy'
|
||||
Provides-Extra: mysql
|
||||
Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
|
||||
Provides-Extra: mysql_connector
|
||||
Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
|
||||
Provides-Extra: oracle
|
||||
Requires-Dist: cx-oracle >=8 ; extra == 'oracle'
|
||||
Provides-Extra: oracle_oracledb
|
||||
Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
|
||||
Provides-Extra: postgresql
|
||||
Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
|
||||
Provides-Extra: postgresql_asyncpg
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
|
||||
Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
|
||||
Provides-Extra: postgresql_pg8000
|
||||
Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
|
||||
Provides-Extra: postgresql_psycopg
|
||||
Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
|
||||
Provides-Extra: postgresql_psycopg2binary
|
||||
Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
|
||||
Provides-Extra: postgresql_psycopg2cffi
|
||||
Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
|
||||
Provides-Extra: postgresql_psycopgbinary
|
||||
Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
|
||||
Provides-Extra: pymysql
|
||||
Requires-Dist: pymysql ; extra == 'pymysql'
|
||||
Provides-Extra: sqlcipher
|
||||
Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
|
||||
|
||||
SQLAlchemy
|
||||
==========
|
||||
|
||||
|PyPI| |Python| |Downloads|
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI
|
||||
|
||||
.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI - Python Version
|
||||
|
||||
.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
|
||||
:target: https://pepy.tech/project/sqlalchemy
|
||||
:alt: PyPI - Downloads
|
||||
|
||||
|
||||
The Python SQL Toolkit and Object Relational Mapper
|
||||
|
||||
Introduction
|
||||
-------------
|
||||
|
||||
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
|
||||
that gives application developers the full power and
|
||||
flexibility of SQL. SQLAlchemy provides a full suite
|
||||
of well known enterprise-level persistence patterns,
|
||||
designed for efficient and high-performing database
|
||||
access, adapted into a simple and Pythonic domain
|
||||
language.
|
||||
|
||||
Major SQLAlchemy features include:
|
||||
|
||||
* An industrial strength ORM, built
|
||||
from the core on the identity map, unit of work,
|
||||
and data mapper patterns. These patterns
|
||||
allow transparent persistence of objects
|
||||
using a declarative configuration system.
|
||||
Domain models
|
||||
can be constructed and manipulated naturally,
|
||||
and changes are synchronized with the
|
||||
current transaction automatically.
|
||||
* A relationally-oriented query system, exposing
|
||||
the full range of SQL's capabilities
|
||||
explicitly, including joins, subqueries,
|
||||
correlation, and most everything else,
|
||||
in terms of the object model.
|
||||
Writing queries with the ORM uses the same
|
||||
techniques of relational composition you use
|
||||
when writing SQL. While you can drop into
|
||||
literal SQL at any time, it's virtually never
|
||||
needed.
|
||||
* A comprehensive and flexible system
|
||||
of eager loading for related collections and objects.
|
||||
Collections are cached within a session,
|
||||
and can be loaded on individual access, all
|
||||
at once using joins, or by query per collection
|
||||
across the full result set.
|
||||
* A Core SQL construction system and DBAPI
|
||||
interaction layer. The SQLAlchemy Core is
|
||||
separate from the ORM and is a full database
|
||||
abstraction layer in its own right, and includes
|
||||
an extensible Python-based SQL expression
|
||||
language, schema metadata, connection pooling,
|
||||
type coercion, and custom types.
|
||||
* All primary and foreign key constraints are
|
||||
assumed to be composite and natural. Surrogate
|
||||
integer primary keys are of course still the
|
||||
norm, but SQLAlchemy never assumes or hardcodes
|
||||
to this model.
|
||||
* Database introspection and generation. Database
|
||||
schemas can be "reflected" in one step into
|
||||
Python structures representing database metadata;
|
||||
those same structures can then generate
|
||||
CREATE statements right back out - all within
|
||||
the Core, independent of the ORM.
|
||||
|
||||
SQLAlchemy's philosophy:
|
||||
|
||||
* SQL databases behave less and less like object
|
||||
collections the more size and performance start to
|
||||
matter; object collections behave less and less like
|
||||
tables and rows the more abstraction starts to matter.
|
||||
SQLAlchemy aims to accommodate both of these
|
||||
principles.
|
||||
* An ORM doesn't need to hide the "R". A relational
|
||||
database provides rich, set-based functionality
|
||||
that should be fully exposed. SQLAlchemy's
|
||||
ORM provides an open-ended set of patterns
|
||||
that allow a developer to construct a custom
|
||||
mediation layer between a domain model and
|
||||
a relational schema, turning the so-called
|
||||
"object relational impedance" issue into
|
||||
a distant memory.
|
||||
* The developer, in all cases, makes all decisions
|
||||
regarding the design, structure, and naming conventions
|
||||
of both the object model as well as the relational
|
||||
schema. SQLAlchemy only provides the means
|
||||
to automate the execution of these decisions.
|
||||
* With SQLAlchemy, there's no such thing as
|
||||
"the ORM generated a bad query" - you
|
||||
retain full control over the structure of
|
||||
queries, including how joins are organized,
|
||||
how subqueries and correlation is used, what
|
||||
columns are requested. Everything SQLAlchemy
|
||||
does is ultimately the result of a developer-initiated
|
||||
decision.
|
||||
* Don't use an ORM if the problem doesn't need one.
|
||||
SQLAlchemy consists of a Core and separate ORM
|
||||
component. The Core offers a full SQL expression
|
||||
language that allows Pythonic construction
|
||||
of SQL constructs that render directly to SQL
|
||||
strings for a target database, returning
|
||||
result sets that are essentially enhanced DBAPI
|
||||
cursors.
|
||||
* Transactions should be the norm. With SQLAlchemy's
|
||||
ORM, nothing goes to permanent storage until
|
||||
commit() is called. SQLAlchemy encourages applications
|
||||
to create a consistent means of delineating
|
||||
the start and end of a series of operations.
|
||||
* Never render a literal value in a SQL statement.
|
||||
Bound parameters are used to the greatest degree
|
||||
possible, allowing query optimizers to cache
|
||||
query plans effectively and making SQL injection
|
||||
attacks a non-issue.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Latest documentation is at:
|
||||
|
||||
https://www.sqlalchemy.org/docs/
|
||||
|
||||
Installation / Requirements
|
||||
---------------------------
|
||||
|
||||
Full documentation for installation is at
|
||||
`Installation <https://www.sqlalchemy.org/docs/intro.html#installation>`_.
|
||||
|
||||
Getting Help / Development / Bug reporting
|
||||
------------------------------------------
|
||||
|
||||
Please refer to the `SQLAlchemy Community Guide <https://www.sqlalchemy.org/support.html>`_.
|
||||
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
|
||||
constructive communication between users and developers.
|
||||
Please see our current Code of Conduct at
|
||||
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
SQLAlchemy is distributed under the `MIT license
|
||||
<https://www.opensource.org/licenses/mit-license.php>`_.
|
||||
|
||||
@ -0,0 +1,530 @@
|
||||
SQLAlchemy-2.0.36.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
SQLAlchemy-2.0.36.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100
|
||||
SQLAlchemy-2.0.36.dist-info/METADATA,sha256=EZH514FydYtyOhgoZk_OF1ZQEtI4eTAEddlnUlRjzac,9692
|
||||
SQLAlchemy-2.0.36.dist-info/RECORD,,
|
||||
SQLAlchemy-2.0.36.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
SQLAlchemy-2.0.36.dist-info/WHEEL,sha256=7B4nnId14TToQHuAKpxbDLCJbNciqBsV-mvXE2hVLJc,151
|
||||
SQLAlchemy-2.0.36.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
|
||||
sqlalchemy/__init__.py,sha256=J2PsdiJiNW93Etxk6YN8o_C3TcpR1_DckU71r4LBcGE,13033
|
||||
sqlalchemy/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/inspection.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/log.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__init__.py,sha256=PzXPqZqi3BzEnrs1eW0DcsR4lyknAzhhN9rWcQ97hb4,476
|
||||
sqlalchemy/connectors/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/aioodbc.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/asyncio.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/aioodbc.py,sha256=GSTiNMO9h0qjPxgqaxDwWZ8HvhWMFNVR6MJQnN1oc40,5288
|
||||
sqlalchemy/connectors/asyncio.py,sha256=Hq2bkXmG6-KO_RfCrwMqx4oGH-uH1Z1WWKqPWNjz8p4,6138
|
||||
sqlalchemy/connectors/pyodbc.py,sha256=t7AjyxIOnaWg3CrlUEpBs4Y5l0HFdNt3P_cSSKhbi0Y,8501
|
||||
sqlalchemy/cyextension/__init__.py,sha256=GzhhN8cjMnDTE0qerlUlpbrNmFPHQWCZ4Gk74OAxl04,244
|
||||
sqlalchemy/cyextension/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/cyextension/collections.cpython-312-x86_64-linux-gnu.so,sha256=ofziMIrcxCV-AGNBEvHL7QorRR2SPA9bkQj_k3WLD9E,1932256
|
||||
sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571
|
||||
sqlalchemy/cyextension/immutabledict.cpython-312-x86_64-linux-gnu.so,sha256=7SNuSRYPX4hzKqjAtbxJT2xLwa6Aegqtjfc9MYYZV0w,805632
|
||||
sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291
|
||||
sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535
|
||||
sqlalchemy/cyextension/processors.cpython-312-x86_64-linux-gnu.so,sha256=hBeVC8lWoCgLmD5det5fcoL4V8LYT9Cu8TRLaAzWeW0,530680
|
||||
sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792
|
||||
sqlalchemy/cyextension/resultproxy.cpython-312-x86_64-linux-gnu.so,sha256=n6E3F0rPZFVUnpAm1pr3T-Rc8ZMqUcLjdRoOetVvJ8M,621328
|
||||
sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725
|
||||
sqlalchemy/cyextension/util.cpython-312-x86_64-linux-gnu.so,sha256=IbH9FP4ihbuGMhZ95oiRo_6F2wkSAlE2YDzegCVqErg,950928
|
||||
sqlalchemy/cyextension/util.pyx,sha256=B85orxa9LddLuQEaDoVSq1XmAXIbLKxrxpvuB8ogV_o,2530
|
||||
sqlalchemy/dialects/__init__.py,sha256=Kos9Gf5JZg1Vg6GWaCqEbD6e0r1jCwCmcnJIfcxDdcY,1770
|
||||
sqlalchemy/dialects/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/_typing.py,sha256=hyv0nKucX2gI8ispB1IsvaUgrEPn9zEcq9hS7kfstEw,888
|
||||
sqlalchemy/dialects/mssql/__init__.py,sha256=r5t8wFRNtBQoiUWh0WfIEWzXZW6f3D0uDt6NZTW_7Cc,1880
|
||||
sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/aioodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/aioodbc.py,sha256=UQd9ecSMIML713TDnLAviuBVJle7P7i1FtqGZZePk2Y,2022
|
||||
sqlalchemy/dialects/mssql/base.py,sha256=msl_N_a_z8ali7Nthx55AGoV7b5wakCWvWu560BvH9o,132423
|
||||
sqlalchemy/dialects/mssql/information_schema.py,sha256=HswjDc6y0mPXCf_x6VyylHlBdBa4PSY6Evxmmlch700,8084
|
||||
sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816
|
||||
sqlalchemy/dialects/mssql/provision.py,sha256=ZAtt6Div9NLIngMs8kyloxfphw0KDNMsnRCAVd7-esE,5593
|
||||
sqlalchemy/dialects/mssql/pymssql.py,sha256=LAv43q4vBCB85OsAwHQItaQUYTYIO0QJ-jvzaBrswmY,4097
|
||||
sqlalchemy/dialects/mssql/pyodbc.py,sha256=vwM-vBlmRwrqxOc73P0sFOrBSwn24wzc5IkEOpalbXQ,27056
|
||||
sqlalchemy/dialects/mysql/__init__.py,sha256=bxbi4hkysUK2OOVvr1F49akUj1cky27kKb07tgFzI9U,2153
|
||||
sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/expression.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/aiomysql.py,sha256=-oMZnCqNsSki8mlQRTWIwiQPT1OVdZIuANkb90q8LAs,9999
|
||||
sqlalchemy/dialects/mysql/asyncmy.py,sha256=YpuuOh8VknEeqHqUXQGfQ3jhfO3Xb-vZv78Jq5cscJ0,10067
|
||||
sqlalchemy/dialects/mysql/base.py,sha256=giGlZNGrKsNMoSkbzY0PGgfamKjA9rOkSq1o5vKvno4,122755
|
||||
sqlalchemy/dialects/mysql/cymysql.py,sha256=eXT1ry0w_qRxjiO24M980c-8PZ9qSsbhqBHntjEiKB0,2300
|
||||
sqlalchemy/dialects/mysql/dml.py,sha256=HXJMAvimJsqvhj3UZO4vW_6LkF5RqaKbHvklAjor7yU,7645
|
||||
sqlalchemy/dialects/mysql/enumerated.py,sha256=ipEPPQqoXfFwcywNdcLlZCEzHBtnitHRah1Gn6nItcg,8448
|
||||
sqlalchemy/dialects/mysql/expression.py,sha256=lsmQCHKwfPezUnt27d2kR6ohk4IRFCA64KBS16kx5dc,4097
|
||||
sqlalchemy/dialects/mysql/json.py,sha256=l6MEZ0qp8FgiRrIQvOMhyEJq0q6OqiEnvDTx5Cbt9uQ,2269
|
||||
sqlalchemy/dialects/mysql/mariadb.py,sha256=kTfBLioLKk4JFFst4TY_iWqPtnvvQXFHknLfm89H2N8,853
|
||||
sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=_S1aV93kyP52Nvj7HR9weThML4oUvSLsLqiVFdoLR2o,8623
|
||||
sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=oq3mtsNOMldUjs32JbJG2u3Hy3DObyVzUUMYfOkwkHg,5729
|
||||
sqlalchemy/dialects/mysql/mysqldb.py,sha256=qUBbA6STeYGozutyTxHCo5p1W3p59QFFS2FwCgPrjBA,9503
|
||||
sqlalchemy/dialects/mysql/provision.py,sha256=Jnk8UO9_Apd2odR2IQFLrscCfAmYxuBKcB8giS3bBog,3575
|
||||
sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083
|
||||
sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297
|
||||
sqlalchemy/dialects/mysql/reflection.py,sha256=3u34YwT1JJh3uThGZJZ3FKdnUcT7v08QB-tAl1r7VRk,22834
|
||||
sqlalchemy/dialects/mysql/reserved_words.py,sha256=ucKX2p2c3UnMq2ayZuOHuf73eXhu7SKsOsTlIN1Q83I,9258
|
||||
sqlalchemy/dialects/mysql/types.py,sha256=L5cTCsMT1pTedszNEM3jSxFNZEMcHQLprYCZ0vmfsnA,24343
|
||||
sqlalchemy/dialects/oracle/__init__.py,sha256=p4-2gw7TT0bX_MoJXTGD4i8WHctYsK9kCRbkpzykBrc,1493
|
||||
sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/dictionary.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/oracledb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/base.py,sha256=zLMZedrr6j1LvJz4qYnoSjikI5RZY92YFeQHiZ_YvW0,119676
|
||||
sqlalchemy/dialects/oracle/cx_oracle.py,sha256=q8Nyj15UZCE2TWOmxuWp5ZsxiCiGMzqfd_9UkmjIja0,55235
|
||||
sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519
|
||||
sqlalchemy/dialects/oracle/oracledb.py,sha256=fZRKGqNIwW9LG4i8yDOXABrucbfzn_yC86Od-BJ3PcM,13619
|
||||
sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304
|
||||
sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231
|
||||
sqlalchemy/dialects/postgresql/__init__.py,sha256=wwnNAq4wDQzrlPRzDNB06ayuq3L2HNO99nzeEvq-YcU,3892
|
||||
sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/_psycopg_common.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/array.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/named_types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/operators.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg_catalog.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=7TudtgsPiSB8O5kX8W8KxcNYR8t5h_UHb86b_ChL0P8,5696
|
||||
sqlalchemy/dialects/postgresql/array.py,sha256=bWcame7ntmI_Kx6gmBX0-chwADFdLHeCvaDQ4iX8id8,13734
|
||||
sqlalchemy/dialects/postgresql/asyncpg.py,sha256=9P0Itn9eeSBu67kGSsHuzx8xd4YYwRKdiZ5m7bF5onU,41074
|
||||
sqlalchemy/dialects/postgresql/base.py,sha256=dGPsaV3Esw6-AwE3QcgHF0Fray3Yw5-gLLgCvgdxvS0,179083
|
||||
sqlalchemy/dialects/postgresql/dml.py,sha256=Pc69Le6qzmUHHb1FT5zeUSD31dWm6SBgdCAGW89cs3s,11212
|
||||
sqlalchemy/dialects/postgresql/ext.py,sha256=1bZ--iNh2O9ym7l2gXZX48yP3yMO4dqb9RpYro2Mj2Q,16262
|
||||
sqlalchemy/dialects/postgresql/hstore.py,sha256=otAx-RTDfpi_tcXkMuQV0JOIXtYgevgnsikLKKOkI6U,11541
|
||||
sqlalchemy/dialects/postgresql/json.py,sha256=53rQWon9cUXd1yCjIvUpJjWwNyRSy3U7Kz0HV70ftrc,11618
|
||||
sqlalchemy/dialects/postgresql/named_types.py,sha256=3IV1ufo7zJjKmX4VtGDEnoXE6xEqLJAtGG82IiqHXwY,17594
|
||||
sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808
|
||||
sqlalchemy/dialects/postgresql/pg8000.py,sha256=3yoekiWSF-xnaWMqG76XrYPMqerg-42TdmfsW_ivK9E,18640
|
||||
sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=hY3NXEUHxTWD4umhd2aowNu3laC-61Q_qQ_pReyXTUM,9254
|
||||
sqlalchemy/dialects/postgresql/provision.py,sha256=t6TZj0XaWG9zrpCjNr0oJRjAC_WQzaNdp3kaKJIbS8I,5770
|
||||
sqlalchemy/dialects/postgresql/psycopg.py,sha256=Uwf45f9fInOtaExiEdwiP9xzRo7hw0XyZTkRtgdom44,23168
|
||||
sqlalchemy/dialects/postgresql/psycopg2.py,sha256=kwEnflz5bAqJcuO_20eYiCtha_a4m_tg5_lppdDnaeU,31998
|
||||
sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756
|
||||
sqlalchemy/dialects/postgresql/ranges.py,sha256=6CgV7qkxEMJ9AQsiibo_XBLJYzGh-2ZxpG83sRaesVY,32949
|
||||
sqlalchemy/dialects/postgresql/types.py,sha256=Jfxqw9JaKNOq29JRWBublywgb3lLMyzx8YZI7CXpS2s,7300
|
||||
sqlalchemy/dialects/sqlite/__init__.py,sha256=lp9DIggNn349M-7IYhUA8et8--e8FRExWD2V_r1LJk4,1182
|
||||
sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=g3qGV6jmiXabWyb3282g_Nmxtj1jThxGSe9C9yalb-U,12345
|
||||
sqlalchemy/dialects/sqlite/base.py,sha256=LcnW6hzxqTtPlDBOInHumvuDt8a31THA5Jnm4vFvdFI,97811
|
||||
sqlalchemy/dialects/sqlite/dml.py,sha256=9GE55WvwoktKy2fHeT-Wbc9xPHgsbh5oBfd_fckMH5Q,8443
|
||||
sqlalchemy/dialects/sqlite/json.py,sha256=Eoplbb_4dYlfrtmQaI8Xddd2suAIHA-IdbDQYM-LIhs,2777
|
||||
sqlalchemy/dialects/sqlite/provision.py,sha256=UCpmwxf4IWlrpb2eLHGbPTpCFVbdI_KAh2mKtjiLYao,5632
|
||||
sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=OL2S_05DK9kllZj6DOz7QtEl7jI7syxjW6woS725ii4,5356
|
||||
sqlalchemy/dialects/sqlite/pysqlite.py,sha256=aDp47n0J509kl2hDchoaBKXEQVZtkux54DwfKytUAe4,28068
|
||||
sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239
|
||||
sqlalchemy/engine/__init__.py,sha256=Stb2oV6l8w65JvqEo6J4qtKoApcmOpXy3AAxQud4C1o,2818
|
||||
sqlalchemy/engine/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_processors.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_row.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_util.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/characteristics.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/create.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/cursor.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/default.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/interfaces.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/mock.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/processors.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/reflection.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/result.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/row.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/strategies.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/url.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/engine/_py_processors.py,sha256=j9i_lcYYQOYJMcsDerPxI0sVFBIlX5sqoYMdMJlgWPI,3744
|
||||
sqlalchemy/engine/_py_row.py,sha256=wSqoUFzLOJ1f89kgDb6sJm9LUrF5LMFpXPcK1vUsKcs,3787
|
||||
sqlalchemy/engine/_py_util.py,sha256=f2DI3AN1kv6EplelowesCVpwS8hSXNufRkZoQmJtSH8,2484
|
||||
sqlalchemy/engine/base.py,sha256=frWSMmt3dlentYH4QNN3cijdGzp8NbunColUZwWsWgI,122958
|
||||
sqlalchemy/engine/characteristics.py,sha256=N3kbvw_ApMh86wb5yAGnxtPYD4YRhYMWion1H_aVZBI,4765
|
||||
sqlalchemy/engine/create.py,sha256=mYJtOG2ZKM8sgyfjpGpamW15RDU7JXi5s6iibbJHMIs,33206
|
||||
sqlalchemy/engine/cursor.py,sha256=cFq61yrw76k-QR_xNUBWuL-Zeyb14ltG-6jo2Q2iuuw,76392
|
||||
sqlalchemy/engine/default.py,sha256=2wwKKdsagb3QTajRSEw8Hl-EnQ-LmRxy822xOGyenHc,84648
|
||||
sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381
|
||||
sqlalchemy/engine/interfaces.py,sha256=fcVHOmnMo7JZLHzgSKoK3QsdVHH7kJ_AmrDvwW9Ka3k,112936
|
||||
sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179
|
||||
sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379
|
||||
sqlalchemy/engine/reflection.py,sha256=gwGs8y7x6py5z-ZWx3hQqQrwpHepMCTJyQcFwWJjPlw,75364
|
||||
sqlalchemy/engine/result.py,sha256=NZEskTMAcDzK-vjE96Fw8VvBL58s5Y6rt9vXcmZdM4w,77651
|
||||
sqlalchemy/engine/row.py,sha256=9AAQo9zYDL88GcZ3bjcQTwMT-YIcuGTSMAyTfmBJ_yM,12032
|
||||
sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442
|
||||
sqlalchemy/engine/url.py,sha256=8eWkUaIUyDExOcJ2D4xJXRcn4OY1GQJ3Q2duSX6UGAg,30784
|
||||
sqlalchemy/engine/util.py,sha256=bNirO8k1S8yOW61uNH-a9QrWtAJ9VGFgbiR0lk1lUQU,5682
|
||||
sqlalchemy/event/__init__.py,sha256=KBrp622xojnC3FFquxa2JsMamwAbfkvzfv6Op0NKiYc,997
|
||||
sqlalchemy/event/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/api.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/attr.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/legacy.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/registry.cpython-312.pyc,,
|
||||
sqlalchemy/event/api.py,sha256=DtDVgjKSorOfp9MGJ7fgMWrj4seC_hkwF4D8CW1RFZU,8226
|
||||
sqlalchemy/event/attr.py,sha256=X8QeHGK4ioSYht1vkhc11f606_mq_t91jMNIT314ubs,20751
|
||||
sqlalchemy/event/base.py,sha256=270OShTD17-bSFUFnPtKdVnB0NFJZ2AouYPo1wT0aJw,15127
|
||||
sqlalchemy/event/legacy.py,sha256=teMPs00fO-4g8a_z2omcVKkYce5wj_1uvJO2n2MIeuo,8227
|
||||
sqlalchemy/event/registry.py,sha256=nfTSSyhjZZXc5wseWB4sXn-YibSc0LKX8mg17XlWmAo,10835
|
||||
sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525
|
||||
sqlalchemy/exc.py,sha256=M_8-O1hd8i6gbyx-TapV400p_Lxq2QqTGMXUAO-YgCc,23976
|
||||
sqlalchemy/ext/__init__.py,sha256=S1fGKAbycnQDV01gs-JWGaFQ9GCD4QHwKcU2wnugg_o,322
|
||||
sqlalchemy/ext/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/associationproxy.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/automap.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/baked.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/compiler.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/horizontal_shard.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/hybrid.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/indexable.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/instrumentation.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/mutable.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/orderinglist.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/serializer.cpython-312.pyc,,
|
||||
sqlalchemy/ext/associationproxy.py,sha256=ZGc_ssGf7FC6eKrja1iTvnWEKLkFZQA8CiVAjR8iVRw,66062
|
||||
sqlalchemy/ext/asyncio/__init__.py,sha256=1OqSxEyIUn7RWLGyO12F-jAUIvk1I6DXlVy80-Gvkds,1317
|
||||
sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/engine.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/result.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/session.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/base.py,sha256=fl7wxZD9KjgFiCtG3WXrYjHEvanamcsodCqq9pH9lOk,8905
|
||||
sqlalchemy/ext/asyncio/engine.py,sha256=S_IRWX4QAjj2veLSu4Y3gKBIXkKQt7_2StJAK2_KUDY,48190
|
||||
sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639
|
||||
sqlalchemy/ext/asyncio/result.py,sha256=3rbVIY_wySi50JwaK3Kf2qa3c5Fc8W84FtUpt-9i9Vk,30477
|
||||
sqlalchemy/ext/asyncio/scoping.py,sha256=UxHAFxtWKqA7TEozyN2h7MJyzSspTCrS-1SlgQLTExo,52608
|
||||
sqlalchemy/ext/asyncio/session.py,sha256=QpXnqspwYnT28znD1EdpUIaVjQOO1BirtS0BJeBxeZk,63087
|
||||
sqlalchemy/ext/automap.py,sha256=r0mUSyogNyqdBL4m9AA1NXbLiTLQmtvyQymsssNEipo,61581
|
||||
sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807
|
||||
sqlalchemy/ext/compiler.py,sha256=6X6sZCAo9v-PQfLbwBSYQUK0-XH2xTE5Jm0Zg6Ka6eM,20877
|
||||
sqlalchemy/ext/declarative/__init__.py,sha256=20psLdFQbbOWfpdXHZ0CTY6I1k4UqXvKemNVu1LvPOI,1818
|
||||
sqlalchemy/ext/declarative/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/declarative/__pycache__/extensions.cpython-312.pyc,,
|
||||
sqlalchemy/ext/declarative/extensions.py,sha256=uCjN1GisQt54AjqYnKYzJdUjnGd2pZBW47WWdPlS7FE,19547
|
||||
sqlalchemy/ext/horizontal_shard.py,sha256=wuwAPnHymln0unSBnyx-cpX0AfESKSsypaSQTYCvzDk,16750
|
||||
sqlalchemy/ext/hybrid.py,sha256=IYkCaPZ29gm2cPKPg0cWMkLCEqMykD8-JJTvgacGbmc,52458
|
||||
sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032
|
||||
sqlalchemy/ext/instrumentation.py,sha256=sg8ghDjdHSODFXh_jAmpgemnNX1rxCeeXEG3-PMdrNk,15707
|
||||
sqlalchemy/ext/mutable.py,sha256=L5ZkHBGYhMaqO75Xtyrk2DBR44RDk0g6Rz2HzHH0F8Q,37355
|
||||
sqlalchemy/ext/mypy/__init__.py,sha256=0WebDIZmqBD0OTq5JLtd_PmfF9JGxe4d4Qv3Ml3PKUg,241
|
||||
sqlalchemy/ext/mypy/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/apply.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/infer.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/names.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/plugin.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/apply.py,sha256=Aek_-XA1eXihT4attxhfE43yBKtCgsxBSb--qgZKUqc,10550
|
||||
sqlalchemy/ext/mypy/decl_class.py,sha256=1vVJRII2apnLTUbc5HkJS6Z2GueaUv_eKvhbqh7Wik4,17384
|
||||
sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369
|
||||
sqlalchemy/ext/mypy/names.py,sha256=Q3ef8XQBgVm9WUwlItqlYCXDNi_kbV5DdLEgbtEMEI8,10479
|
||||
sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750
|
||||
sqlalchemy/ext/mypy/util.py,sha256=DKRaurkXHI2lAMAAcEO5GLXbX_m2Xqy7l_juh8Byf5U,9960
|
||||
sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384
|
||||
sqlalchemy/ext/serializer.py,sha256=D0g4jMZkRk0Gjr0L-FZe81SR63h0Zs-9JzuWtT_SD7k,6140
|
||||
sqlalchemy/future/__init__.py,sha256=q2mw-gxk_xoxJLEvRoyMha3vO1xSRHrslcExOHZwmPA,512
|
||||
sqlalchemy/future/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/future/__pycache__/engine.cpython-312.pyc,,
|
||||
sqlalchemy/future/engine.py,sha256=AgIw6vMsef8W6tynOTkxsjd6o_OQDwGjLdbpoMD8ue8,495
|
||||
sqlalchemy/inspection.py,sha256=MF-LE358wZDUEl1IH8-Uwt2HI65EsQpQW5o5udHkZwA,5063
|
||||
sqlalchemy/log.py,sha256=8x9UR3nj0uFm6or6bQF-JWb4fYv2zOeQjG_w-0wOJFA,8607
|
||||
sqlalchemy/orm/__init__.py,sha256=ZYys5nL3RFUDCMOLFDBrRI52F6er3S1U1OY9TeORuKs,8463
|
||||
sqlalchemy/orm/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_orm_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/attributes.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/bulk_persistence.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/clsregistry.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/collections.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/context.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_api.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_base.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dependency.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/descriptor_props.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dynamic.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/evaluator.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/identity.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/instrumentation.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/interfaces.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/loading.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapped_collection.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapper.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/path_registry.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/persistence.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/properties.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/query.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/relationships.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/scoping.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/session.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state_changes.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategies.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategy_options.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/sync.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/unitofwork.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/writeonly.cpython-312.pyc,,
|
||||
sqlalchemy/orm/_orm_constructors.py,sha256=8EQfYsDL2k_ev0eK-wxMl3algouczN38Gu43CrRlAlo,103434
|
||||
sqlalchemy/orm/_typing.py,sha256=DVBfpHmDVK4x1zxaGJPY2GoTrAsyR6uexv20Lzf1afc,4973
|
||||
sqlalchemy/orm/attributes.py,sha256=lorOHBJvJJYndOuafWJhHBbQ1pR6FAyimhqz-mErBRQ,92534
|
||||
sqlalchemy/orm/base.py,sha256=FXkYTSCDUJFQSB5pcyPt2wG-dRctf5P6ySjyjVxQsX0,27502
|
||||
sqlalchemy/orm/bulk_persistence.py,sha256=1FC23bRJKjpfbp2D5hYuV1qOVIKGSswu9XPXbbSJ5Mo,72663
|
||||
sqlalchemy/orm/clsregistry.py,sha256=IjoDZwWpjG42ji59L4M1EZvjBEoXPZykzENDtKWxU8A,17974
|
||||
sqlalchemy/orm/collections.py,sha256=WEKuUCRgLhDhJEIBhZ21UrE0pBOyRm2zxD20GvbgA9g,52243
|
||||
sqlalchemy/orm/context.py,sha256=FMPyw07OA9OXWQ32RQx52AEa2xTLSkqdYgx9R_yN1x0,112955
|
||||
sqlalchemy/orm/decl_api.py,sha256=_WPKQ_vSE5k2TLtNmkaxxYmvbhZvkRMrrvCeDxdqDQE,63998
|
||||
sqlalchemy/orm/decl_base.py,sha256=8R7go5sULTYNRlhYiEjXIJkQ34oPp7DY_fC2nS5D5is,83343
|
||||
sqlalchemy/orm/dependency.py,sha256=hgjksUWhgbmgHK5GdJdiDCBgDAIGQXIrY-Tj79tbL2k,47631
|
||||
sqlalchemy/orm/descriptor_props.py,sha256=dR_h4Gvdtpcdp4sj_ZOR4P5Nng2J2vhsvFHouRLlntc,37244
|
||||
sqlalchemy/orm/dynamic.py,sha256=rWAZ-nfAkREuNjt8e_FRdqYrvHDdbODn1CcfyP8Y18k,9816
|
||||
sqlalchemy/orm/evaluator.py,sha256=tRETz4dNZ71VsEA8nG0hpefByB-W0zBt02IxcSR5H2g,12353
|
||||
sqlalchemy/orm/events.py,sha256=1PiGT7JMUWTDAb3X1T79P02BMVDmcWEpatz1FwpLqoA,127777
|
||||
sqlalchemy/orm/exc.py,sha256=IP40P-wOeXhkYk0YizuTC3wqm6W9cPTaQU08f5MMaQ0,7413
|
||||
sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249
|
||||
sqlalchemy/orm/instrumentation.py,sha256=M-kZmkUvHUxtf-0mCA8RIM5QmMH1hWlYR_pKMwaidjA,24321
|
||||
sqlalchemy/orm/interfaces.py,sha256=7Lni4Cue41b1CsmN4VbeUyWwzuNMcKtkrpihc9U-WIw,48690
|
||||
sqlalchemy/orm/loading.py,sha256=9RacpzFOWbuKgPRWHFmyIvD4fYCLAnkpwBFASyQ2CoI,58277
|
||||
sqlalchemy/orm/mapped_collection.py,sha256=zK3d3iozORzDruBUrAmkVC0RR3Orj5szk-TSQ24xzIU,19682
|
||||
sqlalchemy/orm/mapper.py,sha256=W-srpoEc3UIYv_6qTXTd_dG_TVeQcToG77VGrXt85PM,171738
|
||||
sqlalchemy/orm/path_registry.py,sha256=sJZMv_WPqUpHfQtKWaX3WYFeKBcNJ8C3wOM2mkBGkTE,25920
|
||||
sqlalchemy/orm/persistence.py,sha256=dzyB2JOXNwQgaCbN8kh0sEz00WFePr48qf8NWVCUZH8,61701
|
||||
sqlalchemy/orm/properties.py,sha256=eDPFzxYUgdM3uWjHywnb1XW-i0tVKKyx7A2MCD31GQU,29306
|
||||
sqlalchemy/orm/query.py,sha256=Cf0e94-u1XyoXJoOAmr4iFvtCwNY98kxUYyMPenaWTE,117708
|
||||
sqlalchemy/orm/relationships.py,sha256=dS5SY0v1MiD7iCNnAQlHaI6prUQhL5EkXT7ijc8FR8E,128644
|
||||
sqlalchemy/orm/scoping.py,sha256=rJVc7_Lic4V00HZ-UvYFWkVpXqdrMayRmIs4fIwH1UA,78688
|
||||
sqlalchemy/orm/session.py,sha256=CZJTQ-wPwIy0c3AMFxgJnBgaft6eEf4JzcCLcaaCSjg,195979
|
||||
sqlalchemy/orm/state.py,sha256=327-F4TG29s6mLC8oWRiO2PuvYIUZzY1MqUPjtUy7M4,37670
|
||||
sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815
|
||||
sqlalchemy/orm/strategies.py,sha256=-tsBRsmEqkaxAAIn4t2F-U5SrRIPoPCyzpqFYGTAwNs,119866
|
||||
sqlalchemy/orm/strategy_options.py,sha256=oeDl_rMDNAC_90N7ytsni-psXWAeQMhABQFyKBSmai0,85353
|
||||
sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779
|
||||
sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033
|
||||
sqlalchemy/orm/util.py,sha256=5SC4MOVU0cPObexDjpMvXvetueiU5pze42raL94gj24,81021
|
||||
sqlalchemy/orm/writeonly.py,sha256=SYu2sAaHZONk2pW4PmtE871LG-O0P_bjidvKzY1H_zI,22305
|
||||
sqlalchemy/pool/__init__.py,sha256=qiDdq4r4FFAoDrK6ncugF_i6usi_X1LeJt-CuBHey0s,1804
|
||||
sqlalchemy/pool/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/impl.cpython-312.pyc,,
|
||||
sqlalchemy/pool/base.py,sha256=WF4az4ZKuzQGuKeSJeyexaYjmWZUvYdC6KIi8zTGodw,52236
|
||||
sqlalchemy/pool/events.py,sha256=xGjkIUZl490ZDtCHqnQF9ZCwe2Jv93eGXmnQxftB11E,13147
|
||||
sqlalchemy/pool/impl.py,sha256=JwpALSkH-pCoO_6oENbkHYY00Jx9nlttyoI61LivRNc,18944
|
||||
sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/schema.py,sha256=dKiWmgHYjcKQ4TiiD6vD0UMmIsD8u0Fsor1M9AAeGUs,3194
|
||||
sqlalchemy/sql/__init__.py,sha256=UNa9EUiYWoPayf-FzNcwVgQvpsBdInPZfpJesAStN9o,5820
|
||||
sqlalchemy/sql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_dml_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_elements_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_orm_types.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_py_util.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_selectable_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/annotation.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/cache_key.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/coercions.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/compiler.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/crud.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/ddl.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/default_comparator.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/elements.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/expression.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/functions.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/lambdas.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/naming.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/operators.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/roles.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/selectable.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/sqltypes.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/traversals.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/type_api.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/visitors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/_dml_constructors.py,sha256=YdBJex0MCVACv4q2nl_ii3uhxzwU6aDB8zAsratX5UQ,3867
|
||||
sqlalchemy/sql/_elements_constructors.py,sha256=833Flez92odZkE2Vy6SXK8LcoO1AwkfVzOnATJLWFsA,63168
|
||||
sqlalchemy/sql/_orm_types.py,sha256=T-vjcry4C1y0GToFKVxQCnmly_-Zsq4IO4SHN6bvUF4,625
|
||||
sqlalchemy/sql/_py_util.py,sha256=hiM9ePbRSGs60bAMxPFuJCIC_p9SQ1VzqXGiPchiYwE,2173
|
||||
sqlalchemy/sql/_selectable_constructors.py,sha256=wjE6HrLm9cR7bxvZXT8sFLUqT6t_J9G1XyQCnYmBDl0,18780
|
||||
sqlalchemy/sql/_typing.py,sha256=oqwrYHVMtK-AuKGH9c4SgfiOEJUt5vjkzSEzzscMHkM,12771
|
||||
sqlalchemy/sql/annotation.py,sha256=aqbbVz9kfbCT3_66CZ9GEirVN197Cukoqt8rq48FgkQ,18245
|
||||
sqlalchemy/sql/base.py,sha256=M1b-Tg49ikUW2mnZv0aI38oASG6dgeo4jBNWDgJgAg8,73925
|
||||
sqlalchemy/sql/cache_key.py,sha256=0Db8mR8IrpBgdzXs4TGTt98LOpL3c7KABd72MAPKUQQ,33668
|
||||
sqlalchemy/sql/coercions.py,sha256=hAEou9Ycyswzu8yz_Q7QkwL2_c3nctzBJQS2oDEr4iE,40664
|
||||
sqlalchemy/sql/compiler.py,sha256=hrTptbOKIgVIHapywj4Lk5OMwpXvHS-KGg3odFwlo-I,274687
|
||||
sqlalchemy/sql/crud.py,sha256=HBX4QPtW_PYYJmIKfNr-wE8IdEr963N24WXzFBUZOo0,56514
|
||||
sqlalchemy/sql/ddl.py,sha256=lKqvOigbcYrDG0euxd5F4tu9HbBi1kmp3eFPc45HH-8,45636
|
||||
sqlalchemy/sql/default_comparator.py,sha256=utXWsZVGEjflhFfCT4ywa6RnhORc1Rryo87Hga71Rps,16707
|
||||
sqlalchemy/sql/dml.py,sha256=pn0Lm1ofC5qVZzwGWFW73lPCiNba8OsTeemurJgwRyg,65614
|
||||
sqlalchemy/sql/elements.py,sha256=YfccXzQc9DlgF8q15kDf-zKBUY_vpIe0FGaVDBPoic4,176544
|
||||
sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290
|
||||
sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586
|
||||
sqlalchemy/sql/functions.py,sha256=kMMYplvuIHFAPwxBI03SizwaLcYEHzysecWk-R1V-JM,63762
|
||||
sqlalchemy/sql/lambdas.py,sha256=DP0Qz7Ypo8QhzMwygGHYgRhwJMx-rNezO1euouH3iYU,49292
|
||||
sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858
|
||||
sqlalchemy/sql/operators.py,sha256=himArRqBzrljob3Zfhi_ZS-Jleg1u6YFp0g3d7Co6IM,76106
|
||||
sqlalchemy/sql/roles.py,sha256=pOsVn_OZD7mF2gJByHf24Rjopt0_Hu3dUCEOK5t4KS8,7662
|
||||
sqlalchemy/sql/schema.py,sha256=iFleWHkxi-3mKGiK_N1TzUqxnNwOpypB4bWDuAVQe8c,229717
|
||||
sqlalchemy/sql/selectable.py,sha256=cgyV0AsPy4CXAFdhMiTCkbgaHiFilW9sclzxlHJKH3o,236460
|
||||
sqlalchemy/sql/sqltypes.py,sha256=5_N9MhprQFWYc3yjcXgFC_DmvkQU-Jz-Ok9nIMYp2Q4,127469
|
||||
sqlalchemy/sql/traversals.py,sha256=3ScTC1fh1-y8Y478h_2Azmd2xdQdWPWkDve4YgrwMf8,33664
|
||||
sqlalchemy/sql/type_api.py,sha256=SN16_oNZG6G65cvG6ABPcptz_YV5vfB2fknwJZxrkOs,84464
|
||||
sqlalchemy/sql/util.py,sha256=qGHQF-tPCj-m1FBerzT7weCanGcXU7dK5m-W7NHio-4,48077
|
||||
sqlalchemy/sql/visitors.py,sha256=71wdVvhhZL4nJvVwFAs6ssaW-qZgNRSmKjpAcOzF_TA,36317
|
||||
sqlalchemy/testing/__init__.py,sha256=zgitAYzsCWT_U48ZiifXHHLJFo8nZBYmI-5TueA4_lE,3160
|
||||
sqlalchemy/testing/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertions.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertsql.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/asyncio.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/config.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/engines.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/entities.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/exclusions.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/pickleable.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/profiling.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/requirements.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/warnings.cpython-312.pyc,,
|
||||
sqlalchemy/testing/assertions.py,sha256=gL0rA7CCZJbcVgvWOPV91tTZTRwQc1_Ta0-ykBn83Ew,31439
|
||||
sqlalchemy/testing/assertsql.py,sha256=IgQG7l94WaiRP8nTbilJh1ZHZl125g7GPq-S5kmQZN0,16817
|
||||
sqlalchemy/testing/asyncio.py,sha256=kM8uuOqDBagZF0r9xvGmsiirUVLUQ_KBzjUFU67W-b8,3830
|
||||
sqlalchemy/testing/config.py,sha256=AqyH1qub_gDqX0BvlL-JBQe7N-t2wo8655FtwblUNOY,12090
|
||||
sqlalchemy/testing/engines.py,sha256=HFJceEBD3Q_TTFQMTtIV5wGWO_a7oUgoKtUF_z636SM,13481
|
||||
sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354
|
||||
sqlalchemy/testing/exclusions.py,sha256=T8B01hmm8WVs-EKcUOQRzabahPqblWJfOidi6bHJ6GA,12460
|
||||
sqlalchemy/testing/fixtures/__init__.py,sha256=dMClrIoxqlYIFpk2ia4RZpkbfxsS_3EBigr9QsPJ66g,1198
|
||||
sqlalchemy/testing/fixtures/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/mypy.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/orm.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/sql.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/base.py,sha256=9r_J2ksiTzClpUxW0TczICHrWR7Ny8PV8IsBz6TsGFI,12256
|
||||
sqlalchemy/testing/fixtures/mypy.py,sha256=gdxiwNFIzDlNGSOdvM3gbwDceVCC9t8oM5kKbwyhGBk,11973
|
||||
sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095
|
||||
sqlalchemy/testing/fixtures/sql.py,sha256=KZMjco9_3dsuspmkew5Ejp88Wlr9PsSBB1qeJGFxQAk,15900
|
||||
sqlalchemy/testing/pickleable.py,sha256=U9mIqk-zaxq9Xfy7HErP7UrKgTov-A3QFnhZh-NiOjI,2833
|
||||
sqlalchemy/testing/plugin/__init__.py,sha256=79F--BIY_NTBzVRIlJGgAY5LNJJ3cD19XvrAo4X0W9A,247
|
||||
sqlalchemy/testing/plugin/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/bootstrap.py,sha256=oYScMbEW4pCnWlPEAq1insFruCXFQeEVBwo__i4McpU,1685
|
||||
sqlalchemy/testing/plugin/plugin_base.py,sha256=BgNzWNEmgpK4CwhyblQQKnH-7FDKVi_Uul5vw8fFjBU,21578
|
||||
sqlalchemy/testing/plugin/pytestplugin.py,sha256=6jkQHH2VQMD75k2As9CuWXmEy9jrscoFRhCNg6-PaTw,27656
|
||||
sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148
|
||||
sqlalchemy/testing/provision.py,sha256=3qFor_sN1FFlS7odUGkKqLUxGmQZC9XM67I9vQ_zeXo,14626
|
||||
sqlalchemy/testing/requirements.py,sha256=Z__o-1Rj9B7dI8E_l3qsKTvsg0rK198vB0A1p7A5dcM,52832
|
||||
sqlalchemy/testing/schema.py,sha256=lr4GkGrGwagaHMuSGzWdzkMaj3HnS7dgfLLWfxt__-U,6513
|
||||
sqlalchemy/testing/suite/__init__.py,sha256=Y5DRNG0Yl1u3ypt9zVF0Z9suPZeuO_UQGLl-wRgvTjU,722
|
||||
sqlalchemy/testing/suite/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_cte.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_insert.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_results.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_select.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_types.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/test_cte.py,sha256=6zBC3W2OwX1Xs-HedzchcKN2S7EaLNkgkvV_JSZ_Pq0,6451
|
||||
sqlalchemy/testing/suite/test_ddl.py,sha256=1Npkf0C_4UNxphthAGjG078n0vPEgnSIHpDu5MfokxQ,12031
|
||||
sqlalchemy/testing/suite/test_deprecations.py,sha256=BcJxZTcjYqeOAENVElCg3hVvU6fkGEW3KGBMfnW8bng,5337
|
||||
sqlalchemy/testing/suite/test_dialect.py,sha256=EH4ZQWbnGdtjmx5amZtTyhYmrkXJCvW1SQoLahoE7uk,22923
|
||||
sqlalchemy/testing/suite/test_insert.py,sha256=9azifj6-OCD7s8h_tAO1uPw100ibQv8YoKc_VA3hn3c,18824
|
||||
sqlalchemy/testing/suite/test_reflection.py,sha256=7sML8-owubSQeEM7Ve6LbnB8uIVlNV00WWepKwII2a8,109648
|
||||
sqlalchemy/testing/suite/test_results.py,sha256=X720GafdA4p75SOGS93j-dXkt6QDEnnJbU2bh18VCcg,16914
|
||||
sqlalchemy/testing/suite/test_rowcount.py,sha256=3KDTlRgjpQ1OVfp__1cv8Hvq4CsDKzmrhJQ_WIJWoJg,7900
|
||||
sqlalchemy/testing/suite/test_select.py,sha256=ulRZQJlzkwwcewEyisuBEXVWFR0Wshz9MEDxYYiYLwQ,61732
|
||||
sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923
|
||||
sqlalchemy/testing/suite/test_types.py,sha256=K4MGHvnTtgqeksoQOBCZRVQYC7HoYO6Z6rVt5vj2t9o,67805
|
||||
sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141
|
||||
sqlalchemy/testing/suite/test_update_delete.py,sha256=yTiM2unnfOK9rK8ZkqeTTU_MkT-RsKFLmdYliniZfAY,3994
|
||||
sqlalchemy/testing/util.py,sha256=qldXKw8gRJ4I2x3uXsBssYMqwatmcMFMTOveRQCmfDU,14469
|
||||
sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546
|
||||
sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168
|
||||
sqlalchemy/util/__init__.py,sha256=5D5Mquvx3SOmud0QErKzzGvBTkqMdhrrd_sXijOILeo,8312
|
||||
sqlalchemy/util/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_collections.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_has_cy.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_py_collections.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/compat.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/concurrency.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/deprecations.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/langhelpers.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/preloaded.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/queue.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/tool_support.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/topological.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/typing.cpython-312.pyc,,
|
||||
sqlalchemy/util/_collections.py,sha256=aZoSAVOXnHBoYEsxDOi0O9odg9wqLbGb7PGjaWQKiyY,20078
|
||||
sqlalchemy/util/_concurrency_py3k.py,sha256=zb0Bow2Y_QjTdaACEviBEEaFvqDuVvpJfmwCjaw8xNE,9170
|
||||
sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247
|
||||
sqlalchemy/util/_py_collections.py,sha256=U6L5AoyLdgSv7cdqB4xxQbw1rpeJjyOZVXffgxgga8I,16714
|
||||
sqlalchemy/util/compat.py,sha256=cnucBQOKspo58vjRpQXUBrHGguHOSFvftpD-I8vfUy0,8760
|
||||
sqlalchemy/util/concurrency.py,sha256=9lT_cMoO1fZNdY8QTUZ22oeSf-L5I-79Ke7chcBNPA0,3304
|
||||
sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971
|
||||
sqlalchemy/util/langhelpers.py,sha256=uIK3szZuq9aMnO-vEpSlNekNWv4I-E391e56bkTnUm0,65090
|
||||
sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904
|
||||
sqlalchemy/util/queue.py,sha256=CaeSEaYZ57YwtmLdNdOIjT5PK_LCuwMFiO0mpp39ybM,10185
|
||||
sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135
|
||||
sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458
|
||||
sqlalchemy/util/typing.py,sha256=lFcGo1dJbZIZ9drAnvef-PzP0cX4LMxMSwgk3lJBb0g,18182
|
||||
@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.1.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||
Tag: cp312-cp312-manylinux2014_x86_64
|
||||
|
||||
@ -0,0 +1 @@
|
||||
sqlalchemy
|
||||
Binary file not shown.
@ -0,0 +1,16 @@
|
||||
# -*- test-case-name: automat -*-
|
||||
"""
|
||||
State-machines.
|
||||
"""
|
||||
from ._typed import TypeMachineBuilder, pep614, AlreadyBuiltError, TypeMachine
|
||||
from ._core import NoTransition
|
||||
from ._methodical import MethodicalMachine
|
||||
|
||||
__all__ = [
|
||||
"TypeMachineBuilder",
|
||||
"TypeMachine",
|
||||
"NoTransition",
|
||||
"AlreadyBuiltError",
|
||||
"pep614",
|
||||
"MethodicalMachine",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,203 @@
|
||||
# -*- test-case-name: automat._test.test_core -*-
|
||||
|
||||
"""
|
||||
A core state-machine abstraction.
|
||||
|
||||
Perhaps something that could be replaced with or integrated into machinist.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from itertools import chain
|
||||
from typing import Callable, Generic, Optional, Sequence, TypeVar, Hashable
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
_NO_STATE = "<no state>"
|
||||
State = TypeVar("State", bound=Hashable)
|
||||
Input = TypeVar("Input", bound=Hashable)
|
||||
Output = TypeVar("Output", bound=Hashable)
|
||||
|
||||
|
||||
class NoTransition(Exception, Generic[State, Input]):
|
||||
"""
|
||||
A finite state machine in C{state} has no transition for C{symbol}.
|
||||
|
||||
@ivar state: See C{state} init parameter.
|
||||
|
||||
@ivar symbol: See C{symbol} init parameter.
|
||||
"""
|
||||
|
||||
def __init__(self, state: State, symbol: Input):
|
||||
"""
|
||||
Construct a L{NoTransition}.
|
||||
|
||||
@param state: the finite state machine's state at the time of the
|
||||
illegal transition.
|
||||
|
||||
@param symbol: the input symbol for which no transition exists.
|
||||
"""
|
||||
self.state = state
|
||||
self.symbol = symbol
|
||||
super(Exception, self).__init__(
|
||||
"no transition for {} in {}".format(symbol, state)
|
||||
)
|
||||
|
||||
|
||||
class Automaton(Generic[State, Input, Output]):
|
||||
"""
|
||||
A declaration of a finite state machine.
|
||||
|
||||
Note that this is not the machine itself; it is immutable.
|
||||
"""
|
||||
|
||||
def __init__(self, initial: State | None = None) -> None:
|
||||
"""
|
||||
Initialize the set of transitions and the initial state.
|
||||
"""
|
||||
if initial is None:
|
||||
initial = _NO_STATE # type:ignore[assignment]
|
||||
assert initial is not None
|
||||
self._initialState: State = initial
|
||||
self._transitions: set[tuple[State, Input, State, Sequence[Output]]] = set()
|
||||
self._unhandledTransition: Optional[tuple[State, Sequence[Output]]] = None
|
||||
|
||||
@property
|
||||
def initialState(self) -> State:
|
||||
"""
|
||||
Return this automaton's initial state.
|
||||
"""
|
||||
return self._initialState
|
||||
|
||||
@initialState.setter
|
||||
def initialState(self, state: State) -> None:
|
||||
"""
|
||||
Set this automaton's initial state. Raises a ValueError if
|
||||
this automaton already has an initial state.
|
||||
"""
|
||||
|
||||
if self._initialState is not _NO_STATE:
|
||||
raise ValueError(
|
||||
"initial state already set to {}".format(self._initialState)
|
||||
)
|
||||
|
||||
self._initialState = state
|
||||
|
||||
def addTransition(
|
||||
self,
|
||||
inState: State,
|
||||
inputSymbol: Input,
|
||||
outState: State,
|
||||
outputSymbols: tuple[Output, ...],
|
||||
):
|
||||
"""
|
||||
Add the given transition to the outputSymbol. Raise ValueError if
|
||||
there is already a transition with the same inState and inputSymbol.
|
||||
"""
|
||||
# keeping self._transitions in a flat list makes addTransition
|
||||
# O(n^2), but state machines don't tend to have hundreds of
|
||||
# transitions.
|
||||
for anInState, anInputSymbol, anOutState, _ in self._transitions:
|
||||
if anInState == inState and anInputSymbol == inputSymbol:
|
||||
raise ValueError(
|
||||
"already have transition from {} to {} via {}".format(
|
||||
inState, anOutState, inputSymbol
|
||||
)
|
||||
)
|
||||
self._transitions.add((inState, inputSymbol, outState, tuple(outputSymbols)))
|
||||
|
||||
def unhandledTransition(
|
||||
self, outState: State, outputSymbols: Sequence[Output]
|
||||
) -> None:
|
||||
"""
|
||||
All unhandled transitions will be handled by transitioning to the given
|
||||
error state and error-handling output symbols.
|
||||
"""
|
||||
self._unhandledTransition = (outState, tuple(outputSymbols))
|
||||
|
||||
def allTransitions(self) -> frozenset[tuple[State, Input, State, Sequence[Output]]]:
|
||||
"""
|
||||
All transitions.
|
||||
"""
|
||||
return frozenset(self._transitions)
|
||||
|
||||
def inputAlphabet(self) -> set[Input]:
|
||||
"""
|
||||
The full set of symbols acceptable to this automaton.
|
||||
"""
|
||||
return {
|
||||
inputSymbol
|
||||
for (inState, inputSymbol, outState, outputSymbol) in self._transitions
|
||||
}
|
||||
|
||||
def outputAlphabet(self) -> set[Output]:
|
||||
"""
|
||||
The full set of symbols which can be produced by this automaton.
|
||||
"""
|
||||
return set(
|
||||
chain.from_iterable(
|
||||
outputSymbols
|
||||
for (inState, inputSymbol, outState, outputSymbols) in self._transitions
|
||||
)
|
||||
)
|
||||
|
||||
def states(self) -> frozenset[State]:
|
||||
"""
|
||||
All valid states; "Q" in the mathematical description of a state
|
||||
machine.
|
||||
"""
|
||||
return frozenset(
|
||||
chain.from_iterable(
|
||||
(inState, outState)
|
||||
for (inState, inputSymbol, outState, outputSymbol) in self._transitions
|
||||
)
|
||||
)
|
||||
|
||||
def outputForInput(
|
||||
self, inState: State, inputSymbol: Input
|
||||
) -> tuple[State, Sequence[Output]]:
|
||||
"""
|
||||
A 2-tuple of (outState, outputSymbols) for inputSymbol.
|
||||
"""
|
||||
for anInState, anInputSymbol, outState, outputSymbols in self._transitions:
|
||||
if (inState, inputSymbol) == (anInState, anInputSymbol):
|
||||
return (outState, list(outputSymbols))
|
||||
if self._unhandledTransition is None:
|
||||
raise NoTransition(state=inState, symbol=inputSymbol)
|
||||
return self._unhandledTransition
|
||||
|
||||
|
||||
OutputTracer = Callable[[Output], None]
|
||||
Tracer: TypeAlias = "Callable[[State, Input, State], OutputTracer[Output] | None]"
|
||||
|
||||
|
||||
class Transitioner(Generic[State, Input, Output]):
|
||||
"""
|
||||
The combination of a current state and an L{Automaton}.
|
||||
"""
|
||||
|
||||
def __init__(self, automaton: Automaton[State, Input, Output], initialState: State):
|
||||
self._automaton: Automaton[State, Input, Output] = automaton
|
||||
self._state: State = initialState
|
||||
self._tracer: Tracer[State, Input, Output] | None = None
|
||||
|
||||
def setTrace(self, tracer: Tracer[State, Input, Output] | None) -> None:
|
||||
self._tracer = tracer
|
||||
|
||||
def transition(
|
||||
self, inputSymbol: Input
|
||||
) -> tuple[Sequence[Output], OutputTracer[Output] | None]:
|
||||
"""
|
||||
Transition between states, returning any outputs.
|
||||
"""
|
||||
outState, outputSymbols = self._automaton.outputForInput(
|
||||
self._state, inputSymbol
|
||||
)
|
||||
outTracer = None
|
||||
if self._tracer:
|
||||
outTracer = self._tracer(self._state, inputSymbol, outState)
|
||||
self._state = outState
|
||||
return (outputSymbols, outTracer)
|
||||
@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import inspect
|
||||
from typing import Any, Iterator
|
||||
|
||||
from twisted.python.modules import PythonAttribute, PythonModule, getModule
|
||||
|
||||
from automat import MethodicalMachine
|
||||
|
||||
from ._typed import TypeMachine, InputProtocol, Core
|
||||
|
||||
|
||||
def isOriginalLocation(attr: PythonAttribute | PythonModule) -> bool:
|
||||
"""
|
||||
Attempt to discover if this appearance of a PythonAttribute
|
||||
representing a class refers to the module where that class was
|
||||
defined.
|
||||
"""
|
||||
sourceModule = inspect.getmodule(attr.load())
|
||||
if sourceModule is None:
|
||||
return False
|
||||
|
||||
currentModule = attr
|
||||
while not isinstance(currentModule, PythonModule):
|
||||
currentModule = currentModule.onObject
|
||||
|
||||
return currentModule.name == sourceModule.__name__
|
||||
|
||||
|
||||
def findMachinesViaWrapper(
|
||||
within: PythonModule | PythonAttribute,
|
||||
) -> Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]]:
|
||||
"""
|
||||
Recursively yield L{MethodicalMachine}s and their FQPNs within a
|
||||
L{PythonModule} or a L{twisted.python.modules.PythonAttribute}
|
||||
wrapper object.
|
||||
|
||||
Note that L{PythonModule}s may refer to packages, as well.
|
||||
|
||||
The discovery heuristic considers L{MethodicalMachine} instances
|
||||
that are module-level attributes or class-level attributes
|
||||
accessible from module scope. Machines inside nested classes will
|
||||
be discovered, but those returned from functions or methods will not be.
|
||||
|
||||
@type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute}
|
||||
@param within: Where to start the search.
|
||||
|
||||
@return: a generator which yields FQPN, L{MethodicalMachine} pairs.
|
||||
"""
|
||||
queue = collections.deque([within])
|
||||
visited: set[
|
||||
PythonModule
|
||||
| PythonAttribute
|
||||
| MethodicalMachine
|
||||
| TypeMachine[InputProtocol, Core]
|
||||
| type[Any]
|
||||
] = set()
|
||||
|
||||
while queue:
|
||||
attr = queue.pop()
|
||||
value = attr.load()
|
||||
if (
|
||||
isinstance(value, MethodicalMachine) or isinstance(value, TypeMachine)
|
||||
) and value not in visited:
|
||||
visited.add(value)
|
||||
yield attr.name, value
|
||||
elif (
|
||||
inspect.isclass(value) and isOriginalLocation(attr) and value not in visited
|
||||
):
|
||||
visited.add(value)
|
||||
queue.extendleft(attr.iterAttributes())
|
||||
elif isinstance(attr, PythonModule) and value not in visited:
|
||||
visited.add(value)
|
||||
queue.extendleft(attr.iterAttributes())
|
||||
queue.extendleft(attr.iterModules())
|
||||
|
||||
|
||||
class InvalidFQPN(Exception):
|
||||
"""
|
||||
The given FQPN was not a dot-separated list of Python objects.
|
||||
"""
|
||||
|
||||
|
||||
class NoModule(InvalidFQPN):
|
||||
"""
|
||||
A prefix of the FQPN was not an importable module or package.
|
||||
"""
|
||||
|
||||
|
||||
class NoObject(InvalidFQPN):
|
||||
"""
|
||||
A suffix of the FQPN was not an accessible object
|
||||
"""
|
||||
|
||||
|
||||
def wrapFQPN(fqpn: str) -> PythonModule | PythonAttribute:
|
||||
"""
|
||||
Given an FQPN, retrieve the object via the global Python module
|
||||
namespace and wrap it with a L{PythonModule} or a
|
||||
L{twisted.python.modules.PythonAttribute}.
|
||||
"""
|
||||
# largely cribbed from t.p.reflect.namedAny
|
||||
|
||||
if not fqpn:
|
||||
raise InvalidFQPN("FQPN was empty")
|
||||
|
||||
components = collections.deque(fqpn.split("."))
|
||||
|
||||
if "" in components:
|
||||
raise InvalidFQPN(
|
||||
"name must be a string giving a '.'-separated list of Python "
|
||||
"identifiers, not %r" % (fqpn,)
|
||||
)
|
||||
|
||||
component = components.popleft()
|
||||
try:
|
||||
module = getModule(component)
|
||||
except KeyError:
|
||||
raise NoModule(component)
|
||||
|
||||
# find the bottom-most module
|
||||
while components:
|
||||
component = components.popleft()
|
||||
try:
|
||||
module = module[component]
|
||||
except KeyError:
|
||||
components.appendleft(component)
|
||||
break
|
||||
else:
|
||||
module.load()
|
||||
else:
|
||||
return module
|
||||
|
||||
# find the bottom-most attribute
|
||||
attribute = module
|
||||
for component in components:
|
||||
try:
|
||||
attribute = next(
|
||||
child
|
||||
for child in attribute.iterAttributes()
|
||||
if child.name.rsplit(".", 1)[-1] == component
|
||||
)
|
||||
except StopIteration:
|
||||
raise NoObject("{}.{}".format(attribute.name, component))
|
||||
|
||||
return attribute
|
||||
|
||||
|
||||
def findMachines(
|
||||
fqpn: str,
|
||||
) -> Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]]:
|
||||
"""
|
||||
Recursively yield L{MethodicalMachine}s and their FQPNs in and under the a
|
||||
Python object specified by an FQPN.
|
||||
|
||||
The discovery heuristic considers L{MethodicalMachine} instances that are
|
||||
module-level attributes or class-level attributes accessible from module
|
||||
scope. Machines inside nested classes will be discovered, but those
|
||||
returned from functions or methods will not be.
|
||||
|
||||
@param fqpn: a fully-qualified Python identifier (i.e. the dotted
|
||||
identifier of an object defined at module or class scope, including the
|
||||
package and modele names); where to start the search.
|
||||
|
||||
@return: a generator which yields (C{FQPN}, L{MethodicalMachine}) pairs.
|
||||
"""
|
||||
return findMachinesViaWrapper(wrapFQPN(fqpn))
|
||||
@ -0,0 +1,57 @@
|
||||
"""
|
||||
Python introspection helpers.
|
||||
"""
|
||||
|
||||
from types import CodeType as code, FunctionType as function
|
||||
|
||||
|
||||
def copycode(template, changes):
|
||||
if hasattr(code, "replace"):
|
||||
return template.replace(**{"co_" + k: v for k, v in changes.items()})
|
||||
names = [
|
||||
"argcount",
|
||||
"nlocals",
|
||||
"stacksize",
|
||||
"flags",
|
||||
"code",
|
||||
"consts",
|
||||
"names",
|
||||
"varnames",
|
||||
"filename",
|
||||
"name",
|
||||
"firstlineno",
|
||||
"lnotab",
|
||||
"freevars",
|
||||
"cellvars",
|
||||
]
|
||||
if hasattr(code, "co_kwonlyargcount"):
|
||||
names.insert(1, "kwonlyargcount")
|
||||
if hasattr(code, "co_posonlyargcount"):
|
||||
# PEP 570 added "positional only arguments"
|
||||
names.insert(1, "posonlyargcount")
|
||||
values = [changes.get(name, getattr(template, "co_" + name)) for name in names]
|
||||
return code(*values)
|
||||
|
||||
|
||||
def copyfunction(template, funcchanges, codechanges):
|
||||
names = [
|
||||
"globals",
|
||||
"name",
|
||||
"defaults",
|
||||
"closure",
|
||||
]
|
||||
values = [
|
||||
funcchanges.get(name, getattr(template, "__" + name + "__")) for name in names
|
||||
]
|
||||
return function(copycode(template.__code__, codechanges), *values)
|
||||
|
||||
|
||||
def preserveName(f):
|
||||
"""
|
||||
Preserve the name of the given function on the decorated function.
|
||||
"""
|
||||
|
||||
def decorator(decorated):
|
||||
return copyfunction(decorated, dict(name=f.__name__), dict(name=f.__name__))
|
||||
|
||||
return decorator
|
||||
@ -0,0 +1,545 @@
|
||||
# -*- test-case-name: automat._test.test_methodical -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from functools import wraps
|
||||
from inspect import getfullargspec as getArgsSpec
|
||||
from itertools import count
|
||||
from typing import Any, Callable, Hashable, Iterable, TypeVar
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
from typing_extensions import TypeAlias
|
||||
else:
|
||||
from typing import TypeAlias
|
||||
|
||||
from ._core import Automaton, OutputTracer, Tracer, Transitioner
|
||||
from ._introspection import preserveName
|
||||
|
||||
ArgSpec = collections.namedtuple(
|
||||
"ArgSpec",
|
||||
[
|
||||
"args",
|
||||
"varargs",
|
||||
"varkw",
|
||||
"defaults",
|
||||
"kwonlyargs",
|
||||
"kwonlydefaults",
|
||||
"annotations",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _getArgSpec(func):
|
||||
"""
|
||||
Normalize inspect.ArgSpec across python versions
|
||||
and convert mutable attributes to immutable types.
|
||||
|
||||
:param Callable func: A function.
|
||||
:return: The function's ArgSpec.
|
||||
:rtype: ArgSpec
|
||||
"""
|
||||
spec = getArgsSpec(func)
|
||||
return ArgSpec(
|
||||
args=tuple(spec.args),
|
||||
varargs=spec.varargs,
|
||||
varkw=spec.varkw,
|
||||
defaults=spec.defaults if spec.defaults else (),
|
||||
kwonlyargs=tuple(spec.kwonlyargs),
|
||||
kwonlydefaults=(
|
||||
tuple(spec.kwonlydefaults.items()) if spec.kwonlydefaults else ()
|
||||
),
|
||||
annotations=tuple(spec.annotations.items()),
|
||||
)
|
||||
|
||||
|
||||
def _getArgNames(spec):
|
||||
"""
|
||||
Get the name of all arguments defined in a function signature.
|
||||
|
||||
The name of * and ** arguments is normalized to "*args" and "**kwargs".
|
||||
|
||||
:param ArgSpec spec: A function to interrogate for a signature.
|
||||
:return: The set of all argument names in `func`s signature.
|
||||
:rtype: Set[str]
|
||||
"""
|
||||
return set(
|
||||
spec.args
|
||||
+ spec.kwonlyargs
|
||||
+ (("*args",) if spec.varargs else ())
|
||||
+ (("**kwargs",) if spec.varkw else ())
|
||||
+ spec.annotations
|
||||
)
|
||||
|
||||
|
||||
def _keywords_only(f):
|
||||
"""
|
||||
Decorate a function so all its arguments must be passed by keyword.
|
||||
|
||||
A useful utility for decorators that take arguments so that they don't
|
||||
accidentally get passed the thing they're decorating as their first
|
||||
argument.
|
||||
|
||||
Only works for methods right now.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def g(self, **kw):
|
||||
return f(self, **kw)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MethodicalState(object):
|
||||
"""
|
||||
A state for a L{MethodicalMachine}.
|
||||
"""
|
||||
|
||||
machine: MethodicalMachine = field(repr=False)
|
||||
method: Callable[..., Any] = field()
|
||||
serialized: bool = field(repr=False)
|
||||
|
||||
def upon(
|
||||
self,
|
||||
input: MethodicalInput,
|
||||
enter: MethodicalState | None = None,
|
||||
outputs: Iterable[MethodicalOutput] | None = None,
|
||||
collector: Callable[[Iterable[T]], object] = list,
|
||||
) -> None:
|
||||
"""
|
||||
Declare a state transition within the L{MethodicalMachine} associated
|
||||
with this L{MethodicalState}: upon the receipt of the `input`, enter
|
||||
the `state`, emitting each output in `outputs`.
|
||||
|
||||
@param input: The input triggering a state transition.
|
||||
|
||||
@param enter: The resulting state.
|
||||
|
||||
@param outputs: The outputs to be triggered as a result of the declared
|
||||
state transition.
|
||||
|
||||
@param collector: The function to be used when collecting output return
|
||||
values.
|
||||
|
||||
@raises TypeError: if any of the `outputs` signatures do not match the
|
||||
`inputs` signature.
|
||||
|
||||
@raises ValueError: if the state transition from `self` via `input` has
|
||||
already been defined.
|
||||
"""
|
||||
if enter is None:
|
||||
enter = self
|
||||
if outputs is None:
|
||||
outputs = []
|
||||
inputArgs = _getArgNames(input.argSpec)
|
||||
for output in outputs:
|
||||
outputArgs = _getArgNames(output.argSpec)
|
||||
if not outputArgs.issubset(inputArgs):
|
||||
raise TypeError(
|
||||
"method {input} signature {inputSignature} "
|
||||
"does not match output {output} "
|
||||
"signature {outputSignature}".format(
|
||||
input=input.method.__name__,
|
||||
output=output.method.__name__,
|
||||
inputSignature=getArgsSpec(input.method),
|
||||
outputSignature=getArgsSpec(output.method),
|
||||
)
|
||||
)
|
||||
self.machine._oneTransition(self, input, enter, outputs, collector)
|
||||
|
||||
def _name(self) -> str:
|
||||
return self.method.__name__
|
||||
|
||||
|
||||
def _transitionerFromInstance(
|
||||
oself: object,
|
||||
symbol: str,
|
||||
automaton: Automaton[MethodicalState, MethodicalInput, MethodicalOutput],
|
||||
) -> Transitioner[MethodicalState, MethodicalInput, MethodicalOutput]:
|
||||
"""
|
||||
Get a L{Transitioner}
|
||||
"""
|
||||
transitioner = getattr(oself, symbol, None)
|
||||
if transitioner is None:
|
||||
transitioner = Transitioner(
|
||||
automaton,
|
||||
automaton.initialState,
|
||||
)
|
||||
setattr(oself, symbol, transitioner)
|
||||
return transitioner
|
||||
|
||||
|
||||
def _empty():
|
||||
pass
|
||||
|
||||
|
||||
def _docstring():
|
||||
"""docstring"""
|
||||
|
||||
|
||||
def assertNoCode(f: Callable[..., Any]) -> None:
|
||||
# The function body must be empty, i.e. "pass" or "return None", which
|
||||
# both yield the same bytecode: LOAD_CONST (None), RETURN_VALUE. We also
|
||||
# accept functions with only a docstring, which yields slightly different
|
||||
# bytecode, because the "None" is put in a different constant slot.
|
||||
|
||||
# Unfortunately, this does not catch function bodies that return a
|
||||
# constant value, e.g. "return 1", because their code is identical to a
|
||||
# "return None". They differ in the contents of their constant table, but
|
||||
# checking that would require us to parse the bytecode, find the index
|
||||
# being returned, then making sure the table has a None at that index.
|
||||
|
||||
if f.__code__.co_code not in (_empty.__code__.co_code, _docstring.__code__.co_code):
|
||||
raise ValueError("function body must be empty")
|
||||
|
||||
|
||||
def _filterArgs(args, kwargs, inputSpec, outputSpec):
|
||||
"""
|
||||
Filter out arguments that were passed to input that output won't accept.
|
||||
|
||||
:param tuple args: The *args that input received.
|
||||
:param dict kwargs: The **kwargs that input received.
|
||||
:param ArgSpec inputSpec: The input's arg spec.
|
||||
:param ArgSpec outputSpec: The output's arg spec.
|
||||
:return: The args and kwargs that output will accept.
|
||||
:rtype: Tuple[tuple, dict]
|
||||
"""
|
||||
named_args = tuple(zip(inputSpec.args[1:], args))
|
||||
if outputSpec.varargs:
|
||||
# Only return all args if the output accepts *args.
|
||||
return_args = args
|
||||
else:
|
||||
# Filter out arguments that don't appear
|
||||
# in the output's method signature.
|
||||
return_args = [v for n, v in named_args if n in outputSpec.args]
|
||||
|
||||
# Get any of input's default arguments that were not passed.
|
||||
passed_arg_names = tuple(kwargs)
|
||||
for name, value in named_args:
|
||||
passed_arg_names += (name, value)
|
||||
defaults = zip(inputSpec.args[::-1], inputSpec.defaults[::-1])
|
||||
full_kwargs = {n: v for n, v in defaults if n not in passed_arg_names}
|
||||
full_kwargs.update(kwargs)
|
||||
|
||||
if outputSpec.varkw:
|
||||
# Only pass all kwargs if the output method accepts **kwargs.
|
||||
return_kwargs = full_kwargs
|
||||
else:
|
||||
# Filter out names that the output method does not accept.
|
||||
all_accepted_names = outputSpec.args[1:] + outputSpec.kwonlyargs
|
||||
return_kwargs = {
|
||||
n: v for n, v in full_kwargs.items() if n in all_accepted_names
|
||||
}
|
||||
|
||||
return return_args, return_kwargs
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class MethodicalInput(object):
|
||||
"""
|
||||
An input for a L{MethodicalMachine}.
|
||||
"""
|
||||
|
||||
automaton: Automaton[MethodicalState, MethodicalInput, MethodicalOutput] = field(
|
||||
repr=False
|
||||
)
|
||||
method: Callable[..., Any] = field()
|
||||
symbol: str = field(repr=False)
|
||||
collectors: dict[MethodicalState, Callable[[Iterable[T]], R]] = field(
|
||||
default_factory=dict, repr=False
|
||||
)
|
||||
|
||||
argSpec: ArgSpec = field(init=False, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.argSpec = _getArgSpec(self.method)
|
||||
assertNoCode(self.method)
|
||||
|
||||
def __get__(self, oself: object, type: None = None) -> object:
|
||||
"""
|
||||
Return a function that takes no arguments and returns values returned
|
||||
by output functions produced by the given L{MethodicalInput} in
|
||||
C{oself}'s current state.
|
||||
"""
|
||||
transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton)
|
||||
|
||||
@preserveName(self.method)
|
||||
@wraps(self.method)
|
||||
def doInput(*args: object, **kwargs: object) -> object:
|
||||
self.method(oself, *args, **kwargs)
|
||||
previousState = transitioner._state
|
||||
(outputs, outTracer) = transitioner.transition(self)
|
||||
collector = self.collectors[previousState]
|
||||
values = []
|
||||
for output in outputs:
|
||||
if outTracer is not None:
|
||||
outTracer(output)
|
||||
a, k = _filterArgs(args, kwargs, self.argSpec, output.argSpec)
|
||||
value = output(oself, *a, **k)
|
||||
values.append(value)
|
||||
return collector(values)
|
||||
|
||||
return doInput
|
||||
|
||||
def _name(self) -> str:
|
||||
return self.method.__name__
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MethodicalOutput(object):
|
||||
"""
|
||||
An output for a L{MethodicalMachine}.
|
||||
"""
|
||||
|
||||
machine: MethodicalMachine = field(repr=False)
|
||||
method: Callable[..., Any]
|
||||
argSpec: ArgSpec = field(init=False, repr=False, compare=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.__dict__["argSpec"] = _getArgSpec(self.method)
|
||||
|
||||
def __get__(self, oself, type=None):
|
||||
"""
|
||||
Outputs are private, so raise an exception when we attempt to get one.
|
||||
"""
|
||||
raise AttributeError(
|
||||
"{cls}.{method} is a state-machine output method; "
|
||||
"to produce this output, call an input method instead.".format(
|
||||
cls=type.__name__, method=self.method.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def __call__(self, oself, *args, **kwargs):
|
||||
"""
|
||||
Call the underlying method.
|
||||
"""
|
||||
return self.method(oself, *args, **kwargs)
|
||||
|
||||
def _name(self) -> str:
|
||||
return self.method.__name__
|
||||
|
||||
|
||||
StringOutputTracer = Callable[[str], None]
|
||||
StringTracer: TypeAlias = "Callable[[str, str, str], StringOutputTracer | None]"
|
||||
|
||||
|
||||
def wrapTracer(
|
||||
wrapped: StringTracer | None,
|
||||
) -> Tracer[MethodicalState, MethodicalInput, MethodicalOutput] | None:
|
||||
if wrapped is None:
|
||||
return None
|
||||
|
||||
def tracer(
|
||||
state: MethodicalState,
|
||||
input: MethodicalInput,
|
||||
output: MethodicalState,
|
||||
) -> OutputTracer[MethodicalOutput] | None:
|
||||
result = wrapped(state._name(), input._name(), output._name())
|
||||
if result is not None:
|
||||
return lambda out: result(out._name())
|
||||
return None
|
||||
|
||||
return tracer
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class MethodicalTracer(object):
|
||||
automaton: Automaton[MethodicalState, MethodicalInput, MethodicalOutput] = field(
|
||||
repr=False
|
||||
)
|
||||
symbol: str = field(repr=False)
|
||||
|
||||
def __get__(
|
||||
self, oself: object, type: object = None
|
||||
) -> Callable[[StringTracer], None]:
|
||||
transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton)
|
||||
|
||||
def setTrace(tracer: StringTracer | None) -> None:
|
||||
transitioner.setTrace(wrapTracer(tracer))
|
||||
|
||||
return setTrace
|
||||
|
||||
|
||||
counter = count()
|
||||
|
||||
|
||||
def gensym():
|
||||
"""
|
||||
Create a unique Python identifier.
|
||||
"""
|
||||
return "_symbol_" + str(next(counter))
|
||||
|
||||
|
||||
class MethodicalMachine(object):
|
||||
"""
|
||||
A L{MethodicalMachine} is an interface to an L{Automaton} that uses methods
|
||||
on a class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._automaton = Automaton()
|
||||
self._reducers = {}
|
||||
self._symbol = gensym()
|
||||
|
||||
def __get__(self, oself, type=None):
|
||||
"""
|
||||
L{MethodicalMachine} is an implementation detail for setting up
|
||||
class-level state; applications should never need to access it on an
|
||||
instance.
|
||||
"""
|
||||
if oself is not None:
|
||||
raise AttributeError("MethodicalMachine is an implementation detail.")
|
||||
return self
|
||||
|
||||
@_keywords_only
|
||||
def state(
|
||||
self, initial: bool = False, terminal: bool = False, serialized: Hashable = None
|
||||
):
|
||||
"""
|
||||
Declare a state, possibly an initial state or a terminal state.
|
||||
|
||||
This is a decorator for methods, but it will modify the method so as
|
||||
not to be callable any more.
|
||||
|
||||
@param initial: is this state the initial state? Only one state on
|
||||
this L{automat.MethodicalMachine} may be an initial state; more
|
||||
than one is an error.
|
||||
|
||||
@param terminal: Is this state a terminal state? i.e. a state that the
|
||||
machine can end up in? (This is purely informational at this
|
||||
point.)
|
||||
|
||||
@param serialized: a serializable value to be used to represent this
|
||||
state to external systems. This value should be hashable; L{str}
|
||||
is a good type to use.
|
||||
"""
|
||||
|
||||
def decorator(stateMethod):
|
||||
state = MethodicalState(
|
||||
machine=self, method=stateMethod, serialized=serialized
|
||||
)
|
||||
if initial:
|
||||
self._automaton.initialState = state
|
||||
return state
|
||||
|
||||
return decorator
|
||||
|
||||
@_keywords_only
|
||||
def input(self):
|
||||
"""
|
||||
Declare an input.
|
||||
|
||||
This is a decorator for methods.
|
||||
"""
|
||||
|
||||
def decorator(inputMethod):
|
||||
return MethodicalInput(
|
||||
automaton=self._automaton, method=inputMethod, symbol=self._symbol
|
||||
)
|
||||
|
||||
return decorator
|
||||
|
||||
@_keywords_only
|
||||
def output(self):
|
||||
"""
|
||||
Declare an output.
|
||||
|
||||
This is a decorator for methods.
|
||||
|
||||
This method will be called when the state machine transitions to this
|
||||
state as specified in the decorated `output` method.
|
||||
"""
|
||||
|
||||
def decorator(outputMethod):
|
||||
return MethodicalOutput(machine=self, method=outputMethod)
|
||||
|
||||
return decorator
|
||||
|
||||
def _oneTransition(self, startState, inputToken, endState, outputTokens, collector):
|
||||
"""
|
||||
See L{MethodicalState.upon}.
|
||||
"""
|
||||
# FIXME: tests for all of this (some of it is wrong)
|
||||
# if not isinstance(startState, MethodicalState):
|
||||
# raise NotImplementedError("start state {} isn't a state"
|
||||
# .format(startState))
|
||||
# if not isinstance(inputToken, MethodicalInput):
|
||||
# raise NotImplementedError("start state {} isn't an input"
|
||||
# .format(inputToken))
|
||||
# if not isinstance(endState, MethodicalState):
|
||||
# raise NotImplementedError("end state {} isn't a state"
|
||||
# .format(startState))
|
||||
# for output in outputTokens:
|
||||
# if not isinstance(endState, MethodicalState):
|
||||
# raise NotImplementedError("output state {} isn't a state"
|
||||
# .format(endState))
|
||||
self._automaton.addTransition(
|
||||
startState, inputToken, endState, tuple(outputTokens)
|
||||
)
|
||||
inputToken.collectors[startState] = collector
|
||||
|
||||
@_keywords_only
|
||||
def serializer(self):
|
||||
""" """
|
||||
|
||||
def decorator(decoratee):
|
||||
@wraps(decoratee)
|
||||
def serialize(oself):
|
||||
transitioner = _transitionerFromInstance(
|
||||
oself, self._symbol, self._automaton
|
||||
)
|
||||
return decoratee(oself, transitioner._state.serialized)
|
||||
|
||||
return serialize
|
||||
|
||||
return decorator
|
||||
|
||||
@_keywords_only
|
||||
def unserializer(self):
|
||||
""" """
|
||||
|
||||
def decorator(decoratee):
|
||||
@wraps(decoratee)
|
||||
def unserialize(oself, *args, **kwargs):
|
||||
state = decoratee(oself, *args, **kwargs)
|
||||
mapping = {}
|
||||
for eachState in self._automaton.states():
|
||||
mapping[eachState.serialized] = eachState
|
||||
transitioner = _transitionerFromInstance(
|
||||
oself, self._symbol, self._automaton
|
||||
)
|
||||
transitioner._state = mapping[state]
|
||||
return None # it's on purpose
|
||||
|
||||
return unserialize
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def _setTrace(self) -> MethodicalTracer:
|
||||
return MethodicalTracer(self._automaton, self._symbol)
|
||||
|
||||
def asDigraph(self):
|
||||
"""
|
||||
Generate a L{graphviz.Digraph} that represents this machine's
|
||||
states and transitions.
|
||||
|
||||
@return: L{graphviz.Digraph} object; for more information, please
|
||||
see the documentation for
|
||||
U{graphviz<https://graphviz.readthedocs.io/>}
|
||||
|
||||
"""
|
||||
from ._visualize import makeDigraph
|
||||
|
||||
return makeDigraph(
|
||||
self._automaton,
|
||||
stateAsString=lambda state: state.method.__name__,
|
||||
inputAsString=lambda input: input.method.__name__,
|
||||
outputAsString=lambda output: output.method.__name__,
|
||||
)
|
||||
@ -0,0 +1,62 @@
|
||||
"""
|
||||
Workaround for U{the lack of TypeForm
|
||||
<https://github.com/python/mypy/issues/9773>}.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, Protocol, TypeVar
|
||||
|
||||
from inspect import signature, Signature
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
ProtocolAtRuntime = Callable[[], T]
|
||||
|
||||
|
||||
def runtime_name(x: ProtocolAtRuntime[T]) -> str:
|
||||
return x.__name__
|
||||
|
||||
|
||||
from inspect import getmembers, isfunction
|
||||
|
||||
emptyProtocolMethods: frozenset[str]
|
||||
if not TYPE_CHECKING:
|
||||
emptyProtocolMethods = frozenset(
|
||||
name
|
||||
for name, each in getmembers(type("Example", tuple([Protocol]), {}), isfunction)
|
||||
)
|
||||
|
||||
|
||||
def actuallyDefinedProtocolMethods(protocol: object) -> frozenset[str]:
|
||||
"""
|
||||
Attempt to ignore implementation details, and get all the methods that the
|
||||
protocol actually defines.
|
||||
|
||||
that includes locally defined methods and also those defined in inherited
|
||||
superclasses.
|
||||
"""
|
||||
return (
|
||||
frozenset(name for name, each in getmembers(protocol, isfunction))
|
||||
- emptyProtocolMethods
|
||||
)
|
||||
|
||||
|
||||
def _fixAnnotation(method: Callable[..., object], it: object, ann: str) -> None:
|
||||
annotation = getattr(it, ann)
|
||||
if isinstance(annotation, str):
|
||||
setattr(it, ann, eval(annotation, method.__globals__))
|
||||
|
||||
|
||||
def _liveSignature(method: Callable[..., object]) -> Signature:
|
||||
"""
|
||||
Get a signature with evaluated annotations.
|
||||
"""
|
||||
# TODO: could this be replaced with get_type_hints?
|
||||
result = signature(method)
|
||||
for param in result.parameters.values():
|
||||
_fixAnnotation(method, param, "_annotation")
|
||||
_fixAnnotation(method, result, "_return_annotation")
|
||||
return result
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,97 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from .._core import Automaton, NoTransition, Transitioner
|
||||
|
||||
|
||||
class CoreTests(TestCase):
|
||||
"""
|
||||
Tests for Automat's (currently private, implementation detail) core.
|
||||
"""
|
||||
|
||||
def test_NoTransition(self):
|
||||
"""
|
||||
A L{NoTransition} exception describes the state and input symbol
|
||||
that caused it.
|
||||
"""
|
||||
# NoTransition requires two arguments
|
||||
with self.assertRaises(TypeError):
|
||||
NoTransition()
|
||||
|
||||
state = "current-state"
|
||||
symbol = "transitionless-symbol"
|
||||
noTransitionException = NoTransition(state=state, symbol=symbol)
|
||||
|
||||
self.assertIs(noTransitionException.symbol, symbol)
|
||||
|
||||
self.assertIn(state, str(noTransitionException))
|
||||
self.assertIn(symbol, str(noTransitionException))
|
||||
|
||||
def test_unhandledTransition(self) -> None:
|
||||
"""
|
||||
Automaton.unhandledTransition sets the outputs and end-state to be used
|
||||
for all unhandled transitions.
|
||||
"""
|
||||
a: Automaton[str, str, str] = Automaton("start")
|
||||
a.addTransition("oops-state", "check", "start", tuple(["checked"]))
|
||||
a.unhandledTransition("oops-state", ["oops-out"])
|
||||
t = Transitioner(a, "start")
|
||||
self.assertEqual(t.transition("check"), (tuple(["oops-out"]), None))
|
||||
self.assertEqual(t.transition("check"), (["checked"], None))
|
||||
self.assertEqual(t.transition("check"), (tuple(["oops-out"]), None))
|
||||
|
||||
def test_noOutputForInput(self):
|
||||
"""
|
||||
L{Automaton.outputForInput} raises L{NoTransition} if no
|
||||
transition for that input is defined.
|
||||
"""
|
||||
a = Automaton()
|
||||
self.assertRaises(NoTransition, a.outputForInput, "no-state", "no-symbol")
|
||||
|
||||
def test_oneTransition(self):
|
||||
"""
|
||||
L{Automaton.addTransition} adds its input symbol to
|
||||
L{Automaton.inputAlphabet}, all its outputs to
|
||||
L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to
|
||||
start returning the new state and output symbols.
|
||||
"""
|
||||
a = Automaton()
|
||||
a.addTransition("beginning", "begin", "ending", ["end"])
|
||||
self.assertEqual(a.inputAlphabet(), {"begin"})
|
||||
self.assertEqual(a.outputAlphabet(), {"end"})
|
||||
self.assertEqual(a.outputForInput("beginning", "begin"), ("ending", ["end"]))
|
||||
self.assertEqual(a.states(), {"beginning", "ending"})
|
||||
|
||||
def test_oneTransition_nonIterableOutputs(self):
|
||||
"""
|
||||
L{Automaton.addTransition} raises a TypeError when given outputs
|
||||
that aren't iterable and doesn't add any transitions.
|
||||
"""
|
||||
a = Automaton()
|
||||
nonIterableOutputs = 1
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
a.addTransition,
|
||||
"fromState",
|
||||
"viaSymbol",
|
||||
"toState",
|
||||
nonIterableOutputs,
|
||||
)
|
||||
self.assertFalse(a.inputAlphabet())
|
||||
self.assertFalse(a.outputAlphabet())
|
||||
self.assertFalse(a.states())
|
||||
self.assertFalse(a.allTransitions())
|
||||
|
||||
def test_initialState(self):
|
||||
"""
|
||||
L{Automaton.initialState} is a descriptor that sets the initial
|
||||
state if it's not yet set, and raises L{ValueError} if it is.
|
||||
|
||||
"""
|
||||
a = Automaton()
|
||||
a.initialState = "a state"
|
||||
self.assertEqual(a.initialState, "a state")
|
||||
with self.assertRaises(ValueError):
|
||||
a.initialState = "another state"
|
||||
|
||||
|
||||
# FIXME: addTransition for transition that's been added before
|
||||
@ -0,0 +1,638 @@
|
||||
import operator
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
import tempfile
|
||||
from unittest import skipIf, TestCase
|
||||
|
||||
|
||||
def isTwistedInstalled():
|
||||
try:
|
||||
__import__("twisted")
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class _WritesPythonModules(TestCase):
|
||||
"""
|
||||
A helper that enables generating Python module test fixtures.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(_WritesPythonModules, self).setUp()
|
||||
|
||||
from twisted.python.modules import getModule, PythonPath
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
self.getModule = getModule
|
||||
self.PythonPath = PythonPath
|
||||
self.FilePath = FilePath
|
||||
|
||||
self.originalSysModules = set(sys.modules.keys())
|
||||
self.savedSysPath = sys.path[:]
|
||||
|
||||
self.pathDir = tempfile.mkdtemp()
|
||||
self.makeImportable(self.pathDir)
|
||||
|
||||
def tearDown(self):
|
||||
super(_WritesPythonModules, self).tearDown()
|
||||
|
||||
sys.path[:] = self.savedSysPath
|
||||
modulesToDelete = sys.modules.keys() - self.originalSysModules
|
||||
for module in modulesToDelete:
|
||||
del sys.modules[module]
|
||||
|
||||
shutil.rmtree(self.pathDir)
|
||||
|
||||
def makeImportable(self, path):
|
||||
sys.path.append(path)
|
||||
|
||||
def writeSourceInto(self, source, path, moduleName):
|
||||
directory = self.FilePath(path)
|
||||
|
||||
module = directory.child(moduleName)
|
||||
# FilePath always opens a file in binary mode - but that will
|
||||
# break on Python 3
|
||||
with open(module.path, "w") as f:
|
||||
f.write(textwrap.dedent(source))
|
||||
|
||||
return self.PythonPath([directory.path])
|
||||
|
||||
def makeModule(self, source, path, moduleName):
|
||||
pythonModuleName, _ = os.path.splitext(moduleName)
|
||||
return self.writeSourceInto(source, path, moduleName)[pythonModuleName]
|
||||
|
||||
def attributesAsDict(self, hasIterAttributes):
|
||||
return {attr.name: attr for attr in hasIterAttributes.iterAttributes()}
|
||||
|
||||
def loadModuleAsDict(self, module):
|
||||
module.load()
|
||||
return self.attributesAsDict(module)
|
||||
|
||||
def makeModuleAsDict(self, source, path, name):
|
||||
return self.loadModuleAsDict(self.makeModule(source, path, name))
|
||||
|
||||
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class OriginalLocationTests(_WritesPythonModules):
|
||||
"""
|
||||
Tests that L{isOriginalLocation} detects when a
|
||||
L{PythonAttribute}'s FQPN refers to an object inside the module
|
||||
where it was defined.
|
||||
|
||||
For example: A L{twisted.python.modules.PythonAttribute} with a
|
||||
name of 'foo.bar' that refers to a 'bar' object defined in module
|
||||
'baz' does *not* refer to bar's original location, while a
|
||||
L{PythonAttribute} with a name of 'baz.bar' does.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(OriginalLocationTests, self).setUp()
|
||||
from .._discover import isOriginalLocation
|
||||
|
||||
self.isOriginalLocation = isOriginalLocation
|
||||
|
||||
def test_failsWithNoModule(self):
|
||||
"""
|
||||
L{isOriginalLocation} returns False when the attribute refers to an
|
||||
object whose source module cannot be determined.
|
||||
"""
|
||||
source = """\
|
||||
class Fake(object):
|
||||
pass
|
||||
hasEmptyModule = Fake()
|
||||
hasEmptyModule.__module__ = None
|
||||
"""
|
||||
|
||||
moduleDict = self.makeModuleAsDict(source, self.pathDir, "empty_module_attr.py")
|
||||
|
||||
self.assertFalse(
|
||||
self.isOriginalLocation(moduleDict["empty_module_attr.hasEmptyModule"])
|
||||
)
|
||||
|
||||
def test_failsWithDifferentModule(self):
|
||||
"""
|
||||
L{isOriginalLocation} returns False when the attribute refers to
|
||||
an object outside of the module where that object was defined.
|
||||
"""
|
||||
originalSource = """\
|
||||
class ImportThisClass(object):
|
||||
pass
|
||||
importThisObject = ImportThisClass()
|
||||
importThisNestingObject = ImportThisClass()
|
||||
importThisNestingObject.nestedObject = ImportThisClass()
|
||||
"""
|
||||
|
||||
importingSource = """\
|
||||
from original import (ImportThisClass,
|
||||
importThisObject,
|
||||
importThisNestingObject)
|
||||
"""
|
||||
|
||||
self.makeModule(originalSource, self.pathDir, "original.py")
|
||||
importingDict = self.makeModuleAsDict(
|
||||
importingSource, self.pathDir, "importing.py"
|
||||
)
|
||||
self.assertFalse(
|
||||
self.isOriginalLocation(importingDict["importing.ImportThisClass"])
|
||||
)
|
||||
self.assertFalse(
|
||||
self.isOriginalLocation(importingDict["importing.importThisObject"])
|
||||
)
|
||||
|
||||
nestingObject = importingDict["importing.importThisNestingObject"]
|
||||
nestingObjectDict = self.attributesAsDict(nestingObject)
|
||||
nestedObject = nestingObjectDict[
|
||||
"importing.importThisNestingObject.nestedObject"
|
||||
]
|
||||
|
||||
self.assertFalse(self.isOriginalLocation(nestedObject))
|
||||
|
||||
def test_succeedsWithSameModule(self):
|
||||
"""
|
||||
L{isOriginalLocation} returns True when the attribute refers to an
|
||||
object inside the module where that object was defined.
|
||||
"""
|
||||
mSource = textwrap.dedent(
|
||||
"""
|
||||
class ThisClassWasDefinedHere(object):
|
||||
pass
|
||||
anObject = ThisClassWasDefinedHere()
|
||||
aNestingObject = ThisClassWasDefinedHere()
|
||||
aNestingObject.nestedObject = ThisClassWasDefinedHere()
|
||||
"""
|
||||
)
|
||||
mDict = self.makeModuleAsDict(mSource, self.pathDir, "m.py")
|
||||
self.assertTrue(self.isOriginalLocation(mDict["m.ThisClassWasDefinedHere"]))
|
||||
self.assertTrue(self.isOriginalLocation(mDict["m.aNestingObject"]))
|
||||
|
||||
nestingObject = mDict["m.aNestingObject"]
|
||||
nestingObjectDict = self.attributesAsDict(nestingObject)
|
||||
nestedObject = nestingObjectDict["m.aNestingObject.nestedObject"]
|
||||
|
||||
self.assertTrue(self.isOriginalLocation(nestedObject))
|
||||
|
||||
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class FindMachinesViaWrapperTests(_WritesPythonModules):
|
||||
"""
|
||||
L{findMachinesViaWrapper} recursively yields FQPN,
|
||||
L{MethodicalMachine} pairs in and under a given
|
||||
L{twisted.python.modules.PythonModule} or
|
||||
L{twisted.python.modules.PythonAttribute}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(FindMachinesViaWrapperTests, self).setUp()
|
||||
from .._discover import findMachinesViaWrapper
|
||||
|
||||
self.findMachinesViaWrapper = findMachinesViaWrapper
|
||||
|
||||
def test_yieldsMachine(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonAttribute} that refers
|
||||
directly to a L{MethodicalMachine}, L{findMachinesViaWrapper}
|
||||
yields that machine and its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import MethodicalMachine
|
||||
|
||||
rootMachine = MethodicalMachine()
|
||||
"""
|
||||
|
||||
moduleDict = self.makeModuleAsDict(source, self.pathDir, "root.py")
|
||||
rootMachine = moduleDict["root.rootMachine"]
|
||||
self.assertIn(
|
||||
("root.rootMachine", rootMachine.load()),
|
||||
list(self.findMachinesViaWrapper(rootMachine)),
|
||||
)
|
||||
|
||||
def test_yieldsTypeMachine(self) -> None:
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonAttribute} that refers
|
||||
directly to a L{TypeMachine}, L{findMachinesViaWrapper} yields that
|
||||
machine and its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import TypeMachineBuilder
|
||||
from typing import Protocol, Callable
|
||||
class P(Protocol):
|
||||
def method(self) -> None: ...
|
||||
class C:...
|
||||
def buildBuilder() -> Callable[[C], P]:
|
||||
builder = TypeMachineBuilder(P, C)
|
||||
return builder.build()
|
||||
rootMachine = buildBuilder()
|
||||
"""
|
||||
|
||||
moduleDict = self.makeModuleAsDict(source, self.pathDir, "root.py")
|
||||
rootMachine = moduleDict["root.rootMachine"]
|
||||
self.assertIn(
|
||||
("root.rootMachine", rootMachine.load()),
|
||||
list(self.findMachinesViaWrapper(rootMachine)),
|
||||
)
|
||||
|
||||
def test_yieldsMachineInClass(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonAttribute} that refers
|
||||
to a class that contains a L{MethodicalMachine} as a class
|
||||
variable, L{findMachinesViaWrapper} yields that machine and
|
||||
its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import MethodicalMachine
|
||||
|
||||
class PythonClass(object):
|
||||
_classMachine = MethodicalMachine()
|
||||
"""
|
||||
moduleDict = self.makeModuleAsDict(source, self.pathDir, "clsmod.py")
|
||||
PythonClass = moduleDict["clsmod.PythonClass"]
|
||||
self.assertIn(
|
||||
("clsmod.PythonClass._classMachine", PythonClass.load()._classMachine),
|
||||
list(self.findMachinesViaWrapper(PythonClass)),
|
||||
)
|
||||
|
||||
def test_yieldsMachineInNestedClass(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonAttribute} that refers
|
||||
to a nested class that contains a L{MethodicalMachine} as a
|
||||
class variable, L{findMachinesViaWrapper} yields that machine
|
||||
and its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import MethodicalMachine
|
||||
|
||||
class PythonClass(object):
|
||||
class NestedClass(object):
|
||||
_classMachine = MethodicalMachine()
|
||||
"""
|
||||
moduleDict = self.makeModuleAsDict(source, self.pathDir, "nestedcls.py")
|
||||
|
||||
PythonClass = moduleDict["nestedcls.PythonClass"]
|
||||
self.assertIn(
|
||||
(
|
||||
"nestedcls.PythonClass.NestedClass._classMachine",
|
||||
PythonClass.load().NestedClass._classMachine,
|
||||
),
|
||||
list(self.findMachinesViaWrapper(PythonClass)),
|
||||
)
|
||||
|
||||
def test_yieldsMachineInModule(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonModule} that refers to
|
||||
a module that contains a L{MethodicalMachine},
|
||||
L{findMachinesViaWrapper} yields that machine and its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import MethodicalMachine
|
||||
|
||||
rootMachine = MethodicalMachine()
|
||||
"""
|
||||
module = self.makeModule(source, self.pathDir, "root.py")
|
||||
rootMachine = self.loadModuleAsDict(module)["root.rootMachine"].load()
|
||||
self.assertIn(
|
||||
("root.rootMachine", rootMachine), list(self.findMachinesViaWrapper(module))
|
||||
)
|
||||
|
||||
def test_yieldsMachineInClassInModule(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonModule} that refers to
|
||||
the original module of a class containing a
|
||||
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
|
||||
machine and its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import MethodicalMachine
|
||||
|
||||
class PythonClass(object):
|
||||
_classMachine = MethodicalMachine()
|
||||
"""
|
||||
module = self.makeModule(source, self.pathDir, "clsmod.py")
|
||||
PythonClass = self.loadModuleAsDict(module)["clsmod.PythonClass"].load()
|
||||
self.assertIn(
|
||||
("clsmod.PythonClass._classMachine", PythonClass._classMachine),
|
||||
list(self.findMachinesViaWrapper(module)),
|
||||
)
|
||||
|
||||
def test_yieldsMachineInNestedClassInModule(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonModule} that refers to
|
||||
the original module of a nested class containing a
|
||||
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
|
||||
machine and its FQPN.
|
||||
"""
|
||||
source = """\
|
||||
from automat import MethodicalMachine
|
||||
|
||||
class PythonClass(object):
|
||||
class NestedClass(object):
|
||||
_classMachine = MethodicalMachine()
|
||||
"""
|
||||
module = self.makeModule(source, self.pathDir, "nestedcls.py")
|
||||
PythonClass = self.loadModuleAsDict(module)["nestedcls.PythonClass"].load()
|
||||
|
||||
self.assertIn(
|
||||
(
|
||||
"nestedcls.PythonClass.NestedClass._classMachine",
|
||||
PythonClass.NestedClass._classMachine,
|
||||
),
|
||||
list(self.findMachinesViaWrapper(module)),
|
||||
)
|
||||
|
||||
def test_ignoresImportedClass(self):
|
||||
"""
|
||||
When given a L{twisted.python.modules.PythonAttribute} that refers
|
||||
to a class imported from another module, any
|
||||
L{MethodicalMachine}s on that class are ignored.
|
||||
|
||||
This behavior ensures that a machine is only discovered on a
|
||||
class when visiting the module where that class was defined.
|
||||
"""
|
||||
originalSource = """
|
||||
from automat import MethodicalMachine
|
||||
|
||||
class PythonClass(object):
|
||||
_classMachine = MethodicalMachine()
|
||||
"""
|
||||
|
||||
importingSource = """
|
||||
from original import PythonClass
|
||||
"""
|
||||
|
||||
self.makeModule(originalSource, self.pathDir, "original.py")
|
||||
importingModule = self.makeModule(importingSource, self.pathDir, "importing.py")
|
||||
|
||||
self.assertFalse(list(self.findMachinesViaWrapper(importingModule)))
|
||||
|
||||
def test_descendsIntoPackages(self):
|
||||
"""
|
||||
L{findMachinesViaWrapper} descends into packages to discover
|
||||
machines.
|
||||
"""
|
||||
pythonPath = self.PythonPath([self.pathDir])
|
||||
package = self.FilePath(self.pathDir).child("test_package")
|
||||
package.makedirs()
|
||||
package.child("__init__.py").touch()
|
||||
|
||||
source = """
|
||||
from automat import MethodicalMachine
|
||||
|
||||
|
||||
class PythonClass(object):
|
||||
_classMachine = MethodicalMachine()
|
||||
|
||||
|
||||
rootMachine = MethodicalMachine()
|
||||
"""
|
||||
self.makeModule(source, package.path, "module.py")
|
||||
|
||||
test_package = pythonPath["test_package"]
|
||||
machines = sorted(
|
||||
self.findMachinesViaWrapper(test_package), key=operator.itemgetter(0)
|
||||
)
|
||||
|
||||
moduleDict = self.loadModuleAsDict(test_package["module"])
|
||||
rootMachine = moduleDict["test_package.module.rootMachine"].load()
|
||||
PythonClass = moduleDict["test_package.module.PythonClass"].load()
|
||||
|
||||
expectedMachines = sorted(
|
||||
[
|
||||
("test_package.module.rootMachine", rootMachine),
|
||||
(
|
||||
"test_package.module.PythonClass._classMachine",
|
||||
PythonClass._classMachine,
|
||||
),
|
||||
],
|
||||
key=operator.itemgetter(0),
|
||||
)
|
||||
|
||||
self.assertEqual(expectedMachines, machines)
|
||||
|
||||
def test_infiniteLoop(self):
|
||||
"""
|
||||
L{findMachinesViaWrapper} ignores infinite loops.
|
||||
|
||||
Note this test can't fail - it can only run forever!
|
||||
"""
|
||||
source = """
|
||||
class InfiniteLoop(object):
|
||||
pass
|
||||
|
||||
InfiniteLoop.loop = InfiniteLoop
|
||||
"""
|
||||
module = self.makeModule(source, self.pathDir, "loop.py")
|
||||
self.assertFalse(list(self.findMachinesViaWrapper(module)))
|
||||
|
||||
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class WrapFQPNTests(TestCase):
|
||||
"""
|
||||
Tests that ensure L{wrapFQPN} loads the
|
||||
L{twisted.python.modules.PythonModule} or
|
||||
L{twisted.python.modules.PythonAttribute} for a given FQPN.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
from twisted.python.modules import PythonModule, PythonAttribute
|
||||
from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject
|
||||
|
||||
self.PythonModule = PythonModule
|
||||
self.PythonAttribute = PythonAttribute
|
||||
self.wrapFQPN = wrapFQPN
|
||||
self.InvalidFQPN = InvalidFQPN
|
||||
self.NoModule = NoModule
|
||||
self.NoObject = NoObject
|
||||
|
||||
def assertModuleWrapperRefersTo(self, moduleWrapper, module):
|
||||
"""
|
||||
Assert that a L{twisted.python.modules.PythonModule} refers to a
|
||||
particular Python module.
|
||||
"""
|
||||
self.assertIsInstance(moduleWrapper, self.PythonModule)
|
||||
self.assertEqual(moduleWrapper.name, module.__name__)
|
||||
self.assertIs(moduleWrapper.load(), module)
|
||||
|
||||
def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj):
|
||||
"""
|
||||
Assert that a L{twisted.python.modules.PythonAttribute} refers to a
|
||||
particular Python object.
|
||||
"""
|
||||
self.assertIsInstance(attributeWrapper, self.PythonAttribute)
|
||||
self.assertEqual(attributeWrapper.name, fqpn)
|
||||
self.assertIs(attributeWrapper.load(), obj)
|
||||
|
||||
def test_failsWithEmptyFQPN(self):
|
||||
"""
|
||||
L{wrapFQPN} raises L{InvalidFQPN} when given an empty string.
|
||||
"""
|
||||
with self.assertRaises(self.InvalidFQPN):
|
||||
self.wrapFQPN("")
|
||||
|
||||
def test_failsWithBadDotting(self):
|
||||
""" "
|
||||
L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted
|
||||
FQPN. (e.g., x..y).
|
||||
"""
|
||||
for bad in (".fails", "fails.", "this..fails"):
|
||||
with self.assertRaises(self.InvalidFQPN):
|
||||
self.wrapFQPN(bad)
|
||||
|
||||
def test_singleModule(self):
|
||||
"""
|
||||
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
|
||||
referring to the single module a dotless FQPN describes.
|
||||
"""
|
||||
import os
|
||||
|
||||
moduleWrapper = self.wrapFQPN("os")
|
||||
|
||||
self.assertIsInstance(moduleWrapper, self.PythonModule)
|
||||
self.assertIs(moduleWrapper.load(), os)
|
||||
|
||||
def test_failsWithMissingSingleModuleOrPackage(self):
|
||||
"""
|
||||
L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does
|
||||
not refer to a module or package.
|
||||
"""
|
||||
with self.assertRaises(self.NoModule):
|
||||
self.wrapFQPN("this is not an acceptable name!")
|
||||
|
||||
def test_singlePackage(self):
|
||||
"""
|
||||
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
|
||||
referring to the single package a dotless FQPN describes.
|
||||
"""
|
||||
import xml
|
||||
|
||||
self.assertModuleWrapperRefersTo(self.wrapFQPN("xml"), xml)
|
||||
|
||||
def test_multiplePackages(self):
|
||||
"""
|
||||
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
|
||||
referring to the deepest package described by dotted FQPN.
|
||||
"""
|
||||
import xml.etree
|
||||
|
||||
self.assertModuleWrapperRefersTo(self.wrapFQPN("xml.etree"), xml.etree)
|
||||
|
||||
def test_multiplePackagesFinalModule(self):
|
||||
"""
|
||||
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
|
||||
referring to the deepest module described by dotted FQPN.
|
||||
"""
|
||||
import xml.etree.ElementTree
|
||||
|
||||
self.assertModuleWrapperRefersTo(
|
||||
self.wrapFQPN("xml.etree.ElementTree"), xml.etree.ElementTree
|
||||
)
|
||||
|
||||
def test_singleModuleObject(self):
|
||||
"""
|
||||
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
|
||||
referring to the deepest object an FQPN names, traversing one module.
|
||||
"""
|
||||
import os
|
||||
|
||||
self.assertAttributeWrapperRefersTo(
|
||||
self.wrapFQPN("os.path"), "os.path", os.path
|
||||
)
|
||||
|
||||
def test_multiplePackagesObject(self):
|
||||
"""
|
||||
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
|
||||
referring to the deepest object described by an FQPN,
|
||||
descending through several packages.
|
||||
"""
|
||||
import xml.etree.ElementTree
|
||||
import automat
|
||||
|
||||
for fqpn, obj in [
|
||||
("xml.etree.ElementTree.fromstring", xml.etree.ElementTree.fromstring),
|
||||
("automat.MethodicalMachine.__doc__", automat.MethodicalMachine.__doc__),
|
||||
]:
|
||||
self.assertAttributeWrapperRefersTo(self.wrapFQPN(fqpn), fqpn, obj)
|
||||
|
||||
def test_failsWithMultiplePackagesMissingModuleOrPackage(self):
|
||||
"""
|
||||
L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a
|
||||
missing attribute, module, or package.
|
||||
"""
|
||||
for bad in ("xml.etree.nope!", "xml.etree.nope!.but.the.rest.is.believable"):
|
||||
with self.assertRaises(self.NoObject):
|
||||
self.wrapFQPN(bad)
|
||||
|
||||
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class FindMachinesIntegrationTests(_WritesPythonModules):
|
||||
"""
|
||||
Integration tests to check that L{findMachines} yields all
|
||||
machines discoverable at or below an FQPN.
|
||||
"""
|
||||
|
||||
SOURCE = """
|
||||
from automat import MethodicalMachine
|
||||
|
||||
class PythonClass(object):
|
||||
_machine = MethodicalMachine()
|
||||
ignored = "i am ignored"
|
||||
|
||||
rootLevel = MethodicalMachine()
|
||||
|
||||
ignored = "i am ignored"
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(FindMachinesIntegrationTests, self).setUp()
|
||||
from .._discover import findMachines
|
||||
|
||||
self.findMachines = findMachines
|
||||
|
||||
packageDir = self.FilePath(self.pathDir).child("test_package")
|
||||
packageDir.makedirs()
|
||||
self.pythonPath = self.PythonPath([self.pathDir])
|
||||
self.writeSourceInto(self.SOURCE, packageDir.path, "__init__.py")
|
||||
|
||||
subPackageDir = packageDir.child("subpackage")
|
||||
subPackageDir.makedirs()
|
||||
subPackageDir.child("__init__.py").touch()
|
||||
|
||||
self.makeModule(self.SOURCE, subPackageDir.path, "module.py")
|
||||
|
||||
self.packageDict = self.loadModuleAsDict(self.pythonPath["test_package"])
|
||||
self.moduleDict = self.loadModuleAsDict(
|
||||
self.pythonPath["test_package"]["subpackage"]["module"]
|
||||
)
|
||||
|
||||
def test_discoverAll(self):
|
||||
"""
|
||||
Given a top-level package FQPN, L{findMachines} discovers all
|
||||
L{MethodicalMachine} instances in and below it.
|
||||
"""
|
||||
machines = sorted(self.findMachines("test_package"), key=operator.itemgetter(0))
|
||||
|
||||
tpRootLevel = self.packageDict["test_package.rootLevel"].load()
|
||||
tpPythonClass = self.packageDict["test_package.PythonClass"].load()
|
||||
|
||||
mRLAttr = self.moduleDict["test_package.subpackage.module.rootLevel"]
|
||||
mRootLevel = mRLAttr.load()
|
||||
mPCAttr = self.moduleDict["test_package.subpackage.module.PythonClass"]
|
||||
mPythonClass = mPCAttr.load()
|
||||
|
||||
expectedMachines = sorted(
|
||||
[
|
||||
("test_package.rootLevel", tpRootLevel),
|
||||
("test_package.PythonClass._machine", tpPythonClass._machine),
|
||||
("test_package.subpackage.module.rootLevel", mRootLevel),
|
||||
(
|
||||
"test_package.subpackage.module.PythonClass._machine",
|
||||
mPythonClass._machine,
|
||||
),
|
||||
],
|
||||
key=operator.itemgetter(0),
|
||||
)
|
||||
|
||||
self.assertEqual(expectedMachines, machines)
|
||||
@ -0,0 +1,717 @@
|
||||
"""
|
||||
Tests for the public interface of Automat.
|
||||
"""
|
||||
|
||||
from functools import reduce
|
||||
from unittest import TestCase
|
||||
|
||||
from automat._methodical import ArgSpec, _getArgNames, _getArgSpec, _filterArgs
|
||||
from .. import MethodicalMachine, NoTransition
|
||||
from .. import _methodical
|
||||
|
||||
|
||||
class MethodicalTests(TestCase):
|
||||
"""
|
||||
Tests for L{MethodicalMachine}.
|
||||
"""
|
||||
|
||||
def test_oneTransition(self):
|
||||
"""
|
||||
L{MethodicalMachine} provides a way for you to declare a state machine
|
||||
with inputs, outputs, and states as methods. When you have declared an
|
||||
input, an output, and a state, calling the input method in that state
|
||||
will produce the specified output.
|
||||
"""
|
||||
|
||||
class Machination(object):
|
||||
machine = MethodicalMachine()
|
||||
|
||||
@machine.input()
|
||||
def anInput(self):
|
||||
"an input"
|
||||
|
||||
@machine.output()
|
||||
def anOutput(self):
|
||||
"an output"
|
||||
return "an-output-value"
|
||||
|
||||
@machine.output()
|
||||
def anotherOutput(self):
|
||||
"another output"
|
||||
return "another-output-value"
|
||||
|
||||
@machine.state(initial=True)
|
||||
def anState(self):
|
||||
"a state"
|
||||
|
||||
@machine.state()
|
||||
def anotherState(self):
|
||||
"another state"
|
||||
|
||||
anState.upon(anInput, enter=anotherState, outputs=[anOutput])
|
||||
anotherState.upon(anInput, enter=anotherState, outputs=[anotherOutput])
|
||||
|
||||
m = Machination()
|
||||
self.assertEqual(m.anInput(), ["an-output-value"])
|
||||
self.assertEqual(m.anInput(), ["another-output-value"])
|
||||
|
||||
def test_machineItselfIsPrivate(self):
|
||||
"""
|
||||
L{MethodicalMachine} is an implementation detail. If you attempt to
|
||||
access it on an instance of your class, you will get an exception.
|
||||
However, since tools may need to access it for the purposes of, for
|
||||
example, visualization, you may access it on the class itself.
|
||||
"""
|
||||
expectedMachine = MethodicalMachine()
|
||||
|
||||
class Machination(object):
|
||||
machine = expectedMachine
|
||||
|
||||
machination = Machination()
|
||||
with self.assertRaises(AttributeError) as cm:
|
||||
machination.machine
|
||||
self.assertIn(
|
||||
"MethodicalMachine is an implementation detail", str(cm.exception)
|
||||
)
|
||||
self.assertIs(Machination.machine, expectedMachine)
|
||||
|
||||
def test_outputsArePrivate(self):
|
||||
"""
|
||||
One of the benefits of using a state machine is that your output method
|
||||
implementations don't need to take invalid state transitions into
|
||||
account - the methods simply won't be called. This property would be
|
||||
broken if client code called output methods directly, so output methods
|
||||
are not directly visible under their names.
|
||||
"""
|
||||
|
||||
class Machination(object):
|
||||
machine = MethodicalMachine()
|
||||
counter = 0
|
||||
|
||||
@machine.input()
|
||||
def anInput(self):
|
||||
"an input"
|
||||
|
||||
@machine.output()
|
||||
def anOutput(self):
|
||||
self.counter += 1
|
||||
|
||||
@machine.state(initial=True)
|
||||
def state(self):
|
||||
"a machine state"
|
||||
|
||||
state.upon(anInput, enter=state, outputs=[anOutput])
|
||||
|
||||
mach1 = Machination()
|
||||
mach1.anInput()
|
||||
self.assertEqual(mach1.counter, 1)
|
||||
mach2 = Machination()
|
||||
with self.assertRaises(AttributeError) as cm:
|
||||
mach2.anOutput
|
||||
self.assertEqual(mach2.counter, 0)
|
||||
|
||||
self.assertIn(
|
||||
"Machination.anOutput is a state-machine output method; to "
|
||||
"produce this output, call an input method instead.",
|
||||
str(cm.exception),
|
||||
)
|
||||
|
||||
def test_multipleMachines(self):
|
||||
"""
|
||||
Two machines may co-exist happily on the same instance; they don't
|
||||
interfere with each other.
|
||||
"""
|
||||
|
||||
class MultiMach(object):
|
||||
a = MethodicalMachine()
|
||||
b = MethodicalMachine()
|
||||
|
||||
@a.input()
|
||||
def inputA(self):
|
||||
"input A"
|
||||
|
||||
@b.input()
|
||||
def inputB(self):
|
||||
"input B"
|
||||
|
||||
@a.state(initial=True)
|
||||
def initialA(self):
|
||||
"initial A"
|
||||
|
||||
@b.state(initial=True)
|
||||
def initialB(self):
|
||||
"initial B"
|
||||
|
||||
@a.output()
|
||||
def outputA(self):
|
||||
return "A"
|
||||
|
||||
@b.output()
|
||||
def outputB(self):
|
||||
return "B"
|
||||
|
||||
initialA.upon(inputA, initialA, [outputA])
|
||||
initialB.upon(inputB, initialB, [outputB])
|
||||
|
||||
mm = MultiMach()
|
||||
self.assertEqual(mm.inputA(), ["A"])
|
||||
self.assertEqual(mm.inputB(), ["B"])
|
||||
|
||||
def test_collectOutputs(self):
|
||||
"""
|
||||
Outputs can be combined with the "collector" argument to "upon".
|
||||
"""
|
||||
import operator
|
||||
|
||||
class Machine(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
|
||||
@m.output()
|
||||
def outputA(self):
|
||||
return "A"
|
||||
|
||||
@m.output()
|
||||
def outputB(self):
|
||||
return "B"
|
||||
|
||||
@m.state(initial=True)
|
||||
def state(self):
|
||||
"a state"
|
||||
|
||||
state.upon(
|
||||
input,
|
||||
state,
|
||||
[outputA, outputB],
|
||||
collector=lambda x: reduce(operator.add, x),
|
||||
)
|
||||
|
||||
m = Machine()
|
||||
self.assertEqual(m.input(), "AB")
|
||||
|
||||
def test_methodName(self):
|
||||
"""
|
||||
Input methods preserve their declared names.
|
||||
"""
|
||||
|
||||
class Mech(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def declaredInputName(self):
|
||||
"an input"
|
||||
|
||||
@m.state(initial=True)
|
||||
def aState(self):
|
||||
"state"
|
||||
|
||||
m = Mech()
|
||||
with self.assertRaises(TypeError) as cm:
|
||||
m.declaredInputName("too", "many", "arguments")
|
||||
self.assertIn("declaredInputName", str(cm.exception))
|
||||
|
||||
def test_inputWithArguments(self):
|
||||
"""
|
||||
If an input takes an argument, it will pass that along to its output.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self, x, y=1):
|
||||
"an input"
|
||||
|
||||
@m.state(initial=True)
|
||||
def state(self):
|
||||
"a state"
|
||||
|
||||
@m.output()
|
||||
def output(self, x, y=1):
|
||||
self._x = x
|
||||
return x + y
|
||||
|
||||
state.upon(input, state, [output])
|
||||
|
||||
m = Mechanism()
|
||||
self.assertEqual(m.input(3), [4])
|
||||
self.assertEqual(m._x, 3)
|
||||
|
||||
def test_outputWithSubsetOfArguments(self):
|
||||
"""
|
||||
Inputs pass arguments that output will accept.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self, x, y=1):
|
||||
"an input"
|
||||
|
||||
@m.state(initial=True)
|
||||
def state(self):
|
||||
"a state"
|
||||
|
||||
@m.output()
|
||||
def outputX(self, x):
|
||||
self._x = x
|
||||
return x
|
||||
|
||||
@m.output()
|
||||
def outputY(self, y):
|
||||
self._y = y
|
||||
return y
|
||||
|
||||
@m.output()
|
||||
def outputNoArgs(self):
|
||||
return None
|
||||
|
||||
state.upon(input, state, [outputX, outputY, outputNoArgs])
|
||||
|
||||
m = Mechanism()
|
||||
|
||||
# Pass x as positional argument.
|
||||
self.assertEqual(m.input(3), [3, 1, None])
|
||||
self.assertEqual(m._x, 3)
|
||||
self.assertEqual(m._y, 1)
|
||||
|
||||
# Pass x as key word argument.
|
||||
self.assertEqual(m.input(x=4), [4, 1, None])
|
||||
self.assertEqual(m._x, 4)
|
||||
self.assertEqual(m._y, 1)
|
||||
|
||||
# Pass y as positional argument.
|
||||
self.assertEqual(m.input(6, 3), [6, 3, None])
|
||||
self.assertEqual(m._x, 6)
|
||||
self.assertEqual(m._y, 3)
|
||||
|
||||
# Pass y as key word argument.
|
||||
self.assertEqual(m.input(5, y=2), [5, 2, None])
|
||||
self.assertEqual(m._x, 5)
|
||||
self.assertEqual(m._y, 2)
|
||||
|
||||
def test_inputFunctionsMustBeEmpty(self):
|
||||
"""
|
||||
The wrapped input function must have an empty body.
|
||||
"""
|
||||
# input functions are executed to assert that the signature matches,
|
||||
# but their body must be empty
|
||||
|
||||
_methodical._empty() # chase coverage
|
||||
_methodical._docstring()
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
list() # pragma: no cover
|
||||
|
||||
self.assertEqual(str(cm.exception), "function body must be empty")
|
||||
|
||||
# all three of these cases should be valid. Functions/methods with
|
||||
# docstrings produce slightly different bytecode than ones without.
|
||||
|
||||
class MechanismWithDocstring(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
|
||||
MechanismWithDocstring().input()
|
||||
|
||||
class MechanismWithPass(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
pass
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
|
||||
MechanismWithPass().input()
|
||||
|
||||
class MechanismWithDocstringAndPass(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
pass
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
|
||||
MechanismWithDocstringAndPass().input()
|
||||
|
||||
class MechanismReturnsNone(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
return None
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
|
||||
MechanismReturnsNone().input()
|
||||
|
||||
class MechanismWithDocstringAndReturnsNone(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
return None
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
|
||||
MechanismWithDocstringAndReturnsNone().input()
|
||||
|
||||
def test_inputOutputMismatch(self):
|
||||
"""
|
||||
All the argument lists of the outputs for a given input must match; if
|
||||
one does not the call to C{upon} will raise a C{TypeError}.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def nameOfInput(self, a):
|
||||
"an input"
|
||||
|
||||
@m.output()
|
||||
def outputThatMatches(self, a):
|
||||
"an output that matches"
|
||||
|
||||
@m.output()
|
||||
def outputThatDoesntMatch(self, b):
|
||||
"an output that doesn't match"
|
||||
|
||||
@m.state()
|
||||
def state(self):
|
||||
"a state"
|
||||
|
||||
with self.assertRaises(TypeError) as cm:
|
||||
state.upon(
|
||||
nameOfInput, state, [outputThatMatches, outputThatDoesntMatch]
|
||||
)
|
||||
self.assertIn("nameOfInput", str(cm.exception))
|
||||
self.assertIn("outputThatDoesntMatch", str(cm.exception))
|
||||
|
||||
def test_stateLoop(self):
|
||||
"""
|
||||
It is possible to write a self-loop by omitting "enter"
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
|
||||
@m.input()
|
||||
def say_hi(self):
|
||||
"an input"
|
||||
|
||||
@m.output()
|
||||
def _start_say_hi(self):
|
||||
return "hi"
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"a state"
|
||||
|
||||
def said_hi(self):
|
||||
"a state with no inputs"
|
||||
|
||||
start.upon(input, outputs=[])
|
||||
start.upon(say_hi, outputs=[_start_say_hi])
|
||||
|
||||
a_mechanism = Mechanism()
|
||||
[a_greeting] = a_mechanism.say_hi()
|
||||
self.assertEqual(a_greeting, "hi")
|
||||
|
||||
def test_defaultOutputs(self):
|
||||
"""
|
||||
It is possible to write a transition with no outputs
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.input()
|
||||
def finish(self):
|
||||
"final transition"
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"a start state"
|
||||
|
||||
@m.state()
|
||||
def finished(self):
|
||||
"a final state"
|
||||
|
||||
start.upon(finish, enter=finished)
|
||||
|
||||
Mechanism().finish()
|
||||
|
||||
def test_getArgNames(self):
|
||||
"""
|
||||
Type annotations should be included in the set of
|
||||
"""
|
||||
spec = ArgSpec(
|
||||
args=("a", "b"),
|
||||
varargs=None,
|
||||
varkw=None,
|
||||
defaults=None,
|
||||
kwonlyargs=(),
|
||||
kwonlydefaults=None,
|
||||
annotations=(("a", int), ("b", str)),
|
||||
)
|
||||
self.assertEqual(
|
||||
_getArgNames(spec),
|
||||
{"a", "b", ("a", int), ("b", str)},
|
||||
)
|
||||
|
||||
def test_filterArgs(self):
|
||||
"""
|
||||
filterArgs() should not filter the `args` parameter
|
||||
if outputSpec accepts `*args`.
|
||||
"""
|
||||
inputSpec = _getArgSpec(lambda *args, **kwargs: None)
|
||||
outputSpec = _getArgSpec(lambda *args, **kwargs: None)
|
||||
argsIn = ()
|
||||
argsOut, _ = _filterArgs(argsIn, {}, inputSpec, outputSpec)
|
||||
self.assertIs(argsIn, argsOut)
|
||||
|
||||
def test_multipleInitialStatesFailure(self):
|
||||
"""
|
||||
A L{MethodicalMachine} can only have one initial state.
|
||||
"""
|
||||
|
||||
class WillFail(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.state(initial=True)
|
||||
def firstInitialState(self):
|
||||
"The first initial state -- this is OK."
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
|
||||
@m.state(initial=True)
|
||||
def secondInitialState(self):
|
||||
"The second initial state -- results in a ValueError."
|
||||
|
||||
def test_multipleTransitionsFailure(self):
|
||||
"""
|
||||
A L{MethodicalMachine} can only have one transition per start/event
|
||||
pair.
|
||||
"""
|
||||
|
||||
class WillFail(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"We start here."
|
||||
|
||||
@m.state()
|
||||
def end(self):
|
||||
"Rainbows end."
|
||||
|
||||
@m.input()
|
||||
def event(self):
|
||||
"An event."
|
||||
|
||||
start.upon(event, enter=end, outputs=[])
|
||||
with self.assertRaises(ValueError):
|
||||
start.upon(event, enter=end, outputs=[])
|
||||
|
||||
def test_badTransitionForCurrentState(self):
|
||||
"""
|
||||
Calling any input method that lacks a transition for the machine's
|
||||
current state raises an informative L{NoTransition}.
|
||||
"""
|
||||
|
||||
class OnlyOnePath(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"Start state."
|
||||
|
||||
@m.state()
|
||||
def end(self):
|
||||
"End state."
|
||||
|
||||
@m.input()
|
||||
def advance(self):
|
||||
"Move from start to end."
|
||||
|
||||
@m.input()
|
||||
def deadEnd(self):
|
||||
"A transition from nowhere to nowhere."
|
||||
|
||||
start.upon(advance, end, [])
|
||||
|
||||
machine = OnlyOnePath()
|
||||
with self.assertRaises(NoTransition) as cm:
|
||||
machine.deadEnd()
|
||||
self.assertIn("deadEnd", str(cm.exception))
|
||||
self.assertIn("start", str(cm.exception))
|
||||
machine.advance()
|
||||
with self.assertRaises(NoTransition) as cm:
|
||||
machine.deadEnd()
|
||||
self.assertIn("deadEnd", str(cm.exception))
|
||||
self.assertIn("end", str(cm.exception))
|
||||
|
||||
def test_saveState(self):
|
||||
"""
|
||||
L{MethodicalMachine.serializer} is a decorator that modifies its
|
||||
decoratee's signature to take a "state" object as its first argument,
|
||||
which is the "serialized" argument to the L{MethodicalMachine.state}
|
||||
decorator.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
def __init__(self):
|
||||
self.value = 1
|
||||
|
||||
@m.state(serialized="first-state", initial=True)
|
||||
def first(self):
|
||||
"First state."
|
||||
|
||||
@m.state(serialized="second-state")
|
||||
def second(self):
|
||||
"Second state."
|
||||
|
||||
@m.serializer()
|
||||
def save(self, state):
|
||||
return {
|
||||
"machine-state": state,
|
||||
"some-value": self.value,
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
Mechanism().save(),
|
||||
{
|
||||
"machine-state": "first-state",
|
||||
"some-value": 1,
|
||||
},
|
||||
)
|
||||
|
||||
def test_restoreState(self):
|
||||
"""
|
||||
L{MethodicalMachine.unserializer} decorates a function that becomes a
|
||||
machine-state unserializer; its return value is mapped to the
|
||||
C{serialized} parameter to C{state}, and the L{MethodicalMachine}
|
||||
associated with that instance's state is updated to that state.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
def __init__(self):
|
||||
self.value = 1
|
||||
self.ranOutput = False
|
||||
|
||||
@m.state(serialized="first-state", initial=True)
|
||||
def first(self):
|
||||
"First state."
|
||||
|
||||
@m.state(serialized="second-state")
|
||||
def second(self):
|
||||
"Second state."
|
||||
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
|
||||
@m.output()
|
||||
def output(self):
|
||||
self.value = 2
|
||||
self.ranOutput = True
|
||||
return 1
|
||||
|
||||
@m.output()
|
||||
def output2(self):
|
||||
return 2
|
||||
|
||||
first.upon(input, second, [output], collector=lambda x: list(x)[0])
|
||||
second.upon(input, second, [output2], collector=lambda x: list(x)[0])
|
||||
|
||||
@m.serializer()
|
||||
def save(self, state):
|
||||
return {
|
||||
"machine-state": state,
|
||||
"some-value": self.value,
|
||||
}
|
||||
|
||||
@m.unserializer()
|
||||
def _restore(self, blob):
|
||||
self.value = blob["some-value"]
|
||||
return blob["machine-state"]
|
||||
|
||||
@classmethod
|
||||
def fromBlob(cls, blob):
|
||||
self = cls()
|
||||
self._restore(blob)
|
||||
return self
|
||||
|
||||
m1 = Mechanism()
|
||||
m1.input()
|
||||
blob = m1.save()
|
||||
m2 = Mechanism.fromBlob(blob)
|
||||
self.assertEqual(m2.ranOutput, False)
|
||||
self.assertEqual(m2.input(), 2)
|
||||
self.assertEqual(
|
||||
m2.save(),
|
||||
{
|
||||
"machine-state": "second-state",
|
||||
"some-value": 2,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# FIXME: error for wrong types on any call to _oneTransition
|
||||
# FIXME: better public API for .upon; maybe a context manager?
|
||||
# FIXME: when transitions are defined, validate that we can always get to
|
||||
# terminal? do we care about this?
|
||||
# FIXME: implementation (and use-case/example) for passing args from in to out
|
||||
|
||||
# FIXME: possibly these need some kind of support from core
|
||||
# FIXME: wildcard state (in all states, when input X, emit Y and go to Z)
|
||||
# FIXME: wildcard input (in state X, when any input, emit Y and go to Z)
|
||||
# FIXME: combined wildcards (in any state for any input, emit Y go to Z)
|
||||
@ -0,0 +1,142 @@
|
||||
from unittest import TestCase
|
||||
from .._methodical import MethodicalMachine
|
||||
|
||||
|
||||
class SampleObject(object):
|
||||
mm = MethodicalMachine()
|
||||
|
||||
@mm.state(initial=True)
|
||||
def begin(self):
|
||||
"initial state"
|
||||
|
||||
@mm.state()
|
||||
def middle(self):
|
||||
"middle state"
|
||||
|
||||
@mm.state()
|
||||
def end(self):
|
||||
"end state"
|
||||
|
||||
@mm.input()
|
||||
def go1(self):
|
||||
"sample input"
|
||||
|
||||
@mm.input()
|
||||
def go2(self):
|
||||
"sample input"
|
||||
|
||||
@mm.input()
|
||||
def back(self):
|
||||
"sample input"
|
||||
|
||||
@mm.output()
|
||||
def out(self):
|
||||
"sample output"
|
||||
|
||||
setTrace = mm._setTrace
|
||||
|
||||
begin.upon(go1, middle, [out])
|
||||
middle.upon(go2, end, [out])
|
||||
end.upon(back, middle, [])
|
||||
middle.upon(back, begin, [])
|
||||
|
||||
|
||||
class TraceTests(TestCase):
|
||||
def test_only_inputs(self):
|
||||
traces = []
|
||||
|
||||
def tracer(old_state, input, new_state):
|
||||
traces.append((old_state, input, new_state))
|
||||
return None # "I only care about inputs, not outputs"
|
||||
|
||||
s = SampleObject()
|
||||
s.setTrace(tracer)
|
||||
|
||||
s.go1()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle"),
|
||||
],
|
||||
)
|
||||
|
||||
s.go2()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle"),
|
||||
("middle", "go2", "end"),
|
||||
],
|
||||
)
|
||||
s.setTrace(None)
|
||||
s.back()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle"),
|
||||
("middle", "go2", "end"),
|
||||
],
|
||||
)
|
||||
s.go2()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle"),
|
||||
("middle", "go2", "end"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_inputs_and_outputs(self):
|
||||
traces = []
|
||||
|
||||
def tracer(old_state, input, new_state):
|
||||
traces.append((old_state, input, new_state, None))
|
||||
|
||||
def trace_outputs(output):
|
||||
traces.append((old_state, input, new_state, output))
|
||||
|
||||
return trace_outputs # "I care about outputs too"
|
||||
|
||||
s = SampleObject()
|
||||
s.setTrace(tracer)
|
||||
|
||||
s.go1()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
],
|
||||
)
|
||||
|
||||
s.go2()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
("middle", "go2", "end", None),
|
||||
("middle", "go2", "end", "out"),
|
||||
],
|
||||
)
|
||||
s.setTrace(None)
|
||||
s.back()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
("middle", "go2", "end", None),
|
||||
("middle", "go2", "end", "out"),
|
||||
],
|
||||
)
|
||||
s.go2()
|
||||
self.assertEqual(
|
||||
traces,
|
||||
[
|
||||
("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
("middle", "go2", "end", None),
|
||||
("middle", "go2", "end", "out"),
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,534 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Generic, List, Protocol, TypeVar
|
||||
from unittest import TestCase, skipIf
|
||||
|
||||
from .. import AlreadyBuiltError, NoTransition, TypeMachineBuilder, pep614
|
||||
|
||||
try:
|
||||
from zope.interface import Interface, implementer # type:ignore[import-untyped]
|
||||
except ImportError:
|
||||
hasInterface = False
|
||||
else:
|
||||
hasInterface = True
|
||||
|
||||
class ISomething(Interface):
|
||||
def something() -> int: ... # type:ignore[misc,empty-body]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ProtocolForTesting(Protocol):
|
||||
|
||||
def change(self) -> None:
|
||||
"Switch to the other state."
|
||||
|
||||
def value(self) -> int:
|
||||
"Give a value specific to the given state."
|
||||
|
||||
|
||||
class ArgTaker(Protocol):
|
||||
def takeSomeArgs(self, arg1: int = 0, arg2: str = "") -> None: ...
|
||||
def value(self) -> int: ...
|
||||
|
||||
|
||||
class NoOpCore:
|
||||
"Just an object, you know?"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen(Generic[T]):
|
||||
t: T
|
||||
|
||||
|
||||
def buildTestBuilder() -> tuple[
|
||||
TypeMachineBuilder[ProtocolForTesting, NoOpCore],
|
||||
Callable[[NoOpCore], ProtocolForTesting],
|
||||
]:
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
first = builder.state("first")
|
||||
second = builder.state("second")
|
||||
|
||||
first.upon(ProtocolForTesting.change).to(second).returns(None)
|
||||
second.upon(ProtocolForTesting.change).to(first).returns(None)
|
||||
|
||||
@pep614(first.upon(ProtocolForTesting.value).loop())
|
||||
def firstValue(machine: ProtocolForTesting, core: NoOpCore) -> int:
|
||||
return 3
|
||||
|
||||
@pep614(second.upon(ProtocolForTesting.value).loop())
|
||||
def secondValue(machine: ProtocolForTesting, core: NoOpCore) -> int:
|
||||
return 4
|
||||
|
||||
return builder, builder.build()
|
||||
|
||||
|
||||
builder, machineFactory = buildTestBuilder()
|
||||
|
||||
|
||||
def needsSomething(proto: ProtocolForTesting, core: NoOpCore, value: str) -> int:
|
||||
"we need data to build this state"
|
||||
return 3 # pragma: no cover
|
||||
|
||||
|
||||
def needsNothing(proto: ArgTaker, core: NoOpCore) -> str:
|
||||
return "state-specific data" # pragma: no cover
|
||||
|
||||
|
||||
class SimpleProtocol(Protocol):
|
||||
def method(self) -> None:
|
||||
"A method"
|
||||
|
||||
|
||||
class Counter(Protocol):
|
||||
def start(self) -> None:
|
||||
"enter the counting state"
|
||||
|
||||
def increment(self) -> None:
|
||||
"increment the counter"
|
||||
|
||||
def stop(self) -> int:
|
||||
"stop"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Count:
|
||||
value: int = 0
|
||||
|
||||
|
||||
class TypeMachineTests(TestCase):
|
||||
|
||||
def test_oneTransition(self) -> None:
|
||||
|
||||
machine = machineFactory(NoOpCore())
|
||||
|
||||
self.assertEqual(machine.value(), 3)
|
||||
machine.change()
|
||||
self.assertEqual(machine.value(), 4)
|
||||
self.assertEqual(machine.value(), 4)
|
||||
machine.change()
|
||||
self.assertEqual(machine.value(), 3)
|
||||
|
||||
def test_stateSpecificData(self) -> None:
|
||||
|
||||
builder = TypeMachineBuilder(Counter, NoOpCore)
|
||||
initial = builder.state("initial")
|
||||
counting = builder.state("counting", lambda machine, core: Count())
|
||||
initial.upon(Counter.start).to(counting).returns(None)
|
||||
|
||||
@pep614(counting.upon(Counter.increment).loop())
|
||||
def incf(counter: Counter, core: NoOpCore, count: Count) -> None:
|
||||
count.value += 1
|
||||
|
||||
@pep614(counting.upon(Counter.stop).to(initial))
|
||||
def finish(counter: Counter, core: NoOpCore, count: Count) -> int:
|
||||
return count.value
|
||||
|
||||
machineFactory = builder.build()
|
||||
machine = machineFactory(NoOpCore())
|
||||
machine.start()
|
||||
machine.increment()
|
||||
machine.increment()
|
||||
self.assertEqual(machine.stop(), 2)
|
||||
machine.start()
|
||||
machine.increment()
|
||||
self.assertEqual(machine.stop(), 1)
|
||||
|
||||
def test_stateSpecificDataWithoutData(self) -> None:
|
||||
"""
|
||||
To facilitate common implementations of transition behavior methods,
|
||||
sometimes you want to implement a transition within a data state
|
||||
without taking a data parameter. To do this, pass the 'nodata=True'
|
||||
parameter to 'upon'.
|
||||
"""
|
||||
builder = TypeMachineBuilder(Counter, NoOpCore)
|
||||
initial = builder.state("initial")
|
||||
counting = builder.state("counting", lambda machine, core: Count())
|
||||
startCalls = []
|
||||
|
||||
@pep614(initial.upon(Counter.start).to(counting))
|
||||
@pep614(counting.upon(Counter.start, nodata=True).loop())
|
||||
def start(counter: Counter, core: NoOpCore) -> None:
|
||||
startCalls.append("started!")
|
||||
|
||||
@pep614(counting.upon(Counter.increment).loop())
|
||||
def incf(counter: Counter, core: NoOpCore, count: Count) -> None:
|
||||
count.value += 1
|
||||
|
||||
@pep614(counting.upon(Counter.stop).to(initial))
|
||||
def finish(counter: Counter, core: NoOpCore, count: Count) -> int:
|
||||
return count.value
|
||||
|
||||
machineFactory = builder.build()
|
||||
machine = machineFactory(NoOpCore())
|
||||
machine.start()
|
||||
self.assertEqual(len(startCalls), 1)
|
||||
machine.start()
|
||||
self.assertEqual(len(startCalls), 2)
|
||||
machine.increment()
|
||||
self.assertEqual(machine.stop(), 1)
|
||||
|
||||
def test_incompleteTransitionDefinition(self) -> None:
|
||||
builder = TypeMachineBuilder(SimpleProtocol, NoOpCore)
|
||||
sample = builder.state("sample")
|
||||
sample.upon(SimpleProtocol.method).loop() # oops, no '.returns(None)'
|
||||
with self.assertRaises(ValueError) as raised:
|
||||
builder.build()
|
||||
self.assertIn(
|
||||
"incomplete transition from sample to sample upon SimpleProtocol.method",
|
||||
str(raised.exception),
|
||||
)
|
||||
|
||||
def test_dataToData(self) -> None:
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
|
||||
@dataclass
|
||||
class Data1:
|
||||
value: int
|
||||
|
||||
@dataclass
|
||||
class Data2:
|
||||
stuff: List[str]
|
||||
|
||||
initial = builder.state("initial")
|
||||
counting = builder.state("counting", lambda proto, core: Data1(1))
|
||||
appending = builder.state("appending", lambda proto, core: Data2([]))
|
||||
|
||||
initial.upon(ProtocolForTesting.change).to(counting).returns(None)
|
||||
|
||||
@pep614(counting.upon(ProtocolForTesting.value).loop())
|
||||
def countup(p: ProtocolForTesting, c: NoOpCore, d: Data1) -> int:
|
||||
d.value *= 2
|
||||
return d.value
|
||||
|
||||
counting.upon(ProtocolForTesting.change).to(appending).returns(None)
|
||||
|
||||
@pep614(appending.upon(ProtocolForTesting.value).loop())
|
||||
def appendup(p: ProtocolForTesting, c: NoOpCore, d: Data2) -> int:
|
||||
d.stuff.extend("abc")
|
||||
return len(d.stuff)
|
||||
|
||||
machineFactory = builder.build()
|
||||
machine = machineFactory(NoOpCore())
|
||||
machine.change()
|
||||
self.assertEqual(machine.value(), 2)
|
||||
self.assertEqual(machine.value(), 4)
|
||||
machine.change()
|
||||
self.assertEqual(machine.value(), 3)
|
||||
self.assertEqual(machine.value(), 6)
|
||||
|
||||
def test_dataFactoryArgs(self) -> None:
|
||||
"""
|
||||
Any data factory that takes arguments will constrain the allowed
|
||||
signature of all protocol methods that transition into that state.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
initial = builder.state("initial")
|
||||
data = builder.state("data", needsSomething)
|
||||
data2 = builder.state("data2", needsSomething)
|
||||
# toState = initial.to(data)
|
||||
|
||||
# 'assertions' in the form of expected type errors:
|
||||
# (no data -> data)
|
||||
uponNoData = initial.upon(ProtocolForTesting.change)
|
||||
uponNoData.to(data) # type:ignore[arg-type]
|
||||
|
||||
# (data -> data)
|
||||
uponData = data.upon(ProtocolForTesting.change)
|
||||
uponData.to(data2) # type:ignore[arg-type]
|
||||
|
||||
def test_dataFactoryNoArgs(self) -> None:
|
||||
"""
|
||||
Inverse of C{test_dataFactoryArgs} where the data factory specifically
|
||||
does I{not} take arguments, but the input specified does.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ArgTaker, NoOpCore)
|
||||
initial = builder.state("initial")
|
||||
data = builder.state("data", needsNothing)
|
||||
(
|
||||
initial.upon(ArgTaker.takeSomeArgs)
|
||||
.to(data) # type:ignore[arg-type]
|
||||
.returns(None)
|
||||
)
|
||||
|
||||
def test_invalidTransition(self) -> None:
|
||||
"""
|
||||
Invalid transitions raise a NoTransition exception.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
builder.state("initial")
|
||||
factory = builder.build()
|
||||
machine = factory(NoOpCore())
|
||||
with self.assertRaises(NoTransition):
|
||||
machine.change()
|
||||
|
||||
def test_reentrancy(self) -> None:
|
||||
"""
|
||||
During the execution of a transition behavior implementation function,
|
||||
you may invoke other methods on your state machine. However, the
|
||||
execution of the behavior of those methods will be deferred until the
|
||||
current behavior method is done executing. In order to implement that
|
||||
deferral, we restrict the set of methods that can be invoked to those
|
||||
that return None.
|
||||
|
||||
@note: it may be possible to implement deferral via Awaitables or
|
||||
Deferreds later, but we are starting simple.
|
||||
"""
|
||||
|
||||
class SomeMethods(Protocol):
|
||||
def start(self) -> None:
|
||||
"Start the machine."
|
||||
|
||||
def later(self) -> None:
|
||||
"Do some deferrable work."
|
||||
|
||||
builder = TypeMachineBuilder(SomeMethods, NoOpCore)
|
||||
|
||||
initial = builder.state("initial")
|
||||
second = builder.state("second")
|
||||
|
||||
order = []
|
||||
|
||||
@pep614(initial.upon(SomeMethods.start).to(second))
|
||||
def startup(methods: SomeMethods, core: NoOpCore) -> None:
|
||||
order.append("startup")
|
||||
methods.later()
|
||||
order.append("startup done")
|
||||
|
||||
@pep614(second.upon(SomeMethods.later).loop())
|
||||
def later(methods: SomeMethods, core: NoOpCore) -> None:
|
||||
order.append("later")
|
||||
|
||||
machineFactory = builder.build()
|
||||
machine = machineFactory(NoOpCore())
|
||||
machine.start()
|
||||
self.assertEqual(order, ["startup", "startup done", "later"])
|
||||
|
||||
def test_reentrancyNotNoneError(self) -> None:
|
||||
class SomeMethods(Protocol):
|
||||
def start(self) -> None:
|
||||
"Start the machine."
|
||||
|
||||
def later(self) -> int:
|
||||
"Do some deferrable work."
|
||||
|
||||
builder = TypeMachineBuilder(SomeMethods, NoOpCore)
|
||||
|
||||
initial = builder.state("initial")
|
||||
second = builder.state("second")
|
||||
|
||||
order = []
|
||||
|
||||
@pep614(initial.upon(SomeMethods.start).to(second))
|
||||
def startup(methods: SomeMethods, core: NoOpCore) -> None:
|
||||
order.append("startup")
|
||||
methods.later()
|
||||
order.append("startup done") # pragma: no cover
|
||||
|
||||
@pep614(second.upon(SomeMethods.later).loop())
|
||||
def later(methods: SomeMethods, core: NoOpCore) -> int:
|
||||
order.append("later")
|
||||
return 3
|
||||
|
||||
machineFactory = builder.build()
|
||||
machine = machineFactory(NoOpCore())
|
||||
with self.assertRaises(RuntimeError):
|
||||
machine.start()
|
||||
self.assertEqual(order, ["startup"])
|
||||
# We do actually do the state transition, which happens *before* the
|
||||
# output is generated; TODO: maybe we should have exception handling
|
||||
# that transitions into an error state that requires explicit recovery?
|
||||
self.assertEqual(machine.later(), 3)
|
||||
self.assertEqual(order, ["startup", "later"])
|
||||
|
||||
def test_buildLock(self) -> None:
|
||||
"""
|
||||
``.build()`` locks the builder so it can no longer be modified.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
state = builder.state("test-state")
|
||||
state2 = builder.state("state2")
|
||||
state3 = builder.state("state3")
|
||||
upon = state.upon(ProtocolForTesting.change)
|
||||
to = upon.to(state2)
|
||||
to2 = upon.to(state3)
|
||||
to.returns(None)
|
||||
with self.assertRaises(ValueError) as ve:
|
||||
to2.returns(None)
|
||||
with self.assertRaises(AlreadyBuiltError):
|
||||
to.returns(None)
|
||||
builder.build()
|
||||
with self.assertRaises(AlreadyBuiltError):
|
||||
builder.state("hello")
|
||||
with self.assertRaises(AlreadyBuiltError):
|
||||
builder.build()
|
||||
|
||||
def test_methodMembership(self) -> None:
|
||||
"""
|
||||
Input methods must be members of their protocol.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
state = builder.state("test-state")
|
||||
|
||||
def stateful(proto: ProtocolForTesting, core: NoOpCore) -> int:
|
||||
return 4 # pragma: no cover
|
||||
|
||||
state2 = builder.state("state2", stateful)
|
||||
|
||||
def change(self: ProtocolForTesting) -> None: ...
|
||||
|
||||
def rogue(self: ProtocolForTesting) -> int:
|
||||
return 3 # pragma: no cover
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
state.upon(change)
|
||||
with self.assertRaises(ValueError) as ve:
|
||||
state2.upon(change)
|
||||
with self.assertRaises(ValueError):
|
||||
state.upon(rogue)
|
||||
|
||||
def test_startInAlternateState(self) -> None:
|
||||
"""
|
||||
The state machine can be started in an alternate state.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
one = builder.state("one")
|
||||
two = builder.state("two")
|
||||
|
||||
@dataclass
|
||||
class Three:
|
||||
proto: ProtocolForTesting
|
||||
core: NoOpCore
|
||||
value: int = 0
|
||||
|
||||
three = builder.state("three", Three)
|
||||
one.upon(ProtocolForTesting.change).to(two).returns(None)
|
||||
one.upon(ProtocolForTesting.value).loop().returns(1)
|
||||
two.upon(ProtocolForTesting.change).to(three).returns(None)
|
||||
two.upon(ProtocolForTesting.value).loop().returns(2)
|
||||
|
||||
@pep614(three.upon(ProtocolForTesting.value).loop())
|
||||
def threevalue(proto: ProtocolForTesting, core: NoOpCore, three: Three) -> int:
|
||||
return 3 + three.value
|
||||
|
||||
onetwothree = builder.build()
|
||||
|
||||
# confirm positive behavior first, particularly the value of the three
|
||||
# state's change
|
||||
normal = onetwothree(NoOpCore())
|
||||
self.assertEqual(normal.value(), 1)
|
||||
normal.change()
|
||||
self.assertEqual(normal.value(), 2)
|
||||
normal.change()
|
||||
self.assertEqual(normal.value(), 3)
|
||||
|
||||
# now try deserializing it in each state
|
||||
self.assertEqual(onetwothree(NoOpCore()).value(), 1)
|
||||
self.assertEqual(onetwothree(NoOpCore(), two).value(), 2)
|
||||
self.assertEqual(
|
||||
onetwothree(
|
||||
NoOpCore(), three, lambda proto, core: Three(proto, core, 4)
|
||||
).value(),
|
||||
7,
|
||||
)
|
||||
|
||||
def test_genericData(self) -> None:
|
||||
"""
|
||||
Test to cover get_origin in generic assertion.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ArgTaker, NoOpCore)
|
||||
one = builder.state("one")
|
||||
|
||||
def dat(
|
||||
proto: ArgTaker, core: NoOpCore, arg1: int = 0, arg2: str = ""
|
||||
) -> Gen[int]:
|
||||
return Gen(arg1)
|
||||
|
||||
two = builder.state("two", dat)
|
||||
one.upon(ArgTaker.takeSomeArgs).to(two).returns(None)
|
||||
|
||||
@pep614(two.upon(ArgTaker.value).loop())
|
||||
def val(proto: ArgTaker, core: NoOpCore, data: Gen[int]) -> int:
|
||||
return data.t
|
||||
|
||||
b = builder.build()
|
||||
m = b(NoOpCore())
|
||||
m.takeSomeArgs(3)
|
||||
self.assertEqual(m.value(), 3)
|
||||
|
||||
@skipIf(not hasInterface, "zope.interface not installed")
|
||||
def test_interfaceData(self) -> None:
|
||||
"""
|
||||
Test to cover providedBy assertion.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ArgTaker, NoOpCore)
|
||||
one = builder.state("one")
|
||||
|
||||
@implementer(ISomething)
|
||||
@dataclass
|
||||
class Something:
|
||||
val: int
|
||||
|
||||
def something(self) -> int:
|
||||
return self.val
|
||||
|
||||
def dat(
|
||||
proto: ArgTaker, core: NoOpCore, arg1: int = 0, arg2: str = ""
|
||||
) -> ISomething:
|
||||
return Something(arg1) # type:ignore[return-value]
|
||||
|
||||
two = builder.state("two", dat)
|
||||
one.upon(ArgTaker.takeSomeArgs).to(two).returns(None)
|
||||
|
||||
@pep614(two.upon(ArgTaker.value).loop())
|
||||
def val(proto: ArgTaker, core: NoOpCore, data: ISomething) -> int:
|
||||
return data.something() # type:ignore[misc]
|
||||
|
||||
b = builder.build()
|
||||
m = b(NoOpCore())
|
||||
m.takeSomeArgs(3)
|
||||
self.assertEqual(m.value(), 3)
|
||||
|
||||
def test_noMethodsInAltStateDataFactory(self) -> None:
|
||||
"""
|
||||
When the state machine is received by a data factory during
|
||||
construction, it is in an invalid state. It may be invoked after
|
||||
construction is complete.
|
||||
"""
|
||||
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
|
||||
|
||||
@dataclass
|
||||
class Data:
|
||||
value: int
|
||||
proto: ProtocolForTesting
|
||||
|
||||
start = builder.state("start")
|
||||
data = builder.state("data", lambda proto, core: Data(3, proto))
|
||||
|
||||
@pep614(data.upon(ProtocolForTesting.value).loop())
|
||||
def getval(proto: ProtocolForTesting, core: NoOpCore, data: Data) -> int:
|
||||
return data.value
|
||||
|
||||
@pep614(start.upon(ProtocolForTesting.value).loop())
|
||||
def minusone(proto: ProtocolForTesting, core: NoOpCore) -> int:
|
||||
return -1
|
||||
|
||||
factory = builder.build()
|
||||
self.assertEqual(factory(NoOpCore()).value(), -1)
|
||||
|
||||
def touchproto(proto: ProtocolForTesting, core: NoOpCore) -> Data:
|
||||
return Data(proto.value(), proto)
|
||||
|
||||
catchdata = []
|
||||
|
||||
def notouchproto(proto: ProtocolForTesting, core: NoOpCore) -> Data:
|
||||
catchdata.append(new := Data(4, proto))
|
||||
return new
|
||||
|
||||
with self.assertRaises(NoTransition):
|
||||
factory(NoOpCore(), data, touchproto)
|
||||
machine = factory(NoOpCore(), data, notouchproto)
|
||||
self.assertIs(machine, catchdata[0].proto)
|
||||
self.assertEqual(machine.value(), 4)
|
||||
@ -0,0 +1,478 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
from unittest import TestCase, skipIf
|
||||
|
||||
from automat import TypeMachineBuilder, pep614
|
||||
|
||||
from .._methodical import MethodicalMachine
|
||||
from .._typed import TypeMachine
|
||||
from .test_discover import isTwistedInstalled
|
||||
|
||||
|
||||
def isGraphvizModuleInstalled():
|
||||
"""
|
||||
Is the graphviz Python module installed?
|
||||
"""
|
||||
try:
|
||||
__import__("graphviz")
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def isGraphvizInstalled():
|
||||
"""
|
||||
Are the graphviz tools installed?
|
||||
"""
|
||||
r, w = os.pipe()
|
||||
os.close(w)
|
||||
try:
|
||||
return not subprocess.call("dot", stdin=r, shell=True)
|
||||
finally:
|
||||
os.close(r)
|
||||
|
||||
|
||||
def sampleMachine():
|
||||
"""
|
||||
Create a sample L{MethodicalMachine} with some sample states.
|
||||
"""
|
||||
mm = MethodicalMachine()
|
||||
|
||||
class SampleObject(object):
|
||||
@mm.state(initial=True)
|
||||
def begin(self):
|
||||
"initial state"
|
||||
|
||||
@mm.state()
|
||||
def end(self):
|
||||
"end state"
|
||||
|
||||
@mm.input()
|
||||
def go(self):
|
||||
"sample input"
|
||||
|
||||
@mm.output()
|
||||
def out(self):
|
||||
"sample output"
|
||||
|
||||
begin.upon(go, end, [out])
|
||||
|
||||
so = SampleObject()
|
||||
so.go()
|
||||
return mm
|
||||
|
||||
|
||||
class Sample(Protocol):
|
||||
def go(self) -> None: ...
|
||||
class Core: ...
|
||||
|
||||
|
||||
def sampleTypeMachine() -> TypeMachine[Sample, Core]:
|
||||
"""
|
||||
Create a sample L{TypeMachine} with some sample states.
|
||||
"""
|
||||
builder = TypeMachineBuilder(Sample, Core)
|
||||
begin = builder.state("begin")
|
||||
|
||||
def buildit(proto: Sample, core: Core) -> int:
|
||||
return 3 # pragma: no cover
|
||||
|
||||
data = builder.state("data", buildit)
|
||||
end = builder.state("end")
|
||||
begin.upon(Sample.go).to(data).returns(None)
|
||||
data.upon(Sample.go).to(end).returns(None)
|
||||
|
||||
@pep614(end.upon(Sample.go).to(begin))
|
||||
def out(sample: Sample, core: Core) -> None: ...
|
||||
|
||||
return builder.build()
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class ElementMakerTests(TestCase):
|
||||
"""
|
||||
L{elementMaker} generates HTML representing the specified element.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
from .._visualize import elementMaker
|
||||
|
||||
self.elementMaker = elementMaker
|
||||
|
||||
def test_sortsAttrs(self):
|
||||
"""
|
||||
L{elementMaker} orders HTML attributes lexicographically.
|
||||
"""
|
||||
expected = r'<div a="1" b="2" c="3"></div>'
|
||||
self.assertEqual(expected, self.elementMaker("div", b="2", a="1", c="3"))
|
||||
|
||||
def test_quotesAttrs(self):
|
||||
"""
|
||||
L{elementMaker} quotes HTML attributes according to DOT's quoting rule.
|
||||
|
||||
See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1.
|
||||
"""
|
||||
expected = r'<div a="1" b="a \" quote" c="a string"></div>'
|
||||
self.assertEqual(
|
||||
expected, self.elementMaker("div", b='a " quote', a=1, c="a string")
|
||||
)
|
||||
|
||||
def test_noAttrs(self):
|
||||
"""
|
||||
L{elementMaker} should render an element with no attributes.
|
||||
"""
|
||||
expected = r"<div ></div>"
|
||||
self.assertEqual(expected, self.elementMaker("div"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTMLElement(object):
|
||||
"""Holds an HTML element, as created by elementMaker."""
|
||||
|
||||
name: str
|
||||
children: list[HTMLElement]
|
||||
attributes: dict[str, str]
|
||||
|
||||
|
||||
def findElements(element, predicate):
|
||||
"""
|
||||
Recursively collect all elements in an L{HTMLElement} tree that
|
||||
match the optional predicate.
|
||||
"""
|
||||
if predicate(element):
|
||||
return [element]
|
||||
elif isLeaf(element):
|
||||
return []
|
||||
|
||||
return [
|
||||
result
|
||||
for child in element.children
|
||||
for result in findElements(child, predicate)
|
||||
]
|
||||
|
||||
|
||||
def isLeaf(element):
|
||||
"""
|
||||
This HTML element is actually leaf node.
|
||||
"""
|
||||
return not isinstance(element, HTMLElement)
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class TableMakerTests(TestCase):
|
||||
"""
|
||||
Tests that ensure L{tableMaker} generates HTML tables usable as
|
||||
labels in DOT graphs.
|
||||
|
||||
For more information, read the "HTML-Like Labels" section of
|
||||
U{http://www.graphviz.org/doc/info/shapes.html}.
|
||||
"""
|
||||
|
||||
def fakeElementMaker(self, name, *children, **attributes):
|
||||
return HTMLElement(name=name, children=children, attributes=attributes)
|
||||
|
||||
def setUp(self):
|
||||
from .._visualize import tableMaker
|
||||
|
||||
self.inputLabel = "input label"
|
||||
self.port = "the port"
|
||||
self.tableMaker = functools.partial(tableMaker, _E=self.fakeElementMaker)
|
||||
|
||||
def test_inputLabelRow(self):
|
||||
"""
|
||||
The table returned by L{tableMaker} always contains the input
|
||||
symbol label in its first row, and that row contains one cell
|
||||
with a port attribute set to the provided port.
|
||||
"""
|
||||
|
||||
def hasPort(element):
|
||||
return not isLeaf(element) and element.attributes.get("port") == self.port
|
||||
|
||||
for outputLabels in ([], ["an output label"]):
|
||||
table = self.tableMaker(self.inputLabel, outputLabels, port=self.port)
|
||||
self.assertGreater(len(table.children), 0)
|
||||
inputLabelRow = table.children[0]
|
||||
|
||||
portCandidates = findElements(table, hasPort)
|
||||
|
||||
self.assertEqual(len(portCandidates), 1)
|
||||
self.assertEqual(portCandidates[0].name, "td")
|
||||
self.assertEqual(findElements(inputLabelRow, isLeaf), [self.inputLabel])
|
||||
|
||||
def test_noOutputLabels(self):
|
||||
"""
|
||||
L{tableMaker} does not add a colspan attribute to the input
|
||||
label's cell or a second row if there no output labels.
|
||||
"""
|
||||
table = self.tableMaker("input label", (), port=self.port)
|
||||
self.assertEqual(len(table.children), 1)
|
||||
(inputLabelRow,) = table.children
|
||||
self.assertNotIn("colspan", inputLabelRow.attributes)
|
||||
|
||||
def test_withOutputLabels(self):
|
||||
"""
|
||||
L{tableMaker} adds a colspan attribute to the input label's cell
|
||||
equal to the number of output labels and a second row that
|
||||
contains the output labels.
|
||||
"""
|
||||
table = self.tableMaker(
|
||||
self.inputLabel, ("output label 1", "output label 2"), port=self.port
|
||||
)
|
||||
|
||||
self.assertEqual(len(table.children), 2)
|
||||
inputRow, outputRow = table.children
|
||||
|
||||
def hasCorrectColspan(element):
|
||||
return (
|
||||
not isLeaf(element)
|
||||
and element.name == "td"
|
||||
and element.attributes.get("colspan") == "2"
|
||||
)
|
||||
|
||||
self.assertEqual(len(findElements(inputRow, hasCorrectColspan)), 1)
|
||||
self.assertEqual(
|
||||
findElements(outputRow, isLeaf), ["output label 1", "output label 2"]
|
||||
)
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class IntegrationTests(TestCase):
|
||||
"""
|
||||
Tests which make sure Graphviz can understand the output produced by
|
||||
Automat.
|
||||
"""
|
||||
|
||||
def test_validGraphviz(self) -> None:
|
||||
"""
|
||||
C{graphviz} emits valid graphviz data.
|
||||
"""
|
||||
digraph = sampleMachine().asDigraph()
|
||||
text = "".join(digraph).encode("utf-8")
|
||||
p = subprocess.Popen("dot", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
out, err = p.communicate(text)
|
||||
self.assertEqual(p.returncode, 0)
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class SpotChecks(TestCase):
|
||||
"""
|
||||
Tests to make sure that the output contains salient features of the machine
|
||||
being generated.
|
||||
"""
|
||||
|
||||
def test_containsMachineFeatures(self):
|
||||
"""
|
||||
The output of L{graphviz.Digraph} should contain the names of the
|
||||
states, inputs, outputs in the state machine.
|
||||
"""
|
||||
gvout = "".join(sampleMachine().asDigraph())
|
||||
self.assertIn("begin", gvout)
|
||||
self.assertIn("end", gvout)
|
||||
self.assertIn("go", gvout)
|
||||
self.assertIn("out", gvout)
|
||||
|
||||
def test_containsTypeMachineFeatures(self):
|
||||
"""
|
||||
The output of L{graphviz.Digraph} should contain the names of the states,
|
||||
inputs, outputs in the state machine.
|
||||
"""
|
||||
gvout = "".join(sampleTypeMachine().asDigraph())
|
||||
self.assertIn("begin", gvout)
|
||||
self.assertIn("end", gvout)
|
||||
self.assertIn("go", gvout)
|
||||
self.assertIn("data:buildit", gvout)
|
||||
self.assertIn("out", gvout)
|
||||
|
||||
|
||||
class RecordsDigraphActions(object):
|
||||
"""
|
||||
Records calls made to L{FakeDigraph}.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.renderCalls = []
|
||||
self.saveCalls = []
|
||||
|
||||
|
||||
class FakeDigraph(object):
|
||||
"""
|
||||
A fake L{graphviz.Digraph}. Instantiate it with a
|
||||
L{RecordsDigraphActions}.
|
||||
"""
|
||||
|
||||
def __init__(self, recorder):
|
||||
self._recorder = recorder
|
||||
|
||||
def render(self, **kwargs):
|
||||
self._recorder.renderCalls.append(kwargs)
|
||||
|
||||
def save(self, **kwargs):
|
||||
self._recorder.saveCalls.append(kwargs)
|
||||
|
||||
|
||||
class FakeMethodicalMachine(object):
|
||||
"""
|
||||
A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph}
|
||||
"""
|
||||
|
||||
def __init__(self, digraph):
|
||||
self._digraph = digraph
|
||||
|
||||
def asDigraph(self):
|
||||
return self._digraph
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class VisualizeToolTests(TestCase):
|
||||
def setUp(self):
|
||||
self.digraphRecorder = RecordsDigraphActions()
|
||||
self.fakeDigraph = FakeDigraph(self.digraphRecorder)
|
||||
|
||||
self.fakeProgname = "tool-test"
|
||||
self.fakeSysPath = ["ignored"]
|
||||
self.collectedOutput = []
|
||||
self.fakeFQPN = "fake.fqpn"
|
||||
|
||||
def collectPrints(self, *args):
|
||||
self.collectedOutput.append(" ".join(args))
|
||||
|
||||
def fakeFindMachines(self, fqpn):
|
||||
yield fqpn, FakeMethodicalMachine(self.fakeDigraph)
|
||||
|
||||
def tool(
|
||||
self, progname=None, argv=None, syspath=None, findMachines=None, print=None
|
||||
):
|
||||
from .._visualize import tool
|
||||
|
||||
return tool(
|
||||
_progname=progname or self.fakeProgname,
|
||||
_argv=argv or [self.fakeFQPN],
|
||||
_syspath=syspath or self.fakeSysPath,
|
||||
_findMachines=findMachines or self.fakeFindMachines,
|
||||
_print=print or self.collectPrints,
|
||||
)
|
||||
|
||||
def test_checksCurrentDirectory(self):
|
||||
"""
|
||||
L{tool} adds '' to sys.path to ensure
|
||||
L{automat._discover.findMachines} searches the current
|
||||
directory.
|
||||
"""
|
||||
self.tool(argv=[self.fakeFQPN])
|
||||
self.assertEqual(self.fakeSysPath[0], "")
|
||||
|
||||
def test_quietHidesOutput(self):
|
||||
"""
|
||||
Passing -q/--quiet hides all output.
|
||||
"""
|
||||
self.tool(argv=[self.fakeFQPN, "--quiet"])
|
||||
self.assertFalse(self.collectedOutput)
|
||||
self.tool(argv=[self.fakeFQPN, "-q"])
|
||||
self.assertFalse(self.collectedOutput)
|
||||
|
||||
def test_onlySaveDot(self):
|
||||
"""
|
||||
Passing an empty string for --image-directory/-i disables
|
||||
rendering images.
|
||||
"""
|
||||
for arg in ("--image-directory", "-i"):
|
||||
self.digraphRecorder.reset()
|
||||
self.collectedOutput = []
|
||||
|
||||
self.tool(argv=[self.fakeFQPN, arg, ""])
|
||||
self.assertFalse(any("image" in line for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
|
||||
(call,) = self.digraphRecorder.saveCalls
|
||||
self.assertEqual("{}.dot".format(self.fakeFQPN), call["filename"])
|
||||
|
||||
self.assertFalse(self.digraphRecorder.renderCalls)
|
||||
|
||||
def test_saveOnlyImage(self):
|
||||
"""
|
||||
Passing an empty string for --dot-directory/-d disables saving dot
|
||||
files.
|
||||
"""
|
||||
for arg in ("--dot-directory", "-d"):
|
||||
self.digraphRecorder.reset()
|
||||
self.collectedOutput = []
|
||||
self.tool(argv=[self.fakeFQPN, arg, ""])
|
||||
|
||||
self.assertFalse(any("dot" in line for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
|
||||
(call,) = self.digraphRecorder.renderCalls
|
||||
self.assertEqual("{}.dot".format(self.fakeFQPN), call["filename"])
|
||||
self.assertTrue(call["cleanup"])
|
||||
|
||||
self.assertFalse(self.digraphRecorder.saveCalls)
|
||||
|
||||
def test_saveDotAndImagesInDifferentDirectories(self):
|
||||
"""
|
||||
Passing different directories to --image-directory and --dot-directory
|
||||
writes images and dot files to those directories.
|
||||
"""
|
||||
imageDirectory = "image"
|
||||
dotDirectory = "dot"
|
||||
self.tool(
|
||||
argv=[
|
||||
self.fakeFQPN,
|
||||
"--image-directory",
|
||||
imageDirectory,
|
||||
"--dot-directory",
|
||||
dotDirectory,
|
||||
]
|
||||
)
|
||||
|
||||
self.assertTrue(any("image" in line for line in self.collectedOutput))
|
||||
self.assertTrue(any("dot" in line for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
|
||||
(renderCall,) = self.digraphRecorder.renderCalls
|
||||
self.assertEqual(renderCall["directory"], imageDirectory)
|
||||
self.assertTrue(renderCall["cleanup"])
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
|
||||
(saveCall,) = self.digraphRecorder.saveCalls
|
||||
self.assertEqual(saveCall["directory"], dotDirectory)
|
||||
|
||||
def test_saveDotAndImagesInSameDirectory(self):
|
||||
"""
|
||||
Passing the same directory to --image-directory and --dot-directory
|
||||
writes images and dot files to that one directory.
|
||||
"""
|
||||
directory = "imagesAndDot"
|
||||
self.tool(
|
||||
argv=[
|
||||
self.fakeFQPN,
|
||||
"--image-directory",
|
||||
directory,
|
||||
"--dot-directory",
|
||||
directory,
|
||||
]
|
||||
)
|
||||
|
||||
self.assertTrue(any("image and dot" in line for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
|
||||
(renderCall,) = self.digraphRecorder.renderCalls
|
||||
self.assertEqual(renderCall["directory"], directory)
|
||||
self.assertFalse(renderCall["cleanup"])
|
||||
|
||||
self.assertFalse(len(self.digraphRecorder.saveCalls))
|
||||
@ -0,0 +1,736 @@
|
||||
# -*- test-case-name: automat._test.test_type_based -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
get_origin,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphviz import Digraph
|
||||
try:
|
||||
from zope.interface.interface import InterfaceClass # type:ignore[import-untyped]
|
||||
except ImportError:
|
||||
hasInterface = False
|
||||
else:
|
||||
hasInterface = True
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
from typing_extensions import Concatenate, ParamSpec, TypeAlias
|
||||
else:
|
||||
from typing import Concatenate, ParamSpec, TypeAlias
|
||||
|
||||
from ._core import Automaton, Transitioner
|
||||
from ._runtimeproto import (
|
||||
ProtocolAtRuntime,
|
||||
_liveSignature,
|
||||
actuallyDefinedProtocolMethods,
|
||||
runtime_name,
|
||||
)
|
||||
|
||||
|
||||
class AlreadyBuiltError(Exception):
|
||||
"""
|
||||
The L{TypeMachine} is already built, and thus can no longer be
|
||||
modified.
|
||||
"""
|
||||
|
||||
|
||||
InputProtocol = TypeVar("InputProtocol")
|
||||
Core = TypeVar("Core")
|
||||
Data = TypeVar("Data")
|
||||
P = ParamSpec("P")
|
||||
P1 = ParamSpec("P1")
|
||||
R = TypeVar("R")
|
||||
OtherData = TypeVar("OtherData")
|
||||
Decorator = Callable[[Callable[P, R]], Callable[P, R]]
|
||||
FactoryParams = ParamSpec("FactoryParams")
|
||||
OtherFactoryParams = ParamSpec("OtherFactoryParams")
|
||||
|
||||
|
||||
def pep614(t: R) -> R:
|
||||
"""
|
||||
This is a workaround for Python 3.8, which has U{some restrictions on its
|
||||
grammar for decorators <https://peps.python.org/pep-0614/>}, and makes
|
||||
C{@state.to(other).upon(Protocol.input)} invalid syntax; for code that
|
||||
needs to run on these older Python versions, you can do
|
||||
C{@pep614(state.to(other).upon(Protocol.input))} instead.
|
||||
"""
|
||||
return t
|
||||
|
||||
|
||||
@dataclass()
|
||||
class TransitionRegistrar(Generic[P, P1, R]):
|
||||
"""
|
||||
This is a record of a transition that need finalizing; it is the result of
|
||||
calling L{TypeMachineBuilder.state} and then ``.upon(input).to(state)`` on
|
||||
the result of that.
|
||||
|
||||
It can be used as a decorator, like::
|
||||
|
||||
registrar = state.upon(Proto.input).to(state2)
|
||||
@registrar
|
||||
def inputImplementation(proto: Proto, core: Core) -> Result: ...
|
||||
|
||||
Or, it can be used used to implement a constant return value with
|
||||
L{TransitionRegistrar.returns}, like::
|
||||
|
||||
registrar = state.upon(Proto.input).to(state2)
|
||||
registrar.returns(value)
|
||||
|
||||
Type parameter P: the precise signature of the decorated implementation
|
||||
callable.
|
||||
|
||||
Type parameter P1: the precise signature of the input method from the
|
||||
outward-facing state-machine protocol.
|
||||
|
||||
Type parameter R: the return type of both the protocol method and the input
|
||||
method.
|
||||
"""
|
||||
|
||||
_signature: Callable[P1, R]
|
||||
_old: AnyState
|
||||
_new: AnyState
|
||||
_nodata: bool = False
|
||||
_callback: Callable[P, R] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._old.builder._registrars.append(self)
|
||||
|
||||
def __call__(self, impl: Callable[P, R]) -> Callable[P, R]:
|
||||
"""
|
||||
Finalize it with C{__call__} to indicate that there is an
|
||||
implementation to the transition, which can be treated as an output.
|
||||
"""
|
||||
if self._callback is not None:
|
||||
raise AlreadyBuiltError(
|
||||
f"already registered transition from {self._old.name!r} to {self._new.name!r}"
|
||||
)
|
||||
self._callback = impl
|
||||
builder = self._old.builder
|
||||
assert builder is self._new.builder, "states must be from the same builder"
|
||||
builder._automaton.addTransition(
|
||||
self._old,
|
||||
self._signature.__name__,
|
||||
self._new,
|
||||
tuple(self._new._produceOutputs(impl, self._old, self._nodata)),
|
||||
)
|
||||
return impl
|
||||
|
||||
def returns(self, result: R) -> None:
|
||||
"""
|
||||
Finalize it with C{.returns(constant)} to indicate that there is no
|
||||
method body, and the given result can just be yielded each time after
|
||||
the state transition. The only output generated in this case would be
|
||||
the data-construction factory for the target state.
|
||||
"""
|
||||
|
||||
def constant(*args: object, **kwargs: object) -> R:
|
||||
return result
|
||||
|
||||
constant.__name__ = f"returns({result})"
|
||||
self(constant)
|
||||
|
||||
def _checkComplete(self) -> None:
|
||||
"""
|
||||
Raise an exception if the user forgot to decorate a method
|
||||
implementation or supply a return value for this transition.
|
||||
"""
|
||||
# TODO: point at the line where `.to`/`.loop`/`.upon` are called so the
|
||||
# user can more immediately see the incomplete transition
|
||||
if not self._callback:
|
||||
raise ValueError(
|
||||
f"incomplete transition from {self._old.name} to "
|
||||
f"{self._new.name} upon {self._signature.__qualname__}: "
|
||||
"remember to use the transition as a decorator or call "
|
||||
"`.returns` on it."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UponFromNo(Generic[InputProtocol, Core, P, R]):
|
||||
"""
|
||||
Type parameter P: the signature of the input method.
|
||||
"""
|
||||
|
||||
old: TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Any, ...]
|
||||
input: Callable[Concatenate[InputProtocol, P], R]
|
||||
|
||||
@overload
|
||||
def to(
|
||||
self, state: TypedState[InputProtocol, Core]
|
||||
) -> TransitionRegistrar[Concatenate[InputProtocol, Core, P], P, R]: ...
|
||||
@overload
|
||||
def to(
|
||||
self,
|
||||
state: TypedDataState[InputProtocol, Core, OtherData, P],
|
||||
) -> TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, P],
|
||||
Concatenate[InputProtocol, P],
|
||||
R,
|
||||
]: ...
|
||||
def to(
|
||||
self,
|
||||
state: (
|
||||
TypedState[InputProtocol, Core]
|
||||
| TypedDataState[InputProtocol, Core, Any, P]
|
||||
),
|
||||
) -> (
|
||||
TransitionRegistrar[Concatenate[InputProtocol, Core, P], P, R]
|
||||
| TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, P],
|
||||
Concatenate[InputProtocol, P],
|
||||
R,
|
||||
]
|
||||
):
|
||||
"""
|
||||
Declare a state transition to a new state.
|
||||
"""
|
||||
return TransitionRegistrar(self.input, self.old, state, True)
|
||||
|
||||
def loop(self) -> TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, P],
|
||||
Concatenate[InputProtocol, P],
|
||||
R,
|
||||
]:
|
||||
"""
|
||||
Register a transition back to the same state.
|
||||
"""
|
||||
return TransitionRegistrar(self.input, self.old, self.old, True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UponFromData(Generic[InputProtocol, Core, P, R, Data]):
|
||||
"""
|
||||
Type parameter P: the signature of the input method.
|
||||
"""
|
||||
|
||||
old: TypedDataState[InputProtocol, Core, Data, ...]
|
||||
input: Callable[Concatenate[InputProtocol, P], R]
|
||||
|
||||
@overload
|
||||
def to(
|
||||
self, state: TypedState[InputProtocol, Core]
|
||||
) -> TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, Data, P], Concatenate[InputProtocol, P], R
|
||||
]: ...
|
||||
@overload
|
||||
def to(
|
||||
self,
|
||||
state: TypedDataState[InputProtocol, Core, OtherData, P],
|
||||
) -> TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, Data, P],
|
||||
Concatenate[InputProtocol, P],
|
||||
R,
|
||||
]: ...
|
||||
def to(
|
||||
self,
|
||||
state: (
|
||||
TypedState[InputProtocol, Core]
|
||||
| TypedDataState[InputProtocol, Core, Any, P]
|
||||
),
|
||||
) -> (
|
||||
TransitionRegistrar[Concatenate[InputProtocol, Core, P], P, R]
|
||||
| TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, Data, P],
|
||||
Concatenate[InputProtocol, P],
|
||||
R,
|
||||
]
|
||||
):
|
||||
"""
|
||||
Declare a state transition to a new state.
|
||||
"""
|
||||
return TransitionRegistrar(self.input, self.old, state)
|
||||
|
||||
def loop(self) -> TransitionRegistrar[
|
||||
Concatenate[InputProtocol, Core, Data, P],
|
||||
Concatenate[InputProtocol, P],
|
||||
R,
|
||||
]:
|
||||
"""
|
||||
Register a transition back to the same state.
|
||||
"""
|
||||
return TransitionRegistrar(self.input, self.old, self.old)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TypedState(Generic[InputProtocol, Core]):
|
||||
"""
|
||||
The result of L{.state() <automat.TypeMachineBuilder.state>}.
|
||||
"""
|
||||
|
||||
name: str
|
||||
builder: TypeMachineBuilder[InputProtocol, Core] = field(repr=False)
|
||||
|
||||
def upon(
|
||||
self, input: Callable[Concatenate[InputProtocol, P], R]
|
||||
) -> UponFromNo[InputProtocol, Core, P, R]:
|
||||
".upon()"
|
||||
self.builder._checkMembership(input)
|
||||
return UponFromNo(self, input)
|
||||
|
||||
def _produceOutputs(
|
||||
self,
|
||||
impl: Callable[..., object],
|
||||
old: (
|
||||
TypedDataState[InputProtocol, Core, OtherData, OtherFactoryParams]
|
||||
| TypedState[InputProtocol, Core]
|
||||
),
|
||||
nodata: bool = False,
|
||||
) -> Iterable[SomeOutput]:
|
||||
yield MethodOutput._fromImpl(impl, isinstance(old, TypedDataState))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TypedDataState(Generic[InputProtocol, Core, Data, FactoryParams]):
|
||||
name: str
|
||||
builder: TypeMachineBuilder[InputProtocol, Core] = field(repr=False)
|
||||
factory: Callable[Concatenate[InputProtocol, Core, FactoryParams], Data]
|
||||
|
||||
@overload
|
||||
def upon(
|
||||
self, input: Callable[Concatenate[InputProtocol, P], R]
|
||||
) -> UponFromData[InputProtocol, Core, P, R, Data]: ...
|
||||
@overload
|
||||
def upon(
|
||||
self, input: Callable[Concatenate[InputProtocol, P], R], nodata: Literal[False]
|
||||
) -> UponFromData[InputProtocol, Core, P, R, Data]: ...
|
||||
@overload
|
||||
def upon(
|
||||
self, input: Callable[Concatenate[InputProtocol, P], R], nodata: Literal[True]
|
||||
) -> UponFromNo[InputProtocol, Core, P, R]: ...
|
||||
def upon(
|
||||
self,
|
||||
input: Callable[Concatenate[InputProtocol, P], R],
|
||||
nodata: bool = False,
|
||||
) -> (
|
||||
UponFromData[InputProtocol, Core, P, R, Data]
|
||||
| UponFromNo[InputProtocol, Core, P, R]
|
||||
):
|
||||
self.builder._checkMembership(input)
|
||||
if nodata:
|
||||
return UponFromNo(self, input)
|
||||
else:
|
||||
return UponFromData(self, input)
|
||||
|
||||
def _produceOutputs(
|
||||
self,
|
||||
impl: Callable[..., object],
|
||||
old: (
|
||||
TypedDataState[InputProtocol, Core, OtherData, OtherFactoryParams]
|
||||
| TypedState[InputProtocol, Core]
|
||||
),
|
||||
nodata: bool,
|
||||
) -> Iterable[SomeOutput]:
|
||||
if self is not old:
|
||||
yield DataOutput(self.factory)
|
||||
yield MethodOutput._fromImpl(
|
||||
impl, isinstance(old, TypedDataState) and not nodata
|
||||
)
|
||||
|
||||
|
||||
AnyState: TypeAlias = "TypedState[Any, Any] | TypedDataState[Any, Any, Any, Any]"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TypedInput:
|
||||
name: str
|
||||
|
||||
|
||||
class SomeOutput(Protocol):
|
||||
"""
|
||||
A state machine output.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"read-only name property"
|
||||
|
||||
def __call__(*args: Any, **kwargs: Any) -> Any: ...
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"must be hashable"
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputImplementer(Generic[InputProtocol, Core]):
|
||||
"""
|
||||
An L{InputImplementer} implements an input protocol in terms of a
|
||||
state machine.
|
||||
|
||||
When the factory returned from L{TypeMachine}
|
||||
"""
|
||||
|
||||
__automat_core__: Core
|
||||
__automat_transitioner__: Transitioner[
|
||||
TypedState[InputProtocol, Core]
|
||||
| TypedDataState[InputProtocol, Core, object, ...],
|
||||
str,
|
||||
SomeOutput,
|
||||
]
|
||||
__automat_data__: object | None = None
|
||||
__automat_postponed__: list[Callable[[], None]] | None = None
|
||||
|
||||
|
||||
def implementMethod(
|
||||
method: Callable[..., object],
|
||||
) -> Callable[..., object]:
|
||||
"""
|
||||
Construct a function for populating in the synthetic provider of the Input
|
||||
Protocol to a L{TypeMachineBuilder}. It should have a signature matching that
|
||||
of the C{method} parameter, a function from that protocol.
|
||||
"""
|
||||
methodInput = method.__name__
|
||||
# side-effects can be re-ordered until later. If you need to compute a
|
||||
# value in your method, then obviously it can't be invoked reentrantly.
|
||||
returnAnnotation = _liveSignature(method).return_annotation
|
||||
returnsNone = returnAnnotation is None
|
||||
|
||||
def implementation(
|
||||
self: InputImplementer[InputProtocol, Core], *args: object, **kwargs: object
|
||||
) -> object:
|
||||
transitioner = self.__automat_transitioner__
|
||||
dataAtStart = self.__automat_data__
|
||||
if self.__automat_postponed__ is not None:
|
||||
if not returnsNone:
|
||||
raise RuntimeError(
|
||||
f"attempting to reentrantly run {method.__qualname__} "
|
||||
f"but it wants to return {returnAnnotation!r} not None"
|
||||
)
|
||||
|
||||
def rerunme() -> None:
|
||||
implementation(self, *args, **kwargs)
|
||||
|
||||
self.__automat_postponed__.append(rerunme)
|
||||
return None
|
||||
postponed = self.__automat_postponed__ = []
|
||||
try:
|
||||
[outputs, tracer] = transitioner.transition(methodInput)
|
||||
result: Any = None
|
||||
for output in outputs:
|
||||
# here's the idea: there will be a state-setup output and a
|
||||
# state-teardown output. state-setup outputs are added to the
|
||||
# *beginning* of any entry into a state, so that by the time you
|
||||
# are running the *implementation* of a method that has entered
|
||||
# that state, the protocol is in a self-consistent state and can
|
||||
# run reentrant outputs. not clear that state-teardown outputs are
|
||||
# necessary
|
||||
result = output(self, dataAtStart, *args, **kwargs)
|
||||
finally:
|
||||
self.__automat_postponed__ = None
|
||||
while postponed:
|
||||
postponed.pop(0)()
|
||||
return result
|
||||
|
||||
implementation.__qualname__ = implementation.__name__ = (
|
||||
f"<implementation for {method}>"
|
||||
)
|
||||
return implementation
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MethodOutput(Generic[Core]):
|
||||
"""
|
||||
This is the thing that goes into the automaton's outputs list, and thus
|
||||
(per the implementation of L{implementMethod}) takes the 'self' of the
|
||||
InputImplementer instance (i.e. the synthetic protocol implementation) and the
|
||||
previous result computed by the former output, which will be None
|
||||
initially.
|
||||
"""
|
||||
|
||||
method: Callable[..., Any]
|
||||
requiresData: bool
|
||||
_assertion: Callable[[object], None]
|
||||
|
||||
@classmethod
|
||||
def _fromImpl(
|
||||
cls: type[MethodOutput[Core]], method: Callable[..., Any], requiresData: bool
|
||||
) -> MethodOutput[Core]:
|
||||
parameter = None
|
||||
annotation: type[object] = object
|
||||
|
||||
def assertion(data: object) -> None:
|
||||
"""
|
||||
No assertion about the data.
|
||||
"""
|
||||
|
||||
# Do our best to compute the declared signature, so that we caan verify
|
||||
# it's the right type. We can't always do that.
|
||||
try:
|
||||
sig = _liveSignature(method)
|
||||
except NameError:
|
||||
...
|
||||
# An inner function may refer to type aliases that only appear as
|
||||
# local variables, and those are just lost here; give up.
|
||||
else:
|
||||
if requiresData:
|
||||
# 0: self, 1: self.__automat_core__, 2: self.__automat_data__
|
||||
declaredParams = list(sig.parameters.values())
|
||||
if len(declaredParams) >= 3:
|
||||
parameter = declaredParams[2]
|
||||
annotation = parameter.annotation
|
||||
origin = get_origin(annotation)
|
||||
if origin is not None:
|
||||
annotation = origin
|
||||
if hasInterface and isinstance(annotation, InterfaceClass):
|
||||
|
||||
def assertion(data: object) -> None:
|
||||
assert annotation.providedBy(data), (
|
||||
f"expected {parameter} to provide {annotation} "
|
||||
f"but got {type(data)} instead"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
def assertion(data: object) -> None:
|
||||
assert isinstance(data, annotation), (
|
||||
f"expected {parameter} to be {annotation} "
|
||||
f"but got {type(data)} instead"
|
||||
)
|
||||
|
||||
return cls(method, requiresData, assertion)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"{self.method.__name__}"
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
machine: InputImplementer[InputProtocol, Core],
|
||||
dataAtStart: Data,
|
||||
/,
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> object:
|
||||
extraArgs = [machine, machine.__automat_core__]
|
||||
if self.requiresData:
|
||||
self._assertion(dataAtStart)
|
||||
extraArgs += [dataAtStart]
|
||||
# if anything is invoked reentrantly here, then we can't possibly have
|
||||
# set __automat_data__ and the data argument to the reentrant method
|
||||
# will be wrong. we *need* to split out the construction / state-enter
|
||||
# hook, because it needs to run separately.
|
||||
return self.method(*extraArgs, *args, **kwargs)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DataOutput(Generic[Data]):
|
||||
"""
|
||||
Construct an output for the given data objects.
|
||||
"""
|
||||
|
||||
dataFactory: Callable[..., Data]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"data:{self.dataFactory.__name__}"
|
||||
|
||||
def __call__(
|
||||
realself,
|
||||
self: InputImplementer[InputProtocol, Core],
|
||||
dataAtStart: object,
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> Data:
|
||||
newData = realself.dataFactory(self, self.__automat_core__, *args, **kwargs)
|
||||
self.__automat_data__ = newData
|
||||
return newData
|
||||
|
||||
|
||||
INVALID_WHILE_DESERIALIZING: TypedState[Any, Any] = TypedState(
|
||||
"automat:invalid-while-deserializing",
|
||||
None, # type:ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TypeMachine(Generic[InputProtocol, Core]):
|
||||
"""
|
||||
A L{TypeMachine} is a factory for instances of C{InputProtocol}.
|
||||
"""
|
||||
|
||||
__automat_type__: type[InputImplementer[InputProtocol, Core]]
|
||||
__automat_automaton__: Automaton[
|
||||
TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Any, ...],
|
||||
str,
|
||||
SomeOutput,
|
||||
]
|
||||
|
||||
@overload
|
||||
def __call__(self, core: Core) -> InputProtocol: ...
|
||||
@overload
|
||||
def __call__(
|
||||
self, core: Core, state: TypedState[InputProtocol, Core]
|
||||
) -> InputProtocol: ...
|
||||
@overload
|
||||
def __call__(
|
||||
self,
|
||||
core: Core,
|
||||
state: TypedDataState[InputProtocol, Core, OtherData, ...],
|
||||
dataFactory: Callable[[InputProtocol, Core], OtherData],
|
||||
) -> InputProtocol: ...
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
core: Core,
|
||||
state: (
|
||||
TypedState[InputProtocol, Core]
|
||||
| TypedDataState[InputProtocol, Core, OtherData, ...]
|
||||
| None
|
||||
) = None,
|
||||
dataFactory: Callable[[InputProtocol, Core], OtherData] | None = None,
|
||||
) -> InputProtocol:
|
||||
"""
|
||||
Construct an instance of C{InputProtocol} from an instance of the
|
||||
C{Core} protocol.
|
||||
"""
|
||||
if state is None:
|
||||
state = initial = self.__automat_automaton__.initialState
|
||||
elif isinstance(state, TypedDataState):
|
||||
assert dataFactory is not None, "data state requires a data factory"
|
||||
# Ensure that the machine is in a state with *no* transitions while
|
||||
# we are doing the initial construction of its state-specific data.
|
||||
initial = INVALID_WHILE_DESERIALIZING
|
||||
else:
|
||||
initial = state
|
||||
|
||||
internals: InputImplementer[InputProtocol, Core] = self.__automat_type__(
|
||||
core, txnr := Transitioner(self.__automat_automaton__, initial)
|
||||
)
|
||||
result: InputProtocol = internals # type:ignore[assignment]
|
||||
|
||||
if dataFactory is not None:
|
||||
internals.__automat_data__ = dataFactory(result, core)
|
||||
txnr._state = state
|
||||
return result
|
||||
|
||||
def asDigraph(self) -> Digraph:
|
||||
from ._visualize import makeDigraph
|
||||
|
||||
return makeDigraph(
|
||||
self.__automat_automaton__,
|
||||
stateAsString=lambda state: state.name,
|
||||
inputAsString=lambda input: input,
|
||||
outputAsString=lambda output: output.name,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class TypeMachineBuilder(Generic[InputProtocol, Core]):
|
||||
"""
|
||||
The main entry-point into Automat, used to construct a factory for
|
||||
instances of C{InputProtocol} that take an instance of C{Core}.
|
||||
|
||||
Describe the machine with L{TypeMachineBuilder.state} L{.upon
|
||||
<automat._typed.TypedState.upon>} L{.to
|
||||
<automat._typed.UponFromNo.to>}, then build it with
|
||||
L{TypeMachineBuilder.build}, like so::
|
||||
|
||||
from typing import Protocol
|
||||
class Inputs(Protocol):
|
||||
def method(self) -> None: ...
|
||||
class Core: ...
|
||||
|
||||
from automat import TypeMachineBuilder
|
||||
builder = TypeMachineBuilder(Inputs, Core)
|
||||
state = builder.state("state")
|
||||
state.upon(Inputs.method).loop().returns(None)
|
||||
Machine = builder.build()
|
||||
|
||||
machine = Machine(Core())
|
||||
machine.method()
|
||||
"""
|
||||
|
||||
# Public constructor parameters.
|
||||
inputProtocol: ProtocolAtRuntime[InputProtocol]
|
||||
coreType: type[Core]
|
||||
|
||||
# Internal state, not in the constructor.
|
||||
_automaton: Automaton[
|
||||
TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Any, ...],
|
||||
str,
|
||||
SomeOutput,
|
||||
] = field(default_factory=Automaton, repr=False, init=False)
|
||||
_initial: bool = field(default=True, init=False)
|
||||
_registrars: list[TransitionRegistrar[..., ..., Any]] = field(
|
||||
default_factory=list, init=False
|
||||
)
|
||||
_built: bool = field(default=False, init=False)
|
||||
|
||||
@overload
|
||||
def state(self, name: str) -> TypedState[InputProtocol, Core]: ...
|
||||
@overload
|
||||
def state(
|
||||
self,
|
||||
name: str,
|
||||
dataFactory: Callable[Concatenate[InputProtocol, Core, P], Data],
|
||||
) -> TypedDataState[InputProtocol, Core, Data, P]: ...
|
||||
def state(
|
||||
self,
|
||||
name: str,
|
||||
dataFactory: Callable[Concatenate[InputProtocol, Core, P], Data] | None = None,
|
||||
) -> TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Data, P]:
|
||||
"""
|
||||
Construct a state.
|
||||
"""
|
||||
if self._built:
|
||||
raise AlreadyBuiltError(
|
||||
"Cannot add states to an already-built state machine."
|
||||
)
|
||||
if dataFactory is None:
|
||||
state = TypedState(name, self)
|
||||
if self._initial:
|
||||
self._initial = False
|
||||
self._automaton.initialState = state
|
||||
return state
|
||||
else:
|
||||
assert not self._initial, "initial state cannot require state-specific data"
|
||||
return TypedDataState(name, self, dataFactory)
|
||||
|
||||
def build(self) -> TypeMachine[InputProtocol, Core]:
|
||||
"""
|
||||
Create a L{TypeMachine}, and prevent further modification to the state
|
||||
machine being built.
|
||||
"""
|
||||
# incompleteness check
|
||||
if self._built:
|
||||
raise AlreadyBuiltError("Cannot build a state machine twice.")
|
||||
self._built = True
|
||||
|
||||
for registrar in self._registrars:
|
||||
registrar._checkComplete()
|
||||
|
||||
# We were only hanging on to these for error-checking purposes, so we
|
||||
# can drop them now.
|
||||
del self._registrars[:]
|
||||
|
||||
runtimeType: type[InputImplementer[InputProtocol, Core]] = type(
|
||||
f"Typed<{runtime_name(self.inputProtocol)}>",
|
||||
tuple([InputImplementer]),
|
||||
{
|
||||
method_name: implementMethod(getattr(self.inputProtocol, method_name))
|
||||
for method_name in actuallyDefinedProtocolMethods(self.inputProtocol)
|
||||
},
|
||||
)
|
||||
|
||||
return TypeMachine(runtimeType, self._automaton)
|
||||
|
||||
def _checkMembership(self, input: Callable[..., object]) -> None:
|
||||
"""
|
||||
Ensure that ``input`` is a valid member function of the input protocol,
|
||||
not just a function that happens to take the right first argument.
|
||||
"""
|
||||
if (checked := getattr(self.inputProtocol, input.__name__, None)) is not input:
|
||||
raise ValueError(
|
||||
f"{input.__qualname__} is not a member of {self.inputProtocol.__module__}.{self.inputProtocol.__name__}"
|
||||
)
|
||||
@ -0,0 +1,230 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from functools import wraps
|
||||
from typing import Callable, Iterator
|
||||
|
||||
import graphviz
|
||||
|
||||
from ._core import Automaton, Input, Output, State
|
||||
from ._discover import findMachines
|
||||
from ._methodical import MethodicalMachine
|
||||
from ._typed import TypeMachine, InputProtocol, Core
|
||||
|
||||
|
||||
def _gvquote(s: str) -> str:
|
||||
return '"{}"'.format(s.replace('"', r"\""))
|
||||
|
||||
|
||||
def _gvhtml(s: str) -> str:
|
||||
return "<{}>".format(s)
|
||||
|
||||
|
||||
def elementMaker(name: str, *children: str, **attrs: str) -> str:
|
||||
"""
|
||||
Construct a string from the HTML element description.
|
||||
"""
|
||||
formattedAttrs = " ".join(
|
||||
"{}={}".format(key, _gvquote(str(value)))
|
||||
for key, value in sorted(attrs.items())
|
||||
)
|
||||
formattedChildren = "".join(children)
|
||||
return "<{name} {attrs}>{children}</{name}>".format(
|
||||
name=name, attrs=formattedAttrs, children=formattedChildren
|
||||
)
|
||||
|
||||
|
||||
def tableMaker(
|
||||
inputLabel: str,
|
||||
outputLabels: list[str],
|
||||
port: str,
|
||||
_E: Callable[..., str] = elementMaker,
|
||||
) -> str:
|
||||
"""
|
||||
Construct an HTML table to label a state transition.
|
||||
"""
|
||||
colspan = {}
|
||||
if outputLabels:
|
||||
colspan["colspan"] = str(len(outputLabels))
|
||||
|
||||
inputLabelCell = _E(
|
||||
"td",
|
||||
_E("font", inputLabel, face="menlo-italic"),
|
||||
color="purple",
|
||||
port=port,
|
||||
**colspan,
|
||||
)
|
||||
|
||||
pointSize = {"point-size": "9"}
|
||||
outputLabelCells = [
|
||||
_E("td", _E("font", outputLabel, **pointSize), color="pink")
|
||||
for outputLabel in outputLabels
|
||||
]
|
||||
|
||||
rows = [_E("tr", inputLabelCell)]
|
||||
|
||||
if outputLabels:
|
||||
rows.append(_E("tr", *outputLabelCells))
|
||||
|
||||
return _E("table", *rows)
|
||||
|
||||
|
||||
def escapify(x: Callable[[State], str]) -> Callable[[State], str]:
|
||||
@wraps(x)
|
||||
def impl(t: State) -> str:
|
||||
return x(t).replace("<", "<").replace(">", ">")
|
||||
|
||||
return impl
|
||||
|
||||
|
||||
def makeDigraph(
|
||||
automaton: Automaton[State, Input, Output],
|
||||
inputAsString: Callable[[Input], str] = repr,
|
||||
outputAsString: Callable[[Output], str] = repr,
|
||||
stateAsString: Callable[[State], str] = repr,
|
||||
) -> graphviz.Digraph:
|
||||
"""
|
||||
Produce a L{graphviz.Digraph} object from an automaton.
|
||||
"""
|
||||
|
||||
inputAsString = escapify(inputAsString)
|
||||
outputAsString = escapify(outputAsString)
|
||||
stateAsString = escapify(stateAsString)
|
||||
|
||||
digraph = graphviz.Digraph(
|
||||
graph_attr={"pack": "true", "dpi": "100"},
|
||||
node_attr={"fontname": "Menlo"},
|
||||
edge_attr={"fontname": "Menlo"},
|
||||
)
|
||||
|
||||
for state in automaton.states():
|
||||
if state is automaton.initialState:
|
||||
stateShape = "bold"
|
||||
fontName = "Menlo-Bold"
|
||||
else:
|
||||
stateShape = ""
|
||||
fontName = "Menlo"
|
||||
digraph.node(
|
||||
stateAsString(state),
|
||||
fontame=fontName,
|
||||
shape="ellipse",
|
||||
style=stateShape,
|
||||
color="blue",
|
||||
)
|
||||
for n, eachTransition in enumerate(automaton.allTransitions()):
|
||||
inState, inputSymbol, outState, outputSymbols = eachTransition
|
||||
thisTransition = "t{}".format(n)
|
||||
inputLabel = inputAsString(inputSymbol)
|
||||
|
||||
port = "tableport"
|
||||
table = tableMaker(
|
||||
inputLabel,
|
||||
[outputAsString(outputSymbol) for outputSymbol in outputSymbols],
|
||||
port=port,
|
||||
)
|
||||
|
||||
digraph.node(thisTransition, label=_gvhtml(table), margin="0.2", shape="none")
|
||||
|
||||
digraph.edge(
|
||||
stateAsString(inState),
|
||||
"{}:{}:w".format(thisTransition, port),
|
||||
arrowhead="none",
|
||||
)
|
||||
digraph.edge("{}:{}:e".format(thisTransition, port), stateAsString(outState))
|
||||
|
||||
return digraph
|
||||
|
||||
|
||||
def tool(
|
||||
_progname: str = sys.argv[0],
|
||||
_argv: list[str] = sys.argv[1:],
|
||||
_syspath: list[str] = sys.path,
|
||||
_findMachines: Callable[
|
||||
[str],
|
||||
Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]],
|
||||
] = findMachines,
|
||||
_print: Callable[..., None] = print,
|
||||
) -> None:
|
||||
"""
|
||||
Entry point for command line utility.
|
||||
"""
|
||||
|
||||
DESCRIPTION = """
|
||||
Visualize automat.MethodicalMachines as graphviz graphs.
|
||||
"""
|
||||
EPILOG = """
|
||||
You must have the graphviz tool suite installed. Please visit
|
||||
http://www.graphviz.org for more information.
|
||||
"""
|
||||
if _syspath[0]:
|
||||
_syspath.insert(0, "")
|
||||
argumentParser = argparse.ArgumentParser(
|
||||
prog=_progname, description=DESCRIPTION, epilog=EPILOG
|
||||
)
|
||||
argumentParser.add_argument(
|
||||
"fqpn",
|
||||
help="A Fully Qualified Path name" " representing where to find machines.",
|
||||
)
|
||||
argumentParser.add_argument(
|
||||
"--quiet", "-q", help="suppress output", default=False, action="store_true"
|
||||
)
|
||||
argumentParser.add_argument(
|
||||
"--dot-directory",
|
||||
"-d",
|
||||
help="Where to write out .dot files.",
|
||||
default=".automat_visualize",
|
||||
)
|
||||
argumentParser.add_argument(
|
||||
"--image-directory",
|
||||
"-i",
|
||||
help="Where to write out image files.",
|
||||
default=".automat_visualize",
|
||||
)
|
||||
argumentParser.add_argument(
|
||||
"--image-type",
|
||||
"-t",
|
||||
help="The image format.",
|
||||
choices=graphviz.FORMATS,
|
||||
default="png",
|
||||
)
|
||||
argumentParser.add_argument(
|
||||
"--view",
|
||||
"-v",
|
||||
help="View rendered graphs with" " default image viewer",
|
||||
default=False,
|
||||
action="store_true",
|
||||
)
|
||||
args = argumentParser.parse_args(_argv)
|
||||
|
||||
explicitlySaveDot = args.dot_directory and (
|
||||
not args.image_directory or args.image_directory != args.dot_directory
|
||||
)
|
||||
if args.quiet:
|
||||
|
||||
def _print(*args):
|
||||
pass
|
||||
|
||||
for fqpn, machine in _findMachines(args.fqpn):
|
||||
_print(fqpn, "...discovered")
|
||||
|
||||
digraph = machine.asDigraph()
|
||||
|
||||
if explicitlySaveDot:
|
||||
digraph.save(filename="{}.dot".format(fqpn), directory=args.dot_directory)
|
||||
_print(fqpn, "...wrote dot into", args.dot_directory)
|
||||
|
||||
if args.image_directory:
|
||||
deleteDot = not args.dot_directory or explicitlySaveDot
|
||||
digraph.format = args.image_type
|
||||
digraph.render(
|
||||
filename="{}.dot".format(fqpn),
|
||||
directory=args.image_directory,
|
||||
view=args.view,
|
||||
cleanup=deleteDot,
|
||||
)
|
||||
if deleteDot:
|
||||
msg = "...wrote image into"
|
||||
else:
|
||||
msg = "...wrote image and dot into"
|
||||
_print(fqpn, msg, args.image_directory)
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,20 @@
|
||||
Copyright 2010 Jason Kirtland
|
||||
|
||||
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,60 @@
|
||||
Metadata-Version: 2.3
|
||||
Name: blinker
|
||||
Version: 1.9.0
|
||||
Summary: Fast, simple object-to-object and broadcast signaling
|
||||
Author: Jason Kirtland
|
||||
Maintainer-email: Pallets Ecosystem <contact@palletsprojects.com>
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Typing :: Typed
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Project-URL: Documentation, https://blinker.readthedocs.io
|
||||
Project-URL: Source, https://github.com/pallets-eco/blinker/
|
||||
|
||||
# Blinker
|
||||
|
||||
Blinker provides a fast dispatching system that allows any number of
|
||||
interested parties to subscribe to events, or "signals".
|
||||
|
||||
|
||||
## Pallets Community Ecosystem
|
||||
|
||||
> [!IMPORTANT]\
|
||||
> This project is part of the Pallets Community Ecosystem. Pallets is the open
|
||||
> source organization that maintains Flask; Pallets-Eco enables community
|
||||
> maintenance of related projects. If you are interested in helping maintain
|
||||
> this project, please reach out on [the Pallets Discord server][discord].
|
||||
>
|
||||
> [discord]: https://discord.gg/pallets
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
Signal receivers can subscribe to specific senders or receive signals
|
||||
sent by any sender.
|
||||
|
||||
```pycon
|
||||
>>> from blinker import signal
|
||||
>>> started = signal('round-started')
|
||||
>>> def each(round):
|
||||
... print(f"Round {round}")
|
||||
...
|
||||
>>> started.connect(each)
|
||||
|
||||
>>> def round_two(round):
|
||||
... print("This is round two.")
|
||||
...
|
||||
>>> started.connect(round_two, sender=2)
|
||||
|
||||
>>> for round in range(1, 4):
|
||||
... started.send(round)
|
||||
...
|
||||
Round 1!
|
||||
Round 2!
|
||||
This is round two.
|
||||
Round 3!
|
||||
```
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054
|
||||
blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633
|
||||
blinker-1.9.0.dist-info/RECORD,,
|
||||
blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
||||
blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317
|
||||
blinker/__pycache__/__init__.cpython-312.pyc,,
|
||||
blinker/__pycache__/_utilities.cpython-312.pyc,,
|
||||
blinker/__pycache__/base.cpython-312.pyc,,
|
||||
blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675
|
||||
blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132
|
||||
blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.10.1
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import ANY
|
||||
from .base import default_namespace
|
||||
from .base import NamedSignal
|
||||
from .base import Namespace
|
||||
from .base import Signal
|
||||
from .base import signal
|
||||
|
||||
__all__ = [
|
||||
"ANY",
|
||||
"default_namespace",
|
||||
"NamedSignal",
|
||||
"Namespace",
|
||||
"Signal",
|
||||
"signal",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import inspect
|
||||
import typing as t
|
||||
from weakref import ref
|
||||
from weakref import WeakMethod
|
||||
|
||||
T = t.TypeVar("T")
|
||||
|
||||
|
||||
class Symbol:
|
||||
"""A constant symbol, nicer than ``object()``. Repeated calls return the
|
||||
same instance.
|
||||
|
||||
>>> Symbol('foo') is Symbol('foo')
|
||||
True
|
||||
>>> Symbol('foo')
|
||||
foo
|
||||
"""
|
||||
|
||||
symbols: t.ClassVar[dict[str, Symbol]] = {}
|
||||
|
||||
def __new__(cls, name: str) -> Symbol:
|
||||
if name in cls.symbols:
|
||||
return cls.symbols[name]
|
||||
|
||||
obj = super().__new__(cls)
|
||||
cls.symbols[name] = obj
|
||||
return obj
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __getnewargs__(self) -> tuple[t.Any, ...]:
|
||||
return (self.name,)
|
||||
|
||||
|
||||
def make_id(obj: object) -> c.Hashable:
|
||||
"""Get a stable identifier for a receiver or sender, to be used as a dict
|
||||
key or in a set.
|
||||
"""
|
||||
if inspect.ismethod(obj):
|
||||
# The id of a bound method is not stable, but the id of the unbound
|
||||
# function and instance are.
|
||||
return id(obj.__func__), id(obj.__self__)
|
||||
|
||||
if isinstance(obj, (str, int)):
|
||||
# Instances with the same value always compare equal and have the same
|
||||
# hash, even if the id may change.
|
||||
return obj
|
||||
|
||||
# Assume other types are not hashable but will always be the same instance.
|
||||
return id(obj)
|
||||
|
||||
|
||||
def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]:
|
||||
if inspect.ismethod(obj):
|
||||
return WeakMethod(obj, callback) # type: ignore[arg-type, return-value]
|
||||
|
||||
return ref(obj, callback)
|
||||
@ -0,0 +1,512 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import sys
|
||||
import typing as t
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import cached_property
|
||||
from inspect import iscoroutinefunction
|
||||
|
||||
from ._utilities import make_id
|
||||
from ._utilities import make_ref
|
||||
from ._utilities import Symbol
|
||||
|
||||
F = t.TypeVar("F", bound=c.Callable[..., t.Any])
|
||||
|
||||
ANY = Symbol("ANY")
|
||||
"""Symbol for "any sender"."""
|
||||
|
||||
ANY_ID = 0
|
||||
|
||||
|
||||
class Signal:
|
||||
"""A notification emitter.
|
||||
|
||||
:param doc: The docstring for the signal.
|
||||
"""
|
||||
|
||||
ANY = ANY
|
||||
"""An alias for the :data:`~blinker.ANY` sender symbol."""
|
||||
|
||||
set_class: type[set[t.Any]] = set
|
||||
"""The set class to use for tracking connected receivers and senders.
|
||||
Python's ``set`` is unordered. If receivers must be dispatched in the order
|
||||
they were connected, an ordered set implementation can be used.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def receiver_connected(self) -> Signal:
|
||||
"""Emitted at the end of each :meth:`connect` call.
|
||||
|
||||
The signal sender is the signal instance, and the :meth:`connect`
|
||||
arguments are passed through: ``receiver``, ``sender``, and ``weak``.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
return Signal(doc="Emitted after a receiver connects.")
|
||||
|
||||
@cached_property
|
||||
def receiver_disconnected(self) -> Signal:
|
||||
"""Emitted at the end of each :meth:`disconnect` call.
|
||||
|
||||
The sender is the signal instance, and the :meth:`disconnect` arguments
|
||||
are passed through: ``receiver`` and ``sender``.
|
||||
|
||||
This signal is emitted **only** when :meth:`disconnect` is called
|
||||
explicitly. This signal cannot be emitted by an automatic disconnect
|
||||
when a weakly referenced receiver or sender goes out of scope, as the
|
||||
instance is no longer be available to be used as the sender for this
|
||||
signal.
|
||||
|
||||
An alternative approach is available by subscribing to
|
||||
:attr:`receiver_connected` and setting up a custom weakref cleanup
|
||||
callback on weak receivers and senders.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
return Signal(doc="Emitted after a receiver disconnects.")
|
||||
|
||||
def __init__(self, doc: str | None = None) -> None:
|
||||
if doc:
|
||||
self.__doc__ = doc
|
||||
|
||||
self.receivers: dict[
|
||||
t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]
|
||||
] = {}
|
||||
"""The map of connected receivers. Useful to quickly check if any
|
||||
receivers are connected to the signal: ``if s.receivers:``. The
|
||||
structure and data is not part of the public API, but checking its
|
||||
boolean value is.
|
||||
"""
|
||||
|
||||
self.is_muted: bool = False
|
||||
self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
|
||||
self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
|
||||
self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}
|
||||
|
||||
def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:
|
||||
"""Connect ``receiver`` to be called when the signal is sent by
|
||||
``sender``.
|
||||
|
||||
:param receiver: The callable to call when :meth:`send` is called with
|
||||
the given ``sender``, passing ``sender`` as a positional argument
|
||||
along with any extra keyword arguments.
|
||||
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
|
||||
called when :meth:`send` is called with this sender. If ``ANY``, the
|
||||
receiver will be called for any sender. A receiver may be connected
|
||||
to multiple senders by calling :meth:`connect` multiple times.
|
||||
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
|
||||
be automatically disconnected when it is garbage collected. When
|
||||
connecting a receiver defined within a function, set to ``False``,
|
||||
otherwise it will be disconnected when the function scope ends.
|
||||
"""
|
||||
receiver_id = make_id(receiver)
|
||||
sender_id = ANY_ID if sender is ANY else make_id(sender)
|
||||
|
||||
if weak:
|
||||
self.receivers[receiver_id] = make_ref(
|
||||
receiver, self._make_cleanup_receiver(receiver_id)
|
||||
)
|
||||
else:
|
||||
self.receivers[receiver_id] = receiver
|
||||
|
||||
self._by_sender[sender_id].add(receiver_id)
|
||||
self._by_receiver[receiver_id].add(sender_id)
|
||||
|
||||
if sender is not ANY and sender_id not in self._weak_senders:
|
||||
# store a cleanup for weakref-able senders
|
||||
try:
|
||||
self._weak_senders[sender_id] = make_ref(
|
||||
sender, self._make_cleanup_sender(sender_id)
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
|
||||
try:
|
||||
self.receiver_connected.send(
|
||||
self, receiver=receiver, sender=sender, weak=weak
|
||||
)
|
||||
except TypeError:
|
||||
# TODO no explanation or test for this
|
||||
self.disconnect(receiver, sender)
|
||||
raise
|
||||
|
||||
return receiver
|
||||
|
||||
def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:
|
||||
"""Connect the decorated function to be called when the signal is sent
|
||||
by ``sender``.
|
||||
|
||||
The decorated function will be called when :meth:`send` is called with
|
||||
the given ``sender``, passing ``sender`` as a positional argument along
|
||||
with any extra keyword arguments.
|
||||
|
||||
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
|
||||
called when :meth:`send` is called with this sender. If ``ANY``, the
|
||||
receiver will be called for any sender. A receiver may be connected
|
||||
to multiple senders by calling :meth:`connect` multiple times.
|
||||
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
|
||||
be automatically disconnected when it is garbage collected. When
|
||||
connecting a receiver defined within a function, set to ``False``,
|
||||
otherwise it will be disconnected when the function scope ends.=
|
||||
|
||||
.. versionadded:: 1.1
|
||||
"""
|
||||
|
||||
def decorator(fn: F) -> F:
|
||||
self.connect(fn, sender, weak)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
@contextmanager
|
||||
def connected_to(
|
||||
self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
|
||||
) -> c.Generator[None, None, None]:
|
||||
"""A context manager that temporarily connects ``receiver`` to the
|
||||
signal while a ``with`` block executes. When the block exits, the
|
||||
receiver is disconnected. Useful for tests.
|
||||
|
||||
:param receiver: The callable to call when :meth:`send` is called with
|
||||
the given ``sender``, passing ``sender`` as a positional argument
|
||||
along with any extra keyword arguments.
|
||||
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
|
||||
called when :meth:`send` is called with this sender. If ``ANY``, the
|
||||
receiver will be called for any sender.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
"""
|
||||
self.connect(receiver, sender=sender, weak=False)
|
||||
|
||||
try:
|
||||
yield None
|
||||
finally:
|
||||
self.disconnect(receiver)
|
||||
|
||||
@contextmanager
|
||||
def muted(self) -> c.Generator[None, None, None]:
|
||||
"""A context manager that temporarily disables the signal. No receivers
|
||||
will be called if the signal is sent, until the ``with`` block exits.
|
||||
Useful for tests.
|
||||
"""
|
||||
self.is_muted = True
|
||||
|
||||
try:
|
||||
yield None
|
||||
finally:
|
||||
self.is_muted = False
|
||||
|
||||
def send(
|
||||
self,
|
||||
sender: t.Any | None = None,
|
||||
/,
|
||||
*,
|
||||
_async_wrapper: c.Callable[
|
||||
[c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]
|
||||
]
|
||||
| None = None,
|
||||
**kwargs: t.Any,
|
||||
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
|
||||
"""Call all receivers that are connected to the given ``sender``
|
||||
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
|
||||
argument along with any extra keyword arguments. Return a list of
|
||||
``(receiver, return value)`` tuples.
|
||||
|
||||
The order receivers are called is undefined, but can be influenced by
|
||||
setting :attr:`set_class`.
|
||||
|
||||
If a receiver raises an exception, that exception will propagate up.
|
||||
This makes debugging straightforward, with an assumption that correctly
|
||||
implemented receivers will not raise.
|
||||
|
||||
:param sender: Call receivers connected to this sender, in addition to
|
||||
those connected to :data:`ANY`.
|
||||
:param _async_wrapper: Will be called on any receivers that are async
|
||||
coroutines to turn them into sync callables. For example, could run
|
||||
the receiver with an event loop.
|
||||
:param kwargs: Extra keyword arguments to pass to each receiver.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Added the ``_async_wrapper`` argument.
|
||||
"""
|
||||
if self.is_muted:
|
||||
return []
|
||||
|
||||
results = []
|
||||
|
||||
for receiver in self.receivers_for(sender):
|
||||
if iscoroutinefunction(receiver):
|
||||
if _async_wrapper is None:
|
||||
raise RuntimeError("Cannot send to a coroutine function.")
|
||||
|
||||
result = _async_wrapper(receiver)(sender, **kwargs)
|
||||
else:
|
||||
result = receiver(sender, **kwargs)
|
||||
|
||||
results.append((receiver, result))
|
||||
|
||||
return results
|
||||
|
||||
async def send_async(
|
||||
self,
|
||||
sender: t.Any | None = None,
|
||||
/,
|
||||
*,
|
||||
_sync_wrapper: c.Callable[
|
||||
[c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]
|
||||
]
|
||||
| None = None,
|
||||
**kwargs: t.Any,
|
||||
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
|
||||
"""Await all receivers that are connected to the given ``sender``
|
||||
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
|
||||
argument along with any extra keyword arguments. Return a list of
|
||||
``(receiver, return value)`` tuples.
|
||||
|
||||
The order receivers are called is undefined, but can be influenced by
|
||||
setting :attr:`set_class`.
|
||||
|
||||
If a receiver raises an exception, that exception will propagate up.
|
||||
This makes debugging straightforward, with an assumption that correctly
|
||||
implemented receivers will not raise.
|
||||
|
||||
:param sender: Call receivers connected to this sender, in addition to
|
||||
those connected to :data:`ANY`.
|
||||
:param _sync_wrapper: Will be called on any receivers that are sync
|
||||
callables to turn them into async coroutines. For example,
|
||||
could call the receiver in a thread.
|
||||
:param kwargs: Extra keyword arguments to pass to each receiver.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
if self.is_muted:
|
||||
return []
|
||||
|
||||
results = []
|
||||
|
||||
for receiver in self.receivers_for(sender):
|
||||
if not iscoroutinefunction(receiver):
|
||||
if _sync_wrapper is None:
|
||||
raise RuntimeError("Cannot send to a non-coroutine function.")
|
||||
|
||||
result = await _sync_wrapper(receiver)(sender, **kwargs)
|
||||
else:
|
||||
result = await receiver(sender, **kwargs)
|
||||
|
||||
results.append((receiver, result))
|
||||
|
||||
return results
|
||||
|
||||
def has_receivers_for(self, sender: t.Any) -> bool:
|
||||
"""Check if there is at least one receiver that will be called with the
|
||||
given ``sender``. A receiver connected to :data:`ANY` will always be
|
||||
called, regardless of sender. Does not check if weakly referenced
|
||||
receivers are still live. See :meth:`receivers_for` for a stronger
|
||||
search.
|
||||
|
||||
:param sender: Check for receivers connected to this sender, in addition
|
||||
to those connected to :data:`ANY`.
|
||||
"""
|
||||
if not self.receivers:
|
||||
return False
|
||||
|
||||
if self._by_sender[ANY_ID]:
|
||||
return True
|
||||
|
||||
if sender is ANY:
|
||||
return False
|
||||
|
||||
return make_id(sender) in self._by_sender
|
||||
|
||||
def receivers_for(
|
||||
self, sender: t.Any
|
||||
) -> c.Generator[c.Callable[..., t.Any], None, None]:
|
||||
"""Yield each receiver to be called for ``sender``, in addition to those
|
||||
to be called for :data:`ANY`. Weakly referenced receivers that are not
|
||||
live will be disconnected and skipped.
|
||||
|
||||
:param sender: Yield receivers connected to this sender, in addition
|
||||
to those connected to :data:`ANY`.
|
||||
"""
|
||||
# TODO: test receivers_for(ANY)
|
||||
if not self.receivers:
|
||||
return
|
||||
|
||||
sender_id = make_id(sender)
|
||||
|
||||
if sender_id in self._by_sender:
|
||||
ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
|
||||
else:
|
||||
ids = self._by_sender[ANY_ID].copy()
|
||||
|
||||
for receiver_id in ids:
|
||||
receiver = self.receivers.get(receiver_id)
|
||||
|
||||
if receiver is None:
|
||||
continue
|
||||
|
||||
if isinstance(receiver, weakref.ref):
|
||||
strong = receiver()
|
||||
|
||||
if strong is None:
|
||||
self._disconnect(receiver_id, ANY_ID)
|
||||
continue
|
||||
|
||||
yield strong
|
||||
else:
|
||||
yield receiver
|
||||
|
||||
def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:
|
||||
"""Disconnect ``receiver`` from being called when the signal is sent by
|
||||
``sender``.
|
||||
|
||||
:param receiver: A connected receiver callable.
|
||||
:param sender: Disconnect from only this sender. By default, disconnect
|
||||
from all senders.
|
||||
"""
|
||||
sender_id: c.Hashable
|
||||
|
||||
if sender is ANY:
|
||||
sender_id = ANY_ID
|
||||
else:
|
||||
sender_id = make_id(sender)
|
||||
|
||||
receiver_id = make_id(receiver)
|
||||
self._disconnect(receiver_id, sender_id)
|
||||
|
||||
if (
|
||||
"receiver_disconnected" in self.__dict__
|
||||
and self.receiver_disconnected.receivers
|
||||
):
|
||||
self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
|
||||
|
||||
def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:
|
||||
if sender_id == ANY_ID:
|
||||
if self._by_receiver.pop(receiver_id, None) is not None:
|
||||
for bucket in self._by_sender.values():
|
||||
bucket.discard(receiver_id)
|
||||
|
||||
self.receivers.pop(receiver_id, None)
|
||||
else:
|
||||
self._by_sender[sender_id].discard(receiver_id)
|
||||
self._by_receiver[receiver_id].discard(sender_id)
|
||||
|
||||
def _make_cleanup_receiver(
|
||||
self, receiver_id: c.Hashable
|
||||
) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:
|
||||
"""Create a callback function to disconnect a weakly referenced
|
||||
receiver when it is garbage collected.
|
||||
"""
|
||||
|
||||
def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:
|
||||
# If the interpreter is shutting down, disconnecting can result in a
|
||||
# weird ignored exception. Don't call it in that case.
|
||||
if not sys.is_finalizing():
|
||||
self._disconnect(receiver_id, ANY_ID)
|
||||
|
||||
return cleanup
|
||||
|
||||
def _make_cleanup_sender(
|
||||
self, sender_id: c.Hashable
|
||||
) -> c.Callable[[weakref.ref[t.Any]], None]:
|
||||
"""Create a callback function to disconnect all receivers for a weakly
|
||||
referenced sender when it is garbage collected.
|
||||
"""
|
||||
assert sender_id != ANY_ID
|
||||
|
||||
def cleanup(ref: weakref.ref[t.Any]) -> None:
|
||||
self._weak_senders.pop(sender_id, None)
|
||||
|
||||
for receiver_id in self._by_sender.pop(sender_id, ()):
|
||||
self._by_receiver[receiver_id].discard(sender_id)
|
||||
|
||||
return cleanup
|
||||
|
||||
def _cleanup_bookkeeping(self) -> None:
|
||||
"""Prune unused sender/receiver bookkeeping. Not threadsafe.
|
||||
|
||||
Connecting & disconnecting leaves behind a small amount of bookkeeping
|
||||
data. Typical workloads using Blinker, for example in most web apps,
|
||||
Flask, CLI scripts, etc., are not adversely affected by this
|
||||
bookkeeping.
|
||||
|
||||
With a long-running process performing dynamic signal routing with high
|
||||
volume, e.g. connecting to function closures, senders are all unique
|
||||
object instances. Doing all of this over and over may cause memory usage
|
||||
to grow due to extraneous bookkeeping. (An empty ``set`` for each stale
|
||||
sender/receiver pair.)
|
||||
|
||||
This method will prune that bookkeeping away, with the caveat that such
|
||||
pruning is not threadsafe. The risk is that cleanup of a fully
|
||||
disconnected receiver/sender pair occurs while another thread is
|
||||
connecting that same pair. If you are in the highly dynamic, unique
|
||||
receiver/sender situation that has lead you to this method, that failure
|
||||
mode is perhaps not a big deal for you.
|
||||
"""
|
||||
for mapping in (self._by_sender, self._by_receiver):
|
||||
for ident, bucket in list(mapping.items()):
|
||||
if not bucket:
|
||||
mapping.pop(ident, None)
|
||||
|
||||
def _clear_state(self) -> None:
|
||||
"""Disconnect all receivers and senders. Useful for tests."""
|
||||
self._weak_senders.clear()
|
||||
self.receivers.clear()
|
||||
self._by_sender.clear()
|
||||
self._by_receiver.clear()
|
||||
|
||||
|
||||
class NamedSignal(Signal):
|
||||
"""A named generic notification emitter. The name is not used by the signal
|
||||
itself, but matches the key in the :class:`Namespace` that it belongs to.
|
||||
|
||||
:param name: The name of the signal within the namespace.
|
||||
:param doc: The docstring for the signal.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, doc: str | None = None) -> None:
|
||||
super().__init__(doc)
|
||||
|
||||
#: The name of this signal.
|
||||
self.name: str = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
base = super().__repr__()
|
||||
return f"{base[:-1]}; {self.name!r}>" # noqa: E702
|
||||
|
||||
|
||||
class Namespace(dict[str, NamedSignal]):
|
||||
"""A dict mapping names to signals."""
|
||||
|
||||
def signal(self, name: str, doc: str | None = None) -> NamedSignal:
|
||||
"""Return the :class:`NamedSignal` for the given ``name``, creating it
|
||||
if required. Repeated calls with the same name return the same signal.
|
||||
|
||||
:param name: The name of the signal.
|
||||
:param doc: The docstring of the signal.
|
||||
"""
|
||||
if name not in self:
|
||||
self[name] = NamedSignal(name, doc)
|
||||
|
||||
return self[name]
|
||||
|
||||
|
||||
class _PNamespaceSignal(t.Protocol):
|
||||
def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...
|
||||
|
||||
|
||||
default_namespace: Namespace = Namespace()
|
||||
"""A default :class:`Namespace` for creating named signals. :func:`signal`
|
||||
creates a :class:`NamedSignal` in this namespace.
|
||||
"""
|
||||
|
||||
signal: _PNamespaceSignal = default_namespace.signal
|
||||
"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given
|
||||
``name``, creating it if required. Repeated calls with the same name return the
|
||||
same signal.
|
||||
"""
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,28 @@
|
||||
Copyright 2018 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,64 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: cachelib
|
||||
Version: 0.13.0
|
||||
Summary: A collection of cache libraries in the same API interface.
|
||||
Home-page: https://github.com/pallets-eco/cachelib/
|
||||
Maintainer: Pallets
|
||||
Maintainer-email: contact@palletsprojects.com
|
||||
License: BSD-3-Clause
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Documentation, https://cachelib.readthedocs.io/
|
||||
Project-URL: Changes, https://cachelib.readthedocs.io/changes/
|
||||
Project-URL: Source Code, https://github.com/pallets-eco/cachelib/
|
||||
Project-URL: Issue Tracker, https://github.com/pallets-eco/cachelib/issues/
|
||||
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE.rst
|
||||
|
||||
CacheLib
|
||||
========
|
||||
|
||||
A collection of cache libraries in the same API interface. Extracted
|
||||
from Werkzeug.
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install and update using `pip`_:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install -U cachelib
|
||||
|
||||
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||
|
||||
|
||||
Donate
|
||||
------
|
||||
|
||||
The Pallets organization develops and supports Flask and the libraries
|
||||
it uses. In order to grow the community of contributors and users, and
|
||||
allow the maintainers to devote more time to the projects, `please
|
||||
donate today`_.
|
||||
|
||||
.. _please donate today: https://palletsprojects.com/donate
|
||||
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
- Documentation: https://cachelib.readthedocs.io/
|
||||
- Changes: https://cachelib.readthedocs.io/changes/
|
||||
- PyPI Releases: https://pypi.org/project/cachelib/
|
||||
- Source Code: https://github.com/pallets/cachelib/
|
||||
- Issue Tracker: https://github.com/pallets/cachelib/issues/
|
||||
- Twitter: https://twitter.com/PalletsTeam
|
||||
- Chat: https://discord.gg/pallets
|
||||
@ -0,0 +1,27 @@
|
||||
cachelib-0.13.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
cachelib-0.13.0.dist-info/LICENSE.rst,sha256=zUGBIIEtwmJiga4CfoG2SCKdFmtaynRyzs1RADjTbn0,1475
|
||||
cachelib-0.13.0.dist-info/METADATA,sha256=lqDsbA03sUETX-dGoDR1uDqqXtyu1m0sS0wBjCDEeXE,1960
|
||||
cachelib-0.13.0.dist-info/RECORD,,
|
||||
cachelib-0.13.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
||||
cachelib-0.13.0.dist-info/top_level.txt,sha256=AYC4q8wgGd_hR_F2YcDkmtQm41gv9-5AThKuQtNPEXk,9
|
||||
cachelib/__init__.py,sha256=xTX_F13-FA58DjBGip4XZeCSlEo6B4c4VTRi49MBywY,575
|
||||
cachelib/__pycache__/__init__.cpython-312.pyc,,
|
||||
cachelib/__pycache__/base.cpython-312.pyc,,
|
||||
cachelib/__pycache__/dynamodb.cpython-312.pyc,,
|
||||
cachelib/__pycache__/file.cpython-312.pyc,,
|
||||
cachelib/__pycache__/memcached.cpython-312.pyc,,
|
||||
cachelib/__pycache__/mongodb.cpython-312.pyc,,
|
||||
cachelib/__pycache__/redis.cpython-312.pyc,,
|
||||
cachelib/__pycache__/serializers.cpython-312.pyc,,
|
||||
cachelib/__pycache__/simple.cpython-312.pyc,,
|
||||
cachelib/__pycache__/uwsgi.cpython-312.pyc,,
|
||||
cachelib/base.py,sha256=3_B-cB1VEh_x-VzH9g3qvzdqCxDX2ywDzQ7a_aYFJlE,6731
|
||||
cachelib/dynamodb.py,sha256=fSmp8G7V0yBcRC2scdIhz8d0D2-9OMZEwQ9AcBONyC8,8512
|
||||
cachelib/file.py,sha256=xu6m7nzTMcca_wYS0oM4iOxLarQHZFrhUnQnFYansy4,12270
|
||||
cachelib/memcached.py,sha256=KyUN4wblVPf2XNLYk15kwN9QTfkFK6jrpVGrj4NAoFA,7160
|
||||
cachelib/mongodb.py,sha256=b9l8fTKMFm8hAXFn748GKertHUASSVDBgPgrtGaZ6cA,6901
|
||||
cachelib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cachelib/redis.py,sha256=hSKV9fVD7gzk1X_B3Ac9XJYN3G3F6eYBoreqDo9pces,6295
|
||||
cachelib/serializers.py,sha256=MXk1moN6ljOPUFQ0E0D129mZlrDDwuQ5DvInNkitvoI,3343
|
||||
cachelib/simple.py,sha256=8UPp95_oc3bLeW_gzFUzdppIKV8hV_o_yQYoKb8gVMk,3422
|
||||
cachelib/uwsgi.py,sha256=4DX3C9QGvB6mVcg1d7qpLIEkI6bccuq-8M6I_YbPicY,2563
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.42.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -0,0 +1 @@
|
||||
cachelib
|
||||
@ -0,0 +1,22 @@
|
||||
from cachelib.base import BaseCache
|
||||
from cachelib.base import NullCache
|
||||
from cachelib.dynamodb import DynamoDbCache
|
||||
from cachelib.file import FileSystemCache
|
||||
from cachelib.memcached import MemcachedCache
|
||||
from cachelib.mongodb import MongoDbCache
|
||||
from cachelib.redis import RedisCache
|
||||
from cachelib.simple import SimpleCache
|
||||
from cachelib.uwsgi import UWSGICache
|
||||
|
||||
__all__ = [
|
||||
"BaseCache",
|
||||
"NullCache",
|
||||
"SimpleCache",
|
||||
"FileSystemCache",
|
||||
"MemcachedCache",
|
||||
"RedisCache",
|
||||
"UWSGICache",
|
||||
"DynamoDbCache",
|
||||
"MongoDbCache",
|
||||
]
|
||||
__version__ = "0.13.0"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue