From 4d073a328490fb4f3993cdd44db0ec37bde96b3f Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 12 May 2025 19:18:00 +0200 Subject: [PATCH 01/33] Add ty for type checking --- pyproject.toml | 7 +++++ src/backend/InvenTree/InvenTree/settings.py | 2 ++ src/backend/requirements-dev.in | 2 ++ src/backend/requirements-dev.txt | 34 +++++++++++++++++++-- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69176dbccf50..af50633b53a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,13 @@ python-version = "3.9.2" no-strip-extras=true generate-hashes=true +[tool.ty] +src = [ + "src/backend/InvenTree", + "./", +] + + [tool.coverage.run] source = ["src/backend/InvenTree", "InvenTree"] dynamic_context = "test_function" diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 95a338833002..ccaa3c878673 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -20,6 +20,7 @@ from django.core.validators import URLValidator from django.http import Http404, HttpResponseGone +import django_stubs_ext import structlog from corsheaders.defaults import default_headers as default_cors_headers from dotenv import load_dotenv @@ -38,6 +39,7 @@ from . import config, locales +django_stubs_ext.monkeypatch() checkMinPythonVersion() INVENTREE_BASE_URL = 'https://inventree.org' diff --git a/src/backend/requirements-dev.in b/src/backend/requirements-dev.in index 42ddbb679c03..821de3059ea7 100644 --- a/src/backend/requirements-dev.in +++ b/src/backend/requirements-dev.in @@ -9,3 +9,5 @@ pip-tools # Compile pip requirements pre-commit # Git pre-commit setuptools # Standard dependency pdfminer.six # PDF validation +ty # type checking +django-types # typing diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index af9ce3868f57..d54998e4f0ba 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -309,9 +309,13 @@ django-test-migrations==1.4.0 \ --hash=sha256:294dff98f6d43d020d4046b971bac5339e7c71458a35e9ad6450c388fe16ed6b \ --hash=sha256:f0c9c92864ed27d0c9a582e92056637e91227f54bd868a50cb9a1726668c563e # via -r src/backend/requirements-dev.in -filelock==3.17.0 \ - --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ - --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e +django-types==0.20.0 \ + --hash=sha256:4e55d2c56155e3d69d75def9eb1d95a891303f2ac19fccf6fe8056da4293fae7 \ + --hash=sha256:a0b5c2c9a1e591684bb21a93b64e50ca6cb2d3eab48f49faff1eac706bd3a9c7 + # via -r src/backend/requirements-dev.in +filelock==3.18.0 \ + --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ + --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de # via virtualenv identify==2.6.7 \ --hash=sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0 \ @@ -479,6 +483,30 @@ tomli==2.2.1 \ # build # coverage # pip-tools +ty==0.0.0a8 \ + --hash=sha256:0635e72feb984944d7477035b78dcf38618a9f9f01bd5c45204c9d92eef6e1b8 \ + --hash=sha256:0b7522562908da2a2189441cc787646479aceb95ab6296bc805e2d89ef867b43 \ + --hash=sha256:1d537231769bb0ae6c678a696ed0cf93a56cf998ce27a3ffc1cf36b5f02b629b \ + --hash=sha256:1eba8ea7e2d820d304883b6608e464700c5ae3b4fee391800f286e062f8212d4 \ + --hash=sha256:33851d860d9044249f81aec19ac02e7b0bff13f94d713482328927142f20fbaf \ + --hash=sha256:35c5422739cb04198163539b0d7e0cd867b5c19a083499d92b79b0565da1b014 \ + --hash=sha256:3aeedb99ede34bc16081d5540b5e08e96a52fb6bfc2b1e3ce57908ea1013e5ea \ + --hash=sha256:55d69343312b615631152a1a5748bb6b42799f09f1732720e080220ea54ef1f5 \ + --hash=sha256:5f2f4d836c569ed7d50fef5eaf342fd869868341ae6181a31ac7a474e00cdc75 \ + --hash=sha256:8f9bc829ef45ea3ce0427f58fcab7a7c9d00b3f9696066a0f3002db43d7a4e52 \ + --hash=sha256:9420018ea3dae0138d6d5c6a508bc7051a2ec7d3ced69bc2ec6517096bb88e29 \ + --hash=sha256:98174efc5cd8991b64b141eb0531f055460f7f2f37b24c11d84c37b79c5942a6 \ + --hash=sha256:aa7140d67b6a4b2a1cbe2c7536d3af1be16d095cc36da87cdd1986084a80eab9 \ + --hash=sha256:b1a86035380fd85ad7428dd8a1001d03b5bc31ef8453ef75c712b12511f4edaa \ + --hash=sha256:b2725a5bc8924578a9687bebbb840187f9ae2a2ae5583129aac4e037934b2eea \ + --hash=sha256:ea51192cef819b7f9fc84586bccdccbcde5221a2041e736db7cf3bf3611e3336 \ + --hash=sha256:ec7ce7d8e5b274b7496ffe04ac63db7455d6e7afd1a037d584ea98b4fd61c095 \ + --hash=sha256:f00c19020d8c7d5806d54564d4a1fe7e5c6efd5743cefddf619575451491496e + # via -r src/backend/requirements-dev.in +types-psycopg2==2.9.21.20250318 \ + --hash=sha256:7296d111ad950bbd2fc979a1ab0572acae69047f922280e77db657c00d2c79c0 \ + --hash=sha256:eb6eac5bfb16adfd5f16b818918b9e26a40ede147e0f2bbffdf53a6ef7025a87 + # via django-types typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 From 777eb12bbdbdac7614be3395deedb5a1d2ed9b8a Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 13 May 2025 11:55:24 +0200 Subject: [PATCH 02/33] fix various typing issues --- docs/docs/hooks.py | 2 +- docs/main.py | 2 +- src/backend/InvenTree/InvenTree/api.py | 1 + src/backend/InvenTree/InvenTree/config.py | 2 +- src/backend/InvenTree/InvenTree/format.py | 2 +- src/backend/InvenTree/InvenTree/helpers.py | 2 ++ .../InvenTree/InvenTree/helpers_email.py | 2 +- .../InvenTree/InvenTree/helpers_model.py | 1 + .../InvenTree/management/commands/schema.py | 2 +- .../management/commands/wait_for_db.py | 3 ++- src/backend/InvenTree/InvenTree/sanitizer.py | 4 ++-- src/backend/InvenTree/InvenTree/settings.py | 4 +++- src/backend/InvenTree/InvenTree/tasks.py | 4 +++- src/backend/InvenTree/InvenTree/tracing.py | 2 +- src/backend/InvenTree/InvenTree/validators.py | 1 + src/backend/InvenTree/build/api.py | 1 + src/backend/InvenTree/build/serializers.py | 1 + src/backend/InvenTree/common/api.py | 1 + src/backend/InvenTree/common/models.py | 5 ++-- src/backend/InvenTree/common/tests.py | 4 ++-- src/backend/InvenTree/data_exporter/apps.py | 2 +- .../InvenTree/generic/states/__init__.py | 2 ++ .../InvenTree/generic/states/states.py | 2 +- src/backend/InvenTree/importer/operations.py | 3 ++- src/backend/InvenTree/part/stocktake.py | 1 + src/backend/InvenTree/part/test_api.py | 8 +++---- .../InvenTree/plugin/base/barcodes/mixins.py | 2 +- .../plugin/base/integration/DataExport.py | 6 ++--- .../base/integration/ValidationMixin.py | 24 +++++++++---------- .../plugin/builtin/exporter/bom_exporter.py | 4 +++- src/backend/InvenTree/plugin/models.py | 4 ++-- src/backend/InvenTree/plugin/plugin.py | 6 ++--- src/backend/InvenTree/plugin/test_plugin.py | 2 +- src/backend/InvenTree/report/apps.py | 2 +- .../InvenTree/report/templatetags/barcode.py | 2 +- .../InvenTree/report/templatetags/report.py | 4 ++-- src/backend/InvenTree/stock/generators.py | 2 +- src/backend/InvenTree/stock/models.py | 2 +- src/backend/InvenTree/stock/serializers.py | 1 + src/backend/InvenTree/users/models.py | 2 +- .../InvenTree/web/templatetags/__init__.py | 0 .../InvenTree/web/templatetags/spa_helper.py | 1 + tasks.py | 24 ++++++++++++------- 43 files changed, 90 insertions(+), 62 deletions(-) create mode 100644 src/backend/InvenTree/web/templatetags/__init__.py diff --git a/docs/docs/hooks.py b/docs/docs/hooks.py index fe3f7096cf0d..090c54e51763 100644 --- a/docs/docs/hooks.py +++ b/docs/docs/hooks.py @@ -4,7 +4,7 @@ import os import re from datetime import datetime -from distutils.version import StrictVersion +from distutils.version import StrictVersion # type: ignore[import] from pathlib import Path import requests diff --git a/docs/main.py b/docs/main.py index 25b435c1919a..3a706ec2cb94 100644 --- a/docs/main.py +++ b/docs/main.py @@ -110,7 +110,7 @@ def check_link(url) -> bool: return False -def get_build_environment() -> str: +def get_build_environment() -> str | None: """Returns the branch we are currently building on, based on the environment variables of the various CI platforms.""" # Check if we are in ReadTheDocs if os.environ.get('READTHEDOCS') == 'True': diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index e5535e26f53c..0cc2a51611d2 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -17,6 +17,7 @@ from rest_framework.serializers import ValidationError from rest_framework.views import APIView +import InvenTree.permissions import InvenTree.version from common.settings import get_global_setting from InvenTree import helpers diff --git a/src/backend/InvenTree/InvenTree/config.py b/src/backend/InvenTree/InvenTree/config.py index 02326f0e90bd..f82df57c2585 100644 --- a/src/backend/InvenTree/InvenTree/config.py +++ b/src/backend/InvenTree/InvenTree/config.py @@ -113,7 +113,7 @@ def get_config_file(create=True) -> Path: return cfg_filename -def load_config_data(set_cache: bool = False) -> map: +def load_config_data(set_cache: bool = False) -> map | None: """Load configuration data from the config file. Arguments: diff --git a/src/backend/InvenTree/InvenTree/format.py b/src/backend/InvenTree/InvenTree/format.py index 5fc8c546bb74..adf3f241e406 100644 --- a/src/backend/InvenTree/InvenTree/format.py +++ b/src/backend/InvenTree/InvenTree/format.py @@ -124,7 +124,7 @@ def construct_format_regex(fmt_string: str) -> str: return pattern -def validate_string(value: str, fmt_string: str) -> str: +def validate_string(value: str, fmt_string: str) -> bool: """Validate that the provided string matches the specified format. Args: diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index ee6ffe642c1f..f59f24a1cbed 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -21,6 +21,8 @@ from django.utils.translation import gettext_lazy as _ import bleach +import bleach.css_sanitizer +import bleach.sanitizer import structlog from bleach import clean from djmoney.money import Money diff --git a/src/backend/InvenTree/InvenTree/helpers_email.py b/src/backend/InvenTree/InvenTree/helpers_email.py index e8fb6b6476dc..5ea9aba155fc 100644 --- a/src/backend/InvenTree/InvenTree/helpers_email.py +++ b/src/backend/InvenTree/InvenTree/helpers_email.py @@ -91,7 +91,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None): ) -def get_email_for_user(user) -> str: +def get_email_for_user(user) -> str | None: """Find an email address for the specified user.""" # First check if the user has an associated email address if user.email: diff --git a/src/backend/InvenTree/InvenTree/helpers_model.py b/src/backend/InvenTree/InvenTree/helpers_model.py index 23a7c71f1c12..b636c85ba163 100644 --- a/src/backend/InvenTree/InvenTree/helpers_model.py +++ b/src/backend/InvenTree/InvenTree/helpers_model.py @@ -11,6 +11,7 @@ from django.utils.translation import gettext_lazy as _ import requests +import requests.exceptions import structlog from djmoney.contrib.exchange.models import convert_money from djmoney.money import Money diff --git a/src/backend/InvenTree/InvenTree/management/commands/schema.py b/src/backend/InvenTree/InvenTree/management/commands/schema.py index ae07c8b41ed7..badf5702adf5 100644 --- a/src/backend/InvenTree/InvenTree/management/commands/schema.py +++ b/src/backend/InvenTree/InvenTree/management/commands/schema.py @@ -26,7 +26,7 @@ def prep_name(ref): return f'{dja_ref_prefix}.{ref}' -def sub_component_name(name: T) -> T: +def sub_component_name(name: T) -> T | str: """Clean up component references.""" if not isinstance(name, str): return name diff --git a/src/backend/InvenTree/InvenTree/management/commands/wait_for_db.py b/src/backend/InvenTree/InvenTree/management/commands/wait_for_db.py index 6bfdc98b5828..68b33adfff9f 100644 --- a/src/backend/InvenTree/InvenTree/management/commands/wait_for_db.py +++ b/src/backend/InvenTree/InvenTree/management/commands/wait_for_db.py @@ -2,9 +2,10 @@ import time +from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand from django.db import connection -from django.db.utils import ImproperlyConfigured, OperationalError +from django.db.utils import OperationalError class Command(BaseCommand): diff --git a/src/backend/InvenTree/InvenTree/sanitizer.py b/src/backend/InvenTree/InvenTree/sanitizer.py index 0272b50f943c..ce093b96fec5 100644 --- a/src/backend/InvenTree/InvenTree/sanitizer.py +++ b/src/backend/InvenTree/InvenTree/sanitizer.py @@ -188,8 +188,8 @@ def sanitize_svg( file_data, strip: bool = True, - elements: str = ALLOWED_ELEMENTS_SVG, - attributes: str = ALLOWED_ATTRIBUTES_SVG, + elements: list[str] = ALLOWED_ELEMENTS_SVG, + attributes: list[str] = ALLOWED_ATTRIBUTES_SVG, ) -> str: """Sanitize a SVG file. diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index ccaa3c878673..48d9480ed7ec 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -678,7 +678,9 @@ # Specific options for postgres backend if 'postgres' in db_engine: # pragma: no cover - from django.db.backends.postgresql.psycopg_any import IsolationLevel + from django.db.backends.postgresql.psycopg_any import ( # type: ignore[unresolved-import] + IsolationLevel, + ) # Connection timeout if 'connect_timeout' not in db_options: diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index 5c39b8e08359..6492a8d19f3b 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -305,7 +305,9 @@ def register(self, task, schedule, minutes: Optional[int] = None): def scheduled_task( - interval: str, minutes: Optional[int] = None, tasklist: TaskRegister = None + interval: str, + minutes: Optional[int] = None, + tasklist: Optional[TaskRegister] = None, ): """Register the given task as a scheduled task. diff --git a/src/backend/InvenTree/InvenTree/tracing.py b/src/backend/InvenTree/InvenTree/tracing.py index a04a71c9e2c0..44151d470c85 100644 --- a/src/backend/InvenTree/InvenTree/tracing.py +++ b/src/backend/InvenTree/InvenTree/tracing.py @@ -4,7 +4,7 @@ import logging from typing import Optional -from opentelemetry import metrics, trace +from opentelemetry import metrics, trace # type: ignore[import] from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor diff --git a/src/backend/InvenTree/InvenTree/validators.py b/src/backend/InvenTree/InvenTree/validators.py index 6a0fe6cd9d4e..3c85483c4bfa 100644 --- a/src/backend/InvenTree/InvenTree/validators.py +++ b/src/backend/InvenTree/InvenTree/validators.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ import pint +import pint.errors from moneyed import CURRENCIES import InvenTree.conversion diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 1b017d73dcc2..e62dd926560a 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -13,6 +13,7 @@ from rest_framework.exceptions import ValidationError import build.admin +import build.models import build.serializers import common.models import part.models as part_models diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index aaef6bae8182..2f83f7167ae7 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -23,6 +23,7 @@ import build.tasks import common.models +import common.settings import company.serializers import InvenTree.helpers import InvenTree.tasks diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 6d5027864963..001b69c733d9 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -1,6 +1,7 @@ """Provides a JSON API for common components.""" import json +import json.decoder from django.conf import settings from django.contrib.contenttypes.models import ContentType diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index d498a2114397..e10332e8647a 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -27,6 +27,7 @@ from django.core.files.storage import default_storage from django.core.validators import MinValueValidator from django.db import models, transaction +from django.db.models import enums from django.db.models.signals import post_delete, post_save from django.db.utils import IntegrityError, OperationalError, ProgrammingError from django.dispatch.dispatcher import receiver @@ -55,7 +56,7 @@ logger = structlog.get_logger('inventree') -class RenderMeta(models.enums.ChoicesMeta): +class RenderMeta(enums.ChoicesMeta): """Metaclass for rendering choices.""" choice_fnc = None @@ -916,7 +917,7 @@ def model_name(self) -> str: return setting.get('model', None) - def model_filters(self) -> dict: + def model_filters(self) -> dict | None: """Return the model filters associated with this setting.""" setting = self.get_setting_definition( self.key, **self.get_filters_for_instance() diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index dd2d89a0a9b3..b69d56b73a12 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -18,7 +18,7 @@ from django.test.utils import override_settings from django.urls import reverse -import PIL +from PIL import Image import common.validators from common.settings import get_global_setting, set_global_setting @@ -1298,7 +1298,7 @@ def test_valid_image(self): n = NotesImage.objects.count() # Construct a simple image file - image = PIL.Image.new('RGB', (100, 100), color='red') + image = Image.new('RGB', (100, 100), color='red') with io.BytesIO() as output: image.save(output, format='PNG') diff --git a/src/backend/InvenTree/data_exporter/apps.py b/src/backend/InvenTree/data_exporter/apps.py index a9eb5d8bbd9d..9cef065b15ae 100644 --- a/src/backend/InvenTree/data_exporter/apps.py +++ b/src/backend/InvenTree/data_exporter/apps.py @@ -18,7 +18,7 @@ def ready(self): def cleanup(self): """Cleanup any old export files.""" try: - from data_exporter.tasks import cleanup_old_export_outputs + from data_exporter.tasks import cleanup_old_export_outputs # type: ignore cleanup_old_export_outputs() except Exception: diff --git a/src/backend/InvenTree/generic/states/__init__.py b/src/backend/InvenTree/generic/states/__init__.py index 90922e9695ba..189150f56113 100644 --- a/src/backend/InvenTree/generic/states/__init__.py +++ b/src/backend/InvenTree/generic/states/__init__.py @@ -6,6 +6,7 @@ States can be extended with custom options for each InvenTree instance - those options are stored in the database and need to link back to state values. """ +from . import fields from .states import ColorEnum, StatusCode, StatusCodeMixin from .transition import StateTransitionMixin, TransitionMethod, storage @@ -15,5 +16,6 @@ 'StatusCode', 'StatusCodeMixin', 'TransitionMethod', + 'fields', 'storage', ] diff --git a/src/backend/InvenTree/generic/states/states.py b/src/backend/InvenTree/generic/states/states.py index 3304cc3e5c81..b40ac9c55110 100644 --- a/src/backend/InvenTree/generic/states/states.py +++ b/src/backend/InvenTree/generic/states/states.py @@ -297,7 +297,7 @@ def get_status(self) -> int: """Return the status code for this object.""" return getattr(self, self.STATUS_FIELD) - def get_custom_status(self) -> int: + def get_custom_status(self) -> int | None: """Return the custom status code for this object.""" return getattr(self, f'{self.STATUS_FIELD}_custom_key', None) diff --git a/src/backend/InvenTree/importer/operations.py b/src/backend/InvenTree/importer/operations.py index dcb79e8baad7..25a1530b57b5 100644 --- a/src/backend/InvenTree/importer/operations.py +++ b/src/backend/InvenTree/importer/operations.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ import tablib +import tablib.core import InvenTree.helpers @@ -82,7 +83,7 @@ def extract_column_names(data_file) -> list: return headers -def get_field_label(field) -> str: +def get_field_label(field) -> str | None: """Return the label for a field in a serializer class. Check for labels in the following order of descending priority: diff --git a/src/backend/InvenTree/part/stocktake.py b/src/backend/InvenTree/part/stocktake.py index 4878265f8fe9..97f307cdc585 100644 --- a/src/backend/InvenTree/part/stocktake.py +++ b/src/backend/InvenTree/part/stocktake.py @@ -14,6 +14,7 @@ import common.currency import common.models +import common.notifications import InvenTree.helpers import part.models import stock.models diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 4d8af0584645..d4b22940c12e 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -11,7 +11,7 @@ from django.test.utils import CaptureQueriesContext from django.urls import reverse -import PIL +from PIL import Image from rest_framework.test import APIClient import build.models @@ -66,7 +66,7 @@ def create_test_image(self): fn = BASE_DIR / '_testfolder' / 'part_image_123abc.png' - img = PIL.Image.new('RGB', (128, 128), color='blue') + img = Image.new('RGB', (128, 128), color='blue') img.save(fn) with open(fn, 'rb') as img_file: @@ -1694,7 +1694,7 @@ def test_image_upload(self): for fmt in ['jpg', 'j2k', 'png', 'bmp', 'webp']: fn = f'{test_path}.{fmt}' - img = PIL.Image.new('RGB', (128, 128), color='red') + img = Image.new('RGB', (128, 128), color='red') img.save(fn) with open(fn, 'rb') as dummy_image: @@ -1744,7 +1744,7 @@ def test_update_existing_image(self): fn = BASE_DIR / '_testfolder' / 'part_image_123abc.png' - img = PIL.Image.new('RGB', (128, 128), color='blue') + img = Image.new('RGB', (128, 128), color='blue') img.save(fn) # Upload the image to a part diff --git a/src/backend/InvenTree/plugin/base/barcodes/mixins.py b/src/backend/InvenTree/plugin/base/barcodes/mixins.py index 848f55258544..13cbbbe04460 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/mixins.py +++ b/src/backend/InvenTree/plugin/base/barcodes/mixins.py @@ -260,7 +260,7 @@ def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]: 'extract_barcode_fields must be implemented by each plugin' ) - def scan(self, barcode_data: str) -> dict: + def scan(self, barcode_data: str) -> dict | None: """Perform a generic 'scan' operation on a supplier barcode. The supplier barcode may provide sufficient information to match against diff --git a/src/backend/InvenTree/plugin/base/integration/DataExport.py b/src/backend/InvenTree/plugin/base/integration/DataExport.py index 6d1eadb6e185..603f5d234d75 100644 --- a/src/backend/InvenTree/plugin/base/integration/DataExport.py +++ b/src/backend/InvenTree/plugin/base/integration/DataExport.py @@ -1,7 +1,7 @@ """Plugin class for custom data exporting.""" from collections import OrderedDict -from typing import Union +from typing import Optional, Union from django.contrib.auth.models import User from django.db.models import QuerySet @@ -36,8 +36,8 @@ def supports_export( self, model_class: type, user: User, - serializer_class: serializers.Serializer = None, - view_class: views.APIView = None, + serializer_class: Optional[serializers.Serializer] = None, + view_class: Optional[views.APIView] = None, *args, **kwargs, ) -> bool: diff --git a/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py b/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py index 341696bf6258..dd30704ee895 100644 --- a/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py @@ -72,7 +72,7 @@ def validate_model_deletion(self, instance: Model) -> None: def validate_model_instance( self, instance: Model, deltas: Optional[dict] = None - ) -> None: + ) -> bool | None: """Run custom validation on a database model instance. This method is called when a model instance is being validated. @@ -90,7 +90,7 @@ def validate_model_instance( """ return None - def validate_part_name(self, name: str, part: part.models.Part) -> None: + def validate_part_name(self, name: str, part: part.models.Part) -> bool | None: """Perform validation on a proposed Part name. Arguments: @@ -105,7 +105,7 @@ def validate_part_name(self, name: str, part: part.models.Part) -> None: """ return None - def validate_part_ipn(self, ipn: str, part: part.models.Part) -> None: + def validate_part_ipn(self, ipn: str, part: part.models.Part) -> bool | None: """Perform validation on a proposed Part IPN (internal part number). Arguments: @@ -122,7 +122,7 @@ def validate_part_ipn(self, ipn: str, part: part.models.Part) -> None: def validate_batch_code( self, batch_code: str, item: stock.models.StockItem - ) -> None: + ) -> bool | None: """Validate the supplied batch code. Arguments: @@ -137,7 +137,7 @@ def validate_batch_code( """ return None - def generate_batch_code(self, **kwargs) -> str: + def generate_batch_code(self, **kwargs) -> str | None: """Generate a new batch code. This method is called when a new batch code is required. @@ -154,8 +154,8 @@ def validate_serial_number( self, serial: str, part: part.models.Part, - stock_item: stock.models.StockItem = None, - ) -> None: + stock_item: Optional[stock.models.StockItem] = None, + ) -> bool | None: """Validate the supplied serial number. Arguments: @@ -171,7 +171,7 @@ def validate_serial_number( """ return None - def convert_serial_to_int(self, serial: str) -> int: + def convert_serial_to_int(self, serial: str) -> int | None: """Convert a serial number (string) into an integer representation. This integer value is used for efficient sorting based on serial numbers. @@ -192,7 +192,7 @@ def convert_serial_to_int(self, serial: str) -> int: """ return None - def get_latest_serial_number(self, part, **kwargs): + def get_latest_serial_number(self, part, **kwargs) -> str | None: """Return the 'latest' serial number for a given Part instance. A plugin which implements this method can either return: @@ -209,8 +209,8 @@ def get_latest_serial_number(self, part, **kwargs): return None def increment_serial_number( - self, serial: str, part: part.models.Part = None, **kwargs - ) -> str: + self, serial: str, part: Optional[part.models.Part] = None, **kwargs + ) -> str | None: """Return the next sequential serial based on the provided value. A plugin which implements this method can either return: @@ -229,7 +229,7 @@ def increment_serial_number( def validate_part_parameter( self, parameter: part.models.PartParameter, data: str - ) -> None: + ) -> bool | None: """Validate a parameter value. Arguments: diff --git a/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py b/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py index d5fbc385bdc4..5a4f906c57b1 100644 --- a/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py +++ b/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py @@ -1,5 +1,7 @@ """Multi-level BOM exporter plugin.""" +from typing import Optional + from django.utils.translation import gettext_lazy as _ import rest_framework.serializers as serializers @@ -179,7 +181,7 @@ def export_data( return self.bom_data - def process_bom_row(self, bom_item, level, **kwargs) -> list: + def process_bom_row(self, bom_item, level, **kwargs) -> Optional[list]: """Process a single BOM row. Arguments: diff --git a/src/backend/InvenTree/plugin/models.py b/src/backend/InvenTree/plugin/models.py index c5947c24f519..f60056a47dac 100644 --- a/src/backend/InvenTree/plugin/models.py +++ b/src/backend/InvenTree/plugin/models.py @@ -195,7 +195,7 @@ def is_package(self) -> bool: return getattr(self.plugin, 'is_package', False) @property - def admin_source(self) -> str: + def admin_source(self) -> str | None: """Return the path to the javascript file which renders custom admin content for this plugin. - It is required that the file provides a 'renderPluginSettings' function! @@ -215,7 +215,7 @@ def admin_source(self) -> str: return None @property - def admin_context(self) -> dict: + def admin_context(self) -> dict | None: """Return the context data for the admin integration.""" if not self.plugin: return None diff --git a/src/backend/InvenTree/plugin/plugin.py b/src/backend/InvenTree/plugin/plugin.py index 2ba7de00b962..93ace6be9c6d 100644 --- a/src/backend/InvenTree/plugin/plugin.py +++ b/src/backend/InvenTree/plugin/plugin.py @@ -4,7 +4,7 @@ import inspect import warnings from datetime import datetime -from distutils.sysconfig import get_python_lib +from distutils.sysconfig import get_python_lib # type: ignore[import] from importlib.metadata import PackageNotFoundError, metadata from pathlib import Path from typing import Optional, Union @@ -489,7 +489,7 @@ def plugin_static_file(self, *args): return url - def get_admin_source(self) -> str: + def get_admin_source(self) -> str | None: """Return a path to a JavaScript file which contains custom UI settings. The frontend code expects that this file provides a function named 'renderPluginSettings'. @@ -499,7 +499,7 @@ def get_admin_source(self) -> str: return self.plugin_static_file(self.ADMIN_SOURCE) - def get_admin_context(self) -> dict: + def get_admin_context(self) -> dict | None: """Return a context dictionary for the admin panel settings. This is an optional method which can be overridden by the plugin. diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index e7eb2aea2f5d..dea5434b68c8 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -337,7 +337,7 @@ class DummyCIPlugin(InvenTreePlugin): def create_plugin_file( version: str, enabled: bool = True, reload: bool = True - ) -> str: + ) -> str | None: """Create a plugin file with the given version. Arguments: diff --git a/src/backend/InvenTree/report/apps.py b/src/backend/InvenTree/report/apps.py index cf7d679ffd92..c227a6e50935 100644 --- a/src/backend/InvenTree/report/apps.py +++ b/src/backend/InvenTree/report/apps.py @@ -67,7 +67,7 @@ def ready(self): def cleanup(self): """Cleanup old label and report outputs.""" try: - from report.tasks import cleanup_old_report_outputs + from report.tasks import cleanup_old_report_outputs # type: ignore[import] cleanup_old_report_outputs() except Exception: diff --git a/src/backend/InvenTree/report/templatetags/barcode.py b/src/backend/InvenTree/report/templatetags/barcode.py index 75237bbd7a56..f02c3e8bd2bf 100644 --- a/src/backend/InvenTree/report/templatetags/barcode.py +++ b/src/backend/InvenTree/report/templatetags/barcode.py @@ -148,7 +148,7 @@ def datamatrix(data: str, **kwargs) -> str: Returns: image (str): base64 encoded image data """ - from ppf.datamatrix import DataMatrix + from ppf.datamatrix.datamatrix import DataMatrix data = str(data).strip() diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index ed6b5c3da801..29d40d611c92 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -50,7 +50,7 @@ def filter_queryset(queryset: QuerySet, **kwargs) -> QuerySet: @register.simple_tag() -def filter_db_model(model_name: str, **kwargs) -> QuerySet: +def filter_db_model(model_name: str, **kwargs) -> QuerySet | None: """Filter a database model based on the provided keyword arguments. Arguments: @@ -316,7 +316,7 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa @register.simple_tag() -def part_parameter(part: Part, parameter_name: str) -> str: +def part_parameter(part: Part, parameter_name: str) -> str | None: """Return a PartParameter object for the given part and parameter name. Arguments: diff --git a/src/backend/InvenTree/stock/generators.py b/src/backend/InvenTree/stock/generators.py index 7b11dc7db8c6..b06682670a27 100644 --- a/src/backend/InvenTree/stock/generators.py +++ b/src/backend/InvenTree/stock/generators.py @@ -74,7 +74,7 @@ def generate_batch_code(**kwargs): return Template(batch_template).render(context) -def generate_serial_number(part=None, quantity=1, **kwargs) -> str: +def generate_serial_number(part=None, quantity=1, **kwargs) -> str | None: """Generate a default 'serial number' for a new StockItem.""" quantity = quantity or 1 diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 168a684eaacf..530c727c2e05 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -593,7 +593,7 @@ def _create_serial_numbers(cls, serials: list, **kwargs) -> QuerySet: return StockItem.objects.filter(part=part, serial__in=serials) @staticmethod - def convert_serial_to_int(serial: str) -> int: + def convert_serial_to_int(serial: str) -> int | None: """Convert the provided serial number to an integer value. This function hooks into the plugin system to allow for custom serial number conversion. diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 2c6b83839425..5607a9ee1362 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -20,6 +20,7 @@ import company.models import company.serializers as company_serializers import InvenTree.helpers +import InvenTree.ready import InvenTree.serializers import order.models import part.filters as part_filters diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index cc39ba9ec35b..9cde4395e486 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -224,7 +224,7 @@ class Meta: unique_together = (('name', 'group'),) @property - def label(self) -> str: + def label(self) -> str | None: """Return the translated label for this ruleset.""" return dict(RULESET_CHOICES).get(self.name, self.name) diff --git a/src/backend/InvenTree/web/templatetags/__init__.py b/src/backend/InvenTree/web/templatetags/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/backend/InvenTree/web/templatetags/spa_helper.py b/src/backend/InvenTree/web/templatetags/spa_helper.py index 743d1ef17bd1..0355ca906046 100644 --- a/src/backend/InvenTree/web/templatetags/spa_helper.py +++ b/src/backend/InvenTree/web/templatetags/spa_helper.py @@ -1,6 +1,7 @@ """Template tag to render SPA imports.""" import json +import json.decoder from pathlib import Path from typing import Union diff --git a/tasks.py b/tasks.py index 84c0c021e59f..929e235e393d 100644 --- a/tasks.py +++ b/tasks.py @@ -19,21 +19,21 @@ def is_docker_environment(): """Check if the InvenTree environment is running in a Docker container.""" - from src.backend.InvenTree.InvenTree.config import is_true + from src.backend.InvenTree.InvenTree.config import is_true # type: ignore[import] return is_true(os.environ.get('INVENTREE_DOCKER', 'False')) def is_rtd_environment(): """Check if the InvenTree environment is running on ReadTheDocs.""" - from src.backend.InvenTree.InvenTree.config import is_true + from src.backend.InvenTree.InvenTree.config import is_true # type: ignore[import] return is_true(os.environ.get('READTHEDOCS', 'False')) def is_debug_environment(): """Check if the InvenTree environment is running in a debug environment.""" - from src.backend.InvenTree.InvenTree.config import is_true + from src.backend.InvenTree.InvenTree.config import is_true # type: ignore[import] return is_true(os.environ.get('INVENTREE_DEBUG', 'False')) or is_true( os.environ.get('RUNNER_DEBUG', 'False') @@ -384,7 +384,9 @@ def check_file_existence(filename: Path, overwrite: bool = False): @state_logger('TASK01') def plugins(c, uv=False): """Installs all plugins as specified in 'plugins.txt'.""" - from src.backend.InvenTree.InvenTree.config import get_plugin_file + from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] + get_plugin_file, + ) plugin_file = get_plugin_file() @@ -488,7 +490,9 @@ def rebuild_models(c): @task def rebuild_thumbnails(c): """Rebuild missing image thumbnails.""" - from src.backend.InvenTree.InvenTree.config import get_media_dir + from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] + get_media_dir, + ) info(f'Rebuilding image thumbnails in {get_media_dir()}') manage(c, 'rebuild_thumbnails', pty=True) @@ -1069,7 +1073,7 @@ def test_translations(c): info('Fill in dummy translations...') file_path = pathlib.Path(settings.LOCALE_PATHS[0], 'xx', 'LC_MESSAGES', 'django.po') - new_file_path = str(file_path) + '_new' + new_file_path = Path(str(file_path) + '_new') # compile regex reg = re.compile( @@ -1205,7 +1209,9 @@ def setup_test( path='inventree-demo-dataset', ): """Setup a testing environment.""" - from src.backend.InvenTree.InvenTree.config import get_media_dir + from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] + get_media_dir, + ) if not ignore_update: update(c) @@ -1352,8 +1358,8 @@ def export_definitions(c, basedir: str = ''): @task(default=True) def version(c): """Show the current version of InvenTree.""" - import src.backend.InvenTree.InvenTree.version as InvenTreeVersion - from src.backend.InvenTree.InvenTree.config import ( + import src.backend.InvenTree.InvenTree.version as InvenTreeVersion # type: ignore[import] + from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] get_backup_dir, get_config_file, get_media_dir, From d8d27c2b3e69ef010778e4547f334025b94e7928 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 13 May 2025 11:59:04 +0200 Subject: [PATCH 03/33] fix req --- src/backend/requirements-dev.in | 1 + src/backend/requirements-dev.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/backend/requirements-dev.in b/src/backend/requirements-dev.in index 821de3059ea7..d9782f1a19d8 100644 --- a/src/backend/requirements-dev.in +++ b/src/backend/requirements-dev.in @@ -11,3 +11,4 @@ setuptools # Standard dependency pdfminer.six # PDF validation ty # type checking django-types # typing +django-stubs # typing diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index d54998e4f0ba..6a1987966284 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -6,6 +6,7 @@ asgiref==3.8.1 \ # via # -c src/backend/requirements.txt # django + # django-stubs build==1.2.2.post1 \ --hash=sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5 \ --hash=sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7 @@ -299,12 +300,22 @@ django==4.2.20 \ # via # -c src/backend/requirements.txt # django-slowtests + # django-stubs + # django-stubs-ext django-querycount==0.8.3 \ --hash=sha256:0782484e8a1bd29498fa0195a67106e47cdcc98fafe80cebb1991964077cb694 # via -r src/backend/requirements-dev.in django-slowtests==1.1.1 \ --hash=sha256:3c6936d420c9df444ac03625b41d97de043c662bbde61fbcd33e4cd407d0c247 # via -r src/backend/requirements-dev.in +django-stubs==5.1.3 \ + --hash=sha256:716758ced158b439213062e52de6df3cff7c586f9f9ad7ab59210efbea5dfe78 \ + --hash=sha256:8c230bc5bebee6da282ba8a27ad1503c84a0c4cd2f46e63d149e76d2a63e639a + # via -r src/backend/requirements-dev.in +django-stubs-ext==5.1.3 \ + --hash=sha256:3e60f82337f0d40a362f349bf15539144b96e4ceb4dbd0239be1cd71f6a74ad0 \ + --hash=sha256:64561fbc53e963cc1eed2c8eb27e18b8e48dcb90771205180fe29fc8a59e55fd + # via django-stubs django-test-migrations==1.4.0 \ --hash=sha256:294dff98f6d43d020d4046b971bac5339e7c71458a35e9ad6450c388fe16ed6b \ --hash=sha256:f0c9c92864ed27d0c9a582e92056637e91227f54bd868a50cb9a1726668c563e @@ -482,6 +493,7 @@ tomli==2.2.1 \ # -c src/backend/requirements.txt # build # coverage + # django-stubs # pip-tools ty==0.0.0a8 \ --hash=sha256:0635e72feb984944d7477035b78dcf38618a9f9f01bd5c45204c9d92eef6e1b8 \ @@ -507,12 +519,18 @@ types-psycopg2==2.9.21.20250318 \ --hash=sha256:7296d111ad950bbd2fc979a1ab0572acae69047f922280e77db657c00d2c79c0 \ --hash=sha256:eb6eac5bfb16adfd5f16b818918b9e26a40ede147e0f2bbffdf53a6ef7025a87 # via django-types +types-pyyaml==6.0.12.20250402 \ + --hash=sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681 \ + --hash=sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075 + # via django-stubs typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via # -c src/backend/requirements.txt # asgiref + # django-stubs + # django-stubs-ext # django-test-migrations virtualenv==20.29.1 \ --hash=sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779 \ From 7a82efb3d81444940c57157e16c901eaaca45a0b Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 15 May 2025 00:13:49 +0200 Subject: [PATCH 04/33] more fixes --- src/backend/InvenTree/InvenTree/helpers.py | 2 +- src/backend/InvenTree/InvenTree/models.py | 2 +- src/backend/InvenTree/InvenTree/permissions.py | 16 +++++++++++----- src/backend/InvenTree/common/notifications.py | 10 +++++----- .../InvenTree/common/test_notifications.py | 8 ++++---- .../migrations/0019_auto_20200413_0642.py | 2 +- src/backend/InvenTree/plugin/plugin.py | 5 +++-- src/backend/InvenTree/plugin/registry.py | 2 +- src/backend/InvenTree/plugin/test_plugin.py | 4 ++-- 9 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index f59f24a1cbed..2abd8dcc771b 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -125,7 +125,7 @@ def do_clip(value: int, clip: int, allow_negative: bool) -> int: return ref_int -def generateTestKey(test_name: str) -> str: +def generateTestKey(test_name: str | None) -> str: """Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template. Tests must be named such that they will have unique keys. diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index 24d6ea649fd9..0abcbce0b1d0 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -994,7 +994,7 @@ def assign_barcode( raise ValueError("Provide either 'barcode_hash' or 'barcode_data'") # If barcode_hash is not provided, create from supplier barcode_data - if barcode_hash is None: + if barcode_hash is None and barcode_data is not None: barcode_hash = InvenTree.helpers.hash_barcode(barcode_data) # Check for existing item diff --git a/src/backend/InvenTree/InvenTree/permissions.py b/src/backend/InvenTree/InvenTree/permissions.py index c78a24f38443..059bbd9ccb9b 100644 --- a/src/backend/InvenTree/InvenTree/permissions.py +++ b/src/backend/InvenTree/InvenTree/permissions.py @@ -249,7 +249,9 @@ def has_permission(self, request, view): def get_required_alternate_scopes(self, request, view): """Return the required scopes for the current request.""" scopes = map_scope( - only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS + only_read=True, + read_name=DEFAULT_STAFF, + map_read=list(permissions.SAFE_METHODS), ) return scopes @@ -287,7 +289,7 @@ def get_required_alternate_scopes(self, request, view): return map_scope( only_read=True, read_name=DEFAULT_SUPERUSER, - map_read=permissions.SAFE_METHODS, + map_read=list(permissions.SAFE_METHODS), ) @@ -312,7 +314,9 @@ def has_permission(self, request, view): def get_required_alternate_scopes(self, request, view): """Return the required scopes for the current request.""" return map_scope( - only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS + only_read=True, + read_name=DEFAULT_STAFF, + map_read=list(permissions.SAFE_METHODS), ) @@ -342,7 +346,7 @@ def auth_exempt(view_func): def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) - wrapped_view.auth_exempt = True + wrapped_view.auth_exempt = True # type:ignore[unresolved-attribute] return wraps(view_func)(wrapped_view) @@ -382,5 +386,7 @@ def has_permission(self, request, view): def get_required_alternate_scopes(self, request, view): """Return the required scopes for the current request.""" return map_scope( - only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS + only_read=True, + read_name=DEFAULT_STAFF, + map_read=list(permissions.SAFE_METHODS), ) diff --git a/src/backend/InvenTree/common/notifications.py b/src/backend/InvenTree/common/notifications.py index 26f1935de8b5..762699f25db9 100644 --- a/src/backend/InvenTree/common/notifications.py +++ b/src/backend/InvenTree/common/notifications.py @@ -33,7 +33,9 @@ class NotificationMethod: GLOBAL_SETTING = None USER_SETTING = None - def __init__(self, obj: Model, category: str, targets: list, context) -> None: + def __init__( + self, obj: Model, category: str, targets: Optional[list], context + ) -> None: """Check that the method is read. This checks that: @@ -185,7 +187,7 @@ class MethodStorageClass: Is initialized on startup as one instance named `storage` in this file. """ - methods_list = None + methods_list: list = [] user_settings = {} @property @@ -359,9 +361,7 @@ class InvenTreeNotificationBodies: ) -def trigger_notification( - obj: Model, category: Optional[str] = None, obj_ref: str = 'pk', **kwargs -): +def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', **kwargs): """Send out a notification. Args: diff --git a/src/backend/InvenTree/common/test_notifications.py b/src/backend/InvenTree/common/test_notifications.py index b1900342891d..b56b69cbd392 100644 --- a/src/backend/InvenTree/common/test_notifications.py +++ b/src/backend/InvenTree/common/test_notifications.py @@ -36,19 +36,19 @@ def send(self): # no send / send bulk with self.assertRaises(NotImplementedError): - FalseNotificationMethod('', '', '', '') + FalseNotificationMethod('', '', None, '') # no METHOD_NAME with self.assertRaises(NotImplementedError): - NoNameNotificationMethod('', '', '', '') + NoNameNotificationMethod('', '', None, '') # a not existent context check with self.assertRaises(NotImplementedError): - WrongContextNotificationMethod('', '', '', '') + WrongContextNotificationMethod('', '', None, '') # no get_targets with self.assertRaises(NotImplementedError): - AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2}) + AnotherFalseNotificationMethod('', '', None, {'name': 1, 'message': 2}) def test_failing_passing(self): """Ensure that an error in one deliverymethod is not blocking all mehthods.""" diff --git a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py index 3b7c928489e3..60fcdf98dec8 100644 --- a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -106,7 +106,7 @@ def get_manufacturer_name(part_id): response = cursor.execute(query) row = cursor.fetchone() - if len(row) > 0: + if len(row or []) > 0: return row[0] return '' # pragma: no cover diff --git a/src/backend/InvenTree/plugin/plugin.py b/src/backend/InvenTree/plugin/plugin.py index 93ace6be9c6d..c2bc6cbfe8d5 100644 --- a/src/backend/InvenTree/plugin/plugin.py +++ b/src/backend/InvenTree/plugin/plugin.py @@ -468,8 +468,9 @@ def define_package(self): package = {} # process date - if package.get('date'): - package['date'] = datetime.fromisoformat(package.get('date')) + date = package.get('date') + if date: + package['date'] = datetime.fromisoformat(date) # set variables self.package = package diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 63ba3f4bdc47..e91af9dd66e4 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -220,7 +220,7 @@ def call_plugin_function(self, slug: str, func: str, *args, **kwargs): # region registry functions def with_mixin( - self, mixin: str, active: bool = True, builtin: Optional[bool] = None + self, mixin: str, active: bool | None = True, builtin: Optional[bool] = None ) -> list: """Returns reference to all plugins that have a specified mixin enabled. diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index dea5434b68c8..0487577ef3d6 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -283,14 +283,14 @@ def test_broken_samples(self): self.assertEqual(len(registry.errors), 2) # There should be at least one discovery error in the module `broken_file` - self.assertGreater(len(registry.errors.get('discovery')), 0) + self.assertGreater(len(registry.errors.get('discovery') or []), 0) self.assertEqual( registry.errors.get('discovery')[0]['broken_file'], "name 'bb' is not defined", ) # There should be at least one load error with an intentional KeyError - self.assertGreater(len(registry.errors.get('init')), 0) + self.assertGreater(len(registry.errors.get('init') or []), 0) self.assertEqual( registry.errors.get('init')[0]['broken_sample'], "'This is a dummy error'" ) From 9c399befaabf29f2d9baf0720d0bdd4cbd8a0611 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 15 May 2025 00:25:59 +0200 Subject: [PATCH 05/33] and more types --- src/backend/InvenTree/common/notifications.py | 2 +- src/backend/InvenTree/machine/registry.py | 4 +++- src/backend/InvenTree/plugin/registry.py | 13 +++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/backend/InvenTree/common/notifications.py b/src/backend/InvenTree/common/notifications.py index 762699f25db9..e274269b3126 100644 --- a/src/backend/InvenTree/common/notifications.py +++ b/src/backend/InvenTree/common/notifications.py @@ -419,7 +419,7 @@ def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', ** target_exclude = set() # Collect possible targets - if not targets: + if not targets and target_fnc: targets = target_fnc(*target_args, **target_kwargs) # Convert list of targets to a list of users diff --git a/src/backend/InvenTree/machine/registry.py b/src/backend/InvenTree/machine/registry.py index af4a61b9928e..aca853039a96 100644 --- a/src/backend/InvenTree/machine/registry.py +++ b/src/backend/InvenTree/machine/registry.py @@ -276,7 +276,9 @@ def _calculate_registry_hash(self): # If the plugin registry has changed, the machine registry hash will change plugin_registry.update_plugin_hash() - data.update(plugin_registry.registry_hash.encode()) + current_hash = plugin_registry.registry_hash + if current_hash: + data.update(current_hash.encode()) for pk, machine in self.machines.items(): data.update(str(pk).encode()) diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index e91af9dd66e4..c42760475949 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -84,7 +84,7 @@ def __init__(self) -> None: ] = {} # List of all plugin instances # Keep an internal hash of the plugin registry state - self.registry_hash = None + self.registry_hash: str | None = None self.plugin_modules: list[InvenTreePlugin] = [] # Holds all discovered plugins self.mixin_modules: dict[str, Any] = {} # Holds all discovered mixins @@ -538,10 +538,9 @@ def safe_reference(plugin, key: str, active: bool = True): package_name = getattr(plugin, 'package_name', None) # Auto-enable default builtin plugins - if builtin and plg_db and plg_db.is_mandatory(): - if not plg_db.active: - plg_db.active = True - plg_db.save() + if builtin and plg_db and plg_db.is_mandatory() and not plg_db.active: + plg_db.active = True + plg_db.save() # Save the package_name attribute to the plugin if plg_db.package_name != package_name: @@ -929,11 +928,13 @@ def _load_source(modname, filename): # loader = importlib.machinery.SourceFileLoader(modname, filename) spec = importlib.util.spec_from_file_location(modname, filename) # , loader=loader) + if spec is None: + raise ImportError(f"Cannot find module '{modname}'") module = importlib.util.module_from_spec(spec) sys.modules[module.__name__] = module - if spec.loader: + if spec.loader is not None: spec.loader.exec_module(module) return module From d217f7a6baf73afcee05903171d2df95c45d13e2 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 15 May 2025 09:54:47 +0200 Subject: [PATCH 06/33] and more typing --- .github/scripts/version_check.py | 8 ++++---- src/backend/InvenTree/InvenTree/apps.py | 3 +++ src/backend/InvenTree/InvenTree/tasks.py | 2 +- src/backend/InvenTree/InvenTree/unit_test.py | 4 +++- src/backend/InvenTree/InvenTree/version.py | 2 +- src/backend/InvenTree/common/models.py | 4 ++-- .../company/migrations/0019_auto_20200413_0642.py | 2 +- .../InvenTree/generic/states/test_transition.py | 6 +++--- src/backend/InvenTree/generic/states/transition.py | 14 ++++++++------ src/backend/InvenTree/plugin/admin.py | 2 +- src/backend/InvenTree/plugin/registry.py | 5 +++-- src/backend/InvenTree/plugin/test_plugin.py | 4 +++- src/backend/InvenTree/report/models.py | 10 +++++++--- 13 files changed, 40 insertions(+), 26 deletions(-) diff --git a/.github/scripts/version_check.py b/.github/scripts/version_check.py index 4d596395a329..fb2f39452515 100644 --- a/.github/scripts/version_check.py +++ b/.github/scripts/version_check.py @@ -48,7 +48,7 @@ def get_existing_release_tags(include_prerelease=True): tag = release['tag_name'].strip() match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag) - if len(match.groups()) != 3: + if not match or len(match.groups()) != 3: print(f"Version '{tag}' did not match expected pattern") continue @@ -164,16 +164,16 @@ def check_version_number(version_string, allow_duplicate=False): highest_release = check_version_number(version, allow_duplicate=allow_duplicate) # Determine which docker tag we are going to use - docker_tags = None + docker_tags: list[str] | None = None if GITHUB_REF_TYPE == 'tag': # GITHUB_REF should be of the form /refs/heads/ - version_tag = GITHUB_REF.split('/')[-1] + version_tag: str = GITHUB_REF.split('/')[-1] print(f"Checking requirements for tagged release - '{version_tag}':") if version_tag != version: print(f"Version number '{version}' does not match tag '{version_tag}'") - sys.exit + sys.exit() docker_tags = [version_tag, 'stable'] if highest_release else [version_tag] diff --git a/src/backend/InvenTree/InvenTree/apps.py b/src/backend/InvenTree/InvenTree/apps.py index ea13d092ccc1..29cc46aece51 100644 --- a/src/backend/InvenTree/InvenTree/apps.py +++ b/src/backend/InvenTree/InvenTree/apps.py @@ -134,6 +134,9 @@ def start_background_tasks(self): tasks = InvenTree.tasks.tasks.task_list for task in tasks: + if not task: + continue + ref_name = f'{task.func.__module__}.{task.func.__name__}' if ref_name in existing_tasks: diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index 6492a8d19f3b..f7b122a76655 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -514,7 +514,7 @@ def check_for_updates(): match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag) - if len(match.groups()) != 3: # pragma: no cover + if not match or len(match.groups()) != 3: # pragma: no cover logger.warning("Version '%s' did not match expected pattern", tag) return diff --git a/src/backend/InvenTree/InvenTree/unit_test.py b/src/backend/InvenTree/InvenTree/unit_test.py index 23600e81ca6b..df59ea93998e 100644 --- a/src/backend/InvenTree/InvenTree/unit_test.py +++ b/src/backend/InvenTree/InvenTree/unit_test.py @@ -76,7 +76,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True): oldest_num = num oldest_file = f - if exclude_extension: + if exclude_extension and oldest_file: oldest_file = oldest_file.replace('.py', '') return oldest_file @@ -518,6 +518,8 @@ def download_file( result = re.search( r'(attachment|inline); filename=[\'"]([\w\d\-.]+)[\'"]', disposition ) + if not result: + raise ValueError('No filename match found in disposition') fn = result.groups()[1] diff --git a/src/backend/InvenTree/InvenTree/version.py b/src/backend/InvenTree/InvenTree/version.py index 5ee6f1af662f..7c9cbb10b46e 100644 --- a/src/backend/InvenTree/InvenTree/version.py +++ b/src/backend/InvenTree/InvenTree/version.py @@ -107,7 +107,7 @@ def inventreeVersionTuple(version=None): match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', str(version)) - return [int(g) for g in match.groups()] + return [int(g) for g in match.groups()] if match else [] def isInvenTreeDevelopmentVersion(): diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index e10332e8647a..1eb068dda95b 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -1416,8 +1416,8 @@ def save_data(self, payload=None, headers=None, request=None): request (optional): Original request object. Defaults to None. """ return WebhookMessage.objects.create( - host=request.get_host(), - header=json.dumps(dict(headers.items())), + host=request.get_host() if request else '', + header=json.dumps(dict(headers.items())) if headers else None, body=payload, endpoint=self, ) diff --git a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py index 60fcdf98dec8..95d57056966f 100644 --- a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -51,7 +51,7 @@ def reverse_association(apps, schema_editor): # pragma: no cover row = cursor.fetchone() - if len(row) > 0: + if row and len(row) > 0: try: manufacturer_id = int(row[0]) except (TypeError, ValueError): diff --git a/src/backend/InvenTree/generic/states/test_transition.py b/src/backend/InvenTree/generic/states/test_transition.py index 38f4946e32e6..050fa40b9a96 100644 --- a/src/backend/InvenTree/generic/states/test_transition.py +++ b/src/backend/InvenTree/generic/states/test_transition.py @@ -58,7 +58,7 @@ def transition(self, *args, **kwargs): storage.collect() # Ensure the class is registered - self.assertIn(RaisingImplementation, storage.list) + self.assertIn(RaisingImplementation, storage.method_list) # Ensure stuff is passed to the class with self.assertRaises(MyPrivateError) as exp: @@ -90,8 +90,8 @@ def transition(self, *args, **kwargs): return False # pragma: no cover # Return false to keep other transitions working storage.collect() - self.assertIn(ValidImplementationNoEffect, storage.list) - self.assertIn(ValidImplementation, storage.list) + self.assertIn(ValidImplementationNoEffect, storage.method_list) + self.assertIn(ValidImplementation, storage.method_list) # Ensure that the function is called self.assertEqual( diff --git a/src/backend/InvenTree/generic/states/transition.py b/src/backend/InvenTree/generic/states/transition.py index 6abc7f101025..a1aba3105c8b 100644 --- a/src/backend/InvenTree/generic/states/transition.py +++ b/src/backend/InvenTree/generic/states/transition.py @@ -1,5 +1,7 @@ """Classes and functions for plugin controlled object state transitions.""" +from typing import Any + import InvenTree.helpers @@ -28,11 +30,11 @@ class TransitionMethodStorageClass: Is initialized on startup as one instance named `storage` in this file. """ - list = None + method_list: list | None = None def collect(self): """Collect all classes in the environment that are transition methods.""" - filtered_list = {} + filtered_list: dict[str, Any] = {} for item in InvenTree.helpers.inheritors(TransitionMethod): # Try if valid try: @@ -41,11 +43,11 @@ def collect(self): continue filtered_list[f'{item.__module__}.{item.__qualname__}'] = item - self.list = list(filtered_list.values()) + self.method_list: list = list(filtered_list.values()) # Ensure the list has items - if not self.list: - self.list = [] + if not self.method_list: + self.method_list = [] storage = TransitionMethodStorageClass() @@ -77,7 +79,7 @@ def handle_transition( default_action: Default action to be taken if none of the transitions returns a boolean true value """ # Check if there is a custom override function for this transition - for override in storage.list: + for override in storage.method_list: rslt = override.transition( current_state, target_state, instance, default_action, **kwargs ) diff --git a/src/backend/InvenTree/plugin/admin.py b/src/backend/InvenTree/plugin/admin.py index 49afa1aed611..5584e11ac0dd 100644 --- a/src/backend/InvenTree/plugin/admin.py +++ b/src/backend/InvenTree/plugin/admin.py @@ -2,8 +2,8 @@ from django.contrib import admin -import plugin.registry as pl_registry from plugin import models +from plugin.registry import registry as pl_registry def plugin_update(queryset, new_status: bool): diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index c42760475949..155ea5665e3c 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -934,7 +934,8 @@ def _load_source(modname, filename): sys.modules[module.__name__] = module - if spec.loader is not None: - spec.loader.exec_module(module) + loader = spec.loader + if loader is not None: + loader.exec_module(module) return module diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index 0487577ef3d6..35a2b45bf8aa 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -201,7 +201,9 @@ def test_version(self): self.assertFalse(self.plugin_version.check_version([0, 1, 4])) plug = registry.plugins_full.get('sampleversion') - self.assertEqual(plug.is_active(), False) + self.assertIsNotNone(plug) + if plug: + self.assertEqual(plug.is_active(), False) class RegistryTests(TestCase): diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 8cc046c91d5d..f9468576bdbf 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -225,7 +225,7 @@ def save(self, *args, **kwargs): ), ) - def generate_filename(self, context, **kwargs): + def generate_filename(self, context, **kwargs) -> str: """Generate a filename for this report.""" template_string = Template(self.filename_pattern) @@ -438,7 +438,7 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: debug_mode = get_global_setting('REPORT_DEBUG_MODE', False) # Start with a default report name - report_name = None + report_name: str | None = None report_plugins = registry.with_mixin(PluginMixinEnum.REPORT) @@ -513,6 +513,9 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: 'path': request.path if request else None, }) + if not report_name: + report_name = '' + if not report_name.endswith('.pdf'): report_name += '.pdf' @@ -541,7 +544,8 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: # Save the generated report to the database output.complete = True - output.output = ContentFile(data, report_name) + if data: + output.output = ContentFile(data, report_name) output.save() return output From 7a4d71425bb77ab9036ed67cd3f1c4c6a64b9d87 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 15 May 2025 12:41:59 +0200 Subject: [PATCH 07/33] fix imports --- src/backend/InvenTree/InvenTree/filters.py | 11 ++++++----- src/backend/InvenTree/build/api.py | 9 +++++---- src/backend/InvenTree/common/api.py | 5 +++-- src/backend/InvenTree/company/api.py | 11 ++++++----- src/backend/InvenTree/order/api.py | 11 ++++++----- src/backend/InvenTree/part/api.py | 19 ++++++++++--------- src/backend/InvenTree/plugin/api.py | 5 +++-- .../InvenTree/plugin/base/barcodes/api.py | 4 ++-- src/backend/InvenTree/report/api.py | 5 +++-- src/backend/InvenTree/stock/api.py | 9 +++++---- 10 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/filters.py b/src/backend/InvenTree/InvenTree/filters.py index 30745679a026..befdc8910833 100644 --- a/src/backend/InvenTree/InvenTree/filters.py +++ b/src/backend/InvenTree/InvenTree/filters.py @@ -6,7 +6,8 @@ from django.utils import timezone from django.utils.timezone import make_aware -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters +import django_filters.rest_framework.backends as drf_backend from rest_framework import filters import InvenTree.helpers @@ -160,17 +161,17 @@ def get_ordering(self, request, queryset, view): SEARCH_ORDER_FILTER = [ - rest_filters.DjangoFilterBackend, + drf_backend.DjangoFilterBackend, InvenTreeSearchFilter, filters.OrderingFilter, ] SEARCH_ORDER_FILTER_ALIAS = [ - rest_filters.DjangoFilterBackend, + drf_backend.DjangoFilterBackend, InvenTreeSearchFilter, InvenTreeOrderingFilter, ] -ORDER_FILTER = [rest_filters.DjangoFilterBackend, filters.OrderingFilter] +ORDER_FILTER = [drf_backend.DjangoFilterBackend, filters.OrderingFilter] -ORDER_FILTER_ALIAS = [rest_filters.DjangoFilterBackend, InvenTreeOrderingFilter] +ORDER_FILTER_ALIAS = [drf_backend.DjangoFilterBackend, InvenTreeOrderingFilter] diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index e62dd926560a..247d679ad6fc 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -7,7 +7,8 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters +from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -28,7 +29,7 @@ from users.models import Owner -class BuildFilter(rest_filters.FilterSet): +class BuildFilter(FilterSet): """Custom filterset for BuildList API endpoint.""" class Meta: @@ -440,7 +441,7 @@ def get_serializer_context(self): return ctx -class BuildLineFilter(rest_filters.FilterSet): +class BuildLineFilter(FilterSet): """Custom filterset for the BuildLine API endpoint.""" class Meta: @@ -756,7 +757,7 @@ class BuildItemDetail(RetrieveUpdateDestroyAPI): serializer_class = build.serializers.BuildItemSerializer -class BuildItemFilter(rest_filters.FilterSet): +class BuildItemFilter(FilterSet): """Custom filterset for the BuildItemList API endpoint.""" class Meta: diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 001b69c733d9..37458da31e9c 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -14,8 +14,9 @@ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import csrf_exempt +import django_filters.filters as rest_filters import django_q.models -from django_filters import rest_framework as rest_filters +from django_filters.rest_framework.filterset import FilterSet from django_q.tasks import async_task from djmoney.contrib.exchange.models import ExchangeBackend, Rate from drf_spectacular.utils import OpenApiResponse, extend_schema @@ -699,7 +700,7 @@ def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) -class AttachmentFilter(rest_filters.FilterSet): +class AttachmentFilter(FilterSet): """Filterset for the AttachmentList API endpoint.""" class Meta: diff --git a/src/backend/InvenTree/company/api.py b/src/backend/InvenTree/company/api.py index d4ba2c09907b..28b2bbdb1c30 100644 --- a/src/backend/InvenTree/company/api.py +++ b/src/backend/InvenTree/company/api.py @@ -4,7 +4,8 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters +from django_filters.rest_framework.filterset import FilterSet import part.models from data_exporter.mixins import DataExportViewMixin @@ -127,7 +128,7 @@ class AddressDetail(RetrieveUpdateDestroyAPI): serializer_class = AddressSerializer -class ManufacturerPartFilter(rest_filters.FilterSet): +class ManufacturerPartFilter(FilterSet): """Custom API filters for the ManufacturerPart list endpoint.""" class Meta: @@ -204,7 +205,7 @@ class ManufacturerPartDetail(RetrieveUpdateDestroyAPI): serializer_class = ManufacturerPartSerializer -class ManufacturerPartParameterFilter(rest_filters.FilterSet): +class ManufacturerPartParameterFilter(FilterSet): """Custom filterset for the ManufacturerPartParameterList API endpoint.""" class Meta: @@ -259,7 +260,7 @@ class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI): serializer_class = ManufacturerPartParameterSerializer -class SupplierPartFilter(rest_filters.FilterSet): +class SupplierPartFilter(FilterSet): """API filters for the SupplierPartList endpoint.""" class Meta: @@ -416,7 +417,7 @@ class SupplierPartDetail(SupplierPartMixin, RetrieveUpdateDestroyAPI): """ -class SupplierPriceBreakFilter(rest_filters.FilterSet): +class SupplierPriceBreakFilter(FilterSet): """Custom API filters for the SupplierPriceBreak list endpoint.""" class Meta: diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 283e616cd9ad..409499b8106d 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -11,8 +11,9 @@ from django.urls import include, path, re_path from django.utils.translation import gettext_lazy as _ +import django_filters.filters as rest_filters import rest_framework.serializers -from django_filters import rest_framework as rest_filters +from django_filters.rest_framework.filterset import FilterSet from django_ical.views import ICalFeed from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field @@ -97,7 +98,7 @@ def create(self, request, *args, **kwargs): ) -class OrderFilter(rest_filters.FilterSet): +class OrderFilter(FilterSet): """Base class for custom API filters for the OrderList endpoint.""" # Filter against order status @@ -255,7 +256,7 @@ def filter_max_date(self, queryset, name, value): return queryset.filter(q1 | q2 | q3 | q4).distinct() -class LineItemFilter(rest_filters.FilterSet): +class LineItemFilter(FilterSet): """Base class for custom API filters for order line item list(s).""" # Filter by order status @@ -1087,7 +1088,7 @@ class SalesOrderAllocate(SalesOrderContextMixin, CreateAPI): serializer_class = serializers.SalesOrderShipmentAllocationSerializer -class SalesOrderAllocationFilter(rest_filters.FilterSet): +class SalesOrderAllocationFilter(FilterSet): """Custom filterset for the SalesOrderAllocationList endpoint.""" class Meta: @@ -1240,7 +1241,7 @@ class SalesOrderAllocationDetail(SalesOrderAllocationMixin, RetrieveUpdateDestro """API endpoint for detali view of a SalesOrderAllocation object.""" -class SalesOrderShipmentFilter(rest_filters.FilterSet): +class SalesOrderShipmentFilter(FilterSet): """Custom filterset for the SalesOrderShipmentList endpoint.""" class Meta: diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index f61f9da261bf..94d90a20077e 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -8,8 +8,9 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -102,7 +103,7 @@ def get_serializer_context(self): return ctx -class CategoryFilter(rest_filters.FilterSet): +class CategoryFilter(FilterSet): """Custom filterset class for the PartCategoryList endpoint.""" class Meta: @@ -403,7 +404,7 @@ class PartInternalPriceList(DataExportViewMixin, ListCreateAPI): ordering = 'quantity' -class PartTestTemplateFilter(rest_filters.FilterSet): +class PartTestTemplateFilter(FilterSet): """Custom filterset class for the PartTestTemplateList endpoint.""" class Meta: @@ -891,7 +892,7 @@ def update(self, request, *args, **kwargs): return Response({'checksum': part.bom_checksum}) -class PartFilter(rest_filters.FilterSet): +class PartFilter(FilterSet): """Custom filters for the PartList endpoint. Uses the django_filters extension framework @@ -1435,7 +1436,7 @@ def update(self, request, *args, **kwargs): return response -class PartRelatedFilter(rest_filters.FilterSet): +class PartRelatedFilter(FilterSet): """FilterSet for PartRelated objects.""" class Meta: @@ -1482,7 +1483,7 @@ class PartRelatedDetail(PartRelatedMixin, RetrieveUpdateDestroyAPI): """API endpoint for accessing detail view of a PartRelated object.""" -class PartParameterTemplateFilter(rest_filters.FilterSet): +class PartParameterTemplateFilter(FilterSet): """FilterSet for PartParameterTemplate objects.""" class Meta: @@ -1609,7 +1610,7 @@ def get_serializer(self, *args, **kwargs): return super().get_serializer(*args, **kwargs) -class PartParameterFilter(rest_filters.FilterSet): +class PartParameterFilter(FilterSet): """Custom filters for the PartParameterList API endpoint.""" class Meta: @@ -1670,7 +1671,7 @@ class PartParameterDetail(PartParameterAPIMixin, RetrieveUpdateDestroyAPI): """API endpoint for detail view of a single PartParameter object.""" -class PartStocktakeFilter(rest_filters.FilterSet): +class PartStocktakeFilter(FilterSet): """Custom filter for the PartStocktakeList endpoint.""" class Meta: @@ -1753,7 +1754,7 @@ def get_serializer_context(self): return context -class BomFilter(rest_filters.FilterSet): +class BomFilter(FilterSet): """Custom filters for the BOM list.""" class Meta: diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py index dacd6e2ac8c9..b42756c7fed6 100644 --- a/src/backend/InvenTree/plugin/api.py +++ b/src/backend/InvenTree/plugin/api.py @@ -6,8 +6,9 @@ from django.urls import include, path, re_path from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.exceptions import NotFound @@ -36,7 +37,7 @@ from plugin.registry import registry -class PluginFilter(rest_filters.FilterSet): +class PluginFilter(FilterSet): """Filter for the PluginConfig model. Provides custom filtering options for the FilterList API endpoint. diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 301667c50904..579b9abddf51 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _ import structlog -from django_filters import rest_framework as rest_filters +from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import status from rest_framework.exceptions import PermissionDenied, ValidationError @@ -770,7 +770,7 @@ def get_queryset(self): return queryset -class BarcodeScanResultFilter(rest_filters.FilterSet): +class BarcodeScanResultFilter(FilterSet): """Custom filterset for the BarcodeScanResult API.""" class Meta: diff --git a/src/backend/InvenTree/report/api.py b/src/backend/InvenTree/report/api.py index b873cde37f69..cfbf24f0cb6b 100644 --- a/src/backend/InvenTree/report/api.py +++ b/src/backend/InvenTree/report/api.py @@ -6,8 +6,9 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework.filterset import FilterSet from rest_framework.generics import GenericAPIView from rest_framework.response import Response @@ -32,7 +33,7 @@ class TemplatePermissionMixin: permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope] -class ReportFilterBase(rest_filters.FilterSet): +class ReportFilterBase(FilterSet): """Base filter class for label and report templates.""" enabled = rest_filters.BooleanFilter() diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 81c9bea06d99..5a7785a76f8e 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -9,7 +9,8 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework as rest_filters +import django_filters.filters as rest_filters +from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import status @@ -228,7 +229,7 @@ def get_serializer_context(self): return ctx -class StockLocationFilter(rest_filters.FilterSet): +class StockLocationFilter(FilterSet): """Base class for custom API filters for the StockLocation endpoint.""" class Meta: @@ -476,7 +477,7 @@ def get_queryset(self): return queryset -class StockFilter(rest_filters.FilterSet): +class StockFilter(FilterSet): """FilterSet for StockItem LIST API.""" class Meta: @@ -1272,7 +1273,7 @@ class StockItemTestResultDetail(StockItemTestResultMixin, RetrieveUpdateDestroyA """Detail endpoint for StockItemTestResult.""" -class StockItemTestResultFilter(rest_filters.FilterSet): +class StockItemTestResultFilter(FilterSet): """API filter for the StockItemTestResult list.""" class Meta: From 5475249a4aaaa96e19d8383538cf171f68420676 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 15 May 2025 22:41:59 +0200 Subject: [PATCH 08/33] more fixes --- .github/scripts/version_check.py | 23 +++++++++++++------ src/backend/InvenTree/InvenTree/exceptions.py | 3 ++- src/backend/InvenTree/InvenTree/test_api.py | 2 +- src/backend/InvenTree/build/models.py | 2 +- .../InvenTree/generic/states/transition.py | 16 +++++++------ src/backend/InvenTree/order/tasks.py | 5 ++-- src/backend/InvenTree/part/api.py | 10 ++++---- .../plugin/base/integration/APICallMixin.py | 2 +- src/backend/InvenTree/plugin/test_plugin.py | 2 +- src/backend/InvenTree/stock/api.py | 10 ++++---- 10 files changed, 44 insertions(+), 31 deletions(-) diff --git a/.github/scripts/version_check.py b/.github/scripts/version_check.py index fb2f39452515..f8564d5b8b99 100644 --- a/.github/scripts/version_check.py +++ b/.github/scripts/version_check.py @@ -94,11 +94,12 @@ def check_version_number(version_string, allow_duplicate=False): return highest_release -if __name__ == '__main__': +def main() -> bool: + """Main function to check the version number.""" # Ensure that we are running in GH Actions if os.environ.get('GITHUB_ACTIONS', '') != 'true': print('This script is intended to be run within a GitHub Action!') - sys.exit(1) + return False if 'only_version' in sys.argv: here = Path(__file__).parent.absolute() @@ -111,7 +112,7 @@ def check_version_number(version_string, allow_duplicate=False): if len(sys.argv) > 2 and sys.argv[2] == 'true': results[0] = str(int(results[0]) - 1) print(results[0]) - exit(0) + return True # GITHUB_REF_TYPE may be either 'branch' or 'tag' GITHUB_REF_TYPE = os.environ['GITHUB_REF_TYPE'] @@ -142,7 +143,7 @@ def check_version_number(version_string, allow_duplicate=False): if len(results) != 1: print(f'Could not find INVENTREE_SW_VERSION in {version_file}') - sys.exit(1) + return False version = results[0] @@ -173,7 +174,7 @@ def check_version_number(version_string, allow_duplicate=False): if version_tag != version: print(f"Version number '{version}' does not match tag '{version_tag}'") - sys.exit() + return True docker_tags = [version_tag, 'stable'] if highest_release else [version_tag] @@ -187,11 +188,11 @@ def check_version_number(version_string, allow_duplicate=False): print('GITHUB_REF_TYPE:', GITHUB_REF_TYPE) print('GITHUB_BASE_REF:', GITHUB_BASE_REF) print('GITHUB_REF:', GITHUB_REF) - sys.exit(1) + return False if docker_tags is None: print('Docker tags could not be determined') - sys.exit(1) + return False print(f"Version check passed for '{version}'!") print(f"Docker tags: '{docker_tags}'") @@ -208,3 +209,11 @@ def check_version_number(version_string, allow_duplicate=False): if GITHUB_REF_TYPE == 'tag' and highest_release: env_file.write('stable_release=true\n') + return True + + +if __name__ == '__main__': + rslt = main() + if rslt is not True: + print('Version check failed!') + sys.exit(1) diff --git a/src/backend/InvenTree/InvenTree/exceptions.py b/src/backend/InvenTree/InvenTree/exceptions.py index ac84befa93d1..07ec874ddb4c 100644 --- a/src/backend/InvenTree/InvenTree/exceptions.py +++ b/src/backend/InvenTree/InvenTree/exceptions.py @@ -50,7 +50,8 @@ def log_error(path, error_name=None, error_info=None, error_data=None): data = error_data else: try: - data = '\n'.join(traceback.format_exception(kind, info, data)) + formatted_exception = traceback.format_exception(kind, info, data) # type: ignore[no-matching-overload] + data = '\n'.join(formatted_exception) except AttributeError: data = 'No traceback information available' diff --git a/src/backend/InvenTree/InvenTree/test_api.py b/src/backend/InvenTree/InvenTree/test_api.py index 289a204a94fc..95aa69054ac9 100644 --- a/src/backend/InvenTree/InvenTree/test_api.py +++ b/src/backend/InvenTree/InvenTree/test_api.py @@ -567,7 +567,7 @@ def test_api_license(self): self.assertIn('License file not found at', str(log.output)) - with TemporaryDirectory() as tmp: + with TemporaryDirectory() as tmp: # type: ignore[no-matching-overload] sample_file = Path(tmp, 'temp.txt') sample_file.write_text('abc') diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 37e3e4faa7c4..91ddebb4bd70 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -1307,7 +1307,7 @@ def stock_sort(item, bom_item, variant_parts): except (ValidationError, serializers.ValidationError) as exc: # Catch model errors and re-throw as DRF errors raise ValidationError( - detail=serializers.as_serializer_error(exc) + exc.message, detail=serializers.as_serializer_error(exc) ) if unallocated_quantity <= 0: diff --git a/src/backend/InvenTree/generic/states/transition.py b/src/backend/InvenTree/generic/states/transition.py index a1aba3105c8b..0e5059e88d41 100644 --- a/src/backend/InvenTree/generic/states/transition.py +++ b/src/backend/InvenTree/generic/states/transition.py @@ -78,13 +78,15 @@ def handle_transition( instance: Object instance default_action: Default action to be taken if none of the transitions returns a boolean true value """ - # Check if there is a custom override function for this transition - for override in storage.method_list: - rslt = override.transition( - current_state, target_state, instance, default_action, **kwargs - ) - if rslt: - return rslt + list_vals = storage.method_list + if list_vals: + # Check if there is a custom override function for this transition + for override in list_vals: + rslt = override.transition( + current_state, target_state, instance, default_action, **kwargs + ) + if rslt: + return rslt # Default action return default_action(current_state, target_state, instance, **kwargs) diff --git a/src/backend/InvenTree/order/tasks.py b/src/backend/InvenTree/order/tasks.py index 21babfe2c166..7d315f2b00b4 100644 --- a/src/backend/InvenTree/order/tasks.py +++ b/src/backend/InvenTree/order/tasks.py @@ -1,6 +1,7 @@ """Background tasks for the 'order' app.""" from datetime import datetime, timedelta +from typing import Union from django.contrib.auth.models import Group, User from django.db import transaction @@ -99,7 +100,7 @@ def check_overdue_purchase_orders(): def notify_overdue_sales_order(so: order.models.SalesOrder) -> None: """Notify appropriate users that a SalesOrder has just become 'overdue'.""" - targets: list[User, Group, Owner] = [] + targets: list[Union[User, Group, Owner]] = [] if so.created_by: targets.append(so.created_by) @@ -164,7 +165,7 @@ def check_overdue_sales_orders(): def notify_overdue_return_order(ro: order.models.ReturnOrder) -> None: """Notify appropriate users that a ReturnOrder has just become 'overdue'.""" - targets: list[User, Group, Owner] = [] + targets: list[Union[User, Group, Owner]] = [] if ro.created_by: targets.append(ro.created_by) diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 94d90a20077e..a8eed46c9f11 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -287,11 +287,11 @@ def destroy(self, request, *args, **kwargs): return super().destroy( request, *args, - **dict( - kwargs, - delete_parts=delete_parts, - delete_child_categories=delete_child_categories, - ), + **{ + **kwargs, + 'delete_parts': delete_parts, + 'delete_child_categories': delete_child_categories, + }, ) diff --git a/src/backend/InvenTree/plugin/base/integration/APICallMixin.py b/src/backend/InvenTree/plugin/base/integration/APICallMixin.py index d67bc64857b2..46e1d2b1ec53 100644 --- a/src/backend/InvenTree/plugin/base/integration/APICallMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/APICallMixin.py @@ -175,7 +175,7 @@ def api_call( kwargs['data'] = data # run command - response = requests.request(method, **kwargs) + response = requests.request(method, url=url, **kwargs) # return if simple_response: diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index 35a2b45bf8aa..c6c2b7b8f54c 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -249,7 +249,7 @@ def test_subfolder_loading(self): def test_folder_loading(self): """Test that plugins in folders outside of BASE_DIR get loaded.""" # Run in temporary directory -> always a new random name - with tempfile.TemporaryDirectory() as tmp: + with tempfile.TemporaryDirectory() as tmp: # type: ignore[no-matching-overload] # Fill directory with sample data new_dir = Path(tmp).joinpath('mock') shutil.copytree(self.mockDir(), new_dir) diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 5a7785a76f8e..9c670281b078 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -397,11 +397,11 @@ def destroy(self, request, *args, **kwargs): return super().destroy( request, *args, - **dict( - kwargs, - delete_sub_locations=delete_sub_locations, - delete_stock_items=delete_stock_items, - ), + **{ + **kwargs, + 'delete_sub_locations': delete_sub_locations, + 'delete_stock_items': delete_stock_items, + }, ) From 7d7f9f7288e15be539e060726f3daab974aad9b8 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 15 May 2025 23:59:49 +0200 Subject: [PATCH 09/33] fix types and optional statements --- .github/scripts/version_check.py | 3 ++- docs/main.py | 2 +- pyproject.toml | 11 ++++++++++ src/backend/InvenTree/InvenTree/config.py | 3 ++- src/backend/InvenTree/InvenTree/helpers.py | 4 ++-- .../InvenTree/InvenTree/helpers_email.py | 4 +++- src/backend/InvenTree/build/api.py | 7 ++++--- src/backend/InvenTree/common/models.py | 4 ++-- .../migrations/0019_auto_20200413_0642.py | 10 +++++----- .../InvenTree/generic/states/states.py | 3 ++- .../InvenTree/generic/states/transition.py | 4 ++-- src/backend/InvenTree/importer/operations.py | 4 +++- .../InvenTree/plugin/base/barcodes/mixins.py | 16 ++++++++------- .../base/integration/ValidationMixin.py | 20 +++++++++---------- src/backend/InvenTree/plugin/helpers.py | 2 +- src/backend/InvenTree/plugin/models.py | 5 +++-- src/backend/InvenTree/plugin/plugin.py | 4 ++-- src/backend/InvenTree/plugin/registry.py | 6 +++--- src/backend/InvenTree/plugin/test_plugin.py | 15 ++++++++------ src/backend/InvenTree/report/models.py | 2 +- .../InvenTree/report/templatetags/report.py | 4 ++-- src/backend/InvenTree/stock/generators.py | 3 ++- src/backend/InvenTree/stock/models.py | 4 ++-- src/backend/InvenTree/users/models.py | 3 ++- 24 files changed, 85 insertions(+), 58 deletions(-) diff --git a/.github/scripts/version_check.py b/.github/scripts/version_check.py index f8564d5b8b99..0c91797fb580 100644 --- a/.github/scripts/version_check.py +++ b/.github/scripts/version_check.py @@ -16,6 +16,7 @@ import re import sys from pathlib import Path +from typing import Optional import requests @@ -165,7 +166,7 @@ def main() -> bool: highest_release = check_version_number(version, allow_duplicate=allow_duplicate) # Determine which docker tag we are going to use - docker_tags: list[str] | None = None + docker_tags: Optional[list[str]] = None if GITHUB_REF_TYPE == 'tag': # GITHUB_REF should be of the form /refs/heads/ diff --git a/docs/main.py b/docs/main.py index 3a706ec2cb94..67ee3e245690 100644 --- a/docs/main.py +++ b/docs/main.py @@ -110,7 +110,7 @@ def check_link(url) -> bool: return False -def get_build_environment() -> str | None: +def get_build_environment() -> Optional[str]: """Returns the branch we are currently building on, based on the environment variables of the various CI platforms.""" # Check if we are in ReadTheDocs if os.environ.get('READTHEDOCS') == 'True': diff --git a/pyproject.toml b/pyproject.toml index af50633b53a4..5620f70a280f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,17 @@ src = [ "src/backend/InvenTree", "./", ] +[tool.ty.rules] +unresolved-reference="ignore" +unresolved-attribute="ignore" +call-non-callable="ignore" +invalid-return-type="ignore" +invalid-argument-type="ignore" +invalid-type-form="ignore" +invalid-assignment="ignore" +possibly-unbound-attribute="ignore" +#call-possibly-unbound-method="ignore" +unknown-argument="ignore" [tool.coverage.run] diff --git a/src/backend/InvenTree/InvenTree/config.py b/src/backend/InvenTree/InvenTree/config.py index f82df57c2585..dcc49106977f 100644 --- a/src/backend/InvenTree/InvenTree/config.py +++ b/src/backend/InvenTree/InvenTree/config.py @@ -8,6 +8,7 @@ import shutil import string from pathlib import Path +from typing import Union logger = logging.getLogger('inventree') CONFIG_DATA = None @@ -113,7 +114,7 @@ def get_config_file(create=True) -> Path: return cfg_filename -def load_config_data(set_cache: bool = False) -> map | None: +def load_config_data(set_cache: bool = False) -> Union[map, None]: """Load configuration data from the config file. Arguments: diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index 2abd8dcc771b..54b62b1ac09e 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -8,7 +8,7 @@ import os.path import re from decimal import Decimal, InvalidOperation -from typing import Optional, TypeVar +from typing import Optional, TypeVar, Union from wsgiref.util import FileWrapper from zoneinfo import ZoneInfo, ZoneInfoNotFoundError @@ -125,7 +125,7 @@ def do_clip(value: int, clip: int, allow_negative: bool) -> int: return ref_int -def generateTestKey(test_name: str | None) -> str: +def generateTestKey(test_name: Union[str, None]) -> str: """Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template. Tests must be named such that they will have unique keys. diff --git a/src/backend/InvenTree/InvenTree/helpers_email.py b/src/backend/InvenTree/InvenTree/helpers_email.py index 5ea9aba155fc..9ec41ae7470a 100644 --- a/src/backend/InvenTree/InvenTree/helpers_email.py +++ b/src/backend/InvenTree/InvenTree/helpers_email.py @@ -1,5 +1,7 @@ """Code for managing email functionality in InvenTree.""" +from typing import Optional + from django.conf import settings from django.core import mail as django_mail @@ -91,7 +93,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None): ) -def get_email_for_user(user) -> str | None: +def get_email_for_user(user) -> Optional[str]: """Find an email address for the specified user.""" # First check if the user has an associated email address if user.email: diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 247d679ad6fc..4c692fc6ba62 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Optional + from django.contrib.auth.models import User from django.db.models import F, Q from django.urls import include, path @@ -13,7 +15,6 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -import build.admin import build.models import build.serializers import common.models @@ -602,7 +603,7 @@ class BuildLineList(BuildLineEndpoint, DataExportViewMixin, ListCreateAPI): 'bom_item__reference', ] - def get_source_build(self) -> Build | None: + def get_source_build(self) -> Optional[Build]: """Return the target build for the BuildLine queryset.""" source_build = None @@ -619,7 +620,7 @@ def get_source_build(self) -> Build | None: class BuildLineDetail(BuildLineEndpoint, RetrieveUpdateDestroyAPI): """API endpoint for detail view of a BuildLine object.""" - def get_source_build(self) -> Build | None: + def get_source_build(self) -> Optional[Build]: """Return the target source location for the BuildLine queryset.""" return None diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 1eb068dda95b..2d06e68c435d 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -14,7 +14,7 @@ from enum import Enum from io import BytesIO from secrets import compare_digest -from typing import Any, Union +from typing import Any, Optional, Union from django.apps import apps from django.conf import settings as django_settings @@ -917,7 +917,7 @@ def model_name(self) -> str: return setting.get('model', None) - def model_filters(self) -> dict | None: + def model_filters(self) -> Optional[dict]: """Return the model filters associated with this setting.""" setting = self.get_setting_definition( self.key, **self.get_filters_for_instance() diff --git a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py index 95d57056966f..858215cf9547 100644 --- a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -67,12 +67,12 @@ def reverse_association(apps, schema_editor): # pragma: no cover response = cursor.execute(f"SELECT name from company_company where id={manufacturer_id};") row = cursor.fetchone() + if row: + name = row[0] - name = row[0] + print(" - Manufacturer name: '{name}'".format(name=name)) - print(" - Manufacturer name: '{name}'".format(name=name)) - - response = cursor.execute("UPDATE part_supplierpart SET manufacturer_name='{name}' WHERE id={ID};".format(name=name, ID=supplier_part_id)) + response = cursor.execute("UPDATE part_supplierpart SET manufacturer_name='{name}' WHERE id={ID};".format(name=name, ID=supplier_part_id)) def associate_manufacturers(apps, schema_editor): """ @@ -106,7 +106,7 @@ def get_manufacturer_name(part_id): response = cursor.execute(query) row = cursor.fetchone() - if len(row or []) > 0: + if row and len(row) > 0: return row[0] return '' # pragma: no cover diff --git a/src/backend/InvenTree/generic/states/states.py b/src/backend/InvenTree/generic/states/states.py index b40ac9c55110..a252e9053df9 100644 --- a/src/backend/InvenTree/generic/states/states.py +++ b/src/backend/InvenTree/generic/states/states.py @@ -4,6 +4,7 @@ import logging import re from enum import Enum +from typing import Optional logger = logging.getLogger('inventree') @@ -297,7 +298,7 @@ def get_status(self) -> int: """Return the status code for this object.""" return getattr(self, self.STATUS_FIELD) - def get_custom_status(self) -> int | None: + def get_custom_status(self) -> Optional[int]: """Return the custom status code for this object.""" return getattr(self, f'{self.STATUS_FIELD}_custom_key', None) diff --git a/src/backend/InvenTree/generic/states/transition.py b/src/backend/InvenTree/generic/states/transition.py index 0e5059e88d41..72b7d68cc192 100644 --- a/src/backend/InvenTree/generic/states/transition.py +++ b/src/backend/InvenTree/generic/states/transition.py @@ -1,6 +1,6 @@ """Classes and functions for plugin controlled object state transitions.""" -from typing import Any +from typing import Any, Optional import InvenTree.helpers @@ -30,7 +30,7 @@ class TransitionMethodStorageClass: Is initialized on startup as one instance named `storage` in this file. """ - method_list: list | None = None + method_list: Optional[list] = None def collect(self): """Collect all classes in the environment that are transition methods.""" diff --git a/src/backend/InvenTree/importer/operations.py b/src/backend/InvenTree/importer/operations.py index 25a1530b57b5..0efca9017c59 100644 --- a/src/backend/InvenTree/importer/operations.py +++ b/src/backend/InvenTree/importer/operations.py @@ -1,5 +1,7 @@ """Data import operational functions.""" +from typing import Optional + from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -83,7 +85,7 @@ def extract_column_names(data_file) -> list: return headers -def get_field_label(field) -> str | None: +def get_field_label(field) -> Optional[str]: """Return the label for a field in a serializer class. Check for labels in the following order of descending priority: diff --git a/src/backend/InvenTree/plugin/base/barcodes/mixins.py b/src/backend/InvenTree/plugin/base/barcodes/mixins.py index 13cbbbe04460..f60b735c67bc 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/mixins.py +++ b/src/backend/InvenTree/plugin/base/barcodes/mixins.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Optional + from django.core.exceptions import ValidationError from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -113,7 +115,7 @@ def get_field_value(self, key, backup_value=None): return fields.get(key, backup_value) - def get_part(self) -> Part | None: + def get_part(self) -> Optional[Part]: """Extract the Part object from the barcode fields.""" # TODO: Implement this return None @@ -128,7 +130,7 @@ def supplier_part_number(self): """Return the supplier part number from the barcode fields.""" return self.get_field_value(self.SUPPLIER_PART_NUMBER) - def get_supplier_part(self) -> SupplierPart | None: + def get_supplier_part(self) -> Optional[SupplierPart]: """Return the SupplierPart object for the scanned barcode. Returns: @@ -172,7 +174,7 @@ def manufacturer_part_number(self): """Return the manufacturer part number from the barcode fields.""" return self.get_field_value(self.MANUFACTURER_PART_NUMBER) - def get_manufacturer_part(self) -> ManufacturerPart | None: + def get_manufacturer_part(self) -> Optional[ManufacturerPart]: """Return the ManufacturerPart object for the scanned barcode. Returns: @@ -213,7 +215,7 @@ def supplier_order_number(self): """Return the supplier order number from the barcode fields.""" return self.get_field_value(self.SUPPLIER_ORDER_NUMBER) - def get_purchase_order(self) -> PurchaseOrder | None: + def get_purchase_order(self) -> Optional[PurchaseOrder]: """Extract the PurchaseOrder object from the barcode fields. Inspect the customer_order_number and supplier_order_number fields, @@ -260,7 +262,7 @@ def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]: 'extract_barcode_fields must be implemented by each plugin' ) - def scan(self, barcode_data: str) -> dict | None: + def scan(self, barcode_data: str) -> Optional[dict]: """Perform a generic 'scan' operation on a supplier barcode. The supplier barcode may provide sufficient information to match against @@ -319,7 +321,7 @@ def scan_receive_item( location=None, auto_allocate: bool = True, **kwargs, - ) -> dict | None: + ) -> Optional[dict]: """Attempt to receive an item against a PurchaseOrder via barcode scanning. Arguments: @@ -430,7 +432,7 @@ def scan_receive_item( return response - def get_supplier(self, cache: bool = False) -> Company | None: + def get_supplier(self, cache: bool = False) -> Optional[Company]: """Get the supplier for the SUPPLIER_ID set in the plugin settings. If it's not defined, try to guess it and set it if possible. diff --git a/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py b/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py index dd30704ee895..0678bb3260b8 100644 --- a/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py @@ -72,7 +72,7 @@ def validate_model_deletion(self, instance: Model) -> None: def validate_model_instance( self, instance: Model, deltas: Optional[dict] = None - ) -> bool | None: + ) -> Optional[bool]: """Run custom validation on a database model instance. This method is called when a model instance is being validated. @@ -90,7 +90,7 @@ def validate_model_instance( """ return None - def validate_part_name(self, name: str, part: part.models.Part) -> bool | None: + def validate_part_name(self, name: str, part: part.models.Part) -> Optional[bool]: """Perform validation on a proposed Part name. Arguments: @@ -105,7 +105,7 @@ def validate_part_name(self, name: str, part: part.models.Part) -> bool | None: """ return None - def validate_part_ipn(self, ipn: str, part: part.models.Part) -> bool | None: + def validate_part_ipn(self, ipn: str, part: part.models.Part) -> Optional[bool]: """Perform validation on a proposed Part IPN (internal part number). Arguments: @@ -122,7 +122,7 @@ def validate_part_ipn(self, ipn: str, part: part.models.Part) -> bool | None: def validate_batch_code( self, batch_code: str, item: stock.models.StockItem - ) -> bool | None: + ) -> Optional[bool]: """Validate the supplied batch code. Arguments: @@ -137,7 +137,7 @@ def validate_batch_code( """ return None - def generate_batch_code(self, **kwargs) -> str | None: + def generate_batch_code(self, **kwargs) -> Optional[str]: """Generate a new batch code. This method is called when a new batch code is required. @@ -155,7 +155,7 @@ def validate_serial_number( serial: str, part: part.models.Part, stock_item: Optional[stock.models.StockItem] = None, - ) -> bool | None: + ) -> Optional[bool]: """Validate the supplied serial number. Arguments: @@ -171,7 +171,7 @@ def validate_serial_number( """ return None - def convert_serial_to_int(self, serial: str) -> int | None: + def convert_serial_to_int(self, serial: str) -> Optional[int]: """Convert a serial number (string) into an integer representation. This integer value is used for efficient sorting based on serial numbers. @@ -192,7 +192,7 @@ def convert_serial_to_int(self, serial: str) -> int | None: """ return None - def get_latest_serial_number(self, part, **kwargs) -> str | None: + def get_latest_serial_number(self, part, **kwargs) -> Optional[str]: """Return the 'latest' serial number for a given Part instance. A plugin which implements this method can either return: @@ -210,7 +210,7 @@ def get_latest_serial_number(self, part, **kwargs) -> str | None: def increment_serial_number( self, serial: str, part: Optional[part.models.Part] = None, **kwargs - ) -> str | None: + ) -> Optional[str]: """Return the next sequential serial based on the provided value. A plugin which implements this method can either return: @@ -229,7 +229,7 @@ def increment_serial_number( def validate_part_parameter( self, parameter: part.models.PartParameter, data: str - ) -> bool | None: + ) -> Optional[bool]: """Validate a parameter value. Arguments: diff --git a/src/backend/InvenTree/plugin/helpers.py b/src/backend/InvenTree/plugin/helpers.py index d3f45c4a1921..ddb0bf722f17 100644 --- a/src/backend/InvenTree/plugin/helpers.py +++ b/src/backend/InvenTree/plugin/helpers.py @@ -51,7 +51,7 @@ class MixinNotImplementedError(NotImplementedError): def log_error(error, reference: str = 'general'): """Log an plugin error.""" - from plugin import registry + from plugin.registry import registry # make sure the registry is set up if reference not in registry.errors: diff --git a/src/backend/InvenTree/plugin/models.py b/src/backend/InvenTree/plugin/models.py index f60056a47dac..4e40213b9a7e 100644 --- a/src/backend/InvenTree/plugin/models.py +++ b/src/backend/InvenTree/plugin/models.py @@ -2,6 +2,7 @@ import inspect import warnings +from typing import Optional from django.conf import settings from django.contrib import admin @@ -195,7 +196,7 @@ def is_package(self) -> bool: return getattr(self.plugin, 'is_package', False) @property - def admin_source(self) -> str | None: + def admin_source(self) -> Optional[str]: """Return the path to the javascript file which renders custom admin content for this plugin. - It is required that the file provides a 'renderPluginSettings' function! @@ -215,7 +216,7 @@ def admin_source(self) -> str | None: return None @property - def admin_context(self) -> dict | None: + def admin_context(self) -> Optional[dict]: """Return the context data for the admin integration.""" if not self.plugin: return None diff --git a/src/backend/InvenTree/plugin/plugin.py b/src/backend/InvenTree/plugin/plugin.py index c2bc6cbfe8d5..6aeb43c2c4a5 100644 --- a/src/backend/InvenTree/plugin/plugin.py +++ b/src/backend/InvenTree/plugin/plugin.py @@ -490,7 +490,7 @@ def plugin_static_file(self, *args): return url - def get_admin_source(self) -> str | None: + def get_admin_source(self) -> Union[str, None]: """Return a path to a JavaScript file which contains custom UI settings. The frontend code expects that this file provides a function named 'renderPluginSettings'. @@ -500,7 +500,7 @@ def get_admin_source(self) -> str | None: return self.plugin_static_file(self.ADMIN_SOURCE) - def get_admin_context(self) -> dict | None: + def get_admin_context(self) -> Union[dict, None]: """Return a context dictionary for the admin panel settings. This is an optional method which can be overridden by the plugin. diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 155ea5665e3c..89c0dce65a53 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -84,12 +84,12 @@ def __init__(self) -> None: ] = {} # List of all plugin instances # Keep an internal hash of the plugin registry state - self.registry_hash: str | None = None + self.registry_hash: Optional[str] = None self.plugin_modules: list[InvenTreePlugin] = [] # Holds all discovered plugins self.mixin_modules: dict[str, Any] = {} # Holds all discovered mixins - self.errors = {} # Holds discovering errors + self.errors: dict[str, list[Any]] = {} # Holds discovering errors self.loading_lock = Lock() # Lock to prevent multiple loading at the same time @@ -220,7 +220,7 @@ def call_plugin_function(self, slug: str, func: str, *args, **kwargs): # region registry functions def with_mixin( - self, mixin: str, active: bool | None = True, builtin: Optional[bool] = None + self, mixin: str, active: Optional[bool] = True, builtin: Optional[bool] = None ) -> list: """Returns reference to all plugins that have a specified mixin enabled. diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index c6c2b7b8f54c..bee0d6ba2976 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -7,6 +7,7 @@ import textwrap from datetime import datetime from pathlib import Path +from typing import Optional from unittest import mock from unittest.mock import patch @@ -14,7 +15,8 @@ from django.test import TestCase, override_settings import plugin.templatetags.plugin_extras as plugin_tags -from plugin import InvenTreePlugin, registry +from plugin import InvenTreePlugin +from plugin.registry import registry from plugin.samples.integration.another_sample import ( NoIntegrationPlugin, WrongIntegrationPlugin, @@ -285,16 +287,17 @@ def test_broken_samples(self): self.assertEqual(len(registry.errors), 2) # There should be at least one discovery error in the module `broken_file` - self.assertGreater(len(registry.errors.get('discovery') or []), 0) + self.assertGreater(len(registry.errors.get('discovery')), 0) self.assertEqual( - registry.errors.get('discovery')[0]['broken_file'], + registry.errors.get('discovery')[0]['broken_file'], # type: ignore[call-possibly-unbound-method] "name 'bb' is not defined", ) # There should be at least one load error with an intentional KeyError - self.assertGreater(len(registry.errors.get('init') or []), 0) + self.assertGreater(len(registry.errors.get('init')), 0) self.assertEqual( - registry.errors.get('init')[0]['broken_sample'], "'This is a dummy error'" + registry.errors.get('init')[0]['broken_sample'], # type: ignore[call-possibly-unbound-method] + "'This is a dummy error'", ) @override_settings(PLUGIN_TESTING=True, PLUGIN_TESTING_SETUP=True) @@ -339,7 +342,7 @@ class DummyCIPlugin(InvenTreePlugin): def create_plugin_file( version: str, enabled: bool = True, reload: bool = True - ) -> str | None: + ) -> Optional[str]: """Create a plugin file with the given version. Arguments: diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index f9468576bdbf..5bb4d40cd53a 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -438,7 +438,7 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: debug_mode = get_global_setting('REPORT_DEBUG_MODE', False) # Start with a default report name - report_name: str | None = None + report_name: Optional[str] = None report_plugins = registry.with_mixin(PluginMixinEnum.REPORT) diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 29d40d611c92..e8ded8aaa8fe 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -50,7 +50,7 @@ def filter_queryset(queryset: QuerySet, **kwargs) -> QuerySet: @register.simple_tag() -def filter_db_model(model_name: str, **kwargs) -> QuerySet | None: +def filter_db_model(model_name: str, **kwargs) -> Optional[QuerySet]: """Filter a database model based on the provided keyword arguments. Arguments: @@ -316,7 +316,7 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa @register.simple_tag() -def part_parameter(part: Part, parameter_name: str) -> str | None: +def part_parameter(part: Part, parameter_name: str) -> Optional[str]: """Return a PartParameter object for the given part and parameter name. Arguments: diff --git a/src/backend/InvenTree/stock/generators.py b/src/backend/InvenTree/stock/generators.py index b06682670a27..9c0a7aef6eb2 100644 --- a/src/backend/InvenTree/stock/generators.py +++ b/src/backend/InvenTree/stock/generators.py @@ -1,6 +1,7 @@ """Generator functions for the stock app.""" from inspect import signature +from typing import Optional from django.core.exceptions import ValidationError @@ -74,7 +75,7 @@ def generate_batch_code(**kwargs): return Template(batch_template).render(context) -def generate_serial_number(part=None, quantity=1, **kwargs) -> str | None: +def generate_serial_number(part=None, quantity=1, **kwargs) -> Optional[str]: """Generate a default 'serial number' for a new StockItem.""" quantity = quantity or 1 diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 530c727c2e05..b08a4f6e1880 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -593,7 +593,7 @@ def _create_serial_numbers(cls, serials: list, **kwargs) -> QuerySet: return StockItem.objects.filter(part=part, serial__in=serials) @staticmethod - def convert_serial_to_int(serial: str) -> int | None: + def convert_serial_to_int(serial: str) -> Optional[int]: """Convert the provided serial number to an integer value. This function hooks into the plugin system to allow for custom serial number conversion. @@ -1644,7 +1644,7 @@ def add_tracking_entry( self, entry_type: int, user: User, - deltas: dict | None = None, + deltas: Optional[dict] = None, notes: str = '', commit: bool = True, **kwargs, diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index 9cde4395e486..ed7c10df829c 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -1,6 +1,7 @@ """Database model definitions for the 'users' app.""" import datetime +from typing import Optional from django.conf import settings from django.contrib import admin @@ -224,7 +225,7 @@ class Meta: unique_together = (('name', 'group'),) @property - def label(self) -> str | None: + def label(self) -> Optional[str]: """Return the translated label for this ruleset.""" return dict(RULESET_CHOICES).get(self.name, self.name) From 4c3dae8127f369a2d4c2760c43773aa50221993f Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 00:07:00 +0200 Subject: [PATCH 10/33] ensure patch only runs if it is installed --- src/backend/InvenTree/InvenTree/settings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 48d9480ed7ec..6057b2a6bf21 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -20,7 +20,6 @@ from django.core.validators import URLValidator from django.http import Http404, HttpResponseGone -import django_stubs_ext import structlog from corsheaders.defaults import default_headers as default_cors_headers from dotenv import load_dotenv @@ -39,7 +38,13 @@ from . import config, locales -django_stubs_ext.monkeypatch() +try: + import django_stubs_ext + + django_stubs_ext.monkeypatch() # pragma: no cover +except ImportError: + pass + checkMinPythonVersion() INVENTREE_BASE_URL = 'https://inventree.org' From 0874c6cf2a9c95ef511efa0169a3c3f191e53956 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 00:07:16 +0200 Subject: [PATCH 11/33] add type check to qc --- .github/workflows/qc_checks.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index e332729b886f..d99905cdb6cd 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -96,6 +96,10 @@ jobs: run: | pip install --require-hashes -r contrib/dev_reqs/requirements.txt python3 .github/scripts/version_check.py + - name: Check types + run: | + pip install --require-hashes -r src/backend/requirements-dev.txt + ty check mkdocs: name: Style [Documentation] From 2629da2c16e1f953a29ec1f53d03be57657b8d43 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 00:22:28 +0200 Subject: [PATCH 12/33] more fixes --- pyproject.toml | 2 -- src/backend/InvenTree/InvenTree/conversion.py | 2 +- src/backend/InvenTree/InvenTree/helpers.py | 12 ++++++------ src/backend/InvenTree/InvenTree/tasks.py | 2 +- src/backend/InvenTree/machine/models.py | 4 ++-- src/backend/InvenTree/report/models.py | 5 +++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 145c31cdf713..4f2ee3552d9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,9 +113,7 @@ call-non-callable="ignore" invalid-return-type="ignore" invalid-argument-type="ignore" invalid-type-form="ignore" -invalid-assignment="ignore" possibly-unbound-attribute="ignore" -#call-possibly-unbound-method="ignore" unknown-argument="ignore" diff --git a/src/backend/InvenTree/InvenTree/conversion.py b/src/backend/InvenTree/InvenTree/conversion.py index 669fe950fff2..295a2e9fbc80 100644 --- a/src/backend/InvenTree/InvenTree/conversion.py +++ b/src/backend/InvenTree/InvenTree/conversion.py @@ -191,7 +191,7 @@ def convert_physical_value(value: str, unit: Optional[str] = None, strip_units=T attempts.append(f'{value}{unit}') attempts.append(f'{eng}{unit}') - value = None + value: Optional[str] = None # Run through the available "attempts", take the first successful result for attempt in attempts: diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index 54b62b1ac09e..30b495cf7ca8 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -951,7 +951,7 @@ def current_time(local=True): """ if settings.USE_TZ: now = timezone.now() - now = to_local_time(now, target_tz=server_timezone() if local else 'UTC') + now = to_local_time(now, target_tz_str=server_timezone() if local else 'UTC') return now else: return datetime.datetime.now() @@ -970,12 +970,12 @@ def server_timezone() -> str: return settings.TIME_ZONE -def to_local_time(time, target_tz: Optional[str] = None): +def to_local_time(time, target_tz_str: Optional[str] = None): """Convert the provided time object to the local timezone. Arguments: time: The time / date to convert - target_tz: The desired timezone (string) - defaults to server time + target_tz_str: The desired timezone (string) - defaults to server time Returns: A timezone aware datetime object, with the desired timezone @@ -999,11 +999,11 @@ def to_local_time(time, target_tz: Optional[str] = None): # Default to UTC if not provided source_tz = ZoneInfo('UTC') - if not target_tz: - target_tz = server_timezone() + if not target_tz_str: + target_tz_str = server_timezone() try: - target_tz = ZoneInfo(str(target_tz)) + target_tz = ZoneInfo(str(target_tz_str)) except ZoneInfoNotFoundError: target_tz = ZoneInfo('UTC') diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index f7b122a76655..db62fd6b8b34 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -288,7 +288,7 @@ class ScheduledTask: QUARTERLY: str = 'Q' YEARLY: str = 'Y' - TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY) + TYPE: list[str] = [MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] # noqa: RUF008 class TaskRegister: diff --git a/src/backend/InvenTree/machine/models.py b/src/backend/InvenTree/machine/models.py index 0e4852c64e47..e08f90550a2a 100755 --- a/src/backend/InvenTree/machine/models.py +++ b/src/backend/InvenTree/machine/models.py @@ -1,7 +1,7 @@ """Models for the machine app.""" import uuid -from typing import Literal +from typing import Literal, Optional from django.contrib import admin from django.db import models @@ -186,7 +186,7 @@ def get_setting_definition(cls, key, **kwargs): If not provided, we'll look at the machine registry to see what settings this machine driver requires """ if 'settings' not in kwargs: - machine_config: MachineConfig = kwargs.pop('machine_config', None) + machine_config: Optional[MachineConfig] = kwargs.pop('machine_config', None) if machine_config and machine_config.machine: config_type = kwargs.get('config_type') if config_type == cls.ConfigType.DRIVER: diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 5bb4d40cd53a..094a4422dd47 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -391,7 +391,8 @@ def get_report_size(self) -> str: def get_context(self, instance, request=None, **kwargs): """Supply context data to the report template for rendering.""" base_context = super().get_context(instance, request) - report_context: ReportContextExtension = { + + report_context: ReportContextExtension = { # type: ignore[invalid-assignment] 'page_size': self.get_report_size(), 'landscape': self.landscape, } @@ -602,7 +603,7 @@ def generate_page_style(self, **kwargs): def get_context(self, instance, request=None, **kwargs): """Supply context data to the label template for rendering.""" base_context = super().get_context(instance, request, **kwargs) - label_context: LabelContextExtension = { + label_context: LabelContextExtension = { # type: ignore[invalid-assignment] 'width': self.width, 'height': self.height, 'page_style': None, From 17c2fcda082e2cb316eecb7efd1287539d24959c Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 00:23:50 +0200 Subject: [PATCH 13/33] install all reqs --- .github/workflows/qc_checks.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index d99905cdb6cd..59f8fa87597f 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -98,7 +98,9 @@ jobs: python3 .github/scripts/version_check.py - name: Check types run: | + pip install --require-hashes -r src/backend/requirements.txt pip install --require-hashes -r src/backend/requirements-dev.txt + pip install --require-hashes -r contrib/container/requirements.txt ty check mkdocs: From 5773315576f7d3ff44c5c1768fc2f16b7cb3a822 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 00:30:50 +0200 Subject: [PATCH 14/33] fix more types --- pyproject.toml | 16 ++++++++-------- .../InvenTree/management/commands/schema.py | 4 ++-- src/backend/InvenTree/InvenTree/tasks.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4f2ee3552d9c..da885b9d2ce6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,14 +107,14 @@ src = [ "./", ] [tool.ty.rules] -unresolved-reference="ignore" -unresolved-attribute="ignore" -call-non-callable="ignore" -invalid-return-type="ignore" -invalid-argument-type="ignore" -invalid-type-form="ignore" -possibly-unbound-attribute="ignore" -unknown-argument="ignore" +unresolved-reference="ignore" # 24 +unresolved-attribute="ignore" # 33 +call-non-callable="ignore" # 3 +invalid-return-type="ignore" # 12 +invalid-argument-type="ignore" # 15 +invalid-type-form="ignore" # 5 +possibly-unbound-attribute="ignore" # 18 +unknown-argument="ignore" # 9 [tool.coverage.run] diff --git a/src/backend/InvenTree/InvenTree/management/commands/schema.py b/src/backend/InvenTree/InvenTree/management/commands/schema.py index badf5702adf5..22368c74c728 100644 --- a/src/backend/InvenTree/InvenTree/management/commands/schema.py +++ b/src/backend/InvenTree/InvenTree/management/commands/schema.py @@ -1,7 +1,7 @@ """Extended schema generator.""" from pathlib import Path -from typing import TypeVar +from typing import TypeVar, Union from django.conf import settings @@ -26,7 +26,7 @@ def prep_name(ref): return f'{dja_ref_prefix}.{ref}' -def sub_component_name(name: T) -> T | str: +def sub_component_name(name: T) -> Union[T, str]: """Clean up component references.""" if not isinstance(name, str): return name diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index db62fd6b8b34..cb102c3ec561 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -288,7 +288,7 @@ class ScheduledTask: QUARTERLY: str = 'Q' YEARLY: str = 'Y' - TYPE: list[str] = [MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] # noqa: RUF008 + TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY) # type: ignore[invalid-assignment] class TaskRegister: From 17ca478dad3e3782c344c28b497cdb5851918194 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 00:59:41 +0200 Subject: [PATCH 15/33] more fixes --- pyproject.toml | 2 +- .../InvenTree/InvenTree/helpers_model.py | 5 +++-- src/backend/InvenTree/build/test_api.py | 13 ++++++++++-- src/backend/InvenTree/order/test_api.py | 12 ++++++++--- .../InvenTree/report/templatetags/report.py | 20 ++++++++++--------- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da885b9d2ce6..4da212fbaeb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ call-non-callable="ignore" # 3 invalid-return-type="ignore" # 12 invalid-argument-type="ignore" # 15 invalid-type-form="ignore" # 5 -possibly-unbound-attribute="ignore" # 18 +possibly-unbound-attribute="ignore" # 7 unknown-argument="ignore" # 9 diff --git a/src/backend/InvenTree/InvenTree/helpers_model.py b/src/backend/InvenTree/InvenTree/helpers_model.py index b636c85ba163..62710407d8cb 100644 --- a/src/backend/InvenTree/InvenTree/helpers_model.py +++ b/src/backend/InvenTree/InvenTree/helpers_model.py @@ -329,8 +329,9 @@ def notify_users( 'template': {'subject': content.name.format(**content_context)}, } - if content.template: - context['template']['html'] = content.template.format(**content_context) + tmp = content.template + if tmp: + context['template']['html'] = tmp.format(**content_context) # Create notification trigger_notification( diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index a29d64143a22..49b961e445f4 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -1,6 +1,7 @@ """Unit tests for the BuildOrder API.""" from datetime import datetime, timedelta +from typing import Optional from django.urls import reverse @@ -664,6 +665,9 @@ def test_invalid_bom_item(self): wrong_line = line break + if not wrong_line: + raise self.fail('No matching BuildLine found for the given stock item') + data = self.post( self.url, { @@ -691,6 +695,9 @@ def test_valid_data(self): right_line = line break + if not right_line: + raise self.fail('No matching BuildLine found for the given stock item') + self.post( self.url, { @@ -718,11 +725,13 @@ def test_reallocate(self): # Find the correct BuildLine si = StockItem.objects.get(pk=2) - right_line = None + right_line: Optional[BuildLine] = None for line in self.build.build_lines.all(): if line.bom_item.sub_part.pk == si.part.pk: - right_line = line + right_line: BuildLine = line break + if not right_line: + raise self.fail('No matching BuildLine found for the given stock item') self.post( self.url, diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index e82a1ac76792..fce697618006 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -4,6 +4,7 @@ import io import json from datetime import date, datetime, timedelta +from typing import Optional from django.core.exceptions import ValidationError from django.db import connection @@ -2036,14 +2037,19 @@ def check_template(line_item): return line_item.part.is_template for line in filter(check_template, self.order.lines.all()): - stock_item = None + stock_item: Optional[StockItem] = None # Allocate a matching variant - parts = Part.objects.filter(salable=True).filter(variant_of=line.part.pk) + parts: list[Part] = Part.objects.filter(salable=True).filter( + variant_of=line.part.pk + ) for part in parts: - stock_item = part.stock_items.last() + stock_item: StockItem = part.stock_items.last() break + if stock_item is None: + raise self.fail('No stock item found for part') + # Fully-allocate each line data['items'].append({ 'line_item': line.pk, diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index e8ded8aaa8fe..a4118d904315 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -301,14 +301,13 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa if type(part) is not Part: raise TypeError(_('part_image tag requires a Part instance')) - if not part.image: + part_img = part.image + if not part_img: img = None elif preview: - img = None if not hasattr(part.image, 'preview') else part.image.preview.name + img = None if not hasattr(part.image, 'preview') else part_img.preview.name elif thumbnail: - img = ( - None if not hasattr(part.image, 'thumbnail') else part.image.thumbnail.name - ) + img = None if not hasattr(part.image, 'thumbnail') else part_img.thumbnail.name else: img = part.image.name @@ -348,12 +347,15 @@ def company_image( if type(company) is not Company: raise TypeError(_('company_image tag requires a Company instance')) - if preview: - img = company.image.preview.name + cmp_img = company.image + if not cmp_img: + img = None + elif preview: + img = cmp_img.preview.name elif thumbnail: - img = company.image.thumbnail.name + img = cmp_img.thumbnail.name else: - img = company.image.name + img = cmp_img.name return uploaded_image(img, **kwargs) From 9c65b1225b8391adaacf2adad75781d46f27175c Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 01:00:46 +0200 Subject: [PATCH 16/33] disable container stuff for now --- .github/workflows/qc_checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 59f8fa87597f..fdd70a8d4eee 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -100,7 +100,7 @@ jobs: run: | pip install --require-hashes -r src/backend/requirements.txt pip install --require-hashes -r src/backend/requirements-dev.txt - pip install --require-hashes -r contrib/container/requirements.txt + # pip install --require-hashes -r contrib/container/requirements.txt ty check mkdocs: From 831390e13c79017640115a9be121719018c88c6d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 01:07:15 +0200 Subject: [PATCH 17/33] move typecheck to seperate job --- .github/workflows/qc_checks.yaml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index fdd70a8d4eee..0686f2b04f7d 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -96,11 +96,25 @@ jobs: run: | pip install --require-hashes -r contrib/dev_reqs/requirements.txt python3 .github/scripts/version_check.py + + typecheck: + name: Style [Typecheck] + runs-on: ubuntu-24.04 + needs: [paths-filter, pre-commit] + if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true' + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 + with: + persist-credentials: false + - name: Environment Setup + uses: ./.github/actions/setup + with: + apt-dependency: gettext poppler-utils + dev-install: true + update: true - name: Check types run: | - pip install --require-hashes -r src/backend/requirements.txt - pip install --require-hashes -r src/backend/requirements-dev.txt - # pip install --require-hashes -r contrib/container/requirements.txt ty check mkdocs: From 7e1e67e7ad921cfca258e8f7f2884c57b94bf3f8 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 01:56:04 +0200 Subject: [PATCH 18/33] try to use putput for path --- .github/actions/setup/action.yaml | 6 ++++++ .github/workflows/qc_checks.yaml | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index ae42c5340c73..33bcc2521c2d 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -30,6 +30,10 @@ inputs: pip-dependency: required: false description: 'Extra python package for install.' +outputs: + python-path: + description: "Pyth to the python executable" + value: ${{ steps.setup-python.outputs.python-path }} runs: using: 'composite' @@ -42,6 +46,7 @@ runs: # Python installs - name: Set up Python ${{ env.python_version }} if: ${{ inputs.python == 'true' }} + id: setup-python uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # pin@v5.0.0 with: python-version: ${{ env.python_version }} @@ -51,6 +56,7 @@ runs: src/backend/requirements-dev.txt contrib/container/requirements.txt contrib/dev_reqs/requirements.txt + - name: Install Base Python Dependencies if: ${{ inputs.python == 'true' }} shell: bash diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 0686f2b04f7d..995cbf7df3aa 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -108,6 +108,7 @@ jobs: with: persist-credentials: false - name: Environment Setup + id: setup uses: ./.github/actions/setup with: apt-dependency: gettext poppler-utils @@ -115,7 +116,7 @@ jobs: update: true - name: Check types run: | - ty check + ty check -- python ${{ steps.setup.outputs.python-path }} mkdocs: name: Style [Documentation] From dd2984842be5aca03aeb6d3b337a5a0a97831008 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 02:11:30 +0200 Subject: [PATCH 19/33] use env instead --- .github/workflows/qc_checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 995cbf7df3aa..7e5d0ff3c6d1 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -116,7 +116,7 @@ jobs: update: true - name: Check types run: | - ty check -- python ${{ steps.setup.outputs.python-path }} + ty check -- python ${Python_ROOT_DIR}/bin/python3 mkdocs: name: Style [Documentation] From bba3ff165b2f58454e07fec8c2743c9c46df910d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 08:39:27 +0200 Subject: [PATCH 20/33] fix typo --- .github/workflows/qc_checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 7e5d0ff3c6d1..397fb200efd2 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -116,7 +116,7 @@ jobs: update: true - name: Check types run: | - ty check -- python ${Python_ROOT_DIR}/bin/python3 + ty check --python ${Python_ROOT_DIR}/bin/python3 mkdocs: name: Style [Documentation] From c494ff57075c85571f06229e7c95633ed2f94c68 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 08:58:09 +0200 Subject: [PATCH 21/33] add missing install --- .github/workflows/qc_checks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 397fb200efd2..52d968d4a919 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -116,6 +116,7 @@ jobs: update: true - name: Check types run: | + pip install django_auth_ldap ty check --python ${Python_ROOT_DIR}/bin/python3 mkdocs: From d4f75f3b227dcff0ec8dbe1cf14752bcd337a711 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 08:58:43 +0200 Subject: [PATCH 22/33] remove unclear imports - not sure why this was done --- src/backend/InvenTree/common/currency.py | 2 +- src/backend/InvenTree/part/models.py | 4 ++-- src/backend/InvenTree/users/serializers.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/common/currency.py b/src/backend/InvenTree/common/currency.py index b7749db39c21..d22e0bba82af 100644 --- a/src/backend/InvenTree/common/currency.py +++ b/src/backend/InvenTree/common/currency.py @@ -158,7 +158,7 @@ def get_price( - If MOQ (minimum order quantity) is required, bump quantity - If order multiples are to be observed, then we need to calculate based on that, too """ - from common.currency import currency_code_default + # from common.currency import currency_code_default if hasattr(instance, break_name): price_breaks = getattr(instance, break_name).all() diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 81f673daca28..cb0b738e5569 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -807,7 +807,7 @@ def validate_serial_number( if not check_duplicates: return - from part.models import Part + # from part.models import Part from stock.models import StockItem if get_global_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False): @@ -838,7 +838,7 @@ def validate_serial_number( def find_conflicting_serial_numbers(self, serials: list) -> list: """For a provided list of serials, return a list of those which are conflicting.""" - from part.models import Part + # from part.models import Part from stock.models import StockItem conflicts = [] diff --git a/src/backend/InvenTree/users/serializers.py b/src/backend/InvenTree/users/serializers.py index 5950fb286dd6..522d4ac649be 100644 --- a/src/backend/InvenTree/users/serializers.py +++ b/src/backend/InvenTree/users/serializers.py @@ -295,7 +295,7 @@ def get_permissions(self, group: Group) -> dict: class ExtendedUserSerializer(UserSerializer): """Serializer for a User with a bit more info.""" - from users.serializers import GroupSerializer + # from users.serializers import GroupSerializer class Meta(UserSerializer.Meta): """Metaclass defines serializer fields.""" From 171c040de79baeece96eb4bd10134c306a2e9561 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 09:00:52 +0200 Subject: [PATCH 23/33] add kwarg names --- src/backend/InvenTree/InvenTree/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/filters.py b/src/backend/InvenTree/InvenTree/filters.py index befdc8910833..2ca128281f0e 100644 --- a/src/backend/InvenTree/InvenTree/filters.py +++ b/src/backend/InvenTree/InvenTree/filters.py @@ -21,7 +21,7 @@ def filter(self, qs, value): if settings.USE_TZ and value is not None: tz = timezone.get_current_timezone() value = datetime(value.year, value.month, value.day) - value = make_aware(value, tz, True) + value = make_aware(value, timezone=tz, is_dst=True) return super().filter(qs, value) From 35a2a66031db6989d84d493ceb87e3f185450d68 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 09:27:29 +0200 Subject: [PATCH 24/33] fix introduced issue in url call --- src/backend/InvenTree/plugin/base/integration/APICallMixin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/plugin/base/integration/APICallMixin.py b/src/backend/InvenTree/plugin/base/integration/APICallMixin.py index 46e1d2b1ec53..85cd21825f83 100644 --- a/src/backend/InvenTree/plugin/base/integration/APICallMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/APICallMixin.py @@ -163,7 +163,8 @@ def api_call( url = f'{self.api_url}/{endpoint}' # build kwargs for call - kwargs.update({'url': url, 'headers': headers}) + kwargs.update({'headers': headers}) + kwargs.pop('url', None) if data and json: raise ValueError('You can either pass `data` or `json` to this function.') From d711d06277d2ace67c9e0e2377d47e8d49f23c45 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 13:35:40 +0200 Subject: [PATCH 25/33] ignore import --- .github/workflows/qc_checks.yaml | 1 - src/backend/InvenTree/InvenTree/settings.py | 2 +- src/backend/InvenTree/users/models.py | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 52d968d4a919..397fb200efd2 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -116,7 +116,6 @@ jobs: update: true - name: Check types run: | - pip install django_auth_ldap ty check --python ${Python_ROOT_DIR}/bin/python3 mkdocs: diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 6057b2a6bf21..c3429b054f87 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -380,7 +380,7 @@ # LDAP support LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False) if LDAP_AUTH: - import django_auth_ldap.config + import django_auth_ldap.config # type: ignore[unresolved-import] import ldap AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index ed7c10df829c..f843f99cf2b4 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -48,7 +48,9 @@ def user_model_str(self): if settings.LDAP_AUTH: - from django_auth_ldap.backend import populate_user + from django_auth_ldap.backend import ( # type: ignore[unresolved-import] + populate_user, + ) @receiver(populate_user) def create_email_address(user, **kwargs): From d9cf797772b91a1c74a94e2f707aaa868470a49e Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 22:02:39 +0200 Subject: [PATCH 26/33] fix broken typing changes --- src/backend/InvenTree/InvenTree/settings.py | 2 +- src/backend/InvenTree/build/api.py | 4 ++-- src/backend/InvenTree/order/api.py | 2 +- src/backend/InvenTree/stock/api.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index c3429b054f87..c17c71d4688c 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -381,7 +381,7 @@ LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False) if LDAP_AUTH: import django_auth_ldap.config # type: ignore[unresolved-import] - import ldap + import ldap # type: ignore[unresolved-import] AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 4c692fc6ba62..74bbddb62075 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -15,7 +15,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -import build.models +import build.models as build_models import build.serializers import common.models import part.models as part_models @@ -804,7 +804,7 @@ def filter_part(self, queryset, name, part): return queryset.filter(stock_item__part=part) build = rest_filters.ModelChoiceFilter( - queryset=build.models.Build.objects.all(), + queryset=build_models.Build.objects.all(), label=_('Build Order'), field_name='build_line__build', ) diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 409499b8106d..87248ac2c05f 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -11,7 +11,7 @@ from django.urls import include, path, re_path from django.utils.translation import gettext_lazy as _ -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters import rest_framework.serializers from django_filters.rest_framework.filterset import FilterSet from django_ical.views import ICalFeed diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 9c670281b078..5544ecbfb237 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -9,7 +9,7 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field From c47ba638c06f3be908c4637ef3a86081125920b8 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 16 May 2025 22:03:02 +0200 Subject: [PATCH 27/33] fix filter import --- src/backend/InvenTree/InvenTree/filters.py | 2 +- src/backend/InvenTree/build/api.py | 2 +- src/backend/InvenTree/common/api.py | 2 +- src/backend/InvenTree/company/api.py | 2 +- src/backend/InvenTree/part/api.py | 2 +- src/backend/InvenTree/plugin/api.py | 2 +- src/backend/InvenTree/report/api.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/filters.py b/src/backend/InvenTree/InvenTree/filters.py index 2ca128281f0e..d6906279fdaf 100644 --- a/src/backend/InvenTree/InvenTree/filters.py +++ b/src/backend/InvenTree/InvenTree/filters.py @@ -6,8 +6,8 @@ from django.utils import timezone from django.utils.timezone import make_aware -import django_filters.filters as rest_filters import django_filters.rest_framework.backends as drf_backend +import django_filters.rest_framework.filters as rest_filters from rest_framework import filters import InvenTree.helpers diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 74bbddb62075..f4202f921f29 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -9,7 +9,7 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import extend_schema_field from rest_framework import serializers diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 37458da31e9c..d49809bb7753 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -14,7 +14,7 @@ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import csrf_exempt -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters import django_q.models from django_filters.rest_framework.filterset import FilterSet from django_q.tasks import async_task diff --git a/src/backend/InvenTree/company/api.py b/src/backend/InvenTree/company/api.py index 28b2bbdb1c30..e6a1275ce1af 100644 --- a/src/backend/InvenTree/company/api.py +++ b/src/backend/InvenTree/company/api.py @@ -4,7 +4,7 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework.filterset import FilterSet import part.models diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index a8eed46c9f11..e23fd32f484a 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -8,7 +8,7 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.types import OpenApiTypes diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py index b42756c7fed6..b7fbcbe34853 100644 --- a/src/backend/InvenTree/plugin/api.py +++ b/src/backend/InvenTree/plugin/api.py @@ -6,7 +6,7 @@ from django.urls import include, path, re_path from django.utils.translation import gettext_lazy as _ -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import extend_schema diff --git a/src/backend/InvenTree/report/api.py b/src/backend/InvenTree/report/api.py index cfbf24f0cb6b..77adf2f80784 100644 --- a/src/backend/InvenTree/report/api.py +++ b/src/backend/InvenTree/report/api.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache -import django_filters.filters as rest_filters +import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework.filterset import FilterSet from rest_framework.generics import GenericAPIView From 2ce8406f8415097f57d2e9ede6f011727eb1beab Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 17 May 2025 01:05:25 +0200 Subject: [PATCH 28/33] reduce change set --- .github/actions/setup/action.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index 33bcc2521c2d..ae42c5340c73 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -30,10 +30,6 @@ inputs: pip-dependency: required: false description: 'Extra python package for install.' -outputs: - python-path: - description: "Pyth to the python executable" - value: ${{ steps.setup-python.outputs.python-path }} runs: using: 'composite' @@ -46,7 +42,6 @@ runs: # Python installs - name: Set up Python ${{ env.python_version }} if: ${{ inputs.python == 'true' }} - id: setup-python uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # pin@v5.0.0 with: python-version: ${{ env.python_version }} @@ -56,7 +51,6 @@ runs: src/backend/requirements-dev.txt contrib/container/requirements.txt contrib/dev_reqs/requirements.txt - - name: Install Base Python Dependencies if: ${{ inputs.python == 'true' }} shell: bash From ef5f4efe64df65e2317955f23dfa1f9a4d0fe8a0 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 17 May 2025 01:08:20 +0200 Subject: [PATCH 29/33] remove api-change --- src/backend/InvenTree/users/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index f843f99cf2b4..019891b8700f 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -1,7 +1,6 @@ """Database model definitions for the 'users' app.""" import datetime -from typing import Optional from django.conf import settings from django.contrib import admin @@ -227,7 +226,7 @@ class Meta: unique_together = (('name', 'group'),) @property - def label(self) -> Optional[str]: + def label(self) -> str: """Return the translated label for this ruleset.""" return dict(RULESET_CHOICES).get(self.name, self.name) From 6989c47ce7cec3dd0462f8ea40fe897b4309b9e9 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 17 May 2025 01:15:34 +0200 Subject: [PATCH 30/33] fix dict --- src/backend/InvenTree/InvenTree/serializers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index 9aa653c1af58..59cbb8153274 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -373,16 +373,15 @@ def run_validation(self, data=empty): instance.full_clean() except (ValidationError, DjangoValidationError) as exc: if hasattr(exc, 'message_dict'): - data = exc.message_dict + data = {**exc.message_dict} elif hasattr(exc, 'message'): data = {'non_field_errors': [str(exc.message)]} else: data = {'non_field_errors': [str(exc)]} # Change '__all__' key (django style) to 'non_field_errors' (DRF style) - if '__all__' in data: - data['non_field_errors'] = data['__all__'] - del data['__all__'] + if hasattr(data, '__all__'): + data['non_field_errors'] = data.pop('__all__') raise ValidationError(data) From b3610397d3b2006b1cc91d6e3e815303a539f4b6 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 17 May 2025 01:21:50 +0200 Subject: [PATCH 31/33] ignore typing errors --- src/backend/InvenTree/InvenTree/apps.py | 2 +- src/backend/InvenTree/InvenTree/settings.py | 2 +- src/backend/InvenTree/InvenTree/unit_test.py | 4 +++- src/backend/InvenTree/build/test_api.py | 12 +++++++++--- src/backend/InvenTree/order/test_api.py | 2 +- src/backend/InvenTree/plugin/registry.py | 2 +- src/backend/InvenTree/report/models.py | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/apps.py b/src/backend/InvenTree/InvenTree/apps.py index 29cc46aece51..720e4c566f40 100644 --- a/src/backend/InvenTree/InvenTree/apps.py +++ b/src/backend/InvenTree/InvenTree/apps.py @@ -135,7 +135,7 @@ def start_background_tasks(self): for task in tasks: if not task: - continue + continue # pragma: no cover ref_name = f'{task.func.__module__}.{task.func.__name__}' diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index c17c71d4688c..769b4880a9c1 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -42,7 +42,7 @@ import django_stubs_ext django_stubs_ext.monkeypatch() # pragma: no cover -except ImportError: +except ImportError: # pragma: no cover pass checkMinPythonVersion() diff --git a/src/backend/InvenTree/InvenTree/unit_test.py b/src/backend/InvenTree/InvenTree/unit_test.py index df59ea93998e..6777ee1270ca 100644 --- a/src/backend/InvenTree/InvenTree/unit_test.py +++ b/src/backend/InvenTree/InvenTree/unit_test.py @@ -519,7 +519,9 @@ def download_file( r'(attachment|inline); filename=[\'"]([\w\d\-.]+)[\'"]', disposition ) if not result: - raise ValueError('No filename match found in disposition') + raise ValueError( + 'No filename match found in disposition' + ) # pragma: no cover fn = result.groups()[1] diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index 49b961e445f4..1c118a775490 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -666,7 +666,9 @@ def test_invalid_bom_item(self): break if not wrong_line: - raise self.fail('No matching BuildLine found for the given stock item') + raise self.fail( + 'No matching BuildLine found for the given stock item' + ) # pragma: no cover data = self.post( self.url, @@ -696,7 +698,9 @@ def test_valid_data(self): break if not right_line: - raise self.fail('No matching BuildLine found for the given stock item') + raise self.fail( + 'No matching BuildLine found for the given stock item' + ) # pragma: no cover self.post( self.url, @@ -731,7 +735,9 @@ def test_reallocate(self): right_line: BuildLine = line break if not right_line: - raise self.fail('No matching BuildLine found for the given stock item') + raise self.fail( + 'No matching BuildLine found for the given stock item' + ) # pragma: no cover self.post( self.url, diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index fce697618006..d3ba68ce1358 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -2048,7 +2048,7 @@ def check_template(line_item): break if stock_item is None: - raise self.fail('No stock item found for part') + raise self.fail('No stock item found for part') # pragma: no cover # Fully-allocate each line data['items'].append({ diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 89c0dce65a53..8a6ed1fe4ba9 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -929,7 +929,7 @@ def _load_source(modname, filename): # loader = importlib.machinery.SourceFileLoader(modname, filename) spec = importlib.util.spec_from_file_location(modname, filename) # , loader=loader) if spec is None: - raise ImportError(f"Cannot find module '{modname}'") + raise ImportError(f"Cannot find module '{modname}'") # pragma: no cover module = importlib.util.module_from_spec(spec) sys.modules[module.__name__] = module diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 094a4422dd47..1bc97ab5d3fb 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -515,7 +515,7 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: }) if not report_name: - report_name = '' + report_name = '' # pragma: no cover if not report_name.endswith('.pdf'): report_name += '.pdf' From 972d003bb6bc0482802abbaa18832faf6e89ad67 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 17 May 2025 02:08:03 +0200 Subject: [PATCH 32/33] fix more type issues --- pyproject.toml | 11 ++- src/backend/InvenTree/InvenTree/cache.py | 5 +- src/backend/InvenTree/InvenTree/format.py | 2 +- src/backend/InvenTree/InvenTree/helpers.py | 4 +- src/backend/InvenTree/InvenTree/settings.py | 77 ++++++++++--------- src/backend/InvenTree/InvenTree/sso.py | 2 +- .../migrations/0019_auto_20200413_0642.py | 4 +- src/backend/InvenTree/data_exporter/mixins.py | 3 +- .../InvenTree/data_exporter/serializers.py | 3 +- src/backend/InvenTree/importer/models.py | 6 +- .../InvenTree/plugin/broken/broken_file.py | 2 +- src/backend/InvenTree/plugin/registry.py | 2 +- .../InvenTree/report/templatetags/barcode.py | 3 +- .../InvenTree/report/templatetags/report.py | 2 +- .../migrations/0061_auto_20210511_0911.py | 4 +- 15 files changed, 65 insertions(+), 65 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4da212fbaeb4..1418b38acd03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,14 +107,13 @@ src = [ "./", ] [tool.ty.rules] -unresolved-reference="ignore" # 24 -unresolved-attribute="ignore" # 33 -call-non-callable="ignore" # 3 -invalid-return-type="ignore" # 12 +unresolved-reference="ignore" # 24 # see https://github.com/astral-sh/ty/issues/220 +unresolved-attribute="ignore" # 33 # need Plugin Mixin typing +call-non-callable="ignore" # 3 ## +invalid-return-type="ignore" # 12 ## invalid-argument-type="ignore" # 15 -invalid-type-form="ignore" # 5 possibly-unbound-attribute="ignore" # 7 -unknown-argument="ignore" # 9 +unknown-argument="ignore" # 9 # need to wait for betterdjango field stubs [tool.coverage.run] diff --git a/src/backend/InvenTree/InvenTree/cache.py b/src/backend/InvenTree/InvenTree/cache.py index a217c94fe1cc..97beb96c36d7 100644 --- a/src/backend/InvenTree/InvenTree/cache.py +++ b/src/backend/InvenTree/InvenTree/cache.py @@ -2,6 +2,7 @@ import socket import threading +from typing import Any import structlog @@ -127,7 +128,7 @@ def delete_session_cache() -> None: del thread_data.request_cache -def get_session_cache(key: str) -> any: +def get_session_cache(key: str) -> Any: """Return a cached value from the session cache.""" # Only return a cached value if the request object is available too if not hasattr(thread_data, 'request'): @@ -139,7 +140,7 @@ def get_session_cache(key: str) -> any: return val -def set_session_cache(key: str, value: any) -> None: +def set_session_cache(key: str, value: Any) -> None: """Set a cached value in the session cache.""" # Only set a cached value if the request object is available too if not hasattr(thread_data, 'request'): diff --git a/src/backend/InvenTree/InvenTree/format.py b/src/backend/InvenTree/InvenTree/format.py index adf3f241e406..3ee674b216a9 100644 --- a/src/backend/InvenTree/InvenTree/format.py +++ b/src/backend/InvenTree/InvenTree/format.py @@ -107,7 +107,7 @@ def construct_format_regex(fmt_string: str) -> str: # Add a named capture group for the format entry if name: # Check if integer values are required - c = '\\d' if _fmt.endswith('d') else '.' + c = '\\d' if _fmt and _fmt.endswith('d') else '.' # Specify width # TODO: Introspect required width diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index 30b495cf7ca8..8271deb70de3 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -364,9 +364,7 @@ def increment(value): except ValueError: pass - number = number.zfill(width) - - return prefix + number + return prefix + str(number).zfill(width) def decimal2string(d): diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 769b4880a9c1..a62c997e4428 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -317,31 +317,31 @@ 'django_ical', # For exporting calendars ] -MIDDLEWARE = CONFIG.get( - 'middleware', - [ - 'django.middleware.security.SecurityMiddleware', - 'x_forwarded_for.middleware.XForwardedForMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'allauth.usersessions.middleware.UserSessionsMiddleware', # DB user sessions - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'InvenTree.middleware.InvenTreeRemoteUserMiddleware', # Remote / proxy auth - 'allauth.account.middleware.AccountMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'InvenTree.middleware.AuthRequiredMiddleware', - 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA - 'oauth2_provider.middleware.OAuth2TokenMiddleware', # oauth2_provider - 'maintenance_mode.middleware.MaintenanceModeMiddleware', - 'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting - 'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching - 'django_structlog.middlewares.RequestMiddleware', # Structured logging - ], +default_middleware = [ + 'django.middleware.security.SecurityMiddleware', + 'x_forwarded_for.middleware.XForwardedForMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'allauth.usersessions.middleware.UserSessionsMiddleware', # DB user sessions + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'InvenTree.middleware.InvenTreeRemoteUserMiddleware', # Remote / proxy auth + 'allauth.account.middleware.AccountMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'InvenTree.middleware.AuthRequiredMiddleware', + 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA + 'oauth2_provider.middleware.OAuth2TokenMiddleware', # oauth2_provider + 'maintenance_mode.middleware.MaintenanceModeMiddleware', + 'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting + 'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching + 'django_structlog.middlewares.RequestMiddleware', # Structured logging +] +MIDDLEWARE = ( + CONFIG.get('middleware', default_middleware) if CONFIG else default_middleware ) # In DEBUG mode, add support for django-querycount @@ -366,15 +366,18 @@ } -AUTHENTICATION_BACKENDS = CONFIG.get( - 'authentication_backends', - [ - 'oauth2_provider.backends.OAuth2Backend', # OAuth2 provider - 'django.contrib.auth.backends.RemoteUserBackend', # proxy login - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers - 'sesame.backends.ModelBackend', # Magic link login django-sesame - ], +default_auth_backends = [ + 'oauth2_provider.backends.OAuth2Backend', # OAuth2 provider + 'django.contrib.auth.backends.RemoteUserBackend', # proxy login + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers + 'sesame.backends.ModelBackend', # Magic link login django-sesame +] + +AUTHENTICATION_BACKENDS = ( + CONFIG.get('authentication_backends', default_auth_backends) + if CONFIG + else default_auth_backends ) # LDAP support @@ -434,7 +437,7 @@ ) AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch( get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'), - ldap.SCOPE_SUBTREE, + ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute] str( get_setting( 'INVENTREE_LDAP_SEARCH_FILTER_STR', @@ -470,7 +473,7 @@ ) AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch( get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'), - ldap.SCOPE_SUBTREE, + ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute] f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})', ) AUTH_LDAP_GROUP_TYPE_CLASS = get_setting( @@ -597,7 +600,7 @@ logger.debug('Configuring database backend:') # Extract database configuration from the config.yaml file -db_config = CONFIG.get('database', None) +db_config = CONFIG.get('database', None) if CONFIG else None if not db_config: db_config = {} diff --git a/src/backend/InvenTree/InvenTree/sso.py b/src/backend/InvenTree/InvenTree/sso.py index c1842d70de49..441a7a6fd912 100644 --- a/src/backend/InvenTree/InvenTree/sso.py +++ b/src/backend/InvenTree/InvenTree/sso.py @@ -50,7 +50,7 @@ def check_provider(provider): if not app: return False - if allauth.app_settings.SITES_ENABLED: + if allauth.app_settings.SITES_ENABLED: # type: ignore[unresolved-attribute] # At least one matching site must be specified if not app.sites.exists(): logger.error('SocialApp %s has no sites configured', app) diff --git a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py index 858215cf9547..0fc852354bcf 100644 --- a/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/src/backend/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -296,11 +296,11 @@ def map_part_to_manufacturer(part_id, idx, total): # Double-check if the typed name corresponds to an existing item elif response in companies.keys(): - link_part(part, companies[response]) + link_part(part_id, companies[response]) return elif response in links.keys(): - link_part(part, links[response]) + link_part(part_id, links[response]) return # No match, create a new manufacturer diff --git a/src/backend/InvenTree/data_exporter/mixins.py b/src/backend/InvenTree/data_exporter/mixins.py index 8274e68ee0e3..374599739b53 100644 --- a/src/backend/InvenTree/data_exporter/mixins.py +++ b/src/backend/InvenTree/data_exporter/mixins.py @@ -1,6 +1,7 @@ """Mixin classes for the exporter app.""" from collections import OrderedDict +from typing import Any from django.core.exceptions import ValidationError from django.core.files.base import ContentFile @@ -127,7 +128,7 @@ def arrange_export_headers(cls, headers: list) -> list: """ return headers - def get_nested_value(self, row: dict, key: str) -> any: + def get_nested_value(self, row: dict, key: str) -> Any: """Get a nested value from a dictionary. This method allows for dot notation to access nested fields. diff --git a/src/backend/InvenTree/data_exporter/serializers.py b/src/backend/InvenTree/data_exporter/serializers.py index d5f90fe48f7a..815f8fe683ec 100644 --- a/src/backend/InvenTree/data_exporter/serializers.py +++ b/src/backend/InvenTree/data_exporter/serializers.py @@ -6,7 +6,6 @@ import InvenTree.exceptions import InvenTree.helpers -import InvenTree.serializers from plugin import PluginMixinEnum, registry @@ -53,7 +52,7 @@ def __init__(self, *args, **kwargs): try: supports_export = plugin.supports_export( model_class, - user=request.user, + user=request.user if request else None, serializer_class=serializer_class, view_class=view_class, ) diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index c634f741cdd9..b7bdcd81bfdc 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -294,9 +294,7 @@ def import_data(self) -> None: if not any(row_data.values()): continue - row = importer.models.DataImportRow( - session=self, row_data=row_data, row_index=idx - ) + row = DataImportRow(session=self, row_data=row_data, row_index=idx) row.extract_data( field_mapping=field_mapping, @@ -308,7 +306,7 @@ def import_data(self) -> None: imported_rows.append(row) # Perform database writes as a single operation - importer.models.DataImportRow.objects.bulk_create(imported_rows) + DataImportRow.objects.bulk_create(imported_rows) # Mark the import task as "PROCESSING" self.status = DataImportStatusCode.PROCESSING.value diff --git a/src/backend/InvenTree/plugin/broken/broken_file.py b/src/backend/InvenTree/plugin/broken/broken_file.py index f56932e876c4..83437025cf47 100644 --- a/src/backend/InvenTree/plugin/broken/broken_file.py +++ b/src/backend/InvenTree/plugin/broken/broken_file.py @@ -7,4 +7,4 @@ class BrokenFileIntegrationPlugin(InvenTreePlugin): """An very broken plugin.""" -aaa = bb # noqa: F821 +aaa = bb # noqa: F821 # type: ignore[unresolved-reference] diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 8a6ed1fe4ba9..7abd27c5a4c2 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -655,7 +655,7 @@ def _init_plugins(self): logger.exception( '[PLUGIN] Encountered an error with %s:\n%s', - error.path, + getattr(error, 'path', None), str(error), ) diff --git a/src/backend/InvenTree/report/templatetags/barcode.py b/src/backend/InvenTree/report/templatetags/barcode.py index f02c3e8bd2bf..9a87adb3b3fe 100644 --- a/src/backend/InvenTree/report/templatetags/barcode.py +++ b/src/backend/InvenTree/report/templatetags/barcode.py @@ -4,6 +4,7 @@ from django.utils.safestring import mark_safe import barcode as python_barcode +import barcode.writer as python_barcode_writer import qrcode.constants as ECL from PIL import Image, ImageColor from qrcode.main import QRCode @@ -122,7 +123,7 @@ def barcode(data: str, barcode_class='code128', **kwargs) -> str: data = str(data).zfill(constructor.digits) - writer = python_barcode.writer.ImageWriter + writer = python_barcode_writer.ImageWriter barcode_image = constructor(data, writer=writer()) diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index a4118d904315..5c7973730212 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -102,7 +102,7 @@ def getindex(container: list, index: int) -> Any: @register.simple_tag() -def getkey(container: dict, key: str, backup_value: Optional[any] = None) -> Any: +def getkey(container: dict, key: str, backup_value: Optional[Any] = None) -> Any: """Perform key lookup in the provided dict object. This function is provided to get around template rendering limitations. diff --git a/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py index ba44e9c1bd0b..e5d2b823999a 100644 --- a/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py +++ b/src/backend/InvenTree/stock/migrations/0061_auto_20210511_0911.py @@ -51,10 +51,10 @@ def update_history(apps, schema_editor): q = entry.quantity - if idx == 0 or not q == quantity: + if idx == 0 or q != quantity: try: - deltas['quantity']: float(q) + deltas['quantity']= float(q) updated = True except Exception: print(f"WARNING: Error converting quantity '{q}'") From 8f1fb15f57c9addc149e682589a07d1dab21f76f Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 17 May 2025 02:12:57 +0200 Subject: [PATCH 33/33] ignore errors --- src/backend/InvenTree/plugin/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 7abd27c5a4c2..92bca831c807 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -610,9 +610,9 @@ def safe_reference(plugin, key: str, active: bool = True): f"Plugin '{p}' is not compatible with the current InvenTree version {v}" ) if v := plg_i.MIN_VERSION: - _msg += _(f'Plugin requires at least version {v}') + _msg += _(f'Plugin requires at least version {v}') # type: ignore[unsupported-operator] if v := plg_i.MAX_VERSION: - _msg += _(f'Plugin requires at most version {v}') + _msg += _(f'Plugin requires at most version {v}') # type: ignore[unsupported-operator] # Log to error stack log_error(_msg, reference='init') else: