diff --git a/src/ape/pytest/config.py b/src/ape/pytest/config.py index 77825f338f..a60cf15fd9 100644 --- a/src/ape/pytest/config.py +++ b/src/ape/pytest/config.py @@ -1,14 +1,17 @@ from functools import cached_property from typing import TYPE_CHECKING, Any, Optional, Union -from ape.types.trace import ContractFunctionPath from ape.utils.basemodel import ManagerAccessMixin if TYPE_CHECKING: from _pytest.config import Config as PytestConfig + from ape.types.trace import ContractFunctionPath + + +def _get_config_exclusions(config) -> list["ContractFunctionPath"]: + from ape.types.trace import ContractFunctionPath -def _get_config_exclusions(config) -> list[ContractFunctionPath]: return [ ContractFunctionPath(contract_name=x.contract_name, method_name=x.method_name) for x in config.exclude @@ -74,10 +77,12 @@ def show_internal(self) -> bool: return self.pytest_config.getoption("--show-internal") @cached_property - def gas_exclusions(self) -> list[ContractFunctionPath]: + def gas_exclusions(self) -> list["ContractFunctionPath"]: """ The combination of both CLI values and config values. """ + from ape.types.trace import ContractFunctionPath + cli_value = self.pytest_config.getoption("--gas-exclude") exclusions = ( [ContractFunctionPath.from_str(item) for item in cli_value.split(",")] @@ -89,7 +94,7 @@ def gas_exclusions(self) -> list[ContractFunctionPath]: return exclusions @cached_property - def coverage_exclusions(self) -> list[ContractFunctionPath]: + def coverage_exclusions(self) -> list["ContractFunctionPath"]: return _get_config_exclusions(self.ape_test_config.coverage) def get_pytest_plugin(self, name: str) -> Optional[Any]: diff --git a/src/ape/pytest/contextmanagers.py b/src/ape/pytest/contextmanagers.py index 652ce4ba92..905d1baebb 100644 --- a/src/ape/pytest/contextmanagers.py +++ b/src/ape/pytest/contextmanagers.py @@ -4,7 +4,6 @@ from ethpm_types.abi import ErrorABI -from ape.contracts import ContractInstance from ape.exceptions import ContractLogicError, CustomError, TransactionError from ape.utils.basemodel import ManagerAccessMixin @@ -105,6 +104,9 @@ def _check_expected_message(self, exception: ContractLogicError): raise AssertionError(f"{assertion_error_prefix} but got '{actual}'.") def _check_custom_error(self, exception: Union[CustomError]): + # perf: avoid loading from contracts namespace until needed. + from ape.contracts import ContractInstance + expected_error_cls = self.expected_message if not isinstance(expected_error_cls, ErrorABI) and not isinstance( diff --git a/src/ape/pytest/coverage.py b/src/ape/pytest/coverage.py index 784a025f06..768c385492 100644 --- a/src/ape/pytest/coverage.py +++ b/src/ape/pytest/coverage.py @@ -5,7 +5,6 @@ import click from ape.logging import logger -from ape.types.coverage import CoverageProject, CoverageReport from ape.utils.basemodel import ManagerAccessMixin from ape.utils.misc import get_current_timestamp_ms from ape.utils.os import get_full_extension, get_relative_path @@ -17,6 +16,7 @@ from ape.managers.project import ProjectManager from ape.pytest.config import ConfigWrapper + from ape.types.coverage import CoverageReport from ape.types.trace import ContractFunctionPath, ControlFlow, SourceTraceback @@ -30,7 +30,7 @@ def __init__( self._sources: Union[ Iterable["ContractSource"], Callable[[], Iterable["ContractSource"]] ] = sources - self._report: Optional[CoverageReport] = None + self._report: Optional["CoverageReport"] = None @property def sources(self) -> list["ContractSource"]: @@ -45,7 +45,7 @@ def sources(self) -> list["ContractSource"]: return self._sources @property - def report(self) -> CoverageReport: + def report(self) -> "CoverageReport": if self._report is None: self._report = self._init_coverage_profile() @@ -57,7 +57,9 @@ def reset(self): def _init_coverage_profile( self, - ) -> CoverageReport: + ) -> "CoverageReport": + from ape.types.coverage import CoverageProject, CoverageReport + # source_id -> pc(s) -> times hit project_coverage = CoverageProject(name=self.project.name or "__local__") @@ -161,7 +163,7 @@ def __init__( @property def data(self) -> Optional[CoverageData]: - if not self.config_wrapper.track_coverage: + if not self.enabled: return None elif self._data is None: diff --git a/src/ape/pytest/gas.py b/src/ape/pytest/gas.py index 1f37af2b68..c9739717b3 100644 --- a/src/ape/pytest/gas.py +++ b/src/ape/pytest/gas.py @@ -2,7 +2,6 @@ from evm_trace.gas import merge_reports -from ape.types.trace import GasReport from ape.utils.basemodel import ManagerAccessMixin from ape.utils.trace import _exclude_gas, parse_gas_table @@ -13,7 +12,7 @@ from ape.api.trace import TraceAPI from ape.pytest.config import ConfigWrapper from ape.types.address import AddressType - from ape.types.trace import ContractFunctionPath + from ape.types.trace import ContractFunctionPath, GasReport class GasTracker(ManagerAccessMixin): @@ -24,7 +23,7 @@ class GasTracker(ManagerAccessMixin): def __init__(self, config_wrapper: "ConfigWrapper"): self.config_wrapper = config_wrapper - self.session_gas_report: Optional[GasReport] = None + self.session_gas_report: Optional["GasReport"] = None @property def enabled(self) -> bool: diff --git a/src/ape/pytest/plugin.py b/src/ape/pytest/plugin.py index 72d09c1809..e23dd5bf32 100644 --- a/src/ape/pytest/plugin.py +++ b/src/ape/pytest/plugin.py @@ -1,24 +1,7 @@ import sys from pathlib import Path -from typing import TYPE_CHECKING, Optional from ape.exceptions import ConfigError -from ape.pytest.config import ConfigWrapper -from ape.pytest.coverage import CoverageTracker -from ape.pytest.fixtures import PytestApeFixtures, ReceiptCapture -from ape.pytest.gas import GasTracker -from ape.pytest.runners import PytestApeRunner -from ape.utils.basemodel import ManagerAccessMixin - -if TYPE_CHECKING: - from ape.api.networks import EcosystemAPI - - -def _get_default_network(ecosystem: Optional["EcosystemAPI"] = None) -> str: - if ecosystem is None: - ecosystem = ManagerAccessMixin.network_manager.default_ecosystem - - return ecosystem.name def pytest_addoption(parser): @@ -40,7 +23,6 @@ def add_option(*names, **kwargs): add_option( "--network", action="store", - default=_get_default_network(), help="Override the default network and provider (see ``ape networks list`` for options).", ) add_option( @@ -64,7 +46,7 @@ def add_option(*names, **kwargs): action="store", help="A comma-separated list of contract:method-name glob-patterns to ignore.", ) - parser.addoption("--coverage", action="store_true", help="Collect contract coverage.") + add_option("--coverage", action="store_true", help="Collect contract coverage.") # NOTE: Other pytest plugins, such as hypothesis, should integrate with pytest separately @@ -86,17 +68,27 @@ def is_module(v): except AttributeError: pass - config_wrapper = ConfigWrapper(config) - receipt_capture = ReceiptCapture(config_wrapper) - gas_tracker = GasTracker(config_wrapper) - coverage_tracker = CoverageTracker(config_wrapper) - if not config.option.verbose: # Enable verbose output if stdout capture is disabled config.option.verbose = config.getoption("capture") == "no" # else: user has already changes verbosity to an equal or higher level; avoid downgrading. + if "--help" in config.invocation_params.args: + # perf: Don't bother setting up runner if only showing help. + return + + from ape.pytest.config import ConfigWrapper + from ape.pytest.coverage import CoverageTracker + from ape.pytest.fixtures import PytestApeFixtures, ReceiptCapture + from ape.pytest.gas import GasTracker + from ape.pytest.runners import PytestApeRunner + from ape.utils.basemodel import ManagerAccessMixin + # Register the custom Ape test runner + config_wrapper = ConfigWrapper(config) + receipt_capture = ReceiptCapture(config_wrapper) + gas_tracker = GasTracker(config_wrapper) + coverage_tracker = CoverageTracker(config_wrapper) runner = PytestApeRunner(config_wrapper, receipt_capture, gas_tracker, coverage_tracker) config.pluginmanager.register(runner, "ape-test") diff --git a/src/ape/pytest/runners.py b/src/ape/pytest/runners.py index 6ddac468d8..a6528c53cb 100644 --- a/src/ape/pytest/runners.py +++ b/src/ape/pytest/runners.py @@ -9,7 +9,6 @@ from ape.exceptions import ConfigError from ape.logging import LogLevel from ape.utils.basemodel import ManagerAccessMixin -from ape_console._cli import console if TYPE_CHECKING: from ape.api.networks import ProviderContextManager @@ -90,6 +89,8 @@ def pytest_exception_interact(self, report, call): ) if self.config_wrapper.interactive and report.failed: + from ape_console._cli import console + traceback = call.excinfo.traceback[-1] # Suspend capsys to ignore our own output. @@ -124,6 +125,7 @@ def pytest_exception_interact(self, report, call): click.echo("Starting interactive mode. Type `exit` to halt current test.") namespace = {"_callinfo": call, **globals_dict, **locals_dict} + console(extra_locals=namespace, project=self.local_project, embed=True) if capman: