first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -0,0 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .run import cli_run, session_via_cli
|
||||
from .version import __version__
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"cli_run",
|
||||
"session_via_cli",
|
||||
]
|
|
@ -0,0 +1,70 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from timeit import default_timer
|
||||
|
||||
|
||||
def run(args=None, options=None, env=None):
|
||||
env = os.environ if env is None else env
|
||||
start = default_timer()
|
||||
from virtualenv.run import cli_run # noqa: PLC0415
|
||||
from virtualenv.util.error import ProcessCallFailedError # noqa: PLC0415
|
||||
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
try:
|
||||
session = cli_run(args, options, env)
|
||||
logging.warning(LogSession(session, start))
|
||||
except ProcessCallFailedError as exception:
|
||||
print(f"subprocess call failed for {exception.cmd} with code {exception.code}") # noqa: T201
|
||||
print(exception.out, file=sys.stdout, end="") # noqa: T201
|
||||
print(exception.err, file=sys.stderr, end="") # noqa: T201
|
||||
raise SystemExit(exception.code) # noqa: B904
|
||||
|
||||
|
||||
class LogSession:
|
||||
def __init__(self, session, start) -> None:
|
||||
self.session = session
|
||||
self.start = start
|
||||
|
||||
def __str__(self) -> str:
|
||||
spec = self.session.creator.interpreter.spec
|
||||
elapsed = (default_timer() - self.start) * 1000
|
||||
lines = [
|
||||
f"created virtual environment {spec} in {elapsed:.0f}ms",
|
||||
f" creator {self.session.creator!s}",
|
||||
]
|
||||
if self.session.seeder.enabled:
|
||||
lines.append(f" seeder {self.session.seeder!s}")
|
||||
path = self.session.creator.purelib.iterdir()
|
||||
packages = sorted("==".join(i.stem.split("-")) for i in path if i.suffix == ".dist-info")
|
||||
lines.append(f" added seed packages: {', '.join(packages)}")
|
||||
|
||||
if self.session.activators:
|
||||
lines.append(f" activators {','.join(i.__class__.__name__ for i in self.session.activators)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def run_with_catch(args=None, env=None):
|
||||
from virtualenv.config.cli.parser import VirtualEnvOptions # noqa: PLC0415
|
||||
|
||||
env = os.environ if env is None else env
|
||||
options = VirtualEnvOptions()
|
||||
try:
|
||||
run(args, options, env)
|
||||
except (KeyboardInterrupt, SystemExit, Exception) as exception:
|
||||
try:
|
||||
if getattr(options, "with_traceback", False):
|
||||
raise
|
||||
if not (isinstance(exception, SystemExit) and exception.code == 0):
|
||||
logging.error("%s: %s", type(exception).__name__, exception) # noqa: TRY400
|
||||
code = exception.code if isinstance(exception, SystemExit) else 1
|
||||
sys.exit(code)
|
||||
finally:
|
||||
logging.shutdown() # force flush of log messages before the trace is printed
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cov
|
||||
run_with_catch() # pragma: no cov
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .bash import BashActivator
|
||||
from .batch import BatchActivator
|
||||
from .cshell import CShellActivator
|
||||
from .fish import FishActivator
|
||||
from .nushell import NushellActivator
|
||||
from .powershell import PowerShellActivator
|
||||
from .python import PythonActivator
|
||||
|
||||
__all__ = [
|
||||
"BashActivator",
|
||||
"BatchActivator",
|
||||
"CShellActivator",
|
||||
"FishActivator",
|
||||
"NushellActivator",
|
||||
"PowerShellActivator",
|
||||
"PythonActivator",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,50 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class Activator(ABC):
|
||||
"""Generates activate script for the virtual environment."""
|
||||
|
||||
def __init__(self, options) -> None:
|
||||
"""
|
||||
Create a new activator generator.
|
||||
|
||||
:param options: the parsed options as defined within :meth:`add_parser_arguments`
|
||||
"""
|
||||
self.flag_prompt = os.path.basename(os.getcwd()) if options.prompt == "." else options.prompt
|
||||
|
||||
@classmethod
|
||||
def supports(cls, interpreter): # noqa: ARG003
|
||||
"""
|
||||
Check if the activation script is supported in the given interpreter.
|
||||
|
||||
:param interpreter: the interpreter we need to support
|
||||
:return: ``True`` if supported, ``False`` otherwise
|
||||
"""
|
||||
return True
|
||||
|
||||
@classmethod # noqa: B027
|
||||
def add_parser_arguments(cls, parser, interpreter):
|
||||
"""
|
||||
Add CLI arguments for this activation script.
|
||||
|
||||
:param parser: the CLI parser
|
||||
:param interpreter: the interpreter this virtual environment is based of
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def generate(self, creator):
|
||||
"""
|
||||
Generate activate script for the given creator.
|
||||
|
||||
:param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this \
|
||||
virtual environment
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Activator",
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class BashActivator(ViaTemplateActivator):
|
||||
def templates(self):
|
||||
yield "activate.sh"
|
||||
|
||||
def as_name(self, template):
|
||||
return Path(template).stem
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BashActivator",
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,87 @@
|
|||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
|
||||
if [ "${BASH_SOURCE-}" = "$0" ]; then
|
||||
echo "You must source this script: \$ source $0" >&2
|
||||
exit 33
|
||||
fi
|
||||
|
||||
deactivate () {
|
||||
unset -f pydoc >/dev/null 2>&1 || true
|
||||
|
||||
# reset old environment variables
|
||||
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
|
||||
if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then
|
||||
PATH="$_OLD_VIRTUAL_PATH"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
|
||||
PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# The hash command must be called to get it to forget past
|
||||
# commands. Without forgetting past commands the $PATH changes
|
||||
# we made may not be respected
|
||||
hash -r 2>/dev/null
|
||||
|
||||
if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
|
||||
PS1="$_OLD_VIRTUAL_PS1"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV='__VIRTUAL_ENV__'
|
||||
if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then
|
||||
VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV")
|
||||
fi
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/__BIN_NAME__:$PATH"
|
||||
export PATH
|
||||
|
||||
if [ "x__VIRTUAL_PROMPT__" != x ] ; then
|
||||
VIRTUAL_ENV_PROMPT="__VIRTUAL_PROMPT__"
|
||||
else
|
||||
VIRTUAL_ENV_PROMPT=$(basename "$VIRTUAL_ENV")
|
||||
fi
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
if ! [ -z "${PYTHONHOME+_}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1-}"
|
||||
PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}"
|
||||
export PS1
|
||||
fi
|
||||
|
||||
# Make sure to unalias pydoc if it's already there
|
||||
alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true
|
||||
|
||||
pydoc () {
|
||||
python -m pydoc "$@"
|
||||
}
|
||||
|
||||
# The hash command must be called to get it to forget past
|
||||
# commands. Without forgetting past commands the $PATH changes
|
||||
# we made may not be respected
|
||||
hash -r 2>/dev/null || true
|
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class BatchActivator(ViaTemplateActivator):
|
||||
@classmethod
|
||||
def supports(cls, interpreter):
|
||||
return interpreter.os == "nt"
|
||||
|
||||
def templates(self):
|
||||
yield "activate.bat"
|
||||
yield "deactivate.bat"
|
||||
yield "pydoc.bat"
|
||||
|
||||
def instantiate_template(self, replacements, template, creator):
|
||||
# ensure the text has all newlines as \r\n - required by batch
|
||||
base = super().instantiate_template(replacements, template, creator)
|
||||
return base.replace(os.linesep, "\n").replace("\n", os.linesep)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BatchActivator",
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,52 @@
|
|||
@REM This file is UTF-8 encoded, so we need to update the current code page while executing it
|
||||
@echo off
|
||||
@for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do (
|
||||
@set _OLD_CODEPAGE=%%a
|
||||
)
|
||||
@if defined _OLD_CODEPAGE (
|
||||
"%SystemRoot%\System32\chcp.com" 65001 > nul
|
||||
)
|
||||
|
||||
@set "VIRTUAL_ENV=__VIRTUAL_ENV__"
|
||||
|
||||
@set "VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__"
|
||||
@if NOT DEFINED VIRTUAL_ENV_PROMPT (
|
||||
@for %%d in ("%VIRTUAL_ENV%") do @set "VIRTUAL_ENV_PROMPT=%%~nxd"
|
||||
)
|
||||
|
||||
@if defined _OLD_VIRTUAL_PROMPT (
|
||||
@set "PROMPT=%_OLD_VIRTUAL_PROMPT%"
|
||||
) else (
|
||||
@if not defined PROMPT (
|
||||
@set "PROMPT=$P$G"
|
||||
)
|
||||
@if not defined VIRTUAL_ENV_DISABLE_PROMPT (
|
||||
@set "_OLD_VIRTUAL_PROMPT=%PROMPT%"
|
||||
)
|
||||
)
|
||||
@if not defined VIRTUAL_ENV_DISABLE_PROMPT (
|
||||
@set "PROMPT=(%VIRTUAL_ENV_PROMPT%) %PROMPT%"
|
||||
)
|
||||
|
||||
@REM Don't use () to avoid problems with them in %PATH%
|
||||
@if defined _OLD_VIRTUAL_PYTHONHOME @goto ENDIFVHOME
|
||||
@set "_OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%"
|
||||
:ENDIFVHOME
|
||||
|
||||
@set PYTHONHOME=
|
||||
|
||||
@REM if defined _OLD_VIRTUAL_PATH (
|
||||
@if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH1
|
||||
@set "PATH=%_OLD_VIRTUAL_PATH%"
|
||||
:ENDIFVPATH1
|
||||
@REM ) else (
|
||||
@if defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH2
|
||||
@set "_OLD_VIRTUAL_PATH=%PATH%"
|
||||
:ENDIFVPATH2
|
||||
|
||||
@set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%"
|
||||
|
||||
@if defined _OLD_CODEPAGE (
|
||||
"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
|
||||
@set _OLD_CODEPAGE=
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
@set VIRTUAL_ENV=
|
||||
@set VIRTUAL_ENV_PROMPT=
|
||||
|
||||
@REM Don't use () to avoid problems with them in %PATH%
|
||||
@if not defined _OLD_VIRTUAL_PROMPT @goto ENDIFVPROMPT
|
||||
@set "PROMPT=%_OLD_VIRTUAL_PROMPT%"
|
||||
@set _OLD_VIRTUAL_PROMPT=
|
||||
:ENDIFVPROMPT
|
||||
|
||||
@if not defined _OLD_VIRTUAL_PYTHONHOME @goto ENDIFVHOME
|
||||
@set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%"
|
||||
@set _OLD_VIRTUAL_PYTHONHOME=
|
||||
:ENDIFVHOME
|
||||
|
||||
@if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH
|
||||
@set "PATH=%_OLD_VIRTUAL_PATH%"
|
||||
@set _OLD_VIRTUAL_PATH=
|
||||
:ENDIFVPATH
|
|
@ -0,0 +1 @@
|
|||
python.exe -m pydoc %*
|
|
@ -0,0 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class CShellActivator(ViaTemplateActivator):
|
||||
@classmethod
|
||||
def supports(cls, interpreter):
|
||||
return interpreter.os != "nt"
|
||||
|
||||
def templates(self):
|
||||
yield "activate.csh"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CShellActivator",
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,55 @@
|
|||
# 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>.
|
||||
|
||||
set newline='\
|
||||
'
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV '__VIRTUAL_ENV__'
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH:q"
|
||||
setenv PATH "$VIRTUAL_ENV:q/__BIN_NAME__:$PATH:q"
|
||||
|
||||
|
||||
|
||||
if ('__VIRTUAL_PROMPT__' != "") then
|
||||
setenv VIRTUAL_ENV_PROMPT '__VIRTUAL_PROMPT__'
|
||||
else
|
||||
setenv VIRTUAL_ENV_PROMPT "$VIRTUAL_ENV:t:q"
|
||||
endif
|
||||
|
||||
if ( $?VIRTUAL_ENV_DISABLE_PROMPT ) then
|
||||
if ( $VIRTUAL_ENV_DISABLE_PROMPT == "" ) then
|
||||
set do_prompt = "1"
|
||||
else
|
||||
set do_prompt = "0"
|
||||
endif
|
||||
else
|
||||
set do_prompt = "1"
|
||||
endif
|
||||
|
||||
if ( $do_prompt == "1" ) then
|
||||
# Could be in a non-interactive environment,
|
||||
# in which case, $prompt is undefined and we wouldn't
|
||||
# care about the prompt anyway.
|
||||
if ( $?prompt ) then
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt:q"
|
||||
if ( "$prompt:q" =~ *"$newline:q"* ) then
|
||||
:
|
||||
else
|
||||
set prompt = '('"$VIRTUAL_ENV_PROMPT:q"') '"$prompt:q"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
unset env_name
|
||||
unset do_prompt
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
|
@ -0,0 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class FishActivator(ViaTemplateActivator):
|
||||
def templates(self):
|
||||
yield "activate.fish"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FishActivator",
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,103 @@
|
|||
# This file must be used using `source bin/activate.fish` *within a running fish ( http://fishshell.com ) session*.
|
||||
# Do not run it directly.
|
||||
|
||||
function _bashify_path -d "Converts a fish path to something bash can recognize"
|
||||
set fishy_path $argv
|
||||
set bashy_path $fishy_path[1]
|
||||
for path_part in $fishy_path[2..-1]
|
||||
set bashy_path "$bashy_path:$path_part"
|
||||
end
|
||||
echo $bashy_path
|
||||
end
|
||||
|
||||
function _fishify_path -d "Converts a bash path to something fish can recognize"
|
||||
echo $argv | tr ':' '\n'
|
||||
end
|
||||
|
||||
function deactivate -d 'Exit virtualenv mode and return to the normal environment.'
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
|
||||
if test (echo $FISH_VERSION | head -c 1) -lt 3
|
||||
set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH")
|
||||
else
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
end
|
||||
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"
|
||||
and functions -q _old_fish_prompt
|
||||
# Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`.
|
||||
set -l fish_function_path
|
||||
|
||||
# Erase virtualenv's `fish_prompt` and restore the original.
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
|
||||
if test "$argv[1]" != 'nondestructive'
|
||||
# Self-destruct!
|
||||
functions -e pydoc
|
||||
functions -e deactivate
|
||||
functions -e _bashify_path
|
||||
functions -e _fishify_path
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV '__VIRTUAL_ENV__'
|
||||
|
||||
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
|
||||
if test (echo $FISH_VERSION | head -c 1) -lt 3
|
||||
set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH)
|
||||
else
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
end
|
||||
set -gx PATH "$VIRTUAL_ENV"'/__BIN_NAME__' $PATH
|
||||
|
||||
# Prompt override provided?
|
||||
# If not, just use the environment name.
|
||||
if test -n '__VIRTUAL_PROMPT__'
|
||||
set -gx VIRTUAL_ENV_PROMPT '__VIRTUAL_PROMPT__'
|
||||
else
|
||||
set -gx VIRTUAL_ENV_PROMPT (basename "$VIRTUAL_ENV")
|
||||
end
|
||||
|
||||
# Unset `$PYTHONHOME` if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
function pydoc
|
||||
python -m pydoc $argv
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# Copy the current `fish_prompt` function as `_old_fish_prompt`.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
function fish_prompt
|
||||
# Run the user's prompt first; it might depend on (pipe)status.
|
||||
set -l prompt (_old_fish_prompt)
|
||||
|
||||
printf '(%s) ' $VIRTUAL_ENV_PROMPT
|
||||
|
||||
string join -- \n $prompt # handle multi-line prompts
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class NushellActivator(ViaTemplateActivator):
|
||||
def templates(self):
|
||||
yield "activate.nu"
|
||||
|
||||
def replacements(self, creator, dest_folder): # noqa: ARG002
|
||||
return {
|
||||
"__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt,
|
||||
"__VIRTUAL_ENV__": str(creator.dest),
|
||||
"__VIRTUAL_NAME__": creator.env_name,
|
||||
"__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NushellActivator",
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,96 @@
|
|||
# virtualenv activation module
|
||||
# Activate with `overlay use activate.nu`
|
||||
# Deactivate with `deactivate`, as usual
|
||||
#
|
||||
# To customize the overlay name, you can call `overlay use activate.nu as foo`,
|
||||
# but then simply `deactivate` won't work because it is just an alias to hide
|
||||
# the "activate" overlay. You'd need to call `overlay hide foo` manually.
|
||||
|
||||
export-env {
|
||||
def is-string [x] {
|
||||
($x | describe) == 'string'
|
||||
}
|
||||
|
||||
def has-env [...names] {
|
||||
$names | each {|n|
|
||||
$n in $env
|
||||
} | all {|i| $i == true}
|
||||
}
|
||||
|
||||
# Emulates a `test -z`, but better as it handles e.g 'false'
|
||||
def is-env-true [name: string] {
|
||||
if (has-env $name) {
|
||||
# Try to parse 'true', '0', '1', and fail if not convertible
|
||||
let parsed = (do -i { $env | get $name | into bool })
|
||||
if ($parsed | describe) == 'bool' {
|
||||
$parsed
|
||||
} else {
|
||||
not ($env | get -i $name | is-empty)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
let virtual_env = '__VIRTUAL_ENV__'
|
||||
let bin = '__BIN_NAME__'
|
||||
|
||||
let is_windows = ($nu.os-info.family) == 'windows'
|
||||
let path_name = (if (has-env 'Path') {
|
||||
'Path'
|
||||
} else {
|
||||
'PATH'
|
||||
}
|
||||
)
|
||||
|
||||
let venv_path = ([$virtual_env $bin] | path join)
|
||||
let new_path = ($env | get $path_name | prepend $venv_path)
|
||||
|
||||
# If there is no default prompt, then use the env name instead
|
||||
let virtual_env_prompt = (if ('__VIRTUAL_PROMPT__' | is-empty) {
|
||||
($virtual_env | path basename)
|
||||
} else {
|
||||
'__VIRTUAL_PROMPT__'
|
||||
})
|
||||
|
||||
let new_env = {
|
||||
$path_name : $new_path
|
||||
VIRTUAL_ENV : $virtual_env
|
||||
VIRTUAL_ENV_PROMPT : $virtual_env_prompt
|
||||
}
|
||||
|
||||
let new_env = (if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') {
|
||||
$new_env
|
||||
} else {
|
||||
# Creating the new prompt for the session
|
||||
let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) '
|
||||
|
||||
# Back up the old prompt builder
|
||||
let old_prompt_command = (if (has-env 'PROMPT_COMMAND') {
|
||||
$env.PROMPT_COMMAND
|
||||
} else {
|
||||
''
|
||||
})
|
||||
|
||||
let new_prompt = (if (has-env 'PROMPT_COMMAND') {
|
||||
if 'closure' in ($old_prompt_command | describe) {
|
||||
{|| $'($virtual_prefix)(do $old_prompt_command)' }
|
||||
} else {
|
||||
{|| $'($virtual_prefix)($old_prompt_command)' }
|
||||
}
|
||||
} else {
|
||||
{|| $'($virtual_prefix)' }
|
||||
})
|
||||
|
||||
$new_env | merge {
|
||||
PROMPT_COMMAND : $new_prompt
|
||||
VIRTUAL_PREFIX : $virtual_prefix
|
||||
}
|
||||
})
|
||||
|
||||
# Environment variables that will be loaded as the virtual env
|
||||
load-env $new_env
|
||||
}
|
||||
|
||||
export alias pydoc = python -m pydoc
|
||||
export alias deactivate = overlay hide activate
|
|
@ -0,0 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class PowerShellActivator(ViaTemplateActivator):
|
||||
def templates(self):
|
||||
yield "activate.ps1"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PowerShellActivator",
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,61 @@
|
|||
$script:THIS_PATH = $myinvocation.mycommand.path
|
||||
$script:BASE_DIR = Split-Path (Resolve-Path "$THIS_PATH/..") -Parent
|
||||
|
||||
function global:deactivate([switch] $NonDestructive) {
|
||||
if (Test-Path variable:_OLD_VIRTUAL_PATH) {
|
||||
$env:PATH = $variable:_OLD_VIRTUAL_PATH
|
||||
Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global
|
||||
}
|
||||
|
||||
if (Test-Path function:_old_virtual_prompt) {
|
||||
$function:prompt = $function:_old_virtual_prompt
|
||||
Remove-Item function:\_old_virtual_prompt
|
||||
}
|
||||
|
||||
if ($env:VIRTUAL_ENV) {
|
||||
Remove-Item env:VIRTUAL_ENV -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item env:VIRTUAL_ENV_PROMPT -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (!$NonDestructive) {
|
||||
# Self destruct!
|
||||
Remove-Item function:deactivate
|
||||
Remove-Item function:pydoc
|
||||
}
|
||||
}
|
||||
|
||||
function global:pydoc {
|
||||
python -m pydoc $args
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate -nondestructive
|
||||
|
||||
$VIRTUAL_ENV = $BASE_DIR
|
||||
$env:VIRTUAL_ENV = $VIRTUAL_ENV
|
||||
|
||||
if ("__VIRTUAL_PROMPT__" -ne "") {
|
||||
$env:VIRTUAL_ENV_PROMPT = "__VIRTUAL_PROMPT__"
|
||||
}
|
||||
else {
|
||||
$env:VIRTUAL_ENV_PROMPT = $( Split-Path $env:VIRTUAL_ENV -Leaf )
|
||||
}
|
||||
|
||||
New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH
|
||||
|
||||
$env:PATH = "$env:VIRTUAL_ENV/__BIN_NAME____PATH_SEP__" + $env:PATH
|
||||
if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
function global:_old_virtual_prompt {
|
||||
""
|
||||
}
|
||||
$function:_old_virtual_prompt = $function:prompt
|
||||
|
||||
function global:prompt {
|
||||
# Add the custom prefix to the existing prompt
|
||||
$previous_prompt_value = & $function:_old_virtual_prompt
|
||||
("(" + $env:VIRTUAL_ENV_PROMPT + ") " + $previous_prompt_value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
|
||||
|
||||
class PythonActivator(ViaTemplateActivator):
|
||||
def templates(self):
|
||||
yield "activate_this.py"
|
||||
|
||||
def replacements(self, creator, dest_folder):
|
||||
replacements = super().replacements(creator, dest_folder)
|
||||
lib_folders = OrderedDict((os.path.relpath(str(i), str(dest_folder)), None) for i in creator.libs)
|
||||
lib_folders = os.pathsep.join(lib_folders.keys()).replace("\\", "\\\\") # escape Windows path characters
|
||||
replacements.update(
|
||||
{
|
||||
"__LIB_FOLDERS__": lib_folders,
|
||||
"__DECODE_PATH__": "",
|
||||
},
|
||||
)
|
||||
return replacements
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PythonActivator",
|
||||
]
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
Activate virtualenv for current interpreter:
|
||||
|
||||
import runpy
|
||||
runpy.run_path(this_file)
|
||||
|
||||
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
|
||||
""" # noqa: D415
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import site
|
||||
import sys
|
||||
|
||||
try:
|
||||
abs_file = os.path.abspath(__file__)
|
||||
except NameError as exc:
|
||||
msg = "You must use import runpy; runpy.run_path(this_file)"
|
||||
raise AssertionError(msg) from exc
|
||||
|
||||
bin_dir = os.path.dirname(abs_file)
|
||||
base = bin_dir[: -len("__BIN_NAME__") - 1] # strip away the bin part from the __file__, plus the path separator
|
||||
|
||||
# prepend bin to PATH (this file is inside the bin directory)
|
||||
os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)])
|
||||
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
||||
os.environ["VIRTUAL_ENV_PROMPT"] = "__VIRTUAL_PROMPT__" or os.path.basename(base) # noqa: SIM222
|
||||
|
||||
# add the virtual environments libraries to the host python import mechanism
|
||||
prev_length = len(sys.path)
|
||||
for lib in "__LIB_FOLDERS__".split(os.pathsep):
|
||||
path = os.path.realpath(os.path.join(bin_dir, lib))
|
||||
site.addsitedir(path.decode("utf-8") if "__DECODE_PATH__" else path)
|
||||
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
||||
|
||||
sys.real_prefix = sys.prefix
|
||||
sys.prefix = base
|
|
@ -0,0 +1,76 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .activator import Activator
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from importlib.resources import files
|
||||
|
||||
def read_binary(module_name: str, filename: str) -> bytes:
|
||||
return (files(module_name) / filename).read_bytes()
|
||||
|
||||
else:
|
||||
from importlib.resources import read_binary
|
||||
|
||||
|
||||
class ViaTemplateActivator(Activator, ABC):
|
||||
@abstractmethod
|
||||
def templates(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def generate(self, creator):
|
||||
dest_folder = creator.bin_dir
|
||||
replacements = self.replacements(creator, dest_folder)
|
||||
generated = self._generate(replacements, self.templates(), dest_folder, creator)
|
||||
if self.flag_prompt is not None:
|
||||
creator.pyenv_cfg["prompt"] = self.flag_prompt
|
||||
return generated
|
||||
|
||||
def replacements(self, creator, dest_folder): # noqa: ARG002
|
||||
return {
|
||||
"__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt,
|
||||
"__VIRTUAL_ENV__": str(creator.dest),
|
||||
"__VIRTUAL_NAME__": creator.env_name,
|
||||
"__BIN_NAME__": str(creator.bin_dir.relative_to(creator.dest)),
|
||||
"__PATH_SEP__": os.pathsep,
|
||||
}
|
||||
|
||||
def _generate(self, replacements, templates, to_folder, creator):
|
||||
generated = []
|
||||
for template in templates:
|
||||
text = self.instantiate_template(replacements, template, creator)
|
||||
dest = to_folder / self.as_name(template)
|
||||
# remove the file if it already exists - this prevents permission
|
||||
# errors when the dest is not writable
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
# Powershell assumes Windows 1252 encoding when reading files without BOM
|
||||
encoding = "utf-8-sig" if str(template).endswith(".ps1") else "utf-8"
|
||||
# use write_bytes to avoid platform specific line normalization (\n -> \r\n)
|
||||
dest.write_bytes(text.encode(encoding))
|
||||
generated.append(dest)
|
||||
return generated
|
||||
|
||||
def as_name(self, template):
|
||||
return template
|
||||
|
||||
def instantiate_template(self, replacements, template, creator):
|
||||
# read content as binary to avoid platform specific line normalization (\n -> \r\n)
|
||||
binary = read_binary(self.__module__, template)
|
||||
text = binary.decode("utf-8", errors="strict")
|
||||
for key, value in replacements.items():
|
||||
value_uni = self._repr_unicode(creator, value)
|
||||
text = text.replace(key, value_uni)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def _repr_unicode(creator, value): # noqa: ARG004
|
||||
return value # by default, we just let it be unicode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ViaTemplateActivator",
|
||||
]
|
|
@ -0,0 +1,56 @@
|
|||
"""Application data stored by virtualenv."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from platformdirs import user_data_dir
|
||||
|
||||
from .na import AppDataDisabled
|
||||
from .read_only import ReadOnlyAppData
|
||||
from .via_disk_folder import AppDataDiskFolder
|
||||
from .via_tempdir import TempAppData
|
||||
|
||||
|
||||
def _default_app_data_dir(env):
|
||||
key = "VIRTUALENV_OVERRIDE_APP_DATA"
|
||||
if key in env:
|
||||
return env[key]
|
||||
return user_data_dir(appname="virtualenv", appauthor="pypa")
|
||||
|
||||
|
||||
def make_app_data(folder, **kwargs):
|
||||
is_read_only = kwargs.pop("read_only")
|
||||
env = kwargs.pop("env")
|
||||
if kwargs: # py3+ kwonly
|
||||
msg = "unexpected keywords: {}"
|
||||
raise TypeError(msg)
|
||||
|
||||
if folder is None:
|
||||
folder = _default_app_data_dir(env)
|
||||
folder = os.path.abspath(folder)
|
||||
|
||||
if is_read_only:
|
||||
return ReadOnlyAppData(folder)
|
||||
|
||||
if not os.path.isdir(folder):
|
||||
try:
|
||||
os.makedirs(folder)
|
||||
logging.debug("created app data folder %s", folder)
|
||||
except OSError as exception:
|
||||
logging.info("could not create app data folder %s due to %r", folder, exception)
|
||||
|
||||
if os.access(folder, os.W_OK):
|
||||
return AppDataDiskFolder(folder)
|
||||
logging.debug("app data folder %s has no write access", folder)
|
||||
return TempAppData()
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AppDataDisabled",
|
||||
"AppDataDiskFolder",
|
||||
"ReadOnlyAppData",
|
||||
"TempAppData",
|
||||
"make_app_data",
|
||||
)
|
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,96 @@
|
|||
"""Application data stored by virtualenv."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
|
||||
from virtualenv.info import IS_ZIPAPP
|
||||
|
||||
|
||||
class AppData(ABC):
|
||||
"""Abstract storage interface for the virtualenv application."""
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
"""Called before virtualenv exits."""
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""Called when the user passes in the reset app data."""
|
||||
|
||||
@abstractmethod
|
||||
def py_info(self, path):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def py_info_clear(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def can_update(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def embed_update_log(self, distribution, for_py_version):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def house(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def transient(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def wheel_image(self, for_py_version, name):
|
||||
raise NotImplementedError
|
||||
|
||||
@contextmanager
|
||||
def ensure_extracted(self, path, to_folder=None):
|
||||
"""Some paths might be within the zipapp, unzip these to a path on the disk."""
|
||||
if IS_ZIPAPP:
|
||||
with self.extract(path, to_folder) as result:
|
||||
yield result
|
||||
else:
|
||||
yield path
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def extract(self, path, to_folder):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def locked(self, path):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ContentStore(ABC):
|
||||
@abstractmethod
|
||||
def exists(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def read(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write(self, content):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def remove(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def locked(self):
|
||||
pass
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppData",
|
||||
"ContentStore",
|
||||
]
|
|
@ -0,0 +1,72 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .base import AppData, ContentStore
|
||||
|
||||
|
||||
class AppDataDisabled(AppData):
|
||||
"""No application cache available (most likely as we don't have write permissions)."""
|
||||
|
||||
transient = True
|
||||
can_update = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
error = RuntimeError("no app data folder available, probably no write access to the folder")
|
||||
|
||||
def close(self):
|
||||
"""Do nothing."""
|
||||
|
||||
def reset(self):
|
||||
"""Do nothing."""
|
||||
|
||||
def py_info(self, path): # noqa: ARG002
|
||||
return ContentStoreNA()
|
||||
|
||||
def embed_update_log(self, distribution, for_py_version): # noqa: ARG002
|
||||
return ContentStoreNA()
|
||||
|
||||
def extract(self, path, to_folder): # noqa: ARG002
|
||||
raise self.error
|
||||
|
||||
@contextmanager
|
||||
def locked(self, path): # noqa: ARG002
|
||||
"""Do nothing."""
|
||||
yield
|
||||
|
||||
@property
|
||||
def house(self):
|
||||
raise self.error
|
||||
|
||||
def wheel_image(self, for_py_version, name): # noqa: ARG002
|
||||
raise self.error
|
||||
|
||||
def py_info_clear(self):
|
||||
"""Nothing to clear."""
|
||||
|
||||
|
||||
class ContentStoreNA(ContentStore):
|
||||
def exists(self):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
"""Nothing to read."""
|
||||
return
|
||||
|
||||
def write(self, content):
|
||||
"""Nothing to write."""
|
||||
|
||||
def remove(self):
|
||||
"""Nothing to remove."""
|
||||
|
||||
@contextmanager
|
||||
def locked(self):
|
||||
yield
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppDataDisabled",
|
||||
"ContentStoreNA",
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
|
||||
from virtualenv.util.lock import NoOpFileLock
|
||||
|
||||
from .via_disk_folder import AppDataDiskFolder, PyInfoStoreDisk
|
||||
|
||||
|
||||
class ReadOnlyAppData(AppDataDiskFolder):
|
||||
can_update = False
|
||||
|
||||
def __init__(self, folder: str) -> None:
|
||||
if not os.path.isdir(folder):
|
||||
msg = f"read-only app data directory {folder} does not exist"
|
||||
raise RuntimeError(msg)
|
||||
super().__init__(folder)
|
||||
self.lock = NoOpFileLock(folder)
|
||||
|
||||
def reset(self) -> None:
|
||||
msg = "read-only app data does not support reset"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def py_info_clear(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def py_info(self, path):
|
||||
return _PyInfoStoreDiskReadOnly(self.py_info_at, path)
|
||||
|
||||
def embed_update_log(self, distribution, for_py_version):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _PyInfoStoreDiskReadOnly(PyInfoStoreDisk):
|
||||
def write(self, content): # noqa: ARG002
|
||||
msg = "read-only app data python info cannot be updated"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ReadOnlyAppData",
|
||||
]
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
A rough layout of the current storage goes as:
|
||||
|
||||
virtualenv-app-data
|
||||
├── py - <version> <cache information about python interpreters>
|
||||
│ └── *.json/lock
|
||||
├── wheel <cache wheels used for seeding>
|
||||
│ ├── house
|
||||
│ │ └── *.whl <wheels downloaded go here>
|
||||
│ └── <python major.minor> -> 3.9
|
||||
│ ├── img-<version>
|
||||
│ │ └── image
|
||||
│ │ └── <install class> -> CopyPipInstall / SymlinkPipInstall
|
||||
│ │ └── <wheel name> -> pip-20.1.1-py2.py3-none-any
|
||||
│ └── embed
|
||||
│ └── 3 -> json format versioning
|
||||
│ └── *.json -> for every distribution contains data about newer embed versions and releases
|
||||
└─── unzip <in zip app we cannot refer to some internal files, so first extract them>
|
||||
└── <virtualenv version>
|
||||
├── py_info.py
|
||||
├── debug.py
|
||||
└── _virtualenv.py
|
||||
""" # noqa: D415
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from abc import ABC
|
||||
from contextlib import contextmanager, suppress
|
||||
from hashlib import sha256
|
||||
|
||||
from virtualenv.util.lock import ReentrantFileLock
|
||||
from virtualenv.util.path import safe_delete
|
||||
from virtualenv.util.zipapp import extract
|
||||
from virtualenv.version import __version__
|
||||
|
||||
from .base import AppData, ContentStore
|
||||
|
||||
|
||||
class AppDataDiskFolder(AppData):
|
||||
"""Store the application data on the disk within a folder layout."""
|
||||
|
||||
transient = False
|
||||
can_update = True
|
||||
|
||||
def __init__(self, folder) -> None:
|
||||
self.lock = ReentrantFileLock(folder)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({self.lock.path})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.lock.path)
|
||||
|
||||
def reset(self):
|
||||
logging.debug("reset app data folder %s", self.lock.path)
|
||||
safe_delete(self.lock.path)
|
||||
|
||||
def close(self):
|
||||
"""Do nothing."""
|
||||
|
||||
@contextmanager
|
||||
def locked(self, path):
|
||||
path_lock = self.lock / path
|
||||
with path_lock:
|
||||
yield path_lock.path
|
||||
|
||||
@contextmanager
|
||||
def extract(self, path, to_folder):
|
||||
root = ReentrantFileLock(to_folder()) if to_folder is not None else self.lock / "unzip" / __version__
|
||||
with root.lock_for_key(path.name):
|
||||
dest = root.path / path.name
|
||||
if not dest.exists():
|
||||
extract(path, dest)
|
||||
yield dest
|
||||
|
||||
@property
|
||||
def py_info_at(self):
|
||||
return self.lock / "py_info" / "1"
|
||||
|
||||
def py_info(self, path):
|
||||
return PyInfoStoreDisk(self.py_info_at, path)
|
||||
|
||||
def py_info_clear(self):
|
||||
"""clear py info."""
|
||||
py_info_folder = self.py_info_at
|
||||
with py_info_folder:
|
||||
for filename in py_info_folder.path.iterdir():
|
||||
if filename.suffix == ".json":
|
||||
with py_info_folder.lock_for_key(filename.stem):
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
|
||||
def embed_update_log(self, distribution, for_py_version):
|
||||
return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "3", distribution)
|
||||
|
||||
@property
|
||||
def house(self):
|
||||
path = self.lock.path / "wheel" / "house"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
def wheel_image(self, for_py_version, name):
|
||||
return self.lock.path / "wheel" / for_py_version / "image" / "1" / name
|
||||
|
||||
|
||||
class JSONStoreDisk(ContentStore, ABC):
|
||||
def __init__(self, in_folder, key, msg, msg_args) -> None:
|
||||
self.in_folder = in_folder
|
||||
self.key = key
|
||||
self.msg = msg
|
||||
self.msg_args = (*msg_args, self.file)
|
||||
|
||||
@property
|
||||
def file(self):
|
||||
return self.in_folder.path / f"{self.key}.json"
|
||||
|
||||
def exists(self):
|
||||
return self.file.exists()
|
||||
|
||||
def read(self):
|
||||
data, bad_format = None, False
|
||||
try:
|
||||
data = json.loads(self.file.read_text(encoding="utf-8"))
|
||||
except ValueError:
|
||||
bad_format = True
|
||||
except Exception: # noqa: BLE001, S110
|
||||
pass
|
||||
else:
|
||||
logging.debug("got %s from %s", self.msg, self.msg_args)
|
||||
return data
|
||||
if bad_format:
|
||||
with suppress(OSError): # reading and writing on the same file may cause race on multiple processes
|
||||
self.remove()
|
||||
return None
|
||||
|
||||
def remove(self):
|
||||
self.file.unlink()
|
||||
logging.debug("removed %s at %s", self.msg, self.msg_args)
|
||||
|
||||
@contextmanager
|
||||
def locked(self):
|
||||
with self.in_folder.lock_for_key(self.key):
|
||||
yield
|
||||
|
||||
def write(self, content):
|
||||
folder = self.file.parent
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
self.file.write_text(json.dumps(content, sort_keys=True, indent=2), encoding="utf-8")
|
||||
logging.debug("wrote %s at %s", self.msg, self.msg_args)
|
||||
|
||||
|
||||
class PyInfoStoreDisk(JSONStoreDisk):
|
||||
def __init__(self, in_folder, path) -> None:
|
||||
key = sha256(str(path).encode("utf-8")).hexdigest()
|
||||
super().__init__(in_folder, key, "python info of %s", (path,))
|
||||
|
||||
|
||||
class EmbedDistributionUpdateStoreDisk(JSONStoreDisk):
|
||||
def __init__(self, in_folder, distribution) -> None:
|
||||
super().__init__(
|
||||
in_folder,
|
||||
distribution,
|
||||
"embed update of distribution %s",
|
||||
(distribution,),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppDataDiskFolder",
|
||||
"JSONStoreDisk",
|
||||
"PyInfoStoreDisk",
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from virtualenv.util.path import safe_delete
|
||||
|
||||
from .via_disk_folder import AppDataDiskFolder
|
||||
|
||||
|
||||
class TempAppData(AppDataDiskFolder):
|
||||
transient = True
|
||||
can_update = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(folder=mkdtemp())
|
||||
logging.debug("created temporary app data folder %s", self.lock.path)
|
||||
|
||||
def reset(self):
|
||||
"""This is a temporary folder, is already empty to start with."""
|
||||
|
||||
def close(self):
|
||||
logging.debug("remove temporary app data folder %s", self.lock.path)
|
||||
safe_delete(self.lock.path)
|
||||
|
||||
def embed_update_log(self, distribution, for_py_version):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TempAppData",
|
||||
]
|
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,126 @@
|
|||
from __future__ import annotations # noqa: A005
|
||||
|
||||
import os
|
||||
from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
|
||||
from collections import OrderedDict
|
||||
|
||||
from virtualenv.config.convert import get_type
|
||||
from virtualenv.config.env_var import get_env_var
|
||||
from virtualenv.config.ini import IniConfig
|
||||
|
||||
|
||||
class VirtualEnvOptions(Namespace):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._src = None
|
||||
self._sources = {}
|
||||
|
||||
def set_src(self, key, value, src):
|
||||
setattr(self, key, value)
|
||||
if src.startswith("env var"):
|
||||
src = "env var"
|
||||
self._sources[key] = src
|
||||
|
||||
def __setattr__(self, key, value) -> None:
|
||||
if getattr(self, "_src", None) is not None:
|
||||
self._sources[key] = self._src
|
||||
super().__setattr__(key, value)
|
||||
|
||||
def get_source(self, key):
|
||||
return self._sources.get(key)
|
||||
|
||||
@property
|
||||
def verbosity(self):
|
||||
if not hasattr(self, "verbose") and not hasattr(self, "quiet"):
|
||||
return None
|
||||
return max(self.verbose - self.quiet, 0)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in vars(self).items() if not k.startswith('_'))})"
|
||||
|
||||
|
||||
class VirtualEnvConfigParser(ArgumentParser):
|
||||
"""Custom option parser which updates its defaults by checking the configuration files and environmental vars."""
|
||||
|
||||
def __init__(self, options=None, env=None, *args, **kwargs) -> None:
|
||||
env = os.environ if env is None else env
|
||||
self.file_config = IniConfig(env)
|
||||
self.epilog_list = []
|
||||
self.env = env
|
||||
kwargs["epilog"] = self.file_config.epilog
|
||||
kwargs["add_help"] = False
|
||||
kwargs["formatter_class"] = HelpFormatter
|
||||
kwargs["prog"] = "virtualenv"
|
||||
super().__init__(*args, **kwargs)
|
||||
self._fixed = set()
|
||||
if options is not None and not isinstance(options, VirtualEnvOptions):
|
||||
msg = "options must be of type VirtualEnvOptions"
|
||||
raise TypeError(msg)
|
||||
self.options = VirtualEnvOptions() if options is None else options
|
||||
self._interpreter = None
|
||||
self._app_data = None
|
||||
|
||||
def _fix_defaults(self):
|
||||
for action in self._actions:
|
||||
action_id = id(action)
|
||||
if action_id not in self._fixed:
|
||||
self._fix_default(action)
|
||||
self._fixed.add(action_id)
|
||||
|
||||
def _fix_default(self, action):
|
||||
if hasattr(action, "default") and hasattr(action, "dest") and action.default != SUPPRESS:
|
||||
as_type = get_type(action)
|
||||
names = OrderedDict((i.lstrip("-").replace("-", "_"), None) for i in action.option_strings)
|
||||
outcome = None
|
||||
for name in names:
|
||||
outcome = get_env_var(name, as_type, self.env)
|
||||
if outcome is not None:
|
||||
break
|
||||
if outcome is None and self.file_config:
|
||||
for name in names:
|
||||
outcome = self.file_config.get(name, as_type)
|
||||
if outcome is not None:
|
||||
break
|
||||
if outcome is not None:
|
||||
action.default, action.default_source = outcome
|
||||
else:
|
||||
outcome = action.default, "default"
|
||||
self.options.set_src(action.dest, *outcome)
|
||||
|
||||
def enable_help(self):
|
||||
self._fix_defaults()
|
||||
self.add_argument("-h", "--help", action="help", default=SUPPRESS, help="show this help message and exit")
|
||||
|
||||
def parse_known_args(self, args=None, namespace=None):
|
||||
if namespace is None:
|
||||
namespace = self.options
|
||||
elif namespace is not self.options:
|
||||
msg = "can only pass in parser.options"
|
||||
raise ValueError(msg)
|
||||
self._fix_defaults()
|
||||
self.options._src = "cli" # noqa: SLF001
|
||||
try:
|
||||
namespace.env = self.env
|
||||
return super().parse_known_args(args, namespace=namespace)
|
||||
finally:
|
||||
self.options._src = None # noqa: SLF001
|
||||
|
||||
|
||||
class HelpFormatter(ArgumentDefaultsHelpFormatter):
|
||||
def __init__(self, prog) -> None:
|
||||
super().__init__(prog, max_help_position=32, width=240)
|
||||
|
||||
def _get_help_string(self, action):
|
||||
text = super()._get_help_string(action)
|
||||
if hasattr(action, "default_source"):
|
||||
default = " (default: %(default)s)"
|
||||
if text.endswith(default):
|
||||
text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)"
|
||||
return text
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HelpFormatter",
|
||||
"VirtualEnvConfigParser",
|
||||
"VirtualEnvOptions",
|
||||
]
|
|
@ -0,0 +1,100 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
class TypeData:
|
||||
def __init__(self, default_type, as_type) -> None:
|
||||
self.default_type = default_type
|
||||
self.as_type = as_type
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(base={self.default_type}, as={self.as_type})"
|
||||
|
||||
def convert(self, value):
|
||||
return self.default_type(value)
|
||||
|
||||
|
||||
class BoolType(TypeData):
|
||||
BOOLEAN_STATES: ClassVar[dict[str, bool]] = {
|
||||
"1": True,
|
||||
"yes": True,
|
||||
"true": True,
|
||||
"on": True,
|
||||
"0": False,
|
||||
"no": False,
|
||||
"false": False,
|
||||
"off": False,
|
||||
}
|
||||
|
||||
def convert(self, value):
|
||||
if value.lower() not in self.BOOLEAN_STATES:
|
||||
msg = f"Not a boolean: {value}"
|
||||
raise ValueError(msg)
|
||||
return self.BOOLEAN_STATES[value.lower()]
|
||||
|
||||
|
||||
class NoneType(TypeData):
|
||||
def convert(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return str(value)
|
||||
|
||||
|
||||
class ListType(TypeData):
|
||||
def _validate(self):
|
||||
"""no op."""
|
||||
|
||||
def convert(self, value, flatten=True): # noqa: ARG002, FBT002
|
||||
values = self.split_values(value)
|
||||
result = []
|
||||
for a_value in values:
|
||||
sub_values = a_value.split(os.pathsep)
|
||||
result.extend(sub_values)
|
||||
return [self.as_type(i) for i in result]
|
||||
|
||||
def split_values(self, value):
|
||||
"""
|
||||
Split the provided value into a list.
|
||||
|
||||
First this is done by newlines. If there were no newlines in the text,
|
||||
then we next try to split by comma.
|
||||
"""
|
||||
if isinstance(value, (str, bytes)):
|
||||
# Use `splitlines` rather than a custom check for whether there is
|
||||
# more than one line. This ensures that the full `splitlines()`
|
||||
# logic is supported here.
|
||||
values = value.splitlines()
|
||||
if len(values) <= 1:
|
||||
values = value.split(",")
|
||||
values = filter(None, [x.strip() for x in values])
|
||||
else:
|
||||
values = list(value)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def convert(value, as_type, source):
|
||||
"""Convert the value as a given type where the value comes from the given source."""
|
||||
try:
|
||||
return as_type.convert(value)
|
||||
except Exception as exception:
|
||||
logging.warning("%s failed to convert %r as %r because %r", source, value, as_type, exception)
|
||||
raise
|
||||
|
||||
|
||||
_CONVERT = {bool: BoolType, type(None): NoneType, list: ListType}
|
||||
|
||||
|
||||
def get_type(action):
|
||||
default_type = type(action.default)
|
||||
as_type = default_type if action.type is None else action.type
|
||||
return _CONVERT.get(default_type, TypeData)(default_type, as_type)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"convert",
|
||||
"get_type",
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
from .convert import convert
|
||||
|
||||
|
||||
def get_env_var(key, as_type, env):
|
||||
"""
|
||||
Get the environment variable option.
|
||||
|
||||
:param key: the config key requested
|
||||
:param as_type: the type we would like to convert it to
|
||||
:param env: environment variables to use
|
||||
:return:
|
||||
"""
|
||||
environ_key = f"VIRTUALENV_{key.upper()}"
|
||||
if env.get(environ_key):
|
||||
value = env[environ_key]
|
||||
|
||||
with suppress(Exception): # note the converter already logs a warning when failures happen
|
||||
source = f"env var {environ_key}"
|
||||
as_type = convert(value, as_type, source)
|
||||
return as_type, source
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_env_var",
|
||||
]
|
|
@ -0,0 +1,75 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
from platformdirs import user_config_dir
|
||||
|
||||
from .convert import convert
|
||||
|
||||
|
||||
class IniConfig:
|
||||
VIRTUALENV_CONFIG_FILE_ENV_VAR: ClassVar[str] = "VIRTUALENV_CONFIG_FILE"
|
||||
STATE: ClassVar[dict[bool | None, str]] = {None: "failed to parse", True: "active", False: "missing"}
|
||||
|
||||
section = "virtualenv"
|
||||
|
||||
def __init__(self, env=None) -> None:
|
||||
env = os.environ if env is None else env
|
||||
config_file = env.get(self.VIRTUALENV_CONFIG_FILE_ENV_VAR, None)
|
||||
self.is_env_var = config_file is not None
|
||||
if config_file is None:
|
||||
config_file = Path(user_config_dir(appname="virtualenv", appauthor="pypa")) / "virtualenv.ini"
|
||||
else:
|
||||
config_file = Path(config_file)
|
||||
self.config_file = config_file
|
||||
self._cache = {}
|
||||
|
||||
exception = None
|
||||
self.has_config_file = None
|
||||
try:
|
||||
self.has_config_file = self.config_file.exists()
|
||||
except OSError as exc:
|
||||
exception = exc
|
||||
else:
|
||||
if self.has_config_file:
|
||||
self.config_file = self.config_file.resolve()
|
||||
self.config_parser = ConfigParser()
|
||||
try:
|
||||
self._load()
|
||||
self.has_virtualenv_section = self.config_parser.has_section(self.section)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
exception = exc
|
||||
if exception is not None:
|
||||
logging.error("failed to read config file %s because %r", config_file, exception)
|
||||
|
||||
def _load(self):
|
||||
with self.config_file.open("rt", encoding="utf-8") as file_handler:
|
||||
return self.config_parser.read_file(file_handler)
|
||||
|
||||
def get(self, key, as_type):
|
||||
cache_key = key, as_type
|
||||
if cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
try:
|
||||
source = "file"
|
||||
raw_value = self.config_parser.get(self.section, key.lower())
|
||||
value = convert(raw_value, as_type, source)
|
||||
result = value, source
|
||||
except Exception: # noqa: BLE001
|
||||
result = None
|
||||
self._cache[cache_key] = result
|
||||
return result
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.has_config_file) and bool(self.has_virtualenv_section)
|
||||
|
||||
@property
|
||||
def epilog(self):
|
||||
return (
|
||||
f"\nconfig file {self.config_file} {self.STATE[self.has_config_file]} "
|
||||
f"(change{'d' if self.is_env_var else ''} via env var {self.VIRTUALENV_CONFIG_FILE_ENV_VAR})"
|
||||
)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,226 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentTypeError
|
||||
from ast import literal_eval
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.discovery.cached_py_info import LogCmd
|
||||
from virtualenv.util.path import safe_delete
|
||||
from virtualenv.util.subprocess import run_cmd
|
||||
from virtualenv.version import __version__
|
||||
|
||||
from .pyenv_cfg import PyEnvCfg
|
||||
|
||||
HERE = Path(os.path.abspath(__file__)).parent
|
||||
DEBUG_SCRIPT = HERE / "debug.py"
|
||||
|
||||
|
||||
class CreatorMeta:
|
||||
def __init__(self) -> None:
|
||||
self.error = None
|
||||
|
||||
|
||||
class Creator(ABC):
|
||||
"""A class that given a python Interpreter creates a virtual environment."""
|
||||
|
||||
def __init__(self, options, interpreter) -> None:
|
||||
"""
|
||||
Construct a new virtual environment creator.
|
||||
|
||||
:param options: the CLI option as parsed from :meth:`add_parser_arguments`
|
||||
:param interpreter: the interpreter to create virtual environment from
|
||||
"""
|
||||
self.interpreter = interpreter
|
||||
self._debug = None
|
||||
self.dest = Path(options.dest)
|
||||
self.clear = options.clear
|
||||
self.no_vcs_ignore = options.no_vcs_ignore
|
||||
self.pyenv_cfg = PyEnvCfg.from_folder(self.dest)
|
||||
self.app_data = options.app_data
|
||||
self.env = options.env
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in self._args())})"
|
||||
|
||||
def _args(self):
|
||||
return [
|
||||
("dest", str(self.dest)),
|
||||
("clear", self.clear),
|
||||
("no_vcs_ignore", self.no_vcs_ignore),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def can_create(cls, interpreter): # noqa: ARG003
|
||||
"""
|
||||
Determine if we can create a virtual environment.
|
||||
|
||||
:param interpreter: the interpreter in question
|
||||
:return: ``None`` if we can't create, any other object otherwise that will be forwarded to \
|
||||
:meth:`add_parser_arguments`
|
||||
"""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, parser, interpreter, meta, app_data): # noqa: ARG003
|
||||
"""
|
||||
Add CLI arguments for the creator.
|
||||
|
||||
:param parser: the CLI parser
|
||||
:param app_data: the application data folder
|
||||
:param interpreter: the interpreter we're asked to create virtual environment for
|
||||
:param meta: value as returned by :meth:`can_create`
|
||||
"""
|
||||
parser.add_argument(
|
||||
"dest",
|
||||
help="directory to create virtualenv at",
|
||||
type=cls.validate_dest,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear",
|
||||
dest="clear",
|
||||
action="store_true",
|
||||
help="remove the destination directory if exist before starting (will overwrite files otherwise)",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-vcs-ignore",
|
||||
dest="no_vcs_ignore",
|
||||
action="store_true",
|
||||
help="don't create VCS ignore directive in the destination directory",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def create(self):
|
||||
"""Perform the virtual environment creation."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate_dest(cls, raw_value): # noqa: C901
|
||||
"""No path separator in the path, valid chars and must be write-able."""
|
||||
|
||||
def non_write_able(dest, value):
|
||||
common = Path(*os.path.commonprefix([value.parts, dest.parts]))
|
||||
msg = f"the destination {dest.relative_to(common)} is not write-able at {common}"
|
||||
raise ArgumentTypeError(msg)
|
||||
|
||||
# the file system must be able to encode
|
||||
# note in newer CPython this is always utf-8 https://www.python.org/dev/peps/pep-0529/
|
||||
encoding = sys.getfilesystemencoding()
|
||||
refused = OrderedDict()
|
||||
kwargs = {"errors": "ignore"} if encoding != "mbcs" else {}
|
||||
for char in str(raw_value):
|
||||
try:
|
||||
trip = char.encode(encoding, **kwargs).decode(encoding)
|
||||
if trip == char:
|
||||
continue
|
||||
raise ValueError(trip) # noqa: TRY301
|
||||
except ValueError:
|
||||
refused[char] = None
|
||||
if refused:
|
||||
bad = "".join(refused.keys())
|
||||
msg = f"the file system codec ({encoding}) cannot handle characters {bad!r} within {raw_value!r}"
|
||||
raise ArgumentTypeError(msg)
|
||||
if os.pathsep in raw_value:
|
||||
msg = (
|
||||
f"destination {raw_value!r} must not contain the path separator ({os.pathsep})"
|
||||
f" as this would break the activation scripts"
|
||||
)
|
||||
raise ArgumentTypeError(msg)
|
||||
|
||||
value = Path(raw_value)
|
||||
if value.exists() and value.is_file():
|
||||
msg = f"the destination {value} already exists and is a file"
|
||||
raise ArgumentTypeError(msg)
|
||||
dest = Path(os.path.abspath(str(value))).resolve() # on Windows absolute does not imply resolve so use both
|
||||
value = dest
|
||||
while dest:
|
||||
if dest.exists():
|
||||
if os.access(str(dest), os.W_OK):
|
||||
break
|
||||
non_write_able(dest, value)
|
||||
base, _ = dest.parent, dest.name
|
||||
if base == dest:
|
||||
non_write_able(dest, value) # pragma: no cover
|
||||
dest = base
|
||||
return str(value)
|
||||
|
||||
def run(self):
|
||||
if self.dest.exists() and self.clear:
|
||||
logging.debug("delete %s", self.dest)
|
||||
safe_delete(self.dest)
|
||||
self.create()
|
||||
self.set_pyenv_cfg()
|
||||
if not self.no_vcs_ignore:
|
||||
self.setup_ignore_vcs()
|
||||
|
||||
def set_pyenv_cfg(self):
|
||||
self.pyenv_cfg.content = OrderedDict()
|
||||
self.pyenv_cfg["home"] = os.path.dirname(os.path.abspath(self.interpreter.system_executable))
|
||||
self.pyenv_cfg["implementation"] = self.interpreter.implementation
|
||||
self.pyenv_cfg["version_info"] = ".".join(str(i) for i in self.interpreter.version_info)
|
||||
self.pyenv_cfg["virtualenv"] = __version__
|
||||
|
||||
def setup_ignore_vcs(self):
|
||||
"""Generate ignore instructions for version control systems."""
|
||||
# mark this folder to be ignored by VCS, handle https://www.python.org/dev/peps/pep-0610/#registered-vcs
|
||||
git_ignore = self.dest / ".gitignore"
|
||||
if not git_ignore.exists():
|
||||
git_ignore.write_text("# created by virtualenv automatically\n*\n", encoding="utf-8")
|
||||
# Mercurial - does not support the .hgignore file inside a subdirectory directly, but only if included via the
|
||||
# subinclude directive from root, at which point on might as well ignore the directory itself, see
|
||||
# https://www.selenic.com/mercurial/hgignore.5.html for more details
|
||||
# Bazaar - does not support ignore files in sub-directories, only at root level via .bzrignore
|
||||
# Subversion - does not support ignore files, requires direct manipulation with the svn tool
|
||||
|
||||
@property
|
||||
def debug(self):
|
||||
""":return: debug information about the virtual environment (only valid after :meth:`create` has run)"""
|
||||
if self._debug is None and self.exe is not None:
|
||||
self._debug = get_env_debug_info(self.exe, self.debug_script(), self.app_data, self.env)
|
||||
return self._debug
|
||||
|
||||
@staticmethod
|
||||
def debug_script():
|
||||
return DEBUG_SCRIPT
|
||||
|
||||
|
||||
def get_env_debug_info(env_exe, debug_script, app_data, env):
|
||||
env = env.copy()
|
||||
env.pop("PYTHONPATH", None)
|
||||
|
||||
with app_data.ensure_extracted(debug_script) as debug_script_extracted:
|
||||
cmd = [str(env_exe), str(debug_script_extracted)]
|
||||
logging.debug("debug via %r", LogCmd(cmd))
|
||||
code, out, err = run_cmd(cmd)
|
||||
|
||||
try:
|
||||
if code != 0:
|
||||
if out:
|
||||
result = literal_eval(out)
|
||||
else:
|
||||
if code == 2 and "file" in err: # noqa: PLR2004
|
||||
# Re-raise FileNotFoundError from `run_cmd()`
|
||||
raise OSError(err) # noqa: TRY301
|
||||
raise Exception(err) # noqa: TRY002, TRY301
|
||||
else:
|
||||
result = json.loads(out)
|
||||
if err:
|
||||
result["err"] = err
|
||||
except Exception as exception: # noqa: BLE001
|
||||
return {"out": out, "err": err, "returncode": code, "exception": repr(exception)}
|
||||
if "sys" in result and "path" in result["sys"]:
|
||||
del result["sys"]["path"][0]
|
||||
return result
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Creator",
|
||||
"CreatorMeta",
|
||||
]
|
|
@ -0,0 +1,102 @@
|
|||
"""Inspect a target Python interpreter virtual environment wise."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys # built-in
|
||||
|
||||
|
||||
def encode_path(value):
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, (str, bytes)):
|
||||
value = repr(value) if isinstance(value, type) else repr(type(value))
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode(sys.getfilesystemencoding())
|
||||
return value
|
||||
|
||||
|
||||
def encode_list_path(value):
|
||||
return [encode_path(i) for i in value]
|
||||
|
||||
|
||||
def run():
|
||||
"""Print debug data about the virtual environment."""
|
||||
try:
|
||||
from collections import OrderedDict # noqa: PLC0415
|
||||
except ImportError: # pragma: no cover
|
||||
# this is possible if the standard library cannot be accessed
|
||||
|
||||
OrderedDict = dict # pragma: no cover # noqa: N806
|
||||
result = OrderedDict([("sys", OrderedDict())])
|
||||
path_keys = (
|
||||
"executable",
|
||||
"_base_executable",
|
||||
"prefix",
|
||||
"base_prefix",
|
||||
"real_prefix",
|
||||
"exec_prefix",
|
||||
"base_exec_prefix",
|
||||
"path",
|
||||
"meta_path",
|
||||
)
|
||||
for key in path_keys:
|
||||
value = getattr(sys, key, None)
|
||||
value = encode_list_path(value) if isinstance(value, list) else encode_path(value)
|
||||
result["sys"][key] = value
|
||||
result["sys"]["fs_encoding"] = sys.getfilesystemencoding()
|
||||
result["sys"]["io_encoding"] = getattr(sys.stdout, "encoding", None)
|
||||
result["version"] = sys.version
|
||||
|
||||
try:
|
||||
import sysconfig # noqa: PLC0415
|
||||
|
||||
# https://bugs.python.org/issue22199
|
||||
makefile = getattr(sysconfig, "get_makefile_filename", getattr(sysconfig, "_get_makefile_filename", None))
|
||||
result["makefile_filename"] = encode_path(makefile())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import os # landmark # noqa: PLC0415
|
||||
|
||||
result["os"] = repr(os)
|
||||
|
||||
try:
|
||||
import site # site # noqa: PLC0415
|
||||
|
||||
result["site"] = repr(site)
|
||||
except ImportError as exception: # pragma: no cover
|
||||
result["site"] = repr(exception) # pragma: no cover
|
||||
|
||||
try:
|
||||
import datetime # site # noqa: PLC0415
|
||||
|
||||
result["datetime"] = repr(datetime)
|
||||
except ImportError as exception: # pragma: no cover
|
||||
result["datetime"] = repr(exception) # pragma: no cover
|
||||
|
||||
try:
|
||||
import math # site # noqa: PLC0415
|
||||
|
||||
result["math"] = repr(math)
|
||||
except ImportError as exception: # pragma: no cover
|
||||
result["math"] = repr(exception) # pragma: no cover
|
||||
|
||||
# try to print out, this will validate if other core modules are available (json in this case)
|
||||
try:
|
||||
import json # noqa: PLC0415
|
||||
|
||||
result["json"] = repr(json)
|
||||
except ImportError as exception:
|
||||
result["json"] = repr(exception)
|
||||
else:
|
||||
try:
|
||||
content = json.dumps(result, indent=2)
|
||||
sys.stdout.write(content)
|
||||
except (ValueError, TypeError) as exception: # pragma: no cover
|
||||
sys.stderr.write(repr(exception))
|
||||
sys.stdout.write(repr(result)) # pragma: no cover
|
||||
raise SystemExit(1) # noqa: B904 # pragma: no cover
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
|
@ -0,0 +1,110 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.info import IS_WIN
|
||||
|
||||
|
||||
class Describe:
|
||||
"""Given a host interpreter tell us information about what the created interpreter might look like."""
|
||||
|
||||
suffix = ".exe" if IS_WIN else ""
|
||||
|
||||
def __init__(self, dest, interpreter) -> None:
|
||||
self.interpreter = interpreter
|
||||
self.dest = dest
|
||||
self._stdlib = None
|
||||
self._stdlib_platform = None
|
||||
self._system_stdlib = None
|
||||
self._conf_vars = None
|
||||
|
||||
@property
|
||||
def bin_dir(self):
|
||||
return self.script_dir
|
||||
|
||||
@property
|
||||
def script_dir(self):
|
||||
return self.dest / self.interpreter.install_path("scripts")
|
||||
|
||||
@property
|
||||
def purelib(self):
|
||||
return self.dest / self.interpreter.install_path("purelib")
|
||||
|
||||
@property
|
||||
def platlib(self):
|
||||
return self.dest / self.interpreter.install_path("platlib")
|
||||
|
||||
@property
|
||||
def libs(self):
|
||||
return list(OrderedDict(((self.platlib, None), (self.purelib, None))).keys())
|
||||
|
||||
@property
|
||||
def stdlib(self):
|
||||
if self._stdlib is None:
|
||||
self._stdlib = Path(self.interpreter.sysconfig_path("stdlib", config_var=self._config_vars))
|
||||
return self._stdlib
|
||||
|
||||
@property
|
||||
def stdlib_platform(self):
|
||||
if self._stdlib_platform is None:
|
||||
self._stdlib_platform = Path(self.interpreter.sysconfig_path("platstdlib", config_var=self._config_vars))
|
||||
return self._stdlib_platform
|
||||
|
||||
@property
|
||||
def _config_vars(self):
|
||||
if self._conf_vars is None:
|
||||
self._conf_vars = self._calc_config_vars(self.dest)
|
||||
return self._conf_vars
|
||||
|
||||
def _calc_config_vars(self, to):
|
||||
sys_vars = self.interpreter.sysconfig_vars
|
||||
return {k: (to if v is not None and v.startswith(self.interpreter.prefix) else v) for k, v in sys_vars.items()}
|
||||
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter): # noqa: ARG003
|
||||
"""Knows means it knows how the output will look."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def env_name(self):
|
||||
return self.dest.parts[-1]
|
||||
|
||||
@property
|
||||
def exe(self):
|
||||
return self.bin_dir / f"{self.exe_stem()}{self.suffix}"
|
||||
|
||||
@classmethod
|
||||
def exe_stem(cls):
|
||||
"""Executable name without suffix - there seems to be no standard way to get this without creating it."""
|
||||
raise NotImplementedError
|
||||
|
||||
def script(self, name):
|
||||
return self.script_dir / f"{name}{self.suffix}"
|
||||
|
||||
|
||||
class Python3Supports(Describe, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return interpreter.version_info.major == 3 and super().can_describe(interpreter) # noqa: PLR2004
|
||||
|
||||
|
||||
class PosixSupports(Describe, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return interpreter.os == "posix" and super().can_describe(interpreter)
|
||||
|
||||
|
||||
class WindowsSupports(Describe, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return interpreter.os == "nt" and super().can_describe(interpreter)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Describe",
|
||||
"PosixSupports",
|
||||
"Python3Supports",
|
||||
"WindowsSupports",
|
||||
]
|
|
@ -0,0 +1,66 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class PyEnvCfg:
|
||||
def __init__(self, content, path) -> None:
|
||||
self.content = content
|
||||
self.path = path
|
||||
|
||||
@classmethod
|
||||
def from_folder(cls, folder):
|
||||
return cls.from_file(folder / "pyvenv.cfg")
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path):
|
||||
content = cls._read_values(path) if path.exists() else OrderedDict()
|
||||
return PyEnvCfg(content, path)
|
||||
|
||||
@staticmethod
|
||||
def _read_values(path):
|
||||
content = OrderedDict()
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
equals_at = line.index("=")
|
||||
key = line[:equals_at].strip()
|
||||
value = line[equals_at + 1 :].strip()
|
||||
content[key] = value
|
||||
return content
|
||||
|
||||
def write(self):
|
||||
logging.debug("write %s", self.path)
|
||||
text = ""
|
||||
for key, value in self.content.items():
|
||||
normalized_value = os.path.realpath(value) if value and os.path.exists(value) else value
|
||||
line = f"{key} = {normalized_value}"
|
||||
logging.debug("\t%s", line)
|
||||
text += line
|
||||
text += "\n"
|
||||
self.path.write_text(text, encoding="utf-8")
|
||||
|
||||
def refresh(self):
|
||||
self.content = self._read_values(self.path)
|
||||
return self.content
|
||||
|
||||
def __setitem__(self, key, value) -> None:
|
||||
self.content[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.content[key]
|
||||
|
||||
def __contains__(self, item) -> bool:
|
||||
return item in self.content
|
||||
|
||||
def update(self, other):
|
||||
self.content.update(other)
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(path={self.path})"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PyEnvCfg",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,103 @@
|
|||
"""Patches that are applied at runtime to the virtual environment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
VIRTUALENV_PATCH_FILE = os.path.join(__file__)
|
||||
|
||||
|
||||
def patch_dist(dist):
|
||||
"""
|
||||
Distutils allows user to configure some arguments via a configuration file:
|
||||
https://docs.python.org/3/install/index.html#distutils-configuration-files.
|
||||
|
||||
Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
|
||||
""" # noqa: D205
|
||||
# we cannot allow some install config as that would get packages installed outside of the virtual environment
|
||||
old_parse_config_files = dist.Distribution.parse_config_files
|
||||
|
||||
def parse_config_files(self, *args, **kwargs):
|
||||
result = old_parse_config_files(self, *args, **kwargs)
|
||||
install = self.get_option_dict("install")
|
||||
|
||||
if "prefix" in install: # the prefix governs where to install the libraries
|
||||
install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix)
|
||||
for base in ("purelib", "platlib", "headers", "scripts", "data"):
|
||||
key = f"install_{base}"
|
||||
if key in install: # do not allow global configs to hijack venv paths
|
||||
install.pop(key, None)
|
||||
return result
|
||||
|
||||
dist.Distribution.parse_config_files = parse_config_files
|
||||
|
||||
|
||||
# Import hook that patches some modules to ignore configuration values that break package installation in case
|
||||
# of virtual environments.
|
||||
_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist"
|
||||
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
|
||||
|
||||
|
||||
class _Finder:
|
||||
"""A meta path finder that allows patching the imported distutils modules."""
|
||||
|
||||
fullname = None
|
||||
|
||||
# lock[0] is threading.Lock(), but initialized lazily to avoid importing threading very early at startup,
|
||||
# because there are gevent-based applications that need to be first to import threading by themselves.
|
||||
# See https://github.com/pypa/virtualenv/issues/1895 for details.
|
||||
lock = [] # noqa: RUF012
|
||||
|
||||
def find_spec(self, fullname, path, target=None): # noqa: ARG002
|
||||
if fullname in _DISTUTILS_PATCH and self.fullname is None: # noqa: PLR1702
|
||||
# initialize lock[0] lazily
|
||||
if len(self.lock) == 0:
|
||||
import threading # noqa: PLC0415
|
||||
|
||||
lock = threading.Lock()
|
||||
# there is possibility that two threads T1 and T2 are simultaneously running into find_spec,
|
||||
# observing .lock as empty, and further going into hereby initialization. However due to the GIL,
|
||||
# list.append() operation is atomic and this way only one of the threads will "win" to put the lock
|
||||
# - that every thread will use - into .lock[0].
|
||||
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
|
||||
self.lock.append(lock)
|
||||
|
||||
from functools import partial # noqa: PLC0415
|
||||
from importlib.util import find_spec # noqa: PLC0415
|
||||
|
||||
with self.lock[0]:
|
||||
self.fullname = fullname
|
||||
try:
|
||||
spec = find_spec(fullname, path)
|
||||
if spec is not None:
|
||||
# https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
|
||||
is_new_api = hasattr(spec.loader, "exec_module")
|
||||
func_name = "exec_module" if is_new_api else "load_module"
|
||||
old = getattr(spec.loader, func_name)
|
||||
func = self.exec_module if is_new_api else self.load_module
|
||||
if old is not func:
|
||||
try: # noqa: SIM105
|
||||
setattr(spec.loader, func_name, partial(func, old))
|
||||
except AttributeError:
|
||||
pass # C-Extension loaders are r/o such as zipimporter with <3.7
|
||||
return spec
|
||||
finally:
|
||||
self.fullname = None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def exec_module(old, module):
|
||||
old(module)
|
||||
if module.__name__ in _DISTUTILS_PATCH:
|
||||
patch_dist(module)
|
||||
|
||||
@staticmethod
|
||||
def load_module(old, name):
|
||||
module = old(name)
|
||||
if module.__name__ in _DISTUTILS_PATCH:
|
||||
patch_dist(module)
|
||||
return module
|
||||
|
||||
|
||||
sys.meta_path.insert(0, _Finder())
|
|
@ -0,0 +1,114 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.create.creator import Creator, CreatorMeta
|
||||
from virtualenv.info import fs_supports_symlink
|
||||
|
||||
|
||||
class ViaGlobalRefMeta(CreatorMeta):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.copy_error = None
|
||||
self.symlink_error = None
|
||||
if not fs_supports_symlink():
|
||||
self.symlink_error = "the filesystem does not supports symlink"
|
||||
|
||||
@property
|
||||
def can_copy(self):
|
||||
return not self.copy_error
|
||||
|
||||
@property
|
||||
def can_symlink(self):
|
||||
return not self.symlink_error
|
||||
|
||||
|
||||
class ViaGlobalRefApi(Creator, ABC):
|
||||
def __init__(self, options, interpreter) -> None:
|
||||
super().__init__(options, interpreter)
|
||||
self.symlinks = self._should_symlink(options)
|
||||
self.enable_system_site_package = options.system_site
|
||||
|
||||
@staticmethod
|
||||
def _should_symlink(options):
|
||||
# Priority of where the option is set to follow the order: CLI, env var, file, hardcoded.
|
||||
# If both set at same level prefers copy over symlink.
|
||||
copies, symlinks = getattr(options, "copies", False), getattr(options, "symlinks", False)
|
||||
copy_src, sym_src = options.get_source("copies"), options.get_source("symlinks")
|
||||
for level in ["cli", "env var", "file", "default"]:
|
||||
s_opt = symlinks if sym_src == level else None
|
||||
c_opt = copies if copy_src == level else None
|
||||
if s_opt is True and c_opt is True:
|
||||
return False
|
||||
if s_opt is True:
|
||||
return True
|
||||
if c_opt is True:
|
||||
return False
|
||||
return False # fallback to copy
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, parser, interpreter, meta, app_data):
|
||||
super().add_parser_arguments(parser, interpreter, meta, app_data)
|
||||
parser.add_argument(
|
||||
"--system-site-packages",
|
||||
default=False,
|
||||
action="store_true",
|
||||
dest="system_site",
|
||||
help="give the virtual environment access to the system site-packages dir",
|
||||
)
|
||||
if not meta.can_symlink and not meta.can_copy:
|
||||
msg = "neither symlink or copy method supported"
|
||||
raise RuntimeError(msg)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
if meta.can_symlink:
|
||||
group.add_argument(
|
||||
"--symlinks",
|
||||
default=True,
|
||||
action="store_true",
|
||||
dest="symlinks",
|
||||
help="try to use symlinks rather than copies, when symlinks are not the default for the platform",
|
||||
)
|
||||
if meta.can_copy:
|
||||
group.add_argument(
|
||||
"--copies",
|
||||
"--always-copy",
|
||||
default=not meta.can_symlink,
|
||||
action="store_true",
|
||||
dest="copies",
|
||||
help="try to use copies rather than symlinks, even when symlinks are the default for the platform",
|
||||
)
|
||||
|
||||
def create(self):
|
||||
self.install_patch()
|
||||
|
||||
def install_patch(self):
|
||||
text = self.env_patch_text()
|
||||
if text:
|
||||
pth = self.purelib / "_virtualenv.pth"
|
||||
logging.debug("create virtualenv import hook file %s", pth)
|
||||
pth.write_text("import _virtualenv", encoding="utf-8")
|
||||
dest_path = self.purelib / "_virtualenv.py"
|
||||
logging.debug("create %s", dest_path)
|
||||
dest_path.write_text(text, encoding="utf-8")
|
||||
|
||||
def env_patch_text(self):
|
||||
"""Patch the distutils package to not be derailed by its configuration files."""
|
||||
with self.app_data.ensure_extracted(Path(__file__).parent / "_virtualenv.py") as resolved_path:
|
||||
text = resolved_path.read_text(encoding="utf-8")
|
||||
return text.replace('"__SCRIPT_DIR__"', repr(os.path.relpath(str(self.script_dir), str(self.purelib))))
|
||||
|
||||
def _args(self):
|
||||
return [*super()._args(), ("global", self.enable_system_site_package)]
|
||||
|
||||
def set_pyenv_cfg(self):
|
||||
super().set_pyenv_cfg()
|
||||
self.pyenv_cfg["include-system-site-packages"] = "true" if self.enable_system_site_package else "false"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ViaGlobalRefApi",
|
||||
"ViaGlobalRefMeta",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
|
||||
from virtualenv.create.creator import Creator
|
||||
from virtualenv.create.describe import Describe
|
||||
|
||||
|
||||
class VirtualenvBuiltin(Creator, Describe, ABC):
|
||||
"""A creator that does operations itself without delegation, if we can create it we can also describe it."""
|
||||
|
||||
def __init__(self, options, interpreter) -> None:
|
||||
Creator.__init__(self, options, interpreter)
|
||||
Describe.__init__(self, self.dest, interpreter)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"VirtualenvBuiltin",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,73 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.create.describe import PosixSupports, WindowsSupports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin
|
||||
|
||||
|
||||
class CPython(ViaGlobalRefVirtualenvBuiltin, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return interpreter.implementation == "CPython" and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def exe_stem(cls):
|
||||
return "python"
|
||||
|
||||
|
||||
class CPythonPosix(CPython, PosixSupports, ABC):
|
||||
"""Create a CPython virtual environment on POSIX platforms."""
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter):
|
||||
host_exe = Path(interpreter.system_executable)
|
||||
major, minor = interpreter.version_info.major, interpreter.version_info.minor
|
||||
targets = OrderedDict((i, None) for i in ["python", f"python{major}", f"python{major}.{minor}", host_exe.name])
|
||||
yield host_exe, list(targets.keys()), RefMust.NA, RefWhen.ANY
|
||||
|
||||
|
||||
class CPythonWindows(CPython, WindowsSupports, ABC):
|
||||
@classmethod
|
||||
def _executables(cls, interpreter):
|
||||
# symlink of the python executables does not work reliably, copy always instead
|
||||
# - https://bugs.python.org/issue42013
|
||||
# - venv
|
||||
host = cls.host_python(interpreter)
|
||||
for path in (host.parent / n for n in {"python.exe", host.name}): # noqa: PLC0208
|
||||
yield host, [path.name], RefMust.COPY, RefWhen.ANY
|
||||
# for more info on pythonw.exe see https://stackoverflow.com/a/30313091
|
||||
python_w = host.parent / "pythonw.exe"
|
||||
yield python_w, [python_w.name], RefMust.COPY, RefWhen.ANY
|
||||
|
||||
@classmethod
|
||||
def host_python(cls, interpreter):
|
||||
return Path(interpreter.system_executable)
|
||||
|
||||
|
||||
def is_mac_os_framework(interpreter):
|
||||
if interpreter.platform == "darwin":
|
||||
return interpreter.sysconfig_vars.get("PYTHONFRAMEWORK") == "Python3"
|
||||
return False
|
||||
|
||||
|
||||
def is_macos_brew(interpreter):
|
||||
return interpreter.platform == "darwin" and _BREW.fullmatch(interpreter.system_prefix) is not None
|
||||
|
||||
|
||||
_BREW = re.compile(
|
||||
r"/(usr/local|opt/homebrew)/(opt/python@3\.\d{1,2}|Cellar/python@3\.\d{1,2}/3\.\d{1,2}\.\d{1,2})/Frameworks/"
|
||||
r"Python\.framework/Versions/3\.\d{1,2}",
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CPython",
|
||||
"CPythonPosix",
|
||||
"CPythonWindows",
|
||||
"is_mac_os_framework",
|
||||
"is_macos_brew",
|
||||
]
|
|
@ -0,0 +1,135 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import fnmatch
|
||||
from itertools import chain
|
||||
from operator import methodcaller as method
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from virtualenv.create.describe import Python3Supports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest
|
||||
from virtualenv.create.via_global_ref.store import is_store_python
|
||||
|
||||
from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework, is_macos_brew
|
||||
|
||||
|
||||
class CPython3(CPython, Python3Supports, abc.ABC):
|
||||
"""CPython 3 or later."""
|
||||
|
||||
|
||||
class CPython3Posix(CPythonPosix, CPython3):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return (
|
||||
is_mac_os_framework(interpreter) is False
|
||||
and is_macos_brew(interpreter) is False
|
||||
and super().can_describe(interpreter)
|
||||
)
|
||||
|
||||
def env_patch_text(self):
|
||||
text = super().env_patch_text()
|
||||
if self.pyvenv_launch_patch_active(self.interpreter):
|
||||
text += dedent(
|
||||
"""
|
||||
# for https://github.com/python/cpython/pull/9516, see https://github.com/pypa/virtualenv/issues/1704
|
||||
import os
|
||||
if "__PYVENV_LAUNCHER__" in os.environ:
|
||||
del os.environ["__PYVENV_LAUNCHER__"]
|
||||
""",
|
||||
)
|
||||
return text
|
||||
|
||||
@classmethod
|
||||
def pyvenv_launch_patch_active(cls, interpreter):
|
||||
ver = interpreter.version_info
|
||||
return interpreter.platform == "darwin" and ((3, 7, 8) > ver >= (3, 7) or (3, 8, 3) > ver >= (3, 8))
|
||||
|
||||
|
||||
class CPython3Windows(CPythonWindows, CPython3):
|
||||
"""CPython 3 on Windows."""
|
||||
|
||||
@classmethod
|
||||
def setup_meta(cls, interpreter):
|
||||
if is_store_python(interpreter): # store python is not supported here
|
||||
return None
|
||||
return super().setup_meta(interpreter)
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter):
|
||||
if cls.has_shim(interpreter):
|
||||
refs = cls.executables(interpreter)
|
||||
else:
|
||||
refs = chain(
|
||||
cls.executables(interpreter),
|
||||
cls.dll_and_pyd(interpreter),
|
||||
cls.python_zip(interpreter),
|
||||
)
|
||||
yield from refs
|
||||
|
||||
@classmethod
|
||||
def executables(cls, interpreter):
|
||||
return super().sources(interpreter)
|
||||
|
||||
@classmethod
|
||||
def has_shim(cls, interpreter):
|
||||
return interpreter.version_info.minor >= 7 and cls.shim(interpreter) is not None # noqa: PLR2004
|
||||
|
||||
@classmethod
|
||||
def shim(cls, interpreter):
|
||||
shim = Path(interpreter.system_stdlib) / "venv" / "scripts" / "nt" / "python.exe"
|
||||
if shim.exists():
|
||||
return shim
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def host_python(cls, interpreter):
|
||||
if cls.has_shim(interpreter):
|
||||
# starting with CPython 3.7 Windows ships with a venvlauncher.exe that avoids the need for dll/pyd copies
|
||||
# it also means the wrapper must be copied to avoid bugs such as https://bugs.python.org/issue42013
|
||||
return cls.shim(interpreter)
|
||||
return super().host_python(interpreter)
|
||||
|
||||
@classmethod
|
||||
def dll_and_pyd(cls, interpreter):
|
||||
folders = [Path(interpreter.system_executable).parent]
|
||||
|
||||
# May be missing on some Python hosts.
|
||||
# See https://github.com/pypa/virtualenv/issues/2368
|
||||
dll_folder = Path(interpreter.system_prefix) / "DLLs"
|
||||
if dll_folder.is_dir():
|
||||
folders.append(dll_folder)
|
||||
|
||||
for folder in folders:
|
||||
for file in folder.iterdir():
|
||||
if file.suffix in {".pyd", ".dll"}:
|
||||
yield PathRefToDest(file, cls.to_bin)
|
||||
|
||||
@classmethod
|
||||
def python_zip(cls, interpreter):
|
||||
"""
|
||||
"python{VERSION}.zip" contains compiled *.pyc std lib packages, where
|
||||
"VERSION" is `py_version_nodot` var from the `sysconfig` module.
|
||||
:see: https://docs.python.org/3/using/windows.html#the-embeddable-package
|
||||
:see: `discovery.py_info.PythonInfo` class (interpreter).
|
||||
:see: `python -m sysconfig` output.
|
||||
|
||||
:note: The embeddable Python distribution for Windows includes
|
||||
"python{VERSION}.zip" and "python{VERSION}._pth" files. User can
|
||||
move/rename *zip* file and edit `sys.path` by editing *_pth* file.
|
||||
Here the `pattern` is used only for the default *zip* file name!
|
||||
""" # noqa: D205
|
||||
pattern = f"*python{interpreter.version_nodot}.zip"
|
||||
matches = fnmatch.filter(interpreter.path, pattern)
|
||||
matched_paths = map(Path, matches)
|
||||
existing_paths = filter(method("exists"), matched_paths)
|
||||
path = next(existing_paths, None)
|
||||
if path is not None:
|
||||
yield PathRefToDest(path, cls.to_bin)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CPython3",
|
||||
"CPython3Posix",
|
||||
"CPython3Windows",
|
||||
]
|
|
@ -0,0 +1,279 @@
|
|||
"""The Apple Framework builds require their own customization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import (
|
||||
ExePathRefToDest,
|
||||
PathRefToDest,
|
||||
RefMust,
|
||||
)
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta
|
||||
|
||||
from .common import CPython, CPythonPosix, is_mac_os_framework, is_macos_brew
|
||||
from .cpython3 import CPython3
|
||||
|
||||
|
||||
class CPythonmacOsFramework(CPython, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return is_mac_os_framework(interpreter) and super().can_describe(interpreter)
|
||||
|
||||
def create(self):
|
||||
super().create()
|
||||
|
||||
# change the install_name of the copied python executables
|
||||
target = self.desired_mach_o_image_path()
|
||||
current = self.current_mach_o_image_path()
|
||||
for src in self._sources:
|
||||
if isinstance(src, ExePathRefToDest) and (src.must == RefMust.COPY or not self.symlinks):
|
||||
exes = [self.bin_dir / src.base]
|
||||
if not self.symlinks:
|
||||
exes.extend(self.bin_dir / a for a in src.aliases)
|
||||
for exe in exes:
|
||||
fix_mach_o(str(exe), current, target, self.interpreter.max_size)
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter):
|
||||
for _, targets, must, when in super()._executables(interpreter):
|
||||
# Make sure we use the embedded interpreter inside the framework, even if sys.executable points to the
|
||||
# stub executable in ${sys.prefix}/bin.
|
||||
# See http://groups.google.com/group/python-virtualenv/browse_thread/thread/17cab2f85da75951
|
||||
fixed_host_exe = Path(interpreter.prefix) / "Resources" / "Python.app" / "Contents" / "MacOS" / "Python"
|
||||
yield fixed_host_exe, targets, must, when
|
||||
|
||||
@abstractmethod
|
||||
def current_mach_o_image_path(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def desired_mach_o_image_path(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CPython3macOsFramework(CPythonmacOsFramework, CPython3, CPythonPosix):
|
||||
def current_mach_o_image_path(self):
|
||||
return "@executable_path/../../../../Python3"
|
||||
|
||||
def desired_mach_o_image_path(self):
|
||||
return "@executable_path/../.Python"
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter):
|
||||
yield from super().sources(interpreter)
|
||||
|
||||
# add a symlink to the host python image
|
||||
exe = Path(interpreter.prefix) / "Python3"
|
||||
yield PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK)
|
||||
|
||||
@property
|
||||
def reload_code(self):
|
||||
result = super().reload_code
|
||||
return dedent(
|
||||
f"""
|
||||
# the bundled site.py always adds the global site package if we're on python framework build, escape this
|
||||
import sys
|
||||
before = sys._framework
|
||||
try:
|
||||
sys._framework = None
|
||||
{result}
|
||||
finally:
|
||||
sys._framework = before
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def fix_mach_o(exe, current, new, max_size):
|
||||
"""
|
||||
https://en.wikipedia.org/wiki/Mach-O.
|
||||
|
||||
Mach-O, short for Mach object file format, is a file format for executables, object code, shared libraries,
|
||||
dynamically-loaded code, and core dumps. A replacement for the a.out format, Mach-O offers more extensibility and
|
||||
faster access to information in the symbol table.
|
||||
|
||||
Each Mach-O file is made up of one Mach-O header, followed by a series of load commands, followed by one or more
|
||||
segments, each of which contains between 0 and 255 sections. Mach-O uses the REL relocation format to handle
|
||||
references to symbols. When looking up symbols Mach-O uses a two-level namespace that encodes each symbol into an
|
||||
'object/symbol name' pair that is then linearly searched for by first the object and then the symbol name.
|
||||
|
||||
The basic structure—a list of variable-length "load commands" that reference pages of data elsewhere in the file—was
|
||||
also used in the executable file format for Accent. The Accent file format was in turn, based on an idea from Spice
|
||||
Lisp.
|
||||
|
||||
With the introduction of Mac OS X 10.6 platform the Mach-O file underwent a significant modification that causes
|
||||
binaries compiled on a computer running 10.6 or later to be (by default) executable only on computers running Mac
|
||||
OS X 10.6 or later. The difference stems from load commands that the dynamic linker, in previous Mac OS X versions,
|
||||
does not understand. Another significant change to the Mach-O format is the change in how the Link Edit tables
|
||||
(found in the __LINKEDIT section) function. In 10.6 these new Link Edit tables are compressed by removing unused and
|
||||
unneeded bits of information, however Mac OS X 10.5 and earlier cannot read this new Link Edit table format.
|
||||
"""
|
||||
try:
|
||||
logging.debug("change Mach-O for %s from %s to %s", exe, current, new)
|
||||
_builtin_change_mach_o(max_size)(exe, current, new)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logging.warning("Could not call _builtin_change_mac_o: %s. Trying to call install_name_tool instead.", e)
|
||||
try:
|
||||
cmd = ["install_name_tool", "-change", current, new, exe]
|
||||
subprocess.check_call(cmd)
|
||||
except Exception:
|
||||
logging.fatal("Could not call install_name_tool -- you must have Apple's development tools installed")
|
||||
raise
|
||||
|
||||
|
||||
def _builtin_change_mach_o(maxint): # noqa: C901
|
||||
MH_MAGIC = 0xFEEDFACE # noqa: N806
|
||||
MH_CIGAM = 0xCEFAEDFE # noqa: N806
|
||||
MH_MAGIC_64 = 0xFEEDFACF # noqa: N806
|
||||
MH_CIGAM_64 = 0xCFFAEDFE # noqa: N806
|
||||
FAT_MAGIC = 0xCAFEBABE # noqa: N806
|
||||
BIG_ENDIAN = ">" # noqa: N806
|
||||
LITTLE_ENDIAN = "<" # noqa: N806
|
||||
LC_LOAD_DYLIB = 0xC # noqa: N806
|
||||
|
||||
class FileView:
|
||||
"""A proxy for file-like objects that exposes a given view of a file. Modified from macholib."""
|
||||
|
||||
def __init__(self, file_obj, start=0, size=maxint) -> None:
|
||||
if isinstance(file_obj, FileView):
|
||||
self._file_obj = file_obj._file_obj # noqa: SLF001
|
||||
else:
|
||||
self._file_obj = file_obj
|
||||
self._start = start
|
||||
self._end = start + size
|
||||
self._pos = 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<fileview [{self._start:d}, {self._end:d}] {self._file_obj!r}>"
|
||||
|
||||
def tell(self):
|
||||
return self._pos
|
||||
|
||||
def _checkwindow(self, seek_to, op):
|
||||
if not (self._start <= seek_to <= self._end):
|
||||
msg = f"{op} to offset {seek_to:d} is outside window [{self._start:d}, {self._end:d}]"
|
||||
raise OSError(msg)
|
||||
|
||||
def seek(self, offset, whence=0):
|
||||
seek_to = offset
|
||||
if whence == os.SEEK_SET:
|
||||
seek_to += self._start
|
||||
elif whence == os.SEEK_CUR:
|
||||
seek_to += self._start + self._pos
|
||||
elif whence == os.SEEK_END:
|
||||
seek_to += self._end
|
||||
else:
|
||||
msg = f"Invalid whence argument to seek: {whence!r}"
|
||||
raise OSError(msg)
|
||||
self._checkwindow(seek_to, "seek")
|
||||
self._file_obj.seek(seek_to)
|
||||
self._pos = seek_to - self._start
|
||||
|
||||
def write(self, content):
|
||||
here = self._start + self._pos
|
||||
self._checkwindow(here, "write")
|
||||
self._checkwindow(here + len(content), "write")
|
||||
self._file_obj.seek(here, os.SEEK_SET)
|
||||
self._file_obj.write(content)
|
||||
self._pos += len(content)
|
||||
|
||||
def read(self, size=maxint):
|
||||
assert size >= 0 # noqa: S101
|
||||
here = self._start + self._pos
|
||||
self._checkwindow(here, "read")
|
||||
size = min(size, self._end - here)
|
||||
self._file_obj.seek(here, os.SEEK_SET)
|
||||
read_bytes = self._file_obj.read(size)
|
||||
self._pos += len(read_bytes)
|
||||
return read_bytes
|
||||
|
||||
def read_data(file, endian, num=1):
|
||||
"""Read a given number of 32-bits unsigned integers from the given file with the given endianness."""
|
||||
res = struct.unpack(endian + "L" * num, file.read(num * 4))
|
||||
if len(res) == 1:
|
||||
return res[0]
|
||||
return res
|
||||
|
||||
def mach_o_change(at_path, what, value): # noqa: C901
|
||||
"""
|
||||
Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value),
|
||||
provided it's shorter.
|
||||
""" # noqa: D205
|
||||
|
||||
def do_macho(file, bits, endian):
|
||||
# Read Mach-O header (the magic number is assumed read by the caller)
|
||||
_cpu_type, _cpu_sub_type, _file_type, n_commands, _size_of_commands, _flags = read_data(file, endian, 6)
|
||||
# 64-bits header has one more field.
|
||||
if bits == 64: # noqa: PLR2004
|
||||
read_data(file, endian)
|
||||
# The header is followed by n commands
|
||||
for _ in range(n_commands):
|
||||
where = file.tell()
|
||||
# Read command header
|
||||
cmd, cmd_size = read_data(file, endian, 2)
|
||||
if cmd == LC_LOAD_DYLIB:
|
||||
# The first data field in LC_LOAD_DYLIB commands is the offset of the name, starting from the
|
||||
# beginning of the command.
|
||||
name_offset = read_data(file, endian)
|
||||
file.seek(where + name_offset, os.SEEK_SET)
|
||||
# Read the NUL terminated string
|
||||
load = file.read(cmd_size - name_offset).decode()
|
||||
load = load[: load.index("\0")]
|
||||
# If the string is what is being replaced, overwrite it.
|
||||
if load == what:
|
||||
file.seek(where + name_offset, os.SEEK_SET)
|
||||
file.write(value.encode() + b"\0")
|
||||
# Seek to the next command
|
||||
file.seek(where + cmd_size, os.SEEK_SET)
|
||||
|
||||
def do_file(file, offset=0, size=maxint):
|
||||
file = FileView(file, offset, size)
|
||||
# Read magic number
|
||||
magic = read_data(file, BIG_ENDIAN)
|
||||
if magic == FAT_MAGIC:
|
||||
# Fat binaries contain nfat_arch Mach-O binaries
|
||||
n_fat_arch = read_data(file, BIG_ENDIAN)
|
||||
for _ in range(n_fat_arch):
|
||||
# Read arch header
|
||||
_cpu_type, _cpu_sub_type, offset, size, _align = read_data(file, BIG_ENDIAN, 5)
|
||||
do_file(file, offset, size)
|
||||
elif magic == MH_MAGIC:
|
||||
do_macho(file, 32, BIG_ENDIAN)
|
||||
elif magic == MH_CIGAM:
|
||||
do_macho(file, 32, LITTLE_ENDIAN)
|
||||
elif magic == MH_MAGIC_64:
|
||||
do_macho(file, 64, BIG_ENDIAN)
|
||||
elif magic == MH_CIGAM_64:
|
||||
do_macho(file, 64, LITTLE_ENDIAN)
|
||||
|
||||
assert len(what) >= len(value) # noqa: S101
|
||||
|
||||
with open(at_path, "r+b") as f:
|
||||
do_file(f)
|
||||
|
||||
return mach_o_change
|
||||
|
||||
|
||||
class CPython3macOsBrew(CPython3, CPythonPosix):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return is_macos_brew(interpreter) and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def setup_meta(cls, interpreter): # noqa: ARG003
|
||||
meta = BuiltinViaGlobalRefMeta()
|
||||
meta.copy_error = "Brew disables copy creation: https://github.com/Homebrew/homebrew-core/issues/138159"
|
||||
return meta
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CPython3macOsBrew",
|
||||
"CPython3macOsFramework",
|
||||
"CPythonmacOsFramework",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,53 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin
|
||||
|
||||
|
||||
class PyPy(ViaGlobalRefVirtualenvBuiltin, abc.ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter):
|
||||
return interpreter.implementation == "PyPy" and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter):
|
||||
host = Path(interpreter.system_executable)
|
||||
targets = sorted(f"{name}{PyPy.suffix}" for name in cls.exe_names(interpreter))
|
||||
yield host, targets, RefMust.NA, RefWhen.ANY
|
||||
|
||||
@classmethod
|
||||
def executables(cls, interpreter):
|
||||
yield from super().sources(interpreter)
|
||||
|
||||
@classmethod
|
||||
def exe_names(cls, interpreter):
|
||||
return {
|
||||
cls.exe_stem(),
|
||||
"python",
|
||||
f"python{interpreter.version_info.major}",
|
||||
f"python{interpreter.version_info.major}.{interpreter.version_info.minor}",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter):
|
||||
yield from cls.executables(interpreter)
|
||||
for host in cls._add_shared_libs(interpreter):
|
||||
yield PathRefToDest(host, dest=lambda self, s: self.bin_dir / s.name)
|
||||
|
||||
@classmethod
|
||||
def _add_shared_libs(cls, interpreter):
|
||||
# https://bitbucket.org/pypy/pypy/issue/1922/future-proofing-virtualenv
|
||||
python_dir = Path(interpreter.system_executable).resolve().parent
|
||||
yield from cls._shared_libs(python_dir)
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PyPy",
|
||||
]
|
|
@ -0,0 +1,76 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from pathlib import Path
|
||||
|
||||
from virtualenv.create.describe import PosixSupports, Python3Supports, WindowsSupports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest
|
||||
|
||||
from .common import PyPy
|
||||
|
||||
|
||||
class PyPy3(PyPy, Python3Supports, abc.ABC):
|
||||
@classmethod
|
||||
def exe_stem(cls):
|
||||
return "pypy3"
|
||||
|
||||
@classmethod
|
||||
def exe_names(cls, interpreter):
|
||||
return super().exe_names(interpreter) | {"pypy"}
|
||||
|
||||
|
||||
class PyPy3Posix(PyPy3, PosixSupports):
|
||||
"""PyPy 3 on POSIX."""
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir):
|
||||
# glob for libpypy3-c.so, libpypy3-c.dylib, libpypy3.9-c.so ...
|
||||
return python_dir.glob("libpypy3*.*")
|
||||
|
||||
def to_lib(self, src):
|
||||
return self.dest / "lib" / src.name
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter):
|
||||
yield from super().sources(interpreter)
|
||||
# PyPy >= 3.8 supports a standard prefix installation, where older
|
||||
# versions always used a portable/development style installation.
|
||||
# If this is a standard prefix installation, skip the below:
|
||||
if interpreter.system_prefix == "/usr":
|
||||
return
|
||||
# Also copy/symlink anything under prefix/lib, which, for "portable"
|
||||
# PyPy builds, includes the tk,tcl runtime and a number of shared
|
||||
# objects. In distro-specific builds or on conda this should be empty
|
||||
# (on PyPy3.8+ it will, like on CPython, hold the stdlib).
|
||||
host_lib = Path(interpreter.system_prefix) / "lib"
|
||||
stdlib = Path(interpreter.system_stdlib)
|
||||
if host_lib.exists() and host_lib.is_dir():
|
||||
for path in host_lib.iterdir():
|
||||
if stdlib == path:
|
||||
# For PyPy3.8+ the stdlib lives in lib/pypy3.8
|
||||
# We need to avoid creating a symlink to it since that
|
||||
# will defeat the purpose of a virtualenv
|
||||
continue
|
||||
yield PathRefToDest(path, dest=cls.to_lib)
|
||||
|
||||
|
||||
class Pypy3Windows(PyPy3, WindowsSupports):
|
||||
"""PyPy 3 on Windows."""
|
||||
|
||||
@property
|
||||
def less_v37(self):
|
||||
return self.interpreter.version_info.minor < 7 # noqa: PLR2004
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir):
|
||||
# glob for libpypy*.dll and libffi*.dll
|
||||
for pattern in ["libpypy*.dll", "libffi*.dll"]:
|
||||
srcs = python_dir.glob(pattern)
|
||||
yield from srcs
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PyPy3",
|
||||
"PyPy3Posix",
|
||||
"Pypy3Windows",
|
||||
]
|
|
@ -0,0 +1,178 @@
|
|||
"""
|
||||
Virtual environments in the traditional sense are built as reference to the host python. This file allows declarative
|
||||
references to elements on the file system, allowing our system to automatically detect what modes it can support given
|
||||
the constraints: e.g. can the file system symlink, can the files be read, executed, etc.
|
||||
""" # noqa: D205
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from stat import S_IXGRP, S_IXOTH, S_IXUSR
|
||||
|
||||
from virtualenv.info import fs_is_case_sensitive, fs_supports_symlink
|
||||
from virtualenv.util.path import copy, make_exe, symlink
|
||||
|
||||
|
||||
class RefMust:
|
||||
NA = "NA"
|
||||
COPY = "copy"
|
||||
SYMLINK = "symlink"
|
||||
|
||||
|
||||
class RefWhen:
|
||||
ANY = "ANY"
|
||||
COPY = "copy"
|
||||
SYMLINK = "symlink"
|
||||
|
||||
|
||||
class PathRef(ABC):
|
||||
"""Base class that checks if a file reference can be symlink/copied."""
|
||||
|
||||
FS_SUPPORTS_SYMLINK = fs_supports_symlink()
|
||||
FS_CASE_SENSITIVE = fs_is_case_sensitive()
|
||||
|
||||
def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY) -> None:
|
||||
self.must = must
|
||||
self.when = when
|
||||
self.src = src
|
||||
try:
|
||||
self.exists = src.exists()
|
||||
except OSError:
|
||||
self.exists = False
|
||||
self._can_read = None if self.exists else False
|
||||
self._can_copy = None if self.exists else False
|
||||
self._can_symlink = None if self.exists else False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(src={self.src})"
|
||||
|
||||
@property
|
||||
def can_read(self):
|
||||
if self._can_read is None:
|
||||
if self.src.is_file():
|
||||
try:
|
||||
with self.src.open("rb"):
|
||||
self._can_read = True
|
||||
except OSError:
|
||||
self._can_read = False
|
||||
else:
|
||||
self._can_read = os.access(str(self.src), os.R_OK)
|
||||
return self._can_read
|
||||
|
||||
@property
|
||||
def can_copy(self):
|
||||
if self._can_copy is None:
|
||||
if self.must == RefMust.SYMLINK:
|
||||
self._can_copy = self.can_symlink
|
||||
else:
|
||||
self._can_copy = self.can_read
|
||||
return self._can_copy
|
||||
|
||||
@property
|
||||
def can_symlink(self):
|
||||
if self._can_symlink is None:
|
||||
if self.must == RefMust.COPY:
|
||||
self._can_symlink = self.can_copy
|
||||
else:
|
||||
self._can_symlink = self.FS_SUPPORTS_SYMLINK and self.can_read
|
||||
return self._can_symlink
|
||||
|
||||
@abstractmethod
|
||||
def run(self, creator, symlinks):
|
||||
raise NotImplementedError
|
||||
|
||||
def method(self, symlinks):
|
||||
if self.must == RefMust.SYMLINK:
|
||||
return symlink
|
||||
if self.must == RefMust.COPY:
|
||||
return copy
|
||||
return symlink if symlinks else copy
|
||||
|
||||
|
||||
class ExePathRef(PathRef, ABC):
|
||||
"""Base class that checks if a executable can be references via symlink/copy."""
|
||||
|
||||
def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY) -> None:
|
||||
super().__init__(src, must, when)
|
||||
self._can_run = None
|
||||
|
||||
@property
|
||||
def can_symlink(self):
|
||||
if self.FS_SUPPORTS_SYMLINK:
|
||||
return self.can_run
|
||||
return False
|
||||
|
||||
@property
|
||||
def can_run(self):
|
||||
if self._can_run is None:
|
||||
mode = self.src.stat().st_mode
|
||||
for key in [S_IXUSR, S_IXGRP, S_IXOTH]:
|
||||
if mode & key:
|
||||
self._can_run = True
|
||||
break
|
||||
else:
|
||||
self._can_run = False
|
||||
return self._can_run
|
||||
|
||||
|
||||
class PathRefToDest(PathRef):
|
||||
"""Link a path on the file system."""
|
||||
|
||||
def __init__(self, src, dest, must=RefMust.NA, when=RefWhen.ANY) -> None:
|
||||
super().__init__(src, must, when)
|
||||
self.dest = dest
|
||||
|
||||
def run(self, creator, symlinks):
|
||||
dest = self.dest(creator, self.src)
|
||||
method = self.method(symlinks)
|
||||
dest_iterable = dest if isinstance(dest, list) else (dest,)
|
||||
if not dest.parent.exists():
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
for dst in dest_iterable:
|
||||
method(self.src, dst)
|
||||
|
||||
|
||||
class ExePathRefToDest(PathRefToDest, ExePathRef):
|
||||
"""Link a exe path on the file system."""
|
||||
|
||||
def __init__(self, src, targets, dest, must=RefMust.NA, when=RefWhen.ANY) -> None:
|
||||
ExePathRef.__init__(self, src, must, when)
|
||||
PathRefToDest.__init__(self, src, dest, must, when)
|
||||
if not self.FS_CASE_SENSITIVE:
|
||||
targets = list(OrderedDict((i.lower(), None) for i in targets).keys())
|
||||
self.base = targets[0]
|
||||
self.aliases = targets[1:]
|
||||
self.dest = dest
|
||||
|
||||
def run(self, creator, symlinks):
|
||||
bin_dir = self.dest(creator, self.src).parent
|
||||
dest = bin_dir / self.base
|
||||
method = self.method(symlinks)
|
||||
method(self.src, dest)
|
||||
if not symlinks:
|
||||
make_exe(dest)
|
||||
for extra in self.aliases:
|
||||
link_file = bin_dir / extra
|
||||
if link_file.exists():
|
||||
link_file.unlink()
|
||||
if symlinks:
|
||||
link_file.symlink_to(self.base)
|
||||
else:
|
||||
copy(self.src, link_file)
|
||||
if not symlinks:
|
||||
make_exe(link_file)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(src={self.src}, alias={self.aliases})"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ExePathRef",
|
||||
"ExePathRefToDest",
|
||||
"PathRef",
|
||||
"PathRefToDest",
|
||||
"RefMust",
|
||||
"RefWhen",
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue