diff options
author | Bernát Gábor <bgabor8@bloomberg.net> | 2020-10-19 09:29:51 +0100 |
---|---|---|
committer | Bernát Gábor <bgabor8@bloomberg.net> | 2020-10-19 09:29:51 +0100 |
commit | 61869b12edd970998e235c86fcf72b189445c6eb (patch) | |
tree | 2b9bc817bfc28fa6d11ec2bba82dedd065e4aeed | |
parent | 860983a992f5b6a613c111b5e1cbfd2a7213bea8 (diff) | |
download | tox-git-61869b12edd970998e235c86fcf72b189445c6eb.tar.gz |
Improve documentation
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
73 files changed, 541 insertions, 450 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c2d1053..2b11f69e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,77 +1,60 @@ -default_language_version: - python: python3 repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 - hooks: - - id: check-ast - - id: check-builtin-literals - - id: check-docstring-first - - id: check-merge-conflict - - id: check-yaml - - id: check-toml - - id: debug-statements - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/asottile/add-trailing-comma - rev: v2.0.1 - hooks: - - id: add-trailing-comma -- repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 - hooks: - - id: pyupgrade -- repo: https://github.com/asottile/seed-isort-config - rev: v2.2.0 - hooks: - - id: seed-isort-config - args: - - --application-directories - - .:src -- repo: https://github.com/pre-commit/mirrors-isort - rev: v5.6.4 - hooks: - - id: isort -- repo: https://github.com/ambv/black - rev: 20.8b1 - hooks: - - id: black - args: - - --safe - language_version: python3.8 -- repo: https://github.com/asottile/blacken-docs - rev: v1.8.0 - hooks: - - id: blacken-docs - additional_dependencies: - - black==19.10b0 - language_version: python3.8 -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.6.0 - hooks: - - id: rst-backticks -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.15.0 - hooks: - - id: setup-cfg-fmt - args: - - --min-py3-version - - '3.4' -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear == 20.1.2 - language_version: python3.8 -- repo: local - hooks: - - id: changelogs-rst - name: changelog filenames - language: fail - entry: >- - changelog files must be named - ####.(bugfix|feature|deprecation|breaking|doc|misc).rst - exclude: >- - ^docs/changelog/(\d+\.(bugfix|feature|deprecation|breaking|doc|misc).rst|README.rst|template.jinja2) - files: ^docs/changelog/ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-docstring-first + - id: check-merge-conflict + - id: check-yaml + - id: check-toml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/asottile/pyupgrade + rev: v2.7.2 + hooks: + - id: pyupgrade + - repo: https://github.com/PyCQA/isort + rev: 5.6.4 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + args: + - --safe + - repo: https://github.com/PyCQA/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear == 20.1.2 + language_version: python3.9 + - repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.15.0 + hooks: + - id: setup-cfg-fmt + args: [ '--min-py3-version', '3.6' ] + - repo: https://github.com/asottile/blacken-docs + rev: v1.8.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==19.10b0 + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.6.0 + hooks: + - id: rst-backticks + - repo: local + hooks: + - id: changelogs-rst + name: changelog filenames + language: fail + entry: >- + changelog files must be named + ####.(bugfix|feature|deprecation|breaking|doc|misc).rst + exclude: >- + ^docs/changelog/(\d+\.(bugfix|feature|deprecation|breaking|doc|misc).rst|README.rst|template.jinja2) + files: ^docs/changelog/ diff --git a/pyproject.toml b/pyproject.toml index 566f0bc7..7d394bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,27 @@ [build-system] -requires = [ - "setuptools >= 45.0.0", - "wheel >= 0.30.0", - "setuptools_scm >= 3, <4", -] -build-backend = 'setuptools.build_meta' +requires = ["setuptools >= 44", "wheel >= 0.30", "setuptools_scm[toml]>=3.4"] +build-backend = "setuptools.build_meta" [tool.black] line-length = 120 +[tool.isort] +profile = "black" +known_first_party = ["tox_ini_fmt"] + +[tool.setuptools_scm] +write_to = "src/tox/version.py" +write_to_template = """ +\"\"\" Version information \"\"\" +__version__ = "{version}" +""" + [tool.towncrier] - package = "tox" - package_dir = "" # we purposfully do not set this as src, forcing import from site-package that has version.py - filename = "docs/changelog.rst" - directory = "docs/changelog" - title_format = false - issue_format = "`#{issue} <https://github.com/tox-dev/tox/issues/{issue}>`_" - template = "docs/changelog/template.jinja2" - # possible types, all default: feature, bugfix, doc, removal, misc +package = "tox" +package_dir = "" # we purposfully do not set this as src, forcing import from site-package that has version.py +filename = "docs/changelog.rst" +directory = "docs/changelog" +title_format = false +issue_format = "`#{issue} <https://github.com/tox-dev/tox/issues/{issue}>`_" +template = "docs/changelog/template.jinja2" +# possible types, all default: feature, bugfix, doc, removal, misc @@ -75,3 +75,8 @@ where = src [bdist_wheel] universal = 1 + +[tool:pytest] +testpaths = tests +junit_family = xunit2 +addopts = --tb=auto -ra --showlocals @@ -1,9 +1,3 @@ -from setuptools import __version__, setup +from setuptools import setup -if int(__version__.split(".")[0]) < 41: - raise RuntimeError("setuptools >= 41 required to build") - -setup( - use_scm_version={"write_to": "src/tox/version.py", "write_to_template": '__version__ = "{version}"'}, - setup_requires=["setuptools_scm >= 2"], -) +setup() diff --git a/src/tox/__init__.py b/src/tox/__init__.py index dc2782c3..56a4babe 100644 --- a/src/tox/__init__.py +++ b/src/tox/__init__.py @@ -1,3 +1,7 @@ +from .run import main from .version import __version__ -__all__ = ("__version__",) +__all__ = ( + "__version__", + "main", +) diff --git a/src/tox/config/cli/env_var.py b/src/tox/config/cli/env_var.py index 6e46c6b4..6e5de1be 100644 --- a/src/tox/config/cli/env_var.py +++ b/src/tox/config/cli/env_var.py @@ -1,3 +1,6 @@ +""" +Provides configuration values from the the environment variables. +""" import logging import os @@ -18,12 +21,11 @@ def get_env_var(key, of_type): environ_key = fmt.format(key_upper) if environ_key in os.environ: value = os.environ[environ_key] - # noinspection PyBroadException try: source = f"env var {environ_key}" of_type = CONVERT.to(raw=value, of_type=of_type) return of_type, source - except Exception as exception: + except Exception as exception: # noqa logging.warning( "env var %s=%r cannot be transformed to %r because %r", environ_key, diff --git a/src/tox/config/cli/for_docs.py b/src/tox/config/cli/for_docs.py index 95f6393f..d830034a 100644 --- a/src/tox/config/cli/for_docs.py +++ b/src/tox/config/cli/for_docs.py @@ -1,3 +1,6 @@ -from .parse import _get_core_parser +""" +This module exports the parser for the Sphinx doc generator. +""" +from .parse import _get_parser -parser = _get_core_parser() +parser = _get_parser() diff --git a/src/tox/config/cli/ini.py b/src/tox/config/cli/ini.py index 1a92c5f4..dc407c00 100644 --- a/src/tox/config/cli/ini.py +++ b/src/tox/config/cli/ini.py @@ -1,3 +1,6 @@ +""" +Provides configuration values from tox.ini files. +""" import logging import os from pathlib import Path @@ -27,14 +30,12 @@ class IniConfig: self.config_file = self.config_file.absolute() try: self.ini = Ini(self.config_file) - # noinspection PyProtectedMember - self.has_tox_section = cast(IniLoader, self.ini.core)._section is not None + self.has_tox_section = cast(IniLoader, self.ini.core)._section is not None # noqa except Exception as exception: logging.error("failed to read config file %s because %r", config_file, exception) self.has_config_file = None def get(self, key, of_type): - # noinspection PyBroadException cache_key = key, of_type if cache_key in self._cache: result = self._cache[cache_key] @@ -45,14 +46,8 @@ class IniConfig: result = value, source except KeyError: # just not found result = None - except Exception as exception: - logging.warning( - "%s key %s as type %r failed with %r", - self.config_file, - key, - of_type, - exception, - ) + except Exception as exception: # noqa + logging.warning("%s key %s as type %r failed with %r", self.config_file, key, of_type, exception) result = None self._cache[cache_key] = result return result @@ -62,6 +57,7 @@ class IniConfig: @property def epilog(self): + # text to show within the parsers epilog return ( f"{os.linesep}config file {str(self.config_file)!r} {self.STATE[self.has_config_file]} " f"(change{'d' if self.is_env_var else ''} via env var {self.TOX_CONFIG_FILE_ENV_VAR})" diff --git a/src/tox/config/cli/parse.py b/src/tox/config/cli/parse.py index 5ba8c5f1..a03f3a50 100644 --- a/src/tox/config/cli/parse.py +++ b/src/tox/config/cli/parse.py @@ -1,3 +1,7 @@ +""" +This module pulls together this package: create and parse CLI arguments for tox. +""" + from typing import Dict, List, Tuple from tox.report import setup_report @@ -7,13 +11,14 @@ from .parser import Handler, Parsed, ToxParser def get_options(*args) -> Tuple[Parsed, List[str], Dict[str, Handler]]: guess_verbosity = _get_base(args) - handlers, parsed, unknown = _get_core(args) + handlers, parsed, unknown = _get_all(args) if guess_verbosity != parsed.verbosity: setup_report(parsed.verbosity, parsed.is_colored) # pragma: no cover return parsed, unknown, handlers def _get_base(args): + """First just load the base options (verbosity+color) to setup the logging framework.""" tox_parser = ToxParser.base() parsed, unknown = tox_parser.parse(args) guess_verbosity = parsed.verbosity @@ -21,18 +26,22 @@ def _get_base(args): return guess_verbosity -def _get_core(args): - tox_parser = _get_core_parser() +def _get_all(args): + """Parse all the options.""" + tox_parser = _get_parser() parsed, unknown = tox_parser.parse(args) handlers = {k: p for k, (_, p) in tox_parser.handlers.items()} return handlers, parsed, unknown -def _get_core_parser(): - tox_parser = ToxParser.core() - # noinspection PyUnresolvedReferences - from tox.plugin.manager import MANAGER +def _get_parser(): + tox_parser = ToxParser.core() # load the core options + # plus options setup by plugins + from tox.plugin.manager import MANAGER # noqa MANAGER.tox_add_option(tox_parser) tox_parser.fix_defaults() return tox_parser + + +__all__ = ("get_options",) diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py index 6339149d..23a08785 100644 --- a/src/tox/config/cli/parser.py +++ b/src/tox/config/cli/parser.py @@ -1,22 +1,37 @@ +""" +Customize argparse logic for tox (also contains the base options). +""" + import argparse import logging import os import sys -from argparse import SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace +from argparse import ( + SUPPRESS, + Action, + ArgumentDefaultsHelpFormatter, + ArgumentParser, + Namespace, +) from itertools import chain from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar from tox.config.source.ini import StrConvert -from tox.plugin.util import NAME +from tox.plugin import NAME from tox.session.state import State from .env_var import get_env_var from .ini import IniConfig +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal # noqa + class ArgumentParserWithEnvAndConfig(ArgumentParser): """ - Custom option parser which updates its defaults by checking the configuration files and environmental variables + Argument parser which updates its defaults by checking the configuration files and environmental variables. """ def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -47,10 +62,6 @@ class ArgumentParserWithEnvAndConfig(ArgumentParser): if of_type is None: if isinstance(action, argparse._StoreAction) and action.choices: # noqa loc = locals() - if sys.version_info >= (3, 8): - from typing import Literal # noqa - else: - from typing_extensions import Literal # noqa loc["Literal"] = Literal as_literal = f"Literal[{', '.join(repr(i) for i in action.choices)}]" of_type = eval(as_literal, globals(), loc) @@ -64,12 +75,16 @@ class ArgumentParserWithEnvAndConfig(ArgumentParser): class HelpFormatter(ArgumentDefaultsHelpFormatter): + """ + A help formatter that provides the default value and the source it comes from. + """ + def __init__(self, prog: str) -> None: super().__init__(prog, max_help_position=42, width=240) def _get_help_string(self, action: Action) -> str: - # noinspection PyProtectedMember - text = super()._get_help_string(action) + + text = super()._get_help_string(action) # noqa if hasattr(action, "default_source"): default = " (default: %(default)s)" if text.endswith(default): @@ -77,6 +92,12 @@ class HelpFormatter(ArgumentDefaultsHelpFormatter): return text +Handler = Callable[[State], Optional[int]] + + +ToxParserT = TypeVar("ToxParserT", bound="ToxParser") + + class Parsed(Namespace): @property def verbosity(self) -> int: @@ -87,18 +108,14 @@ class Parsed(Namespace): return self.colored == "yes" -Handler = Callable[[State], Optional[int]] - - -ToxParserT = TypeVar("ToxParserT", bound="ToxParser") - - class ToxParser(ArgumentParserWithEnvAndConfig): + """Argument parser for tox.""" + def __init__(self, *args: Any, root: bool = False, add_cmd: bool = False, **kwargs: Any) -> None: super().__init__(*args, **kwargs) if root is True: self._add_base_options() - self.handlers = {} # type:Dict[str, Tuple[Any, Handler]] + self.handlers: Dict[str, Tuple[Any, Handler]] = {} if add_cmd is True: self._cmd = self.add_subparsers(title="command", help="tox command to execute", dest="command") self._cmd.required = False @@ -130,6 +147,7 @@ class ToxParser(ArgumentParserWithEnvAndConfig): return cls(prog=NAME, formatter_class=HelpFormatter, add_cmd=True, root=True) def _add_base_options(self) -> None: + """Argument options that always make sense.""" from tox.report import LEVELS level_map = "|".join("{} - {}".format(c, logging.getLevelName(l)) for c, l in sorted(list(LEVELS.items()))) @@ -163,7 +181,8 @@ class ToxParser(ArgumentParserWithEnvAndConfig): return result, unknown def _inject_default_cmd(self, args): - # we need to inject the command if not present and reorganize args left of the command + # if the users specifies no command we imply he wants run, however for this to work we need to inject it onto + # the argument parsers left side if self._cmd is None: # no commands yet so must be all global, nothing to fix return args _global = { diff --git a/src/tox/config/core.py b/src/tox/config/core.py index 236deaa4..d74d1157 100644 --- a/src/tox/config/core.py +++ b/src/tox/config/core.py @@ -1,3 +1,4 @@ +"""Define configuration options that are part of the core tox configurations""" from pathlib import Path from tox.config.sets import ConfigSet diff --git a/src/tox/config/sets.py b/src/tox/config/sets.py index 6d8bc82b..7db0632a 100644 --- a/src/tox/config/sets.py +++ b/src/tox/config/sets.py @@ -1,3 +1,6 @@ +""" +Group together configuration values that belong together (such as base tox configuration, tox environment configs) +""" from abc import ABC, abstractmethod from collections import OrderedDict from copy import deepcopy @@ -21,6 +24,8 @@ if TYPE_CHECKING: class ConfigDefinition(ABC): + """Abstract base class for configuration definitions""" + def __init__(self, keys: Iterable[str], desc: str) -> None: self.keys = keys self.desc = desc @@ -31,6 +36,8 @@ class ConfigDefinition(ABC): class ConfigConstantDefinition(ConfigDefinition): + """A configuration definition whose value is defined upfront (such as the tox environment name)""" + def __init__(self, keys: Iterable[str], desc: str, value: Any) -> None: super().__init__(keys, desc) self.value = value @@ -47,6 +54,8 @@ _PLACE_HOLDER = object() class ConfigDynamicDefinition(ConfigDefinition): + """A configuration definition that comes from a source (such as in memory, an ini file, a toml file, etc.)""" + def __init__( self, keys: Iterable[str], @@ -97,9 +106,11 @@ class ConfigDynamicDefinition(ConfigDefinition): class ConfigSet: + """A set of configuration that belong together (such as a tox environment settings, core tox settings)""" + def __init__(self, raw: Loader, conf: "Config"): self._raw = raw - self._defined = {} # type:Dict[str, ConfigDefinition] + self._defined: Dict[str, ConfigDefinition] = {} self._conf = conf self._keys = OrderedDict() self._raw.setup_with_conf(self) @@ -115,14 +126,6 @@ class ConfigSet: ): """ Add configuration value. - - :param keys: - :param of_type: - :param default: - :param desc: - :param post_process: - :param overwrite: - :return: """ keys_ = self._make_keys(keys) for key in keys_: @@ -163,4 +166,5 @@ class ConfigSet: return iter(self._keys.keys()) def unused(self) -> Set[str]: + """Return a list of keys present in the config source but not used""" return self._raw.found_keys() - set(self._defined.keys()) diff --git a/src/tox/config/source/api.py b/src/tox/config/source/api.py index 7d8405a8..53f6b295 100644 --- a/src/tox/config/source/api.py +++ b/src/tox/config/source/api.py @@ -9,14 +9,14 @@ from tox.execute.request import shell_cmd if sys.version_info >= (3, 8): from typing import Literal else: - from typing_extensions import Literal + from typing_extensions import Literal # noqa _NO_MAPPING = object() class Command: def __init__(self, args): - self.args = args # type:List[str] + self.args: List[str] = args def __repr__(self): return f"{type(self).__name__}(args={self.args!r})" @@ -44,6 +44,8 @@ class EnvList: class Convert(ABC): + """Base abstract class that defines transformation of a value to a tox configuration""" + def to(self, raw, of_type): from_module = getattr(of_type, "__module__", None) if from_module in ("typing", "typing_extensions"): @@ -63,7 +65,7 @@ class Convert(ABC): def _to_typing(self, raw, of_type): origin = getattr(of_type, "__origin__", getattr(of_type, "__class__", None)) if origin is not None: - result = _NO_MAPPING # type: Any + result: Any = _NO_MAPPING if origin in (list, List): result = [self.to(i, of_type.__args__[0]) for i in self.to_list(raw)] elif origin in (set, Set): @@ -159,9 +161,11 @@ class Loader(Convert, ABC): class Source(ABC): + """An abstract configuration source. Has a core and per tox environment configuration loader.""" + def __init__(self, core: Loader) -> None: - self.core = core # type: Loader - self._envs = {} # type: Dict[str, Loader] + self.core: Loader = core + self._envs: Dict[str, Loader] = {} @abstractmethod def envs(self, core_conf): diff --git a/src/tox/config/source/ini/__init__.py b/src/tox/config/source/ini/__init__.py index da5678b7..a71d3418 100644 --- a/src/tox/config/source/ini/__init__.py +++ b/src/tox/config/source/ini/__init__.py @@ -1,3 +1,4 @@ +"""Load """ from configparser import ConfigParser, SectionProxy from copy import deepcopy from itertools import chain @@ -15,6 +16,8 @@ TEST_ENV_PREFIX = f"{BASE_TEST_ENV}:" class Ini(Source): + """Configuration sourced from a ini file (such as tox.ini)""" + CORE_PREFIX = "tox" def __init__(self, path: Path) -> None: @@ -30,7 +33,7 @@ class Ini(Source): section_loader=self._get_section, ) super().__init__(core) - self._envs = {} # type: Dict[str, IniLoader] + self._envs: Dict[str, IniLoader] = {} def __deepcopy__(self, memo): # python < 3.7 cannot copy config parser @@ -101,7 +104,7 @@ class Ini(Source): class IniLoader(StrConvert, Loader): - """Load from ini section""" + """Load configuration from an ini section (ini file is a string to string dictionary)""" def __init__( self, @@ -112,10 +115,10 @@ class IniLoader(StrConvert, Loader): section_loader, ) -> None: super().__init__(name) - self._section = section # type:Optional[SectionProxy] - self._src = src # type: Ini - self._default_base = default_base # type:EnvList - self._base = [] # type:List[IniLoader] + self._section: Optional[SectionProxy] = section + self._src: Ini = src + self._default_base: EnvList = default_base + self._base: List[IniLoader] = [] self._section_loader = section_loader def __deepcopy__(self, memo): @@ -131,15 +134,15 @@ class IniLoader(StrConvert, Loader): return result def setup_with_conf(self, conf: ConfigSet): - # noinspection PyUnusedLocal - def load_bases(values, conf_): - result = [] # type: List[IniLoader] + def load_bases(values, conf_): # noqa + result: List[IniLoader] = [] for value in values: name = value.lstrip(TEST_ENV_PREFIX) - ini_loader = self._src.get_section(value, name) # type: IniLoader + ini_loader: IniLoader = self._src.get_section(value, name) result.append(ini_loader) return result + # allow environment inheritance conf.add_config( keys="base", of_type=EnvList, diff --git a/src/tox/config/source/ini/convert.py b/src/tox/config/source/ini/convert.py index 155365a9..a283a35a 100644 --- a/src/tox/config/source/ini/convert.py +++ b/src/tox/config/source/ini/convert.py @@ -1,3 +1,4 @@ +"""Convert string configuration values to tox python configuration objects.""" import shlex from itertools import chain from pathlib import Path diff --git a/src/tox/config/source/ini/factor.py b/src/tox/config/source/ini/factor.py index 77c52636..4c9d9662 100644 --- a/src/tox/config/source/ini/factor.py +++ b/src/tox/config/source/ini/factor.py @@ -1,3 +1,6 @@ +""" +Expand tox factor expressions to tox environment list. +""" import itertools import re @@ -83,4 +86,9 @@ def expand_env_with_negation(value): yield variant_str -__all__ = ("filter_for_env", "find_envs", "expand_factors", "extend_factors") +__all__ = ( + "filter_for_env", + "find_envs", + "expand_factors", + "extend_factors", +) diff --git a/src/tox/config/source/ini/replace.py b/src/tox/config/source/ini/replace.py index 09f8f2a0..10f72b21 100644 --- a/src/tox/config/source/ini/replace.py +++ b/src/tox/config/source/ini/replace.py @@ -1,3 +1,6 @@ +""" +Apply value substitution (replacement) on tox strings. +""" import os import re import sys @@ -20,8 +23,7 @@ BASE_TEST_ENV = "testenv" def substitute_once(val, conf, name, section_loader): - # noinspection PyTypeChecker - return RE_ITEM_REF.sub(partial(_replace_match, conf, name, section_loader), val) + return RE_ITEM_REF.sub(partial(_replace_match, conf, name, section_loader), val) # noqa def replace(value, conf, name, section_loader): @@ -54,7 +56,6 @@ def _replace_match(conf: Config, name, section_loader, match): else: value = groups["key"] section = groups["section"] or name - # noinspection PyBroadException if section not in conf: env_conf = section_loader(section) else: @@ -62,7 +63,6 @@ def _replace_match(conf: Config, name, section_loader, match): try: replace_value = env_conf[value] except Exception: # noqa - # noinspection PyBroadException try: try: if groups["section"] is None: @@ -73,7 +73,7 @@ def _replace_match(conf: Config, name, section_loader, match): if key_missing_value is None: raise replace_value = key_missing_value - except Exception: + except Exception: # noqa start, end = match.span() replace_value = match.string[start:end] if replace_value is None: diff --git a/src/tox/config/source/toml.py b/src/tox/config/source/toml.py deleted file mode 100644 index e69de29b..00000000 --- a/src/tox/config/source/toml.py +++ /dev/null diff --git a/src/tox/execute/__init__.py b/src/tox/execute/__init__.py index e69de29b..58dc0cdf 100644 --- a/src/tox/execute/__init__.py +++ b/src/tox/execute/__init__.py @@ -0,0 +1,10 @@ +""" +Package that handles execution of commands within tox environments. +""" +from .api import Outcome +from .request import ExecuteRequest + +__all__ = ( + "ExecuteRequest", + "Outcome", +) diff --git a/src/tox/execute/api.py b/src/tox/execute/api.py index a882e73f..93078688 100644 --- a/src/tox/execute/api.py +++ b/src/tox/execute/api.py @@ -1,3 +1,6 @@ +""" +Abstract base API for executing commands within tox environments. +""" import logging import signal import sys @@ -17,13 +20,58 @@ Executor = Callable[[ExecuteRequest, ContentHandler, ContentHandler], int] SIGINT = signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT +class Execute(ABC): + """Abstract API for execution of a tox environment""" + + def __call__(self, request: ExecuteRequest, show_on_standard: bool, colored: bool) -> "Outcome": + start = timer() + executor = self.executor() + interrupt = None + try: + with CollectWrite(sys.stdout if show_on_standard else None) as out: + with CollectWrite(sys.stderr if show_on_standard else None, Fore.RED if colored else None) as err: + instance: ExecuteInstance = executor(request, out.collect, err.collect) + try: + exit_code = instance.run() + except KeyboardInterrupt as exception: + interrupt = exception + while True: + try: + is_main = threading.current_thread() == threading.main_thread() + if is_main: + # disable further interrupts until we finish this, main thread only + if sys.platform != "win32": + signal.signal(SIGINT, signal.SIG_IGN) + except KeyboardInterrupt: # pragma: no cover + continue # pragma: no cover + else: + try: + exit_code = instance.interrupt() + break + finally: + if is_main and sys.platform != "win32": # restore signal handler on main thread + signal.signal(SIGINT, signal.default_int_handler) + finally: + end = timer() + result = Outcome(request, show_on_standard, exit_code, out.text, err.text, start, end, instance.cmd) + if interrupt is not None: + raise ToxKeyboardInterrupt(result, interrupt) + return result + + @staticmethod + @abstractmethod + def executor() -> Type["ExecuteInstance"]: + raise NotImplementedError + + class ExecuteInstance: + """An instance of a command execution""" + def __init__(self, request: ExecuteRequest, out_handler: ContentHandler, err_handler: ContentHandler) -> None: def _safe_handler(handler, data): - # noinspection PyBroadException try: handler(data) - except Exception: # pragma: no cover + except Exception: # noqa # pragma: no cover pass # pragma: no cover self.request = request @@ -45,6 +93,8 @@ class ExecuteInstance: class Outcome: + """Result of a command execution""" + OK = 0 def __init__( @@ -82,13 +132,7 @@ class Outcome: print(Fore.RED, file=sys.stderr, end="") print(self.err, file=sys.stderr, end="") print(Fore.RESET, file=sys.stderr) - logger.critical( - "exit code %d for %s: %s in %s", - self.exit_code, - self.request.cwd, - self.shell_cmd, - self.elapsed, - ) + logger.critical("exit code %d for %s: %s in %s", self.exit_code, self.request.cwd, self.shell_cmd, self.elapsed) raise SystemExit(self.exit_code) @property @@ -104,45 +148,3 @@ class ToxKeyboardInterrupt(KeyboardInterrupt): def __init__(self, outcome: Outcome, exc: KeyboardInterrupt): self.outcome = outcome self.exc = exc - - -class Execute(ABC): - def __call__(self, request: ExecuteRequest, show_on_standard: bool, colored: bool) -> Outcome: - start = timer() - executor = self.executor() - interrupt = None - try: - with CollectWrite(sys.stdout if show_on_standard else None) as out: - with CollectWrite(sys.stderr if show_on_standard else None, Fore.RED if colored else None) as err: - instance = executor(request, out.collect, err.collect) # type: ExecuteInstance - try: - exit_code = instance.run() - except KeyboardInterrupt as exception: - interrupt = exception - while True: - try: - is_main = threading.current_thread() == threading.main_thread() - if is_main: - # disable further interrupts until we finish this, main thread only - if sys.platform != "win32": - signal.signal(SIGINT, signal.SIG_IGN) - except KeyboardInterrupt: # pragma: no cover - continue # pragma: no cover - else: - try: - exit_code = instance.interrupt() - break - finally: - if is_main and sys.platform != "win32": # restore signal handler on main thread - signal.signal(SIGINT, signal.default_int_handler) - finally: - end = timer() - result = Outcome(request, show_on_standard, exit_code, out.text, err.text, start, end, instance.cmd) - if interrupt is not None: - raise ToxKeyboardInterrupt(result, interrupt) - return result - - @staticmethod - @abstractmethod - def executor() -> Type[ExecuteInstance]: - raise NotImplementedError diff --git a/src/tox/execute/local_sub_process/__init__.py b/src/tox/execute/local_sub_process/__init__.py index 4f6de97f..e08625d9 100644 --- a/src/tox/execute/local_sub_process/__init__.py +++ b/src/tox/execute/local_sub_process/__init__.py @@ -6,7 +6,14 @@ import sys from subprocess import PIPE, TimeoutExpired from typing import List, Optional, Sequence, Tuple, Type -from ..api import SIGINT, ContentHandler, Execute, ExecuteInstance, ExecuteRequest, Outcome +from ..api import ( + SIGINT, + ContentHandler, + Execute, + ExecuteInstance, + ExecuteRequest, + Outcome, +) from .read_via_thread import WAIT_GENERAL if sys.platform == "win32": @@ -40,7 +47,7 @@ class LocalSubProcessExecuteInstance(ExecuteInstance): def __init__(self, request: ExecuteRequest, out_handler: ContentHandler, err_handler: ContentHandler) -> None: super().__init__(request, out_handler, err_handler) self.process = None - self._cmd = [] # type: Optional[List[str]] + self._cmd: Optional[List[str]] = [] @property def cmd(self) -> Sequence[str]: diff --git a/src/tox/execute/request.py b/src/tox/execute/request.py index 9f4fe415..328499b2 100644 --- a/src/tox/execute/request.py +++ b/src/tox/execute/request.py @@ -1,13 +1,16 @@ +"""Module declaring a command execution request.""" import sys from pathlib import Path from typing import Dict, List, Sequence, Union class ExecuteRequest: + """Defines a commands execution request""" + def __init__(self, cmd: Sequence[Union[str, Path]], cwd: Path, env: Dict[str, str], allow_stdin: bool): if len(cmd) == 0: raise ValueError("cannot execute an empty command") - self.cmd = [str(i) for i in cmd] # type: List[str] + self.cmd: List[str] = [str(i) for i in cmd] self.cwd = cwd self.env = env self.allow_stdin = allow_stdin diff --git a/src/tox/execute/stream.py b/src/tox/execute/stream.py index 465b2751..178fdb66 100644 --- a/src/tox/execute/stream.py +++ b/src/tox/execute/stream.py @@ -12,13 +12,13 @@ class CollectWrite: def __init__(self, target: Optional[IO[bytes]], color: Optional[str] = None) -> None: self._content = bytearray() - self._print_to = None if target is None else target.buffer # type:Optional[IO[bytes]] - self._do_print = target is not None # type: bool - self._keep_printing = Event() # type: Event - self._content_lock = Lock() # type: Lock - self._print_lock = Lock() # type: Lock - self._at = 0 # type: int - self._color = color # type: Optional[str] + self._print_to: Optional[IO[bytes]] = None if target is None else target.buffer + self._do_print: bool = target is not None + self._keep_printing: Event = Event() + self._content_lock: Lock = Lock() + self._print_lock: Lock = Lock() + self._at: int = 0 + self._color: Optional[str] = color def __enter__(self): if self._do_print: diff --git a/src/tox/helper/__init__.py b/src/tox/helper/__init__.py index 482e21b3..7267899c 100644 --- a/src/tox/helper/__init__.py +++ b/src/tox/helper/__init__.py @@ -1,3 +1,6 @@ +""" +Contains helper scripts. +""" from pathlib import Path HERE = Path(__file__).absolute().parent diff --git a/src/tox/helper/build_isolated.py b/src/tox/helper/build_isolated.py index 5de28a96..b6d615a1 100644 --- a/src/tox/helper/build_isolated.py +++ b/src/tox/helper/build_isolated.py @@ -1,3 +1,6 @@ +""" +Build a package per PEP-517. +""" import json import sys @@ -8,8 +11,7 @@ extra = json.loads(sys.argv[4]) backend_spec = sys.argv[5] backend_obj = sys.argv[6] if len(sys.argv) >= 7 else None -# noinspection PyTypeChecker -backend = __import__(backend_spec, fromlist=[None]) +backend = __import__(backend_spec, fromlist=[None]) # noqa if backend_obj: backend = getattr(backend, backend_obj) diff --git a/src/tox/helper/build_requires.py b/src/tox/helper/build_requires.py index 6f47c90b..a76f7e60 100644 --- a/src/tox/helper/build_requires.py +++ b/src/tox/helper/build_requires.py @@ -1,3 +1,7 @@ +""" +Get environment requires per PEP-517. +""" + import json import sys @@ -5,8 +9,8 @@ into = sys.argv[1] backend_spec = sys.argv[2] backend_obj = sys.argv[3] if len(sys.argv) >= 4 else None -# noinspection PyTypeChecker -backend = __import__(backend_spec, fromlist=[None]) + +backend = __import__(backend_spec, fromlist=[None]) # noqa if backend_obj: backend = getattr(backend, backend_obj) diff --git a/src/tox/helper/wheel_meta.py b/src/tox/helper/wheel_meta.py index 9f512253..d63cf3e1 100644 --- a/src/tox/helper/wheel_meta.py +++ b/src/tox/helper/wheel_meta.py @@ -1,3 +1,6 @@ +""" +Get projects metadata per PEP-517. +""" import json import sys @@ -6,8 +9,8 @@ extra = json.loads(sys.argv[2]) backend_spec = sys.argv[3] backend_obj = sys.argv[4] if len(sys.argv) >= 5 else None -# noinspection PyTypeChecker -backend = __import__(backend_spec, fromlist=[None]) + +backend = __import__(backend_spec, fromlist=[None]) # noqa if backend_obj: backend = getattr(backend, backend_obj) diff --git a/src/tox/log/__init__.py b/src/tox/log/__init__.py index ed549068..bfa2defc 100644 --- a/src/tox/log/__init__.py +++ b/src/tox/log/__init__.py @@ -1,4 +1,10 @@ """This module handles collecting and persisting in json format a tox session""" +from .command import CommandLog +from .env import EnvLog from .result import ResultLog -__all__ = ("ResultLog",) +__all__ = ( + "ResultLog", + "EnvLog", + "CommandLog", +) diff --git a/src/tox/log/command.py b/src/tox/log/command.py index 6a3d34bf..db9e3ee9 100644 --- a/src/tox/log/command.py +++ b/src/tox/log/command.py @@ -1,3 +1,6 @@ +"""Record commands executed by tox""" + + class CommandLog: """Report commands interacting with third party tools""" diff --git a/src/tox/log/env.py b/src/tox/log/env.py index e7b9f9e7..e9a2b562 100644 --- a/src/tox/log/env.py +++ b/src/tox/log/env.py @@ -1,3 +1,5 @@ +"""Record information about tox environments""" + from copy import copy from .command import CommandLog diff --git a/src/tox/log/result.py b/src/tox/log/result.py index 785bebbc..051bf7ad 100644 --- a/src/tox/log/result.py +++ b/src/tox/log/result.py @@ -1,4 +1,4 @@ -"""Generate json report of a run""" +"""Generate json report of a tox run""" import json import socket import sys diff --git a/src/tox/plugin/__init__.py b/src/tox/plugin/__init__.py index e69de29b..01dc773d 100644 --- a/src/tox/plugin/__init__.py +++ b/src/tox/plugin/__init__.py @@ -0,0 +1,5 @@ +""" +API for the plugin system used. +""" + +NAME = "tox" diff --git a/src/tox/plugin/impl.py b/src/tox/plugin/impl.py index 858b13ca..bc2f71ab 100644 --- a/src/tox/plugin/impl.py +++ b/src/tox/plugin/impl.py @@ -1,5 +1,5 @@ import pluggy -from .util import NAME +from . import NAME impl = pluggy.HookimplMarker(NAME) diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py index 8d5eb84e..83ec38d4 100644 --- a/src/tox/plugin/manager.py +++ b/src/tox/plugin/manager.py @@ -1,3 +1,4 @@ +"""Contains the plugin manager object""" from typing import List, Type import pluggy @@ -15,13 +16,12 @@ from tox.tox_env.python.virtual_env.package import dev from tox.tox_env.python.virtual_env.package.artifact import sdist, wheel from tox.tox_env.register import REGISTER, ToxEnvRegister -from . import spec -from .util import NAME +from . import NAME, spec class Plugin: def __init__(self) -> None: - self.manager = pluggy.PluginManager(NAME) # type:pluggy.PluginManager + self.manager: pluggy.PluginManager = pluggy.PluginManager(NAME) self.manager.add_hookspecs(spec) internal_plugins = ( diff --git a/src/tox/plugin/spec.py b/src/tox/plugin/spec.py index 7042f7ac..fdaf3438 100644 --- a/src/tox/plugin/spec.py +++ b/src/tox/plugin/spec.py @@ -8,24 +8,21 @@ from tox.config.sets import ConfigSet from tox.tox_env.api import ToxEnv from tox.tox_env.register import ToxEnvRegister -from .util import NAME +from . import NAME hook_spec = pluggy.HookspecMarker(NAME) -# noinspection PyUnusedLocal @hook_spec -def tox_add_option(parser: ArgumentParser) -> None: +def tox_add_option(parser: ArgumentParser) -> None: # noqa """add cli flags""" -# noinspection PyUnusedLocal @hook_spec -def tox_add_core_config(core: ConfigSet) -> None: +def tox_add_core_config(core: ConfigSet) -> None: # noqa """add options to the core section of the tox""" -# noinspection PyUnusedLocal @hook_spec -def tox_register_tox_env(register: ToxEnvRegister) -> Type[ToxEnv]: +def tox_register_tox_env(register: ToxEnvRegister) -> Type[ToxEnv]: # noqa """register new tox environment types that can have their own argument""" diff --git a/src/tox/plugin/util.py b/src/tox/plugin/util.py deleted file mode 100644 index 30f0740a..00000000 --- a/src/tox/plugin/util.py +++ /dev/null @@ -1 +0,0 @@ -NAME = "tox" diff --git a/src/tox/provision/__init__.py b/src/tox/provision/__init__.py index a21dc161..315b3e62 100644 --- a/src/tox/provision/__init__.py +++ b/src/tox/provision/__init__.py @@ -1,3 +1,6 @@ +""" +This package handles provisioning an appropriate tox version per requirements. +""" import sys from typing import List @@ -65,13 +68,12 @@ def tox_add_core_config(core: ConfigSet): post_process=add_tox_requires_min_version, ) core.add_config( - keys=["no_package", "app", "skip_sdist"], + keys=["no_package", "skipsdist"], of_type=bool, default=False, desc="Is there any packaging involved in this project.", ) -# noinspection PyUnusedLocal -def run_provision(deps: List[Requirement], tox_env: ToxEnv): +def run_provision(deps: List[Requirement], tox_env: ToxEnv): # noqa """""" diff --git a/src/tox/pytest.py b/src/tox/pytest.py index d6f201b9..5b5b7ca5 100644 --- a/src/tox/pytest.py +++ b/src/tox/pytest.py @@ -1,3 +1,7 @@ +""" +A pytest plugin useful to test tox itself (and its plugins). +""" + import os import sys import textwrap @@ -83,7 +87,7 @@ def empty_project(tox_project, monkeypatch): class ToxProject: def __init__(self, files: Dict[str, Any], path: Path, capsys, monkeypatch): - self.path = path # type: Path + self.path: Path = path self._capsys = capsys self.monkeypatch = monkeypatch self._setup_files(self.path, files) @@ -155,12 +159,12 @@ class ToxRunOutcome: def __init__(self, cmd: Sequence[str], cwd: Path, code: int, out: str, err: str, state: Optional[State]) -> None: extended_cmd = [sys.executable, "-m", "tox"] extended_cmd.extend(cmd) - self.cmd = extended_cmd # type: List[str] - self.cwd = cwd # type: Path - self.code = code # type:int - self.out = out # type:str - self.err = err # type: str - self.state = state # type:Optional[State] + self.cmd: List[str] = extended_cmd + self.cwd: Path = cwd + self.code: int = code + self.out: str = out + self.err: str = err + self.state: Optional[State] = state @property def success(self) -> bool: diff --git a/src/tox/report.py b/src/tox/report.py index 8ca144e2..bdc02b08 100644 --- a/src/tox/report.py +++ b/src/tox/report.py @@ -1,3 +1,4 @@ +"""Handle reporting from within tox""" import logging import sys diff --git a/src/tox/run.py b/src/tox/run.py index 51d6a1ea..7762de12 100644 --- a/src/tox/run.py +++ b/src/tox/run.py @@ -1,3 +1,4 @@ +"""Main entry point for tox.""" import sys from pathlib import Path from typing import Optional, Sequence @@ -11,27 +12,35 @@ from tox.tox_env.builder import build_tox_envs def run(args: Optional[Sequence[str]] = None) -> None: try: - state = setup_state(args) - command = state.options.command - handler = state.handlers[command] - result = handler(state) - if result is None: - result = 0 - raise SystemExit(result) + result = main(sys.argv[1:] if args is None else args) except KeyboardInterrupt: - raise SystemExit(-2) + result = -2 + raise SystemExit(result) -def make_config(path: Path) -> Config: - tox_ini = path / "tox.ini" - ini_loader = Ini(tox_ini) - return Config(ini_loader) +def main(args: Sequence[str]) -> int: + state = setup_state(args) + command = state.options.command + handler = state.handlers[command] + result = handler(state) + if result is None: + result = 0 + return result -def setup_state(args: Optional[Sequence[str]]) -> State: - if args is None: - args = sys.argv[1:] +def setup_state(args: Sequence[str]) -> State: + """Setup the state object of this run.""" + # parse CLI arguments options = get_options(*args) + # parse configuration file config = make_config(Path().absolute()) + # build tox environment config objects state = build_tox_envs(config, options, args) return state + + +def make_config(path: Path) -> Config: + """Make a tox configuration object.""" + # for now only tox.ini supported + ini_loader = Ini(path / "tox.ini") + return Config(ini_loader) diff --git a/src/tox/session/__init__.py b/src/tox/session/__init__.py index e69de29b..e818d7ac 100644 --- a/src/tox/session/__init__.py +++ b/src/tox/session/__init__.py @@ -0,0 +1,3 @@ +""" +Package that handles execution of various commands within tox. +""" diff --git a/src/tox/session/cmd/list_env.py b/src/tox/session/cmd/list_env.py index 82ca38ae..6207e3cb 100644 --- a/src/tox/session/cmd/list_env.py +++ b/src/tox/session/cmd/list_env.py @@ -1,3 +1,6 @@ +""" +Print available tox environments. +""" from tox.config.cli.parser import ToxParser from tox.plugin.impl import impl from tox.session.state import State diff --git a/src/tox/session/cmd/run/__init__.py b/src/tox/session/cmd/run/__init__.py index e69de29b..791f5094 100644 --- a/src/tox/session/cmd/run/__init__.py +++ b/src/tox/session/cmd/run/__init__.py @@ -0,0 +1,3 @@ +""" +Defines how we execute a tox environment. +""" diff --git a/src/tox/session/cmd/run/common.py b/src/tox/session/cmd/run/common.py index d84d55ec..118f2ed0 100644 --- a/src/tox/session/cmd/run/common.py +++ b/src/tox/session/cmd/run/common.py @@ -1,3 +1,4 @@ +"""Common functionality shared across multiple type of runs""" from tox.config.cli.parser import ToxParser diff --git a/src/tox/session/cmd/run/parallel.py b/src/tox/session/cmd/run/parallel.py index 0511f2ae..affe239d 100644 --- a/src/tox/session/cmd/run/parallel.py +++ b/src/tox/session/cmd/run/parallel.py @@ -1,3 +1,6 @@ +""" +Run tox environments in parallel. +""" import inspect import logging import os @@ -22,7 +25,7 @@ logger = logging.getLogger(__name__) ENV_VAR_KEY = "TOX_PARALLEL_ENV" OFF_VALUE = 0 DEFAULT_PARALLEL = OFF_VALUE -MAIN_FILE = Path(inspect.getsourcefile(tox)) / "__main__.py" +MAIN_FILE = Path(inspect.getsourcefile(tox)).parent / "__main__.py" @impl @@ -169,8 +172,7 @@ def _stop_child_processes(processes, main_threads): """A three level stop mechanism for children - INT (250ms) -> TERM (100ms) -> KILL""" # first stop children - # noinspection PyUnusedLocal - def shutdown(tox_env, action, process): + def shutdown(tox_env, action, process): # noqa action.handle_interrupt(process) threads = [Thread(target=shutdown, args=(n, a, p)) for n, (a, p) in processes.items()] diff --git a/src/tox/session/cmd/run/sequential.py b/src/tox/session/cmd/run/sequential.py index bdf76074..688377c1 100644 --- a/src/tox/session/cmd/run/sequential.py +++ b/src/tox/session/cmd/run/sequential.py @@ -1,3 +1,6 @@ +""" +Run tox environments in sequential order. +""" from typing import Dict from tox.config.cli.parser import ToxParser @@ -19,15 +22,14 @@ def tox_add_option(parser: ToxParser) -> None: def run_sequential(state: State) -> int: - status_codes = {} # type:Dict[str, int] + status_codes: Dict[str, int] = {} for name in state.env_list: tox_env = state.tox_envs[name] status_codes[name] = run_one(tox_env, state.options.recreate, state.options.no_test) return report(status_codes, state.tox_envs) -# noinspection PyUnusedLocal -def report(status_dict: Dict[str, int], tox_envs: Dict[str, ToxEnv]) -> int: +def report(status_dict: Dict[str, int], tox_envs: Dict[str, ToxEnv]) -> int: # noqa for name, status in status_dict.items(): if status == Outcome.OK: msg = "OK " diff --git a/src/tox/session/cmd/run/single.py b/src/tox/session/cmd/run/single.py index 8e96c134..1c29c429 100644 --- a/src/tox/session/cmd/run/single.py +++ b/src/tox/session/cmd/run/single.py @@ -1,3 +1,6 @@ +""" +Defines how to run a single tox environment. +""" from typing import List, cast from tox.config.source.api import Command diff --git a/src/tox/session/cmd/show_config.py b/src/tox/session/cmd/show_config.py index eb61b283..fe277242 100644 --- a/src/tox/session/cmd/show_config.py +++ b/src/tox/session/cmd/show_config.py @@ -1,3 +1,6 @@ +""" +Show materialized configuration of tox environments. +""" from typing import Any, List, Union from tox.config.cli.parser import ToxParser diff --git a/src/tox/session/cmd/version_flag.py b/src/tox/session/cmd/version_flag.py index ed84379f..83c3916c 100644 --- a/src/tox/session/cmd/version_flag.py +++ b/src/tox/session/cmd/version_flag.py @@ -1,3 +1,6 @@ +""" +Display the version information about tox. +""" from pathlib import Path from tox.config.cli.parser import ToxParser diff --git a/src/tox/session/common.py b/src/tox/session/common.py index ee505266..141194c1 100644 --- a/src/tox/session/common.py +++ b/src/tox/session/common.py @@ -7,8 +7,7 @@ from tox.config.source.ini import StrConvert def env_list_flag(parser: ToxParser): class ToxEnvList(argparse.Action): - # noinspection PyShadowingNames - def __call__(self, parser, args, values, option_string=None): + def __call__(self, parser, args, values, option_string=None): # noqa list_envs = StrConvert().to(values, of_type=List[str]) setattr(args, self.dest, list_envs) diff --git a/src/tox/session/state.py b/src/tox/session/state.py index d6068f6b..e66b9f3b 100644 --- a/src/tox/session/state.py +++ b/src/tox/session/state.py @@ -6,8 +6,8 @@ from tox.tox_env.runner import RunToxEnv class State: def __init__(self, conf, tox_envs, opt_parse, args): - self.conf = conf # type:Config - self.tox_envs = tox_envs # type:Dict[str, RunToxEnv] + self.conf: Config = conf + self.tox_envs: Dict[str, RunToxEnv] = tox_envs options, unknown, handlers = opt_parse self.options = options self.unknown_options = unknown diff --git a/src/tox/tox_env/__init__.py b/src/tox/tox_env/__init__.py index e69de29b..b9feade9 100644 --- a/src/tox/tox_env/__init__.py +++ b/src/tox/tox_env/__init__.py @@ -0,0 +1,3 @@ +""" +Package handling the creation and management of tox environments. +""" diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py index 3cc5b225..da0f1257 100644 --- a/src/tox/tox_env/api.py +++ b/src/tox/tox_env/api.py @@ -1,3 +1,6 @@ +""" +Defines the abstract base traits of a tox environment. +""" import itertools import logging import os @@ -11,7 +14,7 @@ from tox.config.sets import ConfigSet from tox.execute.api import Execute from tox.execute.request import ExecuteRequest -from .cache import Cache +from .info import Info if sys.platform == "win32": PASS_ENV_ALWAYS = [ @@ -33,13 +36,13 @@ else: class ToxEnv(ABC): def __init__(self, conf: ConfigSet, core: ConfigSet, options, executor: Execute): - self.conf = conf # type: ConfigSet - self.core = core # type:ConfigSet + self.conf: ConfigSet = conf + self.core: ConfigSet = core self.options = options self._executor = executor self.register_config() - self._cache = Cache(self.conf["env_dir"] / ".tox-cache") - self._paths = [] # type:List[Path] + self._cache = Info(self.conf["env_dir"]) + self._paths: List[Path] = [] self.logger = logging.getLogger(self.conf["env_name"]) def register_config(self): @@ -101,11 +104,11 @@ class ToxEnv(ABC): @property def environment_variables(self) -> Dict[str, str]: - result = {} # type:Dict[str, str] - pass_env = self.conf["pass_env"] # type: List[str] + result: Dict[str, str] = {} + pass_env: List[str] = self.conf["pass_env"] pass_env.extend(PASS_ENV_ALWAYS) - set_env = self.conf["set_env"] # type: Dict[str, str] + set_env: Dict[str, str] = self.conf["set_env"] for key, value in os.environ.items(): if key in pass_env: result[key] = value diff --git a/src/tox/tox_env/builder.py b/src/tox/tox_env/builder.py index 13ea024e..c7576ea1 100644 --- a/src/tox/tox_env/builder.py +++ b/src/tox/tox_env/builder.py @@ -1,3 +1,6 @@ +""" +Build the state of a tox run (creates tox run and build environments). +""" import copy from typing import Dict, cast @@ -18,9 +21,9 @@ def build_tox_envs(config: Config, options, args): class Builder: def __init__(self, options: str, config: Config): - self.tox_env_to_runner = {} # type:Dict[str, RunToxEnv] - self._tox_env_to_runner_type = {} # type:Dict[str, str] - self._pkg_envs = {} # type:Dict[str, PackageToxEnv] + self.tox_env_to_runner: Dict[str, RunToxEnv] = {} + self._tox_env_to_runner_type: Dict[str, str] = {} + self._pkg_envs: Dict[str, PackageToxEnv] = {} self.options = options self._config = config self._run() @@ -49,7 +52,7 @@ class Builder: runner = cast(str, env_conf["runner"]) from .register import REGISTER - env = REGISTER.runner(runner)(env_conf, self._config.core, self.options) # type: RunToxEnv + env: RunToxEnv = REGISTER.runner(runner)(env_conf, self._config.core, self.options) self._tox_env_to_runner_type[env_name] = runner self._build_package_env(env) return env @@ -69,7 +72,7 @@ class Builder: def _get_package_env(self, packager, pkg_name): if pkg_name in self._pkg_envs: - package_tox_env = self._pkg_envs[pkg_name] # type:PackageToxEnv + package_tox_env: PackageToxEnv = self._pkg_envs[pkg_name] else: if pkg_name in self.tox_env_to_runner: # if already detected as runner remove del self.tox_env_to_runner[pkg_name] diff --git a/src/tox/tox_env/errors.py b/src/tox/tox_env/errors.py index 9a561d15..fd3d6217 100644 --- a/src/tox/tox_env/errors.py +++ b/src/tox/tox_env/errors.py @@ -1,10 +1,13 @@ -class Recreate(Exception): +"""Defines tox error types""" + + +class Recreate(RuntimeError): """Recreate the tox environment""" -class Skip(Exception): +class Skip(RuntimeError): """Skip this tox environment""" -class Fail(Exception): +class Fail(RuntimeError): """Failed creating env""" diff --git a/src/tox/tox_env/cache.py b/src/tox/tox_env/info.py index 5730256d..419f5174 100644 --- a/src/tox/tox_env/cache.py +++ b/src/tox/tox_env/info.py @@ -1,11 +1,15 @@ +""" +Declare and handle the tox env info file (a file at the root of every tox environment that contains information about +the status of the tox environment - python version of the environment, installed packages, etc.). +""" import json from contextlib import contextmanager from pathlib import Path -class Cache: +class Info: def __init__(self, path: Path) -> None: - self._path = path + self._path = path / ".tox-info.json" try: value = json.loads(self._path.read_text()) except (ValueError, OSError): diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py index 3906e867..1c6c409d 100644 --- a/src/tox/tox_env/package.py +++ b/src/tox/tox_env/package.py @@ -1,3 +1,6 @@ +""" +A tox environment that can build packages. +""" from abc import ABC, abstractmethod from typing import Any, List diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py index ebb03462..b315610f 100644 --- a/src/tox/tox_env/python/api.py +++ b/src/tox/tox_env/python/api.py @@ -1,3 +1,6 @@ +""" +Declare the abstract base class for tox environments that handle the Python language. +""" import sys from abc import ABC, abstractmethod from pathlib import Path @@ -82,8 +85,7 @@ class Python(ToxEnv, ABC): raise NoInterpreter(base_pythons) return self._python - # noinspection PyMethodMayBeStatic - def get_python(self, base): + def get_python(self, base): # noqa return get_interpreter(base) def cached_install(self, deps, section, of_type): diff --git a/src/tox/tox_env/python/package.py b/src/tox/tox_env/python/package.py index c826d9b9..bd44e01b 100644 --- a/src/tox/tox_env/python/package.py +++ b/src/tox/tox_env/python/package.py @@ -1,3 +1,6 @@ +""" +A tox build environment that handles Python packages. +""" import sys from abc import ABC, abstractmethod from typing import List diff --git a/src/tox/tox_env/python/runner.py b/src/tox/tox_env/python/runner.py index a5994eb1..e1215003 100644 --- a/src/tox/tox_env/python/runner.py +++ b/src/tox/tox_env/python/runner.py @@ -1,3 +1,6 @@ +""" +A tox run environment that handles the Python language. +""" from abc import ABC from typing import List, Set diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py index c2cc6743..1e4d5189 100644 --- a/src/tox/tox_env/python/virtual_env/api.py +++ b/src/tox/tox_env/python/virtual_env/api.py @@ -1,3 +1,6 @@ +""" +Declare the abstract base class for tox environments that handle the Python language via the virtualenv project. +""" import sys from abc import ABC from pathlib import Path @@ -58,8 +61,7 @@ class VirtualEnv(Python, ABC): def perform_install(self, install_command: Sequence[str]) -> Outcome: return self.execute(cmd=install_command, allow_stdin=False) - # noinspection PyMethodMayBeStatic - def install_command(self, develop, force_reinstall, no_deps, packages): + def install_command(self, develop, force_reinstall, no_deps, packages): # noqa install_command = ["python", "-m", "pip", "--disable-pip-version-check", "install"] if develop is True: install_command.append("-e") diff --git a/src/tox/tox_env/python/virtual_env/package/api.py b/src/tox/tox_env/python/virtual_env/package/api.py index 31ff65a3..5d49fe9f 100644 --- a/src/tox/tox_env/python/virtual_env/package/api.py +++ b/src/tox/tox_env/python/virtual_env/package/api.py @@ -39,12 +39,12 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, ABC): def __init__(self, conf: ConfigSet, core: ConfigSet, options) -> None: super().__init__(conf, core, options) backend_module, backend_object, requires = self.load_builder_and_requires() - self._requires = requires # type: List[Requirement] - self.build_backend_module = backend_module # type:str - self.build_backend_obj = backend_object # type: Optional[str] - self._distribution_meta = None # type:Optional[imp_meta.PathDistribution] - self._build_requires = None # type:Optional[List[Requirement]] - self._package = None # type:Optional[Requirement] + self._requires: List[Requirement] = requires + self.build_backend_module: str = backend_module + self.build_backend_obj: Optional[str] = backend_object + self._distribution_meta: Optional[imp_meta.PathDistribution] = None + self._build_requires: Optional[List[Requirement]] = None + self._package: Optional[Requirement] = None def load_builder_and_requires(self) -> Tuple[str, Optional[str], List[Requirement]]: py_project_toml = cast(Path, self.core["tox_root"]) / "pyproject.toml" @@ -113,12 +113,10 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, ABC): extra, _at = None, None if extra is None or extra in extras: if _at is not None: - # noinspection PyProtectedMember del markers[_at] _at -= 1 if _at > 0 and markers[_at] in ("and", "or"): del markers[_at] - # noinspection PyProtectedMember if len(markers) == 0: req.marker = None result.append(req) diff --git a/src/tox/tox_env/python/virtual_env/package/artifact/api.py b/src/tox/tox_env/python/virtual_env/package/artifact/api.py index 41316eac..dbaceac4 100644 --- a/src/tox/tox_env/python/virtual_env/package/artifact/api.py +++ b/src/tox/tox_env/python/virtual_env/package/artifact/api.py @@ -42,7 +42,7 @@ class Pep517VirtualEnvPackageArtifact(Pep517VirtualEnvPackage, ABC): result = self.execute(cmd=cmd, allow_stdin=False, cwd=self.core["tox_root"]) result.assert_success(self.logger) with open(str(out_file)) as file_handler: - base_name = json.load(file_handler) # type:str + base_name: str = json.load(file_handler) return [dest / base_name] def perform_packaging(self) -> List[Path]: diff --git a/src/tox/tox_env/python/virtual_env/runner.py b/src/tox/tox_env/python/virtual_env/runner.py index 86fc2254..15f1fa7a 100644 --- a/src/tox/tox_env/python/virtual_env/runner.py +++ b/src/tox/tox_env/python/virtual_env/runner.py @@ -1,7 +1,12 @@ +""" +A tox python environment runner that uses the virtualenv project. +""" from configparser import ConfigParser, NoSectionError from tox.plugin.impl import impl -from tox.tox_env.python.virtual_env.package.artifact.wheel import Pep517VirtualEnvPackageWheel +from tox.tox_env.python.virtual_env.package.artifact.wheel import ( + Pep517VirtualEnvPackageWheel, +) from tox.tox_env.register import ToxEnvRegister from ..runner import PythonRun diff --git a/src/tox/tox_env/register.py b/src/tox/tox_env/register.py index f8fed5d4..4732b752 100644 --- a/src/tox/tox_env/register.py +++ b/src/tox/tox_env/register.py @@ -6,13 +6,13 @@ from .runner import RunToxEnv class ToxEnvRegister: def __init__(self): - self._run_envs = {} # type:Dict[str, Type[RunToxEnv]] - self._package_envs = {} # type:Dict[str, Type[PackageToxEnv]] - self._default_run_env = "" # type:str + self._run_envs: Dict[str, Type[RunToxEnv]] = {} + self._package_envs: Dict[str, Type[PackageToxEnv]] = {} + self._default_run_env: str = "" def populate(self, manager): manager.tox_register_tox_env(register=self) - self._default_run_env = next(iter(self._run_envs.keys())) # type:str + self._default_run_env: str = next(iter(self._run_envs.keys())) def add_run_env(self, of_type: Type[RunToxEnv]): self._run_envs[of_type.id()] = of_type diff --git a/src/tox/tox_env/runner.py b/src/tox/tox_env/runner.py index 552b0420..fdeacd24 100644 --- a/src/tox/tox_env/runner.py +++ b/src/tox/tox_env/runner.py @@ -13,7 +13,7 @@ from .package import PackageToxEnv class RunToxEnv(ToxEnv, ABC): def __init__(self, conf: ConfigSet, core: ConfigSet, options, execute: Execute): super().__init__(conf, core, options, execute) - self.package_env = None # type: Optional[PackageToxEnv] + self.package_env: Optional[PackageToxEnv] = None def register_config(self): super().register_config() diff --git a/src/tox/util/__init__.py b/src/tox/util/__init__.py index 731b7ecb..e69de29b 100644 --- a/src/tox/util/__init__.py +++ b/src/tox/util/__init__.py @@ -1,16 +0,0 @@ -import os -from contextlib import contextmanager - - -@contextmanager -def set_os_env_var(env_var_name, value): - """Set an environment variable with unrolling once the context exists""" - prev_value = os.environ.get(env_var_name) - try: - os.environ[env_var_name] = str(value) - yield - finally: - if prev_value is None: - del os.environ[env_var_name] - else: - os.environ[env_var_name] = prev_value diff --git a/src/tox/util/cpu.py b/src/tox/util/cpu.py index a29a220e..ac7e5af7 100644 --- a/src/tox/util/cpu.py +++ b/src/tox/util/cpu.py @@ -1,3 +1,4 @@ +"""Helper methods related to the CPU""" import os diff --git a/src/tox/util/graph.py b/src/tox/util/graph.py index 1442f76a..8450f909 100644 --- a/src/tox/util/graph.py +++ b/src/tox/util/graph.py @@ -1,3 +1,4 @@ +"""Helper methods related to graph theory.""" from collections import OrderedDict, defaultdict diff --git a/src/tox/util/lock.py b/src/tox/util/lock.py deleted file mode 100644 index 7908f424..00000000 --- a/src/tox/util/lock.py +++ /dev/null @@ -1,38 +0,0 @@ -"""holds locking functionality that works across processes""" - -import logging -from contextlib import contextmanager - -import py -from filelock import FileLock, Timeout - - -@contextmanager -def hold_lock(lock_file): - py.path.local(lock_file.dirname).ensure(dir=1) - lock = FileLock(str(lock_file)) - try: - try: - lock.acquire(0.0001) - except Timeout: - logging.warning(f"lock file {lock_file} present, will block until released") - lock.acquire() - yield - finally: - lock.release(force=True) - - -def get_unique_file(path, prefix, suffix): - """get a unique file in a folder having a given prefix and suffix, with unique number in between""" - lock_file = path.join(".lock") - prefix = f"{prefix}-" - with hold_lock(lock_file): - max_value = -1 - for candidate in path.listdir(f"{prefix}*{suffix}"): - try: - max_value = max(max_value, int(candidate.basename[len(prefix) : -len(suffix)])) - except ValueError: - continue - winner = path.join(f"{prefix}{max_value + 1}{suffix}") - winner.ensure(dir=0) - return winner diff --git a/src/tox/util/path.py b/src/tox/util/path.py deleted file mode 100644 index 66573744..00000000 --- a/src/tox/util/path.py +++ /dev/null @@ -1,9 +0,0 @@ -import logging -import shutil - - -def ensure_empty_dir(path): - if path.check(): - logging.warning(f" removing {path}") - shutil.rmtree(str(path), ignore_errors=True) - path.ensure(dir=1) diff --git a/tests/unit/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py b/tests/unit/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py index d59bf481..d73a3344 100644 --- a/tests/unit/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py +++ b/tests/unit/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py @@ -10,7 +10,9 @@ from tox.execute.api import Outcome from tox.execute.request import ExecuteRequest from tox.pytest import ToxProjectCreator from tox.tox_env.python.virtual_env.api import VirtualEnv -from tox.tox_env.python.virtual_env.package.artifact.wheel import Pep517VirtualEnvPackageWheel +from tox.tox_env.python.virtual_env.package.artifact.wheel import ( + Pep517VirtualEnvPackageWheel, +) @pytest.fixture() @@ -1,48 +1,64 @@ [tox] envlist = + fix + py39 py38 py37 py36 pypy3 cov - fix_lint - pkg_meta docs -minversion = 3.14.0 + pkg_meta +isolated_build = true skip_missing_interpreters = true -isolated_build = True +minversion = 3.14.0 [testenv] description = run the tests with pytest +passenv = + PYTEST_* + SSL_CERT_FILE setenv = COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} COVERAGE_PROCESS_START = {toxinidir}/.coveragerc _COVERAGE_SRC = {envsitepackagesdir}/tox -passenv = - SSL_CERT_FILE - PYTEST_* -extras = testing +extras = + testing commands = coverage erase coverage run -m pytest \ - --junitxml {toxworkdir}/junit.{envname}.xml \ - tests {posargs:--timeout 30 --durations 5} - + --junitxml {toxworkdir}/junit.{envname}.xml \ + tests {posargs:--timeout 30 --durations 5} coverage combine coverage report --skip-covered --show-missing coverage xml -o {toxworkdir}/coverage.{envname}.xml coverage html -d {envtmpdir}/htmlcov +[testenv:fix] +description = format the code base to adhere to our styles, and complain about what we cannot do automatically +passenv = + {[testenv]passenv} + PROGRAMDATA +basepython = python3.8 +skip_install = true +deps = + pre-commit>=2.2 +commands = + pre-commit run --all-files --show-diff-on-failure {posargs} + python -c 'import pathlib; print("hint: run {} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' + [testenv:cov] description = merge existing coverage reports and report coverage diff against DIFF_MASTER env-var (default origin/master) -deps = - coverage>=5 - diff_cover>=3 -skip_install = true passenv = {[testenv]passenv} DIFF_AGAINST -setenv = COVERAGE_FILE={toxworkdir}/.coverage +setenv = + COVERAGE_FILE = {toxworkdir}/.coverage +skip_install = true +deps = + coverage>=5 + diff_cover>=3 +parallel_show_output = true commands = coverage combine coverage report -m @@ -50,35 +66,44 @@ commands = coverage html -d {toxworkdir}/htmlcov diff-cover --compare-branch {env:DIFF_AGAINST:origin/rewrite} {toxworkdir}/coverage.xml depends = + py39 py38 py37 py36 pypy3 -parallel_show_output = True + +[testenv:docs] +description = build documentation +basepython = python3.8 +extras = + docs +commands = + python -c 'import glob; import subprocess; subprocess.call(["proselint"] + glob.glob("docs/*.rst") + glob.glob("docs/**/*.rst"))' + sphinx-build -d "{envtmpdir}/doctree" docs "{toxworkdir}/docs_out" --color -b html {posargs} + python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' [testenv:pkg_meta] description = check that the long description is valid basepython = python3.8 -deps = - twine >= 3 - pep517 >= 0.8.1 skip_install = true +deps = + build>=0.0.4 + twine>=3 commands = - python -m pep517.build -o {envtmpdir} -s -b . + python -m build -o {envtmpdir} -s -w . twine check {envtmpdir}/* -[testenv:fix_lint] -description = format the code base to adhere to our styles, and complain about what we cannot do automatically -basepython = python3.8 -passenv = - {[testenv]passenv} - PROGRAMDATA +[testenv:dev] +description = dev environment with all deps at {envdir} +usedevelop = true deps = - pre-commit>=2.2.0 -skip_install = true + setuptools_scm>=3 +extras = + docs + testing commands = - pre-commit run --all-files --show-diff-on-failure {posargs} - python -c 'import pathlib; print("hint: run {} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' + python -m pip list --format=columns + python -c "print(r'{envpython}')" [flake8] max-complexity = 22 @@ -87,37 +112,3 @@ ignore = E203, W503, C901, E402, B011 [pep8] max-line-length = 120 - -[pytest] -testpaths = tests -junit_family = xunit2 -addopts = --tb=auto -ra --showlocals - -[isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -line_length = 99 -known_first_party = tox,tests -known_third_party = _overlapped,_pytest,appdirs,colorama,filelock,packaging,pluggy,psutil,py,pytest,setuptools,sphinx_rtd_theme,toml,virtualenv,wheel - -[testenv:dev] -description = dev environment with all deps at {envdir} -deps = - setuptools_scm >= 3 -extras = - testing - docs -usedevelop = True -commands = - python -m pip list --format=columns - python -c "print(r'{envpython}')" - -[testenv:docs] -basepython = python3.8 -description = build documentation -extras = docs -commands = - python -c 'import glob; import subprocess; subprocess.call(["proselint"] + glob.glob("docs/*.rst") + glob.glob("docs/**/*.rst"))' - sphinx-build -d "{envtmpdir}/doctree" docs "{toxworkdir}/docs_out" --color -b html {posargs} - python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' |