Skip to content

Commit a235aa3

Browse files
authored
Merge branch 'main' into refactor/ethereum/safe-proxy-heuristic
2 parents b693334 + d34fbdc commit a235aa3

12 files changed

Lines changed: 110 additions & 58 deletions

File tree

Dockerfile

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,35 @@ WORKDIR /home/harambe/project
2121
# NOTE: In CI, you need to cache `uv.lock` (or create it if it doesn't exist)
2222
COPY pyproject.toml uv.lock ./
2323

24-
# NOTE: Needed to mock version for `setuptools-scm` (pass at build time)
25-
ARG APE_VERSION
26-
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_ETH_APE=${APE_VERSION}
27-
24+
# UV Configurations
25+
# NOTE: use system python (better for our images, that inherit from `python:$VERSION`)
26+
ENV UV_MANAGED_PYTHON=false
27+
# NOTE: skip installing dev-only dependencies
28+
ENV UV_NO_DEV=true
29+
# NOTE: use `uv.lock` that we loaded into build
30+
ENV UV_FROZEN=true
31+
# NOTE: installs everything as non-editable (faster)
32+
ENV UV_NO_EDITABLE=true
33+
# NOTE: improves load speed of dependencies
34+
ENV UV_COMPILE_BYTECODE=true
2835
# NOTE: link mode "copy" silences warnings about hard links in other commands
2936
ENV UV_LINK_MODE=copy
3037

3138
# Install dependencies first
32-
# NOTE: --compile-bytecode improves load speed of dependencies
39+
# NOTE: --no-install-project so that we have our dependencies built first (speeds up incremental builds)
3340
RUN --mount=type=cache,target=/root/.cache/uv \
34-
uv sync --frozen --no-install-project --no-editable --compile-bytecode
41+
uv sync --no-install-project
42+
43+
# NOTE: Needed to mock version for `setuptools-scm` (pass at build time)
44+
ARG APE_VERSION
45+
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_ETH_APE=${APE_VERSION}
3546

3647
# Now copy Ape's source code over
3748
COPY src src
3849

3950
# Install Ape using pre-installed dependencies
40-
# NOTE: --compile-bytecode improves load speed of dependencies
4151
RUN --mount=type=cache,target=/root/.cache/uv \
42-
uv sync --frozen --no-editable --compile-bytecode
52+
uv sync
4353

4454
# Stage 2: Slim image (ape core only)
4555

@@ -48,6 +58,7 @@ FROM python:${PYTHON_VERSION}-slim AS slim
4858
# NOTE: Add a bespoke user to run commands with
4959
RUN useradd --create-home --shell /bin/bash harambe
5060
WORKDIR /home/harambe/project
61+
RUN chown harambe:harambe .
5162

5263
COPY --from=slim-builder --chown=harambe:harambe \
5364
/home/harambe/project/.venv /home/harambe/project/.venv
@@ -68,15 +79,15 @@ FROM slim-builder AS full-builder
6879

6980
# Install recommended plugins
7081
RUN --mount=type=cache,target=/root/.cache/uv \
71-
uv sync --frozen --no-editable --compile-bytecode --extra recommended-plugins
82+
uv sync --extra recommended-plugins
7283

7384
# Stage 4: Full image (slim with recommended plugins from full-builder)
7485

7586
FROM slim AS full
7687

7788
# Install anvil (for the Foundry plugin to be useful)
7889
# NOTE: Adds 33MB to build
79-
COPY --from=ghcr.io/foundry-rs/foundry:latest \
90+
COPY --from=ghcr.io/foundry-rs/foundry:stable \
8091
/usr/local/bin/anvil /home/harambe/.local/bin/anvil
8192

8293
COPY --from=full-builder --chown=harambe:harambe \

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ dependencies = [
5353
"python-dateutil>=2.8.2,<3",
5454
"PyYAML>=5.1,<7",
5555
"requests>=2.28.1,<3",
56-
"rich>=13.9,<14",
56+
"rich>=14.3.3,<15",
5757
"SQLAlchemy>=1.4.35",
5858
"toml; python_version<'3.11'",
5959
"tqdm>=4.67,<5.0",

src/ape/api/accounts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ def deploy(
354354
raise TypeError(
355355
"contract argument must be a ContractContainer type, "
356356
"such as 'project.MyContract' where 'MyContract' is the name of "
357-
"a contract in your project."
357+
f"a contract in your project. Got type={type(contract)}"
358358
)
359359

360360
bytecode = contract.contract_type.deployment_bytecode

src/ape/api/providers.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from ethpm_types import MethodABI
2222
from pydantic import Field, computed_field, field_serializer, model_validator
2323

24-
from ape.api.networks import NetworkAPI
24+
from ape.api.networks import NetworkAPI, ProviderContextManager
2525
from ape.api.query import BlockTransactionQuery
2626
from ape.api.transactions import ReceiptAPI, TransactionAPI
2727
from ape.exceptions import (
@@ -311,6 +311,38 @@ def disconnect(self):
311311
Disconnect from a provider, such as tear-down a process or quit an HTTP session.
312312
"""
313313

314+
def connection(
315+
self,
316+
disconnect_after: bool = False,
317+
disconnect_on_exit: bool = True,
318+
) -> ProviderContextManager:
319+
"""
320+
Use this provider in a temporary context. Connects on enter and disconnects
321+
on exit. Equivalent to :meth:`~ape.managers.networks.NetworkManager.parse_network_choice`
322+
but for a provider instance that has already been resolved.
323+
324+
Usage example::
325+
326+
provider = networks.ethereum.mainnet.get_provider("infura")
327+
with provider.connection():
328+
...
329+
330+
Args:
331+
disconnect_after (bool): Set to ``True`` to force a disconnect after ending
332+
the context. Defaults to ``False`` so the connection can be reused, such
333+
as in a multi-chain scenario.
334+
disconnect_on_exit (bool): Whether to disconnect on the exit of the python
335+
session. Defaults to ``True``.
336+
337+
Returns:
338+
:class:`~ape.api.networks.ProviderContextManager`
339+
"""
340+
return ProviderContextManager(
341+
provider=self,
342+
disconnect_after=disconnect_after,
343+
disconnect_on_exit=disconnect_on_exit,
344+
)
345+
314346
@property
315347
def ipc_path(self) -> Path | None:
316348
"""

src/ape/cli/commands.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,19 @@ def get_param_from_ctx(ctx: "Context", param: str) -> Any | None:
2828

2929
def parse_network(ctx: "Context") -> "ProviderContextManager | None":
3030
from ape.api.providers import ProviderAPI
31+
from ape.cli.options import NETWORK_META_KEY
3132
from ape.utils.basemodel import ManagerAccessMixin as access
3233

3334
interactive = get_param_from_ctx(ctx, "interactive")
3435

36+
# Handle if already parsed by the network_option callback. Preferred
37+
# location is ``ctx.meta`` so we don't collide with user-owned ``ctx.obj``
38+
# (e.g. when a command chains ``click.make_pass_decorator(...)`` that
39+
# replaces ``ctx.obj`` with a typed container).
40+
network_meta = ctx.meta.get(NETWORK_META_KEY)
41+
if network_meta and (provider := network_meta.get("provider")) is not None:
42+
return provider.network.use_provider(provider, disconnect_on_exit=not interactive)
43+
3544
# Handle if already parsed (as when using network-option)
3645
if ctx.obj and "provider" in ctx.obj:
3746
provider = ctx.obj["provider"]

src/ape/cli/options.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ def _get_requested_networks(function, network_object_names):
322322
return [x for x in command_kwargs if x in network_object_names]
323323

324324

325+
NETWORK_META_KEY = "__ape_network__"
326+
327+
325328
def _update_context_with_network(ctx, provider, requested_network_objects):
326329
choice_classes = {
327330
"ecosystem": provider.network.ecosystem,
@@ -336,22 +339,12 @@ def _update_context_with_network(ctx, provider, requested_network_objects):
336339
ctx.params[item] = instance
337340

338341
if isinstance(ctx.command, ConnectedProviderCommand):
339-
# Place all values, regardless of request in
340-
# the context. This helps the Ape CLI backend.
341-
if ctx.obj is None:
342-
# Happens when using commands that don't use the
343-
# Ape context or any context.
344-
ctx.obj = {}
345-
346-
for choice, obj in choice_classes.items():
347-
try:
348-
ctx.obj[choice] = obj
349-
except Exception:
350-
# This would only happen if using an unusual context object.
351-
raise Abort(
352-
"Cannot use connected-provider command type(s) "
353-
"with non key-settable context object."
354-
)
342+
# Place all values, regardless of request, in ``ctx.meta`` so that
343+
# ``parse_network`` (and ``ctx.obj`` consumers that use
344+
# :meth:`click.Context.ensure_object`) can all coexist. ``ctx.obj`` is
345+
# owned by the caller's code; ``ctx.meta`` is Click's cross-cutting
346+
# scratch space and is safe for us to write to.
347+
ctx.meta.setdefault(NETWORK_META_KEY, {}).update(choice_classes)
355348

356349

357350
def _get_provider(value, default, keep_as_choice_str):

src/ape/managers/chain.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -924,11 +924,12 @@ def isolate(self):
924924
receipt = contract.fooBar(sender=owner)
925925
"""
926926

927-
snapshot = None
928927
try:
929928
snapshot = self.snapshot()
930929
except APINotImplementedError:
931-
logger.warning("Provider does not support snapshotting.")
930+
logger.error("Failed to create snapsho: Provider does not support snapshotting.")
931+
snapshot = None
932+
932933
pending = self.pending_timestamp
933934

934935
start_ecosystem_name = self.provider.network.ecosystem.name
@@ -937,30 +938,31 @@ def isolate(self):
937938

938939
try:
939940
yield
940-
finally:
941-
if snapshot is None:
942-
logger.error("Failed to create snapshot.")
943-
return
941+
except Exception:
942+
pass # NOTE: Handle cleanup after any exceptions in yielded context
944943

945-
end_ecosystem_name = self.provider.network.ecosystem.name
946-
end_network_name = self.provider.network.name
947-
end_provider_name = self.provider.name
944+
if snapshot is None:
945+
return
948946

949-
if (
950-
start_ecosystem_name != end_ecosystem_name
951-
or start_network_name != end_network_name
952-
or start_provider_name != end_provider_name
953-
):
954-
logger.warning("Provider changed before isolation completed.")
955-
return
947+
end_ecosystem_name = self.provider.network.ecosystem.name
948+
end_network_name = self.provider.network.name
949+
end_provider_name = self.provider.name
956950

957-
self.chain_manager.restore(snapshot)
951+
if (
952+
start_ecosystem_name != end_ecosystem_name
953+
or start_network_name != end_network_name
954+
or start_provider_name != end_provider_name
955+
):
956+
logger.warning("Provider changed before isolation completed.")
957+
return
958958

959-
try:
960-
self.pending_timestamp = pending
961-
except APINotImplementedError:
962-
# Provider does not support time travel.
963-
pass
959+
self.chain_manager.restore(snapshot)
960+
961+
try:
962+
self.pending_timestamp = pending
963+
except APINotImplementedError:
964+
# Provider does not support time travel.
965+
pass
964966

965967
def mine(
966968
self,

src/ape_ethereum/provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -781,10 +781,10 @@ def _eth_call(
781781

782782
vm_err = self.get_virtual_machine_error(
783783
err,
784-
trace=lambda: _lazy_call_trace.trace,
784+
trace=lambda: None if skip_trace else _lazy_call_trace.trace,
785785
contract_address=contract_address,
786-
source_traceback=lambda: _lazy_call_trace.source_traceback,
787-
set_ape_traceback=raise_on_revert,
786+
source_traceback=lambda: None if skip_trace else _lazy_call_trace.source_traceback,
787+
set_ape_traceback=raise_on_revert and not skip_trace,
788788
)
789789
if raise_on_revert:
790790
raise vm_err.with_ape_traceback() from err

tests/functional/test_accounts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,9 @@ def test_deploy(owner, contract_container, clean_contract_caches):
330330

331331
@explorer_test
332332
@pytest.mark.timeout(360)
333-
def test_deploy_and_publish_local_network(owner, contract_container):
333+
def test_deploy_and_publish_local_network(owner, minimal_proxy_container):
334334
with pytest.raises(ProjectError, match="Can only publish deployments on a live network"):
335-
owner.deploy(contract_container, 0, publish=True)
335+
owner.deploy(minimal_proxy_container, publish=True)
336336

337337

338338
@explorer_test

tests/functional/test_cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,7 @@ def test_get_param_from_ctx(mocker):
873873

874874
def test_parse_network_when_interactive_and_no_param(mocker):
875875
ctx = mocker.MagicMock()
876+
ctx.meta = {}
876877
ctx.params = {"interactive": True}
877878
ctx.parent = None
878879
network_ctx = parse_network(ctx)
@@ -883,6 +884,7 @@ def test_parse_network_when_interactive_and_no_param(mocker):
883884

884885
def test_parse_network_when_interactive_and_str_param(mocker):
885886
ctx = mocker.MagicMock()
887+
ctx.meta = {}
886888
ctx.params = {"interactive": True, "network": "ethereum:local:test"}
887889
network_ctx = parse_network(ctx)
888890
assert network_ctx is not None
@@ -892,6 +894,7 @@ def test_parse_network_when_interactive_and_str_param(mocker):
892894

893895
def test_parse_network_when_interactive_and_class_param(mocker, eth_tester_provider):
894896
ctx = mocker.MagicMock()
897+
ctx.meta = {}
895898
ctx.params = {"interactive": True, "network": eth_tester_provider}
896899
network_ctx = parse_network(ctx)
897900
assert network_ctx is not None
@@ -901,6 +904,7 @@ def test_parse_network_when_interactive_and_class_param(mocker, eth_tester_provi
901904

902905
def test_parse_network_when_explicit_none(mocker):
903906
ctx = mocker.MagicMock()
907+
ctx.meta = {}
904908
ctx.params = {"network": _NONE_NETWORK}
905909
network_ctx = parse_network(ctx)
906910
assert network_ctx is None

0 commit comments

Comments
 (0)