summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2022-12-31 17:17:12 -0500
committerNed Batchelder <ned@nedbatchelder.com>2022-12-31 20:39:09 -0500
commit5a72a1eb736516759201b223463f69f00979818e (patch)
tree280e5b16147f8a6b792dba2a96c4a21f7894e4c5
parent0bcb2cb8344eb4cec24455fa421ece185eec0fac (diff)
downloadpython-coveragepy-git-5a72a1eb736516759201b223463f69f00979818e.tar.gz
mypy: control.py is checked
-rw-r--r--coverage/config.py12
-rw-r--r--coverage/control.py231
-rw-r--r--coverage/html.py2
-rw-r--r--coverage/misc.py6
-rw-r--r--coverage/parser.py2
-rw-r--r--coverage/report.py2
-rw-r--r--coverage/sqldata.py10
-rw-r--r--coverage/summary.py2
-rw-r--r--coverage/types.py2
-rw-r--r--doc/conf.py1
-rw-r--r--tox.ini5
11 files changed, 151 insertions, 124 deletions
diff --git a/coverage/config.py b/coverage/config.py
index 7e4d07db..cde35466 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -189,9 +189,9 @@ class CoverageConfig(TConfigurable):
# Defaults for [run]
self.branch = False
- self.command_line = None
+ self.command_line: Optional[str] = None
self.concurrency: List[str] = []
- self.context = None
+ self.context: Optional[str] = None
self.cover_pylib = False
self.data_file = ".coverage"
self.debug: List[str] = []
@@ -206,12 +206,12 @@ class CoverageConfig(TConfigurable):
self.source: Optional[List[str]] = None
self.source_pkgs: List[str] = []
self.timid = False
- self._crash = None
+ self._crash: Optional[str] = None
# Defaults for [report]
self.exclude_list = DEFAULT_EXCLUDE[:]
self.fail_under = 0.0
- self.format = None
+ self.format: Optional[str] = None
self.ignore_errors = False
self.include_namespace_packages = False
self.report_include: Optional[List[str]] = None
@@ -219,11 +219,11 @@ class CoverageConfig(TConfigurable):
self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:]
self.partial_list = DEFAULT_PARTIAL[:]
self.precision = 0
- self.report_contexts = None
+ self.report_contexts: Optional[List[str]] = None
self.show_missing = False
self.skip_covered = False
self.skip_empty = False
- self.sort = None
+ self.sort: Optional[str] = None
# Defaults for [html]
self.extra_css: Optional[str] = None
diff --git a/coverage/control.py b/coverage/control.py
index be47ec37..24439918 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -11,7 +11,6 @@ import contextlib
import os
import os.path
import platform
-import re
import signal
import sys
import threading
@@ -19,7 +18,10 @@ import time
import warnings
from types import FrameType
-from typing import Any, Callable, Dict, Generator, List, Optional, Union
+from typing import (
+ cast,
+ Any, Callable, Dict, Generator, IO, Iterable, List, Optional, Tuple, Union,
+)
from coverage import env
from coverage.annotate import AnnotateReporter
@@ -44,14 +46,16 @@ from coverage.python import PythonFileReporter
from coverage.report import render_report
from coverage.results import Analysis
from coverage.summary import SummaryReporter
-from coverage.types import TConfigurable, TConfigSection, TConfigValue, TSysInfo
+from coverage.types import (
+ TConfigurable, TConfigSection, TConfigValue, TLineNo, TMorf, TSysInfo,
+)
from coverage.xmlreport import XmlReporter
os = isolate_module(os)
@contextlib.contextmanager
-def override_config(cov: Coverage, **kwargs: Any) -> Generator[None, None, None]:
+def override_config(cov: Coverage, **kwargs: TConfigValue) -> Generator[None, None, None]:
"""Temporarily tweak the configuration of `cov`.
The arguments are applied to `cov.config` with the `from_args` method.
@@ -109,22 +113,22 @@ class Coverage(TConfigurable):
def __init__( # pylint: disable=too-many-arguments
self,
- data_file=DEFAULT_DATAFILE,
- data_suffix=None,
- cover_pylib=None,
- auto_data=False,
- timid=None,
- branch=None,
- config_file=True,
- source=None,
- source_pkgs=None,
- omit=None,
- include=None,
- debug=None,
- concurrency=None,
- check_preimported=False,
- context=None,
- messages=False,
+ data_file: Optional[str]=DEFAULT_DATAFILE, # type: ignore[assignment]
+ data_suffix: Optional[Union[str, bool]]=None,
+ cover_pylib: Optional[bool]=None,
+ auto_data: bool=False,
+ timid: Optional[bool]=None,
+ branch: Optional[bool]=None,
+ config_file: Union[str, bool]=True,
+ source: Optional[List[str]]=None,
+ source_pkgs: Optional[List[str]]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ debug: Optional[List[str]]=None,
+ concurrency: Optional[Union[str, List[str]]]=None,
+ check_preimported: bool=False,
+ context: Optional[str]=None,
+ messages: bool=False,
) -> None:
"""
Many of these arguments duplicate and override values that can be
@@ -245,8 +249,8 @@ class Coverage(TConfigurable):
self._file_mapper: Callable[[str], str]
self._data_suffix = self._run_suffix = None
- self._exclude_re: Dict[str, re.Pattern[str]] = {}
- self._old_sigterm = None
+ self._exclude_re: Dict[str, str] = {}
+ self._old_sigterm: Optional[Callable[[int, Optional[FrameType]], Any]] = None
# State machine variables:
# Have we initialized everything?
@@ -569,9 +573,11 @@ class Coverage(TConfigurable):
# The Python docs seem to imply that SIGTERM works uniformly even
# on Windows, but that's not my experience, and this agrees:
# https://stackoverflow.com/questions/35772001/x/35792192#35792192
- self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm)
+ self._old_sigterm = signal.signal( # type: ignore[assignment]
+ signal.SIGTERM, self._on_sigterm,
+ )
- def _init_data(self, suffix):
+ def _init_data(self, suffix: Optional[Union[str, bool]]) -> None:
"""Create a data file if we don't have one yet."""
if not hasattr(self, "_data"):
# Create the data file. We do this at construction time so that the
@@ -627,7 +633,7 @@ class Coverage(TConfigurable):
self._collector.stop()
self._started = False
- def _atexit(self, event="atexit") -> None:
+ def _atexit(self, event: str="atexit") -> None:
"""Clean up on process shutdown."""
if self._debug.should("process"):
self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
@@ -636,7 +642,7 @@ class Coverage(TConfigurable):
if self._auto_save:
self.save()
- def _on_sigterm(self, signum_unused, frame_unused) -> None:
+ def _on_sigterm(self, signum_unused: int, frame_unused: Optional[FrameType]) -> None:
"""A handler for signal.SIGTERM."""
self._atexit("sigterm")
# Statements after here won't be seen by metacov because we just wrote
@@ -660,7 +666,7 @@ class Coverage(TConfigurable):
del self._data
self._inited_for_start = False
- def switch_context(self, new_context) -> None:
+ def switch_context(self, new_context: str) -> None:
"""Switch to a new dynamic context.
`new_context` is a string to use as the :ref:`dynamic context
@@ -681,13 +687,13 @@ class Coverage(TConfigurable):
self._collector.switch_context(new_context)
- def clear_exclude(self, which='exclude') -> None:
+ def clear_exclude(self, which: str='exclude') -> None:
"""Clear the exclude list."""
self._init()
setattr(self.config, which + "_list", [])
self._exclude_regex_stale()
- def exclude(self, regex, which='exclude') -> None:
+ def exclude(self, regex: str, which: str='exclude') -> None:
"""Exclude source lines from execution consideration.
A number of lists of regular expressions are maintained. Each list
@@ -704,6 +710,7 @@ class Coverage(TConfigurable):
"""
self._init()
excl_list = getattr(self.config, which + "_list")
+ assert isinstance(regex, str)
excl_list.append(regex)
self._exclude_regex_stale()
@@ -711,29 +718,29 @@ class Coverage(TConfigurable):
"""Drop all the compiled exclusion regexes, a list was modified."""
self._exclude_re.clear()
- def _exclude_regex(self, which):
- """Return a compiled regex for the given exclusion list."""
+ def _exclude_regex(self, which: str) -> str:
+ """Return a regex string for the given exclusion list."""
if which not in self._exclude_re:
excl_list = getattr(self.config, which + "_list")
self._exclude_re[which] = join_regex(excl_list)
return self._exclude_re[which]
- def get_exclude_list(self, which='exclude'):
- """Return a list of excluded regex patterns.
+ def get_exclude_list(self, which: str='exclude') -> List[str]:
+ """Return a list of excluded regex strings.
`which` indicates which list is desired. See :meth:`exclude` for the
lists that are available, and their meaning.
"""
self._init()
- return getattr(self.config, which + "_list")
+ return cast(List[str], getattr(self.config, which + "_list"))
def save(self) -> None:
"""Save the collected coverage data to the data file."""
data = self.get_data()
data.write()
- def _make_aliases(self):
+ def _make_aliases(self) -> PathAliases:
"""Create a PathAliases from our configuration."""
aliases = PathAliases(
debugfn=(self._debug.write if self._debug.should("pathmap") else None),
@@ -745,7 +752,12 @@ class Coverage(TConfigurable):
aliases.add(pattern, result)
return aliases
- def combine(self, data_paths=None, strict=False, keep=False) -> None:
+ def combine(
+ self,
+ data_paths: Optional[Iterable[str]]=None,
+ strict: bool=False,
+ keep: bool=False
+ ) -> None:
"""Combine together a number of similarly-named coverage data files.
All coverage data files whose name starts with `data_file` (from the
@@ -785,7 +797,7 @@ class Coverage(TConfigurable):
message=self._message,
)
- def get_data(self):
+ def get_data(self) -> CoverageData:
"""Get the collected data.
Also warn about various problems collecting data.
@@ -835,12 +847,15 @@ class Coverage(TConfigurable):
self._data.touch_files(paths, plugin_name)
# Backward compatibility with version 1.
- def analysis(self, morf):
+ def analysis(self, morf: TMorf) -> Tuple[str, List[TLineNo], List[TLineNo], str]:
"""Like `analysis2` but doesn't return excluded line numbers."""
f, s, _, m, mf = self.analysis2(morf)
return f, s, m, mf
- def analysis2(self, morf):
+ def analysis2(
+ self,
+ morf: TMorf,
+ ) -> Tuple[str, List[TLineNo], List[TLineNo], List[TLineNo], str]:
"""Analyze a module.
`morf` is a module or a file name. It will be analyzed to determine
@@ -866,7 +881,7 @@ class Coverage(TConfigurable):
analysis.missing_formatted(),
)
- def _analyze(self, it) -> Analysis:
+ def _analyze(self, it: Union[FileReporter, TMorf]) -> Analysis:
"""Analyze a single morf or code unit.
Returns an `Analysis` object.
@@ -877,15 +892,17 @@ class Coverage(TConfigurable):
self._post_init()
data = self.get_data()
- if not isinstance(it, FileReporter):
- it = self._get_file_reporter(it)
+ if isinstance(it, FileReporter):
+ fr = it
+ else:
+ fr = self._get_file_reporter(it)
- return Analysis(data, self.config.precision, it, self._file_mapper)
+ return Analysis(data, self.config.precision, fr, self._file_mapper)
- def _get_file_reporter(self, morf):
+ def _get_file_reporter(self, morf: TMorf) -> FileReporter:
"""Get a FileReporter for a module or file name."""
plugin = None
- file_reporter = "python"
+ file_reporter: Union[str, FileReporter] = "python"
if isinstance(morf, str):
mapped_morf = self._file_mapper(morf)
@@ -905,9 +922,10 @@ class Coverage(TConfigurable):
if file_reporter == "python":
file_reporter = PythonFileReporter(morf, self)
+ assert isinstance(file_reporter, FileReporter)
return file_reporter
- def _get_file_reporters(self, morfs=None):
+ def _get_file_reporters(self, morfs: Optional[Iterable[TMorf]]=None) -> List[FileReporter]:
"""Get a list of FileReporters for a list of modules or file names.
For each module or file name in `morfs`, find a FileReporter. Return
@@ -923,7 +941,7 @@ class Coverage(TConfigurable):
# Be sure we have a collection.
if not isinstance(morfs, (list, tuple, set)):
- morfs = [morfs]
+ morfs = [morfs] # type: ignore[list-item]
file_reporters = [self._get_file_reporter(morf) for morf in morfs]
return file_reporters
@@ -937,18 +955,18 @@ class Coverage(TConfigurable):
def report(
self,
- morfs=None,
- show_missing=None,
- ignore_errors=None,
- file=None,
- omit=None,
- include=None,
- skip_covered=None,
- contexts=None,
- skip_empty=None,
- precision=None,
- sort=None,
- output_format=None,
+ morfs: Optional[Iterable[TMorf]]=None,
+ show_missing: Optional[bool]=None,
+ ignore_errors: Optional[bool]=None,
+ file: Optional[IO[str]]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ skip_covered: Optional[bool]=None,
+ contexts: Optional[List[str]]=None,
+ skip_empty: Optional[bool]=None,
+ precision: Optional[int]=None,
+ sort: Optional[str]=None,
+ output_format: Optional[str]=None,
) -> float:
"""Write a textual summary report to `file`.
@@ -974,7 +992,7 @@ class Coverage(TConfigurable):
If `skip_empty` is true, don't report on empty files (those that have
no statements).
- `contexts` is a list of regular expressions. Only data from
+ `contexts` is a list of regular expression strings. Only data from
:ref:`dynamic contexts <dynamic_contexts>` that match one of those
expressions (using :func:`re.search <python:re.search>`) will be
included in the report.
@@ -1019,13 +1037,13 @@ class Coverage(TConfigurable):
def annotate(
self,
- morfs=None,
- directory=None,
- ignore_errors=None,
- omit=None,
- include=None,
- contexts=None,
- ):
+ morfs: Optional[Iterable[TMorf]]=None,
+ directory: Optional[str]=None,
+ ignore_errors: Optional[bool]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ contexts: Optional[List[str]]=None,
+ ) -> None:
"""Annotate a list of modules.
.. note::
@@ -1058,18 +1076,18 @@ class Coverage(TConfigurable):
def html_report(
self,
- morfs=None,
- directory=None,
- ignore_errors=None,
- omit=None,
- include=None,
- extra_css=None,
- title=None,
- skip_covered=None,
- show_contexts=None,
- contexts=None,
- skip_empty=None,
- precision=None,
+ morfs: Optional[Iterable[TMorf]]=None,
+ directory: Optional[str]=None,
+ ignore_errors: Optional[bool]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ extra_css: Optional[str]=None,
+ title: Optional[str]=None,
+ skip_covered: Optional[bool]=None,
+ show_contexts: Optional[bool]=None,
+ contexts: Optional[List[str]]=None,
+ skip_empty: Optional[bool]=None,
+ precision: Optional[int]=None,
) -> float:
"""Generate an HTML report.
@@ -1116,13 +1134,13 @@ class Coverage(TConfigurable):
def xml_report(
self,
- morfs=None,
- outfile=None,
- ignore_errors=None,
- omit=None,
- include=None,
- contexts=None,
- skip_empty=None,
+ morfs: Optional[Iterable[TMorf]]=None,
+ outfile: Optional[str]=None,
+ ignore_errors: Optional[bool]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ contexts: Optional[List[str]]=None,
+ skip_empty: Optional[bool]=None,
) -> float:
"""Generate an XML report of coverage results.
@@ -1150,20 +1168,22 @@ class Coverage(TConfigurable):
def json_report(
self,
- morfs=None,
- outfile=None,
- ignore_errors=None,
- omit=None,
- include=None,
- contexts=None,
- pretty_print=None,
- show_contexts=None,
+ morfs: Optional[Iterable[TMorf]]=None,
+ outfile: Optional[str]=None,
+ ignore_errors: Optional[bool]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ contexts: Optional[List[str]]=None,
+ pretty_print: Optional[bool]=None,
+ show_contexts: Optional[bool]=None,
) -> float:
"""Generate a JSON report of coverage results.
Each module in `morfs` is included in the report. `outfile` is the
path to write the file to, "-" will write to stdout.
+ `pretty_print` is a boolean, whether to pretty-print the JSON output or not.
+
See :meth:`report` for other arguments.
Returns a float, the total percentage covered.
@@ -1186,12 +1206,12 @@ class Coverage(TConfigurable):
def lcov_report(
self,
- morfs=None,
- outfile=None,
- ignore_errors=None,
- omit=None,
- include=None,
- contexts=None,
+ morfs: Optional[Iterable[TMorf]]=None,
+ outfile: Optional[str]=None,
+ ignore_errors: Optional[bool]=None,
+ omit: Optional[List[str]]=None,
+ include: Optional[List[str]]=None,
+ contexts: Optional[List[str]]=None,
) -> float:
"""Generate an LCOV report of coverage results.
@@ -1221,7 +1241,7 @@ class Coverage(TConfigurable):
self._init()
self._post_init()
- def plugin_info(plugins):
+ def plugin_info(plugins: List[Any]) -> List[str]:
"""Make an entry for the sys_info from a list of plug-ins."""
entries = []
for plugin in plugins:
@@ -1279,10 +1299,13 @@ class Coverage(TConfigurable):
if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging
from coverage.debug import decorate_methods, show_calls
- Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage)
+ Coverage = decorate_methods( # type: ignore[misc]
+ show_calls(show_args=True),
+ butnot=['get_data']
+ )(Coverage)
-def process_startup() -> None:
+def process_startup() -> Optional[Coverage]:
"""Call this at Python start-up to perhaps measure coverage.
If the environment variable COVERAGE_PROCESS_START is defined, coverage
@@ -1325,7 +1348,7 @@ def process_startup() -> None:
return None
cov = Coverage(config_file=cps)
- process_startup.coverage = cov
+ process_startup.coverage = cov # type: ignore[attr-defined]
cov._warn_no_data = False
cov._warn_unimported_source = False
cov._warn_preimported_source = False
diff --git a/coverage/html.py b/coverage/html.py
index 3fcecc5d..b10bab24 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -237,7 +237,7 @@ class HtmlReporter:
self.pyfile_html_source = read_data("pyfile.html")
self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals)
- def report(self, morfs: Iterable[TMorf]) -> float:
+ def report(self, morfs: Optional[Iterable[TMorf]]) -> float:
"""Generate an HTML report for `morfs`.
`morfs` is a list of modules or file names.
diff --git a/coverage/misc.py b/coverage/misc.py
index 0da7f398..1e4b4e74 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -16,6 +16,8 @@ import re
import sys
import types
+from typing import Iterable
+
from coverage import env
from coverage.exceptions import CoverageException
@@ -133,8 +135,8 @@ def bool_or_none(b):
return bool(b)
-def join_regex(regexes):
- """Combine a series of regexes into one that matches any of them."""
+def join_regex(regexes: Iterable[str]) -> str:
+ """Combine a series of regex strings into one that matches any of them."""
regexes = list(regexes)
if len(regexes) == 1:
return regexes[0]
diff --git a/coverage/parser.py b/coverage/parser.py
index 3512fdc3..2a8d0a50 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -43,7 +43,7 @@ class PythonParser:
"""
Source can be provided as `text`, the text itself, or `filename`, from
which the text will be read. Excluded lines are those that match
- `exclude`, a regex.
+ `exclude`, a regex string.
"""
assert text or filename, "PythonParser needs either text or filename"
diff --git a/coverage/report.py b/coverage/report.py
index 0c05b044..b44f9c8e 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -10,7 +10,7 @@ from coverage.files import prep_patterns, GlobMatcher
from coverage.misc import ensure_dir_for_file, file_be_gone
-def render_report(output_path, reporter, morfs, msgfn):
+def render_report(output_path, reporter, morfs, msgfn) -> float:
"""Run a one-file report generator, managing the output file.
This function ensures the output file is ready to be written to. Then writes
diff --git a/coverage/sqldata.py b/coverage/sqldata.py
index c76451a7..d9f8ceaf 100644
--- a/coverage/sqldata.py
+++ b/coverage/sqldata.py
@@ -594,16 +594,16 @@ class CoverageData(AutoReprMixin):
def touch_file(self, filename: str, plugin_name: str="") -> None:
"""Ensure that `filename` appears in the data, empty if needed.
- `plugin_name` is the name of the plugin responsible for this file. It is used
- to associate the right filereporter, etc.
+ `plugin_name` is the name of the plugin responsible for this file.
+ It is used to associate the right filereporter, etc.
"""
self.touch_files([filename], plugin_name)
- def touch_files(self, filenames: Iterable[str], plugin_name: str="") -> None:
+ def touch_files(self, filenames: Iterable[str], plugin_name: Optional[str]=None) -> None:
"""Ensure that `filenames` appear in the data, empty if needed.
- `plugin_name` is the name of the plugin responsible for these files. It is used
- to associate the right filereporter, etc.
+ `plugin_name` is the name of the plugin responsible for these files.
+ It is used to associate the right filereporter, etc.
"""
if self._debug.should("dataop"):
self._debug.write(f"Touching {filenames!r}")
diff --git a/coverage/summary.py b/coverage/summary.py
index 464445ef..3f3fd688 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -147,7 +147,7 @@ class SummaryReporter:
for end_line in end_lines:
self.write(end_line)
- def report(self, morfs, outfile=None):
+ def report(self, morfs, outfile=None) -> float:
"""Writes a report summarizing coverage statistics per module.
`outfile` is a text-mode file object to write the summary to.
diff --git a/coverage/types.py b/coverage/types.py
index c9d05958..1e641d1c 100644
--- a/coverage/types.py
+++ b/coverage/types.py
@@ -25,7 +25,7 @@ TCovKwargs = Any
## Configuration
# One value read from a config file.
-TConfigValue = Union[bool, str, List[str]]
+TConfigValue = Optional[Union[bool, int, str, List[str]]]
# An entire config section, mapping option names to values.
TConfigSection = Dict[str, TConfigValue]
diff --git a/doc/conf.py b/doc/conf.py
index 18b56c6e..7423fa15 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -125,6 +125,7 @@ intersphinx_mapping = {
nitpick_ignore = [
("py:class", "frame"),
+ ("py:class", "module"),
]
nitpick_ignore_regex = [
diff --git a/tox.ini b/tox.ini
index 6e08adc1..67464c00 100644
--- a/tox.ini
+++ b/tox.ini
@@ -95,11 +95,12 @@ deps =
setenv =
{[testenv]setenv}
- C_AN=coverage/config.py coverage/data.py coverage/disposition.py coverage/files.py coverage/inorout.py coverage/multiproc.py coverage/numbits.py
+ C_AE=coverage/config.py coverage/control.py coverage/data.py coverage/disposition.py
+ C_FN=coverage/files.py coverage/inorout.py coverage/multiproc.py coverage/numbits.py
C_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/python.py
C_QZ=coverage/results.py coverage/sqldata.py coverage/tomlconfig.py coverage/types.py
T_AN=tests/test_api.py tests/goldtest.py tests/helpers.py tests/test_html.py
- TYPEABLE={env:C_AN} {env:C_OP} {env:C_QZ} {env:T_AN}
+ TYPEABLE={env:C_AE} {env:C_FN} {env:C_OP} {env:C_QZ} {env:T_AN}
commands =
# PYVERSIONS