From b61e9667d0c642079cead0db1144b6ffee47a640 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 16:46:41 +0200 Subject: [PATCH 1/9] refactor: Another attempt at better typing --- django_mongoengine/document.py | 21 +-------------------- django_mongoengine/document.pyi | 31 +++++++++++++++++++++++++++++++ django_mongoengine/queryset.py | 21 +++++++++++++++------ 3 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 django_mongoengine/document.pyi diff --git a/django_mongoengine/document.py b/django_mongoengine/document.py index eca4056..cbd3add 100644 --- a/django_mongoengine/document.py +++ b/django_mongoengine/document.py @@ -2,7 +2,6 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, Any from bson.objectid import ObjectId from django.db.models import Model @@ -43,10 +42,7 @@ def __new__(cls, name, bases, attrs): class DjangoFlavor: - id: Any - objects: Any = QuerySetManager() - _meta: DocumentMetaWrapper - _default_manager: Any = QuerySetManager() + _default_manager = QuerySetManager() _get_pk_val = Model.__dict__["_get_pk_val"] DoesNotExist: type[DoesNotExist] @@ -104,18 +100,3 @@ class DynamicEmbeddedDocument( django_meta(mtc.DocumentMetaclass, DjangoFlavor, me.DynamicEmbeddedDocument) ): swap_base = True - - -if TYPE_CHECKING: - - class Document(DjangoFlavor, me.Document): - ... - - class DynamicDocument(DjangoFlavor, me.DynamicDocument): - ... - - class EmbeddedDocument(DjangoFlavor, me.EmbeddedDocument): - ... - - class DynamicEmbeddedDocument(DjangoFlavor, me.DynamicEmbeddedDocument): - ... diff --git a/django_mongoengine/document.pyi b/django_mongoengine/document.pyi new file mode 100644 index 0000000..2e7542a --- /dev/null +++ b/django_mongoengine/document.pyi @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing_extensions import Self + +from mongoengine import DoesNotExist +from mongoengine import document as me +from mongoengine.fields import StringField + +from .queryset import QuerySetManager +from .forms.document_options import DocumentMetaWrapper + +class DjangoFlavor: + id: StringField + # https://github.com/sbdchd/mongo-types#getting-objects-to-work + # Types moved to pyi, to avoid monkey-patching QuerySet. + objects: QuerySetManager[Self] + _meta: DocumentMetaWrapper + _default_manager: QuerySetManager[Self] + DoesNotExist: type[DoesNotExist] + +class Document(DjangoFlavor, me.Document): + ... + +class DynamicDocument(DjangoFlavor, me.DynamicDocument): + ... + +class EmbeddedDocument(DjangoFlavor, me.EmbeddedDocument): + _instance: Document + +class DynamicEmbeddedDocument(DjangoFlavor, me.DynamicEmbeddedDocument): + ... diff --git a/django_mongoengine/queryset.py b/django_mongoengine/queryset.py index b1e522b..f7ea563 100644 --- a/django_mongoengine/queryset.py +++ b/django_mongoengine/queryset.py @@ -1,8 +1,14 @@ +from typing import TYPE_CHECKING, Generic, TypeVar from django.db.models.query import QuerySet as DjangoQuerySet from django.db.models.utils import resolve_callables from mongoengine import queryset as qs from mongoengine.errors import NotUniqueError +if TYPE_CHECKING: + from .document import Document + +_M = TypeVar("_M", bound=Document) + class QueryWrapper: # XXX: copy funcs from django; now it's just wrapper @@ -14,13 +20,13 @@ def __init__(self, q, ordering): self.order_by = ordering or [] -class BaseQuerySet: +class BaseQuerySet(Generic[_M]): """ A base queryset with django-required attributes """ @property - def model(self): + def model(self) -> type[_M]: return self._document @property @@ -46,7 +52,7 @@ def latest(self, field_name): def earliest(self, field_name): return self.order_by(field_name).first() - def exists(self): + def exists(self) -> bool: return bool(self) def _clone(self): @@ -106,13 +112,16 @@ def update_or_create(self, defaults=None, **kwargs): _extract_model_params = DjangoQuerySet.__dict__["_extract_model_params"] -class QuerySet(BaseQuerySet, qs.QuerySet): +class QuerySet(BaseQuerySet[_M], qs.QuerySet[_M]): pass -class QuerySetNoCache(BaseQuerySet, qs.QuerySetNoCache): +class QuerySetNoCache(BaseQuerySet[_M], qs.QuerySetNoCache[_M]): pass -class QuerySetManager(qs.QuerySetManager): +class QuerySetManager(Generic[_M], qs.QuerySetManager): default = QuerySet + + def __get__(self, instance: object, cls: type[_M]) -> QuerySet[_M]: + return QuerySet(cls, cls._get_collection()) From 8a0dbf616c8e9878f96635a181e53b8d9a43c975 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 17:49:28 +0200 Subject: [PATCH 2/9] fixes in progress --- Makefile | 5 + codegen.py | 31 ++++ django_mongoengine/document.py | 23 ++- django_mongoengine/document.pyi | 31 ---- django_mongoengine/fields/__init__.py | 203 ++++++++++++++++++++++---- django_mongoengine/queryset.py | 11 +- django_mongoengine/utils/monkey.py | 28 ++++ poetry.lock | 13 +- pyproject.toml | 1 + 9 files changed, 278 insertions(+), 68 deletions(-) create mode 100644 codegen.py delete mode 100644 django_mongoengine/document.pyi diff --git a/Makefile b/Makefile index 648ca42..b285e0f 100644 --- a/Makefile +++ b/Makefile @@ -12,3 +12,8 @@ publish: test: poetry run python -m pytest + +codegen: + python codegen.py + black django_mongoengine/fields/__init__.py + ruff django_mongoengine/ --fix # It doesn't work with filename. diff --git a/codegen.py b/codegen.py new file mode 100644 index 0000000..b30221b --- /dev/null +++ b/codegen.py @@ -0,0 +1,31 @@ +def generate_fields(): + """ + Typing support cannot handle monkey-patching at runtime, so we need to generate fields explicitly. + """ + from mongoengine import fields + from django_mongoengine.fields import djangoflavor as mixins + + fields_code = str(_fields) + for fname in fields.__all__: + mixin_name = fname if hasattr(mixins, fname) else "DjangoField" + fields_code += f"class {fname}(_mixins.{mixin_name}, _fields.{fname}):\n pass\n" + + return fields_code + + +_fields = """ +from mongoengine import fields as _fields +from . import djangoflavor as _mixins +from django_mongoengine.utils.monkey import patch_mongoengine_field + +for f in ["StringField", "ObjectIdField"]: + patch_mongoengine_field(f) + +""" + +if __name__ == "__main__": + fname = "django_mongoengine/fields/__init__.py" + # This content required, because otherwise mixins import does not work. + open(fname, "w").write("from mongoengine.fields import *") + content = generate_fields() + open(fname, "w").write(content) diff --git a/django_mongoengine/document.py b/django_mongoengine/document.py index cbd3add..b3bfd72 100644 --- a/django_mongoengine/document.py +++ b/django_mongoengine/document.py @@ -2,6 +2,7 @@ from __future__ import annotations from functools import partial +from typing import TYPE_CHECKING from bson.objectid import ObjectId from django.db.models import Model @@ -10,6 +11,8 @@ from mongoengine import document as me from mongoengine.base import metaclasses as mtc from mongoengine.errors import FieldDoesNotExist +from mongoengine.fields import StringField +from typing_extensions import Self from .fields import ObjectIdField from .forms.document_options import DocumentMetaWrapper @@ -42,8 +45,11 @@ def __new__(cls, name, bases, attrs): class DjangoFlavor: - _default_manager = QuerySetManager() + id: StringField + objects = QuerySetManager[Self]() + _default_manager = QuerySetManager[Self]() _get_pk_val = Model.__dict__["_get_pk_val"] + _meta: DocumentMetaWrapper DoesNotExist: type[DoesNotExist] def __init__(self, *args, **kwargs): @@ -100,3 +106,18 @@ class DynamicEmbeddedDocument( django_meta(mtc.DocumentMetaclass, DjangoFlavor, me.DynamicEmbeddedDocument) ): swap_base = True + + +if TYPE_CHECKING: + + class Document(DjangoFlavor, me.Document): + ... + + class DynamicDocument(DjangoFlavor, me.DynamicDocument): + ... + + class EmbeddedDocument(DjangoFlavor, me.EmbeddedDocument): + _instance: Document + + class DynamicEmbeddedDocument(DjangoFlavor, me.DynamicEmbeddedDocument): + ... diff --git a/django_mongoengine/document.pyi b/django_mongoengine/document.pyi deleted file mode 100644 index 2e7542a..0000000 --- a/django_mongoengine/document.pyi +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from typing_extensions import Self - -from mongoengine import DoesNotExist -from mongoengine import document as me -from mongoengine.fields import StringField - -from .queryset import QuerySetManager -from .forms.document_options import DocumentMetaWrapper - -class DjangoFlavor: - id: StringField - # https://github.com/sbdchd/mongo-types#getting-objects-to-work - # Types moved to pyi, to avoid monkey-patching QuerySet. - objects: QuerySetManager[Self] - _meta: DocumentMetaWrapper - _default_manager: QuerySetManager[Self] - DoesNotExist: type[DoesNotExist] - -class Document(DjangoFlavor, me.Document): - ... - -class DynamicDocument(DjangoFlavor, me.DynamicDocument): - ... - -class EmbeddedDocument(DjangoFlavor, me.EmbeddedDocument): - _instance: Document - -class DynamicEmbeddedDocument(DjangoFlavor, me.DynamicEmbeddedDocument): - ... diff --git a/django_mongoengine/fields/__init__.py b/django_mongoengine/fields/__init__.py index a6b93f3..de68ad5 100644 --- a/django_mongoengine/fields/__init__.py +++ b/django_mongoengine/fields/__init__.py @@ -1,43 +1,182 @@ -from . import djangoflavor +from mongoengine import fields as _fields +from . import djangoflavor as _mixins +from django_mongoengine.utils.monkey import patch_mongoengine_field +for f in ["StringField", "ObjectIdField"]: + patch_mongoengine_field(f) -def init_module(): - """ - Create classes with Django-flavor mixins, - use DjangoField mixin as default - """ - import sys - from mongoengine import fields +class StringField(_mixins.StringField, _fields.StringField): + pass - current_module = sys.modules[__name__] - current_module.__all__ = fields.__all__ - for name in fields.__all__: - fieldcls = getattr(fields, name) - mixin = getattr(djangoflavor, name, djangoflavor.DjangoField) - setattr( - current_module, - name, - type(name, (mixin, fieldcls), {}), - ) +class URLField(_mixins.URLField, _fields.URLField): + pass -def patch_mongoengine_field(field_name): - """ - patch mongoengine.[field_name] for comparison support - becouse it's required in django.forms.models.fields_for_model - importing using mongoengine internal import cache - """ - from mongoengine import common +class EmailField(_mixins.EmailField, _fields.EmailField): + pass - field = common._import_class(field_name) - for k in ["__eq__", "__lt__", "__hash__", "attname", "get_internal_type"]: - if k not in field.__dict__: - setattr(field, k, djangoflavor.DjangoField.__dict__[k]) +class IntField(_mixins.IntField, _fields.IntField): + pass -init_module() -for f in ["StringField", "ObjectIdField"]: - patch_mongoengine_field(f) +class LongField(_mixins.DjangoField, _fields.LongField): + pass + + +class FloatField(_mixins.FloatField, _fields.FloatField): + pass + + +class DecimalField(_mixins.DecimalField, _fields.DecimalField): + pass + + +class BooleanField(_mixins.BooleanField, _fields.BooleanField): + pass + + +class DateTimeField(_mixins.DateTimeField, _fields.DateTimeField): + pass + + +class DateField(_mixins.DjangoField, _fields.DateField): + pass + + +class ComplexDateTimeField(_mixins.DjangoField, _fields.ComplexDateTimeField): + pass + + +class EmbeddedDocumentField(_mixins.EmbeddedDocumentField, _fields.EmbeddedDocumentField): + pass + + +class ObjectIdField(_mixins.DjangoField, _fields.ObjectIdField): + pass + + +class GenericEmbeddedDocumentField(_mixins.DjangoField, _fields.GenericEmbeddedDocumentField): + pass + + +class DynamicField(_mixins.DjangoField, _fields.DynamicField): + pass + + +class ListField(_mixins.ListField, _fields.ListField): + pass + + +class SortedListField(_mixins.DjangoField, _fields.SortedListField): + pass + + +class EmbeddedDocumentListField(_mixins.DjangoField, _fields.EmbeddedDocumentListField): + pass + + +class DictField(_mixins.DictField, _fields.DictField): + pass + + +class MapField(_mixins.DjangoField, _fields.MapField): + pass + + +class ReferenceField(_mixins.ReferenceField, _fields.ReferenceField): + pass + + +class CachedReferenceField(_mixins.DjangoField, _fields.CachedReferenceField): + pass + + +class LazyReferenceField(_mixins.DjangoField, _fields.LazyReferenceField): + pass + + +class GenericLazyReferenceField(_mixins.DjangoField, _fields.GenericLazyReferenceField): + pass + + +class GenericReferenceField(_mixins.DjangoField, _fields.GenericReferenceField): + pass + + +class BinaryField(_mixins.DjangoField, _fields.BinaryField): + pass + + +class GridFSError(_mixins.DjangoField, _fields.GridFSError): + pass + + +class GridFSProxy(_mixins.DjangoField, _fields.GridFSProxy): + pass + + +class FileField(_mixins.FileField, _fields.FileField): + pass + + +class ImageGridFsProxy(_mixins.DjangoField, _fields.ImageGridFsProxy): + pass + + +class ImproperlyConfigured(_mixins.ImproperlyConfigured, _fields.ImproperlyConfigured): + pass + + +class ImageField(_mixins.ImageField, _fields.ImageField): + pass + + +class GeoPointField(_mixins.DjangoField, _fields.GeoPointField): + pass + + +class PointField(_mixins.DjangoField, _fields.PointField): + pass + + +class LineStringField(_mixins.DjangoField, _fields.LineStringField): + pass + + +class PolygonField(_mixins.DjangoField, _fields.PolygonField): + pass + + +class SequenceField(_mixins.DjangoField, _fields.SequenceField): + pass + + +class UUIDField(_mixins.DjangoField, _fields.UUIDField): + pass + + +class EnumField(_mixins.DjangoField, _fields.EnumField): + pass + + +class MultiPointField(_mixins.DjangoField, _fields.MultiPointField): + pass + + +class MultiLineStringField(_mixins.DjangoField, _fields.MultiLineStringField): + pass + + +class MultiPolygonField(_mixins.DjangoField, _fields.MultiPolygonField): + pass + + +class GeoJsonBaseField(_mixins.DjangoField, _fields.GeoJsonBaseField): + pass + + +class Decimal128Field(_mixins.DjangoField, _fields.Decimal128Field): + pass diff --git a/django_mongoengine/queryset.py b/django_mongoengine/queryset.py index f7ea563..1548a1e 100644 --- a/django_mongoengine/queryset.py +++ b/django_mongoengine/queryset.py @@ -4,10 +4,14 @@ from mongoengine import queryset as qs from mongoengine.errors import NotUniqueError +from .utils.monkey import patch_typing_support + if TYPE_CHECKING: from .document import Document -_M = TypeVar("_M", bound=Document) +_M = TypeVar("_M", bound="Document") + +patch_typing_support() class QueryWrapper: @@ -122,6 +126,7 @@ class QuerySetNoCache(BaseQuerySet[_M], qs.QuerySetNoCache[_M]): class QuerySetManager(Generic[_M], qs.QuerySetManager): default = QuerySet + if TYPE_CHECKING: - def __get__(self, instance: object, cls: type[_M]) -> QuerySet[_M]: - return QuerySet(cls, cls._get_collection()) + def __get__(self, instance: object, cls: type[_M]) -> QuerySet[_M]: + ... diff --git a/django_mongoengine/utils/monkey.py b/django_mongoengine/utils/monkey.py index 9c51b9c..b7063ed 100644 --- a/django_mongoengine/utils/monkey.py +++ b/django_mongoengine/utils/monkey.py @@ -1,5 +1,10 @@ import importlib import importlib.util +from types import MethodType + +from mongoengine.queryset import QuerySet, QuerySetNoCache + +from django_mongoengine.fields.djangoflavor import DjangoField def get_patched_django_module(modname: str, **kwargs): @@ -16,3 +21,26 @@ def get_patched_django_module(modname: str, **kwargs): for k, v in kwargs.items(): setattr(module, k, v) return module + + +def patch_mongoengine_field(field_name: str): + """ + patch mongoengine.[field_name] for comparison support + becouse it's required in django.forms.models.fields_for_model + importing using mongoengine internal import cache + """ + from mongoengine import common + + field = common._import_class(field_name) + for k in ["__eq__", "__lt__", "__hash__", "attname", "get_internal_type"]: + if k not in field.__dict__: + setattr(field, k, DjangoField.__dict__[k]) + + +def patch_typing_support(): + """ + Patch classes to support generic types + """ + + for model in [QuerySet, QuerySetNoCache]: + model.__class_getitem__ = MethodType(lambda cls, _: cls, QuerySet) # type: ignore diff --git a/poetry.lock b/poetry.lock index 3ecea44..d09856b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -911,6 +911,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + [[package]] name = "tzdata" version = "2023.3" @@ -951,4 +962,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "4162eec510279930014f0768ba7b66deafde189e3d969e9ab26fbb19fe3c683f" +content-hash = "b6661a54d92b473ac2ef5f5d047cdc49833cc2ee12948e6ba25913b50cde835a" diff --git a/pyproject.toml b/pyproject.toml index e5179f0..37ad844 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = ["Ross Lawley "] python = ">=3.8" django = ">=3.2,<5" mongoengine = ">=0.14" +typing-extensions = "^4.8.0" [tool.poetry.dev-dependencies] sphinx = "*" From 5fd26a59be7b3b414b9077bc962801b17a703c7b Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:05:20 +0200 Subject: [PATCH 3/9] lint fixes --- .github/workflows/test.yml | 2 ++ django_mongoengine/document.py | 4 +++- django_mongoengine/forms/fields.py | 2 +- django_mongoengine/queryset.py | 3 +++ poetry.lock | 28 +++++++++++++++++++++++++++- pyproject.toml | 20 ++++++++++++++------ tests/forms/tests.py | 6 +++--- 7 files changed, 53 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47537fd..045392d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,4 +43,6 @@ jobs: poetry run pip install -q "${{ matrix.django }}" - name: Run tests run: | + poetry run ruff . + poetry run black --check . poetry run python -m pytest diff --git a/django_mongoengine/document.py b/django_mongoengine/document.py index b3bfd72..fdf1d7b 100644 --- a/django_mongoengine/document.py +++ b/django_mongoengine/document.py @@ -11,13 +11,15 @@ from mongoengine import document as me from mongoengine.base import metaclasses as mtc from mongoengine.errors import FieldDoesNotExist -from mongoengine.fields import StringField from typing_extensions import Self from .fields import ObjectIdField from .forms.document_options import DocumentMetaWrapper from .queryset import QuerySetManager +if TYPE_CHECKING: + from mongoengine.fields import StringField + # TopLevelDocumentMetaclass is using ObjectIdField to create default pk field, # if one's not set explicitly. # We need to know it's not editable and auto_created. diff --git a/django_mongoengine/forms/fields.py b/django_mongoengine/forms/fields.py index 4f75bce..8428233 100644 --- a/django_mongoengine/forms/fields.py +++ b/django_mongoengine/forms/fields.py @@ -107,7 +107,7 @@ def __init__(self, form, *args, **kwargs): kwargs['widget'] = EmbeddedFieldWidget(self.form.fields) kwargs['initial'] = [f.initial for f in self.form.fields.values()] kwargs['require_all_fields'] = False - super().__init__(fields=tuple([f for f in self.form.fields.values()]), *args, **kwargs) + super().__init__(fields=tuple(self.form.fields.values()), *args, **kwargs) def bound_data(self, data, initial): return data diff --git a/django_mongoengine/queryset.py b/django_mongoengine/queryset.py index 1548a1e..62f622b 100644 --- a/django_mongoengine/queryset.py +++ b/django_mongoengine/queryset.py @@ -1,4 +1,7 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Generic, TypeVar + from django.db.models.query import QuerySet as DjangoQuerySet from django.db.models.utils import resolve_callables from mongoengine import queryset as qs diff --git a/poetry.lock b/poetry.lock index d09856b..eb55a6d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -719,6 +719,32 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "ruff" +version = "0.1.3" +description = "An extremely fast Python linter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.3-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b46d43d51f7061652eeadb426a9e3caa1e0002470229ab2fc19de8a7b0766901"}, + {file = "ruff-0.1.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b8afeb9abd26b4029c72adc9921b8363374f4e7edb78385ffaa80278313a15f9"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca3cf365bf32e9ba7e6db3f48a4d3e2c446cd19ebee04f05338bc3910114528b"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4874c165f96c14a00590dcc727a04dca0cfd110334c24b039458c06cf78a672e"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eec2dd31eed114e48ea42dbffc443e9b7221976554a504767ceaee3dd38edeb8"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dc3ec4edb3b73f21b4aa51337e16674c752f1d76a4a543af56d7d04e97769613"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e3de9ed2e39160800281848ff4670e1698037ca039bda7b9274f849258d26ce"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c595193881922cc0556a90f3af99b1c5681f0c552e7a2a189956141d8666fe8"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f75e670d529aa2288cd00fc0e9b9287603d95e1536d7a7e0cafe00f75e0dd9d"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76dd49f6cd945d82d9d4a9a6622c54a994689d8d7b22fa1322983389b4892e20"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:918b454bc4f8874a616f0d725590277c42949431ceb303950e87fef7a7d94cb3"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8859605e729cd5e53aa38275568dbbdb4fe882d2ea2714c5453b678dca83784"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b6c55f5ef8d9dd05b230bb6ab80bc4381ecb60ae56db0330f660ea240cb0d4a"}, + {file = "ruff-0.1.3-py3-none-win32.whl", hash = "sha256:3e7afcbdcfbe3399c34e0f6370c30f6e529193c731b885316c5a09c9e4317eef"}, + {file = "ruff-0.1.3-py3-none-win_amd64.whl", hash = "sha256:7a18df6638cec4a5bd75350639b2bb2a2366e01222825562c7346674bdceb7ea"}, + {file = "ruff-0.1.3-py3-none-win_arm64.whl", hash = "sha256:12fd53696c83a194a2db7f9a46337ce06445fb9aa7d25ea6f293cf75b21aca9f"}, + {file = "ruff-0.1.3.tar.gz", hash = "sha256:3ba6145369a151401d5db79f0a47d50e470384d0d89d0d6f7fab0b589ad07c34"}, +] + [[package]] name = "setuptools" version = "67.2.0" @@ -962,4 +988,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "b6661a54d92b473ac2ef5f5d047cdc49833cc2ee12948e6ba25913b50cde835a" +content-hash = "f29499b90951406750235c158355984f0bda304bc9f0fe9acad80b7364116fb6" diff --git a/pyproject.toml b/pyproject.toml index 37ad844..241bf7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,13 +19,21 @@ pytest-django = "*" pytest-cov = "*" pytest-sugar = "*" +[tool.poetry.group.dev.dependencies] +ruff = "^0.1.3" + [tool.black] line-length = 100 skip-string-normalization = true -[tool.isort] -line_length = 100 -known_first_party = ["django_mongoengine"] -multi_line_output = 3 -skip_gitignore = true -include_trailing_comma = true +[tool.ruff] +line-length = 100 +select = [ + "E", + "F", + "FA", + "T20", + "TCH", + "C4", +] +ignore = ["E501"] diff --git a/tests/forms/tests.py b/tests/forms/tests.py index c5f032a..c828afa 100644 --- a/tests/forms/tests.py +++ b/tests/forms/tests.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.forms.models import modelform_factory - +import pytest from django_mongoengine.forms import widgets from django_mongoengine.forms.documents import documentform_factory from django_mongoengine.forms.fields import DictField @@ -159,6 +159,8 @@ def test_rendering(self): self.field.widget.render('widget_name', data_dicts[data]) self._check_structure(self.field.widget, output_structures[data], 'test_rendering:2') + # I don't understand why it fails; disable this check for now + @pytest.mark.skip(reason="Broken for now") def test_static(self): self._init_field(force=True) structure = { @@ -204,8 +206,6 @@ def test_static(self): }, ], } - print("I don't understand why it fails; disable this check for now") - return self._check_structure(self.field.widget, structure, 'test_static:1') def _init_field(self, depth=None, force=False): From 2d265ff95929cd206f2dc560fa6c4813ca4514a1 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:09:21 +0200 Subject: [PATCH 4/9] chore: update --- poetry.lock | 2 +- pyproject.toml | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index eb55a6d..36747d1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -988,4 +988,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "f29499b90951406750235c158355984f0bda304bc9f0fe9acad80b7364116fb6" +content-hash = "6d35b0797c46a1b99a0e176e2537da9df5829bb617b42d9bdb2b06d4360e4fe6" diff --git a/pyproject.toml b/pyproject.toml index 241bf7d..4994a2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,17 +10,15 @@ django = ">=3.2,<5" mongoengine = ">=0.14" typing-extensions = "^4.8.0" -[tool.poetry.dev-dependencies] -sphinx = "*" -pytest = "*" +[tool.poetry.group.dev.dependencies] flake8 = "*" pdbpp = "*" -pytest-django = "*" +pytest = "*" pytest-cov = "*" +pytest-django = "*" pytest-sugar = "*" - -[tool.poetry.group.dev.dependencies] ruff = "^0.1.3" +sphinx = "*" [tool.black] line-length = 100 From ea176d800ffa9ea0c063bed62004da746740fbcb Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:16:12 +0200 Subject: [PATCH 5/9] chore: Add venv cahing --- .github/workflows/test.yml | 6 ++++++ poetry.toml | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 poetry.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 045392d..532b547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,12 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + cache: poetry + - name: Cache virtualenv + uses: actions/cache@v3 + with: + key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('poetry.lock') }}-${{ matrix.django }} + path: .venv - name: Set up env run: | python -m pip install -U -q poetry pip diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true From b5d872c68ef6c9bd34f9e25e91e1b0c8f87901a0 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:22:59 +0200 Subject: [PATCH 6/9] add black --- poetry.lock | 95 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 36747d1..986197c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,6 +85,48 @@ files = [ [package.extras] tzdata = ["tzdata"] +[[package]] +name = "black" +version = "23.10.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2023.7.22" @@ -110,6 +152,20 @@ files = [ [package.extras] unicode-backport = ["unicodedata2"] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -416,6 +472,17 @@ files = [ [package.dependencies] pymongo = ">=3.4,<5.0" +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" version = "23.0" @@ -427,6 +494,17 @@ files = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + [[package]] name = "pdbpp" version = "0.10.3" @@ -447,6 +525,21 @@ wmctrl = "*" funcsigs = ["funcsigs"] testing = ["funcsigs", "pytest"] +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -988,4 +1081,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "6d35b0797c46a1b99a0e176e2537da9df5829bb617b42d9bdb2b06d4360e4fe6" +content-hash = "fcd14244fbb71c85a98d1694f20a6d6f67e89e0eb6ffe5f82905ce0dddf1d509" diff --git a/pyproject.toml b/pyproject.toml index 4994a2e..69874cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ pytest-django = "*" pytest-sugar = "*" ruff = "^0.1.3" sphinx = "*" +black = "^23.10.1" [tool.black] line-length = 100 From 286a851fabea794d9a9c945bf3ee569a4060188a Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:23:53 +0200 Subject: [PATCH 7/9] cleanup --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 532b547..a956f99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Cache virtualenv uses: actions/cache@v3 with: - key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('poetry.lock') }}-${{ matrix.django }} + key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('poetry.lock') }} path: .venv - name: Set up env run: | From 1ebef184ec2847e9b5ad03d392ccc7e5dcd5c9b2 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:25:29 +0200 Subject: [PATCH 8/9] fix cache --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a956f99..f47a783 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - cache: poetry + cache: 'poetry' - name: Cache virtualenv uses: actions/cache@v3 with: From 35e3b5888e5df7cd568442d2ea8decef1c4f62cc Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 3 Nov 2023 22:27:58 +0200 Subject: [PATCH 9/9] chore install poetry --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f47a783..7d033a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,8 @@ jobs: - 27017:27017 steps: - uses: actions/checkout@v3 + - name: Install poetry + run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -44,7 +46,6 @@ jobs: path: .venv - name: Set up env run: | - python -m pip install -U -q poetry pip poetry install -q poetry run pip install -q "${{ matrix.django }}" - name: Run tests