From a92e5d1afe424503d480e977f1993ebd05cae401 Mon Sep 17 00:00:00 2001 From: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:56:08 -0400 Subject: [PATCH 1/2] chore: upgrade linting tools (#2805) --- .github/workflows/test.yaml | 338 +++++++++--------- .gitignore | 3 + .pre-commit-config.yaml | 8 +- docs/userguides/accounts.md | 37 +- docs/userguides/clis.md | 59 +-- docs/userguides/compile.md | 12 +- docs/userguides/console.md | 9 +- docs/userguides/contract_metadata.md | 9 +- docs/userguides/contracts.md | 15 +- docs/userguides/dependencies.md | 4 +- docs/userguides/developing_plugins.md | 28 +- docs/userguides/logging.md | 3 + docs/userguides/networks.md | 3 + docs/userguides/proxy.md | 12 +- docs/userguides/scripts.md | 13 +- docs/userguides/testing.md | 43 ++- docs/userguides/trace.md | 4 +- docs/userguides/transactions.md | 15 +- pyproject.toml | 16 +- src/ape/_cli.py | 22 +- src/ape/api/accounts.py | 2 +- src/ape/api/address.py | 7 +- src/ape/api/compiler.py | 6 +- src/ape/api/config.py | 4 +- src/ape/api/networks.py | 34 +- src/ape/api/providers.py | 8 +- src/ape/api/transactions.py | 6 +- src/ape/cli/choices.py | 2 +- src/ape/cli/options.py | 2 +- src/ape/contracts/base.py | 24 +- src/ape/exceptions.py | 31 +- src/ape/logging.py | 32 +- src/ape/managers/_contractscache.py | 64 ++-- src/ape/managers/_deploymentscache.py | 4 +- src/ape/managers/accounts.py | 11 +- src/ape/managers/chain.py | 37 +- src/ape/managers/compilers.py | 10 +- src/ape/managers/config.py | 2 +- src/ape/managers/converters.py | 17 +- src/ape/managers/networks.py | 28 +- src/ape/managers/plugins.py | 4 +- src/ape/managers/project.py | 115 +++--- src/ape/managers/query.py | 2 +- src/ape/plugins/_utils.py | 4 +- src/ape/pytest/contextmanagers.py | 16 +- src/ape/pytest/fixtures.py | 21 +- src/ape/pytest/plugin.py | 27 +- src/ape/pytest/runners.py | 9 +- src/ape/types/coverage.py | 22 +- src/ape/types/events.py | 19 +- src/ape/types/private_mempool.py | 2 +- src/ape/types/signatures.py | 6 +- src/ape/types/trace.py | 4 +- src/ape/types/units.py | 2 +- src/ape/utils/__init__.py | 2 +- src/ape/utils/_github.py | 10 +- src/ape/utils/abi.py | 2 +- src/ape/utils/basemodel.py | 8 +- src/ape/utils/misc.py | 23 +- src/ape/utils/os.py | 5 +- src/ape/utils/process.py | 9 +- src/ape/utils/trace.py | 27 +- src/ape_accounts/_cli.py | 4 +- src/ape_accounts/accounts.py | 14 +- src/ape_cache/query.py | 10 +- src/ape_compile/_cli.py | 2 +- src/ape_compile/config.py | 6 +- src/ape_console/_cli.py | 2 +- src/ape_console/config.py | 3 +- src/ape_ethereum/__init__.py | 1 - src/ape_ethereum/_converters.py | 5 +- src/ape_ethereum/ecosystem.py | 60 ++-- src/ape_ethereum/provider.py | 135 +++---- src/ape_ethereum/trace.py | 91 ++--- src/ape_ethereum/transactions.py | 17 +- src/ape_networks/_cli.py | 8 +- src/ape_networks/config.py | 5 +- src/ape_node/provider.py | 25 +- src/ape_pm/_cli.py | 8 +- src/ape_pm/compiler.py | 2 +- src/ape_pm/dependency.py | 19 +- src/ape_pm/project.py | 15 +- src/ape_run/_cli.py | 2 +- src/ape_test/_cli.py | 2 +- src/ape_test/_watch.py | 8 +- src/ape_test/accounts.py | 4 +- src/ape_test/config.py | 8 +- src/ape_test/provider.py | 14 +- tests/conftest.py | 4 +- tests/functional/conftest.py | 2 +- tests/functional/geth/test_chain.py | 1 + tests/functional/geth/test_network_manager.py | 5 +- tests/functional/geth/test_provider.py | 99 ++--- tests/functional/geth/test_trace.py | 8 +- tests/functional/test_accounts.py | 49 ++- tests/functional/test_block_container.py | 40 ++- tests/functional/test_chain.py | 14 +- tests/functional/test_compilers.py | 4 +- tests/functional/test_config.py | 35 +- tests/functional/test_contract_event.py | 8 +- .../test_contract_method_handler.py | 10 +- tests/functional/test_contracts_cache.py | 31 ++ .../test_default_sender_context_manager.py | 10 +- tests/functional/test_dependencies.py | 2 +- tests/functional/test_ecosystem.py | 7 +- tests/functional/test_exceptions.py | 6 +- tests/functional/test_fixtures.py | 4 +- tests/functional/test_network_manager.py | 31 +- tests/functional/test_plugins.py | 32 +- tests/functional/test_project.py | 14 +- .../test_reverts_context_manager.py | 28 +- tests/functional/test_test.py | 2 +- tests/functional/utils/test_abi.py | 2 +- tests/integration/cli/conftest.py | 3 +- .../projects/only-dependencies/__init__.py | 1 + .../dependency_in_project_only/__init__.py | 1 + .../cli/projects/script/scripts/error_cli.py | 2 +- .../cli/projects/script/scripts/error_main.py | 2 +- .../projects/script/scripts/error_no_def.py | 2 +- .../cli/projects/test/tests/test_networks.py | 2 +- tests/integration/cli/test_accounts.py | 28 +- tests/integration/cli/test_init.py | 4 +- tests/integration/cli/test_pm.py | 14 +- tests/integration/cli/test_test.py | 4 +- tests/unit/api/test_api_init.py | 4 +- 125 files changed, 1196 insertions(+), 1112 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index aac30e8b27..07042b8120 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,167 +16,177 @@ concurrency: cancel-in-progress: true jobs: - linting: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - - - name: Setup Python - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.10" - enable-cache: true - cache-dependency-glob: pyproject.toml - - - name: Run Ruff Lint - run: uv run --only-group style ruff check . - - - name: Run Ruff Format - run: uv run --only-group style ruff format --check . - - - name: Run mdformat - run: uv run --only-group style mdformat --check *.md docs/**/*.md - - type-check: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - - - name: Setup Python - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.10" - enable-cache: true - cache-dependency-glob: pyproject.toml - - - name: Run MyPy - run: uv run --group lint --group test mypy . - - test: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, macos-latest] # eventually add `windows-latest` - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - - env: - COVERAGE_ARGS: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' && '--cov=src --cov-append' || '--no-cov' }} - GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v5 - - - name: Setup Python - uses: astral-sh/setup-uv@v7 - with: - python-version: ${{ matrix.python-version }} - enable-cache: true - cache-dependency-glob: pyproject.toml - - - name: Install Test Environment - run: uv sync --no-default-groups --group test - - - name: Install Geth - uses: ApeWorX/geth-action@v1 - - - name: Cache Solidity Compilers - uses: actions/cache@v5 - with: - path: ~/.solcx - key: ${{ runner.os }}-solcx-${{ hashFiles('pyproject.toml', 'tests/**/*.sol') }} - restore-keys: | - ${{ runner.os }}-solcx- - - - name: Install Solidity Compilers - run: | - uv run --no-sync python -c " - import re - from pathlib import Path - - from ape.utils import pragma_str_to_specifier_set - from solcx import get_installable_solc_versions, install_solc - - def get_pragma_spec_from_path(path): - source = path.read_text(encoding='utf8') - pragma_match = next( - re.finditer(r'(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)', source), - None, - ) - return pragma_str_to_specifier_set(pragma_match.groups()[0]) if pragma_match else None - - def select_version(pragma_spec, options): - choices = sorted(pragma_spec.filter(options), reverse=True) - return choices[0] if choices else None - - installable_versions = get_installable_solc_versions() - versions = set() - for path in Path('tests').rglob('*.sol'): - pragma_spec = get_pragma_spec_from_path(path) - if pragma_spec and (version := select_version(pragma_spec, installable_versions)): - versions.add(str(version)) - - versions = sorted(versions) - print('Installing Solidity compilers:', ', '.join(versions) or 'none') - for version in versions: - install_solc(version, show_progress=False) - " - - - name: Cache Ape Packages - uses: actions/cache@v5 - with: - path: ~/.ape/packages - key: ${{ runner.os }}-ape-packages-${{ hashFiles('pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-ape-packages- - - - name: Install Project Dependencies - run: uv run --no-sync ape pm install - - - name: Run Functional Tests - run: | - uv run --no-sync ape test \ - -s -v ERROR \ - -n auto --dist loadgroup \ - -m "not hypothesis" \ - $COVERAGE_ARGS \ - tests/functional - - - name: Run Integration Tests - run: | - uv run --no-sync ape test \ - -s -v ERROR \ - -n auto --dist loadgroup \ - -m "not hypothesis" \ - $COVERAGE_ARGS \ - tests/integration - - - name: Run Performance Tests - run: | - uv run --no-sync ape test \ - -s -v ERROR \ - tests/performance - - fuzzing: - runs-on: ubuntu-latest - - strategy: - fail-fast: true - - steps: - - uses: actions/checkout@v5 - - - name: Setup Python - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.10" - enable-cache: true - cache-dependency-glob: pyproject.toml - - - name: Install Test Environment - run: uv sync --no-default-groups --group test - - - name: Run Tests - run: uv run --no-sync ape test -s -m "hypothesis" --no-cov + linting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Setup Python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.10" + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Run Ruff + run: | + echo "=== Ruff Version ===" + uv run --only-group style ruff --version + echo "=== Ruff Configuration ===" + uv run --only-group style ruff config + echo "=== Running Ruff Check ===" + uv run --only-group style ruff check . + echo "=== Running Ruff Format ===" + uv run --only-group style ruff format --check . + + - name: Run Mdformat + run: | + echo "=== Mdformat Version ===" + uv run --only-group style mdformat --version + echo "=== Running Mdformat ===" + uv run --only-group style mdformat --check *.md docs/**/*.md + + type-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Setup Python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.10" + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Run MyPy + run: | + echo "=== Mypy Version ===" + uv run --group lint --group test mypy --version + echo "=== Running Mypy ===" + uv run --group lint --group test mypy . + + test: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, macos-latest] # eventually add `windows-latest` + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + env: + COVERAGE_ARGS: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' && '--cov=src --cov-append' || '--no-cov' }} + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v5 + + - name: Setup Python + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Install Test Environment + run: uv sync --no-default-groups --group test + + - name: Install Geth + uses: ApeWorX/geth-action@v1 + + - name: Cache Solidity Compilers + uses: actions/cache@v5 + with: + path: ~/.solcx + key: ${{ runner.os }}-solcx-${{ hashFiles('pyproject.toml', 'tests/**/*.sol') }} + restore-keys: | + ${{ runner.os }}-solcx- + + - name: Install Solidity Compilers + run: | + uv run --no-sync python -c " + import re + from pathlib import Path + + from ape.utils import pragma_str_to_specifier_set + from solcx import get_installable_solc_versions, install_solc + + def get_pragma_spec_from_path(path): + source = path.read_text(encoding='utf8') + pragma_match = next( + re.finditer(r'(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)', source), + None, + ) + return pragma_str_to_specifier_set(pragma_match.groups()[0]) if pragma_match else None + + def select_version(pragma_spec, options): + choices = sorted(pragma_spec.filter(options), reverse=True) + return choices[0] if choices else None + + installable_versions = get_installable_solc_versions() + versions = set() + for path in Path('tests').rglob('*.sol'): + pragma_spec = get_pragma_spec_from_path(path) + if pragma_spec and (version := select_version(pragma_spec, installable_versions)): + versions.add(str(version)) + + versions = sorted(versions) + print('Installing Solidity compilers:', ', '.join(versions) or 'none') + for version in versions: + install_solc(version, show_progress=False) + " + + - name: Cache Ape Packages + uses: actions/cache@v5 + with: + path: ~/.ape/packages + key: ${{ runner.os }}-ape-packages-${{ hashFiles('pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-ape-packages- + + - name: Install Project Dependencies + run: uv run --no-sync ape pm install + + - name: Run Functional Tests + run: | + uv run --no-sync ape test \ + -s -v ERROR \ + -n auto --dist loadgroup \ + -m "not hypothesis" \ + $COVERAGE_ARGS \ + tests/functional + + - name: Run Integration Tests + run: | + uv run --no-sync ape test \ + -s -v ERROR \ + -n auto --dist loadgroup \ + -m "not hypothesis" \ + $COVERAGE_ARGS \ + tests/integration + + - name: Run Performance Tests + run: | + uv run --no-sync ape test \ + -s -v ERROR \ + tests/performance + + fuzzing: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + + steps: + - uses: actions/checkout@v5 + + - name: Setup Python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.10" + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Run Tests + run: uv run --only-group test ape test -s -m "hypothesis" --no-cov diff --git a/.gitignore b/.gitignore index 63dc15a5ec..bb5fdd6738 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,9 @@ ipython_config.py # pyenv .python-version +# uv pkg mngr +uv.lock + # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da0dc1f106..cc7d6bb7cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,18 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 # Latest ruff version + rev: v0.15.12 # Latest ruff version hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v2.3.1 hooks: - id: mypy additional_dependencies: [ @@ -27,7 +27,7 @@ repos: ] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.22 + rev: 1.0.0 hooks: - id: mdformat additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject] diff --git a/docs/userguides/accounts.md b/docs/userguides/accounts.md index 21c9f27ad0..ec6ee44071 100644 --- a/docs/userguides/accounts.md +++ b/docs/userguides/accounts.md @@ -51,7 +51,9 @@ You can also change settings at run-time, such as the mnemonic: ```python from ape import accounts -accounts.test_accounts.mnemonic = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" +accounts.test_accounts.mnemonic = ( + "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" +) print(accounts.test_accounts[0]) # 0x627306090abaB3A6e1400e9345bC60c78a8BEf57 ``` @@ -91,14 +93,14 @@ account.balance += int(1e18) # Gives `account` 1 Ether In order to eliminate the usage of sender in contract calls, you can use `use_sender` context manager. ```python -with accounts.use_sender(0): # Use first account from test mnemonic - contract.myFunction(1) +with accounts.use_sender(0): # Use first account from test mnemonic + contract.myFunction(1) -with accounts.use_sender("
"): # Impersonate an account - contract.myFunction(1) +with accounts.use_sender("
"): # Impersonate an account + contract.myFunction(1) -with accounts.use_sender(a): # a is a `TestAccountAPI` object - contract.myFunction(1) +with accounts.use_sender(a): # a is a `TestAccountAPI` object + contract.myFunction(1) ``` ## Live Network Accounts @@ -165,8 +167,8 @@ from ape_accounts import generate_account account, mnemonic = generate_account("my-account", "mySecureP@ssphrase") -print(f'Save your mnemonic: {mnemonic}') -print(f'Your new account address is: {account.address}') +print(f"Save your mnemonic: {mnemonic}") +print(f"Your new account address is: {account.address}") ``` See the [documentation for `generate_account()`](../methoddocs/ape_accounts.html#ape_accounts.generate_account) for more options. @@ -209,7 +211,7 @@ mnemonic = "test test test test test test test test test test test junk" account = import_account_from_mnemonic(alias, passphrase, mnemonic) -print(f'Your imported account address is: {account.address}') +print(f"Your imported account address is: {account.address}") ``` Or using a raw private key [using `import_account_from_private_key()`](../methoddocs/ape_accounts.html#ape_accounts.import_account_from_private_key): @@ -224,7 +226,7 @@ private_key = os.urandom(32).hex() account = import_account_from_private_key(alias, passphrase, private_key) -print(f'Your imported account address is: {account.address}') +print(f"Your imported account address is: {account.address}") ``` #### Exporting Accounts @@ -252,16 +254,16 @@ In order to reduce repetition of adding `sender` in your contract calls, you can ```python with accounts.use_sender(0): - contract.myFunction(1) + contract.myFunction(1) with accounts.use_sender("
"): - contract.myFunction(1) + contract.myFunction(1) with accounts.use_sender(""): - contract.myFunction(1) + contract.myFunction(1) -with accounts.use_sender(a): # a is a `AccountAPI` object - contract.myFunction(1) +with accounts.use_sender(a): # a is a `AccountAPI` object + contract.myFunction(1) ``` ## Signing Messages @@ -304,10 +306,12 @@ from eip712.messages import EIP712Message, EIP712Domain from eth_pydantic_types import abi from pydantic import BaseModel + class Person(BaseModel): name: abi.string wallet: abi.address + class Mail(EIP712Message): eip712_domain = EIP712Domain( name="Ether Mail", @@ -319,6 +323,7 @@ class Mail(EIP712Message): sender: Person receivers: list[Person] + alice = Person(name="Alice", wallet="0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826") bob = Person("Bob", "0xB0B0b0b0b0b0B000000000000000000000000000") charlie = Person("Charlie", "0xC0C0c0c0c0c0C000000000000000000000000000") diff --git a/docs/userguides/clis.md b/docs/userguides/clis.md index 2d4cce2ba9..8e081bcbfc 100644 --- a/docs/userguides/clis.md +++ b/docs/userguides/clis.md @@ -40,17 +40,20 @@ In Ape, it is easy to extend the CLI context object and use the extended version from ape.cli import ApeCliContextObject, ape_cli_context import click + class MyManager: - """My custom manager.""" + """My custom manager.""" + class CustomContext(ApeCliContextObject): - my_manager: MyManager = MyManager() - """Add new managers to your custom context""" - - @property - def signer(self): - """Utilize existing managers in your custom context.""" - return self.account_manager.load("my_account") + my_manager: MyManager = MyManager() + """Add new managers to your custom context""" + + @property + def signer(self): + """Utilize existing managers in your custom context.""" + return self.account_manager.load("my_account") + @click.command() @ape_cli_context(obj_type=CustomContext) @@ -82,19 +85,21 @@ Use `ecosystem`, `network`, and `provider` argument names in your command implem import click from ape.cli import network_option + @click.command() @network_option() def cmd(provider): - # This command only needs the provider. - click.echo(provider.name) + # This command only needs the provider. + click.echo(provider.name) + @click.command() @network_option() def cmd_2(ecosystem, network, provider): - # This command uses all parts of the parsed network choice. - click.echo(ecosystem.name) - click.echo(network.name) - click.echo(provider.name) + # This command uses all parts of the parsed network choice. + click.echo(ecosystem.name) + click.echo(network.name) + click.echo(provider.name) ``` The [ConnectedProviderCommand](../methoddocs/cli.html#ape.cli.commands.ConnectedProviderCommand) automatically uses the `--network` option and connects to the network before any of your code executes and then disconnects afterward. @@ -105,22 +110,26 @@ Additionally, specify `ecosystem`, `network`, or `provider` in your command func import click from ape.cli import ConnectedProviderCommand + @click.group() def cli(): pass + @cli.command(cls=ConnectedProviderCommand) def cmd_1(network, provider): - click.echo(network.name) - click.echo(provider.is_connected) # True + click.echo(network.name) + click.echo(provider.is_connected) # True + @cli.command(cls=ConnectedProviderCommand) def cmd_2(provider): - click.echo(provider.is_connected) # True + click.echo(provider.is_connected) # True + @cli.command(cls=ConnectedProviderCommand) def cmd_3(): - click.echo("Using params from ConnectedProviderCommand is optional") + click.echo("Using params from ConnectedProviderCommand is optional") ``` ## Account Tools @@ -168,8 +177,8 @@ from ape.cli import select_account @click.command() def cmd(): - account = select_account("Select an account to use") - click.echo(f"You selected {account.address}.") + account = select_account("Select an account to use") + click.echo(f"You selected {account.address}.") ``` Similarly, there are a couple custom arguments for aliases alone that are useful when making CLIs for account creation. @@ -207,15 +216,18 @@ from ape_accounts.accounts import KeyfileAccount # NOTE: This is just an example and not anything specific or recommended. APPLICATION_PREFIX = "" + @click.command() @existing_alias_argument(account_type=KeyfileAccount) def cli_0(alias): - pass + pass + @click.command() @existing_alias_argument(account_type=lambda a: a.alias.startswith(APPLICATION_PREFIX)) def cli_1(alias): - pass + pass + # Select from the given accounts directly. my_accounts = [accounts.load("me"), accounts.load("me2")] @@ -235,10 +247,11 @@ import click from ape.cli import contract_file_paths_argument + @click.command() @contract_file_paths_argument() def cli(file_paths: set[Path]): - # Loop through all source files given (or all source files in the project). + # Loop through all source files given (or all source files in the project). for path in file_paths: click.echo(f"Source found: {path}") ``` diff --git a/docs/userguides/compile.md b/docs/userguides/compile.md index 67bfdf7460..2108b981fb 100644 --- a/docs/userguides/compile.md +++ b/docs/userguides/compile.md @@ -112,9 +112,7 @@ from pathlib import Path from ape import compilers settings = {"vyper": {"version": "0.3.7"}, "solidity": {"version": "0.8.0"}} -compilers.compile( - ["path/to/contract.vy", "path/to/contract.sol"], settings=settings -) +compilers.compile(["path/to/contract.vy", "path/to/contract.sol"], settings=settings) # Or, more explicitly: vyper = compilers.get_compiler("vyper", settings=settings["vyper"]) @@ -136,10 +134,10 @@ CODE = """ """ container = compilers.compile_source( - "vyper", - CODE, - settings={"vyper": {"version": "0.3.7"}}, - contractName="MyContract", + "vyper", + CODE, + settings={"vyper": {"version": "0.3.7"}}, + contractName="MyContract", ) owner = accounts.test_accounts[0] diff --git a/docs/userguides/console.md b/docs/userguides/console.md index e7b04e4d79..dc62b195d9 100644 --- a/docs/userguides/console.md +++ b/docs/userguides/console.md @@ -59,6 +59,7 @@ An example file might look something like this: from ape import networks from eth_utils import encode_hex, decode_hex + def latest(key): return getattr(networks.active_provider.get_block("latest"), key) ``` @@ -66,11 +67,11 @@ def latest(key): Then both imported util functions and `latest()` will be available when you launch the console. ```python -In [1]: latest('number') +In[1]: latest("number") Out[1]: 14388241 -In [2]: encode_hex(latest('hash')) -Out[2]: '0x68f768988e9bd4be971d527f72483f321975fa52aff9692b6d0e0af71fb77aaf' +In[2]: encode_hex(latest("hash")) +Out[2]: "0x68f768988e9bd4be971d527f72483f321975fa52aff9692b6d0e0af71fb77aaf" ``` ### Init Function @@ -85,7 +86,7 @@ def ape_init_extras(chain): Then `web3` will be available to use immediately. ```python -In [1]: web3.eth.chain_id +In[1]: web3.eth.chain_id Out[1]: 1 ``` diff --git a/docs/userguides/contract_metadata.md b/docs/userguides/contract_metadata.md index 7b25c1f30d..34f51b73ad 100644 --- a/docs/userguides/contract_metadata.md +++ b/docs/userguides/contract_metadata.md @@ -52,6 +52,7 @@ def is_creator(contract, address) -> bool: metadata = contract.creation_metadata return metadata and metadata.deployer == address + # Historical queries at contract inception creation_block = contract.creation_metadata.block initial_balance = contract.balance(block_number=creation_block) @@ -111,13 +112,7 @@ metadata = chain.contracts.get_creation_metadata(address) # Store custom metadata chain.contracts.cache_contract_creation( - address, - ContractCreation( - txn_hash="0xabc...", - block=12345, - deployer="0xdef...", - factory=None - ) + address, ContractCreation(txn_hash="0xabc...", block=12345, deployer="0xdef...", factory=None) ) ``` diff --git a/docs/userguides/contracts.md b/docs/userguides/contracts.md index 2a99d8fdf8..a97f1a0ec6 100644 --- a/docs/userguides/contracts.md +++ b/docs/userguides/contracts.md @@ -99,7 +99,6 @@ You can also use the [at() method](../methoddocs/contracts.html#ape.contracts.ba from ape import project contract = project.MyContract.at("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45") - ``` By default, Ape will detect if the contract is a proxy and use the implementation contract's interface. See the [Proxy Contracts guide](./proxy.html) for more details on proxy handling. @@ -154,8 +153,7 @@ contract = Contract(address, abi="abi.json") # Using a Python dictionary from JSON: contract = Contract( - address, - abi=[{"name":"foo","type":"fallback", "stateMutability":"nonpayable"}] + address, abi=[{"name": "foo", "type": "fallback", "stateMutability": "nonpayable"}] ) ``` @@ -172,8 +170,9 @@ You could then refer back to it like so: ```python from ape import project, chain + def main(): - my_contract = chain.contracts.get_deployments(project.MyContract)[-1] + my_contract = chain.contracts.get_deployments(project.MyContract)[-1] ``` or @@ -461,7 +460,7 @@ import ape usdc = ape.Contract("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") # ABI type for a hex method ID -assert usdc.identifier_lookup['0x70a08231'].selector == 'balanceOf(address)' +assert usdc.identifier_lookup["0x70a08231"].selector == "balanceOf(address)" # Also, selectors from method and event signatures assert usdc.selector_identifiers["balances(address)"] == "0x27e235e3" @@ -486,9 +485,13 @@ Here is an example of how you can use the multicall module: import ape from ape_ethereum import multicall -ADDRESSES = ("0xF4b8A02D4e8D76070bD7092B54D2cBbe90fa72e9", "0x80067013d7F7aF4e86b3890489AcAFe79F31a4Cb") +ADDRESSES = ( + "0xF4b8A02D4e8D76070bD7092B54D2cBbe90fa72e9", + "0x80067013d7F7aF4e86b3890489AcAFe79F31a4Cb", +) POOLS = [ape.project.IPool.at(a) for a in ADDRESSES] + def main(): # Use multi-call. call = multicall.Call() diff --git a/docs/userguides/dependencies.md b/docs/userguides/dependencies.md index 411e158592..26bcf1a86f 100644 --- a/docs/userguides/dependencies.md +++ b/docs/userguides/dependencies.md @@ -209,7 +209,7 @@ You can also use Python to install dependencies, using `**kwargs` as the same fi from ape import project project.dependencies.install( - github="OpenZeppelin/openzeppelin-contracts", name="openzeppelin", version="4.9.6" + github="OpenZeppelin/openzeppelin-contracts", name="openzeppelin", version="4.9.6" ) ``` @@ -367,7 +367,7 @@ from ape import accounts, project # NOTE: This will compile the dependency dependency_project = project.dependencies["my_dependency"]["1.0.0"] -dependency_contract = dependency_project.DependencyContractType +dependency_contract = dependency_project.DependencyContractType my_account = accounts.load("alias") deployed_contract = my_account.deploy(dependency_contract, "argument") print(deployed_contract.address) diff --git a/docs/userguides/developing_plugins.md b/docs/userguides/developing_plugins.md index 250dd23f3a..4893d9dd3a 100644 --- a/docs/userguides/developing_plugins.md +++ b/docs/userguides/developing_plugins.md @@ -44,9 +44,9 @@ from web3 import Web3, HTTPProvider class MyProvider(ProviderAPI): _web3: Web3 = None # type: ignore - + def connect(self): - self._web3 = Web3(HTTPProvider(str("https://localhost:1337"))) + self._web3 = Web3(HTTPProvider(str("https://localhost:1337"))) """Implement rest of abstract methods""" ``` @@ -58,12 +58,13 @@ Once you have finished implementing your API classes, you need to register them ```python from ape import plugins + # Here, we register our provider plugin so we can use it in 'ape'. @plugins.register(plugins.ProviderPlugin) def providers(): # NOTE: By keeping this import local, we avoid slower plugin load times. from ape_my_plugin.provider import MyProvider - + # NOTE: 'MyProvider' defined in a prior code-block. yield "ethereum", "local", MyProvider ``` @@ -87,6 +88,7 @@ To create a CLI plugin, create any type of `click` command (such as a `click.gro ```python import click + @click.group def cli(): """My custom commands.""" @@ -103,11 +105,13 @@ Then, register it using `entrypoints`, which is a built-in python registry of it ```python ... -entry_points={ - "ape_cli_subcommands": [ - "ape_myplugin=ape_myplugin._cli:cli", - ], -}, +entry_points = ( + { + "ape_cli_subcommands": [ + "ape_myplugin=ape_myplugin._cli:cli", + ], + }, +) ... ``` @@ -125,10 +129,11 @@ Follow [this guide](./clis.html) to learn more about what you can do with the ut import click from ape.cli import existing_alias_argument + @click.command() @existing_alias_argument() def my_cmd(alias): - click.echo(f"{alias} is an existing account!") + click.echo(f"{alias} is an existing account!") ``` ## Using Plugins @@ -176,8 +181,10 @@ import click def cli(): pass + """ Or """ + @click.command() def cli(): """Utilities for my plugin""" @@ -200,7 +207,8 @@ logger.info("This is a log message") ```python from ape.cli import ape_cli_context + @ape_cli_context() def my_command(cli_ctx): - cli_ctx.logger.info("my log message") + cli_ctx.logger.info("my log message") ``` diff --git a/docs/userguides/logging.md b/docs/userguides/logging.md index 515f7c8bb5..1830a8d548 100644 --- a/docs/userguides/logging.md +++ b/docs/userguides/logging.md @@ -54,6 +54,7 @@ You can also import and use the logger in your own Python scripts or commands: ```python from ape.logging import logger, LogLevel + def main(): logger.info("You have entered `main()`.") logger.set_level(LogLevel.WARNING) @@ -64,6 +65,7 @@ You can also use the `.at_level()` context-manager to temporarily change the log ```python from ape.logging import logger, LogLevel + def main(): with logger.at_level(LogLevel.WARNING): # An operation where you want to ensure WARN-level logs appear. @@ -75,6 +77,7 @@ You can also disable the logger in Python: ```python from ape.logging import logger, LogLevel + def main(): logger.disable() # Turns off logging entirely. with logger.disabled(): diff --git a/docs/userguides/networks.md b/docs/userguides/networks.md index cfc7c8318f..300ff42576 100644 --- a/docs/userguides/networks.md +++ b/docs/userguides/networks.md @@ -603,6 +603,7 @@ For example, if you are using a script with a default network connection, you ca ```python from ape import chain, networks + def main(): start_provider = chain.provider.name with networks.ethereum.mainnet.use_provider("node") as provider: @@ -616,6 +617,7 @@ Jump between networks to simulate multi-chain behavior. import click from ape import networks + @click.command() def cli(): with networks.polygon.mainnet.use_provider("node"): @@ -675,6 +677,7 @@ We can switch to a forked network by doing this: ```python from ape import networks + def main(): with networks.fork(provider_name="foundry"): ... diff --git a/docs/userguides/proxy.md b/docs/userguides/proxy.md index c394cb243b..571fde47cf 100644 --- a/docs/userguides/proxy.md +++ b/docs/userguides/proxy.md @@ -60,10 +60,7 @@ contract = project.MyImplementation.at( ) # Or disable proxy detection entirely if you know it's not a proxy -contract = project.MyContract.at( - "0xAddress", - detect_proxy=False -) +contract = project.MyContract.at("0xAddress", detect_proxy=False) ``` You can also use the `Contract` factory with proxy parameters: @@ -76,11 +73,8 @@ contract = Contract("0xAddress", detect_proxy=False) # Provide custom proxy info contract = Contract( - "0xProxyAddress", - proxy_info=ProxyInfoAPI( - target="0xImplementationAddress", - type_name=ProxyType.STANDARD - ) + "0xProxyAddress", + proxy_info=ProxyInfoAPI(target="0xImplementationAddress", type_name=ProxyType.STANDARD), ) ``` diff --git a/docs/userguides/scripts.md b/docs/userguides/scripts.md index 85ea67750f..84e4c3bcce 100644 --- a/docs/userguides/scripts.md +++ b/docs/userguides/scripts.md @@ -13,6 +13,7 @@ Follow [this guide](./clis.html) to learn more about what you can do with the ut ```python import click + @click.command() def cli(): print("Hello world!") @@ -46,16 +47,17 @@ from ape.cli import ConnectedProviderCommand @click.command(cls=ConnectedProviderCommand) def cli(ecosystem, network, provider): click.echo(f"Connected to {ecosystem.name}:{network.name} using provider '{provider.name}'.") - + # Access chain and other managers automatically from ape import chain + click.echo(f"Current block: {chain.blocks.height}") + @click.command(cls=ConnectedProviderCommand) def cli(network, provider): click.echo(f"You are connected to network '{network.name}'.") click.echo(provider.chain_id) - ``` ## Multi-Network Commands @@ -122,14 +124,12 @@ Here is an example of a multi-chain script: import click from ape.cli import ape_cli_context + @click.command() @ape_cli_context() def cli(cli_ctx): # There is no connection yet at this point. - testnets = { - "ethereum": ["sepolia"], - "polygon": ["amoy"] - } + testnets = {"ethereum": ["sepolia"], "polygon": ["amoy"]} nm = cli_ctx.network_manager for ecosystem_name, networks in testnets.items(): @@ -172,6 +172,7 @@ To demonstrate, use the following script: from ape import networks import click + def main(): ecosystem_name = networks.provider.network.ecosystem.name network_name = networks.provider.network.name diff --git a/docs/userguides/testing.md b/docs/userguides/testing.md index a2da06452e..1cce727b21 100644 --- a/docs/userguides/testing.md +++ b/docs/userguides/testing.md @@ -51,20 +51,24 @@ import ape import pytest import re + # SETUP PHASE # NOTE: More on fixtures is discussed in later sections of this guide! @pytest.fixture def owner(accounts): return accounts[0] + @pytest.fixture def not_owner(accounts): return accounts[1] + @pytest.fixture def my_contract(owner, project): return owner.deploy(project.MyContract) + def test_authorization(my_contract, owner, not_owner): # INVOCATION PHASE my_contract.set_owner(sender=owner) @@ -73,11 +77,11 @@ def test_authorization(my_contract, owner, not_owner): # ASSERTION PHASE with ape.reverts("!authorized"): my_contract.authorized_method(sender=not_owner) - + # You can also test against custom error types with ape.reverts(my_contract.Unauthorized): my_contract.authorized_method(sender=not_owner) - + # Or match error patterns with regex with ape.reverts(re.compile(r"^!auth.*")): my_contract.authorized_method(sender=not_owner) @@ -111,6 +115,7 @@ For code readability and sustainability, create your own fixtures using the `acc ```python import pytest + @pytest.fixture def owner(accounts): return accounts[0] @@ -121,8 +126,7 @@ def receiver(accounts): return accounts[1] -def test_my_method(owner, receiver): - ... +def test_my_method(owner, receiver): ... ``` You can configure your accounts by changing the `mnemonic`, `number_of_accounts`, and `balance` in the `test` section of your `ape-config.yaml` file: @@ -208,10 +212,12 @@ You also have access to the `project` you are testing. You will need this to dep ```python import pytest + @pytest.fixture def owner(accounts): return accounts[0] + @pytest.fixture def my_contract(project, owner): # ^ use the 'project' fixture from the 'ape-test' plugin @@ -252,6 +258,7 @@ Then, during `test_my_contract_1()`, instead of deploying again, it uses the cac ```python import pytest + @pytest.fixture(scope="session") def my_contract(accounts, project): owner = accounts[0] @@ -260,12 +267,14 @@ def my_contract(accounts, project): contract.initialize(sender=owner) return contract -def test_something_else(): - ... + +def test_something_else(): ... + def test_my_contract_0(my_contract): my_contract.myMethod() + def test_my_contract_1(my_contract): my_contract.myMethod() ``` @@ -293,6 +302,7 @@ If you are using chain-isolation and have a higher-scoped fixture that you know import ape from ape_tokens import tokens + @ape.fixture(scope="session", chain_isolation=False, params=("WETH", "DAI", "BAT")) def token_addresses(request): return tokens[request].address @@ -408,6 +418,7 @@ From our earlier example we can see this in action: ```python import ape + def test_authorization(my_contract, owner, not_owner): my_contract.set_owner(sender=owner) assert owner == my_contract.owner() @@ -451,6 +462,7 @@ Take for example: ```python # @version 0.4.3 + @external def check_value(_value: uint256) -> bool: assert _value != 0 # dev: invalid value @@ -462,6 +474,7 @@ We can explicitly cause a transaction revert and check the failed line by supply ```python import ape + def test_authorization(my_contract, owner): with ape.reverts(dev_message="dev: invalid value"): my_contract.check_value(sender=owner) @@ -516,6 +529,7 @@ Similarly, if you require dev assertions for non-reentrant functions you must be def _foo_internal(): # dev: correct location pass + @external @nonreentrant def foo(): @@ -535,18 +549,22 @@ We can ensure unauthorized withdraws are disallowed by writing the following tes import ape import pytest + @pytest.fixture def owner(accounts): return accounts[0] + @pytest.fixture def hacker(accounts): return accounts[1] + @pytest.fixture def contract(owner, project): return owner.deploy(project.MyContract) + def test_unauthorized_withdraw(contract, hacker): with ape.reverts(contract.Unauthorized, unauth_address=hacker.address): contract.withdraw(sender=hacker) @@ -557,6 +575,7 @@ You can also use custom error types from the contract container (from `ape.proje ```python import ape + def test_unauthorized(contract, hacker, project): with ape.reverts(project.MyContract.Unauthorized, unauth_address=hacker.address): contract.withdraw(sender=hacker) @@ -568,6 +587,7 @@ Here is an example of what that may look like: ```python import ape + def test_error_on_deploy(account, project): with ape.reverts(project.Token.MyCustomError): ape.project.HasError.deploy(sender=account) @@ -582,14 +602,15 @@ The address will only exist for transactions that were published (e.g. not for f ```python import ape + def test_error_on_deploy(account): # NOTE: We are using `as rev` here to capture the revert info # so we can attempt to lookup the contract later. with ape.reverts() as rev: ape.project.HasError.deploy(sender=account) - + assert rev.value.address is not None, "Receipt never found, contract never cached" - + # Grab the cached instance using the error's address # and assert the custom error this way. contract = ape.Contract(rev.value.address) @@ -605,10 +626,12 @@ To run an entire test using a specific network / provider combination, use the ` ```python import pytest + @pytest.mark.use_network("fantom:local:test") def test_my_fantom_test(chain): assert chain.provider.network.ecosystem.name == "fantom" + @pytest.mark.use_network("ethereum:local:test") def test_my_ethereum_test(chain): assert chain.provider.network.ecosystem.name == "ethereum" @@ -627,7 +650,7 @@ def test_my_multichain_test(networks): # You can also use the context manager like this: with networks.parse_network_choice("fantom:local:test") as provider: - assert provider.network.ecosystem.name == "fantom" + assert provider.network.ecosystem.name == "fantom" ``` You can also set the network context in a pytest fixture. @@ -636,11 +659,13 @@ This is useful if certain fixtures must run in certain networks. ```python import pytest + @pytest.fixture def stark_contract(networks, project): with networks.parse_network_choice("starknet:local"): yield project.MyStarknetContract.deploy() + def test_starknet_thing(stark_contract, stark_account): # Uses the starknet connection via the stark_contract fixture receipt = stark_contract.my_method(sender=stark_account) diff --git a/docs/userguides/trace.md b/docs/userguides/trace.md index df0c402ee0..50b52b1617 100644 --- a/docs/userguides/trace.md +++ b/docs/userguides/trace.md @@ -26,7 +26,9 @@ To learn more about networks in Ape, see the [networks guide](./networks.html). ```python from ape import chain -tx = chain.provider.get_receipt('0xb7d7f1d5ce7743e821d3026647df486f517946ef1342a1ae93c96e4a8016eab7') +tx = chain.provider.get_receipt( + "0xb7d7f1d5ce7743e821d3026647df486f517946ef1342a1ae93c96e4a8016eab7" +) # Show the steps the transaction took. tx.show_trace() diff --git a/docs/userguides/transactions.md b/docs/userguides/transactions.md index 800b14bd80..28e106f4d7 100644 --- a/docs/userguides/transactions.md +++ b/docs/userguides/transactions.md @@ -47,6 +47,7 @@ The following example demonstrates a simple deployment script: ```python from ape import accounts, project + def deploy(): account = accounts.load("MyAccount") # Assume you have a contract named `MyContract` in your project's contracts folder. @@ -64,9 +65,9 @@ ape console --network ethereum:sepolia:alchemy This will launch an IPython shell: ```python -In [1]: dev = accounts.load("dev") -In [2]: token = dev.deploy(project.Token) -In [3]: token.contract_method_defined_in_contract() +In[1]: dev = accounts.load("dev") +In[2]: token = dev.deploy(project.Token) +In[3]: token.contract_method_defined_in_contract() ``` For an in depth tutorial on how to deploy, please visit [ApeAcademy](https://academy.apeworx.io/). @@ -104,14 +105,18 @@ Calling certain methods on a deployed-contract is one way to transact. ```python contract = deploy() # Example from above, that returns a contract instance. -contract.fundMyContract(value="1 gwei", sender=sender) # Assuming there is a method named 'fundMyContract' on MyContract. +contract.fundMyContract( + value="1 gwei", sender=sender +) # Assuming there is a method named 'fundMyContract' on MyContract. ``` In the example above, the call to `fundMyContract()` invokes a dynamic-fee transaction. To have more control of the fee-values, you can specify the `max_fee`, the `max_priority_fee`, or both. ```python -contract.fundMyContract(value="1 gwei", max_priority_fee="50 gwei", max_fee="100 gwei", sender=sender) +contract.fundMyContract( + value="1 gwei", max_priority_fee="50 gwei", max_fee="100 gwei", sender=sender +) ``` The `max_priority_fee` cannot exceed the `max_fee`, as the `max_fee` includes both the base fee and the priority fee. diff --git a/pyproject.toml b/pyproject.toml index 3c33fd8674..64fa338216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,21 +127,21 @@ test = [ # `test` GitHub Action jobs use this "ape-solidity>=0.8.5,<0.9", # Needed for compiling test contracts ] style = [ # `linting` GitHub Action job uses this - "ruff>=0.12.0", # Unified linter and formatter - "mdformat>=0.7.22", # Auto-formatter for markdown - "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown - "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates - "mdformat-pyproject>=0.0.2", # Allows configuring in pyproject.toml + "ruff>=0.15", # Unified linter and formatter + "mdformat>=1.0", # Auto-formatter for markdown + "mdformat-gfm", # Needed for formatting GitHub-flavored markdown + "mdformat-frontmatter", # Needed for frontmatters-style headers in issue templates + "mdformat-pyproject", # Allows configuring in pyproject.toml ] lint = [ # `type-check` GitHub Action job uses this {include-group = "style"}, - "mypy>=1.16.1,<1.17.0", # Static type analyzer + "mypy>=2.3,<3", # Static type analyzer "types-PyYAML", # Needed due to mypy typeshed "types-requests", # Needed due to mypy typeshed "types-setuptools", # Needed due to mypy typeshed - "pandas-stubs>=2.2.1.240316", # Needed due to mypy typeshed + "pandas-stubs", # Needed due to mypy typeshed "types-toml", # Needed due to mypy typeshed - "types-SQLAlchemy>=1.4.49", # Needed due to mypy typeshed + "types-SQLAlchemy", # Needed due to mypy typeshed "types-python-dateutil", # Needed due to mypy typeshed ] docs = [ # `docs` GitHub Action jobs use this diff --git a/src/ape/_cli.py b/src/ape/_cli.py index c2efb35466..8a5c3b9406 100644 --- a/src/ape/_cli.py +++ b/src/ape/_cli.py @@ -64,7 +64,7 @@ def format_commands(self, ctx, formatter) -> None: # NOTE: Avoid issue with empty sections (now Plugins installed) continue - with formatter.section(gettext(f"{section_name} Commands")): + with formatter.section(gettext(f"{section_name} Commands")): # noqa: INT001 formatter.write_dl(deflist) def invoke(self, ctx) -> Any: @@ -97,10 +97,9 @@ def invoke(self, ctx) -> Any: raise Abort.from_ape_exception(err) from err def _suggest_cmd(self, ctx, usage_error): - if usage_error.message is None: - raise usage_error - - elif not (match := re.match("No such command '(.*)'.", usage_error.message)): + if usage_error.message is None or not ( + match := re.match("No such command '(.*)'.", usage_error.message) + ): raise usage_error groups = match.groups() @@ -111,11 +110,10 @@ def _suggest_cmd(self, ctx, usage_error): suggested_commands = difflib.get_close_matches( bad_arg, self.list_commands(ctx), cutoff=_DIFFLIB_CUT_OFF ) - if suggested_commands: - if bad_arg not in suggested_commands: - usage_error.message = ( - f"No such command '{bad_arg}'. Did you mean {' or '.join(suggested_commands)}?" - ) + if suggested_commands and bad_arg not in suggested_commands: + usage_error.message = ( + f"No such command '{bad_arg}'. Did you mean {' or '.join(suggested_commands)}?" + ) raise usage_error @@ -139,13 +137,13 @@ def get_command(self, ctx, name) -> click.Command | None: try: return ep.load() - except Exception as err: + except Exception as err: # noqa: BLE001 logger.warn_from_exception(err, f"Unable to load CLI endpoint for plugin 'ape_{name}'") # NOTE: don't return anything so Click displays proper error return None -@click.command(cls=ApeCLI, context_settings=dict(help_option_names=["-h", "--help"])) +@click.command(cls=ApeCLI, context_settings={"help_option_names": ["-h", "--help"]}) @ape_cli_context() @click.version_option(message="%(version)s", package_name="eth-ape") @click.option( diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py index d7df62f45b..8aef97f4c0 100644 --- a/src/ape/api/accounts.py +++ b/src/ape/api/accounts.py @@ -381,7 +381,7 @@ def deploy( self.local_project.deployments.track(instance) try: self.provider.network.publish_contract(address) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Contract was deployed but explorer verification failed: {err}") instance.base_path = contract.base_path or self.local_project.path diff --git a/src/ape/api/address.py b/src/ape/api/address.py index d29694ba27..5ace06571e 100644 --- a/src/ape/api/address.py +++ b/src/ape/api/address.py @@ -209,10 +209,9 @@ def as_transaction(self, **kwargs) -> "TransactionAPI": tx = self.provider.network.ecosystem.create_transaction( receiver=self.address, **converted_kwargs ) - if sender := kwargs.get("sender"): - if hasattr(sender, "prepare_transaction"): - prepared = sender.prepare_transaction(tx) - return (sender.sign_transaction(prepared) or prepared) if sign else prepared + if (sender := kwargs.get("sender")) and hasattr(sender, "prepare_transaction"): + prepared = sender.prepare_transaction(tx) + return (sender.sign_transaction(prepared) or prepared) if sign else prepared return tx diff --git a/src/ape/api/compiler.py b/src/ape/api/compiler.py index dcbd6a31c8..328a8e72cd 100644 --- a/src/ape/api/compiler.py +++ b/src/ape/api/compiler.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import TYPE_CHECKING +from pydantic import Field + from ape.exceptions import APINotImplementedError, ContractLogicError from ape.utils.basemodel import BaseInterfaceModel from ape.utils.misc import log_instead_of_fail, raises_not_implemented @@ -32,7 +34,7 @@ class CompilerAPI(BaseInterfaceModel): this API. """ - compiler_settings: dict = {} + compiler_settings: dict = Field(default_factory=dict) """ Adhoc compiler settings. """ @@ -195,7 +197,7 @@ def supports_source_tracing(self) -> bool: self.trace_source(None, None, None) # type: ignore except APINotImplementedError: return False - except Exception: + except Exception: # noqa: BLE001 # Task failed successfully. return True diff --git a/src/ape/api/config.py b/src/ape/api/config.py index 940b4a3af4..e9e58a3f03 100644 --- a/src/ape/api/config.py +++ b/src/ape/api/config.py @@ -113,7 +113,7 @@ def _find_plugin_config_problems( if plugin_name in raw_global_data: try: cls.model_validate(raw_global_data[plugin_name]) - except Exception: + except Exception: # noqa: BLE001 if problems := cls._find_plugin_config_problems_from_file( err, ape.config_manager.DATA_FOLDER ): @@ -425,7 +425,7 @@ def deployments(self) -> dict[str, dict[str, list[DeploymentConfig]]]: raise ConfigError(f"Invalid ecosystem '{ecosystem_name}' in deployments config.") ecosystem = self.network_manager.ecosystems[ecosystem_name] - for network_name, network_deploys in ecosystem_deploys.items(): + for network_name in ecosystem_deploys: if network_name not in ecosystem.networks: raise ConfigError( f"Invalid network '{ecosystem_name}:{network_name}' in deployments config." diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index dcb0c78767..e9caed2d41 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -8,7 +8,7 @@ from eth_pydantic_types import HexBytes from eth_utils import keccak from evmchains import PUBLIC_CHAIN_META -from pydantic import model_validator +from pydantic import Field, model_validator from ape.exceptions import ( CustomError, @@ -77,8 +77,8 @@ def _validate_type_name(cls, model): # Not sure. try: model["type_name"] = f"{_type}" - except Exception: - pass + except Exception as err: # noqa: BLE001 + logger.debug(str(err)) return model @@ -110,7 +110,7 @@ class EcosystemAPI(ExtraAttributesMixin, BaseInterfaceModel): # TODO: In 0.9, make @property that returns value from config, # and use REQUEST_HEADER as plugin-defined constants. - request_header: dict = {} + request_header: dict = Field(default_factory=dict) """A shareable HTTP header for network requests.""" fee_token_symbol: str @@ -280,7 +280,7 @@ def _networks_from_evmchains(self) -> dict[str, "NetworkAPI"]: if network_name not in self._networks_from_plugins } forked_networks: dict[str, ForkedNetworkAPI] = {} - for network_name, network in networks.items(): + for network_name in networks: if network_name.endswith("-fork"): # Already a fork. continue @@ -392,10 +392,9 @@ def default_network_name(self) -> str: return network networks = self.networks - if network := self.config.get("default_network"): + if (network := self.config.get("default_network")) in networks: # Default found in config. Ensure is an installed network. - if network in networks: - return network + return network if LOCAL_NETWORK_NAME in networks: # Default to the LOCAL_NETWORK_NAME, at last resort. @@ -751,9 +750,10 @@ class ProviderContextManager(ManagerAccessMixin): ... """ - connected_providers: dict[str, "ProviderAPI"] = {} - provider_stack: list[str] = [] - disconnect_map: dict[str, bool] = {} + # NOTE: pydantic v2 allows mutable defaults + connected_providers: ClassVar[dict[str, "ProviderAPI"]] = {} + provider_stack: ClassVar[list[str]] = [] + disconnect_map: ClassVar[dict[str, bool]] = {} # 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 @@ -861,7 +861,7 @@ def disconnect_all(self): provider.disconnect() self.network_manager.active_provider = None - self.connected_providers = {} + ProviderContextManager.connected_providers = {} def _connect_provider(provider: "ProviderAPI") -> "ProviderAPI": @@ -888,7 +888,7 @@ class NetworkAPI(BaseInterfaceModel): # TODO: In 0.9, make @property that returns value from config, # and use REQUEST_HEADER as plugin-defined constants. - request_header: dict = {} + request_header: dict = Field(default_factory=dict) """A shareable network HTTP header.""" # See ``.default_provider`` which is the proper field. @@ -908,11 +908,11 @@ def __repr__(self) -> str: f"{self.choice} chain_id={self.chain_id}" if chain_id is not None else self.choice ) return f"<{content}>" - except Exception: + except Exception: # noqa: BLE001 # Don't allow repr to fail. try: name = self.name - except Exception: + except Exception: # noqa: BLE001 name = None return f"<{name}>" if name else f"{type(self)}" @@ -1176,7 +1176,7 @@ def _get_plugin_providers(self): def _get_plugin_provider_names(self) -> Iterator[str]: for _, plugin_tuple in self._get_plugin_providers(): - ecosystem_name, network_name, provider_class = plugin_tuple + _ecosystem_name, _network_name, provider_class = plugin_tuple yield provider_class.__module__.split(".")[0].replace("_", "-").replace("ape-", "") def get_provider( @@ -1309,7 +1309,7 @@ def default_provider_name(self) -> str | None: elif len(self.providers) > 0: # No default set anywhere - use the first installed. - return list(self.providers)[0] + return next(iter(self.providers)) # There are no providers at all for this network. return None diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 042eeb07a5..2aca92c9ab 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -256,12 +256,12 @@ class ProviderAPI(BaseInterfaceModel): network: NetworkAPI """A reference to the network this provider provides.""" - provider_settings: dict = {} + provider_settings: dict = Field(default_factory=dict) """The settings for the provider, as overrides to the configuration.""" # TODO: In 0.9, make @property that returns value from config, # and use REQUEST_HEADER as plugin-defined constants. - request_header: dict = {} + request_header: dict = Field(default_factory=dict) """A header to set on HTTP/RPC requests.""" block_page_size: int = 100 @@ -383,7 +383,7 @@ def connection_id(self) -> str | None: try: chain_id = self.chain_id - except Exception: + except Exception: # noqa: BLE001 if chain_id := self.settings.get("chain_id"): pass @@ -1176,7 +1176,7 @@ def _disconnect_atexit(self): try: self.disconnect() - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Error while disconnecting: {err}") def disconnect(self): diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 45f91f8979..bcdc66b8a1 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -4,7 +4,7 @@ from collections.abc import Iterator from datetime import datetime as datetime_type from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, NoReturn +from typing import IO, TYPE_CHECKING, Any from eth_pydantic_types import HexBytes, HexStr from eth_utils import humanize_hexstr, is_hex, to_hex, to_int @@ -329,7 +329,7 @@ class ReceiptAPI(ExtraAttributesMixin, BaseInterfaceModel): contract_address: AddressType | None = None block_number: HexInt gas_used: HexInt - logs: list[dict] = [] + logs: list[dict] = Field(default_factory=list) status: HexInt txn_hash: HexStr transaction: TransactionAPI @@ -483,7 +483,7 @@ def decode_logs( list[:class:`~ape.types.ContractLog`] """ - def raise_for_status(self) -> NoReturn | None: + def raise_for_status(self) -> None: """ Handle provider-specific errors regarding a non-successful :class:`~api.providers.TransactionStatusEnum`. diff --git a/src/ape/cli/choices.py b/src/ape/cli/choices.py index d031ea3529..2ad51e7602 100644 --- a/src/ape/cli/choices.py +++ b/src/ape/cli/choices.py @@ -133,7 +133,7 @@ def convert(self, value: Any, param: Parameter | None, ctx: Context | None) -> A return self.choices[choice_index] - except Exception: + except Exception: # noqa: BLE001 return self.fail_from_invalid_choice(param) def fail_from_invalid_choice(self, param): diff --git a/src/ape/cli/options.py b/src/ape/cli/options.py index be5618b4e6..c501622601 100644 --- a/src/ape/cli/options.py +++ b/src/ape/cli/options.py @@ -270,7 +270,7 @@ def callback(ctx, param, value): keep_as_choice_str = param.type.base_type is str try: provider_obj = _get_provider(value, default, keep_as_choice_str) - except Exception as err: + except Exception as err: # noqa: BLE001 raise click.BadOptionUsage("--network", str(err), ctx) if provider_obj: diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index f28f3ec9af..5a7be464f1 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -269,7 +269,7 @@ def decode_input(self, calldata: bytes) -> tuple[str, dict[str, Any]]: decoded_calldata = self.provider.network.ecosystem.decode_calldata( abi, HexBytes(calldata) ) - except Exception: + except Exception: # noqa: BLE001, S112 continue if decoded_calldata: @@ -423,10 +423,9 @@ def as_transaction(self, *args, **kwargs) -> "TransactionAPI": transaction = contract_transaction.serialize_transaction(*args, **kwargs) self.provider.prepare_transaction(transaction) - if sender := kwargs.get("sender"): - if isinstance(sender, AccountAPI): - prepped_tx = sender.prepare_transaction(transaction) - return (sender.sign_transaction(prepped_tx) or prepped_tx) if sign else prepped_tx + if (sender := kwargs.get("sender")) and isinstance(sender, AccountAPI): + prepped_tx = sender.prepare_transaction(transaction) + return (sender.sign_transaction(prepped_tx) or prepped_tx) if sign else prepped_tx return transaction @@ -799,7 +798,7 @@ def range( contract = None try: contract = self.chain_manager.contracts.instance_at(contract_address) - except Exception: + except Exception: # noqa: BLE001, S110 pass # Determine the start block from contract creation metadata @@ -1196,7 +1195,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]] = {} for abi in self.contract_type.view_methods: if abi.name in view_methods: @@ -1215,7 +1214,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]] = {} for abi in self.contract_type.mutable_methods: if abi.name in mutable_methods: @@ -1348,9 +1347,8 @@ def get_error_by_signature(self, signature: str) -> type[CustomError]: raise err for contract_err in options: - if abi := getattr(contract_err, "abi", None): - if abi.signature == signature: - return contract_err + if (abi := getattr(contract_err, "abi", None)) and abi.signature == signature: + return contract_err raise err @@ -1703,7 +1701,7 @@ def deploy(self, *args, publish: bool = False, **kwargs) -> ContractInstance: self.local_project.deployments.track(instance) try: self.provider.network.publish_contract(address) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Contract was deployed but explorer verification failed: {err}") instance.base_path = self.base_path or self.local_project.contracts_folder @@ -1726,7 +1724,7 @@ def _cache_wrap(self, function: Callable) -> "ReceiptAPI": try: # Try enrichment again now that the contract type is cached. new_err = self.compiler_manager.enrich_error(err) - except Exception: + except Exception: # noqa: BLE001, S110 pass if new_err: diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index d143a11200..8b794ef502 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -233,10 +233,10 @@ def address(self) -> "AddressType | None": return addr receiver = getattr(self.txn, "receiver", None) - if receiver in (None, "0x0000000000000000000000000000000000000000"): - # Check if deploy - if addr := getattr(self.txn, "contract_address", None): - return addr + if receiver in (None, "0x0000000000000000000000000000000000000000") and ( + addr := getattr(self.txn, "contract_address", None) + ): + return addr return receiver @@ -289,11 +289,10 @@ def source_traceback(self, value): self._source_traceback = value def _get_ape_traceback(self) -> TracebackType | None: - if src_tb := self.source_traceback: - # Create a custom Pythonic traceback using lines from the sources - # found from analyzing the trace of the transaction. - if py_tb := _get_custom_python_traceback(self, src_tb, project=self._project): - return py_tb + if (src_tb := self.source_traceback) and ( + py_tb := _get_custom_python_traceback(self, src_tb, project=self._project) + ): + return py_tb return None @@ -340,7 +339,7 @@ def __init__( try: # Attempt to use dev message as main exception message. self.message = dev - except Exception: + except Exception: # noqa: BLE001, S110 pass @property @@ -351,7 +350,7 @@ def revert_message(self): def revert_message(self, value): self.message = value if args := self.args: - self.args = tuple([value, *args[1:]]) + self.args = (value, *args[1:]) @property def dev_message(self) -> str | None: @@ -936,7 +935,7 @@ def _get_ape_traceback_from_tx(txn: FailedTxn) -> "SourceTraceback | None": try: receipt: ReceiptAPI = txn if isinstance(txn, ReceiptAPI) else txn.receipt # type: ignore - except Exception: + except Exception: # noqa: BLE001 # Receipt not real enough, maybe was a re-played call. return None @@ -972,7 +971,7 @@ def _get_custom_python_traceback( # TODO: Add support for manifest-projects. return None - _, exc_value, tb = sys.exc_info() + _, _exc_value, tb = sys.exc_info() depth = None idx = len(ape_traceback) - 1 frames = [] @@ -1004,7 +1003,7 @@ def _get_custom_python_traceback( if exec_item.source_path is None: # File is not local. Create a temporary file in its place. # This is necessary for tracebacks to work in Python. - temp_file = tempfile.NamedTemporaryFile(prefix="unknown_contract_") + temp_file = tempfile.NamedTemporaryFile(prefix="unknown_contract_") # noqa: SIM115 filename = temp_file.name else: filename = str(exec_item.source_path) @@ -1017,8 +1016,8 @@ def _get_custom_python_traceback( # Execute the new code to get a new (fake) tb with contract source info. try: - exec(py_code, {"__ape_exception__": err}, {}) - except BaseException: + exec(py_code, {"__ape_exception__": err}, {}) # noqa: S102 + except BaseException: # noqa: BLE001 real_tb = sys.exc_info()[2] fake_tb = getattr(real_tb, "tb_next", None) if isinstance(fake_tb, TracebackType): diff --git a/src/ape/logging.py b/src/ape/logging.py index f15a3f6992..8f1bc236fb 100644 --- a/src/ape/logging.py +++ b/src/ape/logging.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from enum import IntEnum from pathlib import Path -from typing import IO, TYPE_CHECKING, Any +from typing import IO, TYPE_CHECKING, Any, ClassVar from urllib.parse import urlparse, urlunparse import click @@ -43,18 +43,18 @@ def success(self, message, *args, **kws): CLICK_STYLE_KWARGS = { - LogLevel.ERROR: dict(fg="bright_red"), - LogLevel.WARNING: dict(fg="bright_yellow"), - LogLevel.SUCCESS: dict(fg="bright_green"), - LogLevel.INFO: dict(fg="blue"), - LogLevel.DEBUG: dict(fg="blue"), + LogLevel.ERROR: {"fg": "bright_red"}, + LogLevel.WARNING: {"fg": "bright_yellow"}, + LogLevel.SUCCESS: {"fg": "bright_green"}, + LogLevel.INFO: {"fg": "blue"}, + LogLevel.DEBUG: {"fg": "blue"}, } CLICK_ECHO_KWARGS = { - LogLevel.ERROR: dict(err=True), - LogLevel.WARNING: dict(err=True), - LogLevel.SUCCESS: dict(), - LogLevel.INFO: dict(), - LogLevel.DEBUG: dict(), + LogLevel.ERROR: {"err": True}, + LogLevel.WARNING: {"err": True}, + LogLevel.SUCCESS: {}, + LogLevel.INFO: {}, + LogLevel.DEBUG: {}, } @@ -65,7 +65,7 @@ def _isatty(stream: IO) -> bool: # noinspection PyBroadException try: return stream.isatty() - except Exception: + except Exception: # noqa: BLE001 return False @@ -113,13 +113,13 @@ def emit(self, record): click.echo(msg, **self.echo_kwargs[level]) else: click.echo(msg) - except Exception: + except Exception: # noqa: BLE001 self.handleError(record) class ApeLogger: _mentioned_verbosity_option = False - _extra_loggers: dict[str, logging.Logger] = {} + _extra_loggers: ClassVar[dict[str, logging.Logger]] = {} DISABLE_LEVEL: int = 100_000 def __init__( @@ -166,7 +166,7 @@ def _load_from_sys_argv(self, default: str | int | LogLevel | None = None): if sys.argv[arg_i] == "-v" or sys.argv[arg_i] == "--verbosity": try: level = _get_level(sys.argv[arg_i + 1].upper()) - except Exception: + except Exception: # noqa: BLE001, S112 # Let it fail in a better spot, or is not our level. continue @@ -346,7 +346,7 @@ def sanitize_url(url: str) -> str: class _RichConsoleFactory: - rich_console_map: dict[str, "RichConsole"] = {} + rich_console_map: ClassVar[dict[str, "RichConsole"]] = {} def get_console(self, file: IO[str] | None = None, **kwargs) -> "RichConsole": # Configure custom file console diff --git a/src/ape/managers/_contractscache.py b/src/ape/managers/_contractscache.py index 7c64737a20..5594f607a2 100644 --- a/src/ape/managers/_contractscache.py +++ b/src/ape/managers/_contractscache.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from functools import cached_property from pathlib import Path -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar from ethpm_types import ABI, ContractType from ethpm_types.contract_type import ABIList @@ -81,13 +81,12 @@ def get_type(self, key: str, fetch_from_disk: bool = True) -> _BASE_MODEL | None if model := self.memory.get(key): return model - elif fetch_from_disk and self._read_from_disk: - if data := self.get_data(key): - # Found on disk. - model = self._model_type.model_validate(data) - # Cache locally for next time. - self.memory[key] = model - return model + elif fetch_from_disk and self._read_from_disk and (data := self.get_data(key)): + # Found on disk. + model = self._model_type.model_validate(data) + # Cache locally for next time. + self.memory[key] = model + return model return None @@ -104,11 +103,11 @@ class ContractCache(BaseManager): """ # ecosystem_name -> network_name -> cache_name -> cache - _caches: dict[str, dict[str, dict[str, ApeDataCache]]] = {} + _caches: ClassVar[dict[str, dict[str, dict[str, ApeDataCache]]]] = {} # chain_id -> address -> custom_err # Cached to prevent calling `new_class` multiple times with conflicts. - _custom_error_types: dict[int, dict[AddressType, set[type[CustomError]]]] = {} + _custom_error_types: ClassVar[dict[int, dict[AddressType, set[type[CustomError]]]]] = {} @property def contract_types(self) -> ApeDataCache[ContractType]: @@ -256,11 +255,11 @@ def use_temporary_caches(self): Useful for testing. """ caches = self._caches - self._caches = {} + ContractCache._caches = {} with self.deployments.use_temporary_cache(): yield - self._caches = caches + ContractCache._caches = caches def _delete_proxy(self, address: AddressType): if info := self.proxy_infos[address]: @@ -583,24 +582,30 @@ def get( # Either no cached entry, or `replace=True` so we ignore the cache. # Check broader sources, such as an explorer. if not proxy_info and detect_proxy: - # Proxy info not provided. Attempt to detect. - if not (proxy_info := self.proxy_infos[address_key]): - if proxy_info := self.provider.network.ecosystem.get_proxy_info(address_key): - self.proxy_infos[address_key] = proxy_info - - if proxy_info: - if proxy_contract_type := self._get_proxy_contract_type( + proxy_info = self.proxy_infos[address_key] + + if ( + not proxy_info + and detect_proxy + and (detected_proxy_info := self.provider.network.ecosystem.get_proxy_info(address_key)) + ): + proxy_info = detected_proxy_info + self.proxy_infos[address_key] = proxy_info + + if proxy_info and ( + proxy_contract_type := self._get_proxy_contract_type( address_key, proxy_info, fetch_from_explorer=fetch_from_explorer, default=default, replace=replace, - ): - # `proxy_contract_type` is one of the following: - # 1. A ContractType with the combined proxy and implementation ABIs - # 2. Implementation-only ABI ContractType (like forwarder proxies) - # 3. Proxy only ABI (e.g. unverified implementation ContractType) - return proxy_contract_type + ) + ): + # `proxy_contract_type` is one of the following: + # 1. A ContractType with the combined proxy and implementation ABIs + # 2. Implementation-only ABI ContractType (like forwarder proxies) + # 3. Proxy only ABI (e.g. unverified implementation ContractType) + return proxy_contract_type contract_type = None # Also gets cached to disk for faster lookup next time. @@ -675,9 +680,8 @@ def _get_contract_type( Get the _exact_ ContractType for a given address. For proxy contracts, returns the proxy ABIs if there are any and not the implementation ABIs. """ - if not replace: - if contract_type := self.contract_types[address]: - return contract_type + if not replace and (contract_type := self.contract_types[address]): + return contract_type if fetch_from_explorer: return self._get_contract_type_from_explorer(address) @@ -751,7 +755,7 @@ def instance_at( prefix = f"Expected type '{ContractType.__name__}' for argument 'contract_type'" try: suffix = f"; Given '{type(contract_type).__name__}'." - except Exception: + except Exception: # noqa: BLE001 suffix = "." raise TypeError(f"{prefix}{suffix}") @@ -927,7 +931,7 @@ def _get_contract_type_from_explorer(self, address: AddressType) -> ContractType try: contract_type = self.provider.network.explorer.get_contract_type(address) - except Exception as err: + except Exception as err: # noqa: BLE001 explorer_name = self.provider.network.explorer.name if "rate limit" in str(err).lower(): # Don't show any additional error message during rate limit errors, diff --git a/src/ape/managers/_deploymentscache.py b/src/ape/managers/_deploymentscache.py index e31ff7a00e..1efcbe0a0a 100644 --- a/src/ape/managers/_deploymentscache.py +++ b/src/ape/managers/_deploymentscache.py @@ -1,6 +1,8 @@ from contextlib import contextmanager from pathlib import Path +from pydantic import Field + from ape.managers.base import BaseManager from ape.types.address import AddressType from ape.utils.basemodel import BaseModel, DiskCacheableModel @@ -35,7 +37,7 @@ def get(self, key: str): class Deployments(DiskCacheableModel): """The deployments structured JSON.""" - ecosystems: dict[str, dict[str, dict[str, list[Deployment]]]] = {} + ecosystems: dict[str, dict[str, dict[str, list[Deployment]]]] = Field(default_factory=dict) class DeploymentDiskCache(BaseManager): diff --git a/src/ape/managers/accounts.py b/src/ape/managers/accounts.py index e997d4c425..bbe8b77de6 100644 --- a/src/ape/managers/accounts.py +++ b/src/ape/managers/accounts.py @@ -2,7 +2,7 @@ from collections.abc import Generator, Iterator from contextlib import AbstractContextManager as ContextManager from functools import cached_property, singledispatchmethod -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from eth_utils import is_hex @@ -39,8 +39,11 @@ def _use_sender( class TestAccountManager(list, ManagerAccessMixin): __test__ = False - _impersonated_accounts: dict[AddressType, ImpersonatedAccount] = {} - _accounts_by_index: dict[int, AccountAPI] = {} + _impersonated_accounts: ClassVar[dict[AddressType, ImpersonatedAccount]] = {} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._accounts_by_index: dict[int, AccountAPI] = {} @log_instead_of_fail(default="") def __repr__(self) -> str: @@ -233,7 +236,7 @@ class AccountManager(BaseManager): my_accounts = accounts.load("dev") """ - _alias_to_account_cache: dict[str, AccountAPI] = {} + _alias_to_account_cache: ClassVar[dict[str, AccountAPI]] = {} @property def default_sender(self) -> AccountAPI | None: diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 614dacd8f0..e1ec9b3d2d 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -4,9 +4,10 @@ from datetime import datetime from functools import cached_property, partial, singledispatchmethod from statistics import mean, median -from typing import IO, TYPE_CHECKING, cast +from typing import IO, TYPE_CHECKING, ClassVar, cast import pandas as pd +from pydantic import Field from rich.box import SIMPLE from rich.table import Table @@ -178,7 +179,7 @@ def query( columns, self.head.__class__ ) extraction = partial(extract_fields, columns=columns) - data = map(lambda b: extraction(b), blocks) + data = (extraction(b) for b in blocks) return pd.DataFrame(columns=columns, data=data) def range( @@ -288,13 +289,11 @@ def poll_blocks( raise ValueError("'stop' argument must be in the future.") # Get number of last block with the necessary amount of confirmations. - block = None head_minus_confirms = self.height - required_confirmations if start_block is not None and start_block <= head_minus_confirms: # Front-load historical blocks. - for block in self.range(start_block, head_minus_confirms + 1): - yield block + yield from self.range(start_block, head_minus_confirms + 1) yield from self.provider.poll_blocks( stop_block=stop_block, @@ -313,7 +312,7 @@ class AccountHistory(BaseInterfaceModel): The address to get history for. """ - sessional: list[ReceiptAPI] = [] + sessional: list[ReceiptAPI] = Field(default_factory=list) """ The receipts from the current Python session. """ @@ -402,7 +401,7 @@ def query( txns = self.query_manager.query(query, engine_to_use=engine_to_use) columns = validate_and_expand_columns(columns, ReceiptAPI) # type: ignore extraction = partial(extract_fields, columns=columns) - data = map(lambda tx: extraction(tx), txns) + data = (extraction(tx) for tx in txns) return pd.DataFrame(columns=columns, data=data) def __iter__(self) -> Iterator[ReceiptAPI]: # type: ignore[override] @@ -505,8 +504,10 @@ class TransactionHistory(BaseManager): A container mapping Transaction History to the transaction from the active session. """ - _account_history_cache: dict[AddressType, AccountHistory] = {} - _hash_to_receipt_map: dict[str, ReceiptAPI] = {} + _account_history_cache: ClassVar[dict[AddressType, AccountHistory]] = {} + + def __init__(self): + self._hash_to_receipt_map: dict[str, ReceiptAPI] = {} @singledispatchmethod def __getitem__(self, key): @@ -533,7 +534,7 @@ def __getitem_str(self, account_or_hash: str) -> AccountHistory | ReceiptAPI: def _get_receipt() -> ReceiptAPI | None: try: return self._get_receipt(account_or_hash) - except Exception: + except Exception: # noqa: BLE001 return None is_account = False @@ -541,7 +542,7 @@ def _get_receipt() -> ReceiptAPI | None: # Attempt converting. try: account_or_hash = self.conversion_manager.convert(account_or_hash, AddressType) - except Exception: + except Exception: # noqa: BLE001, S110 # Pretend this never happened. pass else: @@ -661,8 +662,8 @@ def show_gas(self, report: "GasReport", file: IO[str] | None = None): f"{len(gases)}", f"{min(gases)}", f"{max(gases)}", - f"{int(round(mean(gases)))}", - f"{int(round(median(gases)))}", + f"{round(mean(gases))}", + f"{round(median(gases))}", ) if has_at_least_1_row: @@ -737,12 +738,12 @@ class ChainManager(BaseManager): from ape import chain """ - _snapshots: defaultdict = defaultdict(list) # chain_id -> snapshots + _snapshots: ClassVar[defaultdict] = defaultdict(list) # chain_id -> snapshots _chain_id_cache: ChainIdCache = ChainIdCache() - _block_container_map: dict[int, BlockContainer] = {} - _transaction_history_map: dict[int, TransactionHistory] = {} + _block_container_map: ClassVar[dict[int, BlockContainer]] = {} + _transaction_history_map: ClassVar[dict[int, TransactionHistory]] = {} _reports: ReportManager = ReportManager() - _code: dict[str, dict[str, dict[AddressType, "ContractCode"]]] = {} + _code: ClassVar[dict[str, dict[str, dict[AddressType, "ContractCode"]]]] = {} @cached_property def contracts(self) -> ContractCache: @@ -960,7 +961,7 @@ def isolate(self): try: yield - except Exception: + except Exception: # noqa: BLE001, S110 pass # NOTE: Handle cleanup after any exceptions in yielded context if snapshot is None: diff --git a/src/ape/managers/compilers.py b/src/ape/managers/compilers.py index a7ab147ad4..90c25aad38 100644 --- a/src/ape/managers/compilers.py +++ b/src/ape/managers/compilers.py @@ -2,7 +2,7 @@ from collections.abc import Iterable, Iterator, Sequence from functools import cached_property from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar from eth_pydantic_types import HexBytes @@ -40,7 +40,7 @@ class CompilerManager(BaseManager, ExtraAttributesMixin): from ape import compilers # "compilers" is the CompilerManager singleton """ - _registered_compilers_cache: dict[Path, dict[str, "CompilerAPI"]] = {} + _registered_compilers_cache: ClassVar[dict[Path, dict[str, "CompilerAPI"]]] = {} @log_instead_of_fail(default="") def __repr__(self) -> str: @@ -155,7 +155,7 @@ def compile( yield contract - except Exception as err: + except Exception as err: # noqa: BLE001 # One of the compilers failed. Show the error but carry on. logger.log_debug_stack_trace() errors.append(err) @@ -314,9 +314,7 @@ def get_custom_error(self, err: ContractLogicError) -> CustomError | None: :class:`~ape.exceptions.CustomError` | None """ message = err.revert_message - if not message.startswith("0x"): - return None - elif not (address := err.address): + if not message.startswith("0x") or not (address := err.address): return None if provider := self.network_manager.active_provider: diff --git a/src/ape/managers/config.py b/src/ape/managers/config.py index a274d73a91..ba3b46bc4c 100644 --- a/src/ape/managers/config.py +++ b/src/ape/managers/config.py @@ -151,7 +151,7 @@ def isolate_data_folder(self, keep: Iterable[str] | str | None = None) -> Iterat dest_path = temp_data_folder / item try: shutil.copytree(path_to_keep, dest_path) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.warning( f"Problem copying '{dest_path.name}' when making isolated project: {err}" ) diff --git a/src/ape/managers/converters.py b/src/ape/managers/converters.py index 467d122a27..d4b12c1021 100644 --- a/src/ape/managers/converters.py +++ b/src/ape/managers/converters.py @@ -12,6 +12,7 @@ from eth_pydantic_types import Address, HexBytes from eth_typing.evm import ChecksumAddress from eth_utils import is_0x_prefixed, is_checksum_address, is_hex, is_hex_address, to_int +from pydantic import PrivateAttr from ape.api.address import BaseAddress from ape.api.convert import ConverterAPI, ConvertibleAPI @@ -34,11 +35,7 @@ class HexConverter(ConverterAPI): """ def is_convertible(self, value: Any) -> bool: - return ( - (isinstance(value, str) and is_hex(value)) - or isinstance(value, bytes) - or isinstance(value, int) - ) + return (isinstance(value, str) and is_hex(value)) or isinstance(value, (bytes, int)) def convert(self, value: str) -> bytes: """ @@ -163,7 +160,7 @@ class IntAddressConverter(ConverterAPI): A converter that converts an integer address to an :class:`~ape.types.address.AddressType`. """ - _cache: dict[int, AddressType | bool] = {} + _cache: dict[int, AddressType | bool] = PrivateAttr(default_factory=dict) def is_convertible(self, value: Any) -> bool: if not isinstance(value, int): @@ -196,7 +193,7 @@ def convert(self, value: Any) -> AddressType: def _convert(self, value: int) -> AddressType | bool: try: val = Address.__eth_pydantic_validate__(value) - except Exception: + except Exception: # noqa: BLE001 return False return AddressType(to_checksum_address(val)) @@ -382,7 +379,7 @@ def _convert_using_converter_apis(self, value: Any, to_type: type) -> Any: for converter in self._converters[to_type]: try: is_convertible = converter.is_convertible(value) - except Exception as err: + except Exception as err: # noqa: BLE001 # If errors while checking if we can convert, log the error # and assume it's not convertible. converter_name = converter.__class__.__name__ @@ -398,7 +395,7 @@ def _convert_using_converter_apis(self, value: Any, to_type: type) -> Any: except Exception as err: try: error_value = f" '{value}' (type={type(value)}) " - except Exception: + except Exception: # noqa: BLE001 error_value = " " message = f"Failed to convert{error_value}" @@ -527,7 +524,7 @@ def _get_type_name_from_type(var_type: type) -> str: message = "Unable to deduce type name" try: str_value = f"{var_type}" - except Exception: + except Exception: # noqa: BLE001 str_value = "" if str_value: diff --git a/src/ape/managers/networks.py b/src/ape/managers/networks.py index 1906bc0388..1d609a8eef 100644 --- a/src/ape/managers/networks.py +++ b/src/ape/managers/networks.py @@ -3,10 +3,10 @@ from collections.abc import Collection, Iterator from functools import cached_property from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar from evmchains import PUBLIC_CHAIN_META -from pydantic import ValidationError +from pydantic import Field, ValidationError from ape.api.networks import ProviderContextManager from ape.exceptions import EcosystemNotFoundError, NetworkError, NetworkNotFoundError @@ -81,7 +81,7 @@ class NodeProcessMap(DiskCacheableModel): All managed running network subprocesses. """ - nodes: dict[int, NodeProcessData] = {} + nodes: dict[int, NodeProcessData] = Field(default_factory=dict) @property def path(self) -> Path: @@ -182,7 +182,7 @@ class NetworkManager(BaseManager, ExtraAttributesMixin): # For adhoc adding custom networks, or incorporating some defined # in other projects' configs. - _custom_networks: list[dict] = [] + _custom_networks: ClassVar[list[dict]] = [] @log_instead_of_fail(default="") def __repr__(self) -> str: @@ -316,7 +316,7 @@ def kill_node_process(self, *process_ids: int) -> dict[int, NodeProcessData]: try: provider = self.get_running_node(pid) - except Exception: + except Exception: # noqa: BLE001, S110 # Still try to kill the process (below). pass else: @@ -325,7 +325,7 @@ def kill_node_process(self, *process_ids: int) -> dict[int, NodeProcessData]: try: os.kill(pid, signal.SIGTERM) - except Exception: + except Exception: # noqa: BLE001, S110 pass else: pids_killed[pid] = data @@ -441,12 +441,12 @@ def provider_names(self) -> set[str]: The set of all provider names in ``ape``. """ - return set( + return { provider for ecosystem in self.ecosystems.values() for network in ecosystem.networks.values() for provider in network.providers - ) + } @property def custom_networks(self) -> list[dict]: @@ -587,7 +587,7 @@ def create_custom_provider( name = provider_name provider_settings: dict = {} - if connection_str.startswith("https://") or connection_str.startswith("http://"): + if connection_str.startswith(("https://", "http://")): provider_settings["uri"] = connection_str elif connection_str.endswith(".ipc"): provider_settings["ipc_path"] = connection_str @@ -992,7 +992,7 @@ def _invalidate_cache(self): # NOTE: Called when changing config programmatically. self.__dict__.pop("_custom_ecosystems", None) self.__dict__.pop("_custom_networks_from_config", None) - self._custom_networks = [] + NetworkManager._custom_networks = [] def _validate_filter(arg: list[str] | str | None, options: set[str]): @@ -1010,9 +1010,7 @@ def _validate_filter(arg: list[str] | str | None, options: set[str]): def _is_adhoc_url(value: str) -> bool: return ( - value.startswith("http://") - or value.startswith("https://") - or value.startswith("ws://") - or value.startswith("wss://") - or (value.endswith(".ipc") and ":" not in value) + value.startswith(("http://", "https://", "ws://", "wss://")) + or value.endswith(".ipc") + and ":" not in value ) diff --git a/src/ape/managers/plugins.py b/src/ape/managers/plugins.py index e9d2ed9aa1..b705d97bc8 100644 --- a/src/ape/managers/plugins.py +++ b/src/ape/managers/plugins.py @@ -2,7 +2,7 @@ from functools import cached_property from importlib import import_module from itertools import chain -from typing import Any +from typing import Any, ClassVar from ape.exceptions import ApeAttributeError from ape.logging import logger @@ -75,7 +75,7 @@ def _get_unimplemented_methods(api) -> Iterable[str]: class PluginManager: - _unimplemented_plugins: list[str] = [] + _unimplemented_plugins: ClassVar[list[str]] = [] def __init__(self) -> None: self.__registered = False diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index f9bcab489a..e9c0907878 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -6,7 +6,7 @@ from functools import cached_property, singledispatchmethod from pathlib import Path from re import Pattern -from typing import Any, cast +from typing import Any, ClassVar, cast from eth_typing import HexStr from eth_utils import to_hex @@ -14,6 +14,7 @@ from ethpm_types import ContractType, PackageManifest, PackageMeta, Source from ethpm_types.source import Compiler, ContractSource from ethpm_types.utils import compute_checksum +from pydantic import Field from pydantic_core import Url from ape.api.projects import ApeProject, DependencyAPI, ProjectAPI @@ -66,8 +67,6 @@ class SourceManager(BaseManager): _path_cache: list[Path] | None = None # perf: calculating paths from source Ids can be expensive. - _path_to_source_id: dict[Path, str] = {} - def __init__( self, root_path: Path, @@ -77,6 +76,7 @@ def __init__( self.root_path = root_path self.get_contracts_path = get_contracts_path self.exclude_globs = exclude_globs or set() + self._path_to_source_id: dict[Path, str] = {} self._sources: dict[str, Source] = {} self._exclude_cache: dict[str, bool] = {} @@ -125,7 +125,7 @@ def get(self, source_id: str) -> Source | None: if path.is_file(): try: text = path.read_text(encoding="utf8") - except Exception: + except Exception: # noqa: BLE001, S112 continue else: @@ -397,11 +397,11 @@ def __iter__(self) -> Iterator[str]: self._compile_missing_contracts(self.sources.paths) if contract_types := self.project.manifest.contract_types: for ct in contract_types.values(): - if not ct.name or not ct.source_id: - continue - - # Ensure was not deleted. - elif not (self.project.path / ct.source_id).is_file(): + if ( + not ct.name + or not ct.source_id + or not (self.project.path / ct.source_id).is_file() + ): continue yield ct.name @@ -561,7 +561,6 @@ def _detect_change(self, path: Path | str) -> bool: source_id: str if isinstance(path, Path): - path = path source_id = self.sources._get_source_id(path) else: source_id = str(path) # str wrap for mypy @@ -628,7 +627,7 @@ def __getattr__(self, name: str) -> Any: def __ape_extra_attributes__(self) -> Iterator[ExtraModelAttributes]: yield ExtraModelAttributes(name="project", attributes=lambda: self.project) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, Dependency): # We can't handle this type. # This line causes python to retry from the other end. @@ -724,11 +723,9 @@ def installed(self) -> bool: # Fails when version ID errors out (bad config / missing required install etc.) return False - if project_path.is_dir(): - if any(x for x in self.project_path.iterdir() if not x.name.startswith(".")): - return True - - return False + return project_path.is_dir() and any( + x for x in self.project_path.iterdir() if not x.name.startswith(".") + ) @property def compiled(self) -> bool: @@ -803,7 +800,7 @@ def install( logger.info(f"Installing {self.clean_package_id} {self.api.version_id}") try: self.api.fetch(self.project_path) - except Exception as err: + except Exception as err: # noqa: BLE001 raise ProjectError(f"Fetching failed: {err}") did_fetch = True @@ -829,7 +826,7 @@ def install( manifest = PackageManifest.model_validate_json( path.read_text(encoding="utf8") ) - except Exception: + except Exception: # noqa: BLE001, S110 # False alarm. pass else: @@ -984,8 +981,7 @@ def _unpack(self, path: Path, tracked: set[str]) -> Iterator["Dependency"]: # Unpack dependencies of dependencies (if they aren't already). for dependency in self.project.dependencies.specified: - for unpacked_dep in dependency._unpack(path, tracked=tracked): - yield unpacked_dep + yield from dependency._unpack(path, tracked=tracked) def _get_cache_versions_suffix(package_id) -> Path: @@ -1205,7 +1201,7 @@ class DependencyManager(BaseManager): """ # Class-level cache - _cache: dict[DependencyAPI, Dependency] = {} + _cache: ClassVar[dict[DependencyAPI, Dependency]] = {} def __init__(self, project: "ProjectManager | None" = None): self.project = project or self.local_project @@ -1326,7 +1322,7 @@ def get_project_dependencies( for api in self.config_apis: try: api_version_id = api.version_id - except Exception: + except Exception: # noqa: BLE001 api_version_id = None if (name is not None and api.name != name and api.package_id != name) or ( @@ -1435,11 +1431,10 @@ def get_dependency_api(self, package_id: str, version: str | None = None) -> Dep :class:`~ape.api.projects.DependencyAPI` """ # Check by package ID first. - if dependency := self._get_dependency_api_by_package_id(package_id, version=version): - return dependency - - elif dependency := self._get_dependency_api_by_package_id( - package_id, version=version, attr="name" + if (dependency := self._get_dependency_api_by_package_id(package_id, version=version)) or ( + dependency := self._get_dependency_api_by_package_id( + package_id, version=version, attr="name" + ) ): return dependency @@ -1471,7 +1466,7 @@ def _get_dependency_api_by_package_id( # else: prioritize the local dependencies, as that is most-likely what the user wants. - return sorted(matching, key=lambda d: d.version_id)[-1] if matching else None + return max(matching, key=lambda d: d.version_id) if matching else None def _get( self, @@ -1492,11 +1487,10 @@ def _get( elif dependency.name == name and dependency.version == version: name_matches.append(dependency) - if name_matches: - if len(name_matches) == 1: - # Return match-by-name after full loop in case was checking by - # package ID, which is more precise. - return name_matches[0] + if name_matches and len(name_matches) == 1: + # Return match-by-name after full loop in case was checking by + # package ID, which is more precise. + return name_matches[0] if name_matches: # If one of the matches is in the `.specified` dependencies, use that one. @@ -1574,10 +1568,7 @@ def get_versions(self, name: str, allow_install: bool = True) -> Iterator[Depend # Never yield. Check if using short-name. for dependency in self.all: - if dependency.name != name: - continue - - elif dependency.version in versions_yielded: + if dependency.name != name or dependency.version in versions_yielded: continue yield dependency @@ -1675,7 +1666,7 @@ def decode_dependency(self, **item: Any) -> DependencyAPI: name = item.get("name") or f"{item}" # NOTE: Using 'or' for short-circuit eval raise ProjectError( f"No installed dependency API that supports '{name}'. " - f"Keys={', '.join([x for x in item.keys()])}" + f"Keys={', '.join([x for x in item])}" ) def add(self, dependency: dict | DependencyAPI) -> Dependency: @@ -1730,14 +1721,9 @@ def install(self, **dependency: Any) -> Dependency | list[Dependency]: return self.install_dependency(dependency, use_cache=use_cache, recurse=recurse) # Install all project's. - result: list[Dependency] = [] - - for dep in self.get_project_dependencies( - use_cache=use_cache, allow_install=True, recurse=recurse - ): - result.append(dep) - - return result + return list( + self.get_project_dependencies(use_cache=use_cache, allow_install=True, recurse=recurse) + ) def install_dependency( self, @@ -1898,19 +1884,14 @@ def __contains__(self, item): @property def name(self) -> str: - if name := self.config.get("name"): - return name - elif name := self.manifest.name: + if (name := self.config.get("name")) or (name := self.manifest.name): return name return f"unknown-project-{random.randint(100_000, 999_999)}" @property def version(self) -> str: - if version := self._config_override.get("version"): - return version - - elif version := self.manifest.version: + if (version := self._config_override.get("version")) or (version := self.manifest.version): return version else: @@ -2114,7 +2095,7 @@ def add_compiler_data(self, compiler_data: Iterable[Compiler]) -> list[Compiler] # Use Compiler.__hash__ to remove duplicated. # Also, sort for consistency. compilers = sorted( - list({*remaining_existing_compilers, *compiler_data}), + {*remaining_existing_compilers, *compiler_data}, key=lambda x: f"{x.name}@{x.version}", ) self.update_manifest(compilers=compilers) @@ -2353,7 +2334,7 @@ class MultiProject(ProjectAPI): containing an ``ape-config.yaml`` file. """ - apis: list[ProjectAPI] = [] + apis: list[ProjectAPI] = Field(default_factory=list) """ An ordered list of APIs to use. The last items take precedence as their configs merge. """ @@ -2425,7 +2406,7 @@ def __init__( try: src_dict = dict(self.sources.items()) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(str(err)) else: if src_dict and not self.manifest.sources: @@ -2480,9 +2461,10 @@ def __getattr__(self, item: str) -> Any: # Possibly, the user does not have compiler plugins installed or working. missing_exts = set() for path in all_files: - if ext := get_full_extension(path): - if ext not in self.compiler_manager.registered_compilers: - missing_exts.add(ext) + if (ext := get_full_extension(path)) and ( + ext not in self.compiler_manager.registered_compilers + ): + missing_exts.add(ext) if missing_exts: start = "Else, could" if did_append else "Could" @@ -2527,9 +2509,7 @@ def _deduce_contracts_folder(self) -> Path: if (self.path / name).is_dir(): return self.path / name - exts_not_json = { - k for k in self.compiler_manager.registered_compilers.keys() if k != ".json" - } + exts_not_json = {k for k in self.compiler_manager.registered_compilers if k != ".json"} if not exts_not_json: # Not really able to look anywhere else. return self.path @@ -2604,9 +2584,7 @@ def _get_ape_project_api(self) -> ApeProject | None: @property def name(self) -> str: - if name := self.config.get("name"): - return name - elif name := self.manifest.name: + if (name := self.config.get("name")) or (name := self.manifest.name): return name return self._base_path.name.replace("_", "-").lower() @@ -2733,9 +2711,8 @@ def unpack(self, destination: Path, config_override: dict | None = None) -> "Loc def copytree(src, dst): try: shutil.copytree(src, dst, dirs_exist_ok=True) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Failed to unpack '{src}' to '{dst}': {err}") - pass # Unpack contracts. if self.contracts_folder.is_dir(): @@ -2785,7 +2762,7 @@ def load_manifest(self) -> PackageManifest: try: manifest = _load_manifest(self.manifest_path) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"__local__.json manifest corrupted! Re-building.\nFull error: {err}.") self.manifest_path.unlink(missing_ok=True) manifest = PackageManifest() @@ -2972,7 +2949,7 @@ def _create_contract_source(self, contract_type: ContractType) -> ContractSource # Not found in this project's sources. try: cwd = Path.cwd() - except Exception: + except Exception: # noqa: BLE001 # Happens when left in a cleaned-up temp path maybe? cwd = None diff --git a/src/ape/managers/query.py b/src/ape/managers/query.py index f25ccbc88e..1992d863a1 100644 --- a/src/ape/managers/query.py +++ b/src/ape/managers/query.py @@ -162,7 +162,7 @@ def query( else: # Get heuristics from all the query engines to perform this query - estimates = map(lambda qe: (qe, qe.estimate_query(query)), self.engines.values()) + estimates = ((qe, qe.estimate_query(query)) for qe in self.engines.values()) # Ignore query engines that can't perform this query valid_estimates = filter(lambda qe: qe[1] is not None, estimates) diff --git a/src/ape/plugins/_utils.py b/src/ape/plugins/_utils.py index 69317b3199..80bea61cef 100644 --- a/src/ape/plugins/_utils.py +++ b/src/ape/plugins/_utils.py @@ -10,7 +10,7 @@ import click from packaging.specifiers import SpecifierSet from packaging.version import Version -from pydantic import field_validator, model_validator +from pydantic import Field, field_validator, model_validator from ape.exceptions import PluginVersionError from ape.logging import logger @@ -658,7 +658,7 @@ class PluginGroup(BaseModel): """ plugin_type: PluginType - plugins: dict[str, PluginMetadata] = {} + plugins: dict[str, PluginMetadata] = Field(default_factory=dict) def __bool__(self) -> bool: return len(self.plugins) > 0 diff --git a/src/ape/pytest/contextmanagers.py b/src/ape/pytest/contextmanagers.py index 15bb818ef1..3fa3a77bd1 100644 --- a/src/ape/pytest/contextmanagers.py +++ b/src/ape/pytest/contextmanagers.py @@ -51,9 +51,8 @@ def _check_dev_message(self, exception: ContractLogicError): err_message = "Could not find the source of the revert." # Attempt to show source traceback so the user can see the real failure. - if (info := self.revert_info) and (ex := info.value): - if tb := ex.source_traceback: - err_message = f"{err_message}\n{tb}" + if (info := self.revert_info) and (ex := info.value) and (tb := ex.source_traceback): + err_message = f"{err_message}\n{tb}" raise AssertionError(err_message) @@ -124,7 +123,7 @@ def _check_custom_error(self, exception: CustomError): # the expected contract instance (e.g. from the same address). # If not address is being compared, this check is skipped. - raise AssertionError( + raise AssertionError( # noqa: TRY004 f"Expected error '{expected_error_cls.__name__}' " f"but was '{type(exception).__name__}'" ) @@ -155,12 +154,17 @@ def __enter__(self, *args, **kwargs): self.revert_info = info return info - def __exit__(self, exc_type: type, exc_value: Exception, traceback) -> bool: + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback, + ) -> bool: if exc_type is None: raise AssertionError("Transaction did not revert.") if not isinstance(exc_value, ContractLogicError): - raise AssertionError( + raise AssertionError( # noqa: TRY004 f"Transaction did not revert.\n" f"However, an exception of type {type(exc_value)} occurred: {exc_value}." ) from exc_value diff --git a/src/ape/pytest/fixtures.py b/src/ape/pytest/fixtures.py index 6d06a39f8c..590a583246 100644 --- a/src/ape/pytest/fixtures.py +++ b/src/ape/pytest/fixtures.py @@ -393,10 +393,7 @@ def is_iterating(self, name: str) -> bool: True when is a non-function scoped parametrized fixture that hasn't fully iterated. """ - if name not in self.parametrized: - return False - - elif not (info_ls := self.get_info(name)): + if name not in self.parametrized or not (info_ls := self.get_info(name)): return False for info in info_ls: @@ -613,7 +610,7 @@ def set_snapshot(self, scope: Scope): try: snapshot_id = self.take_snapshot() - except Exception: + except Exception: # noqa: BLE001 self.supported = False else: if snapshot_id is not None: @@ -662,7 +659,7 @@ def restore_snapshot(self, scope: Scope): ) # To avoid trying again self.supported = False - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Unhandled error with restoring snapshot: {err}") self.snapshots.clear_snapshot_id(scope) @@ -672,11 +669,10 @@ def _restore(self, snapshot_id: "SnapshotID"): class ReceiptCapture(ManagerAccessMixin): - receipt_map: dict[str, dict[str, "ReceiptAPI"]] = {} - enter_blocks: list[int] = [] - def __init__(self, config_wrapper: "ConfigWrapper"): self.config_wrapper = config_wrapper + self.receipt_map: dict[str, dict[str, ReceiptAPI]] = {} + self.enter_blocks: list[int] = [] def __enter__(self): block_number = self._get_block_number() @@ -701,7 +697,7 @@ def capture_range(self, start_block: int, stop_block: int): for txn in transactions: try: txn_hash = to_hex(txn.txn_hash) - except Exception: + except Exception: # noqa: BLE001, S112 # Might have been from an impersonated account. # Those txns need to be added separately, same as tracing calls. # Likely, it was already accounted before this point. @@ -715,10 +711,7 @@ def capture(self, transaction_hash: str): except ChainError: return - if not receipt: - return - - elif not (contract_address := (receipt.receiver or receipt.contract_address)): + if not receipt or not (contract_address := (receipt.receiver or receipt.contract_address)): return elif not (contract_type := self.chain_manager.contracts.get(contract_address)): diff --git a/src/ape/pytest/plugin.py b/src/ape/pytest/plugin.py index 1df52ab4f7..7767969fd6 100644 --- a/src/ape/pytest/plugin.py +++ b/src/ape/pytest/plugin.py @@ -54,20 +54,19 @@ def add_option(*names, **kwargs): def pytest_configure(config): # Do not include ape internals in tracebacks unless explicitly asked - if not config.getoption("--show-internal"): - if path_str := sys.modules["ape"].__file__: - base_path = str(Path(path_str).parent) - - def is_module(v): - return getattr(v, "__file__", None) and v.__file__.startswith(base_path) - - for module in (v for v in sys.modules.values() if is_module(v)): - # NOTE: Using try/except w/ type:ignore (over checking for attr) - # for performance reasons! - try: - module.__tracebackhide__ = True # type: ignore[attr-defined] - except AttributeError: - pass + if not config.getoption("--show-internal") and (path_str := sys.modules["ape"].__file__): + base_path = str(Path(path_str).parent) + + def is_module(v): + return getattr(v, "__file__", None) and v.__file__.startswith(base_path) + + for module in (v for v in sys.modules.values() if is_module(v)): + # NOTE: Using try/except w/ type:ignore (over checking for attr) + # for performance reasons! + try: + module.__tracebackhide__ = True # type: ignore[attr-defined] + except AttributeError: + pass if "--help" in config.invocation_params.args: # perf: Don't bother setting up runner if only showing help. diff --git a/src/ape/pytest/runners.py b/src/ape/pytest/runners.py index 7e5f3e0796..f1fe5cc171 100644 --- a/src/ape/pytest/runners.py +++ b/src/ape/pytest/runners.py @@ -250,7 +250,7 @@ def pytest_fixture_setup(self, fixturedef, request): if self._track_fixture_blocks(fixture_name): try: block_number = self.chain_manager.blocks.height - except Exception: + except Exception: # noqa: BLE001, S110 pass else: self.fixture_manager.add_fixture_info(fixture_name, setup_block=block_number) @@ -274,10 +274,7 @@ def _track_fixture_blocks(self, fixture_name: str) -> bool: return False scope = self.fixture_manager.get_fixture_scope(fixture_name) - if scope in (None, Scope.FUNCTION): - return False - - return True + return scope not in (None, Scope.FUNCTION) @pytest.hookimpl(trylast=True, hookwrapper=True) def pytest_collection_finish(self, session): @@ -363,7 +360,7 @@ def pytest_unconfigure(self): if self._provider_is_connected and self.config_wrapper.disconnect_providers_after: try: self._provider_context.disconnect_all() - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Failed to disconnect {self}: {err}") else: self._provider_is_connected = False diff --git a/src/ape/types/coverage.py b/src/ape/types/coverage.py index fafe6d702d..4307cffc1f 100644 --- a/src/ape/types/coverage.py +++ b/src/ape/types/coverage.py @@ -1,5 +1,5 @@ import itertools -from datetime import datetime +from datetime import datetime, timezone from html.parser import HTMLParser from pathlib import Path from typing import TYPE_CHECKING, Any @@ -8,7 +8,7 @@ import requests from ethpm_types.source import SourceLocation -from pydantic import NonNegativeInt, field_validator +from pydantic import Field, NonNegativeInt, field_validator from ape.logging import logger from ape.utils.basemodel import BaseModel @@ -169,7 +169,7 @@ class FunctionCoverage(BaseModel): The unique name of the function. """ - statements: list[CoverageStatement] = [] + statements: list[CoverageStatement] = Field(default_factory=list) """ For statement coverage, these are the individual items. See :class:`~ape.types.coverage.CoverageStatement` for more details. @@ -277,7 +277,7 @@ class ContractCoverage(BaseModel): The name of the contract. """ - functions: list[FunctionCoverage] = [] + functions: list[FunctionCoverage] = Field(default_factory=list) """ The coverage of each function individually. """ @@ -377,7 +377,7 @@ class ContractSourceCoverage(BaseModel): The ID of the source covered. """ - contracts: list[ContractCoverage] = [] + contracts: list[ContractCoverage] = Field(default_factory=list) """ Coverage for each contract in the source file. """ @@ -475,7 +475,7 @@ class CoverageProject(BaseModel): The name of the project being covered. """ - sources: list[ContractSourceCoverage] = [] + sources: list[ContractSourceCoverage] = Field(default_factory=list) """ Coverage for each source in the project. """ @@ -574,7 +574,7 @@ class CoverageReport(BaseModel): The timestamp the report was generated, in milliseconds. """ - projects: list[CoverageProject] = [] + projects: list[CoverageProject] = Field(default_factory=list) """ Each project with individual coverage tracked. """ @@ -769,7 +769,7 @@ def _get_xml(self): # Is first time coming across this name. fn_map[function.name] = function - for fn_name, function in fn_map.items(): + for function in fn_map.values(): if not function.statements or not any( s.location for s in function.statements ): @@ -793,7 +793,7 @@ def _get_xml(self): lines_to_add[lineno] = statement.hit_count # NOTE: Line numbers must be sorted in the XML! - sorted_nos = sorted(list(lines_to_add.keys())) + sorted_nos = sorted(lines_to_add.keys()) for no in sorted_nos: hits = lines_to_add[no] xline = xml_out.createElement("line") @@ -861,7 +861,7 @@ def write_html(self, path: Path, verbose: bool = False): response = requests.get(url) response.raise_for_status() # Check for any errors during the request favicon.write_bytes(response.content) - except Exception as err: + except Exception as err: # noqa: BLE001 # Don't let this stop us from generating the report. logger.debug(f"Failed finding favicon for coverage HTML. {err}") @@ -908,7 +908,7 @@ def _html_header_sub_element(self, html: Any) -> Any: h1.text += f" for {self.projects[0].name}" paragraph = SubElement(div, "p") - datetime_obj = datetime.fromtimestamp(self.timestamp / 1000) + datetime_obj = datetime.fromtimestamp(self.timestamp / 1000, tz=timezone.utc) datetime_string = datetime_obj.strftime("%Y-%m-%d %H:%M:%S") paragraph.text = f"Generated by Ape Framework v{ape_version}, {datetime_string}" diff --git a/src/ape/types/events.py b/src/ape/types/events.py index be384b481c..052ed22486 100644 --- a/src/ape/types/events.py +++ b/src/ape/types/events.py @@ -5,7 +5,7 @@ from eth_pydantic_types import HexBytes, HexStr from eth_utils import encode_hex, is_hex, keccak, to_hex from ethpm_types.abi import EventABI -from pydantic import BaseModel, field_serializer, field_validator, model_validator +from pydantic import BaseModel, Field, field_serializer, field_validator, model_validator from web3.types import FilterParams from ape.exceptions import ContractNotFoundError @@ -97,11 +97,17 @@ class BaseContractLog(BaseInterfaceModel): contract_address: AddressType = ZERO_ADDRESS """The contract responsible for emitting the log.""" - event_arguments: dict[str, Any] = {} + event_arguments: dict[str, Any] = Field(default_factory=dict) """The arguments to the event, including both indexed and non-indexed data.""" - def __eq__(self, other: Any) -> bool: - if self.contract_address != other.contract_address or self.event_name != other.event_name: + def __eq__(self, other: object) -> bool: + if ( + not hasattr(other, "contract_address") + or not hasattr(other, "event_name") + or not hasattr(other, "event_arguments") + or self.contract_address != other.contract_address + or self.event_name != other.event_name + ): return False for k, v in self.event_arguments.items(): @@ -272,7 +278,7 @@ def __ape_extra_attributes__(self) -> Iterator[ExtraModelAttributes]: def __contains__(self, item: str) -> bool: return item in self.event_arguments - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """ Check for equality between this instance and another ContractLog instance. @@ -325,10 +331,11 @@ class MockContractLog(BaseContractLog): of event arguments between a MockContractLog and a ContractLog instance. """ - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if ( not hasattr(other, "contract_address") or not hasattr(other, "event_name") + or not hasattr(other, "event_arguments") or self.contract_address != other.contract_address or self.event_name != other.event_name ): diff --git a/src/ape/types/private_mempool.py b/src/ape/types/private_mempool.py index 00ab9ef1b9..096b432a64 100644 --- a/src/ape/types/private_mempool.py +++ b/src/ape/types/private_mempool.py @@ -332,7 +332,7 @@ def decode_logs(self, *events: EventABI): return ecosystem.decode_logs(list(self.transaction_logs), *events) @property - def transaction_logs(self, *events: EventABI) -> Iterator[dict]: + def transaction_logs(self) -> Iterator[dict]: yield from _get_transaction_logs_from_sim_logs(self.logs or []) diff --git a/src/ape/types/signatures.py b/src/ape/types/signatures.py index 4f22c2c103..ae6645784f 100644 --- a/src/ape/types/signatures.py +++ b/src/ape/types/signatures.py @@ -45,19 +45,19 @@ def _bytes_to_human_str(bytes_value: bytes) -> str | None: try: # Try as text return bytes_value.decode("utf8") - except Exception: + except Exception: # noqa: BLE001, S110 pass try: # Try as hex return to_hex(bytes_value) - except Exception: + except Exception: # noqa: BLE001, S110 pass try: # Try normal str return str(bytes_value) - except Exception: + except Exception: # noqa: BLE001, S110 pass return None diff --git a/src/ape/types/trace.py b/src/ape/types/trace.py index 517f13d4ae..27795e5286 100644 --- a/src/ape/types/trace.py +++ b/src/ape/types/trace.py @@ -340,13 +340,13 @@ def revert_type(self) -> str | None: """ return self.statements[-1].type if self.statements[-1].type != "source" else None - def append(self, __object) -> None: + def append(self, __object, /) -> None: """ Append the given control flow to this one. """ self.root.append(__object) - def extend(self, __iterable) -> None: + def extend(self, __iterable, /) -> None: """ Append all the control flows from the given traceback to this one. """ diff --git a/src/ape/types/units.py b/src/ape/types/units.py index 18f1531a8c..f814d839c6 100644 --- a/src/ape/types/units.py +++ b/src/ape/types/units.py @@ -21,7 +21,7 @@ class CurrencyValueComparable(int): strings, such as ``"1 ether"``. """ - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, int): return super().__eq__(other) diff --git a/src/ape/utils/__init__.py b/src/ape/utils/__init__.py index 6406a8ed07..b11cbc130c 100644 --- a/src/ape/utils/__init__.py +++ b/src/ape/utils/__init__.py @@ -186,11 +186,11 @@ def __getattr__(name: str): "parse_coverage_tables", "parse_gas_table", "path_match", + "pragma_str_to_specifier_set", "prettify_dict", "prettify_function", "prettify_inputs", "prettify_list", - "pragma_str_to_specifier_set", "raises_not_implemented", "request_with_retry", "returns_array", diff --git a/src/ape/utils/_github.py b/src/ape/utils/_github.py index e5cd826f6c..6db231b1c0 100644 --- a/src/ape/utils/_github.py +++ b/src/ape/utils/_github.py @@ -6,7 +6,7 @@ from collections.abc import Iterator from io import BytesIO from pathlib import Path -from typing import Any +from typing import Any, ClassVar from requests import HTTPError, Session from requests.adapters import HTTPAdapter @@ -44,7 +44,7 @@ def clone(self, url: str, target_path: Path | None = None, branch: str | None = # Often times, `v` is required for tags. try: self.clone(url, target_path, branch=f"v{branch}") - except Exception: + except Exception: # noqa: BLE001 raise ProjectError(fail_msg) # Succeeded when prefixing `v`. @@ -66,7 +66,7 @@ class _GithubClient: # ApeWorX-specific attributes. ORGANIZATION_NAME = "ApeWorX" FRAMEWORK_NAME = "ape" - _repo_cache: dict[str, dict] = {} + _repo_cache: ClassVar[dict[str, dict]] = {} def __init__(self, session: Session | None = None): if session: @@ -75,7 +75,7 @@ def __init__(self, session: Session | None = None): else: headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT} - if auth := os.environ[self.TOKEN_KEY] if self.TOKEN_KEY in os.environ else None: + if auth := os.environ.get(self.TOKEN_KEY, None): headers["Authorization"] = f"token {auth}" session = Session() @@ -120,7 +120,7 @@ def get_release(self, org_name: str, repo_name: str, version: str) -> dict: def _try_get_release(vers): try: return self._get_release(org_name, repo_name, vers) - except Exception: + except Exception: # noqa: BLE001 return None if release := _try_get_release(version): diff --git a/src/ape/utils/abi.py b/src/ape/utils/abi.py index 28ed2829e5..50a9d36c10 100644 --- a/src/ape/utils/abi.py +++ b/src/ape/utils/abi.py @@ -493,7 +493,7 @@ def decode(self, topics: list[str], data: str | bytes, use_hex_on_fail: bool = F # Try again with strict=False try: data_values = decode(data_abi_types, hex_data, strict=False) - except Exception: + except Exception: # noqa: BLE001 # Even with strict=False, we failed to decode. # This should be a rare occasion, if it ever happens. logger.warn_from_exception(err, warning_message) diff --git a/src/ape/utils/basemodel.py b/src/ape/utils/basemodel.py index f8d70af199..2cc36d266b 100644 --- a/src/ape/utils/basemodel.py +++ b/src/ape/utils/basemodel.py @@ -103,7 +103,7 @@ def __get__(self, *args): for argument in args: try: arg_str = str(argument) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.debug(f"Failed calling __str__. Exception: {err}") arg_strs.append("") continue @@ -346,7 +346,7 @@ class ExtraModelAttributes(EthpmTypesBaseModel): def __repr__(self) -> str: try: return f"" - except Exception: + except Exception: # noqa: BLE001 # Disallow exceptions in __repr__ return "" @@ -354,7 +354,7 @@ def __contains__(self, name: Any) -> bool: attrs = self._attrs() try: name = str(name) - except Exception: + except Exception: # noqa: BLE001 return False if name in attrs or hasattr(attrs, name): @@ -500,7 +500,7 @@ def get_attribute_with_extras(obj: Any, name: str, coerce_attr_error: bool = Tru if not isinstance(obj, ExtraAttributesMixin): name = getattr(type(obj), "__name__", "obj") - raise AttributeError(f"{name} must use the '{ExtraAttributesMixin.__name__}' mixin'") + raise AttributeError(f"{name} must use the '{ExtraAttributesMixin.__name__}' mixin'") # noqa: TRY004 try: res = super(ExtraAttributesMixin, obj).__getattribute__(name) diff --git a/src/ape/utils/misc.py b/src/ape/utils/misc.py index ddc7fef716..295fc0dd96 100644 --- a/src/ape/utils/misc.py +++ b/src/ape/utils/misc.py @@ -4,7 +4,7 @@ import json import sys -if sys.version_info.minor >= 11: +if sys.version_info >= (3, 11): # 3.11 or greater # NOTE: type-ignore is for when running mypy on python versions < 3.11 import tomllib # type: ignore[import-not-found] @@ -170,7 +170,7 @@ def get_package_version(obj: Any) -> str: elif not isinstance(obj, str): try: str_value = f"{obj}" - except Exception: + except Exception: # noqa: BLE001 str_value = "" logger.warning(f"Type issue: Unknown if properly handled {str_value}") @@ -223,9 +223,10 @@ def load_config(path: Path, expand_envars=True, must_exist=False) -> dict: config = pyproject_toml.get("tool", {}).get("ape", {}) # Utilize [project] for some settings. - if project_settings := pyproject_toml.get("project"): - if "name" not in config and "name" in project_settings: - config["name"] = project_settings["name"] + if (project_settings := pyproject_toml.get("project")) and ( + "name" not in config and "name" in project_settings + ): + config["name"] = project_settings["name"] elif path.suffix in (".json",): config = json.loads(contents) @@ -256,9 +257,7 @@ def gas_estimation_error_message(tx_error: Exception) -> str: will likely revert. """ txn_error_text = str(tx_error) - if txn_error_text.endswith("."): - # Strip period from initial error so it looks better. - txn_error_text = txn_error_text[:-1] + txn_error_text = txn_error_text.removesuffix(".") return ( f"Gas estimation failed: '{txn_error_text}'. This transaction will likely revert. " @@ -433,7 +432,7 @@ def is_evm_precompile(address: str) -> bool: try: address = address.replace("0x", "") return 0 < sum(int(x) for x in address) < 10 - except Exception: + except Exception: # noqa: BLE001 return False @@ -456,7 +455,7 @@ def is_zero_hex(address: str) -> bool: # "0x" counts as zero. return True - except Exception: + except Exception: # noqa: BLE001 return False @@ -465,7 +464,7 @@ def _dict_overlay(mapping: dict[str, Any], overlay: dict[str, Any], depth: int = for key, value in overlay.items(): if isinstance(value, dict): if key not in mapping: - mapping[key] = dict() + mapping[key] = {} _dict_overlay(mapping[key], value, depth + 1) else: mapping[key] = value @@ -487,7 +486,7 @@ def wrapped(*args, **kwargs): else: return fn(*args, **kwargs) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(str(err)) if default: return default diff --git a/src/ape/utils/os.py b/src/ape/utils/os.py index d075680f6c..a8a49064dc 100644 --- a/src/ape/utils/os.py +++ b/src/ape/utils/os.py @@ -263,10 +263,7 @@ def path_match(path: str | Path, *exclusions: str) -> bool: path_path = Path(path) for excl in exclusions: - if fnmatch(path_str, excl): - return True - - elif fnmatch(path_path.name, excl): + if fnmatch(path_str, excl) or fnmatch(path_path.name, excl): return True else: diff --git a/src/ape/utils/process.py b/src/ape/utils/process.py index 6b8f133e2b..87ff8d983e 100644 --- a/src/ape/utils/process.py +++ b/src/ape/utils/process.py @@ -19,10 +19,11 @@ def __iter__(self): if isinstance(item, StopIteration) or is_stop_iteration_type: return - elif isinstance(item, Exception): - raise item - - elif isinstance(item, type) and issubclass(item, Exception): + elif ( + isinstance(item, Exception) + or isinstance(item, type) + and issubclass(item, Exception) + ): raise item yield item diff --git a/src/ape/utils/trace.py b/src/ape/utils/trace.py index 6506ff32cd..c51634b860 100644 --- a/src/ape/utils/trace.py +++ b/src/ape/utils/trace.py @@ -58,9 +58,12 @@ def prettify_function( arguments_str = "()" signature = f"{method}{arguments_str}" - if not is_create and returndata not in ((), [], None, {}, ""): - if return_str := _get_outputs_str(returndata, stylize=stylize, depth=depth): - signature = f"{signature} -> {return_str}" + if ( + not is_create + and returndata not in ((), [], None, {}, "") + and (return_str := _get_outputs_str(returndata, stylize=stylize, depth=depth)) + ): + signature = f"{signature} -> {return_str}" if contract: signature = f"{contract}.{signature}" @@ -136,7 +139,7 @@ def prettify_list( # Use multi-line if exceeds threshold OR any of the sub-lists use multi-line extra_chars_len = (len(sub_lists) - 1) * 2 use_multiline = len(str(sub_lists)) + extra_chars_len > wrap_threshold or any( - ["\n" in ls for ls in sub_lists] + "\n" in ls for ls in sub_lists ) if not use_multiline: @@ -147,7 +150,7 @@ def prettify_list( num_sub_lists = len(sub_lists) index = 0 spacing = indent * " " * 2 - for formatted_list in sub_lists: + for index, formatted_list in enumerate(sub_lists): if "\n" in formatted_list: # Multi-line sub list. Append 1 more spacing to each line. indented_item = f"\n{spacing}".join(formatted_list.splitlines()) @@ -160,7 +163,6 @@ def prettify_list( value = f"{value}," value = f"{value}\n" - index += 1 value = f"{value}]" return value @@ -187,11 +189,10 @@ def prettify_dict( length = sum(len(str(v)) for v in [*dictionary.keys(), *dictionary.values()]) do_wrap = length > wrap_threshold - index = 0 end_index = len(dictionary) - 1 kv_str = "(\n" if do_wrap else "(" - for key, value in dictionary.items(): + for index, (key, value) in enumerate(dictionary.items()): if do_wrap: kv_str += indent * " " @@ -206,8 +207,6 @@ def prettify_dict( if do_wrap: kv_str += "\n" - index += 1 - return f"{kv_str})" @@ -279,8 +278,8 @@ def parse_gas_table(report: "GasReport") -> list[Table]: f"{len(gases)}", f"{min(gases)}", f"{max(gases)}", - f"{int(round(mean(gases)))}", - f"{int(round(median(gases)))}", + f"{round(mean(gases))}", + f"{round(median(gases))}", ) if has_at_least_1_row: @@ -369,9 +368,7 @@ def _parse_verbose_coverage(coverage: "CoverageReport", statement: bool = True) if any(s.hit_count > 0 for s in fn.statements if s.tag == builtin) else 1 ) - rows.append( - tuple((name, name, "1", f"{miss}", "0.0%" if miss else "100.0%")) - ) + rows.append((name, name, "1", f"{miss}", "0.0%" if miss else "100.0%")) else: row = ( diff --git a/src/ape_accounts/_cli.py b/src/ape_accounts/_cli.py index 3ca1dbea9e..d8d5831452 100644 --- a/src/ape_accounts/_cli.py +++ b/src/ape_accounts/_cli.py @@ -156,7 +156,7 @@ def ask_for_passphrase(): try: passphrase = ask_for_passphrase() account = _account_from_mnemonic(alias, passphrase, mnemonic, hd_path=custom_hd_path) - except Exception as error: + except Exception as error: # noqa: BLE001 error_msg = f"{error}".replace(mnemonic, HIDDEN_MESSAGE) cli_ctx.abort(f"Seed phrase can't be imported: {error_msg}") @@ -165,7 +165,7 @@ def ask_for_passphrase(): try: passphrase = ask_for_passphrase() account = _account_from_key(alias, passphrase, key) - except Exception as error: + except Exception as error: # noqa: BLE001 cli_ctx.abort(f"Key can't be imported: {error}") if account: diff --git a/src/ape_accounts/accounts.py b/src/ape_accounts/accounts.py index 08888ac52d..f12493418f 100644 --- a/src/ape_accounts/accounts.py +++ b/src/ape_accounts/accounts.py @@ -4,7 +4,7 @@ from functools import cached_property from os import environ from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar import click from eip712.messages import EIP712Message @@ -46,7 +46,7 @@ def __init__(self): class AccountContainer(AccountContainerAPI): - loaded_accounts: dict[str, "KeyfileAccount"] = {} + loaded_accounts: ClassVar[dict[str, "KeyfileAccount"]] = {} @property def _keyfiles(self) -> Iterator[Path]: @@ -91,11 +91,11 @@ def sign_authorization( chain_id = self.chain_manager.chain_id signed_authorization = EthAccount.sign_authorization( - dict( - chainId=chain_id, - address=to_canonical_address(address), - nonce=nonce or self.nonce, - ), + { + "chainId": chain_id, + "address": to_canonical_address(address), + "nonce": nonce or self.nonce, + }, self.private_key, ) return MessageSignature( diff --git a/src/ape_cache/query.py b/src/ape_cache/query.py index daf2d67b89..6780af9923 100644 --- a/src/ape_cache/query.py +++ b/src/ape_cache/query.py @@ -149,7 +149,7 @@ def database_connection(self): logger.debug(f"Exception when querying:\n{e}") return None - except Exception as e: + except Exception as e: # noqa: BLE001 logger.warning(f"Unhandled exception when querying:\n{e}") self.database_bypass = True return None @@ -329,8 +329,8 @@ def _perform_block_query(self, query: BlockQuery) -> Iterator[BlockAPI]: # NOTE: Should be unreachable if estimated correctly raise QueryEngineError(f"Could not perform query:\n{query}") - yield from map( - lambda row: self.provider.network.ecosystem.decode_block(dict(row.items())), result + yield from ( + self.provider.network.ecosystem.decode_block(dict(row.items())) for row in result ) @perform_query.register @@ -344,7 +344,7 @@ def _perform_transaction_query(self, query: BlockTransactionQuery) -> Iterator[d # NOTE: Should be unreachable if estimated correctly raise QueryEngineError(f"Could not perform query:\n{query}") - yield from map(lambda row: dict(row.items()), result) + yield from (dict(row.items()) for row in result) @perform_query.register def _perform_contract_events_query(self, query: ContractEventQuery) -> Iterator[ContractLog]: @@ -360,7 +360,7 @@ def _perform_contract_events_query(self, query: ContractEventQuery) -> Iterator[ # NOTE: Should be unreachable if estimated correctly raise QueryEngineError(f"Could not perform query:\n{query}") - yield from map(lambda row: ContractLog.model_validate(dict(row.items())), result) + yield from (ContractLog.model_validate(dict(row.items())) for row in result) @singledispatchmethod def _cache_update_clause(self, query: QueryType) -> Insert: diff --git a/src/ape_compile/_cli.py b/src/ape_compile/_cli.py index 85fb04419f..32bee1c88a 100644 --- a/src/ape_compile/_cli.py +++ b/src/ape_compile/_cli.py @@ -99,7 +99,7 @@ def cli( c.contract_type.name: c.contract_type for c in dependency.compile(use_cache=use_cache, allow_install=True).values() } - except Exception as err: + except Exception as err: # noqa: BLE001 msg = f"Dependency '{dependency.name}' not installed. Reason: {err}" cli_ctx.logger.error(msg) errored = True diff --git a/src/ape_compile/config.py b/src/ape_compile/config.py index 6f12ca01ce..5ccbdc594b 100644 --- a/src/ape_compile/config.py +++ b/src/ape_compile/config.py @@ -1,7 +1,7 @@ import re from re import Pattern -from pydantic import field_serializer, field_validator +from pydantic import Field, field_serializer, field_validator from pydantic_settings import SettingsConfigDict from ape.api.config import ConfigEnum, PluginConfig @@ -27,7 +27,7 @@ class Config(PluginConfig): Configure general compiler settings. """ - exclude: set[str | Pattern] = set() + exclude: set[str | Pattern] = Field(default_factory=set) """ Source exclusion globs or regex patterns across all file types. To use regex, start your values with ``r"`` and they'll be turned @@ -48,7 +48,7 @@ class Config(PluginConfig): should configure ``include_dependencies`` to be ``True``. """ - output_extra: list[OutputExtras] = [] + output_extra: list[OutputExtras] = Field(default_factory=list) """ Extra selections to output. Outputs to ``.build/{key.lower()}``. """ diff --git a/src/ape_console/_cli.py b/src/ape_console/_cli.py index c371d8c333..af4a7fd243 100644 --- a/src/ape_console/_cli.py +++ b/src/ape_console/_cli.py @@ -39,7 +39,7 @@ def _code_callback(ctx, param, value) -> list[str]: @click.command( cls=ConnectedProviderCommand, short_help="Load the console", - context_settings=dict(ignore_unknown_options=True), + context_settings={"ignore_unknown_options": True}, ) @ape_cli_context() @project_option(hidden=True, type=Path) # Hidden as mostly used for test purposes. diff --git a/src/ape_console/config.py b/src/ape_console/config.py index 9066867714..1a6b7257cd 100644 --- a/src/ape_console/config.py +++ b/src/ape_console/config.py @@ -1,10 +1,11 @@ +from pydantic import Field from pydantic_settings import SettingsConfigDict from ape.api.config import PluginConfig class ConsoleConfig(PluginConfig): - plugins: list[str] = [] + plugins: list[str] = Field(default_factory=list) """Additional IPython plugins to include in your session.""" model_config = SettingsConfigDict(extra="allow", env_prefix="APE_CONSOLE_") diff --git a/src/ape_ethereum/__init__.py b/src/ape_ethereum/__init__.py index 864daf5f71..dc9a93c8e7 100644 --- a/src/ape_ethereum/__init__.py +++ b/src/ape_ethereum/__init__.py @@ -107,7 +107,6 @@ def __getattr__(name): "SetCodeTransaction", "SharedBlobReceipt", "SharedBlobTransaction", - "SharedBlobTransaction", "StaticFeeTransaction", "Trace", "TransactionStatusEnum", diff --git a/src/ape_ethereum/_converters.py b/src/ape_ethereum/_converters.py index ed27d95289..53a6db2e5a 100644 --- a/src/ape_ethereum/_converters.py +++ b/src/ape_ethereum/_converters.py @@ -26,10 +26,7 @@ class WeiConversions(ConverterAPI): """Converts units like `1 ether` to 1e18 wei.""" def is_convertible(self, value: str) -> bool: - if not isinstance(value, str): - return False - - elif " " not in value or len(value.split(" ")) > 2: + if not isinstance(value, str) or " " not in value or len(value.split(" ")) > 2: return False val, unit = value.split(" ") diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 357288689a..324079e54c 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -22,7 +22,7 @@ to_hex, ) from ethpm_types.abi import ABIType, ConstructorABI, EventABI, MethodABI -from pydantic import Field, computed_field, field_validator, model_validator +from pydantic import Field, PrivateAttr, computed_field, field_validator, model_validator from pydantic_settings import SettingsConfigDict from ape.api.config import PluginConfig @@ -157,7 +157,7 @@ class NetworkConfig(PluginConfig): it isn't detected as one. """ - request_headers: dict = {} + request_headers: dict = Field(default_factory=dict) """Optionally config extra request headers whenever using this network.""" model_config = SettingsConfigDict(extra="allow", env_prefix="APE_ETHEREUM_") @@ -172,10 +172,7 @@ def validate_gas_limit(cls, value): elif value == "auto": return AutoGasLimit() - elif value in ("auto", "max") or isinstance(value, AutoGasLimit): - return value - - elif isinstance(value, int): + elif value in ("auto", "max") or isinstance(value, (AutoGasLimit, int)): return value elif isinstance(value, str) and value.isnumeric(): @@ -237,11 +234,11 @@ class BaseEthereumConfig(PluginConfig): NETWORKS: ClassVar[dict[str, tuple[int, int]]] = NETWORKS default_network: str = LOCAL_NETWORK_NAME - _forked_configs: dict[str, ForkedNetworkConfig] = {} - _custom_networks: dict[str, NetworkConfig] = {} + _forked_configs: dict[str, ForkedNetworkConfig] = PrivateAttr(default_factory=dict) + _custom_networks: dict[str, NetworkConfig] = PrivateAttr(default_factory=dict) # NOTE: This gets appended to Ape's root User-Agent string. - request_headers: dict = {} + request_headers: dict = Field(default_factory=dict) model_config = SettingsConfigDict(extra="allow", env_prefix="APE_ETHEREUM_") @@ -327,9 +324,8 @@ def __contains__(self, key: str) -> bool: def get(self, key: str, default: Any | None = None) -> Any: net_key = key.replace("-", "_") - if net_key.endswith("_fork"): - if cfg := self._get_forked_config(net_key): - return cfg + if net_key.endswith("_fork") and (cfg := self._get_forked_config(net_key)): + return cfg result: Any if result := super().get(key, default=default): @@ -347,15 +343,14 @@ def _get_forked_config(self, name: str) -> ForkedNetworkConfig | None: return self._forked_configs[live_key] live_cfg: Any - if live_cfg := self.get(live_key): - if isinstance(live_cfg, NetworkConfig): - fork_cfg = create_local_network_config( - use_fork=True, - default_transaction_type=self.DEFAULT_TRANSACTION_TYPE, - gas_limit=self.DEFAULT_LOCAL_GAS_LIMIT, - ) - self._forked_configs[live_key] = fork_cfg - return fork_cfg + if (live_cfg := self.get(live_key)) and isinstance(live_cfg, NetworkConfig): + fork_cfg = create_local_network_config( + use_fork=True, + default_transaction_type=self.DEFAULT_TRANSACTION_TYPE, + gas_limit=self.DEFAULT_LOCAL_GAS_LIMIT, + ) + self._forked_configs[live_key] = fork_cfg + return fork_cfg return None @@ -379,7 +374,7 @@ class Block(BlockAPI): base_fee: HexInt = Field(default=0, alias="baseFeePerGas") difficulty: HexInt = 0 total_difficulty: HexInt = Field(default=0, alias="totalDifficulty") - uncles: list[HexBytes] = [] + uncles: list[HexBytes] = Field(default_factory=list) # Type re-declares. hash: HexBytes | None = None @@ -770,12 +765,9 @@ def decode_calldata(self, abi: ConstructorABI | MethodABI, calldata: bytes) -> d for v, t in zip(raw_input_values, input_types, strict=True) ] arguments = {} - index = 0 - for i, v in zip(abi.inputs, input_values, strict=True): + for index, (i, v) in enumerate(zip(abi.inputs, input_values, strict=True)): name = i.name or f"{index}" arguments[name] = v - index += 1 - return arguments def decode_returndata(self, abi: MethodABI, raw_data: bytes) -> tuple[Any, ...]: @@ -821,7 +813,7 @@ def decode_returndata(self, abi: MethodABI, raw_data: bytes) -> tuple[Any, ...]: else: try: return ([o for o in output_values[0]],) # type: ignore[union-attr] - except Exception: + except Exception: # noqa: BLE001 # On-chains transaction data errors. return (output_values,) @@ -1057,7 +1049,7 @@ def decode_logs(self, logs: Sequence[dict], *events: EventABI) -> Iterator[Contr } def get_abi(_topic: HexStr) -> LogInputABICollection | None: - return abi_inputs[_topic] if _topic in abi_inputs else None + return abi_inputs.get(_topic, None) for log in logs: if log.get("anonymous"): @@ -1191,7 +1183,7 @@ def _enrich_calltree(self, call: dict, **kwargs) -> dict: call["contract_id"] = address = kwargs["contract_address"] = self.decode_address( address ) - except Exception: + except Exception: # noqa: BLE001 # Tx was made with a weird address. call["contract_id"] = address @@ -1205,7 +1197,7 @@ def _enrich_calltree(self, call: dict, **kwargs) -> dict: try: address_int = int(address, 16) - except Exception: + except Exception: # noqa: BLE001, S110 pass else: # Collapse pre-compile address calls @@ -1479,7 +1471,7 @@ def _enrich_trace_event( try: contract_logs = [log for log in self.decode_logs([log_data], abi)] - except Exception as err: + except Exception as err: # noqa: BLE001 logger.debug(f"Failed decoding logs from trace data: {err}") return event @@ -1508,7 +1500,7 @@ def _get_contract_type_for_enrichment( if not (contract_type := kwargs.get("contract_type")): try: contract_type = self.chain_manager.contracts.get(address) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.debug(f"Error getting contract type during event enrichment: {err}") return contract_type @@ -1525,7 +1517,7 @@ def decode_custom_error( # Use an instance (required for proper error caching). try: contract = self.chain_manager.contracts.instance_at(address) - except Exception: + except Exception: # noqa: BLE001 return None selector = data[:4] @@ -1559,7 +1551,7 @@ def decode_custom_error( try: if not (last_addr := next(trace.get_addresses_used(reverse=True), None)): return None - except Exception: + except Exception: # noqa: BLE001 # When unable to get trace-frames properly, such as eth-tester. return None diff --git a/src/ape_ethereum/provider.py b/src/ape_ethereum/provider.py index 17d14d3492..f8cfc99979 100644 --- a/src/ape_ethereum/provider.py +++ b/src/ape_ethereum/provider.py @@ -17,6 +17,7 @@ from eth_typing import HexStr from eth_utils import add_0x_prefix, is_hex, to_hex from evmchains import PUBLIC_CHAIN_META, get_random_rpc +from pydantic import Field, PrivateAttr from pydantic.dataclasses import dataclass from requests import HTTPError from web3 import HTTPProvider, IPCProvider, Web3 @@ -119,7 +120,7 @@ def _post_send_transaction(tx: TransactionAPI, receipt: ReceiptAPI): except TransactionNotFound: # Receipt never published. Likely failed. pass - except Exception as err: + except Exception as err: # noqa: BLE001 # Avoid letting debug logs causes program crashes. logger.debug(f"Unable to show debug logs: {err}") @@ -143,7 +144,7 @@ class Web3Provider(ProviderAPI, ABC): _supports_debug_trace_call: bool | None = None - _transaction_trace_cache: dict[str, TransactionTrace] = {} + _transaction_trace_cache: dict[str, TransactionTrace] = PrivateAttr(default_factory=dict) def __new__(cls, *args, **kwargs): # Post-connection ops @@ -276,10 +277,13 @@ def _get_connected_rpc(self, validator: Callable[[str], bool]) -> str | None: like `ape-node`, configure your URI and that will be returned here instead. """ - if web3 := self._web3: - if endpoint_uri := getattr(web3.provider, "endpoint_uri", None): - if isinstance(endpoint_uri, str) and validator(endpoint_uri): - return endpoint_uri + if ( + (web3 := self._web3) + and (endpoint_uri := getattr(web3.provider, "endpoint_uri", None)) + and isinstance(endpoint_uri, str) + and validator(endpoint_uri) + ): + return endpoint_uri return None @@ -337,17 +341,13 @@ def network_choice(self) -> str: @property def http_uri(self) -> str | None: - if rpc := self._connected_http_uri: + if (rpc := self._connected_http_uri) or (rpc := self._configured_http_uri): return rpc - elif rpc := self._configured_http_uri: + elif (rpc := self._configured_uri) and _is_http_url(rpc): + # "uri" found in config/settings and is WS. return rpc - elif rpc := self._configured_uri: - if _is_http_url(rpc): - # "uri" found in config/settings and is WS. - return rpc - return self._default_http_uri @property @@ -385,10 +385,9 @@ def ws_uri(self) -> str | None: # "ws_uri" found in config/settings return rpc - elif rpc := self._configured_uri: - if _is_ws_url(rpc): - # "uri" found in config/settings and is WS. - return rpc + elif (rpc := self._configured_uri) and _is_ws_url(rpc): + # "uri" found in config/settings and is WS. + return rpc return None @@ -398,10 +397,9 @@ def ipc_path(self) -> Path | None: # "ipc_path" found in config/settings return Path(rpc) - elif rpc := self._configured_uri: - if _is_ipc_path(rpc): - # "uri" found in config/settings and is IPC. - return Path(rpc) + elif (rpc := self._configured_uri) and _is_ipc_path(rpc): + # "uri" found in config/settings and is IPC. + return Path(rpc) return None @@ -422,7 +420,7 @@ def rpc_available(rpc_uri: str) -> bool: try: return Web3(HTTPProvider(rpc_uri)).is_connected() - except Exception: + except Exception: # noqa: BLE001 return False retries = 10 @@ -451,7 +449,7 @@ def client_version(self) -> str: def base_fee(self) -> int: try: fee_history = self._get_fee_history() - except Exception as exc: + except Exception as exc: # noqa: BLE001 logger.debug( f"Failed using `eth_feeHistory` for network '{self.network_choice}'. Error: {exc}" ) @@ -512,7 +510,7 @@ def supports_tracing(self) -> bool: except NotImplementedError: return False - except Exception: + except Exception: # noqa: BLE001 # We know tracing works because we didn't get a NotImplementedError. return True @@ -729,19 +727,23 @@ def send_call( if track_gas and self._test_runner is not None and txn.receiver: self._test_runner.gas_tracker.append_gas(trace, txn.receiver) - if track_coverage and self._test_runner is not None and txn.receiver: - if contract_type := self.chain_manager.contracts.get(txn.receiver): - if contract_src := self.local_project._create_contract_source(contract_type): - method_id = HexBytes(txn.data) - selector = ( - contract_type.methods[method_id].selector - if method_id in contract_type.methods - else None - ) - source_traceback = SourceTraceback.create(contract_src, trace, method_id) - self._test_runner.coverage_tracker.cover( - source_traceback, function=selector, contract=contract_type.name - ) + if ( + track_coverage + and self._test_runner is not None + and txn.receiver + and (contract_type := self.chain_manager.contracts.get(txn.receiver)) + and (contract_src := self.local_project._create_contract_source(contract_type)) + ): + method_id = HexBytes(txn.data) + selector = ( + contract_type.methods[method_id].selector + if method_id in contract_type.methods + else None + ) + source_traceback = SourceTraceback.create(contract_src, trace, method_id) + self._test_runner.coverage_tracker.cover( + source_traceback, function=selector, contract=contract_type.name + ) if show_gas: trace.show_gas_report() @@ -769,15 +771,14 @@ def _eth_call( contract_address = arguments[0].get("to") _lazy_call_trace = _LazyCallTrace(arguments) - if not skip_trace: - if address := contract_address: - try: - contract_type = self.chain_manager.contracts.get(address) - except RecursionError: - # Occurs when already in the middle of fetching this contract. - pass - else: - _lazy_call_trace.contract_type = contract_type + if not skip_trace and (address := contract_address): + try: + contract_type = self.chain_manager.contracts.get(address) + except RecursionError: + # Occurs when already in the middle of fetching this contract. + pass + else: + _lazy_call_trace.contract_type = contract_type vm_err = self.get_virtual_machine_error( err, @@ -1051,7 +1052,7 @@ def assert_chain_activity(): if adjusted_head.number is None or adjusted_head.hash is None: raise ProviderError("Adjusted head block has no number or hash.") - except Exception: + except Exception: # noqa: BLE001 # TODO: I did encounter this sometimes in a re-org, needs better handling # and maybe bubbling up the block number/hash exceptions above. assert_chain_activity() @@ -1114,9 +1115,8 @@ def poll_logs( if required_confirmations is None: required_confirmations = self.network.required_confirmations - if stop_block is not None: - if stop_block <= (self._get_latest_block().number or 0): - raise ValueError("'stop' argument must be in the future.") + if stop_block is not None and stop_block <= (self._get_latest_block().number or 0): + raise ValueError("'stop' argument must be in the future.") for block in self.poll_blocks(stop_block, required_confirmations, new_block_timeout): if block.number is None: @@ -1193,12 +1193,15 @@ def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI: # else: Assume user specified the correct amount or txn will fail and waste gas - if txn_type is TransactionType.ACCESS_LIST and isinstance(txn, AccessListTransaction): - if not txn.access_list: - try: - txn.access_list = self.create_access_list(txn) - except APINotImplementedError: - pass + if ( + txn_type is TransactionType.ACCESS_LIST + and isinstance(txn, AccessListTransaction) + and not txn.access_list + ): + try: + txn.access_list = self.create_access_list(txn) + except APINotImplementedError: + pass gas_limit = self.network.gas_limit if txn.gas_limit is None else txn.gas_limit if gas_limit in (None, "auto") or isinstance(gas_limit, AutoGasLimit): @@ -1467,7 +1470,7 @@ def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMa # NOTE: For some reason, it comes back with single quotes though. try: err_data = json.loads(str(err_data or "").replace("'", '"')) - except Exception: + except Exception: # noqa: BLE001 return VirtualMachineError(base_err=exception, **kwargs) if not (err_msg := err_data.get("message")): @@ -1524,9 +1527,6 @@ def _handle_execution_reverted( if trace is not None: if callable(trace): trace = params["trace"] = trace() - else: - trace = trace - if trace is not None and (revert_message := trace.revert_message): message = revert_message no_reason = False @@ -1580,7 +1580,9 @@ class EthereumNodeProvider(Web3Provider, ABC): name: str = "node" # NOTE: Appends user-agent to base User-Agent string. - request_header: dict = {"User-Agent": f"EthereumNodeProvider/web3.py/{web3_version}"} + request_header: dict = Field( + default_factory=lambda: {"User-Agent": f"EthereumNodeProvider/web3.py/{web3_version}"} + ) @property def connection_str(self) -> str: @@ -1626,7 +1628,7 @@ def has_poa_history(self) -> bool: findings = True break - except Exception: + except Exception: # noqa: BLE001, S112 # Some chains are "light" and we may not be able to detect # if it needs PoA middleware. continue @@ -1817,11 +1819,11 @@ def _is_uri(val: str) -> bool: def _is_http_url(val: str) -> bool: - return val.startswith("https://") or val.startswith("http://") + return val.startswith(("https://", "http://")) def _is_ws_url(val: str) -> bool: - return val.startswith("wss://") or val.startswith("ws://") + return val.startswith(("wss://", "ws://")) def _is_ipc_path(val: str | Path) -> bool: @@ -1847,8 +1849,7 @@ def source_traceback(self) -> SourceTraceback | None: return None method_id = self._arguments[0].get("data", "")[:10] or None - if ct and method_id: - if contract_src := self.local_project._create_contract_source(ct): - return SourceTraceback.create(contract_src, self.trace, method_id) + if ct and method_id and (contract_src := self.local_project._create_contract_source(ct)): + return SourceTraceback.create(contract_src, self.trace, method_id) return None diff --git a/src/ape_ethereum/trace.py b/src/ape_ethereum/trace.py index 1d1badb9a9..e99be62793 100644 --- a/src/ape_ethereum/trace.py +++ b/src/ape_ethereum/trace.py @@ -21,7 +21,7 @@ ) from evm_trace.gas import merge_reports from hexbytes import HexBytes -from pydantic import field_validator +from pydantic import Field, PrivateAttr, field_validator from rich.tree import Tree from ape.api.trace import TraceAPI @@ -104,7 +104,7 @@ class Trace(TraceAPI): def __repr__(self) -> str: try: return f"{self}" - except Exception as err: + except Exception as err: # noqa: BLE001 # Don't let __repr__ fail. logger.debug(f"Problem transaction trace: {err}") return "" @@ -160,16 +160,15 @@ def enriched_calltree(self) -> dict: self._enriched_calltree = {} # Add top-level data if missing. - if not self._enriched_calltree.get("gas_cost"): - # Happens on calltrees built from structLogs. - if gas_used := self.transaction.get("gas_used"): - if "data" in self.transaction: - # Subtract base gas costs. - # (21_000 + 4 gas per 0-byte and 16 gas per non-zero byte). - data_gas = sum( - [4 if x == 0 else 16 for x in HexBytes(self.transaction["data"])] - ) - self._enriched_calltree["gas_cost"] = gas_used - 21_000 - data_gas + if ( + not self._enriched_calltree.get("gas_cost") + and (gas_used := self.transaction.get("gas_used")) + and "data" in self.transaction + ): + # Subtract base gas costs. + # (21_000 + 4 gas per 0-byte and 16 gas per non-zero byte). + data_gas = sum([4 if x == 0 else 16 for x in HexBytes(self.transaction["data"])]) + self._enriched_calltree["gas_cost"] = gas_used - 21_000 - data_gas return self._enriched_calltree @@ -186,7 +185,7 @@ def root_contract_type(self) -> "ContractType | None": if address := self.transaction.get("to"): try: return self.chain_manager.contracts.get(address) - except Exception: + except Exception: # noqa: BLE001 return None return None @@ -197,7 +196,7 @@ def root_method_abi(self) -> "MethodABI | None": if ct := self.root_contract_type: try: return ct.methods[method_id] - except Exception: + except Exception: # noqa: BLE001 return None return None @@ -252,19 +251,20 @@ def _get_return_value_from_calltree( self, calltree: dict | CallTreeNode ) -> tuple[Any | None, ...]: num_outputs = 1 - if raw_return_data := ( - calltree.get("returndata") if isinstance(calltree, dict) else calltree.returndata - ): - if abi := self._get_abi(calltree): - # Ensure we return a tuple with the correct length, even if fails. - num_outputs = len(abi.outputs) - try: - return self._ecosystem.decode_returndata(abi, HexBytes(raw_return_data)) - except Exception as err: - logger.debug( - f"Failed decoding raw returndata: {to_hex(raw_return_data)}. Error: {err}" - ) - return tuple([None for _ in range(num_outputs)]) + if ( + raw_return_data := ( + calltree.get("returndata") if isinstance(calltree, dict) else calltree.returndata + ) + ) and (abi := self._get_abi(calltree)): + # Ensure we return a tuple with the correct length, even if fails. + num_outputs = len(abi.outputs) + try: + return self._ecosystem.decode_returndata(abi, HexBytes(raw_return_data)) + except Exception as err: # noqa: BLE001 + logger.debug( + f"Failed decoding raw returndata: {to_hex(raw_return_data)}. Error: {err}" + ) + return tuple([None for _ in range(num_outputs)]) return tuple([None for _ in range(num_outputs)]) @@ -297,7 +297,7 @@ def try_get_revert_msg(c) -> str | None: def _last_frame(self) -> dict | None: try: frame = deque(self.raw_trace_frames, maxlen=1) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Failed getting traceback: {err}") return None @@ -349,19 +349,20 @@ def show(self, verbose: bool = False, file: IO[str] = sys.stdout): if sender := self.transaction.get("from"): console.print(f"tx.origin=[{TraceStyles.CONTRACTS}]{sender}[/]") - if self.call_trace_approach not in approaches_handling_events and hasattr( - self._ecosystem, "_enrich_trace_events" + if ( + self.call_trace_approach not in approaches_handling_events + and hasattr(self._ecosystem, "_enrich_trace_events") + and (logs := self.transaction.get("logs", [])) ): # We must manually attach the contract logs. # NOTE: With these approaches, we don't know where they appear # in the call-tree so we have to put them at the top. - if logs := self.transaction.get("logs", []): - enriched_events = self._ecosystem._enrich_trace_events(logs) - event_trees = _events_to_trees(enriched_events) - if event_trees: - console.print() - self.chain_manager._reports.show_events(event_trees, console=console) - console.print() + enriched_events = self._ecosystem._enrich_trace_events(logs) + event_trees = _events_to_trees(enriched_events) + if event_trees: + console.print() + self.chain_manager._reports.show_events(event_trees, console=console) + console.print() # else: the events are already included in the right spots in the call tree. @@ -461,8 +462,8 @@ def _get_abi(self, call: dict | CallTreeNode) -> "MethodABI | None": class TransactionTrace(Trace): transaction_hash: HexStr - debug_trace_transaction_parameters: dict = {"enableMemory": True} - _frames: list[dict] = [] + debug_trace_transaction_parameters: dict = Field(default_factory=lambda: {"enableMemory": True}) + _frames: list[dict] = PrivateAttr(default_factory=list) @property def raw_trace_frames(self) -> Iterator[dict]: @@ -531,7 +532,7 @@ def _discover_calltrace_approach(self) -> CallTreeNode: for approach, fn in approaches.items(): try: call = fn() - except Exception as err: + except Exception as err: # noqa: BLE001 reason_map[approach.name] = f"{err}" continue @@ -595,7 +596,7 @@ class CallTrace(Trace): be created near sending the request. """ - arguments: list[Any] = [] + arguments: list[Any] = Field(default_factory=list) """ Remaining eth-call arguments, minus the transaction. """ @@ -631,7 +632,7 @@ def _traced_call(self) -> dict: try: result = self._debug_trace_call() - except Exception: + except Exception: # noqa: BLE001 self._set_supports_trace_call(False) return {} @@ -699,9 +700,9 @@ def _events_to_trees(events: list[dict]) -> list[Tree]: event_counter[tuple_key].append(evt) result = [] - for evt_tup, events in event_counter.items(): - count = len(events) - evt = events[0] + for grouped_events in event_counter.values(): + count = len(grouped_events) + evt = grouped_events[0] if "name" not in evt and "calldata" not in evt: # Not sure; or not worth showing. logger.debug(f"Unknown event data: '{evt}'.") diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 49df031f6f..41f3b3cb76 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -320,14 +320,15 @@ def method_called(self) -> MethodABI | None: @cached_property def source_traceback(self) -> SourceTraceback: - if contract_type := self.contract_type: - if contract_src := self.local_project._create_contract_source(contract_type): - try: - return SourceTraceback.create(contract_src, self.trace, HexBytes(self.data)) - except Exception as err: - # Failing to get a traceback should not halt an Ape application. - # Sometimes, a node crashes and we are left with nothing. - logger.error(f"Problem retrieving traceback: {err}") + if (contract_type := self.contract_type) and ( + contract_src := self.local_project._create_contract_source(contract_type) + ): + try: + return SourceTraceback.create(contract_src, self.trace, HexBytes(self.data)) + except Exception as err: # noqa: BLE001 + # Failing to get a traceback should not halt an Ape application. + # Sometimes, a node crashes and we are left with nothing. + logger.error(f"Problem retrieving traceback: {err}") return SourceTraceback.model_validate([]) diff --git a/src/ape_networks/_cli.py b/src/ape_networks/_cli.py index 05125f2a3e..ebf8b66cb4 100644 --- a/src/ape_networks/_cli.py +++ b/src/ape_networks/_cli.py @@ -89,7 +89,7 @@ def make_sub_tree(data: dict, create_tree: Callable) -> Tree: 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)} - for network_name, network in _networks.items(): + for network in _networks.values(): providers = network["providers"] if providers: network_tree = make_sub_tree(network, ecosystem_tree.add) @@ -112,7 +112,7 @@ def make_sub_tree(data: dict, create_tree: Callable) -> Tree: except ValueError as err: try: data_str = json.dumps(network_data) - except Exception: + except Exception: # noqa: BLE001 data_str = str(network_data) raise NetworkError( @@ -208,7 +208,7 @@ def _run( finally: try: provider.disconnect() - except Exception: + except Exception: # noqa: BLE001 # Prevent not being able to CTRL-C. cli_ctx.abort("Terminated") @@ -246,7 +246,7 @@ def kill(cli_ctx, process_ids, kill_all): click.echo("Stopped the following node(s):") pids_stopped = set() for pid, data in processes_killed.items(): - echo_rich_text(f"\t{repr(data)}") + echo_rich_text(f"\t{data!r}") pids_stopped.add(pid) if rest := [pid for pid in process_ids if pid not in pids_stopped]: diff --git a/src/ape_networks/config.py b/src/ape_networks/config.py index b1ae538d13..ef89ad6987 100644 --- a/src/ape_networks/config.py +++ b/src/ape_networks/config.py @@ -1,3 +1,4 @@ +from pydantic import Field from pydantic_settings import SettingsConfigDict from ape.api.config import PluginConfig @@ -23,7 +24,7 @@ class CustomNetwork(PluginConfig): default_provider: str = "node" """The default provider plugin to use. Default is the default node provider.""" - request_header: dict = {} + request_header: dict = Field(default_factory=dict) """The HTTP request header.""" model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NETWORKS_") @@ -37,5 +38,5 @@ def is_fork(self) -> bool: class NetworksConfig(PluginConfig): - custom: list[CustomNetwork] = [] + custom: list[CustomNetwork] = Field(default_factory=list) model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NETWORKS_") diff --git a/src/ape_node/provider.py b/src/ape_node/provider.py index 893d32ce32..bc8f74afca 100644 --- a/src/ape_node/provider.py +++ b/src/ape_node/provider.py @@ -13,7 +13,7 @@ from geth.chain import initialize_chain as initialize_gethdev_chain from geth.process import BaseGethProcess from geth.wrapper import ALL_APIS, construct_test_chain_kwargs -from pydantic import field_validator +from pydantic import Field, field_validator from pydantic_settings import SettingsConfigDict from requests.exceptions import ConnectionError from web3.middleware import ExtraDataToPOAMiddleware @@ -207,11 +207,8 @@ def __init__( geth_kwargs.pop("password", None) # Ensure IPC path has correct name. - if ipc_path_kwarg := geth_kwargs.get("ipc_path"): - if ipc_path_kwarg.endswith("geth.ipc"): - geth_kwargs["ipc_path"] = ipc_path_kwarg.replace( - "geth.ipc", f"{self.process_name}.ipc" - ) + if (ipc_path_kwarg := geth_kwargs.get("ipc_path")) and ipc_path_kwarg.endswith("geth.ipc"): + geth_kwargs["ipc_path"] = ipc_path_kwarg.replace("geth.ipc", f"{self.process_name}.ipc") # Ensure a clean data-dir. self._clean() @@ -422,11 +419,17 @@ def wait(self, *args, **kwargs): class EthereumNetworkConfig(PluginConfig): # Make sure you are running the right networks when you try for these - mainnet: dict = {} - holesky: dict = {} - sepolia: dict = {} + mainnet: dict = Field(default_factory=dict) + holesky: dict = Field(default_factory=dict) + sepolia: dict = Field(default_factory=dict) # Make sure to run via `geth --dev` (or similar) - local: dict = {**DEFAULT_SETTINGS.copy(), "chain_id": DEFAULT_TEST_CHAIN_ID, "block_time": 0} + local: dict = Field( + default_factory=lambda: { + **DEFAULT_SETTINGS, + "chain_id": DEFAULT_TEST_CHAIN_ID, + "block_time": 0, + } + ) model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NODE_") @@ -483,7 +486,7 @@ class EthereumNodeConfig(PluginConfig): based on your node's client-version and available RPCs. """ - request_headers: dict = {} + request_headers: dict = Field(default_factory=dict) """ Optionally specify request headers to use whenever using this provider. """ diff --git a/src/ape_pm/_cli.py b/src/ape_pm/_cli.py index 7b261fb569..5d8ebb6a04 100644 --- a/src/ape_pm/_cli.py +++ b/src/ape_pm/_cli.py @@ -147,7 +147,7 @@ def _package_callback(ctx, param, value): # Check if is a local package. try: path = Path(value).absolute() - except Exception: + except Exception: # noqa: BLE001, S110 pass else: if path.exists(): @@ -230,7 +230,7 @@ def install(cli_ctx, package, name, version, ref, force, config_override, no_rec try: dependency = pm.dependencies.install(**package, use_cache=not force, recurse=not no_recurse) - except Exception as err: + except Exception as err: # noqa: BLE001 cli_ctx.logger.log_error(err) sys.exit(1) @@ -320,7 +320,7 @@ def _uninstall(dependency: "Dependency", yes: bool = False) -> bool: try: dependency.uninstall() - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error(f"Failed uninstalling '{key}': {err}") return False @@ -376,7 +376,7 @@ def compile(cli_ctx, name, version, force, config_override): def _compile_dependency(cli_ctx, dependency: "Dependency", force: bool): try: result = dependency.compile(use_cache=not force) - except Exception as err: + except Exception as err: # noqa: BLE001 cli_ctx.logger.error(str(err)) else: if result: diff --git a/src/ape_pm/compiler.py b/src/ape_pm/compiler.py index 0e0a9ebeb8..45d2a73e05 100644 --- a/src/ape_pm/compiler.py +++ b/src/ape_pm/compiler.py @@ -125,7 +125,7 @@ def enrich_error(self, err: ContractLogicError) -> ContractLogicError: try: contract = self.chain_manager.contracts.instance_at(address) - except Exception: + except Exception: # noqa: BLE001 return err if ( diff --git a/src/ape_pm/dependency.py b/src/ape_pm/dependency.py index bdb7eac57e..ac602e3e0b 100644 --- a/src/ape_pm/dependency.py +++ b/src/ape_pm/dependency.py @@ -219,7 +219,7 @@ def fetch(self, destination: Path): version = self.version or "latest" try: self._fetch_version(version, destination) - except Exception as err_from_version_approach: + except Exception as err_from_version_approach: # noqa: BLE001 logger.warning( f"No official release found for version '{version}'. " "Use `ref:` instead of `version:` for release tags. " @@ -233,7 +233,7 @@ def fetch(self, destination: Path): shutil.rmtree(destination, onerror=_remove_readonly) try: self._fetch_ref(version, destination) - except Exception: + except Exception: # noqa: BLE001 # NOTE: Ignore this error, it was merely a last attempt. raise err_from_version_approach @@ -241,14 +241,14 @@ def _fetch_ref(self, ref: str, destination: Path): options = _version_to_options(ref) attempt = 0 num_attempts = len(options) - for ref in options: + for ref_option in options: attempt += 1 try: self._github_client.clone_repo( self.org_name, self.repo_name, destination, - branch=ref, + branch=ref_option, scheme=self.scheme, ) except Exception: @@ -398,7 +398,7 @@ def _get_version_from_package_json( try: data = json.loads(package_json.read_text()) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.warning(f"Failed to parse package.json: {err}") return None @@ -555,10 +555,8 @@ def version_from_package_data(self) -> str | None: @cached_property def download_archive_url(self) -> str: - if not (version := self.version): - if not (version := self.version_from_package_data): - # Not sure if this is possible, but just in case API data changes or something. - raise ProjectError(f"Unable to find version for package '{self.package_id}'.") + if not (version := self.version) and not (version := self.version_from_package_data): + raise ProjectError(f"Unable to find version for package '{self.package_id}'.") releases = self.package_data.get("releases", {}) if version not in releases: @@ -596,8 +594,7 @@ def _fetch_archive_file(self, destination) -> Path: with requests.get(download_url, stream=True) as response: response.raise_for_status() with open(archive_destination, "wb") as file: - for chunk in response.iter_content(chunk_size=8192): # 8 KB - file.write(chunk) + file.writelines(response.iter_content(chunk_size=8192)) return archive_destination diff --git a/src/ape_pm/project.py b/src/ape_pm/project.py index 89930b8b1d..7d1540acb7 100644 --- a/src/ape_pm/project.py +++ b/src/ape_pm/project.py @@ -5,7 +5,7 @@ from ape.utils._github import _GithubClient, github_client -if sys.version_info.minor >= 11: +if sys.version_info >= (3, 11): # 3.11 or greater # NOTE: type-ignore is for when running mypy on python versions < 3.11 import tomllib # type: ignore[import-not-found] @@ -42,7 +42,7 @@ def extract_config(self, **overrides) -> ApeConfig: try: brownie_config_data = safe_load(text) or {} - except Exception: + except Exception: # noqa: BLE001 brownie_config_data = {} contracts_folder = brownie_config_data.get("contracts_folder", "contracts") @@ -204,12 +204,9 @@ def _parse_gitmodules(self) -> list[dict[str, str]]: for line in content.splitlines(): line = line.strip() - if line.startswith("[submodule"): - # Add the submodule we have been building to the list - # if it exists. This happens on submodule after the first one. - if submodule: - submodules.append(submodule) - submodule = {} + if line.startswith("[submodule") and submodule: + submodules.append(submodule) + submodule = {} for key in ("path", "url", "release", "branch"): if not line.startswith(f"{key} ="): @@ -358,4 +355,4 @@ def _parse_remappings( # Item seems like a dependency but not found in `dependencies`. ape_sol_remappings.add(f_remap) - return sorted(list(ape_sol_remappings)) + return sorted(ape_sol_remappings) diff --git a/src/ape_run/_cli.py b/src/ape_run/_cli.py index 1602811418..e3877f975c 100644 --- a/src/ape_run/_cli.py +++ b/src/ape_run/_cli.py @@ -116,7 +116,7 @@ def _get_command(self, filepath: Path) -> click.Command | click.Group | None: with use_scripts_sys_path(filepath.parent.parent): try: cli_ns = run_script_module(filepath) - except Exception as e: + except Exception as e: # noqa: BLE001 logger.error_from_exception( e, f"Exception while parsing script: {relative_filepath}" ) diff --git a/src/ape_test/_cli.py b/src/ape_test/_cli.py index 7b83c3dd37..b7b6c08512 100644 --- a/src/ape_test/_cli.py +++ b/src/ape_test/_cli.py @@ -72,7 +72,7 @@ def parse_args(self, ctx, args: list[str]) -> list[str]: @click.command( add_help_option=False, # NOTE: This allows pass-through to pytest's help short_help="Launches pytest and runs the tests for a project", - context_settings=dict(ignore_unknown_options=True), + context_settings={"ignore_unknown_options": True}, cls=ApeTestCommand, ) # NOTE: Using '.value' because more performant. diff --git a/src/ape_test/_watch.py b/src/ape_test/_watch.py index 95627cf4f4..682cef98d0 100644 --- a/src/ape_test/_watch.py +++ b/src/ape_test/_watch.py @@ -1,7 +1,7 @@ import threading import time from collections.abc import Iterable -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from functools import cached_property from pathlib import Path from subprocess import run as run_subprocess @@ -46,7 +46,7 @@ def emit_trigger(): global trigger with trigger_lock: - trigger = datetime.now() + trigger = datetime.now(timezone.utc) class EventHandler(events.FileSystemEventHandler): @@ -79,13 +79,13 @@ def process_event(self, event: events.FileSystemEvent) -> None: def _run_ape_test(*pytest_args): - return run_subprocess(["ape", "test", *[f"{a}" for a in pytest_args]]) + return run_subprocess(["ape", "test", *[f"{a}" for a in pytest_args]], check=False) def _run_main_loop(delay: float, *pytest_args: str) -> None: global trigger - now = datetime.now() + now = datetime.now(timezone.utc) if trigger and now - trigger > timedelta(seconds=delay): _run_ape_test(*pytest_args) diff --git a/src/ape_test/accounts.py b/src/ape_test/accounts.py index 1abc228ed2..43b89721fe 100644 --- a/src/ape_test/accounts.py +++ b/src/ape_test/accounts.py @@ -2,6 +2,8 @@ from functools import cached_property from typing import cast +from pydantic import Field + from ape.api.accounts import TestAccountAPI, TestAccountContainerAPI from ape.exceptions import ProviderNotConnectedError from ape.types import AddressType @@ -11,7 +13,7 @@ class TestAccountContainer(TestAccountContainerAPI): - generated_accounts: list["TestAccount"] = [] + generated_accounts: list["TestAccount"] = Field(default_factory=list) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/ape_test/config.py b/src/ape_test/config.py index f7a030f5e1..45e0e6d057 100644 --- a/src/ape_test/config.py +++ b/src/ape_test/config.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, NewType -from pydantic import NonNegativeInt, field_validator +from pydantic import Field, NonNegativeInt, field_validator from pydantic_settings import SettingsConfigDict from ape.api.config import PluginConfig @@ -37,7 +37,7 @@ class GasConfig(PluginConfig): Configuration related to test gas reports. """ - exclude: list[GasExclusion] = [] + exclude: list[GasExclusion] = Field(default_factory=list) """ Contract methods patterns to skip. Specify ``contract_name:`` and not ``method_name:`` to skip all methods in the contract. Only specify @@ -46,7 +46,7 @@ class GasConfig(PluginConfig): use ``prefix_*`` to skip all items with a certain prefix. """ - reports: list[str] = [] + reports: list[str] = Field(default_factory=list) """ Report-types to use. Currently, only supports `terminal`. """ @@ -117,7 +117,7 @@ class CoverageConfig(PluginConfig): Enable reports. """ - exclude: list[CoverageExclusion] = [] + exclude: list[CoverageExclusion] = Field(default_factory=list) """ Contract methods patterns to skip. Specify ``contract_name:`` and not ``method_name:`` to skip all methods in the contract. Only specify diff --git a/src/ape_test/provider.py b/src/ape_test/provider.py index 9e71017348..390c1832c1 100644 --- a/src/ape_test/provider.py +++ b/src/ape_test/provider.py @@ -460,12 +460,14 @@ def set_timestamp(self, new_timestamp: int): r"timestamp must be strictly later than parent, " r"but is 0 seconds before\.\n- child\s{2}: (\d*)\n- parent : (\d*)\.\s*" ) - if match := re.match(pattern, str(err)): - if groups := match.groups(): - if groups[0].strip() == groups[1].strip(): - # Handle race condition when block headers are the same. - # Treat as noop, same as pre-check. - return + if ( + (match := re.match(pattern, str(err))) + and (groups := match.groups()) + and groups[0].strip() == groups[1].strip() + ): + # Handle race condition when block headers are the same. + # Treat as noop, same as pre-check. + return raise ProviderError(f"Failed to time travel: {err}") from err diff --git a/tests/conftest.py b/tests/conftest.py index d675f3b20f..8dd90bd54f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,7 @@ def validate_cwd(start_dir): # TODO: Find root cause and fix there. try: os.getcwd() - except Exception: + except Exception: # noqa: BLE001 # Change back to project root, hopefully. os.chdir(start_dir) @@ -523,7 +523,7 @@ def invoke(self, *subcommand: str, input=None, timeout: int = 40, env: dict | No if self.project: try: here = os.getcwd() - except Exception: + except Exception: # noqa: BLE001 here = None os.chdir(f"{self.project.path}") diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index da6eb1844e..f5e5bbca03 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -323,7 +323,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): def run(self): try: self._run_until_stop(timeout_iterations=self._max_iterations) - except Exception as err: + except Exception as err: # noqa: BLE001 self._exception = err def stop(self): diff --git a/tests/functional/geth/test_chain.py b/tests/functional/geth/test_chain.py index 0dcdac5e3b..0397c8ca0e 100644 --- a/tests/functional/geth/test_chain.py +++ b/tests/functional/geth/test_chain.py @@ -1,4 +1,5 @@ import pytest + from tests.conftest import geth_process_test diff --git a/tests/functional/geth/test_network_manager.py b/tests/functional/geth/test_network_manager.py index d576dc4d94..2ce9d94cc3 100644 --- a/tests/functional/geth/test_network_manager.py +++ b/tests/functional/geth/test_network_manager.py @@ -60,9 +60,8 @@ def test_fork_upstream_provider_request_headers( }, "node": {"request_headers": {"X-Provider": "node"}}, } - with project.temp_config(**config): - with networks.fork(): - call = mock_fork_provider.partial_call + with project.temp_config(**config), networks.fork(): + call = mock_fork_provider.partial_call settings = call[1]["provider_settings"]["fork"]["ethereum"]["sepolia"] headers = settings["upstream_provider_request_headers"] diff --git a/tests/functional/geth/test_provider.py b/tests/functional/geth/test_provider.py index df0e5a98b3..32c0ac0131 100644 --- a/tests/functional/geth/test_provider.py +++ b/tests/functional/geth/test_provider.py @@ -308,17 +308,21 @@ def test_connect_using_only_ipc_for_uri_already_connected(project, networks, get Shows we can remote-connect to a node that is already running when it exposes its IPC path. """ ipc_path = geth_provider.ipc_path - with project.temp_config(node={"ethereum": {"local": {"uri": f"{ipc_path}"}}}): - with networks.ethereum.local.use_provider("node") as node: - assert node.uri == f"{ipc_path}" + with ( + project.temp_config(node={"ethereum": {"local": {"uri": f"{ipc_path}"}}}), + networks.ethereum.local.use_provider("node") as node, + ): + assert node.uri == f"{ipc_path}" @geth_process_test def test_connect_using_ipc(process_factory_patch, project, networks, geth_provider): ipc_path = geth_provider.ipc_path - with project.temp_config(node={"ethereum": {"local": {"uri": f"{ipc_path}"}}}): - with networks.ethereum.local.use_provider("node") as node: - assert node.uri == f"{ipc_path}" + with ( + project.temp_config(node={"ethereum": {"local": {"uri": f"{ipc_path}"}}}), + networks.ethereum.local.use_provider("node") as node, + ): + assert node.uri == f"{ipc_path}" @geth_process_test @@ -334,46 +338,45 @@ def test_connect_request_headers(project, geth_provider, networks): }, "node": {"request_headers": {"h3": 3, "USER-AGENT": "custom-geth-client/v100"}}, } - with project.temp_config(**config): - with networks.ethereum.local.use_provider("node") as geth: - w3_provider = geth.web3.provider - if isinstance(w3_provider, AutoProvider): - for pot_provider_fn in w3_provider._potential_providers: - pot_provider = pot_provider_fn() - if not isinstance(pot_provider, HTTPProvider): - continue - else: - http_provider = pot_provider - - elif isinstance(w3_provider, HTTPProvider): - http_provider = w3_provider - - else: - pytest.fail("Not using HTTP. Please adjust test.") - - assert http_provider is not None, "Setup failed - HTTP Provider still None." - - assert isinstance(http_provider._request_kwargs, dict) - actual = http_provider._request_kwargs["headers"] - assert actual["h0"] == 0 # top-level - assert actual["h1"] == 1 # ecosystem - assert actual["h2"] == 2 # network - assert actual["h3"] == 3 # provider - - # Also, assert Ape's default user-agent strings. - assert actual["User-Agent"].startswith("Ape/") - assert "Python" in actual["User-Agent"] - assert "ape-ethereum" in actual["User-Agent"] - assert "web3.py/" in actual["User-Agent"] - - # Show other default headers. - assert actual["Content-Type"] == "application/json" - - # Show appended user-agents strings. - assert "myapp/2.0" in actual["User-Agent"] - assert "ETH/1.0" in actual["User-Agent"] - assert "MyPrivateNetwork/0.0.1" in actual["User-Agent"] - assert "custom-geth-client/v100" in actual["User-Agent"] + with project.temp_config(**config), networks.ethereum.local.use_provider("node") as geth: + w3_provider = geth.web3.provider + if isinstance(w3_provider, AutoProvider): + for pot_provider_fn in w3_provider._potential_providers: + pot_provider = pot_provider_fn() + if not isinstance(pot_provider, HTTPProvider): + continue + else: + http_provider = pot_provider + + elif isinstance(w3_provider, HTTPProvider): + http_provider = w3_provider + + else: + pytest.fail("Not using HTTP. Please adjust test.") + + assert http_provider is not None, "Setup failed - HTTP Provider still None." + + assert isinstance(http_provider._request_kwargs, dict) + actual = http_provider._request_kwargs["headers"] + assert actual["h0"] == 0 # top-level + assert actual["h1"] == 1 # ecosystem + assert actual["h2"] == 2 # network + assert actual["h3"] == 3 # provider + + # Also, assert Ape's default user-agent strings. + assert actual["User-Agent"].startswith("Ape/") + assert "Python" in actual["User-Agent"] + assert "ape-ethereum" in actual["User-Agent"] + assert "web3.py/" in actual["User-Agent"] + + # Show other default headers. + assert actual["Content-Type"] == "application/json" + + # Show appended user-agents strings. + assert "myapp/2.0" in actual["User-Agent"] + assert "ETH/1.0" in actual["User-Agent"] + assert "MyPrivateNetwork/0.0.1" in actual["User-Agent"] + assert "custom-geth-client/v100" in actual["User-Agent"] @geth_process_test @@ -883,7 +886,7 @@ def test_start(process_factory_patch, convert, project, geth_provider): with project.temp_config(test={"balance": amount}): try: geth_provider.start() - except Exception: + except Exception: # noqa: BLE001, S110 pass # Exceptions are fine here. actual = process_factory_patch.call_args[1]["balance"] @@ -900,7 +903,7 @@ def test_start_from_ws_uri(process_factory_patch, project, geth_provider, key): geth_provider.provider_settings = {} try: geth_provider.start() - except Exception: + except Exception: # noqa: BLE001, S110 pass # Exceptions are fine here. geth_provider.provider_settings = settings diff --git a/tests/functional/geth/test_trace.py b/tests/functional/geth/test_trace.py index e93b552376..416582a9c6 100644 --- a/tests/functional/geth/test_trace.py +++ b/tests/functional/geth/test_trace.py @@ -119,9 +119,9 @@ def assert_rich_output(rich_capture: list[str], expected: str): assert actual_lines, "No output." output = "\n".join(actual_lines) - for actual, expected in zip(actual_lines, expected_lines, strict=True): + for actual, expected_line in zip(actual_lines, expected_lines, strict=True): fail_message = f"""\n - \tPattern: {expected}\n + \tPattern: {expected_line}\n \tLine : {actual}\n \n Complete output: @@ -129,10 +129,10 @@ def assert_rich_output(rich_capture: list[str], expected: str): """ try: - assert re.match(expected, actual), fail_message + assert re.match(expected_line, actual), fail_message except AssertionError: raise # Let assertion errors raise as normal. - except Exception as err: + except Exception as err: # noqa: BLE001 pytest.fail(f"{fail_message}\n{err}") actual_len = len(actual_lines) diff --git a/tests/functional/test_accounts.py b/tests/functional/test_accounts.py index ef931fe30d..98d6b74f25 100644 --- a/tests/functional/test_accounts.py +++ b/tests/functional/test_accounts.py @@ -148,7 +148,7 @@ def test_sign_message_with_prompts(runner, keyfile_account, message): def test_sign_eip712_message_shows_custom_types(signer): baz = Baz(addr=signer.address) foo = Foo(bar=signer.address, baz=baz) - display_msg, msg = _get_signing_message_with_display(foo) + display_msg, _msg = _get_signing_message_with_display(foo) expected = """ Signing EIP712 Message Domain @@ -261,9 +261,8 @@ def test_transfer_with_prompts(runner, receiver, keyfile_account): assert receipt.receiver == receiver # "n": don't sign - with runner.isolation(input="n\n"): - with pytest.raises(SignatureError): - keyfile_account.transfer(receiver, "1 gwei") + with runner.isolation(input="n\n"), pytest.raises(SignatureError): + keyfile_account.transfer(receiver, "1 gwei") def test_transfer_using_type_0(sender, receiver, convert): @@ -632,11 +631,13 @@ def test_unlock_from_prompt_retries_and_succeeds(runner, keyfile_account): def test_unlock_from_prompt_fails_after_three_attempts(runner, keyfile_account): - with runner.isolation( - input=f"{INVALID_PASSPHRASE}\n{INVALID_PASSPHRASE}\n{INVALID_PASSPHRASE}\n" + with ( + runner.isolation( + input=f"{INVALID_PASSPHRASE}\n{INVALID_PASSPHRASE}\n{INVALID_PASSPHRASE}\n" + ), + pytest.raises(AccountsError, match="Failed to unlock after 3 attempts."), ): - with pytest.raises(AccountsError, match="Failed to unlock after 3 attempts."): - keyfile_account.unlock() + keyfile_account.unlock() assert keyfile_account.locked @@ -949,7 +950,7 @@ def test_load_public_key_from_keyfile(runner, keyfile_account): assert ( to_hex(keyfile_account.public_key) - == "0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5" # noqa: E501 + == "0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5" ) # no need for password when loading from the keyfile assert keyfile_account.public_key @@ -994,14 +995,12 @@ def test_generate_account_invalid_passphrase(): def test_generate_account_insecure_passphrase(delete_account_after): short_alias = "shortaccount" - with delete_account_after(short_alias): - with pytest.warns(UserWarning, match="short"): - generate_account(short_alias, "short") + with delete_account_after(short_alias), pytest.warns(UserWarning, match="short"): + generate_account(short_alias, "short") simple_alias = "simpleaccount" - with delete_account_after(simple_alias): - with pytest.warns(UserWarning, match="simple"): - generate_account(simple_alias, "simple") + with delete_account_after(simple_alias), pytest.warns(UserWarning, match="simple"): + generate_account(simple_alias, "simple") def test_import_account_from_mnemonic(delete_account_after): @@ -1043,14 +1042,12 @@ def test_import_account_from_mnemonic_invalid_passphrase(): def test_import_account_from_mnemonic_insecure_passphrase(delete_account_after): short_alias = "iafmshortaccount" - with delete_account_after(short_alias): - with pytest.warns(UserWarning, match="short"): - import_account_from_mnemonic(short_alias, "short", MNEMONIC) + with delete_account_after(short_alias), pytest.warns(UserWarning, match="short"): + import_account_from_mnemonic(short_alias, "short", MNEMONIC) simple_alias = "iafmsimpleaccount" - with delete_account_after(simple_alias): - with pytest.warns(UserWarning, match="simple"): - import_account_from_mnemonic(simple_alias, "simple", MNEMONIC) + with delete_account_after(simple_alias), pytest.warns(UserWarning, match="simple"): + import_account_from_mnemonic(simple_alias, "simple", MNEMONIC) def test_import_account_from_private_key(delete_account_after): @@ -1098,14 +1095,12 @@ def test_import_account_from_private_key_invalid_passphrase(): def test_import_account_from_private_key_insecure_passphrase(delete_account_after): short_alias = "iafpkshortaccount" - with delete_account_after(short_alias): - with pytest.warns(UserWarning, match="short"): - import_account_from_private_key(short_alias, "short", PRIVATE_KEY) + with delete_account_after(short_alias), pytest.warns(UserWarning, match="short"): + import_account_from_private_key(short_alias, "short", PRIVATE_KEY) simple_alias = "iafpksimpleaccount" - with delete_account_after(simple_alias): - with pytest.warns(UserWarning, match="simple"): - import_account_from_private_key(simple_alias, "simple", PRIVATE_KEY) + with delete_account_after(simple_alias), pytest.warns(UserWarning, match="simple"): + import_account_from_private_key(simple_alias, "simple", PRIVATE_KEY) def test_load(account_manager, keyfile_account): diff --git a/tests/functional/test_block_container.py b/tests/functional/test_block_container.py index b7ae5cca1b..5829c7a300 100644 --- a/tests/functional/test_block_container.py +++ b/tests/functional/test_block_container.py @@ -143,9 +143,11 @@ def test_poll_blocks_timeout( ): poller = chain_that_mined_5.blocks.poll_blocks(new_block_timeout=1) - with pytest.raises(ProviderError, match=r"Timed out waiting for next block."): - with PollDaemon("blocks", poller, lambda x: None, lambda: False): - time.sleep(1.5) + with ( + pytest.raises(ProviderError, match=r"Timed out waiting for next block."), + PollDaemon("blocks", poller, lambda x: None, lambda: False), + ): + time.sleep(1.5) def test_poll_blocks_reorg_with_timeout( @@ -154,26 +156,28 @@ def test_poll_blocks_reorg_with_timeout( blocks: Queue = Queue(maxsize=6) poller = chain_that_mined_5.blocks.poll_blocks(new_block_timeout=1) - with pytest.raises(ProviderError, match=r"Timed out waiting for next block."): - with PollDaemon("blocks", poller, blocks.put, blocks.full): - # Sleep first to ensure listening before mining. - time.sleep(1) + with ( + pytest.raises(ProviderError, match=r"Timed out waiting for next block."), + PollDaemon("blocks", poller, blocks.put, blocks.full), + ): + # Sleep first to ensure listening before mining. + time.sleep(1) - snapshot = chain_that_mined_5.snapshot() - chain_that_mined_5.mine(2) + snapshot = chain_that_mined_5.snapshot() + chain_that_mined_5.mine(2) - # Wait to allow blocks before re-org to get yielded - time.sleep(5) + # Wait to allow blocks before re-org to get yielded + time.sleep(5) - # Simulate re-org by reverting to the snapshot - chain_that_mined_5.restore(snapshot) + # Simulate re-org by reverting to the snapshot + chain_that_mined_5.restore(snapshot) - # Allow it time to trigger realizing there was a re-org - time.sleep(1) - chain_that_mined_5.mine(2) - time.sleep(1) + # Allow it time to trigger realizing there was a re-org + time.sleep(1) + chain_that_mined_5.mine(2) + time.sleep(1) - chain_that_mined_5.mine(3) + chain_that_mined_5.mine(3) def test_poll_blocks_future(chain_that_mined_5, eth_tester_provider, owner, PollDaemon): diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index debf20af8c..5be3918657 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -1,5 +1,5 @@ from collections import defaultdict -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest @@ -151,11 +151,13 @@ def test_snapshot_and_restore_switched_chains(networks, chain): """ snapshot = chain.snapshot() # Switch chains. - with networks.ethereum.local.use_provider( - "test", provider_settings={"chain_id": 11191919191991918223773} + with ( + networks.ethereum.local.use_provider( + "test", provider_settings={"chain_id": 11191919191991918223773} + ), + pytest.raises(UnknownSnapshotError), ): - with pytest.raises(UnknownSnapshotError): - chain.restore(snapshot) + chain.restore(snapshot) def test_isolate(chain, vyper_contract_instance, owner): @@ -238,7 +240,7 @@ def test_set_pending_timestamp_failure(chain): ValueError, match="Cannot give both `timestamp` and `deltatime` arguments together." ): chain.mine( - timestamp=int(datetime.now().timestamp() + timedelta(seconds=10).seconds), + timestamp=int(datetime.now(timezone.utc).timestamp() + timedelta(seconds=10).seconds), deltatime=10, ) diff --git a/tests/functional/test_compilers.py b/tests/functional/test_compilers.py index 42a4e2dbe3..8408ef24f0 100644 --- a/tests/functional/test_compilers.py +++ b/tests/functional/test_compilers.py @@ -231,11 +231,11 @@ def test_compile_in_project_where_source_id_matches_local_project(project, compi argument = Path(contract.source_id) # Compile the file with the same name but different content. - result = [ + result = next( x for x in compilers.compile([argument], project=temp_project) if x.name == contract.name - ][0] + ) # It should reflect the new content and not the one with the same # source ID from the local project. diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 5535b9f6c6..fa78957bd0 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any import pytest -from pydantic import ValidationError +from pydantic import Field, ValidationError from pydantic_settings import SettingsConfigDict from ape.api.config import ApeConfig, ConfigEnum, PluginConfig @@ -318,20 +318,22 @@ def test_deployments_integer_type_addresses(networks, project): def test_deployments_bad_ecosystem(project): deployments = _create_deployments(ecosystem_name="madeup") - with project.temp_config(deployments=deployments): - with pytest.raises( - ConfigError, match=r"Invalid ecosystem 'madeup' in deployments config\." - ): - _ = project.config.deployments + with ( + project.temp_config(deployments=deployments), + pytest.raises(ConfigError, match=r"Invalid ecosystem 'madeup' in deployments config\."), + ): + _ = project.config.deployments def test_deployments_bad_network(project): deployments = _create_deployments(network_name="madeup") - with project.temp_config(deployments=deployments): - with pytest.raises( + with ( + project.temp_config(deployments=deployments), + pytest.raises( ConfigError, match=r"Invalid network 'ethereum:madeup' in deployments config\." - ): - _ = project.config.deployments + ), + ): + _ = project.config.deployments def _create_deployments( @@ -405,7 +407,7 @@ def test_network_gas_limit_string_config(gas_limit, project): assert actual.local.gas_limit == "max" -@pytest.mark.parametrize("gas_limit", (1234, "1234", 0x4D2, "0x4D2")) +@pytest.mark.parametrize("gas_limit", (1234, "1234", "0x4D2")) def test_network_gas_limit_numeric_config(gas_limit, project): eth_config = _sepolia_with_gas_limit(gas_limit) with project.temp_config(**eth_config): @@ -459,7 +461,7 @@ class SubConfig(PluginConfig): bar: int = 1 class MyConfig(PluginConfig): - sub: dict[str, dict[str, SubConfig]] = {} + sub: dict[str, dict[str, SubConfig]] = Field(default_factory=dict) overrides = {"sub": {"baz": {"test": {"foo": 5}}}} actual = MyConfig.from_overrides(overrides) @@ -484,7 +486,7 @@ class MyConfig(PluginConfig): def test_plugin_config_with_union_dicts(override_0, override_1): class SubConfig(PluginConfig): bool_or_dict: bool | dict = True - dict_or_bool: dict | bool = {} + dict_or_bool: dict | bool = Field(default_factory=dict) config = SubConfig.from_overrides({"bool_or_dict": override_0, "dict_or_bool": override_1}) assert config.bool_or_dict == override_0 @@ -588,7 +590,7 @@ class MyConfig(PluginConfig): def test_contracts_folder(project): - with project.temp_config(**{"contracts_folder": "src"}): + with project.temp_config(contracts_folder="src"): assert project.contracts_folder.name == "src" @@ -601,9 +603,8 @@ def test_contracts_folder_invalid(project): """ Show that we can still attempt to use `.get_config()` when config is invalid. """ - with pytest.raises(ConfigError): - with project.temp_config(**{"contracts_folder": {}}): - _ = project.contracts_folder + with pytest.raises(ConfigError), project.temp_config(contracts_folder={}): + _ = project.contracts_folder # Ensure config is fine _after_ something invalid. assert project.contracts_folder diff --git a/tests/functional/test_contract_event.py b/tests/functional/test_contract_event.py index 802c36012c..c7c1857b8f 100644 --- a/tests/functional/test_contract_event.py +++ b/tests/functional/test_contract_event.py @@ -321,9 +321,11 @@ def test_poll_logs_timeout(vyper_contract_instance, eth_tester_provider, owner, new_block_timeout = 1 poller = vyper_contract_instance.NumberChange.poll_logs(new_block_timeout=new_block_timeout) - with pytest.raises(ProviderError) as err: - with PollDaemon("logs-timeout", poller, lambda x: None, lambda: False): - time.sleep(1.5) + with ( + pytest.raises(ProviderError) as err, + PollDaemon("logs-timeout", poller, lambda x: None, lambda: False), + ): + time.sleep(1.5) assert "Timed out waiting for next block" in str(err.value) diff --git a/tests/functional/test_contract_method_handler.py b/tests/functional/test_contract_method_handler.py index e56cf1c4bd..56a4f107e8 100644 --- a/tests/functional/test_contract_method_handler.py +++ b/tests/functional/test_contract_method_handler.py @@ -32,9 +32,9 @@ def test_encode_input_list_for_struct(chain, mocker, owner, method_abi_with_stru def test_info(contract_instance): # The `setNumber()` ABI defined a NatSpec. - abi = [x for x in contract_instance.contract_type.abi if getattr(x, "name", "") == "setNumber"][ - 0 - ] + abi = next( + x for x in contract_instance.contract_type.abi if getattr(x, "name", "") == "setNumber" + ) handler = ContractMethodHandler(contract=contract_instance, abis=[abi]) actual = handler.info expected_doc = ( @@ -58,9 +58,9 @@ def test_info(contract_instance): def test_info_no_natspec(project, vyper_contract_instance): # The `myNumber()` ABI does not have a natspec. - abi = [ + abi = next( x for x in project.VyperContract.contract_type.abi if getattr(x, "name", "") == "myNumber" - ][0] + ) handler = ContractMethodHandler(contract=vyper_contract_instance, abis=[abi]) actual = handler.info assert actual == "" diff --git a/tests/functional/test_contracts_cache.py b/tests/functional/test_contracts_cache.py index a962c4123c..11bd3eaa18 100644 --- a/tests/functional/test_contracts_cache.py +++ b/tests/functional/test_contracts_cache.py @@ -542,6 +542,37 @@ def test_get_proxy(chain, owner, minimal_proxy_container, vyper_contract_instanc assert actual == minimal_proxy.contract_type +def test_get_non_proxy_does_not_cache_negative_result(chain, mocker): + address = "0xBEbeBeBEbeBebeBeBEBEbebEBeBeBebeBeBebebe" + del chain.contracts.proxy_infos[address] + get_proxy_info = mocker.patch( + "ape_ethereum.ecosystem.Ethereum.get_proxy_info", return_value=None + ) + + chain.contracts.get(address, fetch_from_explorer=False) + chain.contracts.get(address, fetch_from_explorer=False) + + assert get_proxy_info.call_count == 2 + assert address not in chain.contracts.proxy_infos.memory + + +def test_get_proxy_caches_positive_result(chain, mocker): + address = "0xBEbeBeBEbeBebeBeBEBEbebEBeBeBebeBeBebebe" + target = "0x1234567890123456789012345678901234567890" + proxy_info = ProxyInfo(type=ProxyType.Minimal, target=target) + del chain.contracts.proxy_infos[address] + get_proxy_info = mocker.patch( + "ape_ethereum.ecosystem.Ethereum.get_proxy_info", return_value=proxy_info + ) + mocker.patch.object(chain.contracts, "_get_proxy_contract_type", return_value=None) + + chain.contracts.get(address, fetch_from_explorer=False) + chain.contracts.get(address, fetch_from_explorer=False) + + get_proxy_info.assert_called_once_with(address) + assert chain.contracts.proxy_infos[address] == proxy_info + + def test_get_proxy_implementation_missing(chain, owner, project): """ Proxy is cached but implementation is missing. diff --git a/tests/functional/test_default_sender_context_manager.py b/tests/functional/test_default_sender_context_manager.py index 68b590ee0f..3d48b7fd57 100644 --- a/tests/functional/test_default_sender_context_manager.py +++ b/tests/functional/test_default_sender_context_manager.py @@ -60,9 +60,8 @@ def test_nested_default_sender(solidity_contract_instance, owner, accounts, not_ with accounts.use_sender(owner): tx = solidity_contract_instance.setNumber(1) assert tx.transaction.sender == owner.address - with accounts.use_sender(not_owner): - with reverts(): - solidity_contract_instance.setNumber(2) + with accounts.use_sender(not_owner), reverts(): + solidity_contract_instance.setNumber(2) solidity_contract_instance.setNumber(3) assert tx.transaction.sender == owner.address @@ -71,8 +70,7 @@ def test_nested_default_sender(solidity_contract_instance, owner, accounts, not_ def test_with_error(solidity_contract_instance, account_manager, not_owner): # safe to use reverts with use_sender and when outside of the use_sender # there is no remaining default_user set - with reverts("!authorized"): - with account_manager.test_accounts.use_sender(not_owner): - solidity_contract_instance.setNumber(2) + with reverts("!authorized"), account_manager.test_accounts.use_sender(not_owner): + solidity_contract_instance.setNumber(2) assert account_manager.default_sender is None diff --git a/tests/functional/test_dependencies.py b/tests/functional/test_dependencies.py index 32c611bce6..fcf66107ec 100644 --- a/tests/functional/test_dependencies.py +++ b/tests/functional/test_dependencies.py @@ -298,7 +298,7 @@ def test_unpack(smaller_project): with create_tempdir() as tempdir: created = list(dep.unpack(tempdir)) assert len(created) > 0 - assert any([x.name == "default" for x in created]) + assert any(x.name == "default" for x in created) assert created[0].package_id.endswith("default") files = [x.name for x in (tempdir / "default" / "local" / "contracts").iterdir()] diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index 27567367b5..a847453501 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -1021,9 +1021,8 @@ def test_networks_multiple_networks_with_same_name(custom_networks_config_dict, data["networks"]["custom"][0]["name"] = "foonet" data["networks"]["custom"][1]["name"] = "foonet" expected = ".*More than one network named 'foonet' in ecosystem 'ethereum'.*" - with project.temp_config(**data): - with pytest.raises(NetworkError, match=expected): - _ = ethereum.networks + with project.temp_config(**data), pytest.raises(NetworkError, match=expected): + _ = ethereum.networks def test_getattr(ethereum): @@ -1101,7 +1100,7 @@ def test_default_network_name_when_not_set_and_no_local_uses_only( if actual == LOCAL_NETWORK_NAME: # For some reason, this test is flake-y. Offer more info # to try and debug when this happens (intermittent CI failure). - all_nets = ", ".join([x for x in ecosystem.networks.keys()]) + all_nets = ", ".join([x for x in ecosystem.networks]) pytest.fail( f"assert '{LOCAL_NETWORK_NAME}' == '{only_network}'. More info below:\n" f"ecosystem_name={ecosystem.name}\n" diff --git a/tests/functional/test_exceptions.py b/tests/functional/test_exceptions.py index 928bbb17e3..d5b7495a8f 100644 --- a/tests/functional/test_exceptions.py +++ b/tests/functional/test_exceptions.py @@ -118,7 +118,7 @@ def test_call_with_source_tb_and_not_txn(self, mocker, project_with_contract): # Have to raise for sys.exc_info() to be available. try: raise err - except Exception: + except Exception: # noqa: BLE001, S110 pass def assert_ape_traceback(err_arg): @@ -137,7 +137,7 @@ def assert_ape_traceback(err_arg): ) try: raise err2 - except Exception: + except Exception: # noqa: BLE001, S110 pass # No Ape frames are here. @@ -147,7 +147,7 @@ def assert_ape_traceback(err_arg): err3 = ContractLogicError(source_traceback=mock_tb, project=project_with_contract) try: raise err3 - except Exception: + except Exception: # noqa: BLE001, S110 pass assert_ape_traceback(err3) diff --git a/tests/functional/test_fixtures.py b/tests/functional/test_fixtures.py index 3f96ea1bb5..73da471304 100644 --- a/tests/functional/test_fixtures.py +++ b/tests/functional/test_fixtures.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar import pytest @@ -88,7 +88,7 @@ def test_isolation_snapshot_id_types(snapshot_id, fixtures): class IsolationManagerWithCustomSnapshot(IsolationManager): take_call_count = 0 restore_call_count = 0 - restore_called_with = [] + restore_called_with: ClassVar[list] = [] def take_snapshot(self) -> "SnapshotID | None": self.take_call_count += 1 diff --git a/tests/functional/test_network_manager.py b/tests/functional/test_network_manager.py index 9c2b177083..8968230aeb 100644 --- a/tests/functional/test_network_manager.py +++ b/tests/functional/test_network_manager.py @@ -338,9 +338,8 @@ def test_ecosystems_when_custom_has_bad_base_ecosystem( rf"base-ecosystem class '{eco_plugin_name}'\. " rf"Are you missing plugin 'ape-{eco_plugin_name}'\?" ) - with project.temp_config(**data): - with pytest.raises(NetworkError, match=expected): - _ = networks.ecosystems + with project.temp_config(**data), pytest.raises(NetworkError, match=expected): + _ = networks.ecosystems def test_fork_network_not_forkable(networks, eth_tester_provider): @@ -348,9 +347,8 @@ def test_fork_network_not_forkable(networks, eth_tester_provider): Show correct failure when trying to fork the local network. """ expected = "Unable to fork network 'local'." - with pytest.raises(NetworkError, match=expected): - with networks.fork(): - pass + with pytest.raises(NetworkError, match=expected), networks.fork(): + pass def test_fork_no_providers(networks, mock_sepolia, disable_fork_providers): @@ -359,9 +357,8 @@ def test_fork_no_providers(networks, mock_sepolia, disable_fork_providers): ape-hardhat or ape-foundry installed. """ expected = "No providers for network 'sepolia-fork'." - with pytest.raises(NetworkError, match=expected): - with networks.fork(): - pass + with pytest.raises(NetworkError, match=expected), networks.fork(): + pass def test_fork_use_non_existing_provider(networks, mock_sepolia): @@ -369,9 +366,11 @@ def test_fork_use_non_existing_provider(networks, mock_sepolia): Show correct failure when specifying a non-existing provider. """ expected = "No provider named 'NOT_EXISTS' in network 'sepolia-fork' in ecosystem 'ethereum'.*" - with pytest.raises(ProviderNotFoundError, match=expected): - with networks.fork(provider_name="NOT_EXISTS"): - pass + with ( + pytest.raises(ProviderNotFoundError, match=expected), + networks.fork(provider_name="NOT_EXISTS"), + ): + pass def test_fork(networks, mock_sepolia, mock_fork_provider): @@ -440,9 +439,11 @@ def test_fork_with_negative_block_number( def test_fork_past_genesis(networks, mock_sepolia, mock_fork_provider, eth_tester_provider): block_id = -10_000_000_000 - with pytest.raises(NetworkError, match="Unable to fork past genesis block."): - with networks.fork(block_number=block_id): - pass + with ( + pytest.raises(NetworkError, match="Unable to fork past genesis block."), + networks.fork(block_number=block_id), + ): + pass def test_getitem(networks): diff --git a/tests/functional/test_plugins.py b/tests/functional/test_plugins.py index a0a6f265db..395597855a 100644 --- a/tests/functional/test_plugins.py +++ b/tests/functional/test_plugins.py @@ -5,7 +5,7 @@ from ape.api import TransactionAPI from ape.exceptions import PluginVersionError from ape.logging import LogLevel -from ape.managers.plugins import _get_unimplemented_methods_warning +from ape.managers.plugins import PluginManager, _get_unimplemented_methods_warning from ape.plugins._utils import CORE_PLUGINS as CORE_PLUGINS_LIST from ape.plugins._utils import ( ApePluginsRepr, @@ -119,9 +119,7 @@ def test_all_plugins(self, plugin_metadata, package_names): class TestPluginMetadata: - @pytest.mark.parametrize( - "name", ("ape-foo-bar", "ape-foo-bar", "ape_foo_bar", "foo-bar", "foo_bar") - ) + @pytest.mark.parametrize("name", ("ape-foo-bar", "ape_foo_bar", "foo-bar", "foo_bar")) def test_names(self, name): metadata = PluginMetadata(name=name) assert metadata.name == "foo-bar" @@ -187,14 +185,14 @@ def test_install_str_only_remote(self): assert metadata.name == "foo" def test_is_available(self): - metadata = PluginMetadata(name=list(AVAILABLE_PLUGINS)[0]) + metadata = PluginMetadata(name=next(iter(AVAILABLE_PLUGINS))) assert metadata.is_available metadata = PluginMetadata(name="foobar") assert not metadata.is_available @parametrize_pip_cmd def test_prepare_install(self, pip_command): - metadata = PluginMetadata(name=list(AVAILABLE_PLUGINS)[0], pip_command=pip_command) + metadata = PluginMetadata(name=next(iter(AVAILABLE_PLUGINS)), pip_command=pip_command) actual = metadata._prepare_install(skip_confirmation=True) assert actual is not None arguments = actual.get("args", []) @@ -214,7 +212,7 @@ def test_prepare_install(self, pip_command): @parametrize_pip_cmd def test_prepare_install_upgrade(self, pip_command): - metadata = PluginMetadata(name=list(AVAILABLE_PLUGINS)[0], pip_command=pip_command) + metadata = PluginMetadata(name=next(iter(AVAILABLE_PLUGINS)), pip_command=pip_command) actual = metadata._prepare_install(upgrade=True, skip_confirmation=True) assert actual is not None arguments = actual.get("args", []) @@ -237,7 +235,7 @@ def test_prepare_install_upgrade(self, pip_command): @mark_specifiers_less_than_ape def test_prepare_install_version_smaller_than_ape(self, specifier, ape_caplog): - metadata = PluginMetadata(name=list(AVAILABLE_PLUGINS)[0], version=specifier) + metadata = PluginMetadata(name=next(iter(AVAILABLE_PLUGINS)), version=specifier) expected = ( r"Unable to install plugin\.\n" r"Reason: Doing so will downgrade Ape's version\.\n" @@ -259,7 +257,8 @@ def test_check_installed_python_39(self, mocker, get_dists_patch): assert metadata.check_installed() @pytest.mark.parametrize( - "plugin,expected", [(list(INSTALLED_PLUGINS)[0], True), (list(AVAILABLE_PLUGINS)[0], False)] + "plugin,expected", + [(next(iter(INSTALLED_PLUGINS)), True), (next(iter(AVAILABLE_PLUGINS)), False)], ) def test_check_installed_python_310_or_greater(self, plugin, expected): installed = PluginMetadata(name=plugin) @@ -416,6 +415,21 @@ def test_get_unimplemented_methods_warning_list_containing_plugin(abstract_metho assert actual == expected +def test_warn_not_fully_implemented_error_only_warns_once(mocker, monkeypatch): + monkeypatch.setattr(PluginManager, "_unimplemented_plugins", []) + warning = mocker.patch("ape.managers.plugins.logger.warning") + manager = PluginManager() + + manager._warn_not_fully_implemented_error(TransactionAPI, "incomplete") + manager._warn_not_fully_implemented_error(TransactionAPI, "incomplete") + + warning.assert_called_once_with( + "'TransactionAPI' from 'incomplete' is not fully implemented. " + "Remaining abstract methods: 'serialize_transaction, txn_hash'." + ) + assert manager._unimplemented_plugins == ["incomplete"] + + def test_core_plugins(): # In case any of these happen to be installed, and this feature # is broken, it will fail. If none are installed, the test will always pass. diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index 6d91acfc45..340b60afb1 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -135,7 +135,7 @@ def test_repr(): assert actual == expected -@pytest.mark.parametrize("name", ("contracts", "contracts")) +@pytest.mark.parametrize("name", ("contracts")) def test_contracts_folder_from_config(smaller_project, name): with smaller_project.temp_config(contracts_folder=name): assert smaller_project.contracts_folder == smaller_project.path / name @@ -399,7 +399,7 @@ def test_load_contracts(small_temp_project): try: contract_to_rm = small_temp_project.contracts["Other"] except KeyError: - existing_contracts = ",".join([k for k in small_temp_project.contracts.keys()]) + existing_contracts = ",".join([k for k in small_temp_project.contracts]) pytest.fail(f"Contract named 'Other' not found. Existing contracts: {existing_contracts}") return @@ -647,12 +647,12 @@ def test_add_compiler_data(project_with_dependency_config): settings={"outputSelection": {"path/to/Bar.vy": "*"}}, ) project.add_compiler_data([compiler_3]) - comp = [c for c in project.manifest.compilers if c.name == "test" and c.version == "2.0.0"][0] + comp = next(c for c in project.manifest.compilers if c.name == "test" and c.version == "2.0.0") assert "bar" not in comp.contractTypes assert "path/to/Bar.vy" not in comp.settings["outputSelection"] - new_comp = [c for c in project.manifest.compilers if c.name == "test" and c.version == "3.0.0"][ - 0 - ] + new_comp = next( + c for c in project.manifest.compilers if c.name == "test" and c.version == "3.0.0" + ) assert "bar" in new_comp.contractTypes assert "path/to/Bar.vy" in new_comp.settings["outputSelection"] @@ -1146,7 +1146,7 @@ def test_instance_map(self, project, vyper_contract_instance): bip122_chain_id = to_hex(project.provider.get_block(0).hash) expected_uri = f"blockchain://{bip122_chain_id[2:]}/block/" - for key in project.deployments.instance_map.keys(): + for key in project.deployments.instance_map: if key.startswith(expected_uri): return diff --git a/tests/functional/test_reverts_context_manager.py b/tests/functional/test_reverts_context_manager.py index abdfdfa8e7..b4bd819f2d 100644 --- a/tests/functional/test_reverts_context_manager.py +++ b/tests/functional/test_reverts_context_manager.py @@ -84,9 +84,8 @@ def test_revert_unexpected_error(error_contract, not_owner): Test when given a different error type than what was raised. """ expected = "Expected error 'OtherError' but was 'Unauthorized'" - with pytest.raises(AssertionError, match=expected): - with reverts(error_contract.OtherError): - error_contract.withdraw(sender=not_owner) + with pytest.raises(AssertionError, match=expected), reverts(error_contract.OtherError): + error_contract.withdraw(sender=not_owner) def test_revert_error_inputs(error_contract, not_owner): @@ -105,9 +104,11 @@ def test_revert_error_unexpected_inputs(error_contract, owner, not_owner): rf"Expected input 'addr' to be '{owner.address}' but was '{not_owner.address}'\." r"\nExpected input 'counter' to be '321' but was '123'\." ) - with pytest.raises(AssertionError, match=expected): - with reverts(error_contract.Unauthorized, addr=owner.address, counter=321): - error_contract.withdraw(sender=not_owner) + with ( + pytest.raises(AssertionError, match=expected), + reverts(error_contract.Unauthorized, addr=owner.address, counter=321), + ): + error_contract.withdraw(sender=not_owner) def test_revert_fails(owner, reverts_contract_instance): @@ -115,9 +116,8 @@ def test_revert_fails(owner, reverts_contract_instance): Test that ``AssertionError`` is raised if the supplied revert reason does not match the actual revert reason. """ - with pytest.raises(AssertionError): - with reverts("one"): - reverts_contract_instance.revertStrings(0, sender=owner) + with pytest.raises(AssertionError), reverts("one"): + reverts_contract_instance.revertStrings(0, sender=owner) def test_revert_pattern_fails(owner, reverts_contract_instance): @@ -125,9 +125,8 @@ def test_revert_pattern_fails(owner, reverts_contract_instance): Test that ``AssertionError`` is raised if the actual revert reason does not match the supplied revert pattern. """ - with pytest.raises(AssertionError): - with reverts(re.compile(r"[^zero]+")): - reverts_contract_instance.revertStrings(0, sender=owner) + with pytest.raises(AssertionError), reverts(re.compile(r"[^zero]+")): + reverts_contract_instance.revertStrings(0, sender=owner) def test_revert_partial_fails(owner, reverts_contract_instance): @@ -135,6 +134,5 @@ def test_revert_partial_fails(owner, reverts_contract_instance): Test that ``AssertionError`` is raised if the supplied revert reason does not match the actual revert reason exactly. """ - with pytest.raises(AssertionError): - with reverts("ze"): - reverts_contract_instance.revertStrings(0, sender=owner) + with pytest.raises(AssertionError), reverts("ze"): + reverts_contract_instance.revertStrings(0, sender=owner) diff --git a/tests/functional/test_test.py b/tests/functional/test_test.py index 80e9b208ef..fb81cd0ce2 100644 --- a/tests/functional/test_test.py +++ b/tests/functional/test_test.py @@ -525,4 +525,4 @@ def test_watch(mocker): # NOTE: We had a bug once where the args it received were not strings. # (wasn't deconstructing), so this check is important. - run_subprocess_patch.assert_called_once_with(["ape", "test", "-s"]) + run_subprocess_patch.assert_called_once_with(["ape", "test", "-s"], check=False) diff --git a/tests/functional/utils/test_abi.py b/tests/functional/utils/test_abi.py index f9e3bc30d0..1f76a91c02 100644 --- a/tests/functional/utils/test_abi.py +++ b/tests/functional/utils/test_abi.py @@ -128,7 +128,7 @@ def test_get_and_set_item(self, struct): def test_is_equal(self, struct): # Show struct equality works when props are the same. - assert struct == struct # even self + assert struct == struct # even self # noqa: PLR0124 new_struct = deepcopy(struct) assert struct == new_struct # Show changing a property makes them unequal. diff --git a/tests/integration/cli/conftest.py b/tests/integration/cli/conftest.py index b86c658511..4d198433fb 100644 --- a/tests/integration/cli/conftest.py +++ b/tests/integration/cli/conftest.py @@ -1,6 +1,7 @@ from importlib import import_module from pathlib import Path from shutil import copytree +from typing import ClassVar import pytest @@ -91,7 +92,7 @@ def project_dir_map(): """ class ProjectDirCache: - project_map: dict[str, Path] = {} + project_map: ClassVar[dict[str, Path]] = {} def load(self, name: str) -> Path: base_path = Path(__file__).parent / "projects" diff --git a/tests/integration/cli/projects/only-dependencies/__init__.py b/tests/integration/cli/projects/only-dependencies/__init__.py index e69de29bb2..b7b77a8a5a 100644 --- a/tests/integration/cli/projects/only-dependencies/__init__.py +++ b/tests/integration/cli/projects/only-dependencies/__init__.py @@ -0,0 +1 @@ +# noqa: N999 diff --git a/tests/integration/cli/projects/only-dependencies/dependency_in_project_only/__init__.py b/tests/integration/cli/projects/only-dependencies/dependency_in_project_only/__init__.py index e69de29bb2..b7b77a8a5a 100644 --- a/tests/integration/cli/projects/only-dependencies/dependency_in_project_only/__init__.py +++ b/tests/integration/cli/projects/only-dependencies/dependency_in_project_only/__init__.py @@ -0,0 +1 @@ +# noqa: N999 diff --git a/tests/integration/cli/projects/script/scripts/error_cli.py b/tests/integration/cli/projects/script/scripts/error_cli.py index 95dee78277..e38543e0f7 100644 --- a/tests/integration/cli/projects/script/scripts/error_cli.py +++ b/tests/integration/cli/projects/script/scripts/error_cli.py @@ -4,4 +4,4 @@ @click.command(short_help="Use a subcommand") def cli(): local_variable = "test foo bar" # noqa: F841 - raise Exception("Expected exception") + raise Exception("Expected exception") # noqa: TRY002 diff --git a/tests/integration/cli/projects/script/scripts/error_main.py b/tests/integration/cli/projects/script/scripts/error_main.py index 2681ad6fa3..293f208ded 100644 --- a/tests/integration/cli/projects/script/scripts/error_main.py +++ b/tests/integration/cli/projects/script/scripts/error_main.py @@ -5,4 +5,4 @@ def main(): local_variable = "test foo bar" # noqa: F841 provider = ape.chain.provider provider.set_timestamp(123123123123123123) - raise Exception("Expected exception") + raise Exception("Expected exception") # noqa: TRY002 diff --git a/tests/integration/cli/projects/script/scripts/error_no_def.py b/tests/integration/cli/projects/script/scripts/error_no_def.py index 77e9d3c04a..cecb6a975a 100644 --- a/tests/integration/cli/projects/script/scripts/error_no_def.py +++ b/tests/integration/cli/projects/script/scripts/error_no_def.py @@ -3,4 +3,4 @@ local_variable = "test foo bar" provider = ape.chain.provider provider.set_timestamp(123123123123123123) -raise Exception("Expected exception") +raise Exception("Expected exception") # noqa: TRY002 diff --git a/tests/integration/cli/projects/test/tests/test_networks.py b/tests/integration/cli/projects/test/tests/test_networks.py index e83a719b4e..e2dca256ab 100644 --- a/tests/integration/cli/projects/test/tests/test_networks.py +++ b/tests/integration/cli/projects/test/tests/test_networks.py @@ -11,7 +11,7 @@ def test_networks_context_does_not_disconnect(networks): context = networks.ethereum.local.use_provider("test") error_output = f""" ape: {shutil.which("ape")}, - connected_providers: {", ".join([pid for pid in context.connected_providers.keys()])}, + connected_providers: {", ".join([pid for pid in context.connected_providers])}, """ assert False, f"Provider lost connection!\n{error_output}" diff --git a/tests/integration/cli/test_accounts.py b/tests/integration/cli/test_accounts.py index 7c3d05fe3e..0339b336b6 100644 --- a/tests/integration/cli/test_accounts.py +++ b/tests/integration/cli/test_accounts.py @@ -116,7 +116,7 @@ def test_import_invalid_private_key(ape_cli, runner): result = runner.invoke( ape_cli, ("accounts", "import", ALIAS), - input="\n".join(["0xhello", PASSWORD, PASSWORD]), + input=f"0xhello\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 1, result.output assert_failure(result, "Key can't be imported: Non-hexadecimal digit found") @@ -158,7 +158,7 @@ def test_import_mnemonic_default_hdpath( result = runner.invoke( ape_cli, ("accounts", "import", "--use-mnemonic", ALIAS, "-v", "INFO"), - input="\n".join([MNEMONIC, PASSWORD, PASSWORD]), + input=f"{MNEMONIC}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output assert temp_account_mnemonic_default_hdpath.address in result.output @@ -175,7 +175,7 @@ def test_import_mnemonic_custom_hdpath( result = runner.invoke( ape_cli, ("accounts", "import", ALIAS, "--use-mnemonic", "--hd-path", CUSTOM_HDPATH, "-v", "INFO"), - input="\n".join([MNEMONIC, PASSWORD, PASSWORD]), + input=f"{MNEMONIC}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output assert temp_account_mnemonic_custom_hdpath.address in result.output @@ -189,7 +189,7 @@ def test_export(ape_cli, runner, temp_keyfile, keyfile_account, accounts): result = runner.invoke( ape_cli, ("accounts", "export", ALIAS, "-v", "INFO"), - input="\n".join([PASSWORD, PASSWORD]), + input=f"{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output # NOTE: temp_keyfile uses the same address as the keyfile account. @@ -205,7 +205,7 @@ def test_import_invalid_mnemonic(ape_cli, runner): result = runner.invoke( ape_cli, ("accounts", "import", "--use-mnemonic", ALIAS), - input="\n".join([INVALID_MNEMONIC, PASSWORD, PASSWORD]), + input=f"{INVALID_MNEMONIC}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 1, result.output assert_failure(result, "Seed phrase can't be imported") @@ -221,7 +221,7 @@ def test_generate_default(ape_cli, runner, temp_keyfile_path): result = runner.invoke( ape_cli, ("accounts", "generate", ALIAS, "-v", "INFO"), - input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + input=f"random entropy\n{show_mnemonic}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output mnemonic = extract_mnemonic(result.output) @@ -241,7 +241,7 @@ def test_generate_hide_mnemonic_prompt(ape_cli, runner, temp_keyfile_path): result = runner.invoke( ape_cli, ("accounts", "generate", ALIAS, "-v", "INFO"), - input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + input=f"random entropy\n{show_mnemonic}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output assert "Newly generated mnemonic is" not in result.output @@ -257,7 +257,7 @@ def test_generate_hide_mnemonic_option(ape_cli, runner, temp_keyfile_path): result = runner.invoke( ape_cli, ("accounts", "generate", ALIAS, "--hide-mnemonic", "-v", "INFO"), - input="\n".join(["random entropy", PASSWORD, PASSWORD]), + input=f"random entropy\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output assert "Newly generated mnemonic is" not in result.output @@ -275,7 +275,7 @@ def test_generate_24_words(ape_cli, runner, temp_keyfile_path): result = runner.invoke( ape_cli, ("accounts", "generate", ALIAS, "--word-count", word_count, "-v", "INFO"), - input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + input=f"random entropy\n{show_mnemonic}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output mnemonic = extract_mnemonic(result.output) @@ -295,7 +295,7 @@ def test_generate_custom_hdpath(ape_cli, runner, temp_keyfile_path): result = runner.invoke( ape_cli, ("accounts", "generate", ALIAS, "--hd-path", CUSTOM_HDPATH, "-v", "INFO"), - input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + input=f"random entropy\n{show_mnemonic}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output mnemonic = extract_mnemonic(result.output) @@ -326,7 +326,7 @@ def test_generate_24_words_and_custom_hdpath(ape_cli, runner, temp_keyfile_path) "-v", "INFO", ), - input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + input=f"random entropy\n{show_mnemonic}\n{PASSWORD}\n{PASSWORD}", ) assert result.exit_code == 0, result.output mnemonic = extract_mnemonic(result.output) @@ -345,7 +345,7 @@ def invoke_generate(): return runner.invoke( ape_cli, ("accounts", "generate", ALIAS), - input="\n".join(["random entropy", show_mnemonic, PASSWORD, PASSWORD]), + input=f"random entropy\n{show_mnemonic}\n{PASSWORD}\n{PASSWORD}", ) result = invoke_generate() @@ -426,7 +426,7 @@ def test_authorizations_cli(ape_cli, runner, keyfile_account, geth_contract): GETH_URI, ), catch_exceptions=False, - input="\n".join(["y", PASSWORD, "y", "y"]), + input=f"y\n{PASSWORD}\ny\ny", ) assert result.exit_code == 0, result.output @@ -457,7 +457,7 @@ def test_authorizations_cli(ape_cli, runner, keyfile_account, geth_contract): GETH_URI, ), catch_exceptions=False, - input="\n".join(["y", PASSWORD, "y", "y"]), + input=f"y\n{PASSWORD}\ny\ny", ) assert result.exit_code == 0, result.output diff --git a/tests/integration/cli/test_init.py b/tests/integration/cli/test_init.py index e2f3f9ffea..85873ebbfe 100644 --- a/tests/integration/cli/test_init.py +++ b/tests/integration/cli/test_init.py @@ -26,7 +26,7 @@ def test_init_success(ape_cli, runner, integ_project): os.chdir(str(project_folder_path)) try: - result = runner.invoke(ape_cli, ("init",), input="\n".join(["init_success"])) + result = runner.invoke(ape_cli, ("init",), input="init_success") assert result.exit_code == 0, result.output # checks if the directory exist @@ -65,7 +65,7 @@ def test_fail_all_files_and_folders_exist(ape_cli, runner, integ_project): if not folder.exists(): folder.mkdir(exist_ok=False) - result = runner.invoke(ape_cli, ("init", "-v", "info"), input="\n".join(["init_fail"])) + result = runner.invoke(ape_cli, ("init", "-v", "info"), input="init_fail") # checks if the directory existence assert result.exit_code == 0, result.output assert "contracts' exists" in result.output diff --git a/tests/integration/cli/test_pm.py b/tests/integration/cli/test_pm.py index 4a308606f7..0c7f2817b6 100644 --- a/tests/integration/cli/test_pm.py +++ b/tests/integration/cli/test_pm.py @@ -33,7 +33,7 @@ def test_install_path_to_local_package(pm_runner, integ_project): name = path.stem result = pm_runner.invoke("install", path.as_posix(), "--name", project_name) assert result.exit_code == 0, result.output - assert f"Package '{path.as_posix()}' installed." + assert f"Package '{project_name}@local' installed." in result.output # Ensure was installed correctly. assert integ_project.dependencies[name]["local"] @@ -77,7 +77,7 @@ def test_install_github_dependency_with_version(pm_runner): timeout=300, ) assert result.exit_code == 0, result.output - assert "Package 'openzeppelin@4.6.0' installed." + assert "Package 'openzeppelin@4.6.0' installed." in result.output @run_once @@ -93,7 +93,7 @@ def test_install_github_dependency_with_ref(pm_runner): timeout=300, ) assert result.exit_code == 0, result.output - assert "Package 'OpenZeppelin@master' installed." + assert "Package 'OpenZeppelin@master' installed." in result.output @skip_projects_except("with-contracts") @@ -119,7 +119,7 @@ def test_install_config_override(pm_runner, integ_project): @skip_projects_except("with-contracts") def test_install_package_id(pm_runner, integ_project): pm_runner.project = integ_project - package_id = [x for x in integ_project.dependencies][0].package_id + package_id = next(iter(integ_project.dependencies)).package_id result = pm_runner.invoke("install", package_id) assert result.exit_code == 0, result.output @@ -127,7 +127,7 @@ def test_install_package_id(pm_runner, integ_project): @skip_projects_except("with-contracts") def test_install_name(pm_runner, integ_project): pm_runner.project = integ_project - name = [x for x in integ_project.dependencies][0].name + name = next(iter(integ_project.dependencies)).name result = pm_runner.invoke("install", name) assert result.exit_code == 0, result.output @@ -135,7 +135,7 @@ def test_install_name(pm_runner, integ_project): @skip_projects_except("with-contracts") def test_install_name_with_version_in_id(pm_runner, integ_project): pm_runner.project = integ_project - dependency = [x for x in integ_project.dependencies][0] + dependency = next(iter(integ_project.dependencies)) result = pm_runner.invoke("install", f"{dependency.name}@{dependency.version}") assert result.exit_code == 0, result.output assert f"Package '{dependency.name}@{dependency.version}' installed." in result.output @@ -144,7 +144,7 @@ def test_install_name_with_version_in_id(pm_runner, integ_project): @skip_projects_except("with-contracts") def test_install_name_with_version_flag(pm_runner, integ_project): pm_runner.project = integ_project - dependency = [x for x in integ_project.dependencies][0] + dependency = next(iter(integ_project.dependencies)) result = pm_runner.invoke("install", f"{dependency.name}", "--version", dependency.version) assert result.exit_code == 0, result.output assert f"Package '{dependency.name}@{dependency.version}' installed." in result.output diff --git a/tests/integration/cli/test_test.py b/tests/integration/cli/test_test.py index b907338daa..4782b29721 100644 --- a/tests/integration/cli/test_test.py +++ b/tests/integration/cli/test_test.py @@ -63,7 +63,7 @@ def path_check(): # 'Something something' temp path. try: os.getcwd() - except Exception: + except Exception: # noqa: BLE001 os.chdir(f"{Path(__file__).parent}") @@ -379,7 +379,7 @@ def test_gas_flag_exclusions_set_in_config( for _ in range(3): try: geth_account.transfer(geth_account, "1 wei") # Force a clean block. - except Exception as err: + except Exception as err: # noqa: BLE001 # Sometimes tests are weird, since running async. # So give this a chance a few times before bailing out... logger.error(f"Failure bumping block: {err}") diff --git a/tests/unit/api/test_api_init.py b/tests/unit/api/test_api_init.py index ee194232b2..88f8990360 100644 --- a/tests/unit/api/test_api_init.py +++ b/tests/unit/api/test_api_init.py @@ -1,7 +1,7 @@ import pytest -import ape.api as api -import ape.api.providers as providers +from ape import api +from ape.api import providers @pytest.mark.parametrize( From 7cc18bc7d3f27af2b9b77d3645e8ae3310a612c3 Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:29:19 -0500 Subject: [PATCH 2/2] fix: resolve overloaded methods by argument type, not just count (#2800) Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> --- src/ape/contracts/base.py | 71 ++++++++--- src/ape_ethereum/multicall/handlers.py | 4 +- tests/functional/test_contract_instance.py | 136 +++++++++++++++++++++ tests/functional/test_multicall.py | 37 ++++++ 4 files changed, 233 insertions(+), 15 deletions(-) diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 5a7be464f1..553c6420cd 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any import click +from eth_abi.exceptions import EncodingError from eth_pydantic_types import HexBytes from eth_utils import to_hex from ethpm_types.abi import EventABI @@ -26,9 +27,11 @@ ContractDataError, ContractLogicError, ContractNotFoundError, + ConversionError, CustomError, MethodNonPayableError, MissingDeploymentBytecodeError, + MissingStructFieldError, ) from ape.logging import get_rich_console, logger from ape.types.events import ContractLog, LogFilter, MockContractLog @@ -230,8 +233,25 @@ def _get_info(self, enrich: bool = False) -> str: return "\n\n".join(numeric_infos) + def _can_encode(self, abi: "MethodABI", args: tuple | list) -> bool: + # Probe whether args encode against this overload's input types. Used only to pick + # among same-arity overloads; encoding is pure (no network). Only a conversion or + # encoding failure means the args do not fit this overload. Anything else is a real + # error and propagates, so a bug in conversion is not silently read as a no-match. + try: + arguments = self.conversion_manager.convert_method_args(abi, args) + self.provider.network.ecosystem.encode_calldata(abi, *arguments) + except (ConversionError, MissingStructFieldError, EncodingError) as err: + logger.debug( + f"Overload '{abi.selector}' does not accept the given arguments " + f"({type(err).__name__}: {err})." + ) + return False + + return True + def encode_input(self, *args) -> HexBytes: - selected_abi = _select_method_abi(self.abis, args) + selected_abi = _select_method_abi(self.abis, args, encode_check=self._can_encode) arguments = self.conversion_manager.convert_method_args(selected_abi, args) ecosystem = self.provider.network.ecosystem encoded_calldata = ecosystem.encode_calldata(selected_abi, *arguments) @@ -293,7 +313,7 @@ def _validate_is_contract(self): class ContractCallHandler(ContractMethodHandler): def __call__(self, *args, **kwargs) -> Any: self._validate_is_contract() - selected_abi = _select_method_abi(self.abis, args) + selected_abi = _select_method_abi(self.abis, args, encode_check=self._can_encode) arguments = self.conversion_manager.convert_method_args(selected_abi, args) return ContractCall( @@ -347,23 +367,46 @@ def estimate_gas_cost(self, *args, **kwargs) -> int: reported in the fee-currency's smallest unit, e.g. Wei. """ - selected_abi = _select_method_abi(self.abis, args) + selected_abi = _select_method_abi(self.abis, args, encode_check=self._can_encode) arguments = self.conversion_manager.convert_method_args(selected_abi, args) return self.transact.estimate_gas_cost(*arguments, **kwargs) -def _select_method_abi(abis: list["MethodABI"], args: tuple | list) -> "MethodABI": +def _select_method_abi( + abis: list["MethodABI"], + args: tuple | list, + encode_check: "Callable[[MethodABI, tuple | list], bool] | None" = None, +) -> "MethodABI": + """ + Pick the overload to call for ``args``. + + Selection rules, in order: + + 1. Only overloads whose input count matches ``len(args)`` are candidates. No candidate + raises :class:`~ape.exceptions.ArgumentsLengthError`. + 2. With more than one candidate and an ``encode_check``, the first candidate (in ABI + declaration order) whose input types the args encode to wins. This is a behavior + change: selection used to be by count alone, so an ambiguous overload set resolved + to the last declared one regardless of the argument types (#2670). Encoding is pure, + so the probe costs no network calls. + 3. Otherwise (a single candidate, no ``encode_check``, or no candidate the args fit) the + last matching overload wins, which is the prior behavior. + + When several overloads all accept the same args, e.g. ``1`` against both ``uint256`` + and ``int256``, rule 2 falls back on ABI declaration order. That is deterministic but + arbitrary, since the ABI itself does not say which one the caller meant. + """ args = args or [] - selected_abi = None - for abi in abis: - inputs = abi.inputs or [] - if len(args) == len(inputs): - selected_abi = abi - - if not selected_abi: + matching_abis = [abi for abi in abis if len(abi.inputs or []) == len(args)] + if not matching_abis: raise ArgumentsLengthError(len(args), inputs=abis) - return selected_abi + if len(matching_abis) > 1 and encode_check is not None: + for abi in matching_abis: + if encode_check(abi, args): + return abi + + return matching_abis[-1] class ContractTransaction(ManagerAccessMixin): @@ -454,7 +497,7 @@ def estimate_gas_cost(self, *args, **kwargs) -> int: int: The estimated cost of gas to execute the transaction reported in the fee-currency's smallest unit, e.g. Wei. """ - selected_abi = _select_method_abi(self.abis, args) + selected_abi = _select_method_abi(self.abis, args, encode_check=self._can_encode) arguments = self.conversion_manager.convert_method_args(selected_abi, args) txn = self.as_transaction(*arguments, **kwargs) return self.provider.estimate_gas_cost(txn) @@ -479,7 +522,7 @@ def __call__(self, *args, **kwargs) -> "ReceiptAPI": def _as_transaction(self, *args) -> ContractTransaction: self._validate_is_contract() - selected_abi = _select_method_abi(self.abis, args) + selected_abi = _select_method_abi(self.abis, args, encode_check=self._can_encode) return ContractTransaction( abi=selected_abi, address=self.contract.address, diff --git a/src/ape_ethereum/multicall/handlers.py b/src/ape_ethereum/multicall/handlers.py index 5406adcee3..504927bd70 100644 --- a/src/ape_ethereum/multicall/handlers.py +++ b/src/ape_ethereum/multicall/handlers.py @@ -183,7 +183,9 @@ def add(self, call: ContractMethodHandler, *args, **kwargs): raise InvalidOption("value") super().add(call, *args, **kwargs) - self.abis.append(_select_method_abi(call.abis, args)) + # Must match the overload `super().add` encoded the calldata with, or an overloaded + # call is decoded against the wrong return types. + self.abis.append(_select_method_abi(call.abis, args, encode_check=call._can_encode)) return self @property diff --git a/tests/functional/test_contract_instance.py b/tests/functional/test_contract_instance.py index 0d3f8564c1..2f4e72bbf8 100644 --- a/tests/functional/test_contract_instance.py +++ b/tests/functional/test_contract_instance.py @@ -20,6 +20,8 @@ ProviderNotConnectedError, SignatureError, ) +from ape.logging import LogLevel +from ape.managers.converters import ConversionManager from ape.types.address import AddressType from ape_ethereum.transactions import TransactionStatusEnum, TransactionType @@ -874,6 +876,140 @@ def test_encode_input_transaction(contract_instance, calldata): assert actual == expected +BYTES_ARRAY_OVERLOAD = [{"name": "updateData", "type": "bytes[]"}] +UINT_OVERLOAD = [{"name": "updateDataSize", "type": "uint256"}] +INT_OVERLOAD = [{"name": "updateDataSize", "type": "int256"}] + + +@pytest.fixture +def overloaded_contract(chain): + # Overload selection happens at encode time, which is pure, so a hand-crafted ABI at an + # arbitrary address exercises it without deploying anything. Each call needs its own + # address because instance_at caches by address. + def make(overloads, address): + abi = [ + { + "type": "function", + "name": "getUpdateFee", + "stateMutability": "view", + "inputs": inputs, + "outputs": [{"name": "", "type": "uint256"}], + } + for inputs in overloads + ] + return chain.contracts.instance_at( + address, + abi=abi, + detect_proxy=False, + fetch_from_explorer=False, + fetch_from_disk=False, + ) + + return make + + +def selector_for(ecosystem, handler, canonical_type): + abi = next(a for a in handler.abis if a.inputs[0].canonical_type == canonical_type) + return ecosystem.get_method_selector(abi) + + +def test_encode_input_overloaded_selects_convertible_overload( + eth_tester_provider, overloaded_contract +): + # Regression for #2670: when overloads share the same argument count, the arg count + # cannot tell them apart. An empty list is convertible to bytes[] but not uint256, so + # the bytes[] overload must be selected rather than the last-declared one. + ecosystem = eth_tester_provider.network.ecosystem + + # Both declaration orders must resolve to the bytes[] overload for a list argument. + for overloads, address in ( + ([BYTES_ARRAY_OVERLOAD, UINT_OVERLOAD], "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"), + ([UINT_OVERLOAD, BYTES_ARRAY_OVERLOAD], "0x1111111111111111111111111111111111111111"), + ): + handler = overloaded_contract(overloads, address).getUpdateFee + calldata = handler.encode_input([]) + assert calldata[:4] == selector_for(ecosystem, handler, "bytes[]") + + +def test_encode_input_overloaded_ambiguous_picks_first_declared( + eth_tester_provider, overloaded_contract +): + # 1 encodes against both uint256 and int256, so the probe cannot separate them and + # selection falls back on declaration order. That choice is arbitrary, but it has to + # stay stable, so pin it. + ecosystem = eth_tester_provider.network.ecosystem + + for overloads, address in ( + ([UINT_OVERLOAD, INT_OVERLOAD], "0x2222222222222222222222222222222222222222"), + ([INT_OVERLOAD, UINT_OVERLOAD], "0x3333333333333333333333333333333333333333"), + ): + handler = overloaded_contract(overloads, address).getUpdateFee + first_declared = overloads[0][0]["type"] + calldata = handler.encode_input(1) + assert calldata[:4] == selector_for(ecosystem, handler, first_declared) + + +def test_encode_input_overloaded_out_of_bounds_value(eth_tester_provider, overloaded_contract): + # A value that converts fine but is too wide for an overload's type fails at encode time + # rather than conversion time, and that is still a no-match, not an error. + ecosystem = eth_tester_provider.network.ecosystem + small_overload = [{"name": "updateDataSize", "type": "uint8"}] + handler = overloaded_contract( + [small_overload, UINT_OVERLOAD], "0x7777777777777777777777777777777777777777" + ).getUpdateFee + + calldata = handler.encode_input(999999) + + assert calldata[:4] == selector_for(ecosystem, handler, "uint256") + + +def test_encode_input_overloaded_struct_field_mismatch(eth_tester_provider, overloaded_contract): + # A dict that is missing an overload's struct fields means the args do not fit that + # overload, same as a conversion failure, so the probe must treat it as a no-match + # rather than letting the error escape. + ecosystem = eth_tester_provider.network.ecosystem + ab_struct = [ + { + "name": "data", + "type": "tuple", + "components": [{"name": "a", "type": "uint256"}, {"name": "b", "type": "uint256"}], + } + ] + x_struct = [{"name": "data", "type": "tuple", "components": [{"name": "x", "type": "uint256"}]}] + handler = overloaded_contract( + [ab_struct, x_struct], "0x4444444444444444444444444444444444444444" + ).getUpdateFee + + calldata = handler.encode_input({"x": 1}) + + assert calldata[:4] == selector_for(ecosystem, handler, "(uint256)") + + +def test_can_encode_propagates_unexpected_errors(mocker, eth_tester_provider, overloaded_contract): + # The probe only means "these args do not fit this overload". A bug inside conversion is + # not that, so it must surface instead of being read as a failed match. + handler = overloaded_contract( + [BYTES_ARRAY_OVERLOAD, UINT_OVERLOAD], "0x5555555555555555555555555555555555555555" + ).getUpdateFee + mocker.patch.object(ConversionManager, "convert_method_args", side_effect=RuntimeError("boom")) + + with pytest.raises(RuntimeError, match="boom"): + handler._can_encode(handler.abis[0], ([],)) + + +def test_can_encode_logs_probe_failure(eth_tester_provider, overloaded_contract, ape_caplog): + handler = overloaded_contract( + [BYTES_ARRAY_OVERLOAD, UINT_OVERLOAD], "0x6666666666666666666666666666666666666666" + ).getUpdateFee + uint_abi = next(a for a in handler.abis if a.inputs[0].canonical_type == "uint256") + + with ape_caplog.at_level(LogLevel.DEBUG): + assert handler._can_encode(uint_abi, ([],)) is False + + message = next(m for m in ape_caplog.messages if "getUpdateFee(uint256" in m) + assert "ConversionError" in message + + def test_decode_input_call(contract_instance, calldata): method = contract_instance.setNumber.call actual = method.decode_input(calldata) diff --git a/tests/functional/test_multicall.py b/tests/functional/test_multicall.py index a3a80415f5..009ac85eaf 100644 --- a/tests/functional/test_multicall.py +++ b/tests/functional/test_multicall.py @@ -74,3 +74,40 @@ def test_returndata(returndata_key): call = Call() call._result = [result] # type: ignore assert call.returnData[0] == output + + +def test_add_overloaded_records_the_encoded_overload(chain, eth_tester_provider): + # The recorded ABI decodes the return data, so it has to be the same overload that + # encoded the calldata. Both go through type-aware selection or neither does. + # bytes[] first so that count-only selection would land on the uint256 overload and + # disagree with the calldata. + abi = [ + { + "type": "function", + "name": "getUpdateFee", + "stateMutability": "view", + "inputs": [{"name": "updateData", "type": "bytes[]"}], + "outputs": [{"name": "", "type": "uint256"}], + }, + { + "type": "function", + "name": "getUpdateFee", + "stateMutability": "view", + "inputs": [{"name": "updateDataSize", "type": "uint256"}], + "outputs": [{"name": "", "type": "uint256"}], + }, + ] + contract = chain.contracts.instance_at( + "0x8888888888888888888888888888888888888888", + abi=abi, + detect_proxy=False, + fetch_from_explorer=False, + fetch_from_disk=False, + ) + call = Call() + + call.add(contract.getUpdateFee, []) + + ecosystem = eth_tester_provider.network.ecosystem + assert call.abis[0].inputs[0].canonical_type == "bytes[]" + assert call.calls[0]["callData"][:4] == ecosystem.get_method_selector(call.abis[0])