Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions PyAutoEnv.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ function Invoke-PyAutoEnv() {
}
$pyAutoEnv = Join-Path "${pyAutoEnvDir}" "pyautoenv.py"
if (Test-Path "${pyAutoEnv}") {
$expression = "$(python "${pyAutoEnv}" --pwsh)"
if (-Not "${Env:PYAUTOENV_DEBUG}" -Or "${Env:PYAUTOENV_DEBUG}" -Eq "0") {
$expression = "$(python -OO "${pyAutoEnv}" --pwsh)"
}
else {
$expression = "$(python "${pyAutoEnv}" --pwsh)"
}
if (${expression}) {
Invoke-Expression "${expression}"
}
Expand All @@ -48,7 +53,7 @@ function Invoke-PyAutoEnv() {
#>
function Invoke-PyAutoEnvVersion() {
$pyAutoEnv = Join-Path "${pyAutoEnvDir}" "pyautoenv.py"
python "${pyAutoEnv}" --version
python -O "${pyAutoEnv}" --version
}

<#
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ There are some environment variables you can set to configure `pyautoenv`.
The directories, and their children,
will be treated as though no virtual environment exists for them.
This means any active environment will be deactivated when changing to them.
- `PYAUTOENV_DEBUG`: Set to a non-zero value to enable logging.
When active, you can also use `PYAUTOENV_LOG_LEVEL`
to set the logging level to something supported by Python's `logging` module.
The default log level is `DEBUG`.

## Contributing

Expand Down
10 changes: 7 additions & 3 deletions pyautoenv.bash
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if ! [ "$(type cd)" == "cd is a shell builtin" ]; then
return
fi

_pyautoenv_path="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)"
_pyautoenv_path="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"

function _pyautoenv_activate() {
if [ "${PYAUTOENV_DISABLE-0}" -ne 0 ]; then
Expand All @@ -33,12 +33,16 @@ function _pyautoenv_activate() {
fi
local pyautoenv_py="${_pyautoenv_path}/pyautoenv.py"
if [ -f "${pyautoenv_py}" ]; then
eval "$(python3 "${pyautoenv_py}")"
if [ "${PYAUTOENV_DEBUG-0}" -ne 0 ]; then
eval "$(python3 "${pyautoenv_py}")"
else
eval "$(python3 -OO "${pyautoenv_py}")"
fi
fi
}

function _pyautoenv_version() {
python3 "${_pyautoenv_path}/pyautoenv.py" --version
python3 -O "${_pyautoenv_path}/pyautoenv.py" --version
}

function cd() {
Expand Down
10 changes: 7 additions & 3 deletions pyautoenv.fish
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ function _pyautoenv_activate \
if ! command --search python3 >/dev/null
return
end
set _pyautoenv_py "$_pyautoenv_path/pyautoenv.py"
set --local _pyautoenv_py "$_pyautoenv_path/pyautoenv.py"
if test -f "$_pyautoenv_py"
eval (python3 "$_pyautoenv_py" --fish)
if not set -q PYAUTOENV_DEBUG; or test $PYAUTOENV_DEBUG -eq 0
eval (python3 -OO "$_pyautoenv_py" --fish)
else
eval (python3 "$_pyautoenv_py" --fish)
end
end
end

function _pyautoenv_version --description 'Print pyautoenv version'
python3 "$_pyautoenv_path/pyautoenv.py" --version
python3 -O "$_pyautoenv_path/pyautoenv.py" --version
end

emit _pyautoenv_fish_init
8 changes: 6 additions & 2 deletions pyautoenv.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ function _pyautoenv_activate() {
fi
local pyautoenv_py="${_pyautoenv_path}/pyautoenv.py"
if [ -f "${pyautoenv_py}" ]; then
eval "$(python3 "${pyautoenv_py}")"
if [ "${PYAUTOENV_DEBUG-0}" -ne 0 ]; then
eval "$(python3 "${pyautoenv_py}")"
else
eval "$(python3 -OO "${pyautoenv_py}")"
fi
fi
}

function _pyautoenv_version() {
python3 "${_pyautoenv_path}/pyautoenv.py" --version
python3 -O "${_pyautoenv_path}/pyautoenv.py" --version
}

# We need to make sure the shell is fully initialised before we activate the
Expand Down
100 changes: 80 additions & 20 deletions pyautoenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
To specify specific directories where pyautoenv should not activate
environments, add the directory's path to the 'PYAUTOENV_IGNORE_DIR'
environment variable. Paths should be separated using a ';'.

When running the script with __debug__, the logging level can be set
using the 'PYAUTOENV_LOG_LEVEL' environment variable. The level can be
set to any supported by Python's 'logging' module.
"""

import os
Expand All @@ -51,6 +55,24 @@
"""Directory names to search in for venv virtual environments."""


if __debug__:
import logging

LOG_LEVEL = "PYAUTOENV_LOG_LEVEL"
"""The level to set the logger at."""

logging.basicConfig(
level=getattr(
logging,
os.environ.get(LOG_LEVEL, "DEBUG").upper(),
logging.DEBUG,
),
stream=sys.stderr,
format="%(name)s: %(levelname)s: [%(asctime)s]: %(message)s",
)
logger = logging.getLogger("pyautoenv")


class Args:
"""Container for command line arguments."""

Expand All @@ -76,30 +98,62 @@ class Os:

def main(sys_args: List[str], stdout: TextIO) -> int:
"""Write commands to activate/deactivate environments."""
if __debug__:
logger.debug("main(%s)", sys_args)
args = parse_args(sys_args, stdout)
if not os.path.isdir(args.directory):
logger.warning("path '%s' is not a directory", args.directory)
return 1
new_activator = discover_env(args)
active_env_dir = os.environ.get("VIRTUAL_ENV", None)
active_env_dir = active_environment()
if active_env_dir:
if not new_activator:
stdout.write("deactivate")
elif not activator_in_venv(
new_activator,
active_env_dir,
) and os.path.isfile(new_activator):
stdout.write(f"deactivate && . {new_activator}")
elif new_activator and os.path.isfile(new_activator):
stdout.write(f". '{new_activator}'")
deactivate(stdout)
elif not activator_in_venv(new_activator, active_env_dir):
deactivate_and_activate(stdout, new_activator)
elif new_activator:
activate(stdout, new_activator)
return 0


def activate(stream: TextIO, activator: str) -> None:
"""Write the command to execute the given venv activator."""
command = f". '{activator}'"
if __debug__:
logger.debug("activate: '%s'", command)
stream.write(command)


def deactivate(stream: TextIO) -> None:
"""Write the deactivation command to the given stream."""
command = "deactivate"
if __debug__:
logger.debug("deactivate: '%s'", command)
stream.write(command)


def deactivate_and_activate(stream: TextIO, new_activator: str) -> None:
"""Write command to deactivate the current env and activate another."""
command = f"deactivate && . {new_activator}"
if __debug__:
logger.debug("deactivate_and_activate: '%s'", command)
stream.write(command)


def activator_in_venv(activator_path: str, venv_dir: str) -> bool:
"""Return True if the given activator is in the given venv directory."""
activator_venv_dir = os.path.dirname(os.path.dirname(activator_path))
return os.path.samefile(activator_venv_dir, venv_dir)


def active_environment() -> Union[str, None]:
"""Return the directory of the currently active environment."""
active_env_dir = os.environ.get("VIRTUAL_ENV", None)
if __debug__:
logger.debug("active_environment: '%s'", active_env_dir)
return active_env_dir


def parse_args(argv: List[str], stdout: TextIO) -> Args:
"""Parse the sequence of command line arguments."""
# Avoiding argparse gives a good speed boost and the parsing logic
Expand Down Expand Up @@ -136,19 +190,23 @@ def parse_flag(argv: List[str], flag: str) -> bool:
raise ValueError(
f"exactly one positional argument expected, found {len(argv)}",
)
directory = os.path.abspath(argv[0]) if len(argv) else os.getcwd()
directory = os.path.abspath(argv[0]) if argv else os.getcwd()
return Args(directory=directory, fish=fish, pwsh=pwsh)


def discover_env(args: Args) -> Union[str, None]:
"""Find an environment in the given directory or any of its parents."""
"""Find an environment activator in or above the given directory."""
while (not dir_is_ignored(args.directory)) and (
args.directory != os.path.dirname(args.directory)
):
env_dir = get_virtual_env(args)
if env_dir:
return env_dir
env_activator = get_virtual_env(args)
if env_activator:
if __debug__:
logger.debug("discover_env: '%s'", env_activator)
return env_activator
args.directory = os.path.dirname(args.directory)
if __debug__:
logger.debug("discover_env: 'None'")
return None


Expand All @@ -157,7 +215,6 @@ def dir_is_ignored(directory: str) -> bool:
return any(directory == ignored for ignored in ignored_dirs())


@lru_cache(maxsize=128)
def ignored_dirs() -> List[str]:
"""Get the list of directories to not activate an environment within."""
dirs = os.environ.get(IGNORE_DIRS, None)
Expand All @@ -180,7 +237,8 @@ def venv_activator(args: Args) -> Union[str, None]:
"""
Return the venv activator within the given directory.

Return ``None`` if no venv exists.
Return None if the directory does not contain a venv, or the venv
does not contain a suitable activator script.
"""
candidate_venv_dirs = venv_candidate_dirs(args)
for path in candidate_venv_dirs:
Expand Down Expand Up @@ -214,15 +272,17 @@ def has_poetry_env(directory: str) -> bool:

def poetry_activator(args: Args) -> Union[str, None]:
"""
Return the venv activator for a poetry project directory.
Return the activator associated with a poetry project directory.

If there are multiple poetry environments, pick the one with the
latest modification time.
"""
env_list = poetry_env_list(args.directory)
if env_list:
env_dir = max(env_list, key=lambda p: os.stat(p).st_mtime)
return activator(env_dir, args)
env_activator = activator(env_dir, args)
if os.path.isfile(env_activator):
return activator(env_dir, args)
return None


Expand All @@ -249,7 +309,7 @@ def poetry_env_list(directory: str) -> List[str]:
return []


@lru_cache(maxsize=128)
@lru_cache(maxsize=1)
def poetry_cache_dir() -> Union[str, None]:
"""Return the poetry cache directory, or None if it's not found."""
cache_dir = os.environ.get("POETRY_CACHE_DIR", None)
Expand Down Expand Up @@ -396,7 +456,7 @@ def activator(env_directory: str, args: Args) -> str:
return os.path.join(env_directory, dir_name, script)


@lru_cache(maxsize=128)
@lru_cache(maxsize=1)
def operating_system() -> Union[int, None]:
"""
Return the operating system the script's being run on.
Expand Down
1 change: 0 additions & 1 deletion tests/test_poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def fs(self, fs: FakeFilesystem) -> FakeFilesystem:

def setup_method(self):
pyautoenv.poetry_cache_dir.cache_clear()
pyautoenv.ignored_dirs.cache_clear()
self.os_patch = mock.patch(OPERATING_SYSTEM, return_value=self.os)
self.os_patch.start()
os.environ = copy.deepcopy(self.env) # noqa: B003
Expand Down
1 change: 0 additions & 1 deletion tests/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def activator(self) -> str:

def setup_method(self):
pyautoenv.poetry_cache_dir.cache_clear()
pyautoenv.ignored_dirs.cache_clear()
os.environ = {} # noqa: B003
self.os_patch = mock.patch(OPERATING_SYSTEM, return_value=self.os)
self.os_patch.start()
Expand Down