From 16103379c9fa2504bcbf3c1fcf4fc7a8d322886e Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 18 Mar 2026 08:25:50 +0100 Subject: [PATCH] chore(backend): Bump ty (#11537) * bump ty - there is better django support now * more fixes * fix usage of types * add missing type * fix access * ensure itteration is safe * fix uncombat decimal usage * ?potential breaking: change access key * remove now obsolete igtnore * ignore errors on StdImageField * remove usage of unkonw parameter * fix diff error * fix schema creation * fix coverage quirk * those are unneeded now? * this seem to have been an issue with 3.12; not occuring on 3.14 * ignore pydantiics * ignore edge cases for now * include isGenerating fix * make typing python 3.11 compatible * fix more errors --- pyproject.toml | 6 +-- src/backend/InvenTree/InvenTree/helpers.py | 4 +- .../InvenTree/InvenTree/helpers_email.py | 3 +- .../management/commands/migrate_icons.py | 2 +- src/backend/InvenTree/InvenTree/models.py | 4 +- .../InvenTree/InvenTree/permissions.py | 2 +- src/backend/InvenTree/InvenTree/settings.py | 4 +- src/backend/InvenTree/InvenTree/sso.py | 2 +- src/backend/InvenTree/InvenTree/tasks.py | 2 +- src/backend/InvenTree/InvenTree/test_api.py | 2 +- src/backend/InvenTree/InvenTree/tests.py | 4 +- src/backend/InvenTree/InvenTree/tracing.py | 2 +- src/backend/InvenTree/InvenTree/version.py | 6 ++- src/backend/InvenTree/build/api.py | 1 + src/backend/InvenTree/build/models.py | 15 ++++---- src/backend/InvenTree/common/currency.py | 2 +- src/backend/InvenTree/common/models.py | 6 +-- src/backend/InvenTree/common/serializers.py | 6 +-- src/backend/InvenTree/common/setting/type.py | 2 + src/backend/InvenTree/company/models.py | 9 ++++- .../InvenTree/generic/states/fields.py | 6 +-- src/backend/InvenTree/importer/models.py | 4 +- .../machine/machine_types/label_printer.py | 2 +- src/backend/InvenTree/machine/serializers.py | 2 +- src/backend/InvenTree/machine/tests.py | 4 +- src/backend/InvenTree/order/models.py | 24 +++++++----- src/backend/InvenTree/part/models.py | 6 +-- src/backend/InvenTree/part/tasks.py | 1 + .../InvenTree/part/test_notification_stale.py | 6 +-- .../InvenTree/plugin/base/barcodes/mixins.py | 2 +- .../InvenTree/plugin/base/supplier/mixins.py | 2 +- .../InvenTree/plugin/base/ui/mixins.py | 2 +- .../InvenTree/plugin/broken/broken_file.py | 2 +- src/backend/InvenTree/plugin/helpers.py | 2 +- src/backend/InvenTree/plugin/plugin.py | 14 +++---- src/backend/InvenTree/plugin/registry.py | 4 +- .../samples/supplier/supplier_sample.py | 2 +- src/backend/InvenTree/plugin/test_plugin.py | 2 +- src/backend/InvenTree/report/models.py | 2 +- src/backend/requirements-dev.txt | 37 +++++++++---------- 40 files changed, 114 insertions(+), 96 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3d3fa6a7eb..9ce50f3710 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,13 +108,13 @@ root = ["src/backend/InvenTree"] unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220 unresolved-attribute="ignore" # 505 # need Plugin Mixin typing call-non-callable="ignore" # 8 ## -invalid-argument-type="ignore" # 49 invalid-assignment="ignore" # 17 # need to wait for better django field stubs -invalid-method-override="ignore" +invalid-method-override="ignore" # 99 invalid-return-type="ignore" # 22 ## possibly-missing-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164 unknown-argument="ignore" # 3 # need to wait for better django field stubs -no-matching-overload="ignore" # 3 # need to wait for better django field stubs +invalid-argument-type="ignore" # 49 +no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs [tool.coverage.run] source = ["src/backend/InvenTree", "InvenTree"] diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index efa4d355c6..73d83a3c90 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -199,7 +199,7 @@ def regenerate_imagefile(_file, _name: str): _name: Name of the variation (e.g. 'thumbnail', 'preview') """ name = _file.field.attr_class.get_variation_name(_file.name, _name) - return ImageFieldFile(_file.instance, _file, name) # type: ignore + return ImageFieldFile(_file.instance, _file, name) # ty:ignore[too-many-positional-arguments] def image2name(img_obj: StdImageField, do_preview: bool, do_thumbnail: bool): @@ -311,7 +311,7 @@ def TestIfImageURL(url): ] -def str2bool(text, test=True): +def str2bool(text, test=True) -> bool: """Test if a string 'looks' like a boolean value. Args: diff --git a/src/backend/InvenTree/InvenTree/helpers_email.py b/src/backend/InvenTree/InvenTree/helpers_email.py index de90c0d323..f6dca007f2 100644 --- a/src/backend/InvenTree/InvenTree/helpers_email.py +++ b/src/backend/InvenTree/InvenTree/helpers_email.py @@ -8,6 +8,7 @@ import structlog from allauth.account.models import EmailAddress import InvenTree.ready +import InvenTree.tasks as tasks from common.models import Priority, issue_mail logger = structlog.get_logger('inventree') @@ -98,7 +99,7 @@ def send_email( ) return False, 'INVE-W7: no from_email or DEFAULT_FROM_EMAIL specified' - InvenTree.tasks.offload_task( + tasks.offload_task( issue_mail, subject=subject, body=body, diff --git a/src/backend/InvenTree/InvenTree/management/commands/migrate_icons.py b/src/backend/InvenTree/InvenTree/management/commands/migrate_icons.py index 1ff5e6fd62..ee8456fe4f 100644 --- a/src/backend/InvenTree/InvenTree/management/commands/migrate_icons.py +++ b/src/backend/InvenTree/InvenTree/management/commands/migrate_icons.py @@ -99,7 +99,7 @@ class Command(BaseCommand): if kwargs['include_items']: icons[icon]['items'].append({ 'model': model.__name__.lower(), - 'id': item.id, # type: ignore + 'id': item.id, }) self.stdout.write(f'Writing icon map for {len(icons.keys())} icons') diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index 10c6a6b709..07ef3ded71 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -1329,7 +1329,7 @@ class InvenTreeBarcodeMixin(models.Model): @classmethod def lookup_barcode(cls, barcode_hash: str) -> models.Model: """Check if a model instance exists with the specified third-party barcode hash.""" - return cls.objects.filter(barcode_hash=barcode_hash).first() + return cls.objects.filter(barcode_hash=barcode_hash).first() # ty:ignore[invalid-return-type] def assign_barcode( self, @@ -1485,7 +1485,7 @@ class InvenTreeImageMixin(models.Model): def rename_image(self, filename): """Rename the uploaded image file using the IMAGE_RENAME function.""" - return self.IMAGE_RENAME(filename) # type: ignore + return self.IMAGE_RENAME(filename) image = StdImageField( upload_to=rename_image, diff --git a/src/backend/InvenTree/InvenTree/permissions.py b/src/backend/InvenTree/InvenTree/permissions.py index 07006870c5..47af7dabcc 100644 --- a/src/backend/InvenTree/InvenTree/permissions.py +++ b/src/backend/InvenTree/InvenTree/permissions.py @@ -376,7 +376,7 @@ def auth_exempt(view_func): def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) - wrapped_view.auth_exempt = True # type:ignore[unresolved-attribute] + wrapped_view.auth_exempt = True return wraps(view_func)(wrapped_view) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index b8fa0189dc..83dfe37f6b 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -483,7 +483,7 @@ if LDAP_AUTH: # pragma: no cover ) AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch( get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'), - ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute] + ldap.SCOPE_SUBTREE, str( get_setting( 'INVENTREE_LDAP_SEARCH_FILTER_STR', @@ -519,7 +519,7 @@ if LDAP_AUTH: # pragma: no cover ) AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch( get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'), - ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute] + ldap.SCOPE_SUBTREE, f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})', ) AUTH_LDAP_GROUP_TYPE_CLASS = get_setting( diff --git a/src/backend/InvenTree/InvenTree/sso.py b/src/backend/InvenTree/InvenTree/sso.py index 05bacaf38d..d04abf21ba 100644 --- a/src/backend/InvenTree/InvenTree/sso.py +++ b/src/backend/InvenTree/InvenTree/sso.py @@ -49,7 +49,7 @@ def check_provider(provider): if not app: return False - if allauth.app_settings.SITES_ENABLED: # type: ignore[unresolved-attribute] + if allauth.app_settings.SITES_ENABLED: # 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/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index 31c75b9c8f..9b29a68da1 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -330,7 +330,7 @@ class ScheduledTask: QUARTERLY: str = 'Q' YEARLY: str = 'Y' - TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY) # type: ignore[invalid-assignment] + TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY) class TaskRegister: diff --git a/src/backend/InvenTree/InvenTree/test_api.py b/src/backend/InvenTree/InvenTree/test_api.py index 7aeacb6751..55da6ae0f3 100644 --- a/src/backend/InvenTree/InvenTree/test_api.py +++ b/src/backend/InvenTree/InvenTree/test_api.py @@ -574,7 +574,7 @@ class GeneralApiTests(InvenTreeAPITestCase): self.assertIn('License file not found at', str(log.output)) - with TemporaryDirectory() as tmp: # type: ignore[no-matching-overload] + with TemporaryDirectory() as tmp: sample_file = Path(tmp, 'temp.txt') sample_file.write_text('abc', 'utf-8') diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index a28aad7313..c8a999b563 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -703,12 +703,12 @@ class TestHelpers(TestCase): """Test getMediaUrl.""" # Str should not work with self.assertRaises(TypeError): - helpers.getMediaUrl('xx/yy.png') # type: ignore + helpers.getMediaUrl('xx/yy.png') # Correct usage part = Part().image self.assertEqual( - helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # type: ignore + helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # ty:ignore[too-many-positional-arguments] '/media/xx/yy.png', ) diff --git a/src/backend/InvenTree/InvenTree/tracing.py b/src/backend/InvenTree/InvenTree/tracing.py index 4cedbb584d..f97da6c0eb 100644 --- a/src/backend/InvenTree/InvenTree/tracing.py +++ b/src/backend/InvenTree/InvenTree/tracing.py @@ -4,7 +4,7 @@ import base64 import logging from typing import Optional -from opentelemetry import metrics, trace # type: ignore[import] +from opentelemetry import metrics, trace 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/version.py b/src/backend/InvenTree/InvenTree/version.py index b66f834e06..2f77ab765b 100644 --- a/src/backend/InvenTree/InvenTree/version.py +++ b/src/backend/InvenTree/InvenTree/version.py @@ -289,8 +289,10 @@ def inventreeBranch(): return ' '.join(branch.splitlines()) if main_branch is None: - return None - return main_branch.decode('utf-8') + return None # pragma: no cover - branch information may not be available in all environments + return main_branch.decode( + 'utf-8' + ) # pragma: no cover - branch information may not be available in all environments def inventreeTarget(): diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 36ebbd15b0..232b7adca7 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -17,6 +17,7 @@ from rest_framework.response import Response import build.models as build_models import build.serializers import common.models +import common.serializers import part.models as part_models import stock.models as stock_models import stock.serializers diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 7222c95534..39018c661a 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -1,7 +1,7 @@ """Build database model definitions.""" import decimal -from typing import Optional +from typing import Optional, TypedDict from django.contrib.auth.models import User from django.core.exceptions import ValidationError @@ -50,7 +50,7 @@ from stock.status_codes import StockHistoryCode, StockStatus logger = structlog.get_logger('inventree') -class BuildReportContext(report.mixins.BaseReportContext): +class BuildReportContext(report.mixins.BaseReportContext, TypedDict): """Context for the Build model. Attributes: @@ -1039,7 +1039,7 @@ class Build( lines = lines.exclude(bom_item__consumable=True) lines = lines.annotate(allocated=annotate_allocated_quantity()) - for build_line in lines: # type: ignore[non-iterable] + for build_line in lines: reduce_by = build_line.allocated - build_line.quantity if reduce_by <= 0: @@ -1512,10 +1512,10 @@ class Build( unallocated_quantity -= quantity except (ValidationError, serializers.ValidationError) as exc: - # Catch model errors and re-throw as DRF errors + # Re-raise with a Django-compatible validation payload raise ValidationError( - exc.message, detail=serializers.as_serializer_error(exc) - ) + serializers.as_serializer_error(exc) + ) from exc if unallocated_quantity <= 0: # We have now fully-allocated this BomItem - no need to continue! @@ -1695,7 +1695,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs): instance.update_build_line_items() -class BuildLineReportContext(report.mixins.BaseReportContext): +class BuildLineReportContext(report.mixins.BaseReportContext, TypedDict): """Context for the BuildLine model. Attributes: @@ -1745,6 +1745,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo """Return the API URL used to access this model.""" return reverse('api-build-line-list') + # type def report_context(self) -> BuildLineReportContext: """Generate custom report context for this BuildLine object.""" return { diff --git a/src/backend/InvenTree/common/currency.py b/src/backend/InvenTree/common/currency.py index 87d9f59faa..adb3b55cff 100644 --- a/src/backend/InvenTree/common/currency.py +++ b/src/backend/InvenTree/common/currency.py @@ -230,6 +230,6 @@ def get_price( quantity = decimal.Decimal(f'{quantity}') if pb_found: - cost = pb_cost * quantity + cost = decimal.Decimal(pb_cost) * quantity return InvenTree.helpers.normalize(cost + instance.base_cost) return None diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index a2246343a4..42bd00f119 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -85,8 +85,8 @@ class RenderMeta(enums.ChoicesType): return [] -class RenderChoices(models.TextChoices, metaclass=RenderMeta): # type: ignore - """Class for creating enumerated string choices for schema rendering.""" +class RenderChoices(models.TextChoices, metaclass=RenderMeta): + """Class for creating enumerated string choices for schema rendering.""" # ty:ignore[conflicting-metaclass] class MetaMixin(models.Model): @@ -1084,7 +1084,7 @@ class BaseInvenTreeSetting(models.Model): return self.__class__.validator_is_bool(validator) - def as_bool(self): + def as_bool(self) -> bool: """Return the value of this setting converted to a boolean value. Warning: Only use on values where is_bool evaluates to true! diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index 2cb3473e15..574c45f265 100644 --- a/src/backend/InvenTree/common/serializers.py +++ b/src/backend/InvenTree/common/serializers.py @@ -42,7 +42,7 @@ class SettingsValueField(serializers.Field): """Return the object instance, not the attribute value.""" return instance - def to_representation(self, instance: common_models.InvenTreeSetting) -> str: + def to_representation(self, instance: common_models.InvenTreeSetting): """Return the value of the setting. Protected settings are returned as '***' @@ -381,7 +381,7 @@ class ConfigSerializer(serializers.Serializer): """Return the configuration data as a dictionary.""" if not isinstance(instance, str): instance = list(instance.keys())[0] - return {'key': instance, **self.instance[instance]} + return {'key': instance, **self.instance.get(instance)} class NotesImageSerializer(InvenTreeModelSerializer): @@ -457,7 +457,7 @@ class FlagSerializer(serializers.Serializer): data = {'key': instance, 'state': flag_state(instance, request=request)} if request and request.user.is_superuser: - data['conditions'] = self.instance[instance] + data['conditions'] = self.instance.get(instance) return data diff --git a/src/backend/InvenTree/common/setting/type.py b/src/backend/InvenTree/common/setting/type.py index 1971533663..0e788c017f 100644 --- a/src/backend/InvenTree/common/setting/type.py +++ b/src/backend/InvenTree/common/setting/type.py @@ -26,6 +26,7 @@ class SettingsKeyType(TypedDict, total=False): validator: Validation function/list of functions for the setting (optional, default: None, e.g: bool, int, str, MinValueValidator, ...) default: Default value or function that returns default value (optional) choices: Function that returns or value of list[tuple[str: key, str: display value]] (optional) + model_filters: Filters to apply when querying the associated model (optional) hidden: Hide this setting from settings page (optional) before_save: Function that gets called after save with *args, **kwargs (optional) after_save: Function that gets called after save with *args, **kwargs (optional) @@ -42,6 +43,7 @@ class SettingsKeyType(TypedDict, total=False): validator: Callable | list[Callable] | tuple[Callable] default: Callable | Any choices: list[tuple[str, str]] | Callable[[], list[tuple[str, str]]] + model_filters: dict[str, Any] hidden: bool before_save: Callable[..., None] after_save: Callable[..., None] diff --git a/src/backend/InvenTree/company/models.py b/src/backend/InvenTree/company/models.py index 90aeb23ae8..431f7ae11b 100644 --- a/src/backend/InvenTree/company/models.py +++ b/src/backend/InvenTree/company/models.py @@ -2,6 +2,7 @@ import os from decimal import Decimal +from typing import TypedDict from django.apps import apps from django.conf import settings @@ -53,7 +54,7 @@ def rename_company_image(instance, filename): return os.path.join(base, fn) -class CompanyReportContext(report.mixins.BaseReportContext): +class CompanyReportContext(report.mixins.BaseReportContext, TypedDict): """Report context for the Company model. Attributes: @@ -243,7 +244,11 @@ class Company( # We may have a pre-fetched primary address list if hasattr(self, 'primary_address_list'): addresses = self.primary_address_list - return addresses[0] if len(addresses) > 0 else None + return ( + addresses[0] + if len(addresses) > 0 and isinstance(addresses, list) + else None + ) # Otherwise, query the database return self.addresses.filter(primary=True).first() diff --git a/src/backend/InvenTree/generic/states/fields.py b/src/backend/InvenTree/generic/states/fields.py index 9ffa81d8eb..0ca677b4ab 100644 --- a/src/backend/InvenTree/generic/states/fields.py +++ b/src/backend/InvenTree/generic/states/fields.py @@ -194,7 +194,7 @@ class InvenTreeCustomStatusSerializerMixin: """Ensure the custom field is updated if the leader was changed.""" self.gather_custom_fields() # Mirror values from leader to follower - for field in self._custom_fields_leader: + for field in self._custom_fields_leader or []: follower_field_name = f'{field}_custom_key' if ( field in self.initial_data @@ -205,7 +205,7 @@ class InvenTreeCustomStatusSerializerMixin: setattr(self.instance, follower_field_name, self.initial_data[field]) # Mirror values from follower to leader - for field in self._custom_fields_follower: + for field in self._custom_fields_follower or []: leader_field_name = field.replace('_custom_key', '') if field in validated_data and leader_field_name not in self.initial_data: try: @@ -276,7 +276,7 @@ class InvenTreeCustomStatusSerializerMixin: # Inherit choices from leader self.gather_custom_fields() - if field_name in self._custom_fields: + if self._custom_fields and field_name in self._custom_fields: leader_field_name = field_name.replace('_custom_key', '') leader_field = self.fields[leader_field_name] if hasattr(leader_field, 'choices'): diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index c27c6e32ff..76f5d8dcc9 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -152,7 +152,7 @@ class DataImportSession(models.Model): return supported_models().get(self.model_type, None) - def get_related_model(self, field_name: str) -> models.Model: + def get_related_model(self, field_name: str) -> Optional[models.Model]: """Return the related model for a given field name. Arguments: @@ -699,7 +699,7 @@ class DataImportRow(models.Model): if commit: self.save() - def convert_date_field(self, value: str) -> str: + def convert_date_field(self, value: str) -> Optional[str]: """Convert an incoming date field to the correct format for the database.""" if value in [None, '']: return None diff --git a/src/backend/InvenTree/machine/machine_types/label_printer.py b/src/backend/InvenTree/machine/machine_types/label_printer.py index f2499187b3..f598f5dd55 100644 --- a/src/backend/InvenTree/machine/machine_types/label_printer.py +++ b/src/backend/InvenTree/machine/machine_types/label_printer.py @@ -111,7 +111,7 @@ class LabelPrinterBaseDriver(BaseDriver): Returns: A class instance of a DRF serializer class, by default this an instance of self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver """ - return self.PrintingOptionsSerializer(*args, **kwargs) # type: ignore + return self.PrintingOptionsSerializer(*args, **kwargs) # --- helper functions @property diff --git a/src/backend/InvenTree/machine/serializers.py b/src/backend/InvenTree/machine/serializers.py index 669ffe0b75..9bfe97a749 100644 --- a/src/backend/InvenTree/machine/serializers.py +++ b/src/backend/InvenTree/machine/serializers.py @@ -138,7 +138,7 @@ class MachineSettingSerializer(GenericReferencedSettingSerializer): """Custom init method to make the config_type field read only.""" super().__init__(*args, **kwargs) - self.Meta.read_only_fields = ['config_type'] # type: ignore + self.Meta.read_only_fields = ['config_type'] class BaseMachineClassSerializer(serializers.Serializer): diff --git a/src/backend/InvenTree/machine/tests.py b/src/backend/InvenTree/machine/tests.py index 99f9decc35..120ab004b9 100755 --- a/src/backend/InvenTree/machine/tests.py +++ b/src/backend/InvenTree/machine/tests.py @@ -232,10 +232,10 @@ class TestLabelPrinterMachineType(InvenTreeAPITestCase): machine = self.create_machine() # setup the label app - apps.get_app_config('report').create_default_labels() # type: ignore + apps.get_app_config('report').create_default_labels() plg_registry.reload_plugins() - config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) # type: ignore + config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) config.active = True config.save() diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index b2b72a5b5a..2d6f93c932 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1,7 +1,7 @@ """Order model definitions.""" from decimal import Decimal -from typing import Any, Optional +from typing import Any, Optional, TypedDict from django.contrib.auth.models import User from django.core.exceptions import ValidationError @@ -179,7 +179,7 @@ class TotalPriceMixin(models.Model): return total -class BaseOrderReportContext(report.mixins.BaseReportContext): +class BaseOrderReportContext(report.mixins.BaseReportContext, TypedDict): """Base context for all order models. Attributes: @@ -199,7 +199,7 @@ class BaseOrderReportContext(report.mixins.BaseReportContext): title: str -class PurchaseOrderReportContext(report.mixins.BaseReportContext): +class PurchaseOrderReportContext(report.mixins.BaseReportContext, TypedDict): """Context for the purchase order model. Attributes: @@ -221,7 +221,7 @@ class PurchaseOrderReportContext(report.mixins.BaseReportContext): supplier: Optional[Company] -class SalesOrderReportContext(report.mixins.BaseReportContext): +class SalesOrderReportContext(report.mixins.BaseReportContext, TypedDict): """Context for the sales order model. Attributes: @@ -243,7 +243,7 @@ class SalesOrderReportContext(report.mixins.BaseReportContext): customer: Optional[Company] -class ReturnOrderReportContext(report.mixins.BaseReportContext): +class ReturnOrderReportContext(report.mixins.BaseReportContext, TypedDict): """Context for the return order model. Attributes: @@ -570,7 +570,9 @@ class PurchaseOrder(TotalPriceMixin, Order): def report_context(self) -> PurchaseOrderReportContext: """Return report context data for this PurchaseOrder.""" - return {**super().report_context(), 'supplier': self.supplier} + return_ctx = super().report_context() + return_ctx.update({'supplier': self.supplier}) + return return_ctx def get_absolute_url(self) -> str: """Get the 'web' URL for this order.""" @@ -1269,7 +1271,9 @@ class SalesOrder(TotalPriceMixin, Order): def report_context(self) -> SalesOrderReportContext: """Generate report context data for this SalesOrder.""" - return {**super().report_context(), 'customer': self.customer} + return_ctx = super().report_context() + return_ctx.update({'customer': self.customer}) + return return_ctx def get_absolute_url(self) -> str: """Get the 'web' URL for this order.""" @@ -2197,7 +2201,7 @@ class SalesOrderLineItem(OrderLineItem): return self.shipped >= self.quantity -class SalesOrderShipmentReportContext(report.mixins.BaseReportContext): +class SalesOrderShipmentReportContext(report.mixins.BaseReportContext, TypedDict): """Context for the SalesOrderShipment model. Attributes: @@ -2662,7 +2666,9 @@ class ReturnOrder(TotalPriceMixin, Order): def report_context(self) -> ReturnOrderReportContext: """Generate report context data for this ReturnOrder.""" - return {**super().report_context(), 'customer': self.customer} + return_ctx = super().report_context() + return_ctx.update({'customer': self.customer}) + return return_ctx def get_absolute_url(self): """Get the 'web' URL for this order.""" diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index fabc7cc89e..e4e50f4675 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -10,7 +10,7 @@ import os import re from datetime import timedelta from decimal import Decimal, InvalidOperation -from typing import cast +from typing import TypedDict, cast from django.conf import settings from django.contrib.auth.models import User @@ -427,7 +427,7 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): ) -class PartReportContext(report.mixins.BaseReportContext): +class PartReportContext(report.mixins.BaseReportContext, TypedDict): """Report context for the Part model. Attributes: @@ -4216,7 +4216,7 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel): if n <= 0: return 0.0 - return int(available_stock / n) + return int(Decimal(available_stock) / n) def get_required_quantity(self, build_quantity: float) -> float: """Calculate the required part quantity, based on the supplied build_quantity. diff --git a/src/backend/InvenTree/part/tasks.py b/src/backend/InvenTree/part/tasks.py index 87bc6a2f84..296604422b 100644 --- a/src/backend/InvenTree/part/tasks.py +++ b/src/backend/InvenTree/part/tasks.py @@ -12,6 +12,7 @@ from opentelemetry import trace import common.currency import common.notifications +import InvenTree.helpers import InvenTree.helpers_model from common.settings import get_global_setting from InvenTree.tasks import ( diff --git a/src/backend/InvenTree/part/test_notification_stale.py b/src/backend/InvenTree/part/test_notification_stale.py index 86a4189a42..7999ab6ed5 100644 --- a/src/backend/InvenTree/part/test_notification_stale.py +++ b/src/backend/InvenTree/part/test_notification_stale.py @@ -50,8 +50,8 @@ class StaleStockNotificationTests(InvenTreeTestCase): set_global_setting('STOCK_STALE_DAYS', 7, self.user) # Clear notifications - NotificationEntry.objects.all().delete() # type: ignore[attr-defined] - NotificationMessage.objects.all().delete() # type: ignore[attr-defined] + NotificationEntry.objects.all().delete() + NotificationMessage.objects.all().delete() def create_stock_items_with_expiry(self): """Create stock items with various expiry dates for testing.""" @@ -101,7 +101,7 @@ class StaleStockNotificationTests(InvenTreeTestCase): part.tasks.notify_stale_stock(self.user, []) # No notifications should be created - self.assertEqual(NotificationMessage.objects.count(), 0) # type: ignore[attr-defined] + self.assertEqual(NotificationMessage.objects.count(), 0) def test_notify_stale_stock_single_item(self): """Test notify_stale_stock with a single stale item.""" diff --git a/src/backend/InvenTree/plugin/base/barcodes/mixins.py b/src/backend/InvenTree/plugin/base/barcodes/mixins.py index 99c4661f57..c744cc04f2 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/mixins.py +++ b/src/backend/InvenTree/plugin/base/barcodes/mixins.py @@ -60,7 +60,7 @@ class BarcodeMixin: """Does this plugin support barcode generation.""" try: # Attempt to call the generate method - self.generate(None) # type: ignore + self.generate(None) except NotImplementedError: # If a NotImplementedError is raised, then barcode generation is not supported return False diff --git a/src/backend/InvenTree/plugin/base/supplier/mixins.py b/src/backend/InvenTree/plugin/base/supplier/mixins.py index 27c35d4d47..56e924f061 100644 --- a/src/backend/InvenTree/plugin/base/supplier/mixins.py +++ b/src/backend/InvenTree/plugin/base/supplier/mixins.py @@ -161,7 +161,7 @@ class SupplierMixin(SettingsMixin, Generic[PartData]): # assign parent_part to root_part if root_part has no variant of already if root_part and not root_part.is_template and not root_part.variant_of: - root_part.variant_of = parent_part # type: ignore + root_part.variant_of = parent_part root_part.save() return parent_part diff --git a/src/backend/InvenTree/plugin/base/ui/mixins.py b/src/backend/InvenTree/plugin/base/ui/mixins.py index cbffd1fb9a..4ab84ef6ca 100644 --- a/src/backend/InvenTree/plugin/base/ui/mixins.py +++ b/src/backend/InvenTree/plugin/base/ui/mixins.py @@ -83,7 +83,7 @@ class UserInterfaceMixin: def __init__(self): """Register mixin.""" super().__init__() - self.add_mixin(PluginMixinEnum.USER_INTERFACE, True, __class__) # type: ignore + self.add_mixin(PluginMixinEnum.USER_INTERFACE, True, __class__) def get_ui_features( self, feature_type: FeatureType, context: dict, request: Request, **kwargs diff --git a/src/backend/InvenTree/plugin/broken/broken_file.py b/src/backend/InvenTree/plugin/broken/broken_file.py index 83437025cf..f56932e876 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 # type: ignore[unresolved-reference] +aaa = bb # noqa: F821 diff --git a/src/backend/InvenTree/plugin/helpers.py b/src/backend/InvenTree/plugin/helpers.py index cd62e849f7..06de01d06b 100644 --- a/src/backend/InvenTree/plugin/helpers.py +++ b/src/backend/InvenTree/plugin/helpers.py @@ -192,7 +192,7 @@ def get_modules(pkg, path=None): if sys.version_info < (3, 12): module = finder.find_module(name).load_module(name) else: - spec = finder.find_spec(name) + spec = finder.find_spec(name) # type: ignore[missing-argument] module = module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) diff --git a/src/backend/InvenTree/plugin/plugin.py b/src/backend/InvenTree/plugin/plugin.py index 558ca3edf8..d5c58a0c86 100644 --- a/src/backend/InvenTree/plugin/plugin.py +++ b/src/backend/InvenTree/plugin/plugin.py @@ -3,7 +3,7 @@ import inspect import warnings from datetime import datetime -from distutils.sysconfig import get_python_lib # type: ignore[import] +from distutils.sysconfig import get_python_lib # type: ignore[unresolved-import] from importlib.metadata import PackageNotFoundError, metadata from pathlib import Path from typing import Optional @@ -92,16 +92,16 @@ class MetaBase: TITLE = None @mark_final - def get_meta_value(self, key: str, old_key: Optional[str] = None, __default=None): + def get_meta_value(self, key: str, old_key: Optional[str] = None, default=None): """Reference a meta item with a key. Args: key (str): key for the value old_key (str, optional): deprecated key - will throw warning - __default (optional): Value if nothing with key can be found. Defaults to None. + default (optional): Value if nothing with key can be found. Defaults to None. Returns: - Value referenced with key, old_key or __default if set and not value found + Value referenced with key, old_key or default if set and not value found """ value = getattr(self, key, None) @@ -117,9 +117,9 @@ class MetaBase: stacklevel=2, ) - # Use __default if still nothing set - if (value is None) and __default: - return __default + # Use default if still nothing set + if value is None and default is not None: + return default return value @mark_final diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 47f43d997f..af4bf49d02 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -781,9 +781,9 @@ class PluginsRegistry: 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}') # type: ignore[unsupported-operator] + _msg += _(f'Plugin requires at least version {v}') # ty:ignore[unsupported-operator] if v := plg_i.MAX_VERSION: - _msg += _(f'Plugin requires at most version {v}') # type: ignore[unsupported-operator] + _msg += _(f'Plugin requires at most version {v}') # ty:ignore[unsupported-operator] # Log to error stack log_registry_error(_msg, reference=f'{p}:init_plugin') else: diff --git a/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py b/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py index c27476f83e..4ea7d761b8 100644 --- a/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py +++ b/src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py @@ -152,7 +152,7 @@ class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin): # after the template part was created, we need to refresh the part from the db because its tree id may have changed # which results in an error if saved directly part.refresh_from_db() - part.variant_of = parent_part # type: ignore + part.variant_of = parent_part part.save() return part diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index cdabd46477..3f57cf06ac 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -254,7 +254,7 @@ class RegistryTests(TestQueryMixin, PluginRegistryMixin, TestCase): 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: # type: ignore[no-matching-overload] + with tempfile.TemporaryDirectory() as tmp: # Fill directory with sample data new_dir = Path(tmp).joinpath('mock') shutil.copytree(self.mockDir(), new_dir) diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 677500ea3b..8ab0168899 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -704,7 +704,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase): 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 = { # type: ignore[invalid-assignment] + label_context: LabelContextExtension = { 'width': self.width, 'height': self.height, 'page_style': None, diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index fb4fbab55d..b70dca94c1 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -710,25 +710,24 @@ tomli==2.4.0 \ --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 # via coverage -ty==0.0.1a21 \ - --hash=sha256:0efba2e52b58f536f4198ba5c4a36cac2ba67d83ec6f429ebc7704233bcda4c3 \ - --hash=sha256:1474d883129bb63da3b2380fc7ead824cd3baf6a9551e6aa476ffefc58057af3 \ - --hash=sha256:1f276ceab23a1410aec09508248c76ae0989c67fb7a0c287e0d4564994295531 \ - --hash=sha256:218d53e7919e885bd98e9196d9cb952d82178b299aa36da6f7f39333eb7400ed \ - --hash=sha256:21f708d02b6588323ffdbfdba38830dd0ecfd626db50aa6006b296b5470e52f9 \ - --hash=sha256:334d2a212ebf42a0e55d57561926af7679fe1e878175e11dcb81ad8df892844e \ - --hash=sha256:3c3bc66fcae41eff133cfe326dd65d82567a2fb5d4efe2128773b10ec2766819 \ - --hash=sha256:5dfc73299d441cc6454e36ed0a976877415024143dfca6592dc36f7701424383 \ - --hash=sha256:7505aeb8bf2a62f00f12cfa496f6c965074d75c8126268776565284c8a12d5dd \ - --hash=sha256:84243455f295ed850bd53f7089819321807d4e6ee3b1cbff6086137ae0259466 \ - --hash=sha256:87a200c21e02962e8a27374d9d152582331d57d709672431be58f4f898bf6cad \ - --hash=sha256:9463cac96b8f1bb5ba740fe1d42cd6bd152b43c5b159b2f07f8fd629bcdded34 \ - --hash=sha256:a8c769987d00fbc33054ff7e342633f475ea10dc43bc60fb9fb056159d48cb90 \ - --hash=sha256:ba13d03b9e095216ceb4e4d554a308517f28ab0a6e4dcd07cfe94563e4c2c489 \ - --hash=sha256:be8f457d7841b7ead2a3f6b65ba668abc172a1150a0f1f6c0958af3725dbb61a \ - --hash=sha256:cc0880ec344fbdf736b05d8d0da01f0caaaa02409bd9a24b68d18d0127a79b0e \ - --hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \ - --hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a +ty==0.0.21 \ + --hash=sha256:210e7568c9f886c4d01308d751949ee714ad7ad9d7d928d2ba90d329dd880367 \ + --hash=sha256:53508e345b11569f78b21ba8e2b4e61df38a9754947fb3cd9f2ef574367338fb \ + --hash=sha256:553e43571f4a35604c36cfd07d8b61a5eb7a714e3c67f8c4ff2cf674fefbaef9 \ + --hash=sha256:56b01fd2519637a4ca88344f61c96225f540c98ff18bca321d4eaa7bb0f7aa2f \ + --hash=sha256:56d3b198b64dd0a19b2b66e257deaed2ecea568e722ae5352f3c6fb62027f89d \ + --hash=sha256:62f7f5b235c4f7876db305c36997aea07b7af29b1a068f373d0e2547e25f32ff \ + --hash=sha256:666f6822e3b9200abfa7e95eb0ddd576460adb8d66b550c0ad2c70abc84a2048 \ + --hash=sha256:7bdf2f572378de78e1f388d24691c89db51b7caf07cf90f2bfcc1d6b18b70a76 \ + --hash=sha256:7e9613994610431ab8625025bd2880dbcb77c5c9fabdd21134cda12d840a529d \ + --hash=sha256:a0854d008347ce4a5fb351af132f660a390ab2a1163444d075251d43e6f74b9b \ + --hash=sha256:a4c2ba5d67d64df8fcdefd8b280ac1149d24a73dbda82fa953a0dff9d21400ed \ + --hash=sha256:a709d576e5bea84b745d43058d8b9cd4f27f74a0b24acb4b0cbb7d3d41e0d050 \ + --hash=sha256:bef3ab4c7b966bcc276a8ac6c11b63ba222d21355b48d471ea782c4104eee4e0 \ + --hash=sha256:d23d2c34f7a77d974bb08f0860ef700addc8a683d81a0319f71c08f87506cfd0 \ + --hash=sha256:e9de7e11c63c6afc40f3e9ba716374add171aee7fabc70b5146a510705c6d41b \ + --hash=sha256:ee8399f7c453a425291e6688efe430cfae7ab0ac4ffd50eba9f872bf878b54f6 \ + --hash=sha256:f72047996598ac20553fb7e21ba5741e3c82dee4e9eadf10d954551a5fe09391 # via -r src/backend/requirements-dev.in types-psycopg2==2.9.21.20260223 \ --hash=sha256:78ed70de2e56bc6b5c26c8c1da8e9af54e49fdc3c94d1504609f3519e2b84f02 \