-
-
Save snejus/85b47dca884a5aca2646dfbde79e9c92 to your computer and use it in GitHub Desktop.
See python package's direct and reverse dependencies, and version changes between revisions in milliseconds
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Read poetry.lock and display information about dependencies: | |
# | |
# * Project dependencies | |
# * Sub-dependencies and reverse dependencies of packages | |
# * Summary of updates, or change in dependency versions between two revisions of the project | |
# | |
# Author: Sarunas Nejus, 2021 | |
# License: MIT | |
helpstr=" | |
deps [-lq] [<package>] | |
deps diff [<base-ref>=HEAD] [<target-ref>] | |
-h, --help -- display help | |
deps [-l] -- show top-level project dependencies | |
-l -- include sub-dependencies | |
deps [-q] <package> -- show direct and reverse dependencies for the package | |
-q -- exit with status 0 if package is a dependency, 1 otherwise | |
deps diff [<base-ref>=HEAD] [<target-ref>] | |
-- summarise version changes between two revisions for all dependencies | |
-- it defaults to using a dirty poetry.lock in the current worktree | |
" | |
shopt -s extglob | |
PS4='$EPOCHREALTIME:($BASH_SOURCE:$LINENO): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' | |
R=$'\033[0m' | |
B=$'\033[1m' | |
I=$'\033[3m' | |
RED=$'\033[1;38;5;204m' | |
GREEN=$'\033[1;38;5;150m' | |
YELLOW=$'\033[1;38;5;222m' | |
CYAN=$'\033[1;38;5;117m' | |
MAGENTA=$'\033[1;35m' | |
GREY=$'\033[1;38;5;243m' | |
LOCKFILE=${LOCKFILE:-poetry.lock} | |
PYPROJECT=${PYPROJECT:-pyproject.toml} | |
DEPS_COLOR_PAT=' | |
s/^/\t/ | |
# s/=([<>])/ \1/ | |
s/([a-z_=-]+=)([^@]+)/\1'"$R$B$I\2$R"'/g | |
s/(rich.tables|beetcamp|beets)[^ \t]*/'"$CYAN\1$R"'/g | |
s/([ ,])([<0-9.]+[^@ ]+|[*]|$)/\1'"$RED\2$R"'/g | |
/[>~^]=?/{ | |
/[<~^]+/s/([>~^][0-9.=a-z]+)/'"$YELLOW&$R"'/g | |
/</!s/(>[^ @]+)/'"$GREEN&$R"'/ | |
} | |
' | |
msg() { | |
printf >&2 '%s\n' " · $*" | |
} | |
error() { | |
echo | |
msg "${RED}ERROR:$R $*"$'\n' | |
exit 1 | |
} | |
check_exists() { | |
for file in "$@"; do | |
[[ -r $file ]] || error "$file" is not found in the working directory | |
done | |
} | |
deps_diff() { | |
if ((!$#)) && git diff-files --quiet "$LOCKFILE"; then | |
msg "$B$LOCKFILE$R is no different from the committed or staged version" | |
exit | |
fi | |
git diff -U3 --word-diff=plain --word-diff-regex='[^. ]*' "$@" "$LOCKFILE" | | |
tr -d '\r' | | |
# remove the description line | |
grep -v 'description = ' | | |
# keep valid, *package* version *changes* | |
grep '^.?.?(version|optional) = .*(-\]|\+\})' -EC3 | | |
# duplicate the version line to help with reporting in the next command | |
sed '\/version/p; s/version/& green/' | | |
sed -nr ' | |
# quit once we reach the files metadata section | |
/\[metadata.files\]/q | |
# get package name, make it bold and save it | |
/name = /{ s///; s/.*/'"$B&$R"'/; h; } | |
# get the first version line, remove the updated version and save it | |
/version = /{ s///; s/\{\+[^+]+\+\}//g; s/$/ '$'\b''->/; H; } | |
# get the second version line, remove the old version and save it | |
/version green = /{ s///; s/\[-[^-]+-\]//g; H; } | |
/optional = /{ | |
s/// | |
# take the saved string | |
x | |
# remove double quotes | |
s/"//g | |
# color all removals in red | |
s/\[-([^]]*)-\]/'"$RED\1$R"'/g | |
# color all additions in green | |
s/\{\+([^+]+)\+\}/'"$GREEN\1$R"'/g | |
s/\n/'$'\b''/g | |
p | |
} | |
' | { | |
mapfile -t lines | |
if ! test ${#lines[@]} -eq 0; then | |
echo | |
printf '%s\n' "${lines[@]}" | column -ts$'\b' | |
echo | |
fi | |
} | |
} | |
_requires() { | |
pkg_pat=$1 | |
sed -nr ' | |
# take paragraphs starting with queried package name, until next one | |
/^name = "('"$pkg_pat"')"/I,/\[\[package\]\]/{ | |
# its dependencies section, until next empty line | |
s/name = "([^"]+)"/ '"$B\1$R"' requires/p | |
/\[package.dep.*/,/^$/{ | |
# ignore header | |
//d | |
# remove double quotes, curly brackets and backslashes | |
s/[\\"{}]//g | |
s/ =|$/ '$'\b''/ | |
'"$DEPS_COLOR_PAT"' | |
p | |
} | |
} | |
' "$LOCKFILE" | column -ts$'\b' | |
} | |
_required_by() { | |
pkg_pat=$1 | |
echo " $B${pkg_pat%%\|*}$R is required by" | |
{ | |
sed -nr ' | |
# memorise current package name | |
/^name = "([^"]+)"/{ s//\1/; h; } | |
/^"?('"$pkg_pat"')"? = (["{].*["}])/I{ | |
# take the version or markers | |
s//\2/ | |
s/ ([<>=]) /=\1/ | |
# remove quotes and curly brackets | |
s/[\\"{}]//g | |
# retrieve name and version from the memory | |
H; x | |
# split them by \b | |
s/\n/ '$'\b'' / | |
p | |
} | |
' "$LOCKFILE" && | |
sed -rn ' | |
/^('"$pkg_pat"') = (.*)/{ | |
s//'"$project"' '$'\b'' \2/ | |
s/["{}]//g | |
p | |
} | |
' "$PYPROJECT" | |
} | sed -r "$DEPS_COLOR_PAT" | column -ts$'\b' | |
} | |
long_project_deps() { | |
section=${1:+$1.}dependencies | |
maindeps=($(sed -rn ' | |
/tool.poetry.*'"$section"'/,/^\[/{ | |
//d | |
/^python/d | |
s/^([^ ]+) =.*/\1/p | |
} | |
' "$PYPROJECT" | paste -sd'|')) | |
_requires "$maindeps" | |
} | |
short_project_deps() { | |
section=${1:+$1.}dependencies | |
sed -rn $' | |
/tool.poetry.*'"$section"'/,/^\[/{ | |
//d | |
# skip empty lines, python version and comments | |
/^($|(python ?=|#).*$)/d | |
# remove irrelevant characters and comments | |
s/[]\[{}" ]|#.+//g | |
# when requirement is an object, join members with @ | |
/,([^=]+=)/{ | |
s//'$'\b'' \1/g | |
# version requirement is already clear | |
s/version=//g | |
} | |
# split package name and its requirements | |
s/=/ '$'\b'' / | |
'"$DEPS_COLOR_PAT"' | |
p | |
} | |
' "$PYPROJECT" | column -ts$'\b' | |
} | |
project_deps() { | |
if [[ $1 == long ]]; then | |
func=long_project_deps | |
else | |
func=short_project_deps | |
fi | |
echo | |
echo "$B MAIN DEPENDENCIES$R" | |
$func | |
echo | |
echo "$B DEV DEPENDENCIES$R" | |
$func dev | |
echo | |
} | |
pkg_deps() { | |
pkg=$1 | |
pkg_pat="$pkg|${pkg//_/-}|${pkg//-/_}" | |
pkg_with_ver=$( | |
sed -rn ' | |
/^name = "('"$pkg_pat"')"/I{ s//\1/; h; } | |
/^version = "(.+)"/{ | |
s//\1/; H; | |
x; /'"$pkg_pat"'/I{ s/\n/ /; p; q; } | |
} | |
' "$LOCKFILE" | |
) | |
[[ -n $pkg_with_ver ]] || error "Package $B$pkg$R is not found" | |
msg "Package: $B$pkg_with_ver$R" | |
echo | |
_requires "$pkg_pat" | |
echo | |
_required_by "$pkg_pat" | |
echo | |
} | |
show_help() { | |
sed -r ' | |
### Comments | |
s/-- .*/'"$GREY&$R"'/ | |
### Optional arguments | |
# within brackets | |
s/(\W)(-?-(\w|[-])+)/\1'"$B$YELLOW\2$R"'/g | |
### Commands | |
/^( +)([_a-z][^ A-Z]*)( +|\t| *$)/s//\1'"$B$CYAN\2$R"'\3/ | |
# <arg> | |
/<[^>]+>/s//'"$B$MAGENTA&$R"'/g | |
### Default values | |
# =arg|=ARG | |
/=((\w|-)+)/s//='"$B$GREEN\1$R"'/g | |
### Punctuation | |
s/(\]+)( |$)/'"$B$YELLOW\1$R"'\2/g | |
s/([m ])(\[+)/\1'"$B$YELLOW\2$R"'/g | |
' <<<"$helpstr" | |
} | |
[[ " $* " == *" -q "* ]] && quiet=1 | |
_pkg=(${@#-q}) | |
pkg=${_pkg[0]} | |
while read -r line; do | |
if [[ $line =~ ^name.=.\"([^\"]+)\" ]]; then | |
project=${BASH_REMATCH[1]} | |
elif [[ $line =~ ^version.=.\"([^\"]+)\" ]]; then | |
version=${BASH_REMATCH[1]} | |
elif [[ $line =~ ^python.=.\"([^\"]+)\" ]]; then | |
python_req=${BASH_REMATCH[1]} | |
break | |
fi | |
done <"$PYPROJECT" | |
args=(${@##-*}) | |
flags=(${@##[A-Za-z]*}) | |
if ((!quiet)) && [[ ${args[0]} != diff ]]; then | |
msg "Project: $B$project$R" | |
msg "Version: $B$version$R" | |
msg "Python: $B$python_req$R" | |
fi | |
for flag in "${flags[@]}"; do | |
case $flag in | |
-d | --debug) set -x ;; | |
-l | --long) long=1 ;; | |
-q | --quiet) quiet=1 ;; | |
--help | -h | help) | |
show_help | |
exit | |
;; | |
esac | |
done | |
if ((!$#)); then | |
check_exists "$PYPROJECT" | |
project_deps | |
elif ((long)); then | |
check_exists "$PYPROJECT" "$LOCKFILE" | |
project_deps long | |
elif ((quiet)); then | |
pkg_deps "${args[@]}" &>/dev/null | |
elif [[ ${args[0]} == diff ]]; then | |
deps_diff "${@:2}" | |
elif ((${#args[@]})); then | |
pkg_deps "${args[@]}" | |
fi |
deps diff
does not anymore require a version change in order to show the change - dependencies being moved between main / optional will now also trigger it
Fix: dependencies with names starting with python are not anymore ignored in the project dependencies view
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A short demo:
See below for some example outputs