Skip to content

Commit 6806341

Browse files
mgornyarthurzam
authored andcommitted
python: Add a check for misplaced EPYTEST_* variables
Add a check for `EPYTEST_*` variables being declared after calling `distutils_enable_tests`, so they don't affect it. This catches only global scope use, as it is valid to override them in function scope. Since using `${var:=...}` assignment is also valid, hack a quick method to handle them as well. Closes: #739 Signed-off-by: Michał Górny <mgorny@gentoo.org> Part-of: #747 Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
1 parent c1a730b commit 6806341

File tree

6 files changed

+127
-2
lines changed

6 files changed

+127
-2
lines changed

src/pkgcheck/bash/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
cmd_query = query("(command) @call")
1414
func_query = query("(function_definition) @func")
1515
var_assign_query = query("(variable_assignment) @assign")
16+
var_expansion_query = query("(expansion) @exp")
1617
var_query = query("(variable_name) @var")
1718

1819

src/pkgcheck/checks/python.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,25 @@ class PythonMissingSCMDependency(results.VersionResult, results.Warning):
247247
)
248248

249249

250+
class MisplacedEPyTestVar(results.LineResult, results.Error):
251+
"""Invalid placement of ``EPYTEST_*`` variable
252+
253+
``EPYTEST_*`` variables need to be set prior to ``distutils_enable_tests``
254+
to enable its functionality. The exception to that rule are local overrides
255+
in ``python_test()`` -- we presume the author knows what they're doing.
256+
"""
257+
258+
def __init__(self, variable, **kwargs):
259+
super().__init__(**kwargs)
260+
self.variable = variable
261+
262+
@property
263+
def desc(self):
264+
return (
265+
f"line {self.lineno}: variable set after calling distutils_enable_tests: {self.line!r}"
266+
)
267+
268+
250269
class PythonCheck(Check):
251270
"""Python eclass checks.
252271
@@ -256,7 +275,7 @@ class PythonCheck(Check):
256275

257276
_source = sources.EbuildParseRepoSource
258277
known_results = frozenset(
259-
[
278+
{
260279
MissingPythonEclass,
261280
PythonMissingRequiredUse,
262281
PythonMissingDeps,
@@ -268,7 +287,8 @@ class PythonCheck(Check):
268287
PythonAnyMismatchedUseHasVersionCheck,
269288
PythonAnyMismatchedDepHasVersionCheck,
270289
PythonMissingSCMDependency,
271-
]
290+
MisplacedEPyTestVar,
291+
}
272292
)
273293

274294
has_version_known_flags = {
@@ -389,6 +409,53 @@ def check_pep517(self, pkg):
389409
if not self.setuptools_scm.intersection(bdepends):
390410
yield PythonMissingSCMDependency(pkg=pkg)
391411

412+
def _get_all_global_assignments(self, pkg):
413+
"""Iterate over global plain and expansion assignments"""
414+
for node in pkg.global_query(bash.var_assign_query):
415+
name = pkg.node_str(node.child_by_field_name("name"))
416+
yield (name, node)
417+
418+
# ${var:=value} expansions.
419+
for node in pkg.global_query(bash.var_expansion_query):
420+
if node.children[0].text != b"${":
421+
continue
422+
name = node.children[1].text
423+
if node.children[2].text not in (b"=", b":="):
424+
continue
425+
yield (name.decode(), node)
426+
427+
def check_epytest_vars(self, pkg):
428+
"""Check for incorrect use of EPYTEST_* variables"""
429+
# TODO: do we want to check for multiple det calls? Quite unlikely.
430+
for node in pkg.global_query(bash.cmd_query):
431+
name = pkg.node_str(node.child_by_field_name("name"))
432+
if name != "distutils_enable_tests":
433+
continue
434+
for argument_node in node.children_by_field_name("argument"):
435+
argument = pkg.node_str(argument_node)
436+
# Check if the first argument is "pytest", but we need
437+
# to skip line continuations explicitly.
438+
if argument == "pytest":
439+
det_lineno, _ = node.start_point
440+
break
441+
elif argument != "\\":
442+
return
443+
else:
444+
return
445+
446+
for var_name, var_node in self._get_all_global_assignments(pkg):
447+
# While not all variables affect distutils_enable_tests, make it
448+
# future proof. Also, it makes sense to keep them together.
449+
# Just opt out from the long lists.
450+
if var_name.startswith("EPYTEST_") and var_name not in (
451+
"EPYTEST_DESELECT",
452+
"EPYTEST_IGNORE",
453+
):
454+
lineno, _ = var_node.start_point
455+
if lineno > det_lineno:
456+
line = pkg.node_str(var_node)
457+
yield MisplacedEPyTestVar(var_name, line=line, lineno=lineno + 1, pkg=pkg)
458+
392459
@staticmethod
393460
def _prepare_deps(deps: str):
394461
try:
@@ -545,6 +612,7 @@ def feed(self, pkg):
545612

546613
if "distutils-r1" in pkg.inherited:
547614
yield from self.check_pep517(pkg)
615+
yield from self.check_epytest_vars(pkg)
548616

549617
any_dep_func = self.eclass_any_dep_func[eclass]
550618
python_check_deps = self.build_python_gen_any_dep_calls(pkg, any_dep_func)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{"__class__": "MisplacedEPyTestVar", "category": "PythonCheck", "package": "MisplacedEPyTestVar", "version": "0", "line": "EPYTEST_PLUGIN_AUTOLOAD=1", "lineno": 15, "variable": "EPYTEST_PLUGIN_AUTOLOAD"}
2+
{"__class__": "MisplacedEPyTestVar", "category": "PythonCheck", "package": "MisplacedEPyTestVar", "version": "0", "line": "EPYTEST_PLUGINS=( foo bar baz )", "lineno": 16, "variable": "EPYTEST_PLUGINS"}
3+
{"__class__": "MisplacedEPyTestVar", "category": "PythonCheck", "package": "MisplacedEPyTestVar", "version": "0", "line": "EPYTEST_XDIST=1", "lineno": 17, "variable": "EPYTEST_XDIST"}
4+
{"__class__": "MisplacedEPyTestVar", "category": "PythonCheck", "package": "MisplacedEPyTestVar", "version": "0", "line": "${EPYTEST_TIMEOUT:=180}", "lineno": 18, "variable": "EPYTEST_TIMEOUT"}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
diff '--color=auto' -Naur python/PythonCheck/MisplacedEPyTestVar/MisplacedEPyTestVar-0.ebuild fixed/PythonCheck/MisplacedEPyTestVar/MisplacedEPyTestVar-0.ebuild
2+
--- python/PythonCheck/MisplacedEPyTestVar/MisplacedEPyTestVar-0.ebuild 2025-07-12 17:10:51.665298954 +0200
3+
+++ fixed/PythonCheck/MisplacedEPyTestVar/MisplacedEPyTestVar-0.ebuild 2025-07-12 17:15:30.258231253 +0200
4+
@@ -10,13 +10,13 @@
5+
LICENSE="BSD"
6+
SLOT="0"
7+
8+
-distutils_enable_tests pytest
9+
-
10+
EPYTEST_PLUGIN_AUTOLOAD=1
11+
EPYTEST_PLUGINS=( foo bar baz )
12+
EPYTEST_XDIST=1
13+
: ${EPYTEST_TIMEOUT:=180}
14+
15+
+distutils_enable_tests pytest
16+
+
17+
EPYTEST_DESELECT=(
18+
tests/test_foo.py::test_foo
19+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
EAPI=8
2+
3+
DISTUTILS_USE_PEP517=flit
4+
PYTHON_COMPAT=( python3_10 )
5+
6+
inherit distutils-r1
7+
8+
DESCRIPTION="Ebuild with misplaced EPYTEST vars"
9+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
10+
LICENSE="BSD"
11+
SLOT="0"
12+
13+
distutils_enable_tests pytest
14+
15+
EPYTEST_PLUGIN_AUTOLOAD=1
16+
EPYTEST_PLUGINS=( foo bar baz )
17+
EPYTEST_XDIST=1
18+
: ${EPYTEST_TIMEOUT:=180}
19+
20+
EPYTEST_DESELECT=(
21+
tests/test_foo.py::test_foo
22+
)
23+
EPYTEST_IGNORE=(
24+
tests/test_bar.py
25+
)
26+
27+
python_test() {
28+
: ${EPYTEST_TIMEOUT:=300}
29+
local EPYTEST_PLUGINS=( "${EPYTEST_PLUGINS[@]}" more )
30+
EPYTEST_XDIST= epytest
31+
}

testdata/repos/python/eclass/distutils-r1.eclass

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ _distutils_set_globals() {
5858
}
5959
_distutils_set_globals
6060
unset -f _distutils_set_globals
61+
62+
distutils_enable_tests() { :; }

0 commit comments

Comments
 (0)