소스 검색

Closes #17733: Replace `pycodestyle` with `ruff` (#17734)

* Resolve F541 errors

* Resolve F841 errors

* Resolve F811 errors

* Resolve F901 errors

* Resolve E714 errors

* Ignore F821 errors for GraphQL mixins

* Replace pycodestyle with ruff

* Move ignores to ruff.toml
Jeremy Stretch 1 년 전
부모
커밋
7ac6dff96d
65개의 변경된 파일166개의 추가작업 그리고 197개의 파일을 삭제
  1. 2 2
      .github/workflows/ci.yml
  2. 2 2
      docs/development/getting-started.md
  3. 16 16
      docs/development/style-guide.md
  4. 1 1
      netbox/circuits/api/nested_serializers.py
  5. 1 1
      netbox/circuits/tests/test_views.py
  6. 1 1
      netbox/core/api/nested_serializers.py
  7. 1 1
      netbox/core/data_backends.py
  8. 1 1
      netbox/core/graphql/mixins.py
  9. 2 2
      netbox/core/management/commands/syncdatasource.py
  10. 1 1
      netbox/core/models/data.py
  11. 2 2
      netbox/core/models/jobs.py
  12. 1 1
      netbox/dcim/api/nested_serializers.py
  13. 2 2
      netbox/dcim/forms/object_create.py
  14. 20 20
      netbox/dcim/graphql/mixins.py
  15. 1 1
      netbox/dcim/management/commands/trace_paths.py
  16. 0 1
      netbox/dcim/models/device_component_templates.py
  17. 1 2
      netbox/dcim/tables/devicetypes.py
  18. 2 2
      netbox/dcim/tests/test_api.py
  19. 0 7
      netbox/dcim/tests/test_filtersets.py
  20. 26 28
      netbox/dcim/tests/test_models.py
  21. 1 1
      netbox/dcim/tests/test_views.py
  22. 1 1
      netbox/extras/api/nested_serializers.py
  23. 1 1
      netbox/extras/jobs.py
  24. 2 2
      netbox/extras/management/commands/housekeeping.py
  25. 2 2
      netbox/extras/management/commands/reindex.py
  26. 1 1
      netbox/extras/management/commands/runscript.py
  27. 1 1
      netbox/extras/models/customfields.py
  28. 1 1
      netbox/extras/scripts.py
  29. 1 1
      netbox/extras/tests/test_customvalidators.py
  30. 4 4
      netbox/extras/tests/test_models.py
  31. 1 1
      netbox/extras/tests/test_views.py
  32. 1 1
      netbox/ipam/api/nested_serializers.py
  33. 3 3
      netbox/ipam/api/views.py
  34. 2 2
      netbox/ipam/graphql/mixins.py
  35. 0 2
      netbox/ipam/tests/test_api.py
  36. 4 4
      netbox/ipam/tests/test_views.py
  37. 1 1
      netbox/netbox/config/__init__.py
  38. 1 1
      netbox/netbox/data_backends.py
  39. 16 16
      netbox/netbox/navigation/menu.py
  40. 1 1
      netbox/netbox/settings.py
  41. 1 1
      netbox/netbox/staging.py
  42. 2 2
      netbox/netbox/tests/dummy_plugin/views.py
  43. 1 1
      netbox/netbox/tests/test_authentication.py
  44. 0 1
      netbox/netbox/tests/test_import.py
  45. 1 1
      netbox/tenancy/api/nested_serializers.py
  46. 1 1
      netbox/tenancy/graphql/mixins.py
  47. 1 1
      netbox/users/api/nested_serializers.py
  48. 1 1
      netbox/users/api/views.py
  49. 0 1
      netbox/users/forms/model_forms.py
  50. 2 2
      netbox/users/tests/test_preferences.py
  51. 1 1
      netbox/utilities/forms/fields/dynamic.py
  52. 1 1
      netbox/utilities/html.py
  53. 2 2
      netbox/utilities/tests/test_api.py
  54. 1 1
      netbox/utilities/tests/test_counters.py
  55. 1 1
      netbox/virtualization/api/nested_serializers.py
  56. 7 7
      netbox/virtualization/tests/test_views.py
  57. 2 2
      netbox/virtualization/views.py
  58. 1 1
      netbox/vpn/api/nested_serializers.py
  59. 0 11
      netbox/vpn/filtersets.py
  60. 0 7
      netbox/vpn/tests/test_filtersets.py
  61. 4 4
      netbox/vpn/tests/test_views.py
  62. 1 1
      netbox/wireless/api/nested_serializers.py
  63. 3 3
      netbox/wireless/tests/test_views.py
  64. 2 0
      ruff.toml
  65. 2 2
      scripts/git-hooks/pre-commit

+ 2 - 2
.github/workflows/ci.yml

@@ -73,7 +73,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r requirements.txt
-        pip install pycodestyle coverage tblib
+        pip install ruff coverage tblib
 
     - name: Build documentation
       run: mkdocs build
@@ -85,7 +85,7 @@ jobs:
       run: python netbox/manage.py makemigrations --check
 
     - name: Check PEP8 compliance
-      run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
+      run: ruff check netbox/
 
     - name: Check UI ESLint, TypeScript, and Prettier Compliance
       run: yarn --cwd netbox/project-static validate

+ 2 - 2
docs/development/getting-started.md

@@ -70,10 +70,10 @@ NetBox ships with a [git pre-commit hook](https://githooks.com/) script that aut
 cd .git/hooks/
 ln -s ../../scripts/git-hooks/pre-commit
 ```
-For the pre-commit hooks to work, you will also need to install the pycodestyle package:
+For the pre-commit hooks to work, you will also need to install the [ruff](https://docs.astral.sh/ruff/) linter:
 
 ```no-highlight
-python -m pip install pycodestyle
+python -m pip install ruff
 ```
 ...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
 

+ 16 - 16
docs/development/style-guide.md

@@ -1,6 +1,6 @@
 # Style Guide
 
-NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
+NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [ruff](https://docs.astral.sh/ruff/) is used for linting (with certain [exceptions](#linter-exceptions)).
 
 ## Code
 
@@ -20,32 +20,32 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
 
 * Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
 
-### PEP 8 Exceptions
+### Linting
 
-NetBox ignores certain PEP8 assertions. These are listed below.
+The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `ruff` manually, run:
 
-#### Wildcard Imports
+```
+ruff check netbox/
+```
 
-Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
+#### Linter Exceptions
 
-* The library being import contains only constant declarations (e.g. `constants.py`)
-* The library being imported explicitly defines `__all__`
+The following rules are ignored when linting.
 
-#### Maximum Line Length (E501)
+##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
 
-NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
+NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
 
-#### Line Breaks Following Binary Operators (W504)
+##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
 
-Line breaks are permitted following binary operators.
+Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
 
-### Enforcing Code Style
+* The library being import contains only constant declarations (e.g. `constants.py`)
+* The library being imported explicitly defines `__all__`
 
-The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
+##### [F405](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage/): Undefined local with import star usage
 
-```
-pycodestyle --ignore=W504,E501 netbox/
-```
+The justification for ignoring this rule is the same as F403 above.
 
 ### Introducing New Dependencies
 

+ 1 - 1
netbox/circuits/api/nested_serializers.py

@@ -18,7 +18,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 1 - 1
netbox/circuits/tests/test_views.py

@@ -171,7 +171,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         )
 
         cls.csv_update_data = (
-            f"id,cid,description,status",
+            "id,cid,description,status",
             f"{circuits[0].pk},Circuit 7,New description7,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
             f"{circuits[1].pk},Circuit 8,New description8,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
             f"{circuits[2].pk},Circuit 9,New description9,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",

+ 1 - 1
netbox/core/api/nested_serializers.py

@@ -16,7 +16,7 @@ __all__ = (
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 1 - 1
netbox/core/data_backends.py

@@ -34,7 +34,7 @@ class LocalBackend(DataBackend):
 
     @contextmanager
     def fetch(self):
-        logger.debug(f"Data source type is local; skipping fetch")
+        logger.debug("Data source type is local; skipping fetch")
         local_path = urlparse(self.url).path  # Strip file:// scheme
 
         yield local_path

+ 1 - 1
netbox/core/graphql/mixins.py

@@ -15,7 +15,7 @@ __all__ = (
 class ChangelogMixin:
 
     @strawberry_django.field
-    def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
+    def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:  # noqa: F821
         content_type = ContentType.objects.get_for_model(self)
         object_changes = ObjectChange.objects.filter(
             changed_object_type=content_type,

+ 2 - 2
netbox/core/management/commands/syncdatasource.py

@@ -26,7 +26,7 @@ class Command(BaseCommand):
             if invalid_names := set(options['name']) - found_names:
                 raise CommandError(f"Invalid data source names: {', '.join(invalid_names)}")
         else:
-            raise CommandError(f"Must specify at least one data source, or set --all.")
+            raise CommandError("Must specify at least one data source, or set --all.")
 
         if len(options['name']) > 1:
             self.stdout.write(f"Syncing {len(datasources)} data sources.")
@@ -43,4 +43,4 @@ class Command(BaseCommand):
                 raise e
 
         if len(options['name']) > 1:
-            self.stdout.write(f"Finished.")
+            self.stdout.write("Finished.")

+ 1 - 1
netbox/core/models/data.py

@@ -125,7 +125,7 @@ class DataSource(JobsMixin, PrimaryModel):
         # Ensure URL scheme matches selected type
         if self.backend_class.is_local and self.url_scheme not in ('file', ''):
             raise ValidationError({
-                'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
+                'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
             })
 
     def to_objectchange(self, action):

+ 2 - 2
netbox/core/models/jobs.py

@@ -118,9 +118,9 @@ class Job(models.Model):
         # TODO: Employ dynamic registration
         if self.object_type:
             if self.object_type.model == 'reportmodule':
-                return reverse(f'extras:report_result', kwargs={'job_pk': self.pk})
+                return reverse('extras:report_result', kwargs={'job_pk': self.pk})
             elif self.object_type.model == 'scriptmodule':
-                return reverse(f'extras:script_result', kwargs={'job_pk': self.pk})
+                return reverse('extras:script_result', kwargs={'job_pk': self.pk})
         return reverse('core:job', args=[self.pk])
 
     def get_status_color(self):

+ 1 - 1
netbox/dcim/api/nested_serializers.py

@@ -56,7 +56,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 2 - 2
netbox/dcim/forms/object_create.py

@@ -261,8 +261,8 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
             # TODO: Clean up the application of HTMXSelect attributes
             attrs={
                 'hx-get': '.',
-                'hx-include': f'#form_fields',
-                'hx-target': f'#form_fields',
+                'hx-include': '#form_fields',
+                'hx-target': '#form_fields',
             }
         )
     )

+ 20 - 20
netbox/dcim/graphql/mixins.py

@@ -10,18 +10,18 @@ __all__ = (
 
 @strawberry.type
 class CabledObjectMixin:
-    cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
+    cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None  # noqa: F821
 
     link_peers: List[Annotated[Union[
-        Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
-        Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
+        Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],  # noqa: F821
+        Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
     ], strawberry.union("LinkPeerType")]]
 
 
@@ -29,14 +29,14 @@ class CabledObjectMixin:
 class PathEndpointMixin:
 
     connected_endpoints: List[Annotated[Union[
-        Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
-        Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
-        Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
-        Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
+        Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],  # noqa: F821
+        Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
+        Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],  # noqa: F821
+        Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],  # noqa: F821
     ], strawberry.union("ConnectedEndpointType")]]

+ 1 - 1
netbox/dcim/management/commands/trace_paths.py

@@ -60,7 +60,7 @@ class Command(BaseCommand):
             self.stdout.write((self.style.SUCCESS(f'  Deleted {deleted_count} paths')))
 
             # Reinitialize the model's PK sequence
-            self.stdout.write(f'Resetting database sequence for CablePath model')
+            self.stdout.write('Resetting database sequence for CablePath model')
             sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath])
             with connection.cursor() as cursor:
                 for sql in sequence_sql:

+ 0 - 1
netbox/dcim/models/device_component_templates.py

@@ -160,7 +160,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
 
     def _get_module_tree(self, module):
         modules = []
-        all_module_bays = module.device.modulebays.all().select_related('module')
         while module:
             modules.append(module)
             if module.module_bay:

+ 1 - 2
netbox/dcim/tables/devicetypes.py

@@ -1,6 +1,5 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim import models
 from netbox.tables import NetBoxTable, columns

+ 2 - 2
netbox/dcim/tests/test_api.py

@@ -2135,12 +2135,12 @@ class ConnectedDeviceTest(APITestCase):
     def test_get_connected_device(self):
         url = reverse('dcim-api:connected-device-list')
 
-        url_params = f'?peer_device=TestDevice1&peer_interface=eth0'
+        url_params = '?peer_device=TestDevice1&peer_interface=eth0'
         response = self.client.get(url + url_params, **self.header)
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertEqual(response.data['name'], 'TestDevice2')
 
-        url_params = f'?peer_device=TestDevice1&peer_interface=eth1'
+        url_params = '?peer_device=TestDevice1&peer_interface=eth1'
         response = self.client.get(url + url_params, **self.header)
         self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
 

+ 0 - 7
netbox/dcim/tests/test_filtersets.py

@@ -4838,13 +4838,6 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'device_role': [role[0].slug, role[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
-    def test_role(self):
-        role = DeviceRole.objects.all()[:2]
-        params = {'role_id': [role[0].pk, role[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
-        params = {'role': [role[0].slug, role[1].slug]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
-
     def test_device(self):
         devices = Device.objects.all()[:2]
         params = {'device_id': [devices[0].pk, devices[1].pk]}

+ 26 - 28
netbox/dcim/tests/test_models.py

@@ -662,10 +662,8 @@ class ModuleBayTestCase(TestCase):
 
     def test_module_bay_recursion(self):
         module_bay_1 = ModuleBay.objects.get(name='Module Bay 1')
-        module_bay_2 = ModuleBay.objects.get(name='Module Bay 2')
         module_bay_3 = ModuleBay.objects.get(name='Module Bay 3')
         module_1 = Module.objects.get(module_bay=module_bay_1)
-        module_2 = Module.objects.get(module_bay=module_bay_2)
         module_3 = Module.objects.get(module_bay=module_bay_3)
 
         # Confirm error if ModuleBay recurses
@@ -681,8 +679,6 @@ class ModuleBayTestCase(TestCase):
             module_1.save()
 
     def test_single_module_token(self):
-        module_bays = ModuleBay.objects.all()
-        modules = Module.objects.all()
         device_type = DeviceType.objects.first()
         device_role = DeviceRole.objects.first()
         site = Site.objects.first()
@@ -708,7 +704,7 @@ class ModuleBayTestCase(TestCase):
             location=location,
             rack=rack
         )
-        cp = device.consoleports.first()
+        device.consoleports.first()
 
     def test_nested_module_token(self):
         pass
@@ -733,39 +729,41 @@ class CableTestCase(TestCase):
         device2 = Device.objects.create(
             device_type=devicetype, role=role, name='TestDevice2', site=site
         )
-        interface1 = Interface.objects.create(device=device1, name='eth0')
-        interface2 = Interface.objects.create(device=device2, name='eth0')
-        interface3 = Interface.objects.create(device=device2, name='eth1')
-        Cable(a_terminations=[interface1], b_terminations=[interface2]).save()
+        interfaces = (
+            Interface(device=device1, name='eth0'),
+            Interface(device=device2, name='eth0'),
+            Interface(device=device2, name='eth1'),
+        )
+        Interface.objects.bulk_create(interfaces)
+        Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[1]]).save()
+        PowerPort.objects.create(device=device2, name='psu1')
 
-        power_port1 = PowerPort.objects.create(device=device2, name='psu1')
-        patch_pannel = Device.objects.create(
+        patch_panel = Device.objects.create(
             device_type=devicetype, role=role, name='TestPatchPanel', site=site
         )
-        rear_port1 = RearPort.objects.create(device=patch_pannel, name='RP1', type='8p8c')
-        front_port1 = FrontPort.objects.create(
-            device=patch_pannel, name='FP1', type='8p8c', rear_port=rear_port1, rear_port_position=1
-        )
-        rear_port2 = RearPort.objects.create(device=patch_pannel, name='RP2', type='8p8c', positions=2)
-        front_port2 = FrontPort.objects.create(
-            device=patch_pannel, name='FP2', type='8p8c', rear_port=rear_port2, rear_port_position=1
+        rear_ports = (
+            RearPort(device=patch_panel, name='RP1', type='8p8c'),
+            RearPort(device=patch_panel, name='RP2', type='8p8c', positions=2),
+            RearPort(device=patch_panel, name='RP3', type='8p8c', positions=3),
+            RearPort(device=patch_panel, name='RP4', type='8p8c', positions=3),
         )
-        rear_port3 = RearPort.objects.create(device=patch_pannel, name='RP3', type='8p8c', positions=3)
-        front_port3 = FrontPort.objects.create(
-            device=patch_pannel, name='FP3', type='8p8c', rear_port=rear_port3, rear_port_position=1
-        )
-        rear_port4 = RearPort.objects.create(device=patch_pannel, name='RP4', type='8p8c', positions=3)
-        front_port4 = FrontPort.objects.create(
-            device=patch_pannel, name='FP4', type='8p8c', rear_port=rear_port4, rear_port_position=1
+        RearPort.objects.bulk_create(rear_ports)
+        front_ports = (
+            FrontPort(device=patch_panel, name='FP1', type='8p8c', rear_port=rear_ports[0], rear_port_position=1),
+            FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1),
+            FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1),
+            FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1),
         )
+        FrontPort.objects.bulk_create(front_ports)
+
         provider = Provider.objects.create(name='Provider 1', slug='provider-1')
         provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
         circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
         circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
         circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
-        circuittermination1 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
-        circuittermination2 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
-        circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
+        CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
+        CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
+        CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
 
     def test_cable_creation(self):
         """

+ 1 - 1
netbox/dcim/tests/test_views.py

@@ -2571,7 +2571,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         }
 
         cls.csv_data = (
-            f"device,name,type,vrf.pk,poe_mode,poe_type",
+            "device,name,type,vrf.pk,poe_mode,poe_type",
             f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
             f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
             f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",

+ 1 - 1
netbox/extras/api/nested_serializers.py

@@ -24,7 +24,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 1 - 1
netbox/extras/jobs.py

@@ -48,7 +48,7 @@ class ScriptJob(JobRunner):
             except AbortTransaction:
                 script.log_info(message=_("Database changes have been reverted automatically."))
                 if script.failed:
-                    logger.warning(f"Script failed")
+                    logger.warning("Script failed")
                     raise
 
         except Exception as e:

+ 2 - 2
netbox/extras/management/commands/housekeeping.py

@@ -95,7 +95,7 @@ class Command(BaseCommand):
             self.stdout.write("[*] Checking for latest release")
         if settings.ISOLATED_DEPLOYMENT:
             if options['verbosity']:
-                self.stdout.write(f"\tSkipping: ISOLATED_DEPLOYMENT is enabled")
+                self.stdout.write("\tSkipping: ISOLATED_DEPLOYMENT is enabled")
         elif settings.RELEASE_CHECK_URL:
             headers = {
                 'Accept': 'application/vnd.github.v3+json',
@@ -129,7 +129,7 @@ class Command(BaseCommand):
                 self.stdout.write(f"\tRequest error: {exc}", self.style.ERROR)
         else:
             if options['verbosity']:
-                self.stdout.write(f"\tSkipping: RELEASE_CHECK_URL not set")
+                self.stdout.write("\tSkipping: RELEASE_CHECK_URL not set")
 
         if options['verbosity']:
             self.stdout.write("Finished.", self.style.SUCCESS)

+ 2 - 2
netbox/extras/management/commands/reindex.py

@@ -96,9 +96,9 @@ class Command(BaseCommand):
             if i:
                 self.stdout.write(f'{i} entries cached.')
             else:
-                self.stdout.write(f'No objects found.')
+                self.stdout.write('No objects found.')
 
-        msg = f'Completed.'
+        msg = 'Completed.'
         if total_count := search_backend.size:
             msg += f' Total entries: {total_count}'
         self.stdout.write(msg, self.style.SUCCESS)

+ 1 - 1
netbox/extras/management/commands/runscript.py

@@ -51,7 +51,7 @@ class Command(BaseCommand):
             user = User.objects.filter(is_superuser=True).order_by('pk')[0]
 
         # Setup logging to Stdout
-        formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
+        formatter = logging.Formatter('[%(asctime)s][%(levelname)s] - %(message)s')
         stdouthandler = logging.StreamHandler(sys.stdout)
         stdouthandler.setLevel(logging.DEBUG)
         stdouthandler.setFormatter(formatter)

+ 1 - 1
netbox/extras/models/customfields.py

@@ -283,7 +283,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
         """
         for ct in content_types:
             model = ct.model_class()
-            instances = model.objects.exclude(**{f'custom_field_data__contains': self.name})
+            instances = model.objects.exclude(**{'custom_field_data__contains': self.name})
             for instance in instances:
                 instance.custom_field_data[self.name] = self.default
             model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)

+ 1 - 1
netbox/extras/scripts.py

@@ -554,7 +554,7 @@ class BaseScript:
         """
         Run the report and save its results. Each test method will be executed in order.
         """
-        self.logger.info(f"Running report")
+        self.logger.info("Running report")
 
         try:
             for test_name in self.tests:

+ 1 - 1
netbox/extras/tests/test_customvalidators.py

@@ -162,7 +162,7 @@ class CustomValidatorTest(TestCase):
         Site(name='abcdef123', slug='abcdef123').clean()
 
     @override_settings(CUSTOM_VALIDATORS={'dcim.site': [region_validator]})
-    def test_valid(self):
+    def test_related_object(self):
         region1 = Region(name='Foo', slug='foo')
         region1.save()
         region2 = Region(name='Bar', slug='bar')

+ 4 - 4
netbox/extras/tests/test_models.py

@@ -49,11 +49,11 @@ class ConfigContextTest(TestCase):
         sitegroup = SiteGroup.objects.create(name='Site Group')
         site = Site.objects.create(name='Site 1', slug='site-1', region=region, group=sitegroup)
         location = Location.objects.create(name='Location 1', slug='location-1', site=site)
-        platform = Platform.objects.create(name='Platform')
+        Platform.objects.create(name='Platform')
         tenantgroup = TenantGroup.objects.create(name='Tenant Group')
-        tenant = Tenant.objects.create(name='Tenant', group=tenantgroup)
-        tag1 = Tag.objects.create(name='Tag', slug='tag')
-        tag2 = Tag.objects.create(name='Tag2', slug='tag2')
+        Tenant.objects.create(name='Tenant', group=tenantgroup)
+        Tag.objects.create(name='Tag', slug='tag')
+        Tag.objects.create(name='Tag2', slug='tag2')
 
         Device.objects.create(
             name='Device 1',

+ 1 - 1
netbox/extras/tests/test_views.py

@@ -417,7 +417,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            f'name,object_types,event_types,action_type,action_object',
+            'name,object_types,event_types,action_type,action_object',
             f'Webhook 4,dcim.site,"{OBJECT_CREATED},{OBJECT_UPDATED}",webhook,Webhook 1',
         )
 

+ 1 - 1
netbox/ipam/api/nested_serializers.py

@@ -30,7 +30,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 3 - 3
netbox/ipam/api/views.py

@@ -186,13 +186,13 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
         """
         Return the parent object.
         """
-        raise NotImplemented()
+        raise NotImplementedError()
 
     def get_available_objects(self, parent, limit=None):
         """
         Return all available objects for the parent.
         """
-        raise NotImplemented()
+        raise NotImplementedError()
 
     def get_extra_context(self, parent):
         """
@@ -250,7 +250,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
             # Determine if the requested number of objects is available
             if not self.check_sufficient_available(serializer.validated_data, available_objects):
                 return Response(
-                    {"detail": f"Insufficient resources are available to satisfy the request"},
+                    {"detail": "Insufficient resources are available to satisfy the request"},
                     status=status.HTTP_409_CONFLICT
                 )
 

+ 2 - 2
netbox/ipam/graphql/mixins.py

@@ -10,9 +10,9 @@ __all__ = (
 
 @strawberry.type
 class IPAddressesMixin:
-    ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
+    ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]  # noqa: F821
 
 
 @strawberry.type
 class VLANGroupsMixin:
-    vlan_groups: List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]
+    vlan_groups: List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]  # noqa: F821

+ 0 - 2
netbox/ipam/tests/test_api.py

@@ -700,8 +700,6 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
         device1.primary_ip4 = ip_addresses[0]
         device1.save()
 
-        ip2 = ip_addresses[1]
-
         url = reverse('ipam-api:ipaddress-detail', kwargs={'pk': ip1.pk})
         self.add_permissions('ipam.change_ipaddress')
 

+ 4 - 4
netbox/ipam/tests/test_views.py

@@ -50,7 +50,7 @@ class ASNRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            f"name,slug,rir,tenant,start,end,description",
+            "name,slug,rir,tenant,start,end,description",
             f"ASN Range 4,asn-range-4,{rirs[1].name},{tenants[1].name},400,499,Fourth range",
             f"ASN Range 5,asn-range-5,{rirs[1].name},{tenants[1].name},500,599,Fifth range",
             f"ASN Range 6,asn-range-6,{rirs[1].name},{tenants[1].name},600,699,Sixth range",
@@ -770,14 +770,14 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
         }
 
         cls.csv_data = (
-            f"name,slug,scope_type,scope_id,description",
-            f"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
+            "name,slug,scope_type,scope_id,description",
+            "VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
             f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
             f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
         )
 
         cls.csv_update_data = (
-            f"id,name,description",
+            "id,name,description",
             f"{vlan_groups[0].pk},VLAN Group 7,Fourth VLAN group7",
             f"{vlan_groups[1].pk},VLAN Group 8,Fifth VLAN group8",
             f"{vlan_groups[2].pk},VLAN Group 9,Sixth VLAN group9",

+ 1 - 1
netbox/netbox/config/__init__.py

@@ -85,7 +85,7 @@ class Config:
             logger.debug("Loaded configuration data from database")
         except DatabaseError:
             # The database may not be available yet (e.g. when running a management command)
-            logger.warning(f"Skipping config initialization (database unavailable)")
+            logger.warning("Skipping config initialization (database unavailable)")
             return
 
         revision.activate()

+ 1 - 1
netbox/netbox/data_backends.py

@@ -50,4 +50,4 @@ class DataBackend:
         2. Yields the local path at which data has been replicated
         3. Performs any necessary cleanup
         """
-        raise NotImplemented()
+        raise NotImplementedError()

+ 16 - 16
netbox/netbox/navigation/menu.py

@@ -386,57 +386,57 @@ ADMIN_MENU = Menu(
             label=_('Authentication'),
             items=(
                 MenuItem(
-                    link=f'users:user_list',
+                    link='users:user_list',
                     link_text=_('Users'),
                     auth_required=True,
-                    permissions=[f'users.view_user'],
+                    permissions=['users.view_user'],
                     buttons=(
                         MenuItemButton(
-                            link=f'users:user_add',
+                            link='users:user_add',
                             title='Add',
                             icon_class='mdi mdi-plus-thick',
-                            permissions=[f'users.add_user']
+                            permissions=['users.add_user']
                         ),
                         MenuItemButton(
-                            link=f'users:user_import',
+                            link='users:user_import',
                             title='Import',
                             icon_class='mdi mdi-upload',
-                            permissions=[f'users.add_user']
+                            permissions=['users.add_user']
                         )
                     )
                 ),
                 MenuItem(
-                    link=f'users:group_list',
+                    link='users:group_list',
                     link_text=_('Groups'),
                     auth_required=True,
-                    permissions=[f'users.view_group'],
+                    permissions=['users.view_group'],
                     buttons=(
                         MenuItemButton(
-                            link=f'users:group_add',
+                            link='users:group_add',
                             title='Add',
                             icon_class='mdi mdi-plus-thick',
-                            permissions=[f'users.add_group']
+                            permissions=['users.add_group']
                         ),
                         MenuItemButton(
-                            link=f'users:group_import',
+                            link='users:group_import',
                             title='Import',
                             icon_class='mdi mdi-upload',
-                            permissions=[f'users.add_group']
+                            permissions=['users.add_group']
                         )
                     )
                 ),
                 MenuItem(
-                    link=f'users:token_list',
+                    link='users:token_list',
                     link_text=_('API Tokens'),
                     auth_required=True,
-                    permissions=[f'users.view_token'],
+                    permissions=['users.view_token'],
                     buttons=get_model_buttons('users', 'token')
                 ),
                 MenuItem(
-                    link=f'users:objectpermission_list',
+                    link='users:objectpermission_list',
                     link_text=_('Permissions'),
                     auth_required=True,
-                    permissions=[f'users.view_objectpermission'],
+                    permissions=['users.view_objectpermission'],
                     buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
                 ),
             ),

+ 1 - 1
netbox/netbox/settings.py

@@ -198,7 +198,7 @@ if len(SECRET_KEY) < 50:
 if RELEASE_CHECK_URL:
     try:
         URLValidator()(RELEASE_CHECK_URL)
-    except ValidationError as e:
+    except ValidationError:
         raise ImproperlyConfigured(
             "RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox"
         )

+ 1 - 1
netbox/netbox/staging.py

@@ -80,7 +80,7 @@ class checkout:
         Create Change instances for all actions stored in the queue.
         """
         if not self.queue:
-            logger.debug(f"No queued changes; aborting")
+            logger.debug("No queued changes; aborting")
             return
         logger.debug(f"Processing {len(self.queue)} queued changes")
 

+ 2 - 2
netbox/netbox/tests/dummy_plugin/views.py

@@ -21,7 +21,7 @@ class DummyModelsView(View):
 class DummyModelAddView(View):
 
     def get(self, request):
-        return HttpResponse(f"Create an instance")
+        return HttpResponse("Create an instance")
 
     def post(self, request):
         instance = DummyModel(
@@ -29,7 +29,7 @@ class DummyModelAddView(View):
             number=random.randint(1, 100000)
         )
         instance.save()
-        return HttpResponse(f"Instance created")
+        return HttpResponse("Instance created")
 
 
 @register_model_view(Site, 'extra', path='other-stuff')

+ 1 - 1
netbox/netbox/tests/test_authentication.py

@@ -106,7 +106,7 @@ class ExternalAuthenticationTestCase(TestCase):
         self.assertEqual(settings.REMOTE_AUTH_HEADER, 'HTTP_REMOTE_USER')
 
         # Client should not be authenticated
-        response = self.client.get(reverse('home'), follow=True, **headers)
+        self.client.get(reverse('home'), follow=True, **headers)
         self.assertNotIn('_auth_user_id', self.client.session)
 
     @override_settings(

+ 0 - 1
netbox/netbox/tests/test_import.py

@@ -77,7 +77,6 @@ class CSVImportTestCase(ModelViewTestCase):
         self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
         regions = Region.objects.all()
         self.assertEqual(regions.count(), 4)
-        region = Region.objects.get(slug="region-4")
         self.assertEqual(
             list(regions[0].tags.values_list('name', flat=True)),
             ['Alpha', 'Bravo']

+ 1 - 1
netbox/tenancy/api/nested_serializers.py

@@ -15,7 +15,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 1 - 1
netbox/tenancy/graphql/mixins.py

@@ -10,4 +10,4 @@ __all__ = (
 @strawberry.type
 class ContactAssignmentsMixin:
 
-    assignments: List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]
+    assignments: List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]  # noqa: F821

+ 1 - 1
netbox/users/api/nested_serializers.py

@@ -18,7 +18,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 1 - 1
netbox/users/api/views.py

@@ -73,7 +73,7 @@ class TokenProvisionView(APIView):
 
     def perform_create(self, serializer):
         model = serializer.Meta.model
-        logger = logging.getLogger(f'netbox.api.views.TokenProvisionView')
+        logger = logging.getLogger('netbox.api.views.TokenProvisionView')
         logger.info(f"Creating new {model._meta.verbose_name}")
         serializer.save()
 

+ 0 - 1
netbox/users/forms/model_forms.py

@@ -36,7 +36,6 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
         # Emulate a declared field for each supported user preference
         preference_fields = {}
         for field_name, preference in PREFERENCES.items():
-            description = f'{preference.description}<br />' if preference.description else ''
             help_text = f'<code>{field_name}</code>'
             if preference.description:
                 help_text = f'{preference.description}<br />{help_text}'

+ 2 - 2
netbox/users/tests/test_preferences.py

@@ -51,11 +51,11 @@ class UserPreferencesTest(TestCase):
 
         # Check that table ordering preference has been recorded
         self.user.refresh_from_db()
-        ordering = self.user.config.get(f'tables.SiteTable.ordering')
+        ordering = self.user.config.get('tables.SiteTable.ordering')
         self.assertEqual(ordering, ['status'])
 
         # Check that a recorded preference is honored by default
-        self.user.config.set(f'tables.SiteTable.ordering', ['-status'], commit=True)
+        self.user.config.set('tables.SiteTable.ordering', ['-status'], commit=True)
         table = SiteTable(Site.objects.all())
         request = RequestFactory().get(url)
         request.user = self.user

+ 1 - 1
netbox/utilities/forms/fields/dynamic.py

@@ -142,7 +142,7 @@ class DynamicModelChoiceMixin:
 
         if data:
             # When the field is multiple choice pass the data as a list if it's not already
-            if isinstance(bound_field.field, DynamicModelMultipleChoiceField) and not type(data) is list:
+            if isinstance(bound_field.field, DynamicModelMultipleChoiceField) and type(data) is not list:
                 data = [data]
 
             field_name = getattr(self, 'to_field_name') or 'pk'

+ 1 - 1
netbox/utilities/html.py

@@ -59,7 +59,7 @@ def highlight(value, highlight, trim_pre=None, trim_post=None, trim_placeholder=
         else:
             highlight = re.escape(highlight)
             pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE)
-    except ValueError as e:
+    except ValueError:
         # Match not found
         return escape(value)
 

+ 2 - 2
netbox/utilities/tests/test_api.py

@@ -149,7 +149,7 @@ class APIPaginationTestCase(APITestCase):
 
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertEqual(response.data['count'], 100)
-        self.assertTrue(response.data['next'].endswith(f'?limit=10&offset=10'))
+        self.assertTrue(response.data['next'].endswith('?limit=10&offset=10'))
         self.assertIsNone(response.data['previous'])
         self.assertEqual(len(response.data['results']), 10)
 
@@ -159,7 +159,7 @@ class APIPaginationTestCase(APITestCase):
 
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertEqual(response.data['count'], 100)
-        self.assertTrue(response.data['next'].endswith(f'?limit=20&offset=20'))
+        self.assertTrue(response.data['next'].endswith('?limit=20&offset=20'))
         self.assertIsNone(response.data['previous'])
         self.assertEqual(len(response.data['results']), 20)
 

+ 1 - 1
netbox/utilities/tests/test_counters.py

@@ -85,7 +85,7 @@ class CountersTest(TestCase):
     def test_mptt_child_delete(self):
         device1, device2 = Device.objects.all()
         inventory_item1 = InventoryItem.objects.create(device=device1, name='Inventory Item 1')
-        inventory_item2 = InventoryItem.objects.create(device=device1, name='Inventory Item 2', parent=inventory_item1)
+        InventoryItem.objects.create(device=device1, name='Inventory Item 2', parent=inventory_item1)
         device1.refresh_from_db()
         self.assertEqual(device1.inventory_item_count, 2)
 

+ 1 - 1
netbox/virtualization/api/nested_serializers.py

@@ -18,7 +18,7 @@ __all__ = [
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 7 - 7
netbox/virtualization/tests/test_views.py

@@ -354,14 +354,14 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         }
 
         cls.csv_data = (
-            f"virtual_machine,name,vrf.pk",
+            "virtual_machine,name,vrf.pk",
             f"Virtual Machine 2,Interface 4,{vrfs[0].pk}",
             f"Virtual Machine 2,Interface 5,{vrfs[0].pk}",
             f"Virtual Machine 2,Interface 6,{vrfs[0].pk}",
         )
 
         cls.csv_update_data = (
-            f"id,name,description",
+            "id,name,description",
             f"{interfaces[0].pk},Interface 7,New description 7",
             f"{interfaces[1].pk},Interface 8,New description 8",
             f"{interfaces[2].pk},Interface 9,New description 9",
@@ -438,14 +438,14 @@ class VirtualDiskTestCase(ViewTestCases.DeviceComponentViewTestCase):
         }
 
         cls.csv_data = (
-            f"virtual_machine,name,size,description",
-            f"Virtual Machine 1,Disk 4,20,Fourth",
-            f"Virtual Machine 1,Disk 5,20,Fifth",
-            f"Virtual Machine 1,Disk 6,20,Sixth",
+            "virtual_machine,name,size,description",
+            "Virtual Machine 1,Disk 4,20,Fourth",
+            "Virtual Machine 1,Disk 5,20,Fifth",
+            "Virtual Machine 1,Disk 6,20,Sixth",
         )
 
         cls.csv_update_data = (
-            f"id,name,size",
+            "id,name,size",
             f"{disks[0].pk},disk1,20",
             f"{disks[1].pk},disk2,20",
             f"{disks[2].pk},disk3,20",

+ 2 - 2
netbox/virtualization/views.py

@@ -657,7 +657,7 @@ class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView):
     default_return_url = 'virtualization:virtualmachine_list'
 
     def get_required_permission(self):
-        return f'virtualization.add_vminterface'
+        return 'virtualization.add_vminterface'
 
 
 class VirtualMachineBulkAddVirtualDiskView(generic.BulkComponentCreateView):
@@ -671,4 +671,4 @@ class VirtualMachineBulkAddVirtualDiskView(generic.BulkComponentCreateView):
     default_return_url = 'virtualization:virtualmachine_list'
 
     def get_required_permission(self):
-        return f'virtualization.add_virtualdisk'
+        return 'virtualization.add_virtualdisk'

+ 1 - 1
netbox/vpn/api/nested_serializers.py

@@ -21,7 +21,7 @@ __all__ = (
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 0 - 11
netbox/vpn/filtersets.py

@@ -147,17 +147,6 @@ class IKEProposalFilterSet(NetBoxModelFilterSet):
     group = django_filters.MultipleChoiceFilter(
         choices=DHGroupChoices
     )
-    ike_policy_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='ike_policies',
-        queryset=IKEPolicy.objects.all(),
-        label=_('IKE policy (ID)'),
-    )
-    ike_policy = django_filters.ModelMultipleChoiceFilter(
-        field_name='ike_policies__name',
-        queryset=IKEPolicy.objects.all(),
-        to_field_name='name',
-        label=_('IKE policy (name)'),
-    )
 
     class Meta:
         model = IKEProposal

+ 0 - 7
netbox/vpn/tests/test_filtersets.py

@@ -385,13 +385,6 @@ class IKEProposalTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'sa_lifetime': [1000, 2000]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
-    def test_ike_policy(self):
-        ike_policies = IKEPolicy.objects.all()[:2]
-        params = {'ike_policy_id': [ike_policies[0].pk, ike_policies[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-        params = {'ike_policy': [ike_policies[0].name, ike_policies[1].name]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
 
 class IKEPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = IKEPolicy.objects.all()

+ 4 - 4
netbox/vpn/tests/test_views.py

@@ -542,9 +542,9 @@ class IPSecProfileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
         cls.csv_data = (
             "name,mode,ike_policy,ipsec_policy",
-            f"IKE Proposal 4,ah,IKE Policy 2,IPSec Policy 2",
-            f"IKE Proposal 5,ah,IKE Policy 2,IPSec Policy 2",
-            f"IKE Proposal 6,ah,IKE Policy 2,IPSec Policy 2",
+            "IKE Proposal 4,ah,IKE Policy 2,IPSec Policy 2",
+            "IKE Proposal 5,ah,IKE Policy 2,IPSec Policy 2",
+            "IKE Proposal 6,ah,IKE Policy 2,IPSec Policy 2",
         )
 
         cls.csv_update_data = (
@@ -661,7 +661,7 @@ class L2VPNTerminationTestCase(
         )
 
         cls.csv_update_data = (
-            f"id,l2vpn",
+            "id,l2vpn",
             f"{terminations[0].pk},{l2vpns[0].name}",
             f"{terminations[1].pk},{l2vpns[0].name}",
             f"{terminations[2].pk},{l2vpns[0].name}",

+ 1 - 1
netbox/wireless/api/nested_serializers.py

@@ -12,7 +12,7 @@ __all__ = (
 
 # TODO: Remove in v4.2
 warnings.warn(
-    f"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+    "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
     DeprecationWarning
 )
 

+ 3 - 3
netbox/wireless/tests/test_views.py

@@ -102,14 +102,14 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            f"group,ssid,status,tenant",
+            "group,ssid,status,tenant",
             f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name}",
             f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name}",
             f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name}",
         )
 
         cls.csv_update_data = (
-            f"id,ssid",
+            "id,ssid",
             f"{wireless_lans[0].pk},WLAN7",
             f"{wireless_lans[1].pk},WLAN8",
             f"{wireless_lans[2].pk},WLAN9",
@@ -167,7 +167,7 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            f"interface_a,interface_b,status,tenant",
+            "interface_a,interface_b,status,tenant",
             f"{interfaces[6].pk},{interfaces[7].pk},connected,{tenants[0].name}",
             f"{interfaces[8].pk},{interfaces[9].pk},connected,{tenants[1].name}",
             f"{interfaces[10].pk},{interfaces[11].pk},connected,{tenants[2].name}",

+ 2 - 0
ruff.toml

@@ -0,0 +1,2 @@
+[lint]
+ignore = ["E501", "F403", "F405"]

+ 2 - 2
scripts/git-hooks/pre-commit

@@ -28,8 +28,8 @@ if [ ${NOVALIDATE} ]; then
   exit $EXIT
 fi
 
-echo "Validating PEP8 compliance..."
-pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
+echo "Linting with ruff..."
+ruff check netbox/
 if [ $? != 0 ]; then
 	EXIT=1
 fi