nautobot 2.3.15__py3-none-any.whl → 2.3.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (106) hide show
  1. nautobot/circuits/views.py +3 -3
  2. nautobot/cloud/models.py +1 -1
  3. nautobot/core/api/fields.py +5 -5
  4. nautobot/core/api/serializers.py +9 -9
  5. nautobot/core/api/views.py +3 -2
  6. nautobot/core/apps/__init__.py +5 -2
  7. nautobot/core/celery/schedulers.py +1 -1
  8. nautobot/core/filters.py +19 -16
  9. nautobot/core/forms/fields.py +5 -5
  10. nautobot/core/graphql/types.py +1 -1
  11. nautobot/core/jobs/__init__.py +4 -4
  12. nautobot/core/jobs/cleanup.py +1 -1
  13. nautobot/core/jobs/groups.py +1 -1
  14. nautobot/core/management/commands/validate_models.py +1 -1
  15. nautobot/core/models/__init__.py +1 -1
  16. nautobot/core/models/query_functions.py +2 -2
  17. nautobot/core/models/tree_queries.py +2 -2
  18. nautobot/core/tables.py +5 -5
  19. nautobot/core/testing/filters.py +7 -3
  20. nautobot/core/testing/views.py +5 -0
  21. nautobot/core/tests/runner.py +1 -1
  22. nautobot/core/views/generic.py +51 -43
  23. nautobot/core/views/mixins.py +21 -11
  24. nautobot/dcim/api/serializers.py +48 -48
  25. nautobot/dcim/forms.py +2 -0
  26. nautobot/dcim/graphql/types.py +2 -2
  27. nautobot/dcim/models/device_component_templates.py +2 -2
  28. nautobot/dcim/models/device_components.py +22 -20
  29. nautobot/dcim/models/devices.py +1 -1
  30. nautobot/dcim/models/locations.py +3 -3
  31. nautobot/dcim/models/power.py +6 -5
  32. nautobot/dcim/models/racks.py +4 -4
  33. nautobot/dcim/tables/__init__.py +3 -3
  34. nautobot/dcim/tables/devicetypes.py +2 -2
  35. nautobot/dcim/tests/test_filters.py +1 -0
  36. nautobot/dcim/tests/test_graphql.py +52 -0
  37. nautobot/dcim/tests/test_models.py +4 -1
  38. nautobot/dcim/views.py +1 -1
  39. nautobot/extras/api/customfields.py +2 -2
  40. nautobot/extras/api/serializers.py +72 -69
  41. nautobot/extras/api/views.py +4 -4
  42. nautobot/extras/health_checks.py +1 -2
  43. nautobot/extras/jobs.py +5 -5
  44. nautobot/extras/managers.py +3 -1
  45. nautobot/extras/migrations/0018_joblog_data_migration.py +7 -9
  46. nautobot/extras/models/groups.py +13 -9
  47. nautobot/extras/models/jobs.py +4 -4
  48. nautobot/extras/models/models.py +2 -2
  49. nautobot/extras/plugins/views.py +1 -1
  50. nautobot/extras/tables.py +5 -5
  51. nautobot/extras/test_jobs/api_test_job.py +1 -1
  52. nautobot/extras/test_jobs/atomic_transaction.py +2 -2
  53. nautobot/extras/test_jobs/dry_run.py +1 -1
  54. nautobot/extras/test_jobs/fail.py +5 -5
  55. nautobot/extras/test_jobs/file_output.py +1 -1
  56. nautobot/extras/test_jobs/file_upload_fail.py +1 -1
  57. nautobot/extras/test_jobs/file_upload_pass.py +1 -1
  58. nautobot/extras/test_jobs/ipaddress_vars.py +3 -1
  59. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
  60. nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
  61. nautobot/extras/test_jobs/log_redaction.py +1 -1
  62. nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
  63. nautobot/extras/test_jobs/modify_db.py +1 -1
  64. nautobot/extras/test_jobs/object_var_optional.py +1 -1
  65. nautobot/extras/test_jobs/object_var_required.py +1 -1
  66. nautobot/extras/test_jobs/object_vars.py +1 -1
  67. nautobot/extras/test_jobs/pass.py +3 -3
  68. nautobot/extras/test_jobs/profiling.py +1 -1
  69. nautobot/extras/test_jobs/relative_import.py +3 -3
  70. nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
  71. nautobot/extras/test_jobs/task_queues.py +1 -1
  72. nautobot/extras/tests/test_api.py +13 -13
  73. nautobot/extras/tests/test_customfields.py +1 -1
  74. nautobot/extras/tests/test_datasources.py +2 -1
  75. nautobot/extras/tests/test_dynamicgroups.py +1 -1
  76. nautobot/extras/tests/test_filters.py +6 -6
  77. nautobot/extras/tests/test_jobs.py +11 -11
  78. nautobot/extras/tests/test_models.py +10 -10
  79. nautobot/extras/tests/test_relationships.py +1 -1
  80. nautobot/extras/tests/test_views.py +16 -16
  81. nautobot/extras/views.py +20 -16
  82. nautobot/ipam/api/fields.py +3 -3
  83. nautobot/ipam/api/serializers.py +33 -33
  84. nautobot/ipam/api/views.py +37 -61
  85. nautobot/ipam/querysets.py +2 -2
  86. nautobot/ipam/tests/test_api.py +12 -1
  87. nautobot/ipam/tests/test_forms.py +51 -47
  88. nautobot/ipam/tests/test_migrations.py +30 -30
  89. nautobot/ipam/tests/test_querysets.py +14 -0
  90. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
  91. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -1
  92. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
  93. nautobot/project-static/docs/release-notes/version-2.3.html +181 -99
  94. nautobot/project-static/docs/search/search_index.json +1 -1
  95. nautobot/project-static/docs/sitemap.xml +270 -270
  96. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  97. nautobot/users/admin.py +1 -1
  98. nautobot/users/api/serializers.py +4 -4
  99. nautobot/users/api/views.py +1 -1
  100. nautobot/virtualization/api/serializers.py +4 -4
  101. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/METADATA +1 -1
  102. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/RECORD +106 -106
  103. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/WHEEL +1 -1
  104. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/LICENSE.txt +0 -0
  105. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/NOTICE +0 -0
  106. {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/entry_points.txt +0 -0
@@ -79,11 +79,11 @@ class CircuitTerminationUIViewSet(
79
79
  obj.circuit = get_object_or_404(Circuit, pk=self.kwargs["circuit"])
80
80
  return obj
81
81
 
82
- def get_return_url(self, request, obj=None):
82
+ def get_return_url(self, request, obj=None, default_return_url=None):
83
83
  if obj is not None and obj.present_in_database and obj.pk:
84
- return super().get_return_url(request, obj=obj.circuit)
84
+ return super().get_return_url(request, obj=obj.circuit, default_return_url=default_return_url)
85
85
 
86
- return super().get_return_url(request, obj=obj)
86
+ return super().get_return_url(request, obj=obj, default_return_url=default_return_url)
87
87
 
88
88
 
89
89
  class ProviderUIViewSet(NautobotUIViewSet):
nautobot/cloud/models.py CHANGED
@@ -109,7 +109,7 @@ class CloudResourceTypeMixin(models.Model):
109
109
  super().clean()
110
110
 
111
111
  # Copied from nautobot.extras.models.models.ConfigContextSchemaValidationMixin
112
- schema = self.cloud_resource_type.config_schema
112
+ schema = self.cloud_resource_type.config_schema # pylint: disable=no-member
113
113
  if schema:
114
114
  try:
115
115
  Draft7Validator(schema, format_checker=Draft7Validator.FORMAT_CHECKER).validate(self.extra_config)
@@ -53,10 +53,10 @@ class ChoiceField(serializers.Field):
53
53
  data = ""
54
54
  return super().validate_empty_values(data)
55
55
 
56
- def to_representation(self, obj):
57
- if obj == "":
56
+ def to_representation(self, value):
57
+ if value == "":
58
58
  return None
59
- return OrderedDict([("value", obj), ("label", self._choices[obj])])
59
+ return OrderedDict([("value", value), ("label", self._choices[value])])
60
60
 
61
61
  def to_internal_value(self, data):
62
62
  if data == "":
@@ -123,8 +123,8 @@ class ContentTypeField(RelatedField):
123
123
  self.fail("invalid")
124
124
  return None
125
125
 
126
- def to_representation(self, obj):
127
- return f"{obj.app_label}.{obj.model}"
126
+ def to_representation(self, value):
127
+ return f"{value.app_label}.{value.model}"
128
128
 
129
129
 
130
130
  class LaxURLField(URLField):
@@ -759,27 +759,27 @@ class ValidatedModelSerializer(BaseModelSerializer):
759
759
  validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
760
760
  """
761
761
 
762
- def validate(self, data):
762
+ def validate(self, attrs):
763
763
  # Remove custom fields data and tags (if any) prior to model validation
764
- attrs = data.copy()
765
- attrs.pop("custom_fields", None)
766
- attrs.pop("relationships", None)
767
- attrs.pop("tags", None)
764
+ local_attrs = attrs.copy()
765
+ local_attrs.pop("custom_fields", None)
766
+ local_attrs.pop("relationships", None)
767
+ local_attrs.pop("tags", None)
768
768
 
769
769
  # Skip ManyToManyFields
770
770
  for field in self.Meta.model._meta.get_fields():
771
771
  if isinstance(field, models.ManyToManyField):
772
- attrs.pop(field.name, None)
772
+ local_attrs.pop(field.name, None)
773
773
 
774
774
  # Run clean() on an instance of the model
775
775
  if self.instance is None:
776
- instance = self.Meta.model(**attrs)
776
+ instance = self.Meta.model(**local_attrs)
777
777
  else:
778
778
  instance = self.instance
779
- for k, v in attrs.items():
779
+ for k, v in local_attrs.items():
780
780
  setattr(instance, k, v)
781
781
  instance.full_clean()
782
- return data
782
+ return attrs
783
783
 
784
784
 
785
785
  class WritableNestedSerializer(BaseModelSerializer):
@@ -373,7 +373,7 @@ class APIRootView(AuthenticatedAPIRootView):
373
373
  name = "API Root"
374
374
 
375
375
  @extend_schema(exclude=True)
376
- def get(self, request, format=None): # pylint: disable=redefined-builtin
376
+ def get(self, request, *args, format=None, **kwargs): # pylint: disable=redefined-builtin
377
377
  return Response(
378
378
  OrderedDict(
379
379
  (
@@ -572,12 +572,13 @@ class GraphQLDRFAPIView(NautobotAPIVersionMixin, APIView):
572
572
  middleware = None
573
573
  root_value = None
574
574
 
575
- def __init__(self, schema=None, executor=None, middleware=None, root_value=None, backend=None):
575
+ def __init__(self, schema=None, executor=None, middleware=None, root_value=None, backend=None, **kwargs):
576
576
  self.schema = schema
577
577
  self.executor = executor
578
578
  self.middleware = middleware
579
579
  self.root_value = root_value
580
580
  self.backend = backend
581
+ super().__init__(**kwargs)
581
582
 
582
583
  def get_root_value(self, request):
583
584
  return self.root_value
@@ -592,6 +592,7 @@ class NavMenuGroup(NavMenuBase, PermissionsMixin):
592
592
  elif not all(isinstance(item, NavMenuItem) for item in items):
593
593
  raise TypeError("All items defined in a group must be an instance of NavMenuItem")
594
594
  self.items = items
595
+ super().__init__(permissions=self.permissions)
595
596
 
596
597
 
597
598
  class NavMenuItem(NavMenuBase, PermissionsMixin):
@@ -788,7 +789,7 @@ class NavGrouping(NavMenuBase, PermissionsMixin):
788
789
  Items are each specified as a list of NavItem or NavGrouping instances.
789
790
  """
790
791
 
791
- def __init__(self, name, items, weight=1000):
792
+ def __init__(self, name, items, weight=1000): # pylint:disable=super-init-not-called # this is dead code anyway
792
793
  self.name = name
793
794
  self.items = items
794
795
  self.weight = weight
@@ -824,7 +825,9 @@ class NavItem(NavMenuBase, PermissionsMixin):
824
825
  Links are specified as Django reverse URL strings.
825
826
  """
826
827
 
827
- def __init__(self, name, link, *args, permissions=None, weight=1000, **kwargs):
828
+ def __init__( # pylint:disable=super-init-not-called # this is dead code anyway
829
+ self, name, link, *args, permissions=None, weight=1000, **kwargs
830
+ ):
828
831
  self.name = name
829
832
  self.link = link
830
833
  self.permissions = permissions or []
@@ -19,7 +19,7 @@ class NautobotScheduleEntry(ModelEntry):
19
19
  nautobot.extras.models.ScheduledJob model
20
20
  """
21
21
 
22
- def __init__(self, model, app=None):
22
+ def __init__(self, model, app=None): # pylint:disable=super-init-not-called # we must copy-and-paste from super
23
23
  """Initialize the model entry."""
24
24
  # copy-paste from django_celery_beat.schedulers
25
25
  self.app = app or current_app._get_current_object()
nautobot/core/filters.py CHANGED
@@ -668,7 +668,7 @@ class BaseFilterSet(django_filters.FilterSet):
668
668
  the form `<field_name>__<lookup_expr>`
669
669
  """
670
670
  magic_filters = {}
671
- if filter_field.method is not None or filter_field.lookup_expr not in ["exact", "in"]:
671
+ if filter_field.method is not None or filter_field.lookup_expr not in ["exact", "in", "iexact"]:
672
672
  return magic_filters
673
673
 
674
674
  # Choose the lookup expression map based on the filter type
@@ -679,7 +679,7 @@ class BaseFilterSet(django_filters.FilterSet):
679
679
 
680
680
  # Get properties of the existing filter for later use
681
681
  field_name = filter_field.field_name
682
- field = get_model_field(cls._meta.model, field_name)
682
+ field = get_model_field(cls._meta.model, field_name) # pylint: disable=no-member
683
683
 
684
684
  # If there isn't a model field, return.
685
685
  if field is None:
@@ -696,7 +696,7 @@ class BaseFilterSet(django_filters.FilterSet):
696
696
  new_filter_name = f"{filter_name}__{lookup_name}"
697
697
 
698
698
  try:
699
- if filter_name in cls.declared_filters and lookup_expr not in {"isnull"}:
699
+ if filter_name in cls.declared_filters and lookup_expr not in {"isnull"}: # pylint: disable=no-member
700
700
  # The filter field has been explicitly defined on the filterset class so we must manually
701
701
  # create the new filter with the same type because there is no guarantee the defined type
702
702
  # is the same as the default type for the field. This does not apply if the filter
@@ -727,7 +727,10 @@ class BaseFilterSet(django_filters.FilterSet):
727
727
  # If the base filter_field has a custom label, django_filters won't adjust it for the new_filter lookup,
728
728
  # so we have to do it.
729
729
  if filter_field.label and filter_field.label != label_for_filter(
730
- cls.Meta.model, filter_field.field_name, filter_field.lookup_expr, filter_field.exclude
730
+ cls._meta.model, # pylint: disable=no-member
731
+ filter_field.field_name,
732
+ filter_field.lookup_expr,
733
+ filter_field.exclude,
731
734
  ):
732
735
  # Lightly adjusted from label_for_filter() implementation:
733
736
  verbose_expression = ["exclude", filter_field.label] if new_filter.exclude else [filter_field.label]
@@ -750,22 +753,22 @@ class BaseFilterSet(django_filters.FilterSet):
750
753
  if not isinstance(new_filter_field, django_filters.Filter):
751
754
  raise TypeError(f"Tried to add filter ({new_filter_name}) which is not an instance of Django Filter")
752
755
 
753
- if new_filter_name in cls.base_filters:
756
+ if new_filter_name in cls.base_filters: # pylint: disable=no-member
754
757
  raise AttributeError(
755
758
  f"There was a conflict with filter `{new_filter_name}`, the custom filter was ignored."
756
759
  )
757
760
 
758
- cls.base_filters[new_filter_name] = new_filter_field
761
+ cls.base_filters[new_filter_name] = new_filter_field # pylint: disable=no-member
759
762
  # django-filters has no concept of "abstract" filtersets, so we have to fake it
760
- if cls._meta.model is not None:
761
- cls.base_filters.update(
763
+ if cls._meta.model is not None: # pylint: disable=no-member
764
+ cls.base_filters.update( # pylint: disable=no-member
762
765
  cls._generate_lookup_expression_filters(filter_name=new_filter_name, filter_field=new_filter_field)
763
766
  )
764
767
 
765
768
  @classmethod
766
769
  def get_fields(cls):
767
770
  fields = super().get_fields()
768
- if "id" not in fields and (cls._meta.exclude is None or "id" not in cls._meta.exclude):
771
+ if "id" not in fields and (cls._meta.exclude is None or "id" not in cls._meta.exclude): # pylint: disable=no-member
769
772
  # Add "id" as the first key in the `fields` dict
770
773
  fields = {"id": [django_filters.conf.settings.DEFAULT_LOOKUP_EXPR], **fields}
771
774
  return fields
@@ -782,7 +785,7 @@ class BaseFilterSet(django_filters.FilterSet):
782
785
  if filter_name.startswith("_"):
783
786
  del filters[filter_name]
784
787
 
785
- if getattr(cls._meta.model, "is_contact_associable_model", False):
788
+ if getattr(cls._meta.model, "is_contact_associable_model", False): # pylint: disable=no-member
786
789
  # Add "contacts" and "teams" filters
787
790
  from nautobot.extras.models import Contact, Team
788
791
 
@@ -802,13 +805,13 @@ class BaseFilterSet(django_filters.FilterSet):
802
805
  label="Teams (name or ID)",
803
806
  )
804
807
 
805
- if "dynamic_groups" not in filters and getattr(cls._meta.model, "is_dynamic_group_associable_model", False):
806
- if not hasattr(cls._meta.model, "static_group_association_set"):
808
+ if "dynamic_groups" not in filters and getattr(cls._meta.model, "is_dynamic_group_associable_model", False): # pylint: disable=no-member
809
+ if not hasattr(cls._meta.model, "static_group_association_set"): # pylint: disable=no-member
807
810
  logger.warning(
808
811
  "Model %s has 'is_dynamic_group_associable_model = True' but lacks "
809
812
  "a 'static_group_association_set' attribute. Perhaps this is due to it inheriting from "
810
813
  "the deprecated DynamicGroupMixin class instead of the preferred DynamicGroupsModelMixin?",
811
- cls._meta.model,
814
+ cls._meta.model, # pylint: disable=no-member
812
815
  )
813
816
  else:
814
817
  # Add "dynamic_groups" field as the last key
@@ -818,14 +821,14 @@ class BaseFilterSet(django_filters.FilterSet):
818
821
  queryset=DynamicGroup.objects.all(),
819
822
  field_name="static_group_association_set__dynamic_group",
820
823
  to_field_name="name",
821
- query_params={"content_type": cls._meta.model._meta.label_lower},
824
+ query_params={"content_type": cls._meta.model._meta.label_lower}, # pylint: disable=no-member
822
825
  label="Dynamic groups (name or ID)",
823
826
  )
824
827
 
825
828
  # django-filters has no concept of "abstract" filtersets, so we have to fake it
826
- if cls._meta.model is not None:
829
+ if cls._meta.model is not None: # pylint: disable=no-member
827
830
  if "tags" in filters and isinstance(filters["tags"], TagFilter):
828
- filters["tags"].extra["query_params"] = {"content_types": [cls._meta.model._meta.label_lower]}
831
+ filters["tags"].extra["query_params"] = {"content_types": [cls._meta.model._meta.label_lower]} # pylint: disable=no-member
829
832
 
830
833
  new_filters = {}
831
834
  for existing_filter_name, existing_filter in filters.items():
@@ -102,13 +102,13 @@ class CSVFileField(django_forms.FileField):
102
102
  "in double quotes."
103
103
  )
104
104
 
105
- def to_python(self, file):
105
+ def to_python(self, data):
106
106
  """For parity with CSVDataField, this returns the CSV text rather than an UploadedFile object."""
107
- if file is None:
107
+ if data is None:
108
108
  return None
109
109
 
110
- file = super().to_python(file)
111
- return file.read().decode("utf-8-sig").strip()
110
+ data = super().to_python(data)
111
+ return data.read().decode("utf-8-sig").strip()
112
112
 
113
113
 
114
114
  class CSVChoiceField(django_forms.ChoiceField):
@@ -774,7 +774,7 @@ class MultiMatchModelMultipleChoiceField(DynamicModelChoiceMixin, django_filters
774
774
  self.natural_key = kwargs.setdefault("to_field_name", "slug")
775
775
  super().__init__(*args, **kwargs)
776
776
 
777
- def _check_values(self, values):
777
+ def _check_values(self, values): # pylint:disable=arguments-renamed
778
778
  """
779
779
  This method overloads the grandparent method in `django.forms.models.ModelMultipleChoiceField`,
780
780
  re-using some of that method's existing logic and adding support for coupling this field with
@@ -10,7 +10,7 @@ class OptimizedNautobotObjectType(gql_optimizer.OptimizedDjangoObjectType):
10
10
  url = graphene.String()
11
11
 
12
12
  def resolve_url(self, info):
13
- return self.get_absolute_url(api=True)
13
+ return self.get_absolute_url(api=True) # pylint: disable=no-member
14
14
 
15
15
  class Meta:
16
16
  abstract = True
@@ -47,7 +47,7 @@ class GitRepositorySync(Job):
47
47
  description = "Clone and/or pull a Git repository, then refresh data sourced from this repository."
48
48
  has_sensitive_variables = False
49
49
 
50
- def run(self, repository):
50
+ def run(self, repository): # pylint:disable=arguments-differ
51
51
  job_result = self.job_result
52
52
  user = job_result.user
53
53
 
@@ -88,7 +88,7 @@ class GitRepositoryDryRun(Job):
88
88
  description = "Dry run of Git repository sync - will not update data sourced from this repository."
89
89
  has_sensitive_variables = False
90
90
 
91
- def run(self, repository):
91
+ def run(self, repository): # pylint:disable=arguments-differ
92
92
  job_result = self.job_result
93
93
  self.logger.info(f'Performing a Dry Run on Git repository "{repository.name}"...')
94
94
 
@@ -138,7 +138,7 @@ class ExportObjectList(Job):
138
138
  soft_time_limit = 1800
139
139
  time_limit = 2000
140
140
 
141
- def run(self, *, content_type, query_string="", export_format="csv", export_template=None):
141
+ def run(self, *, content_type, query_string="", export_format="csv", export_template=None): # pylint:disable=arguments-differ
142
142
  if not self.user.has_perm(f"{content_type.app_label}.view_{content_type.model}"):
143
143
  self.logger.error('User "%s" does not have permission to view %s objects', self.user, content_type.model)
144
144
  raise PermissionDenied("User does not have view permissions on the requested content-type")
@@ -286,7 +286,7 @@ class ImportObjects(Job):
286
286
  self.logger.error("Row %d: `%s`: `%s`", row, field, err[0])
287
287
  return new_objs, validation_failed
288
288
 
289
- def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False):
289
+ def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False): # pylint:disable=arguments-differ
290
290
  if not self.user.has_perm(f"{content_type.app_label}.add_{content_type.model}"):
