diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 911bc50..5a3bcd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-yaml @@ -10,7 +10,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black name: black @@ -21,13 +21,13 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.13.0 hooks: - id: mypy - additional_dependencies: [types-requests, types-setuptools, pydantic] + additional_dependencies: [types-requests, types-setuptools, flake8-pydantic, flake8-type-checking] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.17 + rev: 0.7.18 hooks: - id: mdformat additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject] diff --git a/ape_solidity/__init__.py b/ape_solidity/__init__.py index 78ecdcc..97e4d82 100644 --- a/ape_solidity/__init__.py +++ b/ape_solidity/__init__.py @@ -1,14 +1,45 @@ -from ape import plugins +from typing import Any -from ._utils import Extension -from .compiler import SolidityCompiler, SolidityConfig +from ape import plugins @plugins.register(plugins.Config) def config_class(): + from .compiler import SolidityConfig + return SolidityConfig @plugins.register(plugins.CompilerPlugin) def register_compiler(): + from ._utils import Extension + from .compiler import SolidityCompiler + return (Extension.SOL.value,), SolidityCompiler + + +def __getattr__(name: str) -> Any: + if name == "Extension": + from ._utils import Extension + + return Extension + + elif name == "SolidityCompiler": + from .compiler import SolidityCompiler + + return SolidityCompiler + + elif name == "SolidityConfig": + from .compiler import SolidityConfig + + return SolidityConfig + + else: + raise AttributeError(name) + + +__all__ = [ + "Extension", + "SolidityCompiler", + "SolidityConfig", +] diff --git a/ape_solidity/_cli.py b/ape_solidity/_cli.py index 4884f66..ce3348f 100644 --- a/ape_solidity/_cli.py +++ b/ape_solidity/_cli.py @@ -2,7 +2,7 @@ import ape import click -from ape.cli import ape_cli_context, project_option +from ape.cli.options import ape_cli_context, project_option @click.group diff --git a/ape_solidity/_models.py b/ape_solidity/_models.py index 91f17d3..6840e44 100644 --- a/ape_solidity/_models.py +++ b/ape_solidity/_models.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional from ape.exceptions import CompilerError, ProjectError -from ape.managers import ProjectManager from ape.utils.basemodel import BaseModel, ManagerAccessMixin, classproperty from ape.utils.os import get_relative_path from pydantic import field_serializer @@ -13,6 +12,8 @@ from ape_solidity._utils import get_single_import_lines if TYPE_CHECKING: + from ape.managers.project import ProjectManager + from ape_solidity.compiler import SolidityCompiler @@ -26,7 +27,7 @@ class ApeSolidityModel(BaseModel, ApeSolidityMixin): pass -def _create_import_remapping(project: ProjectManager) -> dict[str, str]: +def _create_import_remapping(project: "ProjectManager") -> dict[str, str]: prefix = f"{get_relative_path(project.contracts_folder, project.path)}" specified = project.dependencies.install() @@ -102,22 +103,22 @@ def __init__(self): # Cache project paths to import remapping. self._cache: dict[str, dict[str, str]] = {} - def __getitem__(self, project: ProjectManager) -> dict[str, str]: + def __getitem__(self, project: "ProjectManager") -> dict[str, str]: if remapping := self._cache.get(f"{project.path}"): return remapping return self.add_project(project) - def add_project(self, project: ProjectManager) -> dict[str, str]: + def add_project(self, project: "ProjectManager") -> dict[str, str]: remapping = _create_import_remapping(project) return self.add(project, remapping) - def add(self, project: ProjectManager, remapping: dict[str, str]): + def add(self, project: "ProjectManager", remapping: dict[str, str]): self._cache[f"{project.path}"] = remapping return remapping @classmethod - def get_import_remapping(cls, project: ProjectManager): + def get_import_remapping(cls, project: "ProjectManager"): return _create_import_remapping(project) @@ -147,7 +148,7 @@ def value(self) -> str: return self.raw_value @property - def dependency(self) -> Optional[ProjectManager]: + def dependency(self) -> Optional["ProjectManager"]: if name := self.dependency_name: if version := self.dependency_version: return self.local_project.dependencies[name][version] @@ -159,8 +160,8 @@ def parse_line( cls, value: str, reference: Path, - project: ProjectManager, - dependency: Optional[ProjectManager] = None, + project: "ProjectManager", + dependency: Optional["ProjectManager"] = None, ) -> "ImportStatementMetadata": quote = '"' if '"' in value else "'" sep = "\\" if "\\" in value else "/" @@ -186,14 +187,17 @@ def __hash__(self) -> int: return hash(path) def _resolve_source( - self, reference: Path, project: ProjectManager, dependency: Optional[ProjectManager] = None + self, + reference: Path, + project: "ProjectManager", + dependency: Optional["ProjectManager"] = None, ): if not self._resolve_dependency(project, dependency=dependency): # Handle non-dependencies. self._resolve_import_remapping(project) self._resolve_path(reference, project) - def _resolve_import_remapping(self, project: ProjectManager): + def _resolve_import_remapping(self, project: "ProjectManager"): if self.value.startswith("."): # Relative paths should not use import-remappings. return @@ -213,7 +217,7 @@ def _resolve_import_remapping(self, project: ProjectManager): valid_matches, key=lambda x: len(x[0]) ) - def _resolve_path(self, reference: Path, project: ProjectManager): + def _resolve_path(self, reference: Path, project: "ProjectManager"): base_path = None if self.value.startswith("."): base_path = reference.parent @@ -236,7 +240,7 @@ def _resolve_path(self, reference: Path, project: ProjectManager): self.source_id = f"{get_relative_path(self.path, project.path)}" def _resolve_dependency( - self, project: ProjectManager, dependency: Optional[ProjectManager] = None + self, project: "ProjectManager", dependency: Optional["ProjectManager"] = None ) -> bool: config_project = dependency or project # NOTE: Dependency is set if we are getting dependencies of dependencies. @@ -340,9 +344,9 @@ def _serialize_import_statements(self, statements, info): def from_source_files( cls, source_files: Iterable[Path], - project: ProjectManager, + project: "ProjectManager", statements: Optional[dict[tuple[Path, str], set[ImportStatementMetadata]]] = None, - dependency: Optional[ProjectManager] = None, + dependency: Optional["ProjectManager"] = None, ) -> "SourceTree": statements = statements or {} for path in source_files: diff --git a/ape_solidity/_utils.py b/ape_solidity/_utils.py index d9ca03d..61278f6 100644 --- a/ape_solidity/_utils.py +++ b/ape_solidity/_utils.py @@ -3,15 +3,18 @@ from collections.abc import Iterable from enum import Enum from pathlib import Path -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union from ape.exceptions import CompilerError from ape.utils import pragma_str_to_specifier_set -from packaging.specifiers import SpecifierSet from packaging.version import Version from solcx.install import get_executable from solcx.wrapper import get_solc_version as get_solc_version_from_binary +if TYPE_CHECKING: + from packaging.specifiers import SpecifierSet + + OUTPUT_SELECTION = [ "abi", "bin-runtime", @@ -62,7 +65,7 @@ def get_single_import_lines(source_path: Path) -> list[str]: return list(import_set) -def get_pragma_spec_from_path(source_file_path: Union[Path, str]) -> Optional[SpecifierSet]: +def get_pragma_spec_from_path(source_file_path: Union[Path, str]) -> Optional["SpecifierSet"]: """ Extracts pragma information from Solidity source code. @@ -80,7 +83,7 @@ def get_pragma_spec_from_path(source_file_path: Union[Path, str]) -> Optional[Sp return get_pragma_spec_from_str(source_str) -def get_pragma_spec_from_str(source_str: str) -> Optional[SpecifierSet]: +def get_pragma_spec_from_str(source_str: str) -> Optional["SpecifierSet"]: if not ( pragma_match := next( re.finditer(r"(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)", source_str), None @@ -106,11 +109,11 @@ def add_commit_hash(version: Union[str, Version]) -> Version: return get_solc_version_from_binary(solc, with_commit_hash=True) -def get_versions_can_use(pragma_spec: SpecifierSet, options: Iterable[Version]) -> list[Version]: +def get_versions_can_use(pragma_spec: "SpecifierSet", options: Iterable[Version]) -> list[Version]: return sorted(list(pragma_spec.filter(options)), reverse=True) -def select_version(pragma_spec: SpecifierSet, options: Iterable[Version]) -> Optional[Version]: +def select_version(pragma_spec: "SpecifierSet", options: Iterable[Version]) -> Optional[Version]: choices = get_versions_can_use(pragma_spec, options) return choices[0] if choices else None diff --git a/ape_solidity/compiler.py b/ape_solidity/compiler.py index 97fee09..246f51e 100644 --- a/ape_solidity/compiler.py +++ b/ape_solidity/compiler.py @@ -2,10 +2,9 @@ from collections import defaultdict from collections.abc import Iterable, Iterator, Sequence from pathlib import Path -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union from ape.api import CompilerAPI, PluginConfig -from ape.contracts import ContractInstance from ape.exceptions import CompilerError, ConfigError, ContractLogicError from ape.logging import logger from ape.managers.project import LocalProject, ProjectManager @@ -15,7 +14,6 @@ from eth_pydantic_types import HexBytes from eth_utils import add_0x_prefix, is_0x_prefixed from ethpm_types.source import Compiler, Content -from packaging.specifiers import SpecifierSet from packaging.version import Version from pydantic import model_validator from requests.exceptions import ConnectionError @@ -50,6 +48,11 @@ SolcInstallError, ) +if TYPE_CHECKING: + from ape.contracts import ContractInstance + from packaging.specifiers import SpecifierSet + + LICENSES_PATTERN = re.compile(r"(// SPDX-License-Identifier:\s*([^\n]*)\s)") # Comment patterns @@ -234,7 +237,7 @@ def _get_configured_version( def _ape_version(self) -> Version: return Version(version.split(".dev")[0].strip()) - def add_library(self, *contracts: ContractInstance, project: Optional[ProjectManager] = None): + def add_library(self, *contracts: "ContractInstance", project: Optional[ProjectManager] = None): """ Set a library contract type address. This is useful when deploying a library in a local network and then adding the address afterward. Now, when @@ -521,8 +524,6 @@ def _compile( input_contract_names.append(name) for source_id, contracts_out in contracts.items(): - # ast_data = output["sources"][source_id]["ast"] - for contract_name, ct_data in contracts_out.items(): if contract_name not in input_contract_names: # Only return ContractTypes explicitly asked for. @@ -784,7 +785,7 @@ def get_version_map_from_imports( # is more predictable. Also, remove any lingering empties. return {k: result[k] for k in sorted(result) if result[k]} - def _get_pramga_spec_from_str(self, source_str: str) -> Optional[SpecifierSet]: + def _get_pramga_spec_from_str(self, source_str: str) -> Optional["SpecifierSet"]: if not (pragma_spec := get_pragma_spec_from_str(source_str)): return None diff --git a/ape_solidity/exceptions.py b/ape_solidity/exceptions.py index 5a11aae..eebe3f0 100644 --- a/ape_solidity/exceptions.py +++ b/ape_solidity/exceptions.py @@ -1,9 +1,11 @@ from enum import IntEnum -from typing import Union +from typing import TYPE_CHECKING, Union from ape.exceptions import CompilerError, ConfigError, ContractLogicError from ape.logging import LogLevel, logger -from solcx.exceptions import SolcError + +if TYPE_CHECKING: + from solcx.exceptions import SolcError class SolcInstallError(CompilerError): @@ -25,7 +27,7 @@ class SolcCompileError(CompilerError): account Ape's logging verbosity. """ - def __init__(self, solc_error: SolcError): + def __init__(self, solc_error: "SolcError"): self.solc_error = solc_error def __str__(self) -> str: diff --git a/setup.cfg b/setup.cfg index 1277c6b..1c41709 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,9 @@ [flake8] max-line-length = 100 +ignore = E704,W503,PYD002,TC003,TC006 exclude = venv* docs build tests/node_modules +type-checking-pydantic-enabled = True diff --git a/setup.py b/setup.py index 7d2b832..67e78ac 100644 --- a/setup.py +++ b/setup.py @@ -12,13 +12,15 @@ "pytest-benchmark", # For performance tests ], "lint": [ - "black>=24.8.0,<25", # Auto-formatter and linter - "mypy>=1.11.1,<2", # Static type analyzer + "black>=24.10.0,<25", # Auto-formatter and linter + "mypy>=1.13.0,<2", # Static type analyzer "types-requests", # Needed for mypy type shed "types-setuptools", # Needed for mypy type shed "flake8>=7.1.1,<8", # Style linter + "flake8-pydantic", # For detecting issues with Pydantic models + "flake8-type-checking", # Detect imports to move in/out of type-checking blocks "isort>=5.13.2,<6", # Import sorting linter - "mdformat>=0.7.17", # Auto-formatter for markdown + "mdformat>=0.7.18", # 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.1", # Allows configuring in pyproject.toml diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 18ade11..9b47278 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -655,7 +655,7 @@ def test_compile_outputs_compiler_data_to_manifest(project, compiler): actual = project.manifest.compilers[0] assert actual.name == "solidity" assert "CompilesOnce" in actual.contractTypes - assert actual.version == "0.8.27+commit.40a35a09" + assert actual.version == "0.8.28+commit.7893614a" # Compiling again should not add the same compiler again. _ = [c for c in compiler.compile((path,), project=project)] length_again = len(project.manifest.compilers or [])