django-display-ids 0.4.0__py3-none-any.whl → 0.5.0__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.
@@ -5,7 +5,7 @@ Settings can be configured in Django settings under the DISPLAY_IDS namespace:
5
5
  DISPLAY_IDS = {
6
6
  "UUID_FIELD": "uid",
7
7
  "SLUG_FIELD": "slug",
8
- "STRATEGIES": ("display_id", "uuid"),
8
+ "STRATEGIES": ("display_id", "uuid", "slug"),
9
9
  }
10
10
  """
11
11
 
@@ -37,7 +37,7 @@ SLUG_REGEX: str = SlugConverter.regex
37
37
  DEFAULTS: dict[str, str | tuple[str, ...]] = {
38
38
  "UUID_FIELD": "id",
39
39
  "SLUG_FIELD": "slug",
40
- "STRATEGIES": ("display_id", "uuid"),
40
+ "STRATEGIES": ("display_id", "uuid", "slug"),
41
41
  "SLUG_REGEX": SLUG_REGEX,
42
42
  }
43
43
 
@@ -13,9 +13,7 @@ from django_display_ids.conf import (
13
13
  from django_display_ids.encoding import PREFIX_PATTERN
14
14
  from django_display_ids.exceptions import (
15
15
  DisplayIDLookupError,
16
- InvalidIdentifierError,
17
16
  ObjectNotFoundError,
18
- UnknownPrefixError,
19
17
  )
20
18
  from django_display_ids.resolver import resolve_object
21
19
  from django_display_ids.typing import StrategyName # noqa: TC001 - used at runtime
@@ -57,7 +55,6 @@ class DisplayIDMixin:
57
55
  Example:
58
56
  class InvoiceView(DisplayIDMixin, APIView):
59
57
  lookup_url_kwarg = "id"
60
- lookup_strategies = ("display_id", "uuid")
61
58
  display_id_prefix = "inv"
62
59
 
63
60
  def get(self, request, *args, **kwargs):
@@ -69,7 +66,6 @@ class DisplayIDMixin:
69
66
  queryset = Invoice.objects.all()
70
67
  serializer_class = InvoiceSerializer
71
68
  lookup_url_kwarg = "pk"
72
- lookup_strategies = ("display_id", "uuid")
73
69
  """
74
70
 
75
71
  lookup_url_kwarg: str = "pk"
@@ -165,8 +161,6 @@ class DisplayIDMixin:
165
161
  )
166
162
  except ObjectNotFoundError as e:
167
163
  raise NotFound(str(e)) from e
168
- except (InvalidIdentifierError, UnknownPrefixError) as e:
169
- raise ParseError(str(e)) from e
170
164
  except DisplayIDLookupError as e:
171
165
  raise ParseError(str(e)) from e
172
166
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from .conf import SLUG_REGEX
5
+ from .conf import SLUG_REGEX, get_setting
6
6
 
7
7
  __all__ = [
8
8
  "DISPLAY_ID_REGEX",
@@ -19,6 +19,9 @@ __all__ = [
19
19
  DISPLAY_ID_REGEX = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
20
20
  UUID_REGEX = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
21
21
 
22
+ # Slug regex from settings (respects DISPLAY_IDS["SLUG_REGEX"] Django setting)
23
+ _SLUG_REGEX: str = str(get_setting("SLUG_REGEX"))
24
+
22
25
 
23
26
  class BaseConverter:
24
27
  """Base class for URL path converters with pass-through conversion."""
@@ -78,7 +81,7 @@ class DisplayIDOrSlugConverter(BaseConverter):
78
81
 
79
82
  Matches either format:
80
83
  - Display ID: {prefix}_{base62}
81
- - Slug: Django's default slug pattern [-a-zA-Z0-9_]+
84
+ - Slug: matches DISPLAY_IDS["SLUG_REGEX"] setting (default: [-a-zA-Z0-9_]+)
82
85
 
83
86
  Example:
84
87
  from django.urls import path, register_converter
@@ -91,7 +94,7 @@ class DisplayIDOrSlugConverter(BaseConverter):
91
94
  ]
92
95
  """
93
96
 
94
- regex = rf"(?:{DISPLAY_ID_REGEX}|{SLUG_REGEX})"
97
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{_SLUG_REGEX})"
95
98
 
96
99
 
97
100
  class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
@@ -100,7 +103,7 @@ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
100
103
  Matches any of:
101
104
  - Display ID: {prefix}_{base62}
102
105
  - UUID: hyphenated (e.g., 550e8400-e29b-41d4-a716-446655440000)
103
- - Slug: Django's default slug pattern [-a-zA-Z0-9_]+
106
+ - Slug: matches DISPLAY_IDS["SLUG_REGEX"] setting (default: [-a-zA-Z0-9_]+)
104
107
 
105
108
  Example:
106
109
  from django.urls import path, register_converter
@@ -113,7 +116,7 @@ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
113
116
  ]
114
117
  """
115
118
 
116
- regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{SLUG_REGEX})"
119
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{_SLUG_REGEX})"
117
120
 
118
121
 
119
122
  def make_display_id_or_slug_converter(
@@ -140,8 +143,6 @@ def make_display_id_or_slug_converter(
140
143
  path("products/<display_id_or_slug:id>/", ProductDetailView.as_view()),
141
144
  ]
142
145
  """
143
- from .conf import get_setting
144
-
145
146
  pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
146
147
 
147
148
  class CustomDisplayIDOrSlugConverter(DisplayIDOrSlugConverter):
@@ -176,8 +177,6 @@ def make_display_id_or_uuid_or_slug_converter(
176
177
  path("products/<identifier:id>/", ProductDetailView.as_view()),
177
178
  ]
178
179
  """
179
- from .conf import get_setting
180
-
181
180
  pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
182
181
 
183
182
  class CustomDisplayIDOrUUIDOrSlugConverter(DisplayIDOrUUIDOrSlugConverter):
@@ -1,7 +1,26 @@
1
- """Typed exceptions for identifier lookup errors."""
1
+ """Typed exceptions for identifier lookup errors.
2
+
3
+ All exceptions inherit from both ``DisplayIDLookupError`` and a standard
4
+ Django/Python exception, so they integrate naturally with existing error
5
+ handling patterns::
6
+
7
+ # Catch with library-specific base
8
+ except DisplayIDLookupError: ...
9
+
10
+ # Or catch with standard Django/Python exceptions
11
+ except ObjectDoesNotExist: ... # catches ObjectNotFoundError
12
+ except ValueError: ... # catches InvalidIdentifierError, UnknownPrefixError
13
+ except ImproperlyConfigured: ... # catches MissingPrefixError
14
+ """
2
15
 
3
16
  from __future__ import annotations
4
17
 
18
+ from django.core.exceptions import (
19
+ ImproperlyConfigured,
20
+ MultipleObjectsReturned,
21
+ ObjectDoesNotExist,
22
+ )
23
+
5
24
  __all__ = [
6
25
  "AmbiguousIdentifierError",
7
26
  "DisplayIDLookupError",
@@ -18,11 +37,14 @@ class DisplayIDLookupError(Exception):
18
37
  pass
19
38
 
20
39
 
21
- class InvalidIdentifierError(DisplayIDLookupError):
40
+ class InvalidIdentifierError(DisplayIDLookupError, ValueError):
22
41
  """Raised when an identifier has an invalid format.
23
42
 
24
43
  This indicates the identifier string cannot be parsed as any
25
44
  of the supported formats (UUID, display ID, or slug).
45
+
46
+ Inherits from ``ValueError`` because it represents bad input — the
47
+ caller provided a value that isn't a valid identifier.
26
48
  """
27
49
 
28
50
  def __init__(self, value: str, message: str | None = None) -> None:
@@ -31,11 +53,14 @@ class InvalidIdentifierError(DisplayIDLookupError):
31
53
  super().__init__(self.message)
32
54
 
33
55
 
34
- class UnknownPrefixError(DisplayIDLookupError):
56
+ class UnknownPrefixError(DisplayIDLookupError, ValueError):
35
57
  """Raised when a display ID has an unexpected prefix.
36
58
 
37
59
  This occurs when prefix enforcement is enabled and the
38
60
  display ID's prefix doesn't match the expected value.
61
+
62
+ Inherits from ``ValueError`` because it represents bad input — the
63
+ caller provided a display ID with the wrong prefix.
39
64
  """
40
65
 
41
66
  def __init__(self, value: str, actual: str, expected: str | None = None) -> None:
@@ -49,11 +74,14 @@ class UnknownPrefixError(DisplayIDLookupError):
49
74
  super().__init__(message)
50
75
 
51
76
 
52
- class MissingPrefixError(DisplayIDLookupError):
77
+ class MissingPrefixError(DisplayIDLookupError, ImproperlyConfigured):
53
78
  """Raised when a display ID lookup is attempted without a prefix.
54
79
 
55
80
  This occurs when calling get_by_display_id() on a model that
56
81
  doesn't have display_id_prefix configured.
82
+
83
+ Inherits from ``ImproperlyConfigured`` because it represents a
84
+ configuration problem — the model is missing a required setting.
57
85
  """
58
86
 
59
87
  def __init__(self, model_name: str | None = None) -> None:
@@ -68,11 +96,14 @@ class MissingPrefixError(DisplayIDLookupError):
68
96
  super().__init__(message)
69
97
 
70
98
 
71
- class ObjectNotFoundError(DisplayIDLookupError):
99
+ class ObjectNotFoundError(DisplayIDLookupError, ObjectDoesNotExist):
72
100
  """Raised when no object matches the identifier.
73
101
 
74
102
  This indicates the identifier was valid but no matching
75
103
  database record exists.
104
+
105
+ Inherits from ``ObjectDoesNotExist`` so it integrates with Django's
106
+ built-in error handling (e.g., ``get_object_or_404``).
76
107
  """
77
108
 
78
109
  def __init__(self, value: str, model_name: str | None = None) -> None:
@@ -85,11 +116,14 @@ class ObjectNotFoundError(DisplayIDLookupError):
85
116
  super().__init__(message)
86
117
 
87
118
 
88
- class AmbiguousIdentifierError(DisplayIDLookupError):
119
+ class AmbiguousIdentifierError(DisplayIDLookupError, MultipleObjectsReturned):
89
120
  """Raised when an identifier matches multiple objects.
90
121
 
91
122
  This can occur with slug lookups if slugs are not unique,
92
123
  or in edge cases with identifier collisions.
124
+
125
+ Inherits from ``MultipleObjectsReturned`` because the semantics
126
+ are identical — a lookup that expected one result found many.
93
127
  """
94
128
 
95
129
  def __init__(self, value: str, count: int) -> None:
@@ -2,19 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, TypeVar
5
+ import uuid
6
+ from typing import TYPE_CHECKING, Any, Self, TypeVar
6
7
 
8
+ from django.core.exceptions import FieldDoesNotExist
7
9
  from django.db import models
8
10
  from django.db.models import Q
9
11
 
10
12
  from .conf import get_setting
11
13
  from .encoding import decode_display_id
12
14
  from .exceptions import (
13
- AmbiguousIdentifierError,
14
- InvalidIdentifierError,
15
+ DisplayIDLookupError,
15
16
  MissingPrefixError,
16
- ObjectNotFoundError,
17
- UnknownPrefixError,
18
17
  )
19
18
  from .strategies import parse_identifier
20
19
 
@@ -49,57 +48,88 @@ class DisplayIDQuerySet(models.QuerySet[M]):
49
48
  invoice = Invoice.objects.get_by_display_id("inv_1a2B3c4D5e6F7g8H")
50
49
  """
51
50
 
51
+ # Re-annotate inherited QuerySet methods with -> Self so that
52
+ # display ID methods remain visible to type checkers after chaining
53
+ # (e.g. Invoice.objects.filter(...).get_by_identifier(...)).
54
+ def filter(self, *args: Any, **kwargs: Any) -> Self:
55
+ return super().filter(*args, **kwargs)
56
+
57
+ def exclude(self, *args: Any, **kwargs: Any) -> Self:
58
+ return super().exclude(*args, **kwargs)
59
+
60
+ def select_related(self, *fields: Any) -> Self:
61
+ return super().select_related(*fields)
62
+
63
+ def prefetch_related(self, *lookups: Any) -> Self:
64
+ return super().prefetch_related(*lookups)
65
+
66
+ def order_by(self, *fields: Any) -> Self:
67
+ return super().order_by(*fields)
68
+
69
+ def distinct(self, *fields: Any) -> Self:
70
+ return super().distinct(*fields)
71
+
72
+ def all(self) -> Self:
73
+ return super().all()
74
+
75
+ def none(self) -> Self:
76
+ return super().none()
77
+
52
78
  def get_by_display_id(
53
79
  self,
54
- value: str,
80
+ value: str | uuid.UUID,
55
81
  *,
56
82
  prefix: str | None = None,
57
83
  ) -> M:
58
84
  """Get an object by its display ID.
59
85
 
60
86
  Args:
61
- value: The display ID string (e.g., "inv_1a2B3c4D5e6F7g8H").
87
+ value: The display ID string (e.g., "inv_1a2B3c4D5e6F7g8H"),
88
+ or a UUID instance for direct UUID lookup.
62
89
  prefix: Expected prefix for validation. If None, uses model's prefix.
63
90
 
64
91
  Returns:
65
92
  The matching model instance.
66
93
 
67
94
  Raises:
95
+ Model.DoesNotExist: If no matching object exists, if the display ID
96
+ format is invalid, or if the prefix doesn't match.
68
97
  MissingPrefixError: If no prefix is configured on the model.
69
- InvalidIdentifierError: If the display ID format is invalid.
70
- UnknownPrefixError: If the prefix doesn't match expected.
71
- ObjectNotFoundError: If no matching object exists.
72
98
  """
73
- # Get model config
74
99
  model = self.model
75
100
  uuid_field = self._get_uuid_field()
101
+
102
+ # UUID objects skip display ID parsing entirely
103
+ if isinstance(value, uuid.UUID):
104
+ return self.get(**{uuid_field: value})
105
+
106
+ # Get model config
76
107
  expected_prefix = prefix or self._get_model_prefix()
77
108
 
78
109
  # Require a prefix for display ID lookups
79
110
  if expected_prefix is None:
80
111
  raise MissingPrefixError(model_name=model.__name__)
81
112
 
82
- # Decode the display ID
113
+ # Decode the display ID and validate prefix
83
114
  try:
84
115
  decoded_prefix, uuid_value = decode_display_id(value)
85
116
  except ValueError as e:
86
- raise InvalidIdentifierError(value, str(e)) from e
117
+ raise model.DoesNotExist( # type: ignore[attr-defined]
118
+ f"{model.__name__}: invalid display ID: {value!r}"
119
+ ) from e
87
120
 
88
- # Validate prefix
89
121
  if decoded_prefix != expected_prefix:
90
- raise UnknownPrefixError(
91
- value, actual=decoded_prefix, expected=expected_prefix
122
+ raise model.DoesNotExist( # type: ignore[attr-defined]
123
+ f"{model.__name__}: unknown prefix {decoded_prefix!r} "
124
+ f"in {value!r}, expected {expected_prefix!r}"
92
125
  )
93
126
 
94
127
  # Query the database
95
- try:
96
- return self.get(**{uuid_field: uuid_value})
97
- except model.DoesNotExist: # type: ignore[attr-defined]
98
- raise ObjectNotFoundError(value, model_name=model.__name__) from None
128
+ return self.get(**{uuid_field: uuid_value})
99
129
 
100
130
  def get_by_identifier(
101
131
  self,
102
- value: str,
132
+ value: str | uuid.UUID,
103
133
  *,
104
134
  strategies: tuple[StrategyName, ...] | None = None,
105
135
  prefix: str | None = None,
@@ -109,7 +139,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
109
139
  Tries each strategy in order and returns the first match.
110
140
 
111
141
  Args:
112
- value: The identifier string (display ID, UUID, or slug).
142
+ value: The identifier string (display ID, UUID, or slug),
143
+ or a UUID instance for direct UUID lookup.
113
144
  strategies: Strategies to try. Defaults to settings.
114
145
  prefix: Expected display ID prefix for validation.
115
146
 
@@ -117,21 +148,34 @@ class DisplayIDQuerySet(models.QuerySet[M]):
117
148
  The matching model instance.
118
149
 
119
150
  Raises:
120
- InvalidIdentifierError: If no strategy can parse the identifier.
121
- UnknownPrefixError: If display ID prefix doesn't match expected.
122
- ObjectNotFoundError: If no matching object exists.
123
- AmbiguousIdentifierError: If multiple objects match (slug).
151
+ Model.DoesNotExist: If the identifier cannot be parsed or
152
+ no matching object exists.
153
+ Model.MultipleObjectsReturned: If multiple objects match (slug).
124
154
  """
125
155
  model = self.model
126
156
  uuid_field = self._get_uuid_field()
157
+
158
+ # UUID objects skip strategy parsing entirely
159
+ if isinstance(value, uuid.UUID):
160
+ return self.get(**{uuid_field: value})
161
+
127
162
  slug_field = self._get_slug_field()
128
163
  expected_prefix = prefix or self._get_model_prefix()
129
164
  lookup_strategies = strategies or self._get_strategies()
130
165
 
166
+ # Skip slug strategy if the model has no slug field
167
+ if not self._has_slug_field(slug_field):
168
+ lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
169
+
131
170
  # Parse the identifier
132
- result = parse_identifier(
133
- value, lookup_strategies, expected_prefix=expected_prefix
134
- )
171
+ try:
172
+ result = parse_identifier(
173
+ value, lookup_strategies, expected_prefix=expected_prefix
174
+ )
175
+ except DisplayIDLookupError as e:
176
+ raise model.DoesNotExist( # type: ignore[attr-defined]
177
+ f"{model.__name__}: {e}"
178
+ ) from e
135
179
 
136
180
  # Build the lookup
137
181
  lookup: dict[str, Any]
@@ -141,17 +185,11 @@ class DisplayIDQuerySet(models.QuerySet[M]):
141
185
  lookup = {slug_field: result.slug}
142
186
 
143
187
  # Execute the query
144
- try:
145
- return self.get(**lookup)
146
- except model.DoesNotExist: # type: ignore[attr-defined]
147
- raise ObjectNotFoundError(value, model_name=model.__name__) from None
148
- except model.MultipleObjectsReturned: # type: ignore[attr-defined]
149
- count = self.filter(**lookup).count()
150
- raise AmbiguousIdentifierError(value, count) from None
188
+ return self.get(**lookup)
151
189
 
152
190
  def get_by_identifiers(
153
191
  self,
154
- values: Sequence[str],
192
+ values: Sequence[str | uuid.UUID],
155
193
  *,
156
194
  strategies: tuple[StrategyName, ...] | None = None,
157
195
  prefix: str | None = None,
@@ -162,7 +200,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
162
200
  then executes a single database query using `__in` lookups.
163
201
 
164
202
  Args:
165
- values: A sequence of identifier strings (display IDs, UUIDs, or slugs).
203
+ values: A sequence of identifier strings (display IDs, UUIDs, or slugs)
204
+ or UUID instances. UUID instances skip strategy parsing.
166
205
  strategies: Strategies to try. Defaults to settings.
167
206
  prefix: Expected display ID prefix for validation.
168
207
 
@@ -178,6 +217,7 @@ class DisplayIDQuerySet(models.QuerySet[M]):
178
217
  'inv_2aUyqjCzEIiEcYMKj7TZtw',
179
218
  'inv_7kN3xPqRmLwYvTzJ5HfUaB',
180
219
  '550e8400-e29b-41d4-a716-446655440000',
220
+ uuid.UUID('550e8400-e29b-41d4-a716-446655440000'),
181
221
  ])
182
222
  """
183
223
  if not values:
@@ -188,11 +228,20 @@ class DisplayIDQuerySet(models.QuerySet[M]):
188
228
  expected_prefix = prefix or self._get_model_prefix()
189
229
  lookup_strategies = strategies or self._get_strategies()
190
230
 
231
+ # Skip slug strategy if the model has no slug field
232
+ if not self._has_slug_field(slug_field):
233
+ lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
234
+
191
235
  # Collect UUIDs and slugs separately
192
236
  uuids: list[Any] = []
193
237
  slugs: list[str] = []
194
238
 
195
239
  for value in values:
240
+ # UUID objects skip strategy parsing entirely
241
+ if isinstance(value, uuid.UUID):
242
+ uuids.append(value)
243
+ continue
244
+
196
245
  result = parse_identifier(
197
246
  value, lookup_strategies, expected_prefix=expected_prefix
198
247
  )
@@ -228,6 +277,14 @@ class DisplayIDQuerySet(models.QuerySet[M]):
228
277
  """Get the default strategies."""
229
278
  return get_setting("STRATEGIES") # type: ignore[return-value]
230
279
 
280
+ def _has_slug_field(self, slug_field: str) -> bool:
281
+ """Check whether the model has the configured slug field."""
282
+ try:
283
+ self.model._meta.get_field(slug_field)
284
+ return True
285
+ except FieldDoesNotExist:
286
+ return False
287
+
231
288
  def _get_model_prefix(self) -> str | None:
232
289
  """Get the display ID prefix from the model, if defined."""
233
290
  if hasattr(self.model, "get_display_id_prefix"):
@@ -253,7 +310,7 @@ class DisplayIDManager(models.Manager[M]):
253
310
 
254
311
  def get_by_display_id(
255
312
  self,
256
- value: str,
313
+ value: str | uuid.UUID,
257
314
  *,
258
315
  prefix: str | None = None,
259
316
  ) -> M:
@@ -265,7 +322,7 @@ class DisplayIDManager(models.Manager[M]):
265
322
 
266
323
  def get_by_identifier(
267
324
  self,
268
- value: str,
325
+ value: str | uuid.UUID,
269
326
  *,
270
327
  strategies: tuple[StrategyName, ...] | None = None,
271
328
  prefix: str | None = None,
@@ -280,7 +337,7 @@ class DisplayIDManager(models.Manager[M]):
280
337
 
281
338
  def get_by_identifiers(
282
339
  self,
283
- values: Sequence[str],
340
+ values: Sequence[str | uuid.UUID],
284
341
  *,
285
342
  strategies: tuple[StrategyName, ...] | None = None,
286
343
  prefix: str | None = None,
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import uuid
5
6
  from typing import TYPE_CHECKING, Any, TypeVar
6
7
 
8
+ from django.core.exceptions import FieldDoesNotExist
7
9
  from django.db import models
8
10
 
9
11
  from .exceptions import AmbiguousIdentifierError, ObjectNotFoundError
@@ -23,7 +25,7 @@ M = TypeVar("M", bound=models.Model)
23
25
  def resolve_object(
24
26
  *,
25
27
  model: type[M],
26
- value: str,
28
+ value: str | uuid.UUID,
27
29
  strategies: tuple[StrategyName, ...] = DEFAULT_STRATEGIES,
28
30
  prefix: str | None = None,
29
31
  uuid_field: str = "id",
@@ -36,7 +38,8 @@ def resolve_object(
36
38
 
37
39
  Args:
38
40
  model: The Django model class.
39
- value: The identifier string (UUID, display ID, or slug).
41
+ value: The identifier string (UUID, display ID, or slug),
42
+ or a UUID instance for direct UUID lookup.
40
43
  strategies: Tuple of strategy names to try in order.
41
44
  prefix: Expected display ID prefix (for validation).
42
45
  uuid_field: Name of the UUID field on the model.
@@ -53,9 +56,6 @@ def resolve_object(
53
56
  AmbiguousIdentifierError: If multiple objects match (slug lookup).
54
57
  TypeError: If queryset is not for the specified model.
55
58
  """
56
- # Parse the identifier to determine type
57
- result = parse_identifier(value, strategies, expected_prefix=prefix)
58
-
59
59
  # Get the base queryset
60
60
  if queryset is not None:
61
61
  if queryset.model is not model:
@@ -67,6 +67,22 @@ def resolve_object(
67
67
  else:
68
68
  qs = model._default_manager.all()
69
69
 
70
+ # UUID objects skip strategy parsing entirely
71
+ if isinstance(value, uuid.UUID):
72
+ try:
73
+ return qs.get(**{uuid_field: value})
74
+ except model.DoesNotExist: # type: ignore[attr-defined]
75
+ raise ObjectNotFoundError(str(value), model_name=model.__name__) from None
76
+
77
+ # Skip slug strategy if the model has no slug field
78
+ try:
79
+ model._meta.get_field(slug_field)
80
+ except FieldDoesNotExist:
81
+ strategies = tuple(s for s in strategies if s != "slug")
82
+
83
+ # Parse the identifier to determine type
84
+ result = parse_identifier(value, strategies, expected_prefix=prefix)
85
+
70
86
  # Build the lookup based on strategy
71
87
  lookup: dict[str, Any]
72
88
  if result.strategy in ("uuid", "display_id"):
@@ -12,6 +12,7 @@ __all__ = [
12
12
  # Supported lookup strategy names
13
13
  StrategyName = Literal["uuid", "display_id", "slug"]
14
14
 
15
- # Default strategy order: display_id first (most specific), then uuid
16
- # Slug is excluded by default since it's a catch-all that matches any string
17
- DEFAULT_STRATEGIES: tuple[StrategyName, ...] = ("display_id", "uuid")
15
+ # Default strategy order: display_id first (most specific), then uuid, then slug
16
+ # Slug is a catch-all it's safe to include by default because the manager
17
+ # and resolver automatically skip it for models without a slug field.
18
+ DEFAULT_STRATEGIES: tuple[StrategyName, ...] = ("display_id", "uuid", "slug")
@@ -8,12 +8,7 @@ from django.http import Http404
8
8
 
9
9
  from .conf import NOT_SET, get_setting, get_slug_field, get_uuid_field
10
10
  from .encoding import PREFIX_PATTERN
11
- from .exceptions import (
12
- DisplayIDLookupError,
13
- InvalidIdentifierError,
14
- ObjectNotFoundError,
15
- UnknownPrefixError,
16
- )
11
+ from .exceptions import DisplayIDLookupError
17
12
  from .resolver import resolve_object
18
13
  from .typing import StrategyName # noqa: TC001 - used at runtime in type hints
19
14
 
@@ -43,7 +38,6 @@ class DisplayIDMixin:
43
38
  class InvoiceDetailView(DisplayIDMixin, DetailView):
44
39
  model = Invoice
45
40
  lookup_param = "id"
46
- lookup_strategies = ("display_id", "uuid")
47
41
  display_id_prefix = "inv"
48
42
  """
49
43
 
@@ -136,9 +130,5 @@ class DisplayIDMixin:
136
130
  slug_field=self._get_slug_field(),
137
131
  queryset=qs,
138
132
  )
139
- except ObjectNotFoundError as e:
140
- raise Http404(str(e)) from e
141
- except (InvalidIdentifierError, UnknownPrefixError) as e:
142
- raise Http404(str(e)) from e
143
133
  except DisplayIDLookupError as e:
144
134
  raise Http404(str(e)) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-display-ids
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
5
5
  Keywords: django,stripe,uuid,base62,prefixed-id,drf,shortuuid,nanoid,ulid
6
6
  License: MIT
@@ -19,6 +19,7 @@ Requires-Dist: django>=4.2
19
19
  Requires-Python: >=3.12
20
20
  Project-URL: Documentation, https://django-display-ids.readthedocs.io/
21
21
  Project-URL: Repository, https://github.com/josephabrahams/django-display-ids
22
+ Project-URL: Changelog, https://github.com/josephabrahams/django-display-ids/blob/main/CHANGELOG.md
22
23
  Description-Content-Type: text/markdown
23
24
 
24
25
  # django-display-ids
@@ -71,7 +72,6 @@ from django_display_ids import DisplayIDMixin
71
72
  class InvoiceDetailView(DisplayIDMixin, DetailView):
72
73
  model = Invoice
73
74
  lookup_param = "id"
74
- lookup_strategies = ("display_id", "uuid", "slug")
75
75
  display_id_prefix = "inv"
76
76
  ```
77
77
 
@@ -85,7 +85,6 @@ class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
85
85
  queryset = Invoice.objects.all()
86
86
  serializer_class = InvoiceSerializer
87
87
  lookup_url_kwarg = "id"
88
- lookup_strategies = ("display_id", "uuid", "slug")
89
88
  display_id_prefix = "inv"
90
89
  ```
91
90
 
@@ -1,25 +1,25 @@
1
1
  django_display_ids/__init__.py,sha256=zFd0XNNOeVgpIImWBe_b-HD59svbVwkDVYq8X6lsLug,3256
2
2
  django_display_ids/admin.py,sha256=UPmU-kGsZ5x_-r9n99P17La5lr-3BLvB9qTfWJgklt8,2569
3
3
  django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
4
- django_display_ids/conf.py,sha256=RXcWGyRfLS9J5fyUaoM5ASq2rwOFSxX22tvSX-TB2ig,2215
4
+ django_display_ids/conf.py,sha256=cBL_b8AW13hUYmN7mMViNQyndZisBxXU-1vSLLIxzv4,2231
5
5
  django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
6
6
  django_display_ids/contrib/drf_spectacular/__init__.py,sha256=9swSJge_dQldC4AZMkYI3M04LzU20eJ_2oz3XABviFA,5427
7
7
  django_display_ids/contrib/rest_framework/__init__.py,sha256=pKV6BVB7k0dJm29C7XMr0M-wK2lXPUgW8Hf4BZHHwuE,198
8
8
  django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
9
- django_display_ids/contrib/rest_framework/views.py,sha256=I6jpsdpM1sRKrPBVyrjypsKgFyUdu6WljGcffHXDiCw,5961
10
- django_display_ids/converters.py,sha256=ElwrfA7DXiadSZ-Sjvl6ZALgH7tfEZ-tLI7UdE6MsAs,5797
9
+ django_display_ids/contrib/rest_framework/views.py,sha256=88qe3w5QL0WNCOY17WjbXl8vrVzLDD149QIEfSl6tCY,5689
10
+ django_display_ids/converters.py,sha256=KGD5FYWkuXztO-6TvTtPwMP7xbnAXAaYuk9LvIpxkBM,5918
11
11
  django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
12
12
  django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
13
- django_display_ids/exceptions.py,sha256=ATIW6SLM9zQ0s-26c3qdQfnPDbuiMpMZK6fxdyQN0tA,3085
14
- django_display_ids/managers.py,sha256=PymcK4BZL6UsUOtoloHP34MCRNmvNHSKEcOImhZxGag,9779
13
+ django_display_ids/exceptions.py,sha256=68Kmp9Qdyg3LtzmXfDHIWRuI7MW3M2acZ3C6Wi8zFEw,4489
14
+ django_display_ids/managers.py,sha256=tQ1ryAIClAg-mhDp5CgkCEqxld9XVMe6fl-u__DSiOI,11831
15
15
  django_display_ids/models.py,sha256=_hmgAR4HC3I-5wU2DND6uNLjEllu1Y9eaXeBQ9dWMNI,4313
16
16
  django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- django_display_ids/resolver.py,sha256=ZlDVoxX0PmVf0MSwPyiNNwQVzdqJGDGE8fm2iyV7QjE,2848
17
+ django_display_ids/resolver.py,sha256=blyLo06tIYrYqOA9Ir260tzs9njX1GMAE6ICDJTwfGk,3485
18
18
  django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
19
19
  django_display_ids/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  django_display_ids/templatetags/display_ids.py,sha256=4KHE8r8mgSKb7LgIuXJaJB_3UGrzRZvTdLqSCYQtb5I,1157
21
- django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
22
- django_display_ids/views.py,sha256=uAXQJryWQNnTSHIjTLhisrYnNiaTo1pLzkUdvYyOLRs,4992
23
- django_display_ids-0.4.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
24
- django_display_ids-0.4.0.dist-info/METADATA,sha256=La5mR7VliD_sGMKjFazUtnrgPFPQ1anLrsxWou9_ej8,5259
25
- django_display_ids-0.4.0.dist-info/RECORD,,
21
+ django_display_ids/typing.py,sha256=hxX2QVYtkvFXZBH9ZskbV98N-uOhX_XAMoO-UOBRnEA,568
22
+ django_display_ids/views.py,sha256=OOhv5G6ake8Siv-yAFFA1s2wOuNng9NYETS1feDygio,4662
23
+ django_display_ids-0.5.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
24
+ django_display_ids-0.5.0.dist-info/METADATA,sha256=twAX7DSsXtTAbGl5IJbZaJ7MrjndlQV1TVin1nhsLhI,5249
25
+ django_display_ids-0.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.28
2
+ Generator: uv 0.9.30
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any