diff options
Diffstat (limited to 'eclass/python-utils-r1.eclass')
-rw-r--r-- | eclass/python-utils-r1.eclass | 1075 |
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 |