diff --git a/docs/userguides/networks.md b/docs/userguides/networks.md index ae6237b0fb..9e91b89db6 100644 --- a/docs/userguides/networks.md +++ b/docs/userguides/networks.md @@ -1,8 +1,9 @@ # Networks When interacting with a blockchain, you will have to select an ecosystem (e.g. Ethereum, Arbitrum, or Fantom), a network (e.g. Mainnet or Sepolia) and a provider (e.g. Eth-Tester, Node (Geth), or Alchemy). -Networks are part of ecosystems and typically defined in plugins. -For example, the `ape-ethereum` plugin comes with Ape and can be used for handling EVM-like behavior. +The `ape-ethereum` ecosystem and network(s) plugin comes with Ape and can be used for handling EVM-like behavior. +Networks are part of ecosystems and typically defined in plugins or custom-network configurations. +However, Ape works out-of-the-box (in a limited way) with any network defined in the [evmchains](https://github.com/ApeWorX/evmchains) library. ## Selecting a Network @@ -25,7 +26,7 @@ ape test --network ethereum:local:foundry ape console --network arbitrum:testnet:alchemy # NOTICE: All networks, even from other ecosystems, use this. ``` -To see all possible values for `--network`, run the command: +To see all networks that work with the `--network` flag (besides those _only_ defined in `evmchains`), run the command: ```shell ape networks list @@ -100,6 +101,20 @@ ape networks list In the remainder of this guide, any example below using Ethereum, you can replace with an L2 ecosystem's name and network combination. +## evmchains Networks + +If a network is in the [evmchains](https://github.com/ApeWorX/evmchains) library, it will work in Ape automatically, even without a plugin or any custom configuration for that network. + +```shell +ape console --network moonbeam +``` + +This works because the `moonbeam` network data is available in the `evmchains` library, and Ape is able to look it up. + +```{warning} +Support for networks from evm-chains alone may be limited and require additional configuration to work in production use-cases. +``` + ## Custom Network Connection You can add custom networks to Ape without creating a plugin. diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 5f965428f1..07c249135c 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -12,6 +12,7 @@ ) from eth_pydantic_types import HexBytes from eth_utils import keccak, to_int +from evmchains import PUBLIC_CHAIN_META from pydantic import model_validator from ape.exceptions import ( @@ -109,7 +110,7 @@ def data_folder(self) -> Path: """ return self.config_manager.DATA_FOLDER / self.name - @cached_property + @property def custom_network(self) -> "NetworkAPI": """ A :class:`~ape.api.networks.NetworkAPI` for custom networks where the @@ -125,13 +126,11 @@ def custom_network(self) -> "NetworkAPI": if ethereum_class is None: raise NetworkError("Core Ethereum plugin missing.") - request_header = self.config_manager.REQUEST_HEADER - init_kwargs = {"name": "ethereum", "request_header": request_header} - ethereum = ethereum_class(**init_kwargs) # type: ignore + init_kwargs = {"name": "ethereum"} + evm_ecosystem = ethereum_class(**init_kwargs) # type: ignore return NetworkAPI( name="custom", - ecosystem=ethereum, - request_header=request_header, + ecosystem=evm_ecosystem, _default_provider="node", _is_custom=True, ) @@ -301,6 +300,11 @@ def networks(self) -> dict[str, "NetworkAPI"]: network_api._is_custom = True networks[net_name] = network_api + # Add any remaining networks from EVM chains here (but don't override). + # NOTE: Only applicable to EVM-based ecosystems, of course. + # Otherwise, this is a no-op. + networks = {**self._networks_from_evmchains, **networks} + return networks @cached_property @@ -311,6 +315,17 @@ def _networks_from_plugins(self) -> dict[str, "NetworkAPI"]: if ecosystem_name == self.name } + @cached_property + def _networks_from_evmchains(self) -> dict[str, "NetworkAPI"]: + # NOTE: Purposely exclude plugins here so we also prefer plugins. + return { + network_name: create_network_type(data["chainId"], data["chainId"])( + name=network_name, ecosystem=self + ) + for network_name, data in PUBLIC_CHAIN_META.get(self.name, {}).items() + if network_name not in self._networks_from_plugins + } + def __post_init__(self): if len(self.networks) == 0: raise NetworkError("Must define at least one network in ecosystem") @@ -1057,7 +1072,6 @@ def providers(self): # -> dict[str, Partial[ProviderAPI]] Returns: dict[str, partial[:class:`~ape.api.providers.ProviderAPI`]] """ - from ape.plugins._utils import clean_plugin_name providers = {} @@ -1089,6 +1103,12 @@ def providers(self): # -> dict[str, Partial[ProviderAPI]] network=self, ) + # Any EVM-chain works with node provider. + if "node" not in providers and self.name in self.ecosystem._networks_from_evmchains: + # NOTE: Arbitrarily using sepolia to access the Node class. + node_provider_cls = self.network_manager.ethereum.sepolia.get_provider("node").__class__ + providers["node"] = partial(node_provider_cls, name="node", network=self) + return providers def _get_plugin_providers(self): diff --git a/src/ape/contracts/__init__.py b/src/ape/contracts/__init__.py index 8ebd5e04f2..535514e7af 100644 --- a/src/ape/contracts/__init__.py +++ b/src/ape/contracts/__init__.py @@ -1,4 +1,8 @@ -from .base import ContractContainer, ContractEvent, ContractInstance, ContractLog, ContractNamespace +def __getattr__(name: str): + import ape.contracts.base as module + + return getattr(module, name) + __all__ = [ "ContractContainer", diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 9c73fb8158..3c6be64c0b 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -7,14 +7,10 @@ from typing import TYPE_CHECKING, Any, Optional, Union import click -import pandas as pd from eth_pydantic_types import HexBytes from eth_utils import to_hex -from ethpm_types.abi import EventABI, MethodABI -from ethpm_types.contract_type import ABI_W_SELECTOR_T, ContractType -from IPython.lib.pretty import for_type +from ethpm_types.abi import EventABI -from ape.api.accounts import AccountAPI from ape.api.address import Address, BaseAddress from ape.api.query import ( ContractCreation, @@ -34,7 +30,6 @@ MissingDeploymentBytecodeError, ) from ape.logging import get_rich_console, logger -from ape.types.address import AddressType from ape.types.events import ContractLog, LogFilter, MockContractLog from ape.utils.abi import StructParser, _enrich_natspec from ape.utils.basemodel import ( @@ -49,9 +44,12 @@ from ape.utils.misc import log_instead_of_fail if TYPE_CHECKING: - from ethpm_types.abi import ConstructorABI, ErrorABI + from ethpm_types.abi import ConstructorABI, ErrorABI, MethodABI + from ethpm_types.contract_type import ABI_W_SELECTOR_T, ContractType + from pandas import DataFrame from ape.api.transactions import ReceiptAPI, TransactionAPI + from ape.types.address import AddressType class ContractConstructor(ManagerAccessMixin): @@ -90,7 +88,7 @@ def serialize_transaction(self, *args, **kwargs) -> "TransactionAPI": def __call__(self, private: bool = False, *args, **kwargs) -> "ReceiptAPI": txn = self.serialize_transaction(*args, **kwargs) - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): sender = kwargs["sender"] return sender.call(txn, **kwargs) elif "sender" not in kwargs and self.account_manager.default_sender is not None: @@ -104,7 +102,7 @@ def __call__(self, private: bool = False, *args, **kwargs) -> "ReceiptAPI": class ContractCall(ManagerAccessMixin): - def __init__(self, abi: MethodABI, address: AddressType) -> None: + def __init__(self, abi: "MethodABI", address: "AddressType") -> None: super().__init__() self.abi = abi self.address = address @@ -140,9 +138,9 @@ def __call__(self, *args, **kwargs) -> Any: class ContractMethodHandler(ManagerAccessMixin): contract: "ContractInstance" - abis: list[MethodABI] + abis: list["MethodABI"] - def __init__(self, contract: "ContractInstance", abis: list[MethodABI]) -> None: + def __init__(self, contract: "ContractInstance", abis: list["MethodABI"]) -> None: super().__init__() self.contract = contract self.abis = abis @@ -320,7 +318,7 @@ def estimate_gas_cost(self, *args, **kwargs) -> int: return self.transact.estimate_gas_cost(*arguments, **kwargs) -def _select_method_abi(abis: list[MethodABI], args: Union[tuple, list]) -> MethodABI: +def _select_method_abi(abis: list["MethodABI"], args: Union[tuple, list]) -> "MethodABI": args = args or [] selected_abi = None for abi in abis: @@ -335,13 +333,10 @@ def _select_method_abi(abis: list[MethodABI], args: Union[tuple, list]) -> Metho class ContractTransaction(ManagerAccessMixin): - abi: MethodABI - address: AddressType - - def __init__(self, abi: MethodABI, address: AddressType) -> None: + def __init__(self, abi: "MethodABI", address: "AddressType") -> None: super().__init__() - self.abi = abi - self.address = address + self.abi: "MethodABI" = abi + self.address: "AddressType" = address @log_instead_of_fail(default="") def __repr__(self) -> str: @@ -362,7 +357,7 @@ def __call__(self, *args, **kwargs) -> "ReceiptAPI": txn = self.serialize_transaction(*args, **kwargs) private = kwargs.get("private", False) - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): return kwargs["sender"].call(txn, **kwargs) txn = self.provider.prepare_transaction(txn) @@ -441,6 +436,7 @@ def _as_transaction(self, *args) -> ContractTransaction: ) +# TODO: In Ape 0.9 - make not a BaseModel - no reason to. class ContractEvent(BaseInterfaceModel): """ The types of events on a :class:`~ape.contracts.base.ContractInstance`. @@ -616,7 +612,7 @@ def query( stop_block: Optional[int] = None, step: int = 1, engine_to_use: Optional[str] = None, - ) -> pd.DataFrame: + ) -> "DataFrame": """ Iterate through blocks for log events @@ -635,6 +631,8 @@ def query( Returns: pd.DataFrame """ + # perf: pandas import is really slow. Avoid importing at module level. + import pandas as pd if start_block < 0: start_block = self.chain_manager.blocks.height + start_block @@ -800,7 +798,7 @@ def poll_logs( class ContractTypeWrapper(ManagerAccessMixin): - contract_type: ContractType + contract_type: "ContractType" base_path: Optional[Path] = None @property @@ -812,7 +810,7 @@ def selector_identifiers(self) -> dict[str, str]: return self.contract_type.selector_identifiers @property - def identifier_lookup(self) -> dict[str, ABI_W_SELECTOR_T]: + def identifier_lookup(self) -> dict[str, "ABI_W_SELECTOR_T"]: """ Provides a mapping of method, error, and event selector identifiers to ABI Types. @@ -898,6 +896,9 @@ def repr_pretty_for_assignment(cls, *args, **kwargs): info = _get_info() error_type.info = error_type.__doc__ = info # type: ignore if info: + # perf: Avoid forcing everyone to import from IPython. + from IPython.lib.pretty import for_type + error_type._repr_pretty_ = repr_pretty_for_assignment # type: ignore # Register the dynamically-created type with IPython so it integrates. @@ -922,8 +923,8 @@ class ContractInstance(BaseAddress, ContractTypeWrapper): def __init__( self, - address: AddressType, - contract_type: ContractType, + address: "AddressType", + contract_type: "ContractType", txn_hash: Optional[Union[str, HexBytes]] = None, ) -> None: super().__init__() @@ -957,7 +958,9 @@ def __call__(self, *args, **kwargs) -> "ReceiptAPI": return super().__call__(*args, **kwargs) @classmethod - def from_receipt(cls, receipt: "ReceiptAPI", contract_type: ContractType) -> "ContractInstance": + def from_receipt( + cls, receipt: "ReceiptAPI", contract_type: "ContractType" + ) -> "ContractInstance": """ Create a contract instance from the contract deployment receipt. """ @@ -997,7 +1000,7 @@ def __repr__(self) -> str: return f"<{contract_name} {self.address}>" @property - def address(self) -> AddressType: + def address(self) -> "AddressType": """ The address of the contract. @@ -1009,7 +1012,7 @@ def address(self) -> AddressType: @cached_property def _view_methods_(self) -> dict[str, ContractCallHandler]: - view_methods: dict[str, list[MethodABI]] = dict() + view_methods: dict[str, list["MethodABI"]] = dict() for abi in self.contract_type.view_methods: if abi.name in view_methods: @@ -1028,7 +1031,7 @@ def _view_methods_(self) -> dict[str, ContractCallHandler]: @cached_property def _mutable_methods_(self) -> dict[str, ContractTransactionHandler]: - mutable_methods: dict[str, list[MethodABI]] = dict() + mutable_methods: dict[str, list["MethodABI"]] = dict() for abi in self.contract_type.mutable_methods: if abi.name in mutable_methods: @@ -1075,7 +1078,7 @@ def call_view_method(self, method_name: str, *args, **kwargs) -> Any: else: # Didn't find anything that matches - name = self.contract_type.name or ContractType.__name__ + name = self.contract_type.name or "ContractType" raise ApeAttributeError(f"'{name}' has no attribute '{method_name}'.") def invoke_transaction(self, method_name: str, *args, **kwargs) -> "ReceiptAPI": @@ -1110,7 +1113,7 @@ def invoke_transaction(self, method_name: str, *args, **kwargs) -> "ReceiptAPI": else: # Didn't find anything that matches - name = self.contract_type.name or ContractType.__name__ + name = self.contract_type.name or "ContractType" raise ApeAttributeError(f"'{name}' has no attribute '{method_name}'.") def get_event_by_signature(self, signature: str) -> ContractEvent: @@ -1168,7 +1171,7 @@ def get_error_by_signature(self, signature: str) -> type[CustomError]: @cached_property def _events_(self) -> dict[str, list[ContractEvent]]: - events: dict[str, list[EventABI]] = {} + events: dict[str, list["EventABI"]] = {} for abi in self.contract_type.events: if abi.name in events: @@ -1339,7 +1342,7 @@ class ContractContainer(ContractTypeWrapper, ExtraAttributesMixin): contract_container = project.MyContract # Assuming there is a contract named "MyContract" """ - def __init__(self, contract_type: ContractType) -> None: + def __init__(self, contract_type: "ContractType") -> None: self.contract_type = contract_type @log_instead_of_fail(default="") @@ -1404,7 +1407,7 @@ def deployments(self): return self.chain_manager.contracts.get_deployments(self) def at( - self, address: AddressType, txn_hash: Optional[Union[str, HexBytes]] = None + self, address: "AddressType", txn_hash: Optional[Union[str, HexBytes]] = None ) -> ContractInstance: """ Get a contract at the given address. @@ -1473,7 +1476,7 @@ def deploy(self, *args, publish: bool = False, **kwargs) -> ContractInstance: if kwargs.get("value") and not self.contract_type.constructor.is_payable: raise MethodNonPayableError("Sending funds to a non-payable constructor.") - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): # Handle account-related preparation if needed, such as signing receipt = self._cache_wrap(lambda: kwargs["sender"].call(txn, **kwargs)) @@ -1533,7 +1536,7 @@ def declare(self, *args, **kwargs) -> "ReceiptAPI": transaction = self.provider.network.ecosystem.encode_contract_blueprint( self.contract_type, *args, **kwargs ) - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): return kwargs["sender"].call(transaction) receipt = self.provider.send_transaction(transaction) diff --git a/src/ape/managers/networks.py b/src/ape/managers/networks.py index 8297ce43a3..a30c63b06e 100644 --- a/src/ape/managers/networks.py +++ b/src/ape/managers/networks.py @@ -2,6 +2,8 @@ from functools import cached_property from typing import TYPE_CHECKING, Optional, Union +from evmchains import PUBLIC_CHAIN_META + from ape.api.networks import EcosystemAPI, NetworkAPI, ProviderContextManager from ape.exceptions import EcosystemNotFoundError, NetworkError, NetworkNotFoundError from ape.managers.base import BaseManager @@ -53,7 +55,6 @@ def active_provider(self) -> Optional["ProviderAPI"]: """ The currently connected provider if one exists. Otherwise, returns ``None``. """ - return self._active_provider @active_provider.setter @@ -164,7 +165,6 @@ def ecosystem_names(self) -> set[str]: """ The set of all ecosystem names in ``ape``. """ - return set(self.ecosystems) @property @@ -236,7 +236,8 @@ def ecosystems(self) -> dict[str, EcosystemAPI]: existing_cls = plugin_ecosystems[base_ecosystem_name] ecosystem_cls = existing_cls.model_copy( - update={"name": ecosystem_name}, cache_clear=("_networks_from_plugins",) + update={"name": ecosystem_name}, + cache_clear=("_networks_from_plugins", "_networks_from_evmchains"), ) plugin_ecosystems[ecosystem_name] = ecosystem_cls @@ -437,10 +438,29 @@ def get_ecosystem(self, ecosystem_name: str) -> EcosystemAPI: :class:`~ape.api.networks.EcosystemAPI` """ - if ecosystem_name not in self.ecosystem_names: - raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names) + if ecosystem_name in self.ecosystem_names: + return self.ecosystems[ecosystem_name] - return self.ecosystems[ecosystem_name] + elif ecosystem_name.lower().replace(" ", "-") in PUBLIC_CHAIN_META: + ecosystem_name = ecosystem_name.lower().replace(" ", "-") + symbol = None + for net in PUBLIC_CHAIN_META[ecosystem_name].values(): + if not (native_currency := net.get("nativeCurrency")): + continue + + if "symbol" not in native_currency: + continue + + symbol = native_currency["symbol"] + break + + symbol = symbol or "ETH" + + # Is an EVM chain, can automatically make a class using evm-chains. + evm_class = self._plugin_ecosystems["ethereum"].__class__ + return evm_class(name=ecosystem_name, fee_token_symbol=symbol) + + raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names) def get_provider_from_choice( self, @@ -548,7 +568,6 @@ def parse_network_choice( Returns: :class:`~api.api.networks.ProviderContextManager` """ - provider = self.get_provider_from_choice( network_choice=network_choice, provider_settings=provider_settings ) diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index 1b154a573c..9fd69439db 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -1940,7 +1940,6 @@ def reconfigure(self, **overrides): self._config_override = overrides _ = self.config - self.account_manager.test_accounts.reset() def extract_manifest(self) -> PackageManifest: diff --git a/src/ape_ethereum/provider.py b/src/ape_ethereum/provider.py index cd5d67552a..58bba04dde 100644 --- a/src/ape_ethereum/provider.py +++ b/src/ape_ethereum/provider.py @@ -16,7 +16,7 @@ from eth_pydantic_types import HexBytes from eth_typing import BlockNumber, HexStr from eth_utils import add_0x_prefix, is_hex, to_hex -from evmchains import get_random_rpc +from evmchains import PUBLIC_CHAIN_META, get_random_rpc from pydantic.dataclasses import dataclass from requests import HTTPError from web3 import HTTPProvider, IPCProvider, Web3 @@ -1548,6 +1548,18 @@ def _complete_connect(self): self.network.verify_chain_id(chain_id) + # Correct network name, if using custom-URL approach. + if self.network.name == "custom": + for ecosystem_name, network in PUBLIC_CHAIN_META.items(): + for network_name, meta in network.items(): + if "chainId" not in meta or meta["chainId"] != chain_id: + continue + + # Network found. + self.network.name = network_name + self.network.ecosystem.name = ecosystem_name + break + def disconnect(self): self._call_trace_approach = None self._web3 = None diff --git a/src/ape_node/provider.py b/src/ape_node/provider.py index 95bd54d2b7..aa7fb0f2ee 100644 --- a/src/ape_node/provider.py +++ b/src/ape_node/provider.py @@ -208,7 +208,7 @@ def disconnect(self): def _clean(self): if self._data_dir.is_dir(): - shutil.rmtree(self._data_dir) + shutil.rmtree(self._data_dir, ignore_errors=True) # dir must exist when initializing chain. self._data_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/ape_run/_cli.py b/src/ape_run/_cli.py index 94e30d361d..6dc0153295 100644 --- a/src/ape_run/_cli.py +++ b/src/ape_run/_cli.py @@ -8,19 +8,18 @@ from typing import Any, Union import click -from click import Command, Context, Option from ape.cli.commands import ConnectedProviderCommand from ape.cli.options import _VERBOSITY_VALUES, _create_verbosity_kwargs, verbosity_option from ape.exceptions import ApeException, handle_ape_exception from ape.logging import logger -from ape.utils.basemodel import ManagerAccessMixin as access -from ape.utils.os import get_relative_path, use_temp_sys_path -from ape_console._cli import console @contextmanager def use_scripts_sys_path(path: Path): + # perf: avoid importing at top of module so `--help` is faster. + from ape.utils.os import use_temp_sys_path + # First, ensure there is not an existing scripts module. scripts = sys.modules.get("scripts") if scripts: @@ -70,7 +69,9 @@ def __init__(self, *args, **kwargs): self._command_called = None self._has_warned_missing_hook: set[Path] = set() - def invoke(self, ctx: Context) -> Any: + def invoke(self, ctx: click.Context) -> Any: + from ape.utils.basemodel import ManagerAccessMixin as access + try: return super().invoke(ctx) except Exception as err: @@ -95,7 +96,8 @@ def invoke(self, ctx: Context) -> Any: raise def _get_command(self, filepath: Path) -> Union[click.Command, click.Group, None]: - relative_filepath = get_relative_path(filepath, access.local_project.path) + scripts_folder = Path.cwd() / "scripts" + relative_filepath = filepath.relative_to(scripts_folder) # First load the code module by compiling it # NOTE: This does not execute the module @@ -122,14 +124,14 @@ def _get_command(self, filepath: Path) -> Union[click.Command, click.Group, None self._namespace[filepath.stem] = cli_ns cli_obj = cli_ns["cli"] - if not isinstance(cli_obj, Command): + if not isinstance(cli_obj, click.Command): logger.warning("Found `cli()` method but it is not a click command.") return None params = [getattr(x, "name", None) for x in cli_obj.params] if "verbosity" not in params: option_kwargs = _create_verbosity_kwargs() - option = Option(_VERBOSITY_VALUES, **option_kwargs) + option = click.Option(_VERBOSITY_VALUES, **option_kwargs) cli_obj.params.append(option) cli_obj.name = filepath.stem if cli_obj.name in ("cli", "", None) else cli_obj.name @@ -175,13 +177,16 @@ def call(): @property def commands(self) -> dict[str, Union[click.Command, click.Group]]: - if not access.local_project.scripts_folder.is_dir(): + # perf: Don't reference `.local_project.scripts_folder` here; + # it's too slow when doing just doing `--help`. + scripts_folder = Path.cwd() / "scripts" + if not scripts_folder.is_dir(): return {} - return self._get_cli_commands(access.local_project.scripts_folder) + return self._get_cli_commands(scripts_folder) def _get_cli_commands(self, base_path: Path) -> dict: - commands: dict[str, Command] = {} + commands: dict[str, click.Command] = {} for filepath in base_path.iterdir(): if filepath.stem.startswith("_"): @@ -194,6 +199,7 @@ def _get_cli_commands(self, base_path: Path) -> dict: subcommands = self._get_cli_commands(filepath) for subcommand in subcommands.values(): group.add_command(subcommand) + commands[filepath.stem] = group if filepath.suffix == ".py": @@ -223,6 +229,8 @@ def result_callback(self, result, interactive: bool): # type: ignore[override] return result def _launch_console(self): + from ape.utils.basemodel import ManagerAccessMixin as access + trace = inspect.trace() trace_frames = [ x for x in trace if x.filename.startswith(str(access.local_project.scripts_folder)) @@ -247,6 +255,8 @@ def _launch_console(self): if frame: del frame + from ape_console._cli import console + return console(project=access.local_project, extra_locals=extra_locals, embed=True) diff --git a/tests/functional/geth/test_network_manager.py b/tests/functional/geth/test_network_manager.py index 18d5274f6e..8c2d5986aa 100644 --- a/tests/functional/geth/test_network_manager.py +++ b/tests/functional/geth/test_network_manager.py @@ -37,3 +37,17 @@ def test_fork_upstream_provider(networks, mock_geth_sepolia, geth_provider, mock geth_provider.provider_settings["uri"] = orig else: del geth_provider.provider_settings["uri"] + + +@geth_process_test +@pytest.mark.parametrize( + "connection_str", ("moonbeam:moonriver", "https://moonriver.api.onfinality.io/public") +) +def test_parse_network_choice_evmchains(networks, connection_str): + """ + Show we can (without having a plugin installed) connect to a network + that evm-chains knows about. + """ + with networks.parse_network_choice(connection_str) as moon_provider: + assert moon_provider.network.name == "moonriver" + assert moon_provider.network.ecosystem.name == "moonbeam" diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index bb0ee3de41..b432fc1b94 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -673,8 +673,12 @@ class TestProject: def test_init(self, with_dependencies_project_path): # Purpose not using `project_with_contracts` fixture. project = Project(with_dependencies_project_path) - project.manifest_path.unlink(missing_ok=True) assert project.path == with_dependencies_project_path + project.manifest_path.unlink(missing_ok=True) + + # Re-init to show it doesn't create the manifest file. + project = Project(with_dependencies_project_path) + # Manifest should have been created by default. assert not project.manifest_path.is_file()