diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29f1701b43..b9bbcebef7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml @@ -10,7 +10,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black name: black @@ -21,7 +21,7 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.1 hooks: - id: mypy additional_dependencies: [ diff --git a/setup.py b/setup.py index 1dd14c7d14..4c54c5f549 100644 --- a/setup.py +++ b/setup.py @@ -18,8 +18,8 @@ "hypothesis-jsonschema==0.19.0", # JSON Schema fuzzer extension ], "lint": [ - "black>=23.9.1,<24", # Auto-formatter and linter - "mypy>=1.5.1,<2", # Static type analyzer + "black>=23.10.1,<24", # Auto-formatter and linter + "mypy>=1.6.1,<2", # Static type analyzer "types-PyYAML", # Needed due to mypy typeshed "types-requests", # Needed due to mypy typeshed "types-setuptools", # Needed due to mypy typeshed diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index a0840adc26..a0a6fbe2b1 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -1,7 +1,18 @@ from functools import partial from pathlib import Path from tempfile import mkdtemp -from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Collection, + Dict, + Iterator, + List, + Optional, + Tuple, + Type, + Union, +) from eth_account import Account as EthAccount from eth_account._utils.legacy_transactions import ( @@ -430,7 +441,9 @@ def get_network(self, network_name: str) -> "NetworkAPI": raise NetworkNotFoundError(network_name, ecosystem=self.name, options=self.networks) - def get_network_data(self, network_name: str) -> Dict: + def get_network_data( + self, network_name: str, provider_filter: Optional[Collection[str]] = None + ) -> Dict: """ Get a dictionary of data about providers in the network. @@ -439,6 +452,8 @@ def get_network_data(self, network_name: str) -> Dict: Args: network_name (str): The name of the network to get provider data from. + provider_filter (Optional[Collection[str]]): Optionally filter the providers + by name. Returns: dict: A dictionary containing the providers in a network. @@ -456,6 +471,9 @@ def get_network_data(self, network_name: str) -> Dict: data["explorer"] = str(network.explorer.name) for provider_name in network.providers: + if provider_filter and provider_name not in provider_filter: + continue + provider_data: Dict = {"name": str(provider_name)} # Only add isDefault key when True @@ -573,22 +591,22 @@ def push_provider(self): if must_connect: self._provider.connect() - provider_id = self.get_provider_id(self._provider) - if provider_id is None: + connection_id = self._provider.connection_id + if connection_id is None: raise ProviderNotConnectedError() - self.provider_stack.append(provider_id) - self.disconnect_map[provider_id] = self._disconnect_after - if provider_id in self.connected_providers: + self.provider_stack.append(connection_id) + self.disconnect_map[connection_id] = self._disconnect_after + if connection_id in self.connected_providers: # Using already connected instance if must_connect: # Disconnect if had to connect to check chain ID self._provider.disconnect() - self._provider = self.connected_providers[provider_id] + self._provider = self.connected_providers[connection_id] else: # Adding provider for the first time. Retain connection. - self.connected_providers[provider_id] = self._provider + self.connected_providers[connection_id] = self._provider self.network_manager.active_provider = self._provider return self._provider @@ -598,24 +616,28 @@ def pop_provider(self): return # Clear last provider - exiting_provider_id = self.provider_stack.pop() + current_id = self.provider_stack.pop() # Disconnect the provider in same cases. - if self.disconnect_map[exiting_provider_id]: + if self.disconnect_map[current_id]: if provider := self.network_manager.active_provider: provider.disconnect() + del self.disconnect_map[current_id] + if current_id in self.connected_providers: + del self.connected_providers[current_id] + if not self.provider_stack: self.network_manager.active_provider = None return # Reset the original active provider - previous_provider_id = self.provider_stack[-1] - if previous_provider_id == exiting_provider_id: + prior_id = self.provider_stack[-1] + if prior_id == current_id: # Active provider is not changing return - if previous_provider := self.connected_providers[previous_provider_id]: + if previous_provider := self.connected_providers[prior_id]: self.network_manager.active_provider = previous_provider def disconnect_all(self): @@ -628,13 +650,6 @@ def disconnect_all(self): self.network_manager.active_provider = None self.connected_providers = {} - @classmethod - def get_provider_id(cls, provider: "ProviderAPI") -> Optional[str]: - if not provider.is_connected: - return None - - return f"{provider.network_choice}:-{provider.chain_id}" - class NetworkAPI(BaseInterfaceModel): """ @@ -865,14 +880,9 @@ def get_provider( if provider_name in self.providers: provider = self.providers[provider_name](provider_settings=provider_settings) - - provider_id = ProviderContextManager.get_provider_id(provider) - if not provider_id: - # Provider not yet connected - return provider - - if provider_id in ProviderContextManager.connected_providers: - return ProviderContextManager.connected_providers[provider_id] + if provider.connection_id in ProviderContextManager.connected_providers: + # Likely multi-chain testing or utilizing multiple on-going connections. + return ProviderContextManager.connected_providers[provider.connection_id] return provider diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index b3399c0230..b93ad5d440 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -128,7 +128,7 @@ class ProviderAPI(BaseInterfaceModel): network: NetworkAPI """A reference to the network this provider provides.""" - provider_settings: dict + provider_settings: Dict = {} """The settings for the provider, as overrides to the configuration.""" data_folder: Path @@ -184,6 +184,37 @@ def ws_uri(self) -> Optional[str]: """ return None + @property + def settings(self) -> PluginConfig: + """ + The combination of settings from ``ape-config.yaml`` and ``.provider_settings``. + """ + CustomConfig = self.config.__class__ + data = {**self.config.dict(), **self.provider_settings} + return CustomConfig.parse_obj(data) + + @property + def connection_id(self) -> Optional[str]: + """ + A connection ID to uniquely identify and manage multiple + connections to providers, especially when working with multiple + providers of the same type, like multiple Geth --dev nodes. + """ + + try: + chain_id = self.chain_id + except Exception: + if chain_id := self.settings.get("chain_id"): + pass + + else: + # A connection is required to obtain a chain ID for this provider. + return None + + # NOTE: If other provider settings are different, ``.update_settings()`` + # should be called. + return f"{self.network_choice}:{chain_id}" + @abstractmethod def update_settings(self, new_settings: dict): """ @@ -1536,7 +1567,8 @@ def _create_trace_frame(self, evm_frame: EvmTraceFrame) -> TraceFrame: raw=evm_frame.dict(), ) - def _make_request(self, endpoint: str, parameters: List) -> Any: + def _make_request(self, endpoint: str, parameters: Optional[List] = None) -> Any: + parameters = parameters or [] coroutine = self.web3.provider.make_request(RPCEndpoint(endpoint), parameters) result = run_until_complete(coroutine) @@ -1712,6 +1744,11 @@ def _stdout_logger(self) -> Logger: def _stderr_logger(self) -> Logger: return self._get_process_output_logger("stderr", self.stderr_logs_path) + @property + def connection_id(self) -> Optional[str]: + cmd_id = ",".join(self.build_command()) + return f"{self.network_choice}:{cmd_id}" + def _get_process_output_logger(self, name: str, path: Path): logger = getLogger(f"{self.name}_{name}_subprocessProviderLogger") path.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/ape/managers/networks.py b/src/ape/managers/networks.py index 131d4c4dfd..0458822447 100644 --- a/src/ape/managers/networks.py +++ b/src/ape/managers/networks.py @@ -1,6 +1,6 @@ import json from functools import cached_property -from typing import Dict, Iterator, List, Optional, Set, Union +from typing import Collection, Dict, Iterator, List, Optional, Set, Union import yaml @@ -501,15 +501,33 @@ def network_data(self) -> Dict: Returns: dict """ + return self.get_network_data() + + def get_network_data( + self, + ecosystem_filter: Optional[Collection[str]] = None, + network_filter: Optional[Collection[str]] = None, + provider_filter: Optional[Collection[str]] = None, + ): data: Dict = {"ecosystems": []} for ecosystem_name in self: - ecosystem_data = self._get_ecosystem_data(ecosystem_name) + if ecosystem_filter and ecosystem_name not in ecosystem_filter: + continue + + ecosystem_data = self._get_ecosystem_data( + ecosystem_name, network_filter=network_filter, provider_filter=provider_filter + ) data["ecosystems"].append(ecosystem_data) return data - def _get_ecosystem_data(self, ecosystem_name: str) -> Dict: + def _get_ecosystem_data( + self, + ecosystem_name: str, + network_filter: Optional[Collection[str]] = None, + provider_filter: Optional[Collection[str]] = None, + ) -> Dict: ecosystem = self[ecosystem_name] ecosystem_data: Dict = {"name": str(ecosystem_name)} @@ -520,16 +538,21 @@ def _get_ecosystem_data(self, ecosystem_name: str) -> Dict: ecosystem_data["networks"] = [] for network_name in getattr(self, ecosystem_name).networks: - network_data = ecosystem.get_network_data(network_name) + if network_filter and network_name not in network_filter: + continue + + network_data = ecosystem.get_network_data(network_name, provider_filter=provider_filter) ecosystem_data["networks"].append(network_data) return ecosystem_data @property + # TODO: Remove in 0.7 def networks_yaml(self) -> str: """ Get a ``yaml`` ``str`` representing all the networks in all the ecosystems. + **NOTE**: Deprecated. View the result via CLI command ``ape networks list --format yaml``. diff --git a/src/ape/utils/basemodel.py b/src/ape/utils/basemodel.py index 361a9a0786..dfd32ecdbb 100644 --- a/src/ape/utils/basemodel.py +++ b/src/ape/utils/basemodel.py @@ -213,10 +213,10 @@ def __getattr__(self, name: str) -> Any: # The error message mentions the alternative mappings, # such as a contract-type map. - message = f"No attribute with name '{name}'." + message = f"'{repr(self)}' has no attribute '{name}'" if extras_checked: extras_str = ", ".join(extras_checked) - message = f"{message} Also checked '{extras_str}'." + message = f"{message}. Also checked '{extras_str}'" raise ApeAttributeError(message) diff --git a/src/ape_geth/provider.py b/src/ape_geth/provider.py index 872a85a879..8c371ac59d 100644 --- a/src/ape_geth/provider.py +++ b/src/ape_geth/provider.py @@ -7,7 +7,7 @@ from itertools import tee from pathlib import Path from subprocess import DEVNULL, PIPE, Popen -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union import ijson # type: ignore import requests @@ -267,7 +267,7 @@ def uri(self) -> str: # Use adhoc, scripted value return self.provider_settings["uri"] - config = self.config.dict().get(self.network.ecosystem.name, None) + config = self.settings.dict().get(self.network.ecosystem.name, None) if config is None: return DEFAULT_SETTINGS["uri"] @@ -275,10 +275,6 @@ def uri(self) -> str: network_config = config.get(self.network.name) or DEFAULT_SETTINGS return network_config.get("uri", DEFAULT_SETTINGS["uri"]) - @property - def geth_config(self) -> GethConfig: - return cast(GethConfig, self.config_manager.get_config("geth")) - @property def _clean_uri(self) -> str: url = URL(self.uri).with_user(None).with_password(None) @@ -288,12 +284,12 @@ def _clean_uri(self) -> str: @property def ipc_path(self) -> Path: - return self.geth_config.ipc_path or self.data_dir / "geth.ipc" + return self.settings.ipc_path or self.data_dir / "geth.ipc" @property def data_dir(self) -> Path: - if self.geth_config.data_dir: - return self.geth_config.data_dir.expanduser() + if self.settings.data_dir: + return self.settings.data_dir.expanduser() return _get_default_data_dir() @@ -481,12 +477,12 @@ def process_name(self) -> str: @property def chain_id(self) -> int: - return self.geth_config.ethereum.local.get("chain_id", DEFAULT_TEST_CHAIN_ID) + return self.settings.ethereum.local.get("chain_id", DEFAULT_TEST_CHAIN_ID) @property def data_dir(self) -> Path: # Overridden from BaseGeth class for placing debug logs in ape data folder. - return self.geth_config.data_dir or self.data_folder / self.name + return self.settings.data_dir or self.data_folder / self.name def __repr__(self): try: @@ -505,8 +501,8 @@ def start(self, timeout: int = 20): test_config = self.config_manager.get_config("test").dict() # Allow configuring a custom executable besides your $PATH geth. - if self.geth_config.executable is not None: - test_config["executable"] = self.geth_config.executable + if self.settings.executable is not None: + test_config["executable"] = self.settings.executable test_config["ipc_path"] = self.ipc_path test_config["auto_disconnect"] = self._test_runner is None or test_config.get( @@ -514,7 +510,7 @@ def start(self, timeout: int = 20): ) # Include extra accounts to allocated funds to at genesis. - extra_accounts = self.geth_config.ethereum.local.get("extra_funded_accounts", []) + extra_accounts = self.settings.ethereum.local.get("extra_funded_accounts", []) extra_accounts.extend(self.provider_settings.get("extra_funded_accounts", [])) extra_accounts = list(set([HexBytes(a).hex().lower() for a in extra_accounts])) test_config["extra_funded_accounts"] = extra_accounts diff --git a/src/ape_networks/_cli.py b/src/ape_networks/_cli.py index 9acb336172..9606b5d030 100644 --- a/src/ape_networks/_cli.py +++ b/src/ape_networks/_cli.py @@ -1,6 +1,8 @@ +import json from typing import Callable, Dict import click +import yaml from rich import print as echo_rich_text from rich.tree import Tree @@ -9,6 +11,7 @@ from ape.cli import ape_cli_context, network_option from ape.cli.choices import OutputFormat from ape.cli.options import output_format_option +from ape.exceptions import NetworkError from ape.logging import LogLevel from ape.types import _LazySequence @@ -45,10 +48,16 @@ def gen(): @_filter_option("network", _lazy_get("network")) @_filter_option("provider", _lazy_get("provider")) def _list(cli_ctx, output_format, ecosystem_filter, network_filter, provider_filter): + network_data = cli_ctx.network_manager.get_network_data( + ecosystem_filter=ecosystem_filter, + network_filter=network_filter, + provider_filter=provider_filter, + ) + if output_format == OutputFormat.TREE: default_suffix = "[dim default] (default)" - ecosystems = {e["name"]: e for e in cli_ctx.network_manager.network_data["ecosystems"]} - ecosystems = {n: ecosystems[n] for n in sorted(ecosystems)} + ecosystems = network_data["ecosystems"] + ecosystems = sorted(ecosystems, key=lambda e: e["name"]) def make_sub_tree(data: Dict, create_tree: Callable) -> Tree: name = f"[bold green]{data['name']}" @@ -58,24 +67,12 @@ def make_sub_tree(data: Dict, create_tree: Callable) -> Tree: sub_tree = create_tree(name) return sub_tree - for ecosystem_name, ecosystem in ecosystems.items(): - if ecosystem_filter and ecosystem["name"] not in ecosystem_filter: - continue - + for ecosystem in ecosystems: ecosystem_tree = make_sub_tree(ecosystem, Tree) _networks = {n["name"]: n for n in ecosystem["networks"]} _networks = {n: _networks[n] for n in sorted(_networks)} - if network_filter: - _networks = {n: v for n, v in _networks.items() if n in network_filter} - for network_name, network in _networks.items(): - if network_filter and network_name not in network_filter: - continue - providers = network["providers"] - if provider_filter: - providers = [p for p in providers if p["name"] in provider_filter] - if providers: network_tree = make_sub_tree(network, ecosystem_tree.add) providers = sorted(providers, key=lambda p: p["name"]) @@ -86,7 +83,23 @@ def make_sub_tree(data: Dict, create_tree: Callable) -> Tree: echo_rich_text(ecosystem_tree) elif output_format == OutputFormat.YAML: - click.echo(cli_ctx.network_manager.networks_yaml.strip()) + if not isinstance(network_data, dict): + raise TypeError( + f"Unexpected network data type: {type(network_data)}. " + f"Expecting dict. YAML dump will fail." + ) + + try: + click.echo(yaml.dump(network_data, sort_keys=True).strip()) + except ValueError as err: + try: + data_str = json.dumps(network_data) + except Exception: + data_str = str(network_data) + + raise NetworkError( + f"Network data did not dump to YAML: {data_str}\nActual err: {err}" + ) from err @cli.command() diff --git a/src/ape_test/provider.py b/src/ape_test/provider.py index 3cbb5e7db1..a270e52055 100644 --- a/src/ape_test/provider.py +++ b/src/ape_test/provider.py @@ -1,12 +1,13 @@ import re from ast import literal_eval -from typing import Optional, cast +from typing import Dict, Optional, cast from eth.exceptions import HeaderNotFound from eth_tester.backends import PyEVMBackend # type: ignore from eth_tester.exceptions import TransactionFailed # type: ignore from eth_utils import is_0x_prefixed from eth_utils.exceptions import ValidationError +from eth_utils.toolz import merge from ethpm_types import HexBytes from web3 import EthereumTesterProvider, Web3 from web3.exceptions import ContractPanicError @@ -45,19 +46,19 @@ def evm_backend(self) -> PyEVMBackend: return self._evm_backend def connect(self): - chain_id = self.provider_settings.get("chain_id", self.config.provider.chain_id) + chain_id = self.settings.chain_id if self._web3 is not None: - connected_chain_id = self.chain_id + connected_chain_id = self._make_request("eth_chainId") if connected_chain_id == chain_id: # Is already connected and settings have not changed. return self._evm_backend = PyEVMBackend.from_mnemonic( - mnemonic=self.config["mnemonic"], - num_accounts=self.config["number_of_accounts"], + mnemonic=self.config.mnemonic, + num_accounts=self.config.number_of_accounts, ) endpoints = {**API_ENDPOINTS} - endpoints["eth"]["chainId"] = static_return(chain_id) + endpoints["eth"] = merge(endpoints["eth"], {"chainId": static_return(chain_id)}) tester = EthereumTesterProvider(ethereum_tester=self._evm_backend, api_endpoints=endpoints) self._web3 = Web3(tester) @@ -65,9 +66,13 @@ def disconnect(self): self.cached_chain_id = None self._web3 = None self._evm_backend = None + self.provider_settings = {} - def update_settings(self, new_settings: dict): - pass + def update_settings(self, new_settings: Dict): + self.cached_chain_id = None + self.provider_settings = {**self.provider_settings, **new_settings} + self.disconnect() + self.connect() def estimate_gas_cost(self, txn: TransactionAPI, **kwargs) -> int: if isinstance(self.network.gas_limit, int): @@ -80,9 +85,7 @@ def estimate_gas_cost(self, txn: TransactionAPI, **kwargs) -> int: block_id = kwargs.pop("block_identifier", kwargs.pop("block_id", None)) estimate_gas = self.web3.eth.estimate_gas txn_dict = txn.dict() - if txn_dict.get("gas") == "auto": - # Remove from dict before estimating - txn_dict.pop("gas") + txn_dict.pop("gas", None) txn_data = cast(TxParams, txn_dict) try: @@ -108,15 +111,21 @@ def estimate_gas_cost(self, txn: TransactionAPI, **kwargs) -> int: message, base_err=ape_err, txn=txn, source_traceback=ape_err.source_traceback ) from ape_err + @property + def settings(self) -> EthTesterProviderConfig: + return EthTesterProviderConfig.parse_obj( + {**self.config.provider.dict(), **self.provider_settings} + ) + @property def chain_id(self) -> int: if self.cached_chain_id: return self.cached_chain_id try: - result = self._make_request("eth_chainId", []) + result = self._make_request("eth_chainId") except ProviderNotConnectedError: - result = self.provider_settings.get("chain_id", self.config.provider.chain_id) + result = self.settings.chain_id self.cached_chain_id = result return result diff --git a/tests/conftest.py b/tests/conftest.py index b1f3a794ef..a04453a68b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from ape.logging import LogLevel, logger from ape.managers.config import CONFIG_FILE_NAME from ape.types import AddressType -from ape.utils import ZERO_ADDRESS +from ape.utils import DEFAULT_TEST_CHAIN_ID, ZERO_ADDRESS # NOTE: Ensure that we don't use local paths for these DATA_FOLDER = Path(mkdtemp()).resolve() @@ -204,11 +204,11 @@ def ethereum(networks): @pytest.fixture(autouse=True) def eth_tester_provider(ethereum): - if not ape.networks.active_provider or ape.networks.provider.name != "test": - with ethereum.local.use_provider("test") as provider: - yield provider - else: - yield ape.networks.provider + # NOTE: Ensure it uses the default instance of eth-tester. + with ethereum.local.use_provider( + "test", provider_settings={"chain_id": DEFAULT_TEST_CHAIN_ID} + ) as provider: + yield provider @pytest.fixture diff --git a/tests/functional/test_network_manager.py b/tests/functional/test_network_manager.py index b6548e9d6c..f67e11433a 100644 --- a/tests/functional/test_network_manager.py +++ b/tests/functional/test_network_manager.py @@ -43,11 +43,14 @@ def __call__(self, *args, **kwargs) -> int: def get_provider_with_unused_chain_id(networks_connected_to_tester): networks = networks_connected_to_tester - def fn(): + def fn(**more_settings): chain_id = chain_id_factory() - settings = {"chain_id": chain_id} + settings = {"chain_id": chain_id, **more_settings} choice = "ethereum:local:test" - context = networks.parse_network_choice(choice, provider_settings=settings) + disconnect_after = settings.pop("disconnect_after", True) + context = networks.parse_network_choice( + choice, disconnect_after=disconnect_after, provider_settings=settings + ) return context return fn @@ -165,16 +168,16 @@ def test_parse_network_choice_same_provider(chain, networks_connected_to_tester, context = get_context() start_count = len(context.connected_providers) original_block_number = chain.blocks.height - provider_id = id(chain.provider) + object_id = id(chain.provider) with context: - assert id(chain.provider) == provider_id + assert id(chain.provider) == object_id count = len(context.connected_providers) # Does not create a new provider since it is the same chain ID assert count == start_count - assert id(chain.provider) == provider_id + assert id(chain.provider) == object_id assert len(context.connected_providers) == start_count assert chain.blocks.height == original_block_number @@ -182,6 +185,7 @@ def test_parse_network_choice_same_provider(chain, networks_connected_to_tester, assert provider._web3 is not None +@pytest.mark.xdist_group(name="multiple-eth-testers") def test_parse_network_choice_new_chain_id(get_provider_with_unused_chain_id, get_context): start_count = len(get_context().connected_providers) context = get_provider_with_unused_chain_id() @@ -197,8 +201,26 @@ def test_parse_network_choice_new_chain_id(get_provider_with_unused_chain_id, ge assert provider._web3 is not None -def test_parse_network_choice_multiple_contexts(get_provider_with_unused_chain_id): +@pytest.mark.xdist_group(name="multiple-eth-testers") +def test_disconnect_after(get_provider_with_unused_chain_id): + context = get_provider_with_unused_chain_id() + with context as provider: + connection_id = provider.connection_id + assert connection_id in context.connected_providers + + assert connection_id not in context.connected_providers + + +@pytest.mark.xdist_group(name="multiple-eth-testers") +def test_parse_network_choice_multiple_contexts( + eth_tester_provider, get_provider_with_unused_chain_id +): first_context = get_provider_with_unused_chain_id() + assert ( + eth_tester_provider.chain_id == DEFAULT_TEST_CHAIN_ID + ), "Test setup failed - expecting to start on default chain ID" + assert eth_tester_provider._make_request("eth_chainId") == DEFAULT_TEST_CHAIN_ID + with first_context: start_count = len(first_context.connected_providers) expected_next_count = start_count + 1 @@ -208,6 +230,9 @@ def test_parse_network_choice_multiple_contexts(get_provider_with_unused_chain_i assert len(first_context.connected_providers) == expected_next_count assert len(second_context.connected_providers) == expected_next_count + assert eth_tester_provider.chain_id == DEFAULT_TEST_CHAIN_ID + assert eth_tester_provider._make_request("eth_chainId") == DEFAULT_TEST_CHAIN_ID + def test_getattr_ecosystem_with_hyphenated_name(networks, ethereum): networks.ecosystems["hyphen-in-name"] = networks.ecosystems["ethereum"] diff --git a/tests/integration/cli/test_networks.py b/tests/integration/cli/test_networks.py index fd763e1332..966eede1e1 100644 --- a/tests/integration/cli/test_networks.py +++ b/tests/integration/cli/test_networks.py @@ -109,6 +109,14 @@ def test_list_yaml(ape_cli, runner): # Skip these lines in case test-runner has installed providers continue + if ( + expected_line.lstrip().startswith("- name:") + and expected_line not in result.output + and "explorer:" in result.output + ): + # May have explorers installed - ignore that. + expected_line = expected_line.lstrip(" -") + assert expected_line in result.output, result.output diff --git a/tests/integration/cli/utils.py b/tests/integration/cli/utils.py index 61138b2fd6..fe5922e8c1 100644 --- a/tests/integration/cli/utils.py +++ b/tests/integration/cli/utils.py @@ -4,7 +4,9 @@ import pytest __projects_directory__ = Path(__file__).parent / "projects" -__project_names__ = [p.stem for p in __projects_directory__.iterdir() if p.is_dir()] +__project_names__ = [ + p.stem for p in __projects_directory__.iterdir() if p.is_dir() and not p.stem.startswith(".") +] def assert_failure(result, expected_output):