291
291
  self.logger.error('User "%s" does not have permission to create %s objects', self.user, content_type.model)
292
292
  raise PermissionDenied("User does not have create permissions on the requested content-type")
@@ -70,7 +70,7 @@ class LogsCleanup(Job):
70
70
  deletion_summary.update(deleted_dict)
71
71
  return deletion_summary
72
72
 
73
- def run(self, *, cleanup_types, max_age=None):
73
+ def run(self, *, cleanup_types, max_age=None): # pylint: disable=arguments-differ
74
74
  if max_age in (None, ""):
75
75
  max_age = get_settings_or_config("CHANGELOG_RETENTION")
76
76
  if max_age == 0:
@@ -24,7 +24,7 @@ class RefreshDynamicGroupCaches(Job):
24
24
  description = "Re-calculate and re-cache the membership lists of Dynamic Groups."
25
25
  has_sensitive_variables = False
26
26
 
27
- def run(self, single_group=None):
27
+ def run(self, single_group=None): # pylint: disable=arguments-differ
28
28
  groups = DynamicGroup.objects.restrict(self.user, "view").exclude(
29
29
  group_type=DynamicGroupTypeChoices.TYPE_STATIC
30
30
  )
@@ -63,7 +63,7 @@ class Command(BaseCommand):
63
63
  for model in models:
64
64
  model_name = f"{model._meta.app_label}.{model.__name__}"
65
65
  # Most swap out for user_model
66
- if model_name == "auth.User":
66
+ if model_name == "auth.User": # pylint: disable=hard-coded-auth-user
67
67
  model = get_user_model()
68
68
  # Skip models that aren't actually in the database
69
69
  if not model._meta.managed:
@@ -270,7 +270,7 @@ class BaseModel(models.Model):
270
270
 
271
271
  if not natural_key_field_names:
272
272
  raise AttributeError(
273
- f"Unable to identify an intrinsic natural-key definition for {cls.__name__}. "
273
+ f"Unable to identify an intrinsic natural-key definition for {cls.__name__}. " # pylint: disable=no-member
274
274
  "If there isn't at least one UniqueConstraint, unique_together, or field with unique=True, "
275
275
  "you probably need to explicitly declare the 'natural_key_field_names' for this model, "
276
276
  "or potentially override the default 'natural_key_field_lookups' implementation for this model."
@@ -69,7 +69,7 @@ class JSONSet(Func):
69
69
  }
70
70
  return c
71
71
 
72
- def as_sql(self, compiler, connection, function=None, **extra_context):
72
+ def as_sql(self, compiler, connection, function=None, **extra_context): # pylint:disable=arguments-differ
73
73
  """
74
74
  MySQL implementation based on https://github.com/django/django/pull/18489/files.
75
75
 
@@ -138,7 +138,7 @@ class JSONRemove(Func):
138
138
  self.path = path
139
139
  super().__init__(expression)
140
140
 
141
- def as_sql(self, compiler, connection, function=None, **extra_context):
141
+ def as_sql(self, compiler, connection, function=None, **extra_context): # pylint:disable=arguments-differ
142
142
  """
