summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'eclass/python-utils-r1.eclass')
-rw-r--r--eclass/python-utils-r1.eclass1075
1 files changed, 593 insertions, 482 deletions
diff --git a/eclass/python-utils-r1.eclass b/eclass/python-utils-r1.eclass
index 4af710da363b..bbf751399476 100644
--- a/eclass/python-utils-r1.eclass
+++ b/eclass/python-utils-r1.eclass
@@ -1,4 +1,4 @@
-# Copyright 1999-2020 Gentoo Authors
+# Copyright 1999-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
# @ECLASS: python-utils-r1.eclass
@@ -7,7 +7,7 @@
# @AUTHOR:
# Author: Michał Górny <mgorny@gentoo.org>
# Based on work of: Krzysztof Pawlik <nelchael@gentoo.org>
-# @SUPPORTED_EAPIS: 5 6 7
+# @SUPPORTED_EAPIS: 7 8
# @BLURB: Utility functions for packages with Python parts.
# @DESCRIPTION:
# A utility eclass providing functions to query Python implementations,
@@ -17,35 +17,46 @@
# functions. It can be inherited safely.
#
# For more information, please see the Python Guide:
-# https://dev.gentoo.org/~mgorny/python-guide/
+# https://projects.gentoo.org/python/guide/
-case "${EAPI:-0}" in
- [0-4]) die "Unsupported EAPI=${EAPI:-0} (too old) for ${ECLASS}" ;;
- [5-7]) ;;
- *) die "Unsupported EAPI=${EAPI} (unknown) for ${ECLASS}" ;;
-esac
+# NOTE: When dropping support for EAPIs here, we need to update
+# metadata/install-qa-check.d/60python-pyc
+# See bug #704286, bug #781878
-if [[ ${_PYTHON_ECLASS_INHERITED} ]]; then
- die 'python-r1 suite eclasses can not be used with python.eclass.'
-fi
+case ${EAPI} in
+ 7|8) ;;
+ *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;;
+esac
-if [[ ! ${_PYTHON_UTILS_R1} ]]; then
+if [[ ! ${_PYTHON_UTILS_R1_ECLASS} ]]; then
+_PYTHON_UTILS_R1_ECLASS=1
-[[ ${EAPI} == 5 ]] && inherit eutils multilib
-inherit toolchain-funcs
+[[ ${EAPI} == 7 ]] && inherit eapi8-dosym
+inherit multiprocessing toolchain-funcs
-# @ECLASS-VARIABLE: _PYTHON_ALL_IMPLS
+# @ECLASS_VARIABLE: _PYTHON_ALL_IMPLS
# @INTERNAL
# @DESCRIPTION:
# All supported Python implementations, most preferred last.
_PYTHON_ALL_IMPLS=(
pypy3
- python2_7
- python3_6 python3_7 python3_8
+ python3_{10..12}
)
readonly _PYTHON_ALL_IMPLS
-# @ECLASS-VARIABLE: PYTHON_COMPAT_NO_STRICT
+# @ECLASS_VARIABLE: _PYTHON_HISTORICAL_IMPLS
+# @INTERNAL
+# @DESCRIPTION:
+# All historical Python implementations that are no longer supported.
+_PYTHON_HISTORICAL_IMPLS=(
+ jython2_7
+ pypy pypy1_{8,9} pypy2_0
+ python2_{5..7}
+ python3_{1..9}
+)
+readonly _PYTHON_HISTORICAL_IMPLS
+
+# @ECLASS_VARIABLE: PYTHON_COMPAT_NO_STRICT
# @INTERNAL
# @DESCRIPTION:
# Set to a non-empty value in order to make eclass tolerate (ignore)
@@ -57,36 +68,30 @@ readonly _PYTHON_ALL_IMPLS
# which can involve revisions of this eclass that support a different
# set of Python implementations.
-# @FUNCTION: _python_impl_supported
-# @USAGE: <impl>
+# @FUNCTION: _python_verify_patterns
+# @USAGE: <pattern>...
# @INTERNAL
# @DESCRIPTION:
-# Check whether the implementation <impl> (PYTHON_COMPAT-form)
-# is still supported.
-#
-# Returns 0 if the implementation is valid and supported. If it is
-# unsupported, returns 1 -- and the caller should ignore the entry.
-# If it is invalid, dies with an appopriate error messages.
-_python_impl_supported() {
+# Verify whether the patterns passed to the eclass function are correct
+# (i.e. can match any valid implementation). Dies on wrong pattern.
+_python_verify_patterns() {
debug-print-function ${FUNCNAME} "${@}"
- [[ ${#} -eq 1 ]] || die "${FUNCNAME}: takes exactly 1 argument (impl)."
+ local impl pattern
+ for pattern; do
+ case ${pattern} in
+ -[23]|3.[89]|3.1[012])
+ continue
+ ;;
+ esac
- local impl=${1}
+ for impl in "${_PYTHON_ALL_IMPLS[@]}" "${_PYTHON_HISTORICAL_IMPLS[@]}"
+ do
+ [[ ${impl} == ${pattern/./_} ]] && continue 2
+ done
- # keep in sync with _PYTHON_ALL_IMPLS!
- # (not using that list because inline patterns shall be faster)
- case "${impl}" in
- python2_7|python3_[678]|pypy3)
- return 0
- ;;
- jython2_7|pypy|pypy1_[89]|pypy2_0|python2_[56]|python3_[12345])
- return 1
- ;;
- *)
- [[ ${PYTHON_COMPAT_NO_STRICT} ]] && return 1
- die "Invalid implementation in PYTHON_COMPAT: ${impl}"
- esac
+ die "Invalid implementation pattern: ${pattern}"
+ done
}
# @FUNCTION: _python_set_impls
@@ -109,16 +114,44 @@ _python_impl_supported() {
_python_set_impls() {
local i
- if ! declare -p PYTHON_COMPAT &>/dev/null; then
- die 'PYTHON_COMPAT not declared.'
+ # TODO: drop BASH_VERSINFO check when we require EAPI 8
+ if [[ ${BASH_VERSINFO[0]} -ge 5 ]]; then
+ [[ ${PYTHON_COMPAT@a} == *a* ]]
+ else
+ [[ $(declare -p PYTHON_COMPAT) == "declare -a"* ]]
+ fi
+ if [[ ${?} -ne 0 ]]; then
+ if ! declare -p PYTHON_COMPAT &>/dev/null; then
+ die 'PYTHON_COMPAT not declared.'
+ else
+ die 'PYTHON_COMPAT must be an array.'
+ fi
fi
- if [[ $(declare -p PYTHON_COMPAT) != "declare -a"* ]]; then
- die 'PYTHON_COMPAT must be an array.'
+
+ local obsolete=()
+ if [[ ! ${PYTHON_COMPAT_NO_STRICT} ]]; then
+ for i in "${PYTHON_COMPAT[@]}"; do
+ # check for incorrect implementations
+ # we're using pattern matching as an optimization
+ # please keep them in sync with _PYTHON_ALL_IMPLS
+ # and _PYTHON_HISTORICAL_IMPLS
+ case ${i} in
+ pypy3|python3_9|python3_1[0-2])
+ ;;
+ jython2_7|pypy|pypy1_[89]|pypy2_0|python2_[5-7]|python3_[1-9])
+ obsolete+=( "${i}" )
+ ;;
+ *)
+ if has "${i}" "${_PYTHON_ALL_IMPLS[@]}" \
+ "${_PYTHON_HISTORICAL_IMPLS[@]}"
+ then
+ die "Mis-synced patterns in _python_set_impls: missing ${i}"
+ else
+ die "Invalid implementation in PYTHON_COMPAT: ${i}"
+ fi
+ esac
+ done
fi
- for i in "${PYTHON_COMPAT[@]}"; do
- # trigger validity checks
- _python_impl_supported "${i}"
- done
local supp=() unsupp=()
@@ -163,34 +196,55 @@ _python_set_impls() {
# of the patterns following it. Return 0 if it does, 1 otherwise.
# Matches if no patterns are provided.
#
-# <impl> can be in PYTHON_COMPAT or EPYTHON form. The patterns can be
-# either:
-# a) fnmatch-style patterns, e.g. 'python2*', 'pypy'...
-# b) '-2' to indicate all Python 2 variants (= !python_is_python3)
-# c) '-3' to indicate all Python 3 variants (= python_is_python3)
+# <impl> can be in PYTHON_COMPAT or EPYTHON form. The patterns
+# can either be fnmatch-style or stdlib versions, e.g. "3.8", "3.9".
+# In the latter case, pypy3 will match if there is at least one pypy3
+# version matching the stdlib version.
_python_impl_matches() {
[[ ${#} -ge 1 ]] || die "${FUNCNAME}: takes at least 1 parameter"
[[ ${#} -eq 1 ]] && return 0
- local impl=${1} pattern
+ local impl=${1/./_} pattern
shift
for pattern; do
- if [[ ${pattern} == -2 ]]; then
- python_is_python3 "${impl}" || return 0
- elif [[ ${pattern} == -3 ]]; then
- python_is_python3 "${impl}" && return 0
- return
- # unify value style to allow lax matching
- elif [[ ${impl/./_} == ${pattern/./_} ]]; then
- return 0
- fi
+ case ${pattern} in
+ -2|python2*|pypy)
+ if [[ ${EAPI} != 7 ]]; then
+ eerror
+ eerror "Python 2 is no longer supported in Gentoo, please remove Python 2"
+ eerror "${FUNCNAME[1]} calls."
+ die "Passing ${pattern} to ${FUNCNAME[1]} is banned in EAPI ${EAPI}"
+ fi
+ ;;
+ -3)
+ # NB: "python3*" is fine, as "not pypy3"
+ if [[ ${EAPI} != 7 ]]; then
+ eerror
+ eerror "Python 2 is no longer supported in Gentoo, please remove Python 2"
+ eerror "${FUNCNAME[1]} calls."
+ die "Passing ${pattern} to ${FUNCNAME[1]} is banned in EAPI ${EAPI}"
+ fi
+ return 0
+ ;;
+ 3.10)
+ [[ ${impl} == python${pattern/./_} || ${impl} == pypy3 ]] &&
+ return 0
+ ;;
+ 3.8|3.9|3.1[1-2])
+ [[ ${impl} == python${pattern/./_} ]] && return 0
+ ;;
+ *)
+ # unify value style to allow lax matching
+ [[ ${impl} == ${pattern/./_} ]] && return 0
+ ;;
+ esac
done
return 1
}
-# @ECLASS-VARIABLE: PYTHON
+# @ECLASS_VARIABLE: PYTHON
# @DEFAULT_UNSET
# @DESCRIPTION:
# The absolute path to the current Python interpreter.
@@ -209,7 +263,7 @@ _python_impl_matches() {
# /usr/bin/python2.7
# @CODE
-# @ECLASS-VARIABLE: EPYTHON
+# @ECLASS_VARIABLE: EPYTHON
# @DEFAULT_UNSET
# @DESCRIPTION:
# The executable name of the current Python interpreter.
@@ -228,21 +282,6 @@ _python_impl_matches() {
# python2.7
# @CODE
-# @FUNCTION: python_export
-# @USAGE: [<impl>] <variables>...
-# @INTERNAL
-# @DESCRIPTION:
-# Backwards compatibility function. The relevant API is now considered
-# private, please use python_get* instead.
-python_export() {
- debug-print-function ${FUNCNAME} "${@}"
-
- eqawarn "python_export() is part of private eclass API."
- eqawarn "Please call python_get*() instead."
-
- _python_export "${@}"
-}
-
# @FUNCTION: _python_export
# @USAGE: [<impl>] <variables>...
# @INTERNAL
@@ -288,21 +327,30 @@ _python_export() {
debug-print "${FUNCNAME}: EPYTHON = ${EPYTHON}"
;;
PYTHON)
- export PYTHON=${EPREFIX}/usr/bin/${impl}
+ # Under EAPI 7+, this should just use ${BROOT}, but Portage
+ # <3.0.50 was buggy, and prefix users need this to update.
+ export PYTHON=${BROOT-${EPREFIX}}/usr/bin/${impl}
debug-print "${FUNCNAME}: PYTHON = ${PYTHON}"
;;
PYTHON_SITEDIR)
[[ -n ${PYTHON} ]] || die "PYTHON needs to be set for ${var} to be exported, or requested before it"
- # sysconfig can't be used because:
- # 1) pypy doesn't give site-packages but stdlib
- # 2) jython gives paths with wrong case
- PYTHON_SITEDIR=$("${PYTHON}" -c 'import distutils.sysconfig; print(distutils.sysconfig.get_python_lib())') || die
+ PYTHON_SITEDIR=$(
+ "${PYTHON}" - "${EPREFIX}/usr" <<-EOF || die
+ import sys, sysconfig
+ print(sysconfig.get_path("purelib", vars={"base": sys.argv[1]}))
+ EOF
+ )
export PYTHON_SITEDIR
debug-print "${FUNCNAME}: PYTHON_SITEDIR = ${PYTHON_SITEDIR}"
;;
PYTHON_INCLUDEDIR)
[[ -n ${PYTHON} ]] || die "PYTHON needs to be set for ${var} to be exported, or requested before it"
- PYTHON_INCLUDEDIR=$("${PYTHON}" -c 'import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())') || die
+ PYTHON_INCLUDEDIR=$(
+ "${PYTHON}" - "${ESYSROOT}/usr" <<-EOF || die
+ import sys, sysconfig
+ print(sysconfig.get_path("platinclude", vars={"installed_platbase": sys.argv[1]}))
+ EOF
+ )
export PYTHON_INCLUDEDIR
debug-print "${FUNCNAME}: PYTHON_INCLUDEDIR = ${PYTHON_INCLUDEDIR}"
@@ -313,7 +361,17 @@ _python_export() {
;;
PYTHON_LIBPATH)
[[ -n ${PYTHON} ]] || die "PYTHON needs to be set for ${var} to be exported, or requested before it"
- PYTHON_LIBPATH=$("${PYTHON}" -c 'import os.path, sysconfig; print(os.path.join(sysconfig.get_config_var("LIBDIR"), sysconfig.get_config_var("LDLIBRARY")) if sysconfig.get_config_var("LDLIBRARY") else "")') || die
+ PYTHON_LIBPATH=$(
+ "${PYTHON}" - <<-EOF || die
+ import os.path, sysconfig
+ print(
+ os.path.join(
+ sysconfig.get_config_var("LIBDIR"),
+ sysconfig.get_config_var("LDLIBRARY"))
+ if sysconfig.get_config_var("LDLIBRARY")
+ else "")
+ EOF
+ )
export PYTHON_LIBPATH
debug-print "${FUNCNAME}: PYTHON_LIBPATH = ${PYTHON_LIBPATH}"
@@ -341,10 +399,6 @@ _python_export() {
local val
case "${impl}" in
- python2*|python3.6|python3.7*)
- # python* up to 3.7
- val=$($(tc-getPKG_CONFIG) --libs ${impl/n/n-}) || die
- ;;
python*)
# python3.8+
val=$($(tc-getPKG_CONFIG) --libs ${impl/n/n-}-embed) || die
@@ -363,7 +417,13 @@ _python_export() {
case "${impl}" in
python*)
[[ -n ${PYTHON} ]] || die "PYTHON needs to be set for ${var} to be exported, or requested before it"
- flags=$("${PYTHON}" -c 'import sysconfig; print(sysconfig.get_config_var("ABIFLAGS") or "")') || die
+ flags=$(
+ "${PYTHON}" - <<-EOF || die
+ import sysconfig
+ print(sysconfig.get_config_var("ABIFLAGS")
+ or "")
+ EOF
+ )
val=${PYTHON}${flags}-config
;;
*)
@@ -377,14 +437,12 @@ _python_export() {
PYTHON_PKG_DEP)
local d
case ${impl} in
- python2.7)
- PYTHON_PKG_DEP='>=dev-lang/python-2.7.5-r2:2.7';;
python*)
- PYTHON_PKG_DEP="dev-lang/python:${impl#python}";;
- pypy)
- PYTHON_PKG_DEP='>=dev-python/pypy-7.3.0:0=';;
+ PYTHON_PKG_DEP="dev-lang/python:${impl#python}"
+ ;;
pypy3)
- PYTHON_PKG_DEP='>=dev-python/pypy3-7.3.0:0=';;
+ PYTHON_PKG_DEP="dev-python/${impl}:="
+ ;;
*)
die "Invalid implementation: ${impl}"
esac
@@ -512,46 +570,6 @@ python_get_scriptdir() {
echo "${PYTHON_SCRIPTDIR}"
}
-# @FUNCTION: _python_ln_rel
-# @USAGE: <from> <to>
-# @INTERNAL
-# @DESCRIPTION:
-# Create a relative symlink.
-_python_ln_rel() {
- debug-print-function ${FUNCNAME} "${@}"
-
- local target=${1}
- local symname=${2}
-
- local tgpath=${target%/*}/
- local sympath=${symname%/*}/
- local rel_target=
-
- while [[ ${sympath} ]]; do
- local tgseg= symseg=
-
- while [[ ! ${tgseg} && ${tgpath} ]]; do
- tgseg=${tgpath%%/*}
- tgpath=${tgpath#${tgseg}/}
- done
-
- while [[ ! ${symseg} && ${sympath} ]]; do
- symseg=${sympath%%/*}
- sympath=${sympath#${symseg}/}
- done
-
- if [[ ${tgseg} != ${symseg} ]]; then
- rel_target=../${rel_target}${tgseg:+${tgseg}/}
- fi
- done
- rel_target+=${tgpath}${target##*/}
-
- debug-print "${FUNCNAME}: ${symname} -> ${target}"
- debug-print "${FUNCNAME}: rel_target = ${rel_target}"
-
- ln -fs "${rel_target}" "${symname}"
-}
-
# @FUNCTION: python_optimize
# @USAGE: [<directory>...]
# @DESCRIPTION:
@@ -561,19 +579,11 @@ _python_ln_rel() {
python_optimize() {
debug-print-function ${FUNCNAME} "${@}"
- if [[ ${EBUILD_PHASE} == pre* || ${EBUILD_PHASE} == post* ]]; then
- eerror "The new Python eclasses expect the compiled Python files to"
- eerror "be controlled by the Package Manager. For this reason,"
- eerror "the python_optimize function can be used only during src_* phases"
- eerror "(src_install most commonly) and not during pkg_* phases."
- echo
- die "python_optimize is not to be used in pre/post* phases"
- fi
-
[[ ${EPYTHON} ]] || die 'No Python implementation set (EPYTHON is null).'
local PYTHON=${PYTHON}
[[ ${PYTHON} ]] || _python_export PYTHON
+ [[ -x ${PYTHON} ]] || die "PYTHON (${PYTHON}) is not executable"
# default to sys.path
if [[ ${#} -eq 0 ]]; then
@@ -587,30 +597,40 @@ python_optimize() {
if [[ ${f} == /* && -d ${D%/}${f} ]]; then
set -- "${D%/}${f}" "${@}"
fi
- done < <("${PYTHON}" -c 'import sys; print("".join(x + "\0" for x in sys.path))' || die)
+ done < <(
+ "${PYTHON}" - <<-EOF || die
+ import sys
+ print("".join(x + "\0" for x in sys.path))
+ EOF
+ )
debug-print "${FUNCNAME}: using sys.path: ${*/%/;}"
fi
+ local jobs=$(makeopts_jobs)
local d
for d; do
# make sure to get a nice path without //
local instpath=${d#${D%/}}
instpath=/${instpath##/}
+ einfo "Optimize Python modules for ${instpath}"
case "${EPYTHON}" in
- python2.7|python3.[34])
- "${PYTHON}" -m compileall -q -f -d "${instpath}" "${d}"
- "${PYTHON}" -OO -m compileall -q -f -d "${instpath}" "${d}"
+ python3.8)
+ # both levels of optimization are separate since 3.5
+ "${PYTHON}" -m compileall -j "${jobs}" -q -f -d "${instpath}" "${d}"
+ "${PYTHON}" -O -m compileall -j "${jobs}" -q -f -d "${instpath}" "${d}"
+ "${PYTHON}" -OO -m compileall -j "${jobs}" -q -f -d "${instpath}" "${d}"
;;
python*|pypy3)
- # both levels of optimization are separate since 3.5
+ # Python 3.9+
+ "${PYTHON}" -m compileall -j "${jobs}" -o 0 -o 1 -o 2 --hardlink-dupes -q -f -d "${instpath}" "${d}"
+ ;;
+ pypy|jython2.7)
"${PYTHON}" -m compileall -q -f -d "${instpath}" "${d}"
- "${PYTHON}" -O -m compileall -q -f -d "${instpath}" "${d}"
- "${PYTHON}" -OO -m compileall -q -f -d "${instpath}" "${d}"
;;
*)
- "${PYTHON}" -m compileall -q -f -d "${instpath}" "${d}"
+ die "${FUNCNAME}: unexpected EPYTHON=${EPYTHON}"
;;
esac
done
@@ -636,7 +656,7 @@ python_optimize() {
python_scriptinto() {
debug-print-function ${FUNCNAME} "${@}"
- python_scriptroot=${1}
+ _PYTHON_SCRIPTROOT=${1}
}
# @FUNCTION: python_doexe
@@ -650,6 +670,9 @@ python_scriptinto() {
python_doexe() {
debug-print-function ${FUNCNAME} "${@}"
+ [[ ${EBUILD_PHASE} != install ]] &&
+ die "${FUNCNAME} can only be used in src_install"
+
local f
for f; do
python_newexe "${f}" "${f##*/}"
@@ -668,10 +691,12 @@ python_doexe() {
python_newexe() {
debug-print-function ${FUNCNAME} "${@}"
+ [[ ${EBUILD_PHASE} != install ]] &&
+ die "${FUNCNAME} can only be used in src_install"
[[ ${EPYTHON} ]] || die 'No Python implementation set (EPYTHON is null).'
[[ ${#} -eq 2 ]] || die "Usage: ${FUNCNAME} <path> <new-name>"
- local wrapd=${python_scriptroot:-/usr/bin}
+ local wrapd=${_PYTHON_SCRIPTROOT:-/usr/bin}
local f=${1}
local newfn=${2}
@@ -687,8 +712,9 @@ python_newexe() {
)
# install the wrapper
- _python_ln_rel "${ED%/}"/usr/lib/python-exec/python-exec2 \
- "${ED%/}/${wrapd}/${newfn}" || die
+ local dosym=dosym
+ [[ ${EAPI} == 7 ]] && dosym=dosym8
+ "${dosym}" -r /usr/lib/python-exec/python-exec2 "${wrapd}/${newfn}"
# don't use this at home, just call python_doscript() instead
if [[ ${_PYTHON_REWRITE_SHEBANG} ]]; then
@@ -715,6 +741,9 @@ python_newexe() {
python_doscript() {
debug-print-function ${FUNCNAME} "${@}"
+ [[ ${EBUILD_PHASE} != install ]] &&
+ die "${FUNCNAME} can only be used in src_install"
+
local _PYTHON_REWRITE_SHEBANG=1
python_doexe "${@}"
}
@@ -739,6 +768,9 @@ python_doscript() {
python_newscript() {
debug-print-function ${FUNCNAME} "${@}"
+ [[ ${EBUILD_PHASE} != install ]] &&
+ die "${FUNCNAME} can only be used in src_install"
+
local _PYTHON_REWRITE_SHEBANG=1
python_newexe "${@}"
}
@@ -758,10 +790,10 @@ python_newscript() {
# site-packages directory.
#
# In the relative case, the exact path is determined directly
-# by each python_doscript/python_newscript function. Therefore,
-# python_moduleinto can be safely called before establishing the Python
-# interpreter and/or a single call can be used to set the path correctly
-# for multiple implementations, as can be seen in the following example.
+# by each python_domodule invocation. Therefore, python_moduleinto
+# can be safely called before establishing the Python interpreter and/or
+# a single call can be used to set the path correctly for multiple
+# implementations, as can be seen in the following example.
#
# Example:
# @CODE
@@ -774,7 +806,7 @@ python_newscript() {
python_moduleinto() {
debug-print-function ${FUNCNAME} "${@}"
- python_moduleroot=${1}
+ _PYTHON_MODULEROOT=${1}
}
# @FUNCTION: python_domodule
@@ -785,6 +817,10 @@ python_moduleinto() {
# and packages (directories). All listed files will be installed
# for all enabled implementations, and compiled afterwards.
#
+# The files are installed into ${D} when run in src_install() phase.
+# Otherwise, they are installed into ${BUILD_DIR}/install location
+# that is suitable for picking up by distutils-r1 in PEP517 mode.
+#
# Example:
# @CODE
# src_install() {
@@ -798,22 +834,33 @@ python_domodule() {
[[ ${EPYTHON} ]] || die 'No Python implementation set (EPYTHON is null).'
local d
- if [[ ${python_moduleroot} == /* ]]; then
+ if [[ ${_PYTHON_MODULEROOT} == /* ]]; then
# absolute path
- d=${python_moduleroot}
+ d=${_PYTHON_MODULEROOT}
else
# relative to site-packages
local sitedir=$(python_get_sitedir)
- d=${sitedir#${EPREFIX}}/${python_moduleroot//.//}
+ d=${sitedir#${EPREFIX}}/${_PYTHON_MODULEROOT//.//}
fi
- (
- insopts -m 0644
- insinto "${d}"
- doins -r "${@}" || return ${?}
- )
-
- python_optimize "${ED%/}/${d}"
+ if [[ ${EBUILD_PHASE} == install ]]; then
+ (
+ insopts -m 0644
+ insinto "${d}"
+ doins -r "${@}" || return ${?}
+ )
+ python_optimize "${ED%/}/${d}"
+ elif [[ -n ${BUILD_DIR} ]]; then
+ local dest=${BUILD_DIR}/install${EPREFIX}/${d}
+ mkdir -p "${dest}" || die
+ cp -pR "${@}" "${dest}/" || die
+ (
+ cd "${dest}" &&
+ chmod -R a+rX "${@##*/}"
+ ) || die
+ else
+ die "${FUNCNAME} can only be used in src_install or with BUILD_DIR set"
+ fi
}
# @FUNCTION: python_doheader
@@ -832,10 +879,12 @@ python_domodule() {
python_doheader() {
debug-print-function ${FUNCNAME} "${@}"
+ [[ ${EBUILD_PHASE} != install ]] &&
+ die "${FUNCNAME} can only be used in src_install"
[[ ${EPYTHON} ]] || die 'No Python implementation set (EPYTHON is null).'
local includedir=$(python_get_includedir)
- local d=${includedir#${EPREFIX}}
+ local d=${includedir#${ESYSROOT}}
(
insopts -m 0644
@@ -844,20 +893,6 @@ python_doheader() {
)
}
-# @FUNCTION: python_wrapper_setup
-# @USAGE: [<path> [<impl>]]
-# @DESCRIPTION:
-# Backwards compatibility function. The relevant API is now considered
-# private, please use python_setup instead.
-python_wrapper_setup() {
- debug-print-function ${FUNCNAME} "${@}"
-
- eqawarn "python_wrapper_setup() is part of private eclass API."
- eqawarn "Please call python_setup() instead."
-
- _python_wrapper_setup "${@}"
-}
-
# @FUNCTION: _python_wrapper_setup
# @USAGE: [<path> [<impl>]]
# @INTERNAL
@@ -883,8 +918,6 @@ _python_wrapper_setup() {
[[ ${impl} ]] || die "${FUNCNAME}: no impl nor EPYTHON specified."
if [[ ! -x ${workdir}/bin/python ]]; then
- _python_check_dead_variables
-
mkdir -p "${workdir}"/{bin,pkgconfig} || die
# Clean up, in case we were supposed to do a cheap update.
@@ -895,15 +928,6 @@ _python_wrapper_setup() {
local EPYTHON PYTHON
_python_export "${impl}" EPYTHON PYTHON
- local pyver pyother
- if python_is_python3; then
- pyver=3
- pyother=2
- else
- pyver=2
- pyother=3
- fi
-
# Python interpreter
# note: we don't use symlinks because python likes to do some
# symlink reading magic that breaks stuff
@@ -912,10 +936,10 @@ _python_wrapper_setup() {
#!/bin/sh
exec "${PYTHON}" "\${@}"
_EOF_
- cp "${workdir}/bin/python" "${workdir}/bin/python${pyver}" || die
- chmod +x "${workdir}/bin/python" "${workdir}/bin/python${pyver}" || die
+ cp "${workdir}/bin/python" "${workdir}/bin/python3" || die
+ chmod +x "${workdir}/bin/python" "${workdir}/bin/python3" || die
- local nonsupp=( "python${pyother}" "python${pyother}-config" )
+ local nonsupp=( python2 python2-config )
# CPython-specific
if [[ ${EPYTHON} == python* ]]; then
@@ -924,24 +948,22 @@ _python_wrapper_setup() {
exec "${PYTHON}-config" "\${@}"
_EOF_
cp "${workdir}/bin/python-config" \
- "${workdir}/bin/python${pyver}-config" || die
+ "${workdir}/bin/python3-config" || die
chmod +x "${workdir}/bin/python-config" \
- "${workdir}/bin/python${pyver}-config" || die
+ "${workdir}/bin/python3-config" || die
# Python 2.6+.
ln -s "${PYTHON/python/2to3-}" "${workdir}"/bin/2to3 || die
# Python 2.7+.
ln -s "${EPREFIX}"/usr/$(get_libdir)/pkgconfig/${EPYTHON/n/n-}.pc \
- "${workdir}"/pkgconfig/python${pyver}.pc || die
+ "${workdir}"/pkgconfig/python3.pc || die
# Python 3.8+.
- if [[ ${EPYTHON} != python[23].[67] ]]; then
- ln -s "${EPREFIX}"/usr/$(get_libdir)/pkgconfig/${EPYTHON/n/n-}-embed.pc \
- "${workdir}"/pkgconfig/python${pyver}-embed.pc || die
- fi
+ ln -s "${EPREFIX}"/usr/$(get_libdir)/pkgconfig/${EPYTHON/n/n-}-embed.pc \
+ "${workdir}"/pkgconfig/python3-embed.pc || die
else
- nonsupp+=( 2to3 python-config "python${pyver}-config" )
+ nonsupp+=( 2to3 python-config python3-config )
fi
local x
@@ -967,64 +989,27 @@ _python_wrapper_setup() {
export PATH PKG_CONFIG_PATH
}
-# @FUNCTION: python_is_python3
-# @USAGE: [<impl>]
-# @DESCRIPTION:
-# Check whether <impl> (or ${EPYTHON}) is a Python3k variant
-# (i.e. uses syntax and stdlib of Python 3.*).
-#
-# Returns 0 (true) if it is, 1 (false) otherwise.
-python_is_python3() {
- local impl=${1:-${EPYTHON}}
- [[ ${impl} ]] || die "python_is_python3: no impl nor EPYTHON"
-
- [[ ${impl} == python3* || ${impl} == pypy3 ]]
-}
-
-# @FUNCTION: python_is_installed
-# @USAGE: [<impl>]
-# @DESCRIPTION:
-# Check whether the interpreter for <impl> (or ${EPYTHON}) is installed.
-# Uses has_version with a proper dependency string.
-#
-# Returns 0 (true) if it is, 1 (false) otherwise.
-python_is_installed() {
- local impl=${1:-${EPYTHON}}
- [[ ${impl} ]] || die "${FUNCNAME}: no impl nor EPYTHON"
- local hasv_args=()
-
- case ${EAPI} in
- 5|6)
- hasv_args+=( --host-root )
- ;;
- *)
- hasv_args+=( -b )
- ;;
- esac
-
- local PYTHON_PKG_DEP
- _python_export "${impl}" PYTHON_PKG_DEP
- has_version "${hasv_args[@]}" "${PYTHON_PKG_DEP}"
-}
-
# @FUNCTION: python_fix_shebang
# @USAGE: [-f|--force] [-q|--quiet] <path>...
# @DESCRIPTION:
-# Replace the shebang in Python scripts with the current Python
-# implementation (EPYTHON). If a directory is passed, works recursively
-# on all Python scripts.
+# Replace the shebang in Python scripts with the full path
+# to the current Python implementation (PYTHON, including EPREFIX).
+# If a directory is passed, works recursively on all Python scripts
+# found inside the directory tree.
#
-# Only files having a 'python*' shebang will be modified. Files with
-# other shebang will either be skipped when working recursively
-# on a directory or treated as error when specified explicitly.
+# Only files having a Python shebang (a path to any known Python
+# interpreter, optionally preceded by env(1) invocation) will
+# be processed. Files with any other shebang will either be skipped
+# silently when a directory was passed, or an error will be reported
+# for any files without Python shebangs specified explicitly.
#
-# Shebangs matching explicitly current Python version will be left
-# unmodified. Shebangs requesting another Python version will be treated
-# as fatal error, unless --force is given.
+# Shebangs that are compatible with the current Python version will be
+# mangled unconditionally. Incompatible shebangs will cause a fatal
+# error, unless --force is specified.
#
-# --force causes the function to replace even shebangs that require
-# incompatible Python version. --quiet causes the function not to list
-# modified files verbosely.
+# --force causes the function to replace shebangs with incompatible
+# Python version (but not non-Python shebangs). --quiet causes
+# the function not to list modified files verbosely.
python_fix_shebang() {
debug-print-function ${FUNCNAME} "${@}"
@@ -1044,13 +1029,13 @@ python_fix_shebang() {
local path f
for path; do
- local any_correct any_fixed is_recursive
+ local any_fixed is_recursive
[[ -d ${path} ]] && is_recursive=1
while IFS= read -r -d '' f; do
local shebang i
- local error= from=
+ local error= match=
# note: we can't ||die here since read will fail if file
# has no newline characters
@@ -1059,65 +1044,35 @@ python_fix_shebang() {
# First, check if it's shebang at all...
if [[ ${shebang} == '#!'* ]]; then
local split_shebang=()
- read -r -a split_shebang <<<${shebang} || die
-
- # Match left-to-right in a loop, to avoid matching random
- # repetitions like 'python2.7 python2'.
- for i in "${split_shebang[@]}"; do
- case "${i}" in
- *"${EPYTHON}")
- debug-print "${FUNCNAME}: in file ${f#${D%/}}"
- debug-print "${FUNCNAME}: shebang matches EPYTHON: ${shebang}"
-
- # Nothing to do, move along.
- any_correct=1
- from=${EPYTHON}
- break
- ;;
- *python|*python[23])
- debug-print "${FUNCNAME}: in file ${f#${D%/}}"
- debug-print "${FUNCNAME}: rewriting shebang: ${shebang}"
-
- if [[ ${i} == *python2 ]]; then
- from=python2
- if [[ ! ${force} ]]; then
- python_is_python3 "${EPYTHON}" && error=1
- fi
- elif [[ ${i} == *python3 ]]; then
- from=python3
- if [[ ! ${force} ]]; then
- python_is_python3 "${EPYTHON}" || error=1
- fi
- else
- from=python
- fi
- break
- ;;
- *python[23].[0123456789]|*pypy|*pypy3|*jython[23].[0123456789])
- # Explicit mismatch.
- if [[ ! ${force} ]]; then
- error=1
- else
- case "${i}" in
- *python[23].[0123456789])
- from="python[23].[0123456789]";;
- *pypy)
- from="pypy";;
- *pypy3)
- from="pypy3";;
- *jython[23].[0123456789])
- from="jython[23].[0123456789]";;
- *)
- die "${FUNCNAME}: internal error in 2nd pattern match";;
- esac
- fi
- break
- ;;
- esac
- done
+ read -r -a split_shebang <<<${shebang#"#!"} || die
+
+ local in_path=${split_shebang[0]}
+ local from='^#! *[^ ]*'
+ # if the first component is env(1), skip it
+ if [[ ${in_path} == */env ]]; then
+ in_path=${split_shebang[1]}
+ from+=' *[^ ]*'
+ fi
+
+ case ${in_path##*/} in
+ "${EPYTHON}")
+ match=1
+ ;;
+ python|python3)
+ match=1
+ ;;
+ python2|python[23].[0-9]|python3.[1-9][0-9]|pypy|pypy3|jython[23].[0-9])
+ # Explicit mismatch.
+ match=1
+ error=1
+ ;;
+ esac
fi
- if [[ ! ${error} && ! ${from} ]]; then
+ # disregard mismatches in force mode
+ [[ ${force} ]] && error=
+
+ if [[ ! ${match} ]]; then
# Non-Python shebang. Allowed in recursive mode,
# disallowed when specifying file explicitly.
[[ ${is_recursive} ]] && continue
@@ -1129,13 +1084,9 @@ python_fix_shebang() {
fi
if [[ ! ${error} ]]; then
- # We either want to match ${from} followed by space
- # or at end-of-string.
- if [[ ${shebang} == *${from}" "* ]]; then
- sed -i -e "1s:${from} :${EPYTHON} :" "${f}" || die
- else
- sed -i -e "1s:${from}$:${EPYTHON}:" "${f}" || die
- fi
+ debug-print "${FUNCNAME}: in file ${f#${D%/}}"
+ debug-print "${FUNCNAME}: rewriting shebang: ${shebang}"
+ sed -i -e "1s@${from}@#!${EPREFIX}/usr/bin/${EPYTHON}@" "${f}" || die
any_fixed=1
else
eerror "The file has incompatible shebang:"
@@ -1147,25 +1098,17 @@ python_fix_shebang() {
done < <(find -H "${path}" -type f -print0 || die)
if [[ ! ${any_fixed} ]]; then
- local cmd=eerror
- [[ ${EAPI} == 5 ]] && cmd=eqawarn
-
- "${cmd}" "QA warning: ${FUNCNAME}, ${path#${D%/}} did not match any fixable files."
- if [[ ${any_correct} ]]; then
- "${cmd}" "All files have ${EPYTHON} shebang already."
- else
- "${cmd}" "There are no Python files in specified directory."
- fi
-
- [[ ${cmd} == eerror ]] && die "${FUNCNAME} did not match any fixable files (QA warning fatal in EAPI ${EAPI})"
+ eerror "QA error: ${FUNCNAME}, ${path#${D%/}} did not match any fixable files."
+ eerror "There are no Python files in specified directory."
+ die "${FUNCNAME} did not match any fixable files"
fi
done
}
# @FUNCTION: _python_check_locale_sanity
# @USAGE: <locale>
-# @INTERNAL
# @RETURN: 0 if sane, 1 otherwise
+# @INTERNAL
# @DESCRIPTION:
# Check whether the specified locale sanely maps between lowercase
# and uppercase ASCII characters.
@@ -1191,7 +1134,7 @@ python_export_utf8_locale() {
debug-print-function ${FUNCNAME} "${@}"
# If the locale program isn't available, just return.
- type locale >/dev/null || return 0
+ type locale &>/dev/null || return 0
if [[ $(locale charmap) != UTF-8 ]]; then
# Try English first, then everything else.
@@ -1247,180 +1190,348 @@ build_sphinx() {
sed -i -e 's:^intersphinx_mapping:disabled_&:' \
"${dir}"/conf.py || die
- # not all packages include the Makefile in pypi tarball
- sphinx-build -b html -d "${dir}"/_build/doctrees "${dir}" \
- "${dir}"/_build/html || die
+ # 1. not all packages include the Makefile in pypi tarball,
+ # so we call sphinx-build directly
+ # 2. if autodoc is used, we need to call sphinx via EPYTHON,
+ # to ensure that PEP 517 venv is respected
+ # 3. if autodoc is not used, then sphinx might not be installed
+ # for the current impl, so we need a fallback to sphinx-build
+ local command=( "${EPYTHON}" -m sphinx.cmd.build )
+ if ! "${EPYTHON}" -c "import sphinx.cmd.build" 2>/dev/null; then
+ command=( sphinx-build )
+ fi
+ command+=(
+ -b html
+ -d "${dir}"/_build/doctrees
+ "${dir}"
+ "${dir}"/_build/html
+ )
+ echo "${command[@]}" >&2
+ "${command[@]}" || die
HTML_DOCS+=( "${dir}/_build/html/." )
}
-# -- python.eclass functions --
-
-_python_check_dead_variables() {
- local v
-
- for v in PYTHON_DEPEND PYTHON_USE_WITH{,_OR,_OPT} {RESTRICT,SUPPORT}_PYTHON_ABIS
- do
- if [[ ${!v} ]]; then
- die "${v} is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#Ebuild_head"
- fi
- done
-
- for v in PYTHON_{CPPFLAGS,CFLAGS,CXXFLAGS,LDFLAGS}
- do
- if [[ ${!v} ]]; then
- die "${v} is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#PYTHON_CFLAGS"
- fi
- done
-
- for v in PYTHON_TESTS_RESTRICTED_ABIS PYTHON_EXPORT_PHASE_FUNCTIONS \
- PYTHON_VERSIONED_{SCRIPTS,EXECUTABLES} PYTHON_NONVERSIONED_EXECUTABLES
- do
- if [[ ${!v} ]]; then
- die "${v} is invalid for python-r1 suite"
- fi
- done
-
- for v in DISTUTILS_USE_SEPARATE_SOURCE_DIRECTORIES DISTUTILS_SETUP_FILES \
- DISTUTILS_GLOBAL_OPTIONS DISTUTILS_SRC_TEST PYTHON_MODNAME
- do
- if [[ ${!v} ]]; then
- die "${v} is invalid for distutils-r1, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#${v}"
- fi
- done
-
- if [[ ${DISTUTILS_DISABLE_TEST_DEPENDENCY} ]]; then
- die "${v} is invalid for distutils-r1, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#DISTUTILS_SRC_TEST"
+# @FUNCTION: _python_check_EPYTHON
+# @INTERNAL
+# @DESCRIPTION:
+# Check if EPYTHON is set, die if not.
+_python_check_EPYTHON() {
+ if [[ -z ${EPYTHON} ]]; then
+ die "EPYTHON unset, invalid call context"
fi
-
- # python.eclass::progress
- for v in PYTHON_BDEPEND PYTHON_MULTIPLE_ABIS PYTHON_ABI_TYPE \
- PYTHON_RESTRICTED_ABIS PYTHON_TESTS_FAILURES_TOLERANT_ABIS \
- PYTHON_CFFI_MODULES_GENERATION_COMMANDS
- do
- if [[ ${!v} ]]; then
- die "${v} is invalid for python-r1 suite"
- fi
- done
-}
-
-python_pkg_setup() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#pkg_setup"
-}
-
-python_convert_shebangs() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#python_convert_shebangs"
-}
-
-python_clean_py-compile_files() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
-
-python_clean_installation_image() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
}
-python_execute_function() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#python_execute_function"
-}
+# @FUNCTION: _python_check_occluded_packages
+# @INTERNAL
+# @DESCRIPTION:
+# Check if the current directory does not contain any incomplete
+# package sources that would block installed packages from being used
+# (and effectively e.g. make it impossible to load compiled extensions).
+_python_check_occluded_packages() {
+ debug-print-function ${FUNCNAME} "${@}"
-python_generate_wrapper_scripts() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
+ [[ -z ${BUILD_DIR} || ! -d ${BUILD_DIR}/install ]] && return
+
+ local sitedir="${BUILD_DIR}/install$(python_get_sitedir)"
+ # avoid unnecessarily checking if we are inside install dir
+ [[ ${sitedir} -ef . ]] && return
+
+ local f fn diff l
+ for f in "${sitedir}"/*/; do
+ f=${f%/}
+ fn=${f##*/}
+
+ # skip metadata directories
+ [[ ${fn} == *.dist-info || ${fn} == *.egg-info ]] && continue
+
+ if [[ -d ${fn} ]]; then
+ diff=$(
+ comm -1 -3 <(
+ find "${fn}" -type f -not -path '*/__pycache__/*' |
+ sort
+ assert
+ ) <(
+ cd "${sitedir}" &&
+ find "${fn}" -type f -not -path '*/__pycache__/*' |
+ sort
+ assert
+ )
+ )
+
+ if [[ -n ${diff} ]]; then
+ eqawarn "The directory ${fn} occludes package installed for ${EPYTHON}."
+ eqawarn "The installed package includes additional files:"
+ eqawarn
+ while IFS= read -r l; do
+ eqawarn " ${l}"
+ done <<<"${diff}"
+ eqawarn
+
+ if [[ ! ${_PYTHON_WARNED_OCCLUDED_PACKAGES} ]]; then
+ eqawarn "For more information on occluded packages, please see:"
+ eqawarn "https://projects.gentoo.org/python/guide/test.html#importerrors-for-c-extensions"
+ _PYTHON_WARNED_OCCLUDED_PACKAGES=1
+ fi
+ fi
+ fi
+ done
}
-python_merge_intermediate_installation_images() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+# @VARIABLE: EPYTEST_DESELECT
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# Specifies an array of tests to be deselected via pytest's --deselect
+# parameter, when calling epytest. The list can include file paths,
+# specific test functions or parametrized test invocations.
+#
+# Note that the listed files will still be subject to collection,
+# i.e. modules imported in global scope will need to be available.
+# If this is undesirable, EPYTEST_IGNORE can be used instead.
-python_set_active_version() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#pkg_setup"
-}
+# @VARIABLE: EPYTEST_IGNORE
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# Specifies an array of paths to be ignored via pytest's --ignore
+# parameter, when calling epytest. The listed files will be entirely
+# skipped from test collection.
-python_need_rebuild() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+# @ECLASS_VARIABLE: EPYTEST_TIMEOUT
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# If set to a non-empty value, enables pytest-timeout plugin and sets
+# test timeout to the specified value. This variable can be either set
+# in ebuilds that are known to hang, or by user to prevent hangs
+# in automated test environments. If this variable is set prior
+# to calling distutils_enable_tests in distutils-r1, a test dependency
+# on dev-python/pytest-timeout is added automatically.
+
+# @ECLASS_VARIABLE: EPYTEST_XDIST
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# If set to a non-empty value, enables running tests in parallel
+# via pytest-xdist plugin. If this variable is set prior to calling
+# distutils_enable_tests in distutils-r1, a test dependency
+# on dev-python/pytest-xdist is added automatically.
-PYTHON() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#.24.28PYTHON.29.2C_.24.7BEPYTHON.7D"
-}
+# @ECLASS_VARIABLE: EPYTEST_JOBS
+# @USER_VARIABLE
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# Specifies the number of jobs for parallel (pytest-xdist) test runs.
+# When unset, defaults to -j from MAKEOPTS, or the current nproc.
-python_get_implementation() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+# @FUNCTION: epytest
+# @USAGE: [<args>...]
+# @DESCRIPTION:
+# Run pytest, passing the standard set of pytest options, then
+# --deselect and --ignore options based on EPYTEST_DESELECT
+# and EPYTEST_IGNORE, then user-specified options.
+#
+# This command dies on failure and respects nonfatal.
+epytest() {
+ debug-print-function ${FUNCNAME} "${@}"
-python_get_implementational_package() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ _python_check_EPYTHON
+ _python_check_occluded_packages
+
+ local color=yes
+ [[ ${NO_COLOR} ]] && color=no
+
+ local args=(
+ # verbose progress reporting and tracebacks
+ -vv
+ # list all non-passed tests in the summary for convenience
+ # (includes failures, skips, xfails...)
+ -ra
+ # print local variables in tracebacks, useful for debugging
+ -l
+ # override filterwarnings=error, we do not really want -Werror
+ # for end users, as it tends to fail on new warnings from deps
+ -Wdefault
+ # however, do error out if the package failed to load
+ # an appropriate async plugin
+ -Werror::pytest.PytestUnhandledCoroutineWarning
+ # override color output
+ "--color=${color}"
+ # count is more precise when we're dealing with a large number
+ # of tests
+ -o console_output_style=count
+ # minimize the temporary directory retention, the test suites
+ # of some packages can grow them pretty large and normally
+ # we don't need to preserve them
+ -o tmp_path_retention_count=0
+ -o tmp_path_retention_policy=failed
+ )
-python_get_libdir() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ if [[ ! ${PYTEST_DISABLE_PLUGIN_AUTOLOAD} ]]; then
+ args+=(
+ # disable the undesirable-dependency plugins by default to
+ # trigger missing argument strips. strip options that require
+ # them from config files. enable them explicitly via "-p ..."
+ # if you *really* need them.
+ -p no:cov
+ -p no:flake8
+ -p no:flakes
+ -p no:pylint
+ # sterilize pytest-markdown as it runs code snippets from all
+ # *.md files found without any warning
+ -p no:markdown
+ # pytest-sugar undoes everything that's good about pytest output
+ # and makes it hard to read logs
+ -p no:sugar
+ # pytest-xvfb automatically spawns Xvfb for every test suite,
+ # effectively forcing it even when we'd prefer the tests
+ # not to have DISPLAY at all, causing crashes sometimes
+ # and causing us to miss missing virtualx usage
+ -p no:xvfb
+ # intrusive packages that break random test suites
+ -p no:pytest-describe
+ -p no:plus
+ -p no:tavern
+ # does something to logging
+ -p no:salt-factories
+ )
+ fi
-python_get_library() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ if [[ -n ${EPYTEST_TIMEOUT} ]]; then
+ if [[ ${PYTEST_PLUGINS} != *pytest_timeout* ]]; then
+ args+=(
+ -p timeout
+ )
+ fi
-python_get_version() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ args+=(
+ "--timeout=${EPYTEST_TIMEOUT}"
+ )
+ fi
-python_get_implementation_and_version() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ if [[ ${EPYTEST_XDIST} ]]; then
+ local jobs=${EPYTEST_JOBS:-$(makeopts_jobs)}
+ if [[ ${jobs} -gt 1 ]]; then
+ if [[ ${PYTEST_PLUGINS} != *xdist.plugin* ]]; then
+ args+=(
+ # explicitly enable the plugin, in case the ebuild was
+ # using PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
+ -p xdist
+ )
+ fi
+ args+=(
+ -n "${jobs}"
+ # worksteal ensures that workers don't end up idle when heavy
+ # jobs are unevenly distributed
+ --dist=worksteal
+ )
+ fi
+ fi
-python_execute_nosetests() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ local x
+ for x in "${EPYTEST_DESELECT[@]}"; do
+ args+=( --deselect "${x}" )
+ done
+ for x in "${EPYTEST_IGNORE[@]}"; do
+ args+=( --ignore "${x}" )
+ done
+ set -- "${EPYTHON}" -m pytest "${args[@]}" "${@}"
+
+ echo "${@}" >&2
+ "${@}" || die -n "pytest failed with ${EPYTHON}"
+ local ret=${?}
+
+ # remove common temporary directories left over by pytest plugins
+ rm -rf .hypothesis .pytest_cache || die
+ # pytest plugins create additional .pyc files while testing
+ # see e.g. https://bugs.gentoo.org/847235
+ if [[ -n ${BUILD_DIR} && -d ${BUILD_DIR} ]]; then
+ find "${BUILD_DIR}" -name '*-pytest-*.pyc' -delete || die
+ fi
-python_execute_py.test() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
+ return ${ret}
}
-python_execute_trial() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+# @FUNCTION: eunittest
+# @USAGE: [<args>...]
+# @DESCRIPTION:
+# Run unit tests using dev-python/unittest-or-fail, passing the standard
+# set of options, followed by user-specified options.
+#
+# This command dies on failure and respects nonfatal.
+eunittest() {
+ debug-print-function ${FUNCNAME} "${@}"
-python_enable_pyc() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ _python_check_EPYTHON
+ _python_check_occluded_packages
-python_disable_pyc() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ # unittest fails with "no tests" correctly since Python 3.12
+ local runner=unittest
+ if _python_impl_matches "${EPYTHON}" 3.{9..11}; then
+ runner=unittest_or_fail
+ fi
+ set -- "${EPYTHON}" -m "${runner}" discover -v "${@}"
-python_mod_optimize() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#Python_byte-code_compilation"
+ echo "${@}" >&2
+ "${@}" || die -n "Tests failed with ${EPYTHON}"
+ return ${?}
}
-python_mod_cleanup() {
- die "${FUNCNAME}() is invalid for python-r1 suite, please take a look @ https://wiki.gentoo.org/wiki/Project:Python/Python.eclass_conversion#Python_byte-code_compilation"
-}
+# @FUNCTION: _python_run_check_deps
+# @INTERNAL
+# @USAGE: <impl>
+# @DESCRIPTION:
+# Verify whether <impl> is an acceptable choice to run any-r1 style
+# code. Checks whether the interpreter is installed, runs
+# python_check_deps() if declared.
+_python_run_check_deps() {
+ debug-print-function ${FUNCNAME} "${@}"
-# python.eclass::progress
+ local impl=${1}
-python_abi_depend() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ einfo "Checking whether ${impl} is suitable ..."
-python_install_executables() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
+ local PYTHON_PKG_DEP
+ _python_export "${impl}" PYTHON_PKG_DEP
+ ebegin " ${PYTHON_PKG_DEP}"
+ has_version -b "${PYTHON_PKG_DEP}"
+ eend ${?} || return 1
+ declare -f python_check_deps >/dev/null || return 0
+
+ local PYTHON_USEDEP="python_targets_${impl}(-)"
+ local PYTHON_SINGLE_USEDEP="python_single_target_${impl}(-)"
+ ebegin " python_check_deps"
+ python_check_deps
+ eend ${?}
}
-python_get_extension_module_suffix() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+# @FUNCTION: python_has_version
+# @USAGE: [-b|-d|-r] <atom>...
+# @DESCRIPTION:
+# A convenience wrapper for has_version() with verbose output and better
+# defaults for use in python_check_deps().
+#
+# The wrapper accepts -b/-d/-r options to indicate the root to perform
+# the lookup on. Unlike has_version, the default is -b.
+#
+# The wrapper accepts multiple package specifications. For the check
+# to succeed, *all* specified atoms must match.
+python_has_version() {
+ debug-print-function ${FUNCNAME} "${@}"
-python_byte-compile_modules() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ local root_arg=( -b )
+ case ${1} in
+ -b|-d|-r)
+ root_arg=( "${1}" )
+ shift
+ ;;
+ esac
-python_clean_byte-compiled_modules() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
-}
+ local pkg
+ for pkg; do
+ ebegin " ${pkg}"
+ has_version "${root_arg[@]}" "${pkg}"
+ eend ${?} || return
+ done
-python_generate_cffi_modules() {
- die "${FUNCNAME}() is invalid for python-r1 suite"
+ return 0
}
-_PYTHON_UTILS_R1=1
fi