diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a789c894..935e1d87 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: mkdir test_results uv run ruff check . uv run ruff format --check . - uv run mypy . + uv run pyrefly check working-directory: src/ tests: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d42b14a9..cb45f72e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,21 +12,21 @@ repos: - id: check-added-large-files - repo: https://github.com/PyCQA/bandit - rev: 1.8.6 + rev: 1.9.4 hooks: - id: bandit args: ["-c", "pyproject.toml"] additional_dependencies: ["bandit[toml]"] - repo: https://github.com/asottile/pyupgrade - rev: v3.21.1 + rev: v3.21.2 hooks: - id: pyupgrade args: ["--py313-plus"] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.4 + rev: v0.15.10 hooks: # Run the linter. - id: ruff-check @@ -39,4 +39,12 @@ repos: - id: commitizen-branch stages: - pre-push - rev: v4.9.1 + rev: v4.13.9 + + - repo: https://github.com/facebook/pyrefly-pre-commit + rev: 0.60.1 + hooks: + - id: pyrefly-check + name: Pyrefly (type checking) + pass_filenames: false # Recommended to do full repo checks. However, you can change this to `true` to only check changed files + language: system # Use system-installed pyrefly diff --git a/pyproject.toml b/pyproject.toml index 2184093b..07de25ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,6 @@ dependencies = [ "django-health-check~=4.0", "django-jet-reboot>=1.3.9", "django-storages[s3]>=1.14.4", - "django-stubs[compatible-mypy]~=5.2.0", # Pinned to 5.2.* for compatibility with Django 5.2 - "django-stubs-ext~=5.2.0", # Pinned to 5.2.* for compatibility with Django 5.2 "django~=5.2.0", "djangorestframework>=3.15.2", "drf-spectacular>=0.27.2", @@ -39,9 +37,9 @@ dev = [ "coverage>=7.6.1", "django-debug-toolbar>=4.4.6", "django-filter-stubs>=0.1.3", - "djangorestframework-stubs[compatible-mypy]>=3.16", - "mypy~=1.13.0", # Pinned range for compatibility with Django 5.2 + "django-stubs>=5.2.9", "peek-python>=25.0.7", + "pyrefly>=0.60.1", "pytest>=8.3.3", "pytest-cov>=5.0.0", "pytest-django>=4.9.0", @@ -50,11 +48,6 @@ dev = [ "ruff>=0.6.6", ] -[tool.mypy] -strict = true -plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"] -mypy_path = "src/" - [tool.django-stubs] django_settings_module = "snapy.settings" @@ -163,3 +156,6 @@ insecure = false [tool.semantic_release.publish] dist_glob_patterns = ["dist/*"] upload_to_vcs_release = true + +[tool.pyrefly] +search-path = ["src/"] diff --git a/src/api/admin.py b/src/api/admin.py index edb518e8..9be823c2 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -16,7 +16,7 @@ from api.models.abc import NewsItem -class ArticleForm(forms.ModelForm[NewsItem]): +class ArticleForm(forms.ModelForm): title = forms.CharField(widget=forms.TextInput(attrs={"size": 70}), required=True) @@ -24,7 +24,7 @@ class ArticleForm(forms.ModelForm[NewsItem]): # Models that need customization @admin.register(Article) @admin.register(Blog) -class ArticleAdmin(admin.ModelAdmin[NewsItem]): +class ArticleAdmin(admin.ModelAdmin): """Admin view for articles and blogs.""" list_per_page = 30 @@ -74,14 +74,14 @@ class ArticleAdmin(admin.ModelAdmin[NewsItem]): ] @admin.action(description="Mark selected articles as audited") - def mark_as_audited(self, request: HttpRequest, queryset: QuerySet[NewsItem]) -> None: + def mark_as_audited(self, request: HttpRequest, queryset: QuerySet) -> None: queryset.update(audited=True) @admin.action(description="Unmark selected articles as audited") - def unmark_as_audited(self, request: HttpRequest, queryset: QuerySet[NewsItem]) -> None: + def unmark_as_audited(self, request: HttpRequest, queryset: QuerySet) -> None: queryset.update(audited=False) - def get_queryset(self, request: HttpRequest) -> QuerySet[NewsItem]: + def get_queryset(self, request: HttpRequest) -> QuerySet: """Return the queryset with related fields prefetched.""" qs = super().get_queryset(request).select_related("news_site").prefetch_related("launches", "events") return qs @@ -214,7 +214,7 @@ def changelist_view(self, request: HttpRequest, extra_context: dict[str, str] | extra_context = {"title": "News"} return super().changelist_view(request, extra_context) - def save_model(self, request: HttpRequest, obj: NewsItem, form: forms.ModelForm[NewsItem], change: bool) -> None: + def save_model(self, request: HttpRequest, obj: NewsItem, form: forms.ModelForm, change: bool) -> None: if change: # Ignore the type error as obj will be an instance of Article of Blog old_object: NewsItem = type(obj).objects.get(pk=obj.pk) # type: ignore @@ -231,20 +231,20 @@ def save_model(self, request: HttpRequest, obj: NewsItem, form: forms.ModelForm[ @admin.register(Report) -class ReportAdmin(admin.ModelAdmin[Report]): +class ReportAdmin(admin.ModelAdmin): """Custom admin view for reports.""" list_display = ("title", "news_site", "published_at", "is_deleted") search_fields = ["title"] ordering = ("-published_at",) - def get_queryset(self, request: HttpRequest) -> QuerySet[Report]: + def get_queryset(self, request: HttpRequest) -> QuerySet: """Return the queryset with related fields prefetched.""" return super().get_queryset(request).select_related("news_site") @admin.register(NewsSite) -class NewsSiteAdmin(admin.ModelAdmin[NewsSite]): +class NewsSiteAdmin(admin.ModelAdmin): list_display = ("name", "id") def changelist_view(self, request: HttpRequest, extra_context: dict[str, str] | None = None) -> HttpResponse: @@ -255,13 +255,13 @@ def changelist_view(self, request: HttpRequest, extra_context: dict[str, str] | # Models that can be registered as is @admin.register(Event) -class EventAdmin(admin.ModelAdmin[Event]): +class EventAdmin(admin.ModelAdmin): list_display = ("name",) search_fields = ["name", "event_id"] @admin.register(Launch) -class LaunchAdmin(admin.ModelAdmin[Launch]): +class LaunchAdmin(admin.ModelAdmin): list_display = ("name",) search_fields = ["name", "launch_id"] diff --git a/src/api/serializers/article_serializer.py b/src/api/serializers/article_serializer.py index 94f0eafd..3488a24b 100644 --- a/src/api/serializers/article_serializer.py +++ b/src/api/serializers/article_serializer.py @@ -1,18 +1,18 @@ from rest_framework import serializers -from api.models import Article, NewsSite +from api.models import Article from api.serializers.author_serializer import AuthorSerializer from api.serializers.event_serializer import EventSerializer from api.serializers.launch_serializer import LaunchSerializer -class ArticleSerializer(serializers.ModelSerializer[Article]): - news_site: "serializers.StringRelatedField[NewsSite]" = serializers.StringRelatedField() +class ArticleSerializer(serializers.ModelSerializer): + news_site = serializers.StringRelatedField() launches = LaunchSerializer(many=True) events = EventSerializer(many=True) authors = AuthorSerializer(many=True) - class Meta: + class Meta: # pyrefly: ignore[bad-override] model = Article fields = [ "id", diff --git a/src/api/serializers/author_serializer.py b/src/api/serializers/author_serializer.py index 95db41d4..da5dc0ba 100644 --- a/src/api/serializers/author_serializer.py +++ b/src/api/serializers/author_serializer.py @@ -4,9 +4,9 @@ from api.serializers.socials_serializer import SocialsSerializer -class AuthorSerializer(serializers.ModelSerializer[Author]): +class AuthorSerializer(serializers.ModelSerializer): socials = SocialsSerializer(required=False) - class Meta: + class Meta: # pyrefly: ignore[bad-override] model = Author fields = ["name", "socials"] diff --git a/src/api/serializers/blog_serializer.py b/src/api/serializers/blog_serializer.py index 23cf5212..60a5bd45 100644 --- a/src/api/serializers/blog_serializer.py +++ b/src/api/serializers/blog_serializer.py @@ -1,18 +1,18 @@ from rest_framework import serializers -from api.models import Blog, NewsSite +from api.models import Blog from api.serializers.author_serializer import AuthorSerializer from api.serializers.event_serializer import EventSerializer from api.serializers.launch_serializer import LaunchSerializer class BlogSerializer(serializers.ModelSerializer[Blog]): - news_site: "serializers.StringRelatedField[NewsSite]" = serializers.StringRelatedField() + news_site = serializers.StringRelatedField() launches = LaunchSerializer(many=True) events = EventSerializer(many=True) authors = AuthorSerializer(many=True) - class Meta: + class Meta: # pyrefly: ignore[bad-override] model = Blog fields = [ "id", diff --git a/src/api/serializers/event_serializer.py b/src/api/serializers/event_serializer.py index 6d6813b6..dca05150 100644 --- a/src/api/serializers/event_serializer.py +++ b/src/api/serializers/event_serializer.py @@ -1,11 +1,11 @@ from rest_framework import serializers -from api.models import Event, Provider +from api.models import Event class EventSerializer(serializers.ModelSerializer[Event]): - provider: "serializers.StringRelatedField[Provider]" = serializers.StringRelatedField() + provider = serializers.StringRelatedField() - class Meta: + class Meta: # pyrefly: ignore[bad-override] model = Event fields = ["event_id", "provider"] diff --git a/src/api/serializers/launch_serializer.py b/src/api/serializers/launch_serializer.py index c3402f98..69e568fc 100644 --- a/src/api/serializers/launch_serializer.py +++ b/src/api/serializers/launch_serializer.py @@ -1,11 +1,11 @@ from rest_framework import serializers -from api.models import Launch, Provider +from api.models import Launch -class LaunchSerializer(serializers.ModelSerializer[Launch]): - provider: "serializers.StringRelatedField[Provider]" = serializers.StringRelatedField() +class LaunchSerializer(serializers.ModelSerializer): + provider = serializers.StringRelatedField() - class Meta: + class Meta: # pyrefly: ignore[bad-override] model = Launch fields = ["launch_id", "provider"] diff --git a/src/api/serializers/news_site_serializer.py b/src/api/serializers/news_site_serializer.py index b38ff8ce..8f9c2849 100644 --- a/src/api/serializers/news_site_serializer.py +++ b/src/api/serializers/news_site_serializer.py @@ -3,7 +3,7 @@ from api.models import NewsSite -class NewsSiteSerializer(serializers.ModelSerializer[NewsSite]): - class Meta: +class NewsSiteSerializer(serializers.ModelSerializer): + class Meta: # pyrefly: ignore[bad-override] model = NewsSite fields = ["id", "name"] diff --git a/src/api/serializers/report_serializer.py b/src/api/serializers/report_serializer.py index 32c7f2ae..c3567688 100644 --- a/src/api/serializers/report_serializer.py +++ b/src/api/serializers/report_serializer.py @@ -1,14 +1,14 @@ from rest_framework import serializers -from api.models import NewsSite, Report +from api.models import Report from api.serializers.author_serializer import AuthorSerializer class ReportSerializer(serializers.ModelSerializer[Report]): - news_site: "serializers.StringRelatedField[NewsSite]" = serializers.StringRelatedField() + news_site = serializers.StringRelatedField() authors = AuthorSerializer(many=True) - class Meta: + class Meta: # pyrefly: ignore[bad-override] model = Report fields = [ "id", diff --git a/src/api/serializers/socials_serializer.py b/src/api/serializers/socials_serializer.py index fdef894e..1dbc770e 100644 --- a/src/api/serializers/socials_serializer.py +++ b/src/api/serializers/socials_serializer.py @@ -3,7 +3,7 @@ from api.models.socials import Socials -class SocialsSerializer(serializers.ModelSerializer[Socials]): - class Meta: +class SocialsSerializer(serializers.ModelSerializer): + class Meta: # pyrefly: ignore[bad-override] model = Socials fields = ["x", "youtube", "instagram", "linkedin", "mastodon", "bluesky"] diff --git a/src/snapy/settings.py b/src/snapy/settings.py index 59cf982f..f99dffa6 100644 --- a/src/snapy/settings.py +++ b/src/snapy/settings.py @@ -10,16 +10,14 @@ """ from pathlib import Path +from typing import Any -import django_stubs_ext from environs import Env from snapy import __version__ env = Env() env.read_env() -# Extensions for Django Stubs -django_stubs_ext.monkeypatch() VERSION = __version__ @@ -187,7 +185,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -REST_FRAMEWORK = { +REST_FRAMEWORK: dict[str, Any] = { # quick hack because type hint is not correct for the "DEFAULT_THROTTLE_RATES" "DEFAULT_PAGINATION_CLASS": "api.utils.pagination.CustomLimitOffsetPagination", "PAGE_SIZE": 10, "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], diff --git a/uv.lock b/uv.lock index c20ed21e..af637595 100644 --- a/uv.lock +++ b/uv.lock @@ -443,11 +443,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/05/4c9c419b7051eb4b350100b086be6df487f968ab672d3d370f8ccf7c3746/django_stubs-5.2.9-py3-none-any.whl", hash = "sha256:2317a7130afdaa76f6ff7f623650d7f3bf1b6c86a60f95840e14e6ec6de1a7cd", size = 508656, upload-time = "2026-01-20T23:59:25.12Z" }, ] -[package.optional-dependencies] -compatible-mypy = [ - { name = "mypy" }, -] - [[package]] name = "django-stubs-ext" version = "5.2.9" @@ -487,12 +482,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/be/e53e3b89eaa30c21e036ae4d2ee88a92ef8cb43678400901748ddad870c5/djangorestframework_stubs-3.16.9-py3-none-any.whl", hash = "sha256:27b3e245d5f9c22ff6988d9e54388249f98f88608cc2b365b71e9f39dd096958", size = 57239, upload-time = "2026-03-31T22:40:22.314Z" }, ] -[package.optional-dependencies] -compatible-mypy = [ - { name = "django-stubs", extra = ["compatible-mypy"] }, - { name = "mypy" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -1118,6 +1107,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyrefly" +version = "0.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/6e/f9acf0192cfe6ab4428d81a33f7246c280750b4ea0aae68b66331baf2946/pyrefly-0.60.1.tar.gz", hash = "sha256:d2206aa58de1890cc8e3fc7114b45a12dd603fba7cd9e7b635731023528c0450", size = 5514085, upload-time = "2026-04-09T20:03:01.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ff/ba18efaa78c76511787dbd03f01aa86504535ea3e573c6360ffe97c6c241/pyrefly-0.60.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:833b26d18a3ba724ba2b36c101ee379925f5853c613bf51168267a32597e01b6", size = 12924976, upload-time = "2026-04-09T20:02:35.976Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/9b365c70a5d48f2cd0503633a97dac0e481e68dd5382f0170cf6abbe73d2/pyrefly-0.60.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6430e9e37d39f5a98f403fd3ce012ba38956300e2064eef97b086b78496978d", size = 12434281, upload-time = "2026-04-09T20:02:38.726Z" }, + { url = "https://files.pythonhosted.org/packages/34/15/a759693e1ddd8a662805ae03a156ee133408b4c6e3adea403b3a5000385d/pyrefly-0.60.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:992d2d9c90e26279154a6d8177901f5c55dbd2278d91d607223979a499644936", size = 35960986, upload-time = "2026-04-09T20:02:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/36/ad/0d9efc3759456abdaa34981b4d6997a0a70f2e64ce0bae8948179c754b8a/pyrefly-0.60.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b7f24a3146a40e32e1ae1181263a2e909cdef78ba32cccd5fce3949f126020", size = 38681453, upload-time = "2026-04-09T20:02:44.862Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/bed2925d8c779675b4f1f3bfe3eddd580f6fba4d94e7e72b948071b1ccbe/pyrefly-0.60.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37b70aea286db310bae7a01e18bfeedf8c4b5127091e65486a06c46a0f4992d1", size = 36925245, upload-time = "2026-04-09T20:02:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/24/66/bd3ff3ef548249f96b39103c46f85757da2f6cc6bcc432f44d602db2ad5d/pyrefly-0.60.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f075e019176651c3f444735e505619cc586ddd8785382b47f9fe77178dde2ba3", size = 41464771, upload-time = "2026-04-09T20:02:51.664Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9a/58b7808da0943382f29e6845537bbfe00fb7d898784174e3ac20b2011849/pyrefly-0.60.1-py3-none-win32.whl", hash = "sha256:67a48e683ce3b89f6bcf8d13f52e42f1ad3ae3d4b9576e697997f353e2b83e75", size = 11918343, upload-time = "2026-04-09T20:02:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/32/a4/8175c5e80fc8399608850dc79ad9314d066684120f21eb5411a5047f40dc/pyrefly-0.60.1-py3-none-win_amd64.whl", hash = "sha256:6583cb4faf6e496443b25f6453d49d04df70ba0e00d637e64ac691afe83c5496", size = 12755148, upload-time = "2026-04-09T20:02:56.72Z" }, + { url = "https://files.pythonhosted.org/packages/00/37/5294059a7b89548bd60e9527c2a8aaf23d0cbe798bd9a0892c42481e11be/pyrefly-0.60.1-py3-none-win_arm64.whl", hash = "sha256:abbac5ac29614a7b481fffbb056625fefdf2cc2ff17f3a6c52f6b90b4d9da94a", size = 12258856, upload-time = "2026-04-09T20:02:59.088Z" }, +] + [[package]] name = "pysocks" version = "1.7.1" @@ -1480,8 +1486,6 @@ dependencies = [ { name = "django-jet-reboot" }, { name = "django-redis" }, { name = "django-storages", extra = ["s3"] }, - { name = "django-stubs", extra = ["compatible-mypy"] }, - { name = "django-stubs-ext" }, { name = "djangorestframework" }, { name = "drf-spectacular" }, { name = "environs", extra = ["django"] }, @@ -1506,9 +1510,9 @@ dev = [ { name = "coverage" }, { name = "django-debug-toolbar" }, { name = "django-filter-stubs" }, - { name = "djangorestframework-stubs", extra = ["compatible-mypy"] }, - { name = "mypy" }, + { name = "django-stubs" }, { name = "peek-python" }, + { name = "pyrefly" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-django" }, @@ -1526,8 +1530,6 @@ requires-dist = [ { name = "django-jet-reboot", specifier = ">=1.3.9" }, { name = "django-redis", specifier = ">=6.0.0" }, { name = "django-storages", extras = ["s3"], specifier = ">=1.14.4" }, - { name = "django-stubs", extras = ["compatible-mypy"], specifier = "~=5.2.0" }, - { name = "django-stubs-ext", specifier = "~=5.2.0" }, { name = "djangorestframework", specifier = ">=3.15.2" }, { name = "drf-spectacular", specifier = ">=0.27.2" }, { name = "environs", extras = ["django"], specifier = ">=11.0.0" }, @@ -1552,9 +1554,9 @@ dev = [ { name = "coverage", specifier = ">=7.6.1" }, { name = "django-debug-toolbar", specifier = ">=4.4.6" }, { name = "django-filter-stubs", specifier = ">=0.1.3" }, - { name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = ">=3.16" }, - { name = "mypy", specifier = "~=1.13.0" }, + { name = "django-stubs", specifier = ">=5.2.9" }, { name = "peek-python", specifier = ">=25.0.7" }, + { name = "pyrefly", specifier = ">=0.60.1" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-django", specifier = ">=4.9.0" },