143
143
  MySQL implementation based on https://github.com/django/django/pull/18489/files.
144
144
 
@@ -114,12 +114,12 @@ class TreeModel(TreeNode):
114
114
  if self.parent_id is not None:
115
115
  parent_display_str = cache.get(cache_key.replace(str(self.id), str(self.parent_id)), "")
116
116
  if not parent_display_str:
117
- parent_display_str = self.parent.display
117
+ parent_display_str = self.parent.display # pylint: disable=no-member
118
118
  display_str = parent_display_str + " → "
119
119
  except self.DoesNotExist:
120
120
  # Expected to occur at times during bulk-delete operations
121
121
  pass
122
- display_str += self.name
122
+ display_str += self.name # pylint: disable=no-member # we checked with hasattr() above
123
123
  cache.set(cache_key, display_str, 5)
124
124
  return display_str
125
125
 
nautobot/core/tables.py CHANGED
@@ -480,7 +480,7 @@ class ChoiceFieldColumn(django_tables2.Column):
480
480
  choices. The CSS class is derived by calling .get_FOO_class() on the row record.
481
481
  """
482
482
 
483
- def render(self, record, bound_column, value): # pylint: disable=arguments-differ
483
+ def render(self, *, record, bound_column, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
484
484
  if value:
485
485
  name = bound_column.name
486
486
  css_class = getattr(record, f"get_{name}_class")()
@@ -582,7 +582,7 @@ class LinkedCountColumn(django_tables2.Column):
582
582
  self.model = get_model_for_view_name(self.viewname)
583
583
  super().__init__(*args, default=default, **kwargs)
584
584
 
585
- def render(self, bound_column, record, value): # pylint: disable=arguments-differ
585
+ def render(self, *, bound_column, record, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
586
586
  related_record = None
587
587
  try:
588
588
  lookup = self.lookup or get_related_field_for_models(bound_column._table._meta.model, self.model).name
@@ -666,7 +666,7 @@ class ComputedFieldColumn(django_tables2.Column):
666
666
 
667
667
  super().__init__(*args, **kwargs)
668
668
 
669
- def render(self, record):
669
+ def render(self, *, record): # pylint: disable=arguments-differ # tables2 varies its kwargs
670
670
  return self.computedfield.render({"obj": record})
671
671
 
672
672
 
@@ -685,7 +685,7 @@ class CustomFieldColumn(django_tables2.Column):
685
685
 
686
686
  super().__init__(*args, **kwargs)
687
687
 
688
- def render(self, record, bound_column, value): # pylint: disable=arguments-differ
688
+ def render(self, *, record, bound_column, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
689
689
  if self.customfield.type == choices.CustomFieldTypeChoices.TYPE_BOOLEAN:
690
690
  template = helpers.render_boolean(value)
691
691
  elif self.customfield.type == choices.CustomFieldTypeChoices.TYPE_MULTISELECT:
@@ -716,7 +716,7 @@ class RelationshipColumn(django_tables2.Column):
716
716
  kwargs.setdefault("accessor", Accessor("associations"))
717
717
  super().__init__(orderable=False, *args, **kwargs)
718
718
 
719
- def render(self, record, value): # pylint: disable=arguments-differ
719
+ def render(self, *, record, value): # pylint: disable=arguments-differ # tables2 varies its kwargs
720
720
  # Filter the relationship associations by the relationship instance.
721
721
  # Since associations accessor returns all the relationship associations regardless of the relationship.
722
722
  value = [v for v in value if v.relationship == self.relationship]
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations # python 3.8
2
+
1
3
  import random
2
4
  import string
3
5
 
4
6
  from django.contrib.contenttypes.models import ContentType
5
- from django.db.models import Count, Q
7
+ from django.db.models import Count, Q, QuerySet
6
8
  from django.db.models.fields import CharField, TextField
7
9
  from django.db.models.fields.related import ManyToManyField
8
10
  from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
9
11
  from django.test import tag
12
+ from django_filters import FilterSet
10
13
 
11
14
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
12
15
  from nautobot.core.filters import (
@@ -27,6 +30,8 @@ class FilterTestCases:
27
30
  class BaseFilterTestCase(views.TestCase):
28
31
  """Base class for testing of FilterSets."""
29
32
 
33
+ queryset: QuerySet
34
+
30
35
  def get_filterset_test_values(self, field_name, queryset=None):
31
36
  """Returns a list of distinct values from the requested queryset field to use in filterset tests.
