From 57bf99f56c72cd08713811a297c41d9b541c8fb5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 18 Nov 2024 12:09:42 +0100 Subject: [PATCH] Cleanup (#232) Changes: - Update release notes - Note explicit that app must be a module - move monkay to _monkay.py file - deprecate EnvironmentType - remove edgy.conf.functional. Its only use was the settings. - lazify all imports - provide types for files, fields modules --- docs/migrations/discovery.md | 6 +- docs/release-notes.md | 7 + docs_src/settings/custom_settings.py | 2 +- edgy/__init__.py | 94 +++----------- edgy/_monkay.py | 85 ++++++++++++ edgy/conf/enums.py | 10 +- edgy/conf/functional.py | 188 --------------------------- 7 files changed, 125 insertions(+), 267 deletions(-) create mode 100644 edgy/_monkay.py delete mode 100644 edgy/conf/functional.py diff --git a/docs/migrations/discovery.md b/docs/migrations/discovery.md index 49398224..7fc1123f 100644 --- a/docs/migrations/discovery.md +++ b/docs/migrations/discovery.md @@ -225,9 +225,13 @@ it triggered the auto discovery of the application. ##### Using the --app or EDGY_DISCOVERY_APP +!!! Note + There was a change in 0.23.0: the import path must be to a module in which the registration via the `Instance` object is automatically triggered. + See [Connection](connection.md). + ###### --app -With the `--app` flag. +With the `--app` parameter. ```shell $ edgy --app src.main makemigrations diff --git a/docs/release-notes.md b/docs/release-notes.md index fe1d0def..4b59dabe 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Changed - Rework edgy to use Monkay. +- Imports are now lazy. - Rework the migrate and shell system to simply use Monkay instance. - Replace `get_registry_copy` by `get_migration_prepared_registry`. - Breaking: migration configuration takes place in settings. @@ -23,6 +24,8 @@ hide: - Breaking: `model_apps` is replaced by `preloads` but still available during the migration time. - Breaking: An automatic registration is assumed. See [Connection](connection.md) for examples. +- Breaking: `--app` or `EDGY_DEFAULT_APP` must point to a module which does the self-registration not an app instance anymore. +- Deprecate `edgy.conf.enums.EnvironmentType`. Esmeralds `EnvironmentType` or an own definition should be used instead. ### Fixed @@ -30,6 +33,10 @@ hide: - `get_engine_url_and_metadata` was broken for some operations (thanks @kokoserver). - IPAddressField was not exposed as edgy.IPAddressField. +### Removed + +- `edgy.conf.functional`. It was only used for configuration and is now superseeded by Monkay. + ### Contributors Thanks a lot to @kokoserver. He provided a *lot* of valuable bug reports and PRs. diff --git a/docs_src/settings/custom_settings.py b/docs_src/settings/custom_settings.py index d505cb85..bd22f8ae 100644 --- a/docs_src/settings/custom_settings.py +++ b/docs_src/settings/custom_settings.py @@ -1,7 +1,7 @@ from typing import Optional from edgy import EdgySettings -from edgy.conf.enums import EnvironmentType +from esmerald.conf.enums import EnvironmentType class MyCustomSettings(EdgySettings): diff --git a/edgy/__init__.py b/edgy/__init__.py index cf0b8d78..b32cf762 100644 --- a/edgy/__init__.py +++ b/edgy/__init__.py @@ -1,36 +1,30 @@ from __future__ import annotations __version__ = "0.23.0" -import os -from importlib import import_module -from typing import TYPE_CHECKING, Any, NamedTuple, Optional +from typing import TYPE_CHECKING -from monkay import Monkay - -from edgy.conf.global_settings import EdgySettings -from edgy.core.connection import Database, DatabaseURL, Registry -from edgy.core.db.models import ( - Manager, - Model, - ModelRef, - RedirectManager, - ReflectModel, - StrictModel, -) -from edgy.core.db.querysets import Prefetch, Q, QuerySet, and_, not_, or_ -from edgy.core.utils.sync import run_sync +from ._monkay import Instance, create_monkay +from .core.utils.sync import run_sync if TYPE_CHECKING: + from .conf.global_settings import EdgySettings + from .core import files + from .core.connection import Database, DatabaseURL, Registry + from .core.db import fields from .core.db.datastructures import Index, UniqueConstraint + from .core.db.models import ( + Manager, + Model, + ModelRef, + RedirectManager, + ReflectModel, + StrictModel, + ) + from .core.db.querysets import Prefetch, Q, QuerySet, and_, not_, or_ from .core.signals import Signal from .exceptions import MultipleObjectsReturned, ObjectNotFound -class Instance(NamedTuple): - registry: Registry - app: Optional[Any] = None - - __all__ = [ "Instance", "get_migration_prepared_registry", @@ -108,61 +102,9 @@ class Instance(NamedTuple): "DatabaseURL", "Registry", ] +monkay = create_monkay(globals(), __all__) -monkay: Monkay[Instance, EdgySettings] = Monkay( - globals(), - with_extensions=True, - with_instance=True, - # must be at least an empty string to initialize the settings - settings_path=os.environ.get("EDGY_SETTINGS_MODULE", "edgy.conf.global_settings.EdgySettings"), - settings_extensions_name="extensions", - settings_preloads_name="preloads", - uncached_imports={"settings"}, - lazy_imports={ - "settings": lambda: monkay.settings, - "fields": lambda: import_module("edgy.core.db.fields"), - "files": lambda: import_module("edgy.core.files"), - "Signal": "edgy.core.signals:Signal", - "MultipleObjectsReturned": "edgy.exceptions:MultipleObjectsReturned", - "ObjectNotFound": "edgy.exceptions:ObjectNotFound", - "UniqueConstraint": "edgy.core.db.datastructures:UniqueConstraint", - "Index": "edgy.core.db.datastructures:Index", - }, - deprecated_lazy_imports={ - "Migrate": { - "path": "edgy.cli.base:Migrate", - "reason": "Use the monkay based system instead.", - "new_attribute": "Instance", - }, - "EdgyExtra": { - "path": "edgy.cli.base:Migrate", - "reason": "Use the monkay based system instead.", - "new_attribute": "Instance", - }, - }, - skip_all_update=True, -) -for name in [ - "CASCADE", - "RESTRICT", - "DO_NOTHING", - "SET_NULL", - "SET_DEFAULT", - "PROTECT", - "ConditionalRedirect", -]: - monkay.add_lazy_import(name, f"edgy.core.db.constants.{name}") - -for name in __all__: - if name.endswith("Field") or name in { - "OneToOne", - "ManyToMany", - "ForeignKey", - "RefForeignKey", - }: - monkay.add_lazy_import(name, f"edgy.core.db.fields.{name}") - -del name +del create_monkay def get_migration_prepared_registry() -> Registry: diff --git a/edgy/_monkay.py b/edgy/_monkay.py new file mode 100644 index 00000000..4e223959 --- /dev/null +++ b/edgy/_monkay.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import os +from importlib import import_module +from typing import TYPE_CHECKING, Any, NamedTuple, Optional + +from monkay import Monkay + +if TYPE_CHECKING: + from edgy.conf.global_settings import EdgySettings + from edgy.core.connection import Registry + + +class Instance(NamedTuple): + registry: Registry + app: Optional[Any] = None + + +def create_monkay(global_dict: dict, all_var: list[str]) -> Monkay[Instance, EdgySettings]: + monkay: Monkay[Instance, EdgySettings] = Monkay( + global_dict, + with_extensions=True, + with_instance=True, + # must be at least an empty string to initialize the settings + settings_path=os.environ.get( + "EDGY_SETTINGS_MODULE", "edgy.conf.global_settings.EdgySettings" + ), + settings_extensions_name="extensions", + settings_preloads_name="preloads", + uncached_imports={"settings"}, + lazy_imports={ + "settings": lambda: monkay.settings, + "EdgySettings": "edgy.conf.global_settings:EdgySettings", + "fields": lambda: import_module("edgy.core.db.fields"), + "files": lambda: import_module("edgy.core.files"), + "Signal": "edgy.core.signals:Signal", + "MultipleObjectsReturned": "edgy.exceptions:MultipleObjectsReturned", + "ObjectNotFound": "edgy.exceptions:ObjectNotFound", + "UniqueConstraint": "edgy.core.db.datastructures:UniqueConstraint", + "Index": "edgy.core.db.datastructures:Index", + }, + deprecated_lazy_imports={ + "Migrate": { + "path": "edgy.cli.base:Migrate", + "reason": "Use the monkay based system instead.", + "new_attribute": "Instance", + }, + "EdgyExtra": { + "path": "edgy.cli.base:Migrate", + "reason": "Use the monkay based system instead.", + "new_attribute": "Instance", + }, + }, + skip_all_update=True, + ) + for name in [ + "CASCADE", + "RESTRICT", + "DO_NOTHING", + "SET_NULL", + "SET_DEFAULT", + "PROTECT", + "ConditionalRedirect", + ]: + monkay.add_lazy_import(name, f"edgy.core.db.constants.{name}") + + for name in ["Database", "DatabaseURL", "Registry"]: + monkay.add_lazy_import(name, f"edgy.core.connection.{name}") + + for name in ["Prefetch", "Q", "QuerySet", "and_", "not_", "or_"]: + monkay.add_lazy_import(name, f"edgy.core.db.querysets.{name}") + + for name in ["Manager", "Model", "ModelRef", "RedirectManager", "ReflectModel", "StrictModel"]: + monkay.add_lazy_import(name, f"edgy.core.db.models.{name}") + + for name in all_var: + if name.endswith("Field") or name in { + "OneToOne", + "ManyToMany", + "ForeignKey", + "RefForeignKey", + }: + monkay.add_lazy_import(name, f"edgy.core.db.fields.{name}") + + return monkay diff --git a/edgy/conf/enums.py b/edgy/conf/enums.py index 35694e84..22e664da 100644 --- a/edgy/conf/enums.py +++ b/edgy/conf/enums.py @@ -1,8 +1,16 @@ from enum import Enum +from warnings import warn + +warn( + "This module is deprecated. Use `esmerald.conf.EnvironmentType` instead when using Esmerald " + "or define otherwise your own EnvironmentType.", + DeprecationWarning, + stacklevel=2, +) class EnvironmentType(str, Enum): - """An Enum for HTTP methods.""" + """An Enum for environments.""" DEVELOPMENT = "development" TESTING = "testing" diff --git a/edgy/conf/functional.py b/edgy/conf/functional.py deleted file mode 100644 index 10e22219..00000000 --- a/edgy/conf/functional.py +++ /dev/null @@ -1,188 +0,0 @@ -import copy -import operator -from typing import Any, Callable, TypeVar - -empty = object() -RT = TypeVar("RT") - - -def new_method_proxy(func: Callable[..., RT]) -> Callable[..., RT]: - def inner(self, *args: Any) -> RT: # type: ignore - if self._wrapped is empty: - self._setup() - return func(self._wrapped, *args) - - return inner - - -class LazyObject: # pragma: no cover - """ - A wrapper for another class that can be used to delay instantiation of the - wrapped class. - By subclassing, you have the opportunity to intercept and alter the - instantiation. If you don't need to do that, use SimpleLazyObject. - """ - - # Avoid infinite recursion when tracing __init__ (#19456). - _wrapped = None - - def __init__(self) -> None: - # Note: if a subclass overrides __init__(), it will likely need to - # override __copy__() and __deepcopy__() as well. - self._wrapped = empty - - def __getattribute__(self, name: str) -> Any: - if name == "_wrapped": - # Avoid recursion when getting wrapped object. - return super().__getattribute__(name) - value: Any = super().__getattribute__(name) - # If attribute is a proxy method, raise an AttributeError to call - # __getattr__() and use the wrapped object method. - if not getattr(value, "_mask_wrapped", True): - raise AttributeError - return value - - __getattr__ = new_method_proxy(getattr) - - def __setattr__(self, name: str, value: Any) -> None: - if name == "_wrapped": - # Assign to __dict__ to avoid infinite __setattr__ loops. - self.__dict__["_wrapped"] = value - else: - if self._wrapped is empty: - self._setup() - setattr(self._wrapped, name, value) - - def __delattr__(self, name: str) -> None: - if name == "_wrapped": - raise TypeError("can't delete _wrapped.") - if self._wrapped is empty: - self._setup() - delattr(self._wrapped, name) - - def _setup(self) -> Any: - """ - Must be implemented by subclasses to initialize the wrapped object. - """ - raise NotImplementedError("subclasses of LazyObject must provide a _setup() method") - - # Because we have messed with __class__ below, we confuse pickle as to what - # class we are pickling. We're going to have to initialize the wrapped - # object to successfully pickle it, so we might as well just pickle the - # wrapped object since they're supposed to act the same way. - # - # Unfortunately, if we try to simply act like the wrapped object, the ruse - # will break down when pickle gets our id(). Thus we end up with pickle - # thinking, in effect, that we are a distinct object from the wrapped - # object, but with the same __dict__. This can cause problems (see #25389). - # - # So instead, we define our own __reduce__ method and custom unpickler. We - # pickle the wrapped object as the unpickler's argument, so that pickle - # will pickle it normally, and then the unpickler simply returns its - # argument. - def __reduce__(self) -> Any: - if self._wrapped is empty: - self._setup() - return (unpickle_lazyobject, (self._wrapped,)) - - def __copy__(self) -> Any: - if self._wrapped is empty: - # If uninitialized, copy the wrapper. Use type(self), not - # self.__class__, because the latter is proxied. - return type(self)() - else: - # If initialized, return a copy of the wrapped object. - return copy.copy(self._wrapped) - - def __deepcopy__(self, memo: Any) -> Any: - if self._wrapped is empty: - # We have to use type(self), not self.__class__, because the - # latter is proxied. - result = type(self)() - memo[id(self)] = result - return result - return copy.deepcopy(self._wrapped, memo) - - __bytes__: Any = new_method_proxy(bytes) - __str__: Any = new_method_proxy(str) - __bool__: Any = new_method_proxy(bool) - - # Introspection support - __dir__: Any = new_method_proxy(dir) - - # Need to pretend to be the wrapped class, for the sake of objects that - # care about this (especially in equality tests) - __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) # type: ignore - __eq__: Any = new_method_proxy(operator.eq) - __lt__: Any = new_method_proxy(operator.lt) - __gt__: Any = new_method_proxy(operator.gt) - __ne__: Any = new_method_proxy(operator.ne) - __hash__: Any = new_method_proxy(hash) - - # List/Tuple/Dictionary methods support - __getitem__: Any = new_method_proxy(operator.getitem) - __setitem__: Any = new_method_proxy(operator.setitem) - __delitem__: Any = new_method_proxy(operator.delitem) - __iter__: Any = new_method_proxy(iter) - __len__: Any = new_method_proxy(len) - __contains__: Any = new_method_proxy(operator.contains) - - -def unpickle_lazyobject(wrapped: Any) -> Any: # pragma: no cover - """ - Used to unpickle lazy objects. Just return its argument, which will be the - wrapped object. - """ - return wrapped - - -class SimpleLazyObject(LazyObject): # pragma: no cover - """ - A lazy object initialized from any function. - Designed for compound objects of unknown type. For builtins or objects of - known type, use django.utils.functional.lazy. - """ - - def __init__(self, func: Callable[..., RT]) -> None: - """ - Pass in a callable that returns the object to be wrapped. - If copies are made of the resulting SimpleLazyObject, which can happen - in various circumstances within Django, then you must ensure that the - callable can be safely run more than once and will return the same - value. - """ - self.__dict__["_setupfunc"] = func - super().__init__() - - def _setup(self) -> Any: - self._wrapped = self._setupfunc() - - # Return a meaningful representation of the lazy object for debugging - # without evaluating the wrapped object. - def __repr__(self) -> str: - repr_attr = self._setupfunc if self._wrapped is empty else self._wrapped - return f"<{type(self).__name__}: {repr_attr!r}>" - - def __copy__(self) -> Any: - if self._wrapped is empty: - # If uninitialized, copy the wrapper. Use SimpleLazyObject, not - # self.__class__, because the latter is proxied. - return SimpleLazyObject(self._setupfunc) - else: - # If initialized, return a copy of the wrapped object. - return copy.copy(self._wrapped) - - def __deepcopy__(self, memo: Any) -> Any: - if self._wrapped is empty: - # We have to use SimpleLazyObject, not self.__class__, because the - # latter is proxied. - result = SimpleLazyObject(self._setupfunc) - memo[id(self)] = result - return result - return copy.deepcopy(self._wrapped, memo) - - __add__ = new_method_proxy(operator.add) - - @new_method_proxy - def __radd__(self, other: Any) -> Any: - return other + self