Skip to content

feat(backend): add typechecking with ty #9664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 36 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4d073a3
Add ty for type checking
matmair May 12, 2025
777eb12
fix various typing issues
matmair May 13, 2025
d8d27c2
fix req
matmair May 13, 2025
7a82efb
more fixes
matmair May 14, 2025
9c399be
and more types
matmair May 14, 2025
d217f7a
and more typing
matmair May 15, 2025
7a4d714
fix imports
matmair May 15, 2025
5475249
more fixes
matmair May 15, 2025
7d7f9f7
fix types and optional statements
matmair May 15, 2025
4c3dae8
ensure patch only runs if it is installed
matmair May 15, 2025
0874c6c
add type check to qc
matmair May 15, 2025
7ab0799
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair May 15, 2025
2629da2
more fixes
matmair May 15, 2025
17c2fcd
install all reqs
matmair May 15, 2025
5773315
fix more types
matmair May 15, 2025
17ca478
more fixes
matmair May 15, 2025
9c65b12
disable container stuff for now
matmair May 15, 2025
831390e
move typecheck to seperate job
matmair May 15, 2025
7e1e67e
try to use putput for path
matmair May 15, 2025
dd29848
use env instead
matmair May 16, 2025
bba3ff1
fix typo
matmair May 16, 2025
c494ff5
add missing install
matmair May 16, 2025
d4f75f3
remove unclear imports - not sure why this was done
matmair May 16, 2025
171c040
add kwarg names
matmair May 16, 2025
35a2a66
fix introduced issue in url call
matmair May 16, 2025
d711d06
ignore import
matmair May 16, 2025
d9cf797
fix broken typing changes
matmair May 16, 2025
c47ba63
fix filter import
matmair May 16, 2025
2ce8406
reduce change set
matmair May 16, 2025
ef5f4ef
remove api-change
matmair May 16, 2025
6989c47
fix dict
matmair May 16, 2025
b361039
ignore typing errors
matmair May 16, 2025
972d003
fix more type issues
matmair May 17, 2025
8f1fb15
ignore errors
matmair May 17, 2025
efc9b10
Merge branch 'master' into feat(backend)--Add-typechecking-with-ty
matmair May 19, 2025
7bd98cd
Merge branch 'master' into feat(backend)--Add-typechecking-with-ty
matmair May 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/actions/setup/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 }}
Expand All @@ -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
Expand Down
30 changes: 20 additions & 10 deletions .github/scripts/version_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import re
import sys
from pathlib import Path
from typing import Optional

import requests

Expand Down Expand Up @@ -48,7 +49,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

Expand Down Expand Up @@ -94,11 +95,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()
Expand All @@ -111,7 +113,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']
Expand Down Expand Up @@ -142,7 +144,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]

Expand All @@ -164,16 +166,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: Optional[list[str]] = None

if GITHUB_REF_TYPE == 'tag':
# GITHUB_REF should be of the form /refs/heads/<tag>
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
return True

docker_tags = [version_tag, 'stable'] if highest_release else [version_tag]

Expand All @@ -187,11 +189,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}'")
Expand All @@ -208,3 +210,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)
21 changes: 21 additions & 0 deletions .github/workflows/qc_checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,27 @@
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
id: setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils
dev-install: true
update: true
- name: Check types
run: |
ty check -- python ${{ steps.setup.outputs.python-path }}

mkdocs:
name: Style [Documentation]
runs-on: ubuntu-24.04
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def check_link(url) -> bool:
return False


def get_build_environment() -> str:
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':
Expand Down
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ python-version = "3.9.2"
no-strip-extras=true
generate-hashes=true

[tool.ty]
src = [
"src/backend/InvenTree",
"./",
]
[tool.ty.rules]
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" # 7
unknown-argument="ignore" # 9


[tool.coverage.run]
source = ["src/backend/InvenTree", "InvenTree"]
dynamic_context = "test_function"
Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/InvenTree/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/backend/InvenTree/InvenTree/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/backend/InvenTree/InvenTree/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import shutil
import string
from pathlib import Path
from typing import Union

logger = logging.getLogger('inventree')
CONFIG_DATA = None
Expand Down Expand Up @@ -113,7 +114,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) -> Union[map, None]:
"""Load configuration data from the config file.

Arguments:
Expand Down
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/backend/InvenTree/InvenTree/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
11 changes: 6 additions & 5 deletions src/backend/InvenTree/InvenTree/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 10 additions & 8 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -123,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: 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.
Expand Down Expand Up @@ -949,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()
Expand All @@ -968,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
Expand All @@ -997,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')

Expand Down
4 changes: 3 additions & 1 deletion src/backend/InvenTree/InvenTree/helpers_email.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -91,7 +93,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) -> Optional[str]:
"""Find an email address for the specified user."""
# First check if the user has an associated email address
if user.email:
Expand Down
6 changes: 4 additions & 2 deletions src/backend/InvenTree/InvenTree/helpers_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -328,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(
Expand Down
Loading
Loading