32
37
 
@@ -68,8 +73,7 @@ class FilterTestCases:
68
73
  class FilterTestCase(BaseFilterTestCase):
69
74
  """Add common tests for all FilterSets."""
70
75
 
71
- queryset = None
72
- filterset = None
76
+ filterset: type[FilterSet]
73
77
 
74
78
  # filter predicate fields that should be excluded from q test case
75
79
  exclude_q_filter_predicates = []
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations # python 3.8
2
+
1
3
  import contextlib
2
4
  import re
3
5
  from typing import Optional, Sequence
@@ -21,6 +23,7 @@ from nautobot.core.models.tree_queries import TreeModel
21
23
  from nautobot.core.templatetags import helpers
22
24
  from nautobot.core.testing import mixins, utils
23
25
  from nautobot.core.utils import lookup
26
+ from nautobot.dcim.models.device_components import ComponentModel
24
27
  from nautobot.extras import choices as extras_choices, models as extras_models, querysets as extras_querysets
25
28
  from nautobot.extras.forms import CustomFieldModelFormMixin, RelationshipModelFormMixin
26
29
  from nautobot.extras.models import CustomFieldModel, RelationshipModel
@@ -1528,6 +1531,8 @@ class ViewTestCases:
1528
1531
  maxDiff = None
1529
1532
  bulk_add_data = None
1530
1533
  """Used for bulk-add (distinct from bulk-create) view testing; self.bulk_create_data will be used if unset."""
1534
+ selected_objects: list[ComponentModel]
1535
+ selected_objects_parent_name: str
1531
1536
 
1532
1537
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1533
1538
  def test_bulk_add_component(self):
@@ -142,7 +142,7 @@ class NautobotTestRunner(DiscoverRunner):
142
142
  # branches/releases of Nautobot in separate files.
143
143
  hexdigest = hashlib.shake_128(
144
144
  ",".join(
145
- sorted(f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all())
145
+ sorted(f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all()) # pylint: disable=no-member
146
146
  ).encode("utf-8")
147
147
  ).hexdigest(10)
148
148
  command += ["--fixture-file", f"development/factory_dump.{hexdigest}.json"]