Skip to content

Commit

Permalink
Merge branch 'main' into feat/upgrade-pydantic-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
NotPeopling2day authored Nov 20, 2023
2 parents e79b295 + 9d4b667 commit db4326f
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 14 deletions.
33 changes: 30 additions & 3 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Collection,
Dict,
Iterator,
Expand Down Expand Up @@ -577,19 +578,45 @@ class ProviderContextManager(ManagerAccessMixin):
provider_stack: List[str] = []
disconnect_map: Dict[str, bool] = {}

def __init__(self, provider: "ProviderAPI", disconnect_after: bool = False):
# We store a provider object at the class level for use when disconnecting
# due to an exception, when interactive mode is set. If we don't hold on
# to a reference to this object, the provider is dropped and reconnecting results
# in losing state when using a spawned local provider
_recycled_provider: ClassVar[Optional["ProviderAPI"]] = None

def __init__(
self,
provider: "ProviderAPI",
disconnect_after: bool = False,
disconnect_on_exit: bool = True,
):
self._provider = provider
self._disconnect_after = disconnect_after
self._disconnect_on_exit = disconnect_on_exit
self._skipped_disconnect = False

@property
def empty(self) -> bool:
return not self.connected_providers or not self.provider_stack

def __enter__(self, *args, **kwargs):
# If we have a recycled provider available, this means our last exit
# was due to an exception during interactive mode. We should resume that
# same connection, but also clear the object so we don't do this again
# in later provider contexts, which we would want to behave normally
if self._recycled_provider is not None:
# set inner var to the recycled provider for use in push_provider()
self._provider = self._recycled_provider
ProviderContextManager._recycled_provider = None
return self.push_provider()

def __exit__(self, *args, **kwargs):
self.pop_provider()
def __exit__(self, exception, *args, **kwargs):
if not self._disconnect_on_exit and exception is not None:
# We want to skip disconnection when exiting due to an exception in interactive mode
if provider := self.network_manager.active_provider:
ProviderContextManager._recycled_provider = provider
else:
self.pop_provider()

def push_provider(self):
must_connect = not self._provider.is_connected
Expand Down
13 changes: 12 additions & 1 deletion src/ape/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
from ape import networks


def check_parents_for_interactive(ctx: Context) -> bool:
interactive: bool = ctx.params.get("interactive", False)
if interactive:
return True
# If not found, check the parent context.
if interactive is None and ctx.parent:
return check_parents_for_interactive(ctx.parent)
return False


class NetworkBoundCommand(click.Command):
"""
A command that uses the :meth:`~ape.cli.options.network_option`.
Expand All @@ -14,5 +24,6 @@ class NetworkBoundCommand(click.Command):

def invoke(self, ctx: Context) -> Any:
value = ctx.params.get("network") or networks.default_ecosystem.name
with networks.parse_network_choice(value):
interactive = check_parents_for_interactive(ctx)
with networks.parse_network_choice(value, disconnect_on_exit=not interactive):
super().invoke(ctx)
7 changes: 6 additions & 1 deletion src/ape/managers/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ def parse_network_choice(
network_choice: Optional[str] = None,
provider_settings: Optional[Dict] = None,
disconnect_after: bool = False,
disconnect_on_exit: bool = True,
) -> ProviderContextManager:
"""
Parse a network choice into a context manager for managing a temporary
Expand Down Expand Up @@ -445,7 +446,11 @@ def parse_network_choice(
provider = self.get_provider_from_choice(
network_choice=network_choice, provider_settings=provider_settings
)
return ProviderContextManager(provider=provider, disconnect_after=disconnect_after)
return ProviderContextManager(
provider=provider,
disconnect_after=disconnect_after,
disconnect_on_exit=disconnect_on_exit,
)

@property
def default_ecosystem(self) -> EcosystemAPI:
Expand Down
18 changes: 10 additions & 8 deletions src/ape_run/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import click
from click import Command, Context, Option

from ape import project
from ape import networks, project
from ape.cli import NetworkBoundCommand, network_option, verbosity_option
from ape.cli.options import _VERBOSITY_VALUES, _create_verbosity_kwargs
from ape.exceptions import ApeException, handle_ape_exception
Expand Down Expand Up @@ -76,13 +76,15 @@ def invoke(self, ctx: Context) -> Any:
if ctx.params["interactive"]:
# Print the exception trace and then launch the console
# Attempt to use source-traceback style printing.
if not isinstance(err, ApeException) or not handle_ape_exception(
err, [ctx.obj.project_manager.path]
):
err_info = traceback.format_exc()
click.echo(err_info)

self._launch_console()
network_value = ctx.params.get("network") or networks.default_ecosystem.name
with networks.parse_network_choice(network_value, disconnect_on_exit=False):
if not isinstance(err, ApeException) or not handle_ape_exception(
err, [ctx.obj.project_manager.path]
):
err_info = traceback.format_exc()
click.echo(err_info)

self._launch_console()
else:
# Don't handle error - raise exception as normal.
raise
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/cli/projects/script/scripts/error_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import click

import ape


@click.command(short_help="Use a subcommand")
def cli():
local_variable = "test foo bar" # noqa[F841]
provider = ape.chain.provider
provider.set_timestamp(123123123123123123)
raise Exception("Expected exception") # noqa: T001
5 changes: 5 additions & 0 deletions tests/integration/cli/projects/script/scripts/error_main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import ape


def main():
local_variable = "test foo bar" # noqa[F841]
provider = ape.chain.provider
provider.set_timestamp(123123123123123123)
raise Exception("Expected exception")
4 changes: 4 additions & 0 deletions tests/integration/cli/projects/script/scripts/error_no_def.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
import ape

local_variable = "test foo bar" # noqa[F841]
provider = ape.chain.provider
provider.set_timestamp(123123123123123123)
raise Exception("Expected exception")
3 changes: 2 additions & 1 deletion tests/integration/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,14 @@ def test_run_interactive(ape_cli, runner, project):
]

# Show that the variable namespace from the script is available in the console.
user_input = "local_variable\nexit\n"
user_input = "local_variable\nape.chain.provider.mine()\nape.chain.blocks.head\nexit\n"

result = runner.invoke(ape_cli, ["run", "--interactive", scripts[0].stem], input=user_input)
assert result.exit_code == 0, result.output

# From script: local_variable = "test foo bar"
assert "test foo bar" in result.output
assert "timestamp=123123123123123" in result.output


@skip_projects_except("script")
Expand Down

0 comments on commit db4326f

Please sign in to comment.