Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use uv if installed for ape plugins install / uninstall commands #2000

Merged
merged 34 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
25a49d8
feat: use uv if it exists, oteherwise pip
wakamex Apr 11, 2024
11c32b3
chore: lint with isort
wakamex Apr 15, 2024
af082fe
chore: lint with flake8
wakamex Apr 15, 2024
47498d6
chore: remove setuptools dependency attempt #1
wakamex Apr 15, 2024
c84ed29
chore: bump pandas
wakamex Apr 15, 2024
5b1dffb
chore: remove setuptools dependency attempt #2
wakamex Apr 15, 2024
a2d0633
chore: uncap eth_account
wakamex Apr 15, 2024
2aa9753
allow installing pre-releases in workflows
wakamex Apr 15, 2024
cd9bfa1
chore: fix typo
wakamex Apr 15, 2024
c769119
feat: install with uv in workflow
wakamex Apr 15, 2024
e56a470
chore: make venv
wakamex Apr 15, 2024
9c51369
chore: activate venv at each step
wakamex Apr 15, 2024
4af0ec4
move PIP_COMMAND to plugins._utils
wakamex Apr 15, 2024
ee4f613
merge lists
wakamex Apr 15, 2024
ec63a86
merge more lists
wakamex Apr 15, 2024
06368d0
reset dependencies
wakamex Apr 15, 2024
629cb23
reset github workflows
wakamex Apr 15, 2024
2d3a047
remove PIP_COMMAND from __init__
wakamex Apr 15, 2024
04e257f
lint with black
wakamex Apr 15, 2024
da6ff68
lint with isort
wakamex Apr 15, 2024
c911bd7
move PluginManager to src/ape/managers/plugins.py and clean_plugin_na…
wakamex Apr 15, 2024
6e9cccb
add .venv to flake8 ignore
wakamex Apr 15, 2024
3029c53
move valid_impl and PluginManager import out of ape/plugins/__init__
wakamex Apr 15, 2024
417ad57
fix rebase
wakamex Apr 17, 2024
d158695
update dependencies from main
wakamex Apr 17, 2024
81bbe52
Merge branch 'main' into add_setuptools_pip_requirements
wakamex Apr 22, 2024
a1fc90c
Merge branch 'main' into add_setuptools_pip_requirements
wakamex Apr 22, 2024
a83d3c9
Merge branch 'main' into add_setuptools_pip_requirements
wakamex Apr 22, 2024
6371374
Merge branch 'main' into add_setuptools_pip_requirements
antazoey Apr 23, 2024
c8fea1c
chore: put in missed merge
antazoey Apr 23, 2024
93ae0cd
docs: fix str
antazoey Apr 23, 2024
399bf39
chore: missed lint
antazoey Apr 23, 2024
0e71415
test: uv tests
antazoey Apr 23, 2024
bd50c36
fix: bump trie pkg_resources bug
antazoey Apr 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[flake8]
max-line-length = 100
exclude =
.venv
venv*
docs
build
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"eth-typing>=3.5.2,<4",
"eth-utils>=2.3.1,<3",
"py-geth>=4.4.0,<5",
"trie>=3.0.0,<4", # Peer: stricter pin needed for uv support.
"web3[tester]>=6.17.2,<7",
# ** Dependencies maintained by ApeWorX **
"eip712>=0.2.7,<0.3",
Expand Down
3 changes: 1 addition & 2 deletions src/ape/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
from ape.cli import ape_cli_context
from ape.exceptions import Abort, ApeException, handle_ape_exception
from ape.logging import logger
from ape.plugins import clean_plugin_name
from ape.plugins._utils import PluginMetadataList
from ape.plugins._utils import PluginMetadataList, clean_plugin_name
from ape.utils.basemodel import ManagerAccessMixin

_DIFFLIB_CUT_OFF = 0.6
Expand Down
4 changes: 2 additions & 2 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -964,10 +964,10 @@ def providers(self): # -> Dict[str, Partial[ProviderAPI]]
Dict[str, partial[:class:`~ape.api.providers.ProviderAPI`]]
"""

from ape.plugins import clean_plugin_name
from ape.plugins._utils import clean_plugin_name

providers = {}
for plugin_name, plugin_tuple in self.plugin_manager.providers:
for _, plugin_tuple in self.plugin_manager.providers:
ecosystem_name, network_name, provider_class = plugin_tuple
provider_name = clean_plugin_name(provider_class.__module__.split(".")[0])

Expand Down
2 changes: 1 addition & 1 deletion src/ape/managers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from pathlib import Path as _Path

from ape.plugins import PluginManager
from ape.utils import USER_AGENT, ManagerAccessMixin

from .accounts import AccountManager
Expand All @@ -9,6 +8,7 @@
from .config import ConfigManager
from .converters import ConversionManager
from .networks import NetworkManager
from .plugins import PluginManager
from .project import DependencyManager, ProjectManager
from .query import QueryManager

Expand Down
154 changes: 154 additions & 0 deletions src/ape/managers/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import importlib
from importlib.metadata import distributions
from typing import Any, Generator, Iterator, List, Optional, Set, Tuple

from ape.__modules__ import __modules__
from ape.exceptions import ApeAttributeError
from ape.logging import logger
from ape.plugins._utils import clean_plugin_name
from ape.plugins.pluggy_patch import plugin_manager as pluggy_manager
from ape.utils.basemodel import _assert_not_ipython_check
from ape.utils.misc import log_instead_of_fail


def valid_impl(api_class: Any) -> bool:
"""
Check if an API class is valid. The class must not have any unimplemented
abstract methods.

Args:
api_class (any)

Returns:
bool
"""

if isinstance(api_class, tuple):
return all(valid_impl(c) for c in api_class)

# Is not an ABC base class or abstractdataclass
if not hasattr(api_class, "__abstractmethods__"):
return True # not an abstract class

return len(api_class.__abstractmethods__) == 0


class PluginManager:
_unimplemented_plugins: List[str] = []

def __init__(self) -> None:
self.__registered = False

@log_instead_of_fail(default="<PluginManager>")
def __repr__(self) -> str:
return f"<{PluginManager.__name__}>"

def __getattr__(self, attr_name: str) -> Iterator[Tuple[str, Tuple]]:
_assert_not_ipython_check(attr_name)

# NOTE: The first time this method is called, the actual
# plugin registration occurs. Registration only happens once.
self._register_plugins()

if not hasattr(pluggy_manager.hook, attr_name):
raise ApeAttributeError(f"{PluginManager.__name__} has no attribute '{attr_name}'.")

# Do this to get access to the package name
hook_fn = getattr(pluggy_manager.hook, attr_name)
hookimpls = hook_fn.get_hookimpls()

def get_plugin_name_and_hookfn(h):
return h.plugin_name, getattr(h.plugin, attr_name)()

for plugin_name, results in map(get_plugin_name_and_hookfn, hookimpls):
# NOTE: Some plugins return a tuple and some return iterators
if not isinstance(results, Generator):
validated_plugin = self._validate_plugin(plugin_name, results)
if validated_plugin:
yield validated_plugin
else:
# Only if it's an iterator, provider results as a series
for result in results:
validated_plugin = self._validate_plugin(plugin_name, result)
if validated_plugin:
yield validated_plugin

@property
def registered_plugins(self) -> Set[str]:
self._register_plugins()
return {x[0] for x in pluggy_manager.list_name_plugin()}

def _register_plugins(self):
if self.__registered:
return

plugins = [
x.name.replace("-", "_")
for x in distributions()
if getattr(x, "name", "").startswith("ape-")
]
locals = [p for p in __modules__ if p != "ape"]
plugin_modules = tuple([*plugins, *locals])

for module_name in plugin_modules:
try:
module = importlib.import_module(module_name)
pluggy_manager.register(module)
except Exception as err:
if module_name in __modules__:
# Always raise core plugin registration errors.
raise

logger.warn_from_exception(err, f"Error loading plugin package '{module_name}'.")

self.__registered = True

def _validate_plugin(self, plugin_name: str, plugin_cls) -> Optional[Tuple[str, Tuple]]:
if valid_impl(plugin_cls):
return clean_plugin_name(plugin_name), plugin_cls
else:
self._warn_not_fully_implemented_error(plugin_cls, plugin_name)
return None

def _warn_not_fully_implemented_error(self, results, plugin_name):
if plugin_name in self._unimplemented_plugins:
# Already warned
return

unimplemented_methods = []

# Find the best API name to warn about.
if isinstance(results, (list, tuple)):
classes = [p for p in results if hasattr(p, "__name__")]
if classes:
# Likely only ever a single class in a registration, but just in case.
api_name = " - ".join([p.__name__ for p in classes if hasattr(p, "__name__")])
for api_cls in classes:
if (
abstract_methods := getattr(api_cls, "__abstractmethods__", None)
) and isinstance(abstract_methods, dict):
unimplemented_methods.extend(api_cls.__abstractmethods__)

else:
# This would only happen if the registration consisted of all primitives.
api_name = " - ".join(results)

elif hasattr(results, "__name__"):
api_name = results.__name__
if (abstract_methods := getattr(results, "__abstractmethods__", None)) and isinstance(
abstract_methods, dict
):
unimplemented_methods.extend(results.__abstractmethods__)

else:
api_name = results

message = f"'{api_name}' from '{plugin_name}' is not fully implemented."
if unimplemented_methods:
methods_str = ", ".join(unimplemented_methods)
message = f"{message} Remaining abstract methods: '{methods_str}'."

logger.warning(message)

# Record so we don't warn repeatedly
self._unimplemented_plugins.append(plugin_name)
2 changes: 1 addition & 1 deletion src/ape/managers/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ape.contracts.base import ContractLog, LogFilter
from ape.exceptions import QueryEngineError
from ape.logging import logger
from ape.plugins import clean_plugin_name
from ape.plugins._utils import clean_plugin_name
from ape.utils import ManagerAccessMixin, cached_property, singledispatchmethod


Expand Down
159 changes: 1 addition & 158 deletions src/ape/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import functools
import importlib
from importlib.metadata import distributions
from typing import Any, Callable, Generator, Iterator, List, Optional, Set, Tuple, Type

from ape.__modules__ import __modules__
from ape.exceptions import ApeAttributeError
from ape.logging import logger
from ape.utils.basemodel import _assert_not_ipython_check
from ape.utils.misc import log_instead_of_fail
from typing import Callable, Type

from .account import AccountPlugin
from .compiler import CompilerPlugin
Expand Down Expand Up @@ -45,10 +37,6 @@ class AllPluginHooks(
pluggy_manager.add_hookspecs(AllPluginHooks)


def clean_plugin_name(name: str) -> str:
return name.replace("_", "-").replace("ape-", "")


def get_hooks(plugin_type):
return [name for name, method in plugin_type.__dict__.items() if hasattr(method, "ape_spec")]

Expand Down Expand Up @@ -97,151 +85,6 @@ def check_hook(plugin_type, hookimpl_kwargs, fn):
return functools.partial(check_hook, plugin_type, hookimpl_kwargs)


def valid_impl(api_class: Any) -> bool:
"""
Check if an API class is valid. The class must not have any unimplemented
abstract methods.

Args:
api_class (any)

Returns:
bool
"""

if isinstance(api_class, tuple):
return all(valid_impl(c) for c in api_class)

# Is not an ABC base class or abstractdataclass
if not hasattr(api_class, "__abstractmethods__"):
return True # not an abstract class

return len(api_class.__abstractmethods__) == 0


class PluginManager:
_unimplemented_plugins: List[str] = []

def __init__(self) -> None:
self.__registered = False

@log_instead_of_fail(default="<PluginManager>")
def __repr__(self) -> str:
return f"<{PluginManager.__name__}>"

def __getattr__(self, attr_name: str) -> Iterator[Tuple[str, Tuple]]:
_assert_not_ipython_check(attr_name)

# NOTE: The first time this method is called, the actual
# plugin registration occurs. Registration only happens once.
self._register_plugins()

if not hasattr(pluggy_manager.hook, attr_name):
raise ApeAttributeError(f"{PluginManager.__name__} has no attribute '{attr_name}'.")

# Do this to get access to the package name
hook_fn = getattr(pluggy_manager.hook, attr_name)
hookimpls = hook_fn.get_hookimpls()

def get_plugin_name_and_hookfn(h):
return h.plugin_name, getattr(h.plugin, attr_name)()

for plugin_name, results in map(get_plugin_name_and_hookfn, hookimpls):
# NOTE: Some plugins return a tuple and some return iterators
if not isinstance(results, Generator):
validated_plugin = self._validate_plugin(plugin_name, results)
if validated_plugin:
yield validated_plugin
else:
# Only if it's an iterator, provider results as a series
for result in results:
validated_plugin = self._validate_plugin(plugin_name, result)
if validated_plugin:
yield validated_plugin

@property
def registered_plugins(self) -> Set[str]:
self._register_plugins()
return {x[0] for x in pluggy_manager.list_name_plugin()}

def _register_plugins(self):
if self.__registered:
return

plugins = [
x.name.replace("-", "_")
for x in distributions()
if getattr(x, "name", "").startswith("ape-")
]
locals = [p for p in __modules__ if p != "ape"]
plugin_modules = tuple([*plugins, *locals])

for module_name in plugin_modules:
try:
module = importlib.import_module(module_name)
pluggy_manager.register(module)
except Exception as err:
if module_name in __modules__:
# Always raise core plugin registration errors.
raise

logger.warn_from_exception(err, f"Error loading plugin package '{module_name}'.")

self.__registered = True

def _validate_plugin(self, plugin_name: str, plugin_cls) -> Optional[Tuple[str, Tuple]]:
if valid_impl(plugin_cls):
return clean_plugin_name(plugin_name), plugin_cls
else:
self._warn_not_fully_implemented_error(plugin_cls, plugin_name)
return None

def _warn_not_fully_implemented_error(self, results, plugin_name):
if plugin_name in self._unimplemented_plugins:
# Already warned
return

unimplemented_methods = []

# Find the best API name to warn about.
if isinstance(results, (list, tuple)):
classes = [p for p in results if hasattr(p, "__name__")]
if classes:
# Likely only ever a single class in a registration, but just in case.
api_name = " - ".join([p.__name__ for p in classes if hasattr(p, "__name__")])
for api_cls in classes:
if (
abstract_methods := getattr(api_cls, "__abstractmethods__", None)
) and isinstance(abstract_methods, dict):
unimplemented_methods.extend(api_cls.__abstractmethods__)

else:
# This would only happen if the registration consisted of all primitives.
api_name = " - ".join(results)

elif hasattr(results, "__name__"):
api_name = results.__name__
if (abstract_methods := getattr(results, "__abstractmethods__", None)) and isinstance(
abstract_methods, dict
):
unimplemented_methods.extend(results.__abstractmethods__)

else:
api_name = results

message = f"'{api_name}' from '{plugin_name}' is not fully implemented."
if unimplemented_methods:
methods_str = ", ".join(unimplemented_methods)
message = f"{message} Remaining abstract methods: '{methods_str}'."

logger.warning(message)

# Record so we don't warn repeatedly
self._unimplemented_plugins.append(plugin_name)


__all__ = [
"PluginManager",
"clean_plugin_name",
"register",
]
Loading
Loading