-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpyvenv-activate.sh
More file actions
1148 lines (1000 loc) · 35.4 KB
/
pyvenv-activate.sh
File metadata and controls
1148 lines (1000 loc) · 35.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
#
# pyvenv-activate.sh is a script containing a set of functions to activate
# and deactivate a python virtual environment directly within the current
# shell.
# shellcheck disable=SC2034
# The version of pyvenv-activate.
PYVENV_ACTIVATE_VERSION=3.0
# The file name used to store the path of the virtual environment when using
# venv projects.
PYVENV_ACTIVATE_VENV_PATH_FILE_NAME=.pyvenv_venv_path
# Use top-level environment instead of lowest level by default.
# By default, we choose the lowest level of environment because this is the
# one that should have the correct environment for the current project.
# Set it to '1' to activate using top-level environment.
# Default is '0', which means that we use the lowest level of environment.
#
# WARNING: Activating this option can have some unwanted behaviour with
# Pipenv.
# Pipenv commands always use the lowest level of environment even if a top
# level environment is already activated.
# So `pipenv run <command>` will not have the expected behaviour.
# This feature is not available for 'uv'.
PYVENV_ACTIVATE_TOP_LEVEL_ENV=0
# {{{ Helpers
# Safe version of echo by using printf.
#
# See
# https://oliviercontant.com/why-is-printf-better-than-echo-in-shell-scripting/
# why echo is not safe with uncontrolled data.
_pyvenv_activate_safe_echo() {
(
IFS=" "
printf '%s\n' "$*"
)
}
# Call builtin cd command even if cd has been redefined.
#
# Some shells use `builtin` for calling the original cd command, others
# use `command`.
#
# shellcheck disable=SC2039
if (builtin printf "123" >/dev/null 2>&1); then
_pyvenv_activate_builtin_cd() {
# shellcheck disable=SC2039,SC2164
builtin cd "$@"
}
else
_pyvenv_activate_builtin_cd() {
command cd "$@" || return 1
}
fi
# Get normalized absolute path of given directory.
_pyvenv_activate_get_dir_abs_path() {
(
_pyvenv_activate_builtin_cd -P -- "$1" && pwd -P
)
}
# }}}
# {{{ Pyvenv activate
# Try to find pipenv module for the given python executable.
#
# Args:
# python_exec: string: the path to the python executable to use.
#
# Returns:
# 0 on if found, 1 if not found.
_pyvenv_activate_find_pipenv_module() {
"$1" - <<EOF
import sys
try:
if sys.version_info >= (3, 4):
import importlib.util
res = importlib.util.find_spec('pipenv')
else:
import pkgutil
res = pkgutil.find_loader('pipenv')
except Exception:
res = None
sys.exit(0 if res else 1)
EOF
}
# Get the current Python interpreter of Pipenv by looking at `pipenv`
# executable shebang.
#
# Outputs:
# The path to Python executable.
_pyvenv_activate_get_pipenv_python_exec_from_shebang() {
sed -n '1s/^#!\([^[:space:]]*python[23]*\)$/\1/p' \
"$(command -v pipenv)" 2>/dev/null
}
# Get the current Python interpreter of Pipenv by looking at executables in
# PATH.
#
# Find the first python executable that have the pipenv module.
# We privilege python3 over python2.
#
# Outputs:
# The path to Python executable.
_pyvenv_activate_get_pipenv_python_exec_from_path() {
# Iterate through python3, python2 and python in that order to find a
# valid python executable.
for pa_python_ver in python3 python2 python; do
# Iterate through each directories of $PATH.
while IFS= read -r pa_path; do
if [ -z "$pa_path" ]; then
# Skip empty line.
continue
fi
pa_python_exec="$pa_path/$pa_python_ver"
if [ -r "$pa_python_exec" ] && [ -x "$pa_python_exec" ] \
&& _pyvenv_activate_find_pipenv_module "$pa_python_exec"; then
# pipenv module has been found for the given python
# executable, return now.
_pyvenv_activate_safe_echo "$pa_python_exec"
break 2
fi
done <<EOF
$(printf '%s\n' "$PATH" | tr ':' '\n')
EOF
done
unset pa_python_ver pa_path pa_python_exec
}
# Get the current Python interpreter of Pipenv.
#
# Outputs:
# The path to Python executable.
_pyvenv_activate_get_pipenv_python_exec() {
# First look at the quick resolution with the shebang.
pa_python_exec_="$(_pyvenv_activate_get_pipenv_python_exec_from_shebang)"
if [ -n "$pa_python_exec_" ]; then
echo "$pa_python_exec_"
unset pa_python_exec_
return 0
fi
# Else, look at the executables in PATH.
pa_python_exec_="$(_pyvenv_activate_get_pipenv_python_exec_from_path)"
if [ -n "$pa_python_exec_" ]; then
echo "$pa_python_exec_"
unset pa_python_exec_
return 0
fi
# Not found :(
unset pa_python_exec_
return 1
}
# Get dotenv variables by loading dotenv file with Python dotenv module.
#
# Run python code to load the dotenv file using the dotenv module with Pipenv
# python interpreter.
#
# The script will load the variables from the dotenv file overwriting the
# variables already set in the environment the same way as Pipenv.
#
# The variables are output in the format `$key base64($value)`. Since the
# value is encoded in base64, we are sure there are no special characters that
# can cause issues when parsing it back with the shell.
#
# Args:
# python_exec: string: the path to the python executable to use.
# dotenv_file: string: the path to the dotenv file.
# Outputs:
# The dotenv variables.
_pyvenv_activate_get_dotenv_variables() {
"$1" - "$2" <<EOF
from sys import argv as sys_argv
from base64 import b64encode
try:
# Importing dotenv from pipenv is slow.
# Try first with the regular dotenv package.
from dotenv import dotenv_values
except ImportError:
from pipenv.vendor.dotenv import dotenv_values
dotenv_file = sys_argv[1]
values = dotenv_values(dotenv_file)
for k, v in values.items():
if not isinstance(v, bytes):
v = v.encode('utf-8')
v = b64encode(v).decode('utf-8')
print("{} {}".format(k, v))
EOF
}
# Encode dotenv value in base64 using python.
#
# `base64` command is not POSIX, it is part of GNU coreutils.
# So, the most portable way to do it here is to use python as we are sure that
# its is available for Pipenv.
#
# Args:
# python_exec: string: the path to the python executable to use.
# value: string: the value to encode.
# Outputs:
# The encoded value in base64.
_pyvenv_activate_pipenv_dotenv_encode_value() {
"$1" - "$2" <<EOF
from sys import argv as sys_argv
from base64 import b64encode
v = sys_argv[1]
if not isinstance(v, bytes):
v = v.encode('utf-8')
print(b64encode(v).decode('utf-8'))
EOF
}
# Decode dotenv value as base64 using python.
#
# `base64` command is not POSIX, it is part of GNU coreutils.
# So, the most portable way to do it here is to use python as we are sure that
# its is available for Pipenv.
#
# Args:
# python_exec: string: the path to the python executable to use.
# value: string: the base64 value to decode.
# Outputs:
# The decoded value.
_pyvenv_activate_pipenv_dotenv_decode_value() {
"$1" - "$2" <<EOF
from sys import argv as sys_argv
from base64 import b64decode
v = sys_argv[1]
if not isinstance(v, bytes):
v = v.encode('utf-8')
print(b64decode(v).decode('utf-8'))
EOF
}
# Load variables from dotenv file the same way as Pipenv.
#
# Args:
# proj_dir: string: The path to the Pipenv project.
# Returns:
# 0 on success, 1 on error.
_pyvenv_activate_pipenv_load_dotenv() {
if [ -n "$PIPENV_DONT_LOAD_ENV" ]; then
# Do nothing if PIPENV_DONT_LOAD_ENV is set.
return 0
fi
if [ -n "$PIPENV_DOTENV_LOCATION" ]; then
pa_dotenv_file_="$PIPENV_DOTENV_LOCATION"
else
pa_dotenv_file_="$1/.env"
fi
if ! [ -r "$pa_dotenv_file_" ]; then
# Do nothing if file is not available.
unset pa_dotenv_file_
return 0
fi
# Get the python executable
pa_python_exec_="$(_pyvenv_activate_get_pipenv_python_exec)"
if [ -z "$pa_python_exec_" ]; then
_pyvenv_activate_safe_echo "unable to find python executable" >&2
return 1
fi
# Will contains the list of variables set by the dotenv file.
pa_dotenv_vars_=""
# Will contains the list of existing environment variables
pa_dotenv_existing_vals_=""
# Read variables line by lines.
# Since the values are encoded, we are sure that each line corresponds to
# one and only one variable.
while IFS= read -r pa_dotenv_line_; do
if [ -z "$pa_dotenv_line_" ]; then
# Line is empty, this can happen if the dotenv is empty.
continue
fi
# Split up the key and the value.
IFS=" " read -r pa_dotenv_key_ pa_dotenv_value_ <<EOF
$pa_dotenv_line_
EOF
# Key already exists in shell environment.
# We will need to store it in order to restore it on unload.
if eval "[ -n \"\${$pa_dotenv_key_+x}\" ]"; then
# Get the existing value and encode it.
pa_existing_val_="$(eval "printf '%s' \"\$$pa_dotenv_key_\"")"
pa_existing_val_="$(_pyvenv_activate_pipenv_dotenv_encode_value \
"$pa_python_exec_" "$pa_existing_val_")" || return 1
# Get type of exising variable, if it is exported or not.
# shellcheck disable=SC2039
if export -p | grep -q "$pa_dotenv_key_="; then
pa_existing_kind_='e'
else
pa_existing_kind_='v'
fi
# Create the line to be inserted in the variable.
pa_existing_line_="$(printf '%s %s %s' "$pa_existing_kind_" \
"$pa_dotenv_key_" "$pa_existing_val_")"
if [ -z "$pa_dotenv_existing_vals_" ]; then
pa_dotenv_existing_vals_="$pa_existing_line_"
else
pa_dotenv_existing_vals_="$pa_dotenv_existing_vals_
$pa_existing_line_"
fi
unset pa_existing_val_ pa_existing_kind_ pa_existing_line_
fi
# Add the key to the list of variables.
if [ -z "$pa_dotenv_vars_" ]; then
pa_dotenv_vars_="${pa_dotenv_key_}"
else
pa_dotenv_vars_="${pa_dotenv_vars_}
${pa_dotenv_key_}"
fi
# Decode the value.
pa_dotenv_value_="$(_pyvenv_activate_pipenv_dotenv_decode_value \
"$pa_python_exec_" "$pa_dotenv_value_")" || return 1
# Export the value in the current environment.
export "$pa_dotenv_key_=$pa_dotenv_value_" || return 1
done <<EOF
$(_pyvenv_activate_get_dotenv_variables "$pa_python_exec_" "$pa_dotenv_file_")
EOF
# Set the list of variables to global variable.
_PYVENV_ACTIVATE_PIPENV_DOTENV_VARS="$pa_dotenv_vars_"
# Set the list of existing values to global variable.
_PYVENV_ACTIVATE_PIPENV_DOTENV_EXISTING_VALS="$pa_dotenv_existing_vals_"
unset pa_dotenv_file_ pa_python_exec_ pa_dotenv_vars_ pa_dotenv_line_ \
pa_dotenv_key_ pa_dotenv_value_ pa_current_val_ \
pa_dotenv_existing_vals_
}
# Find project directory containing a Python virtual environment file.
#
# For Pipenv, it respects the PIPENV_MAX_DEPTH, PIPENV_NO_INHERIT and
# PIPENV_PIPFILE environment variables.
#
# Outputs:
# "$proj_type:$proj_dir" with:
# - proj_type: the project type, either "pipenv", "poetry", "uv" or
# "venv".
# - proj_dir: the project root directory.
_pyvenv_activate_find_proj() {
if [ -n "$PIPENV_PIPFILE" ] && [ -r "$PIPENV_PIPFILE" ]; then
# If PIPENV_PIPFILE is set and the file is present, use it instead.
_pyvenv_activate_safe_echo "pipenv:$PIPENV_PIPFILE"
return 0
fi
pa_current_dir_="$PWD"
if [ -n "$PIPENV_NO_INHERIT" ]; then
# PIPENV_NO_INHERIT is set.
pa_pipenv_max_depth_=1
elif [ -z "$PIPENV_MAX_DEPTH" ]; then
# Default PIPENV_MAX_DEPTH is 3 according to Pipenv documentation.
pa_pipenv_max_depth_=3
elif ! [ "$PIPENV_MAX_DEPTH" -ge 1 ] 2>/dev/null; then
# PIPENV_MAX_DEPTH is not an integer or less than 1.
pa_pipenv_max_depth_=1
else
# PIPENV_MAX_DEPTH is an integer greater or equal to 1.
pa_pipenv_max_depth_="$PIPENV_MAX_DEPTH"
fi
pa_i_=0
pa_proj_=
while true; do
pa_proj_file_="$pa_current_dir_/Pipfile.lock"
if [ "$pa_i_" -lt "$pa_pipenv_max_depth_" ] \
&& [ -r "$pa_proj_file_" ]; then
# Pipfile has been found according to the max depth.
pa_proj_="pipenv:$pa_proj_file_"
if [ "$PYVENV_ACTIVATE_TOP_LEVEL_ENV" -eq 0 ]; then
break
fi
fi
pa_proj_file_="$pa_current_dir_/poetry.lock"
if [ -r "$pa_proj_file_" ]; then
# Poetry has been found.
pa_proj_="poetry:$pa_proj_file_"
if [ "$PYVENV_ACTIVATE_TOP_LEVEL_ENV" -eq 0 ]; then
break
fi
fi
pa_proj_file_="$pa_current_dir_/uv.lock"
if [ -r "$pa_proj_file_" ]; then
# UV has been found.
_pyvenv_activate_safe_echo "uv:$pa_proj_file_"
break
fi
# Assuming uv for pylock.toml
pa_proj_file_="$pa_current_dir_/pylock.toml"
if [ -r "$pa_proj_file_" ]; then
# UV has been found.
_pyvenv_activate_safe_echo "uv:$pa_proj_file_"
break
fi
pa_proj_file_="$pa_current_dir_/$PYVENV_ACTIVATE_VENV_PATH_FILE_NAME"
if [ -r "$pa_proj_file_" ]; then
# Venv path file has been found.
pa_proj_="venv:$pa_proj_file_"
if [ "$PYVENV_ACTIVATE_TOP_LEVEL_ENV" -eq 0 ]; then
break
fi
fi
if [ -z "$pa_current_dir_" ] || [ "$pa_current_dir_" = "/" ]; then
# We reached the root directory.
break
fi
pa_i_=$((pa_i_ + 1))
# Use command substitution to get the dirname.
pa_current_dir_="${pa_current_dir_%/*}"
done
if [ -n "$pa_proj_" ]; then
_pyvenv_activate_safe_echo "$pa_proj_"
fi
unset pa_current_dir_ pa_pipenv_max_depth_ pa_i_ pa_proj_ pa_proj_file_
}
# Get python virtual environment directory of the project.
#
# Args:
# proj_file: string: The path to the project file.
# proj_type: string: The type of the project to activate, "pipenv",
# "poetry", "uv", or "venv".
# Outputs:
# The virtual environment directory.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_activate_get_venv_dir() {
pa_proj_file_="$1"
pa_proj_type_="$2"
pa_proj_dir_="${pa_proj_file_%/*}"
case "$pa_proj_type_" in
pipenv)
# Change directory to proj file directory to get the correct
# environment path
if ! (
unset VIRTUAL_ENV;
_pyvenv_activate_builtin_cd "$pa_proj_dir_" && \
pipenv --venv
); then
unset pa_proj_file_ pa_proj_type_
return 1
fi
;;
poetry)
# Change directory to proj file directory to get the correct
# environment path
if ! (
unset VIRTUAL_ENV;
_pyvenv_activate_builtin_cd "$pa_proj_dir_" && \
poetry env info -p
); then
unset pa_proj_file_ pa_proj_type_
return 1
fi
;;
uv)
# Use command substitution to get the dirname.
pa_current_dir_="${pa_proj_file_%/*}"
# The default uv venv dir is in the current '.venv' directory,
# unless `UV_PROJECT_ENVIRONMENT` is set.
if [ -z "$UV_PROJECT_ENVIRONMENT" ]; then
# Default venv directory
_pyvenv_activate_safe_echo "$pa_current_dir_/.venv"
elif [ "${UV_PROJECT_ENVIRONMENT#/}" = "$UV_PROJECT_ENVIRONMENT" ]; then
# Relative $UV_PROJECT_ENVIRONMENT
_pyvenv_activate_safe_echo "$pa_current_dir_/$UV_PROJECT_ENVIRONMENT"
else
# Absolute $UV_PROJECT_ENVIRONMENT
_pyvenv_activate_safe_echo "$UV_PROJECT_ENVIRONMENT"
fi
unset pa_current_dir_
;;
venv)
# stat(1) is not in POSIX, the most portable way to get the rights
# and owner of a file is to use ls and awk.
# shellcheck disable=SC2012
pa_proj_file_infos_="$(ls -ld "$pa_proj_file_" | \
awk '{print $1,$3;}')"
if [ "${pa_proj_file_infos_#* }" != "$(id -nu)" ]; then
_pyvenv_activate_safe_echo \
"'$pa_proj_file_' is not owned by the current user" >&2
unset pa_proj_file_ pa_proj_type_ pa_proj_file_infos_
return 1
fi
if [ "${pa_proj_file_infos_% *}" != '-r--------' ]; then
_pyvenv_activate_safe_echo \
"'$pa_proj_file_' has weak permission, expected read-only by owner (400)" >&2
unset pa_proj_file_ pa_proj_type_ pa_proj_file_infos_
return 1
fi
unset pa_proj_file_infos_
if ! IFS= read -r pa_venv_dir_ < "$pa_proj_file_"; then
_pyvenv_activate_safe_echo "unable to read '$pa_proj_file_'" >&2
unset pa_proj_file_ pa_proj_type_
return 1
fi
if [ "$pa_venv_dir_" = "${pa_venv_dir_#/}" ]; then
_pyvenv_activate_safe_echo "'$pa_venv_dir_' is not an absolute path" >&2
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
return 1
fi
_pyvenv_activate_safe_echo "$pa_venv_dir_"
unset pa_venv_dir_
;;
*)
_pyvenv_activate_safe_echo \
"invalid python virtual environment project type $pa_proj_type_" >&2
unset pa_proj_file_ pa_proj_type_
return 1
;;
esac
unset pa_proj_file_ pa_proj_type_ pa_proj_dir_
return 0
}
# Activate python virtual environment project in the current shell.
#
# Unlike `pipenv shell` or `poetry shell`, this function will not create a
# sub-shell, but will activate the python virtual environment directly the
# current shell.
#
# Args:
# [proj_file]: string: The path to the python virtual environment project
# file.
# Default is to look for the file in the current
# directory.
# [proj_type]: string: The type of the project to activate, "pipenv",
# "poetry", "uv", or "venv".
# If not set, it will be automatically detected.
# [venv_dir]: string: The path to the virtual environment directory to
# activate.
# Default is to use `pipenv --venv`,
# `poetry env info -p`, or manual virtual env path in
# the project directory.
# Returns:
# 0 on success, 1 on error.
_pyvenv_activate_proj() {
pa_proj_file_="$1"
pa_proj_type_="$2"
pa_venv_dir_="$3"
if [ -z "$pa_proj_file_" ]; then
pa_proj_="$(_pyvenv_activate_find_proj)"
pa_proj_file_="${pa_proj_#*:}"
pa_proj_type_="${pa_proj_%%:*}"
unset pa_proj_
if [ -z "$pa_proj_file_" ]; then
_pyvenv_activate_safe_echo "unable to find a valid python virtual environment in $PWD" >&2
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
return 1
fi
fi
if [ -z "$pa_proj_type_" ]; then
pa_base_proj_file_name_="${pa_proj_file_##*/}"
if [ "$pa_base_proj_file_name_" = "Pipfile.lock" ]; then
pa_proj_type_="pipenv"
elif [ "$pa_base_proj_file_name_" = "poetry.lock" ]; then
pa_proj_type_="poetry"
elif [ "$pa_base_proj_file_name_" = "uv.lock" ]; then
pa_proj_type_="uv"
elif [ "$pa_base_proj_file_name_" = "pylock.toml" ]; then
pa_proj_type_="uv"
elif [ "$pa_base_proj_file_name_" = "$PYVENV_ACTIVATE_VENV_PATH_FILE_NAME" ]; then
pa_proj_type_="venv"
else
_pyvenv_activate_safe_echo "unable to find python virtual environment project type for $pa_proj_file_" >&2
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_ pa_base_proj_file_name_
return 1
fi
unset pa_base_proj_file_name_
fi
if [ -z "$pa_venv_dir_" ]; then
if ! pa_venv_dir_="$(_pyvenv_activate_get_venv_dir \
"$pa_proj_file_" "$pa_proj_type_")"; then
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
return 1
fi
fi
if ! [ -f "$pa_venv_dir_/bin/activate" ]; then
_pyvenv_activate_safe_echo "$pa_venv_dir_ is not a valid virtual environment" >&2
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
return 1
fi
if [ -n "$VIRTUAL_ENV" ] && [ "$VIRTUAL_ENV" != "$pa_venv_dir_" ]; then
_pyvenv_activate_safe_echo "another virtual environment is already active" >&2
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
return 1
fi
if [ "$pa_proj_type_" = "pipenv" ]; then
_pyvenv_activate_pipenv_load_dotenv "${pa_proj_file_%/*}" || return 1
export PIPENV_ACTIVE=1
elif [ "$pa_proj_type_" = "poetry" ]; then
export POETRY_ACTIVE=1
fi
# shellcheck disable=SC1090,SC1091
. "$pa_venv_dir_/bin/activate" || return 1
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
return 0
}
# Activate python virtual environment in the current shell.
#
# Unlike `pipenv shell` or `poetry shell`, this function will not create a
# sub-shell, but will activate the python virtual environment directly the
# current shell.
#
# Returns:
# 0 on success, 1 on error.
pyvenv_activate() {
_pyvenv_activate_proj
}
# }}}
# {{{ Pyvenv deactivate
# Unload the dotenv file previous loaded by pyvenv_activate.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_deactivate_pipenv_unload_dotenv() {
# Unset the variables set by the dotenv file.
if [ -n "$_PYVENV_ACTIVATE_PIPENV_DOTENV_VARS" ]; then
while IFS= read -r pa_var_; do
unset "$pa_var_"
done <<EOF
$_PYVENV_ACTIVATE_PIPENV_DOTENV_VARS
EOF
unset pa_var_ _PYVENV_ACTIVATE_PIPENV_DOTENV_VARS
fi
# Restore the existing variables.
if [ -n "$_PYVENV_ACTIVATE_PIPENV_DOTENV_EXISTING_VALS" ]; then
pa_python_exec_="$(_pyvenv_activate_get_pipenv_python_exec)"
if [ -z "$pa_python_exec_" ]; then
_pyvenv_activate_safe_echo "unable to find python executable" >&2
return 1
fi
# Read $_PYVENV_ACTIVATE_PIPENV_DOTENV_EXISTING_VALS line by line.
while IFS= read -r pa_existing_line_; do
# Split line by '.
IFS=" " read -r pa_existing_kind_ pa_existing_key_ pa_existing_val_ <<EOF
$pa_existing_line_
EOF
# Decode the value.
pa_existing_val_="$(_pyvenv_activate_pipenv_dotenv_decode_value \
"$pa_python_exec_" "$pa_existing_val_")" || return 1
# Export the variable or set it as a simple shell variable
# depending of the kind.
if [ "$pa_existing_kind_" = 'e' ]; then
export "$pa_existing_key_=$pa_existing_val_"
else
eval "$pa_existing_key_=\"\$pa_existing_val_\""
fi
done <<EOF
$_PYVENV_ACTIVATE_PIPENV_DOTENV_EXISTING_VALS
EOF
unset pa_existing_line_ pa_existing_kind_ pa_existing_key_ \
_PYVENV_ACTIVATE_PIPENV_DOTENV_EXISTING_VALS
fi
}
# Deactivate python virtual environment in the current shell.
#
# Returns:
# 0 on success, 1 on error.
pyvenv_deactivate() {
if [ -n "$VIRTUAL_ENV" ]; then
command -v deactivate >/dev/null && deactivate nondestructive
unset -f deactivate
fi
unset PIPENV_ACTIVE
_pyvenv_deactivate_pipenv_unload_dotenv || return 1
unset POETRY_ACTIVE
return 0
}
# }}}
# {{{ Pyvenv reactivate
# Deactivate and activate the python virtual environment in the current shell.
#
# It is equivalent of doing `pyvenv_deactivate && pyvenv_activate`.
#
# Returns:
# 0 on success, 1 on error.
pyvenv_reactivate() {
pyvenv_deactivate && pyvenv_activate
}
# }}}
# {{{ Pyvenv auto activate
# {{{ Check project
# Function to be run on prompt or when the current directory is changed to
# auto activate or deactivate the Python virtual environment.
#
# Returns:
# 0 on success, 1 on error.
pyvenv_auto_activate_check_proj() {
pa_proj_="$(_pyvenv_activate_find_proj)"
pa_proj_file_="${pa_proj_#*:}"
pa_proj_type_="${pa_proj_%%:*}"
unset pa_proj_
if [ -n "$_PYVENV_AUTO_ACTIVATE_PROJ_FILE" ] \
&& [ "$pa_proj_file_" != "$_PYVENV_AUTO_ACTIVATE_PROJ_FILE" ]; then
# Deactivate the virtual environment if we have left the project
# directory.
pyvenv_deactivate >&2 || return 1
unset _PYVENV_AUTO_ACTIVATE_PROJ_FILE
fi
if [ -n "$pa_proj_file_" ] && [ -n "$pa_proj_type_" ] \
&& [ "$pa_proj_file_" != "$_PYVENV_AUTO_ACTIVATE_PROJ_FILE" ] \
&& [ -z "$VIRTUAL_ENV" ]; then
pa_venv_dir_="$(_pyvenv_activate_get_venv_dir \
"$pa_proj_file_" "$pa_proj_type_" 2>/dev/null)"
if [ -n "$pa_venv_dir_" ]; then
# Activate the virtual environment if we have entered a new pipenv
# directory and that no virtual environment has been activated
# before.
export _PYVENV_AUTO_ACTIVATE_PROJ_FILE="$pa_proj_file_"
_pyvenv_activate_proj "$pa_proj_file_" "$pa_proj_type_" \
"$pa_venv_dir_" >&2 || return 1
fi
fi
unset pa_proj_file_ pa_proj_type_ pa_venv_dir_
}
# }}}
# {{{ Enable
# Enable auto activate Python virtual environment by redefining the cd
# command.
#
# This should work on any POSIX shell, but there are some drawbacks.
# We need to redefine the command cd with our own function, and we can only
# check the project directory when changing the current directory.
#
# Args:
# mode: string: The auto activate mode to use.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_auto_activate_enable_redefine_cd() {
if [ "$1" = "prompt" ]; then
_pyvenv_activate_safe_echo "prompt mode is not supported when redefining cd" >&2
return 1
fi
# shellcheck disable=SC2039
if [ "$(type cd)" != "cd is a shell builtin" ]; then
_pyvenv_activate_safe_echo "command cd is already redefined" >&2
return 1
fi
cd() {
# shellcheck disable=SC2317
_pyvenv_activate_builtin_cd "$@" && pyvenv_auto_activate_check_proj
}
return 0
}
# Enable auto activate Python virtual environment on change directory.
_pyvenv_auto_activate_bash_chpwd_cmd() {
if [ "$_PYVENV_AUTO_ACTIVATE_OLD_PWD" != "$PWD" ]; then
pyvenv_auto_activate_check_proj
_PYVENV_AUTO_ACTIVATE_OLD_PWD="$PWD"
fi
}
# Enable auto activate Python virtual environment for Bash.
#
# Args:
# mode: string: The auto activate mode to use.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_auto_activate_enable_bash() {
if [ "$1" = "chpwd" ]; then
pa_cmd_="_pyvenv_auto_activate_bash_chpwd_cmd"
else
pa_cmd_="pyvenv_auto_activate_check_proj"
fi
_pyvenv_auto_activate_disable_bash || return 1
PROMPT_COMMAND="${pa_cmd_}${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
unset pa_cmd_
return 0
}
# Enable auto activate Python virtual environment for Zsh.
#
# Args:
# mode: string: The auto activate mode to use.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_auto_activate_enable_zsh() {
if [ "$1" = "chpwd" ]; then
pa_hook_="chpwd"
else
pa_hook_="precmd"
fi
autoload -Uz add-zsh-hook || return 1
_pyvenv_auto_activate_disable_zsh || return 1
add-zsh-hook "$pa_hook_" pyvenv_auto_activate_check_proj || return 1
unset pa_hook_
return 0
}
# Enable auto activate Python virtual environment.
#
# Args:
# [mode]: string: The auto activate mode to use.
# It can be one of the following:
# - prompt: The Python virtual environment is checked and
# activated on prompt.
# Since pyvenv_auto_activate_check_proj() is a no-op most of
# the time and is fast enough in that case, this is the best
# option when available.
# This mode is only supported for Bash and Zsh.
# - chpwd: The Python virtual environment is checked and
# activated when changing directory.
# - default: The default mode, use prompt mode when
# available, chpwd otherwise.
#
# Returns:
# 0 on success, 1 on error.
pyvenv_auto_activate_enable() {
pa_mode_="${1:-default}"
case "$pa_mode_" in
prompt|chpwd|default)
;;
*)
_pyvenv_activate_safe_echo "unknow mode $pa_mode_" >&2
return 1
;;
esac
if [ -n "$BASH_VERSION" ]; then
_pyvenv_auto_activate_enable_bash "$pa_mode_" || return 1
elif [ -n "$ZSH_VERSION" ]; then
_pyvenv_auto_activate_enable_zsh "$pa_mode_" || return 1
else
_pyvenv_auto_activate_enable_redefine_cd "$pa_mode_" || return 1
fi
_PYVENV_AUTO_ACTIVATE_ENABLED=1
unset pa_mode_
return 0
}
# }}}
# {{{ Disable
# Disable auto activate Python virtual environment when redefining the cd
# command.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_auto_activate_disable_redefine_cd() {
# shellcheck disable=SC2039
if [ "$(type cd)" = "cd is a shell builtin" ]; then
# Nothing to do.
return 0
fi
unset -f cd || return 1
return 0
}
# Disable auto activate Python virtual environment on prompt for Bash.
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_auto_activate_disable_bash() {
PROMPT_COMMAND="$(_pyvenv_activate_safe_echo "$PROMPT_COMMAND" | \
sed -E -e 's/pyvenv_auto_activate_check_proj;?//g' \
-e 's/_pyvenv_auto_activate_bash_chpwd_cmd;?//g')"
}
# Disable auto activate Python virtual environment on prompt for Zsh
#
# Returns:
# 0 on success, 1 on error.
_pyvenv_auto_activate_disable_zsh() {
autoload -Uz add-zsh-hook || return 1
add-zsh-hook -D precmd pyvenv_auto_activate_check_proj || return 1
add-zsh-hook -D chpwd pyvenv_auto_activate_check_proj || return 1
return 0
}
# Disable auto activate Python virtual environment.
#
# Returns:
# 0 on success, 1 on error.
pyvenv_auto_activate_disable() {
if [ -n "$BASH_VERSION" ]; then
_pyvenv_auto_activate_disable_bash || return 1
elif [ -n "$ZSH_VERSION" ]; then
_pyvenv_auto_activate_disable_zsh || return 1
else
_pyvenv_auto_activate_disable_redefine_cd || return 1
fi
unset _PYVENV_AUTO_ACTIVATE_ENABLED
}
# }}}
# }}}
# {{{ Pyvenv setup venv file path
# Setup pyvenv-activate to work with an existing python virtual environment.
#
# The virtual environment absolute path will be stored in a file named after
# the variable $PYVENV_ACTIVATE_VENV_PATH_FILE_NAME in the project directory.
#