lockss-turtles 0.6.0.dev23__py3-none-any.whl → 0.6.0.dev25__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.
lockss/turtles/plugin.py CHANGED
@@ -29,11 +29,10 @@
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
31
  """
32
- Library to represent a LOCKSS plugin.
32
+ Module to represent a LOCKSS plugin.
33
33
  """
34
34
 
35
- # Remove in Python 3.14
36
- # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
37
36
  from __future__ import annotations
38
37
 
39
38
  from collections.abc import Callable
@@ -29,11 +29,10 @@
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
31
  """
32
- Library to represent plugin registries and plugin registry catalogs.
32
+ Module to represent plugin registries and plugin registry catalogs.
33
33
  """
34
34
 
35
- # Remove in Python 3.14
36
- # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
37
36
  from __future__ import annotations
38
37
 
39
38
  from abc import ABC, abstractmethod
@@ -318,8 +317,8 @@ class DirectoryPluginRegistryLayout(BasePluginRegistryLayout):
318
317
  type: Literal['directory'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
319
318
 
320
319
  #: This plugin registry layout's file naming convention.
321
- file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT,
322
- **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
320
+ file_naming_convention: PluginRegistryLayoutFileNamingConvention = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT,
321
+ **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
323
322
 
324
323
  def _copy_jar(self,
325
324
  src_path: Path,
@@ -478,7 +477,7 @@ class PluginRegistryLayer(BaseModel):
478
477
  :return: This plugin registry layer's path.
479
478
  :rtype: Path
480
479
  """
481
- return path(self.path)
480
+ return self.get_plugin_registry().get_root().joinpath(self.path)
482
481
 
483
482
  def get_plugin_registry(self) -> PluginRegistry:
484
483
  """
@@ -29,11 +29,10 @@
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
31
  """
32
- Library to represent plugin sets and plugin set catalogs.
32
+ Module to represent plugin sets and plugin set catalogs.
33
33
  """
34
34
 
35
- # Remove in Python 3.14
36
- # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
37
36
  from __future__ import annotations
38
37
 
39
38
  from abc import ABC, abstractmethod
@@ -42,7 +41,7 @@ from pathlib import Path
42
41
  import shlex
43
42
  import subprocess
44
43
  import sys
45
- from typing import Annotated, Any, ClassVar, Literal, Optional, Union
44
+ from typing import Annotated, Any, Callable, ClassVar, Literal, Optional, Union
46
45
 
47
46
  from pydantic import BaseModel, Field
48
47
 
@@ -234,12 +233,12 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
234
233
  type: Literal['maven'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
235
234
 
236
235
  #: This plugin set builder's main code path.
237
- main: Optional[str] = Field(DEFAULT_MAIN,
238
- **BasePluginSetBuilder.MAIN_FIELD)
236
+ main: str = Field(DEFAULT_MAIN,
237
+ **BasePluginSetBuilder.MAIN_FIELD)
239
238
 
240
239
  #: This plugin set builder's unit test path.
241
- test: Optional[str] = Field(DEFAULT_TEST,
242
- **BasePluginSetBuilder.TEST_FIELD)
240
+ test: str = Field(DEFAULT_TEST,
241
+ **BasePluginSetBuilder.TEST_FIELD)
243
242
 
244
243
  #: An internal flag to remember if a build has occurred.
245
244
  _built: bool
@@ -248,7 +247,7 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
248
247
  plugin_id: PluginIdentifier,
249
248
  keystore_path: Path,
250
249
  keystore_alias: str,
251
- keystore_password=None) -> tuple[Path, Plugin]:
250
+ keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
252
251
  """
253
252
  Builds the given plugin with the supplied plugin signing credentials.
254
253
 
@@ -285,7 +284,7 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
285
284
  def _big_build(self,
286
285
  keystore_path: Path,
287
286
  keystore_alias: str,
288
- keystore_password: str=None) -> None:
287
+ keystore_password: Optional[Callable[[], str]]=None) -> None:
289
288
  """
290
289
  Runs ``mvn package`` on the project if the ``_built`` flag is False.
291
290
 
@@ -301,8 +300,9 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
301
300
  # Do build
302
301
  cmd = ['mvn', 'package',
303
302
  f'-Dkeystore.file={keystore_path!s}',
304
- f'-Dkeystore.alias={keystore_alias}',
305
- f'-Dkeystore.password={keystore_password}']
303
+ f'-Dkeystore.alias={keystore_alias}']
304
+ if keystore_password:
305
+ cmd.extend(f'-Dkeystore.password={keystore_password()}')
306
306
  try:
307
307
  subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
308
308
  except subprocess.CalledProcessError as cpe:
@@ -363,12 +363,12 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
363
363
  type: Literal['ant'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
364
364
 
365
365
  #: This plugin set builder's main code path.
366
- main: Optional[str] = Field(DEFAULT_MAIN,
367
- **BasePluginSetBuilder.MAIN_FIELD)
366
+ main: str = Field(DEFAULT_MAIN,
367
+ **BasePluginSetBuilder.MAIN_FIELD)
368
368
 
369
369
  #: This plugin set builder's unit test path.
370
- test: Optional[str] = Field(DEFAULT_TEST,
371
- **BasePluginSetBuilder.TEST_FIELD)
370
+ test: str = Field(DEFAULT_TEST,
371
+ **BasePluginSetBuilder.TEST_FIELD)
372
372
 
373
373
  #: An internal flag to remember if a build has occurred.
374
374
  _built: bool
@@ -377,7 +377,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
377
377
  plugin_id: PluginIdentifier,
378
378
  keystore_path: Path,
379
379
  keystore_alias: str,
380
- keystore_password=None) -> tuple[Path, Plugin]:
380
+ keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
381
381
  """
382
382
  Builds the given plugin with the supplied plugin signing credentials.
383
383
 
@@ -388,7 +388,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
388
388
  :param keystore_alias: The alias to use in the plugin signing keystore.
389
389
  :type keystore_alias: str
390
390
  :param keystore_password: The plugin signing password.
391
- :type keystore_password:
391
+ :type keystore_password: Optional[Callable[[], str]]
392
392
  :return: A tuple of the built and signed JAR file and a Plugin object
393
393
  instantiated from it.
394
394
  :rtype: tuple[Path, Plugin]
@@ -433,7 +433,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
433
433
  plugin_id: PluginIdentifier,
434
434
  keystore_path: Path,
435
435
  keystore_alias: str,
436
- keystore_password: str=None) -> tuple[Path, Plugin]:
436
+ keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
437
437
  """
438
438
  Performs the "little build" of the given plugin.
439
439
 
@@ -444,7 +444,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
444
444
  :param keystore_alias: The alias to use in the plugin signing keystore.
445
445
  :type keystore_alias: str
446
446
  :param keystore_password: The plugin signing password.
447
- :type keystore_password:
447
+ :type keystore_password: Optional[Callable[[], str]]
448
448
  :return: A tuple of the built and signed JAR file and a Plugin object
449
449
  instantiated from it.
450
450
  :rtype: tuple[Path, Plugin]
@@ -483,8 +483,8 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
483
483
  '--jar', str(jar_path),
484
484
  '--alias', keystore_alias,
485
485
  '--keystore', str(keystore_path)]
486
- if keystore_password is not None:
487
- cmd.extend(['--password', keystore_password])
486
+ if keystore_password:
487
+ cmd.extend(['--password', keystore_password()])
488
488
  try:
489
489
  subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
490
490
  except subprocess.CalledProcessError as cpe:
@@ -552,7 +552,7 @@ class PluginSet(BaseModel):
552
552
  plugin_id: PluginIdentifier,
553
553
  keystore_path: Path,
554
554
  keystore_alias: str,
555
- keystore_password=None) -> tuple[Path, Plugin]:
555
+ keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
556
556
  """
557
557
  Builds the given plugin with the supplied plugin signing credentials.
558
558
 
@@ -563,7 +563,7 @@ class PluginSet(BaseModel):
563
563
  :param keystore_alias: The alias to use in the plugin signing keystore.
564
564
  :type keystore_alias: str
565
565
  :param keystore_password: The plugin signing password.
566
- :type keystore_password:
566
+ :type keystore_password: Optional[Callable[[], str]]
567
567
  :return: A tuple of the built and signed JAR file and a Plugin object
568
568
  instantiated from it.
569
569
  """
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # 3. Neither the name of the copyright holder nor the names of its contributors
16
+ # may be used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ # POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ """
32
+ Module to represent plugin signing credentials.
33
+ """
34
+
35
+ from pathlib import Path
36
+ from typing import Literal
37
+
38
+ from pydantic.v1 import Field
39
+
40
+ from .util import BaseModelWithRoot
41
+
42
+
43
+ #: A type alias for the plugin signing credentials kind.
44
+ PluginSigningCredentialsKind = Literal['PluginSigningCredentials']
45
+
46
+
47
+ class PluginSigningCredentials(BaseModelWithRoot):
48
+ """
49
+ A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent
50
+ plugin signing credentials.
51
+ """
52
+
53
+ #: This object's kind.
54
+ kind: PluginSigningCredentialsKind = Field(title='Kind',
55
+ description="This object's kind")
56
+
57
+ #: Path to the plugin signing keystore.
58
+ plugin_signing_keystore: str = Field(title='Plugin Signing Keystore', description='A path to the plugin signing keystore', alias='plugin-signing-keystore')
59
+
60
+ #: Alias to use from the plugin signing keystore.
61
+ plugin_signing_alias: str = Field(title='Plugin Signing Alias', description='The plugin signing alias to use', alias='plugin-signing-alias')
62
+
63
+ def get_plugin_signing_alias(self) -> str:
64
+ """
65
+ Returns the alias to use from the plugin signing keystore.
66
+
67
+ :return: The plugin signing alias.
68
+ :rtype: str
69
+ """
70
+ return self.plugin_signing_alias
71
+
72
+ def get_plugin_signing_keystore(self) -> Path:
73
+ """
74
+ The file path for the plugin signing keystore; a relative path is
75
+ understood to be relative to the file containing the definition.
76
+
77
+ :return: The file path for the plugin signing keystore.
78
+ :rtype: Path
79
+ """
80
+ return self.get_root().joinpath(self.plugin_signing_keystore)
lockss/turtles/util.py CHANGED
@@ -29,11 +29,10 @@
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
31
  """
32
- Utilities for the ``lockss.turtles`` package.
32
+ Utility module.
33
33
  """
34
34
 
35
- # Remove in Python 3.14
36
- # See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
37
36
  from __future__ import annotations
38
37
 
39
38
  from collections.abc import Iterable
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: lockss-turtles
3
- Version: 0.6.0.dev23
3
+ Version: 0.6.0.dev25
4
4
  Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
5
5
  License: BSD-3-Clause
6
+ License-File: LICENSE
6
7
  Author: Thib Guicherd-Callin
7
8
  Author-email: thib@cs.stanford.edu
8
9
  Maintainer: Thib Guicherd-Callin
@@ -20,7 +21,7 @@ Classifier: Topic :: System :: Archiving
20
21
  Classifier: Topic :: Utilities
21
22
  Requires-Dist: exceptiongroup (>=1.3.0,<1.4.0)
22
23
  Requires-Dist: java-manifest (>=1.1.0,<1.2.0)
23
- Requires-Dist: lockss-pybasic (>=0.1.0,<0.2.0)
24
+ Requires-Dist: lockss-pybasic (>=0.1.1,<0.2.0)
24
25
  Requires-Dist: pydantic (>=2.11.0,<2.12.0)
25
26
  Requires-Dist: pyyaml (>=6.0.0,<6.1.0)
26
27
  Requires-Dist: xdg (>=6.0.0,<6.1.0)
@@ -34,7 +35,7 @@ Description-Content-Type: text/x-rst
34
35
  Turtles
35
36
  =======
36
37
 
37
- .. |RELEASE| replace:: 0.6.0-dev23 NOT YET RELEASED
38
+ .. |RELEASE| replace:: 0.6.0-dev25 NOT YET RELEASED
38
39
  .. |RELEASE_DATE| replace:: NOT YET RELEASED
39
40
  .. |TURTLES| replace:: **Turtles**
40
41
 
@@ -0,0 +1,17 @@
1
+ lockss/turtles/__init__.py,sha256=6HUyqmH9_7cfWBkAGl3hejHhz3eXkpNtw3ndfcS5044,1827
2
+ lockss/turtles/__main__.py,sha256=BqRTG3dejCgLh87UJ1Os9DD9unxnQkOdUQM6eqWS3Sg,1680
3
+ lockss/turtles/app.py,sha256=XnBF91r-D3YGSfgIP0v9C064fH8Rc6y4I5y1arEweCQ,30520
4
+ lockss/turtles/cli.py,sha256=x4Ciba5fcDSNeYAhhauOeCP1FcMy5ptOts32CWG3la4,30546
5
+ lockss/turtles/plugin.py,sha256=fxIAaSL06uv9sjYCz2wTQjn_92qPK3EvGNnbkpeSZcA,10972
6
+ lockss/turtles/plugin_registry.py,sha256=8BR6CrqY8hh4zpy13B8gZ9qpCMASlOxvX6g8VmpWeMc,26864
7
+ lockss/turtles/plugin_set.py,sha256=4fUmpp_GF_WY06gyhm9tVofYINoTXYUqAvKik3Z4bE4,24640
8
+ lockss/turtles/plugin_signing_credentials.py,sha256=1ktGascqT2UM6o4ipMS_M80haWA8IzUXFYqzuWUNUJU,3254
9
+ lockss/turtles/util.py,sha256=sBvB96BBED_yVZOybcf-sIwldNm9wstgWssnhugpww0,3536
10
+ unittest/lockss/turtles/__init__.py,sha256=ude-nT_uHF0lCD9qKrzedm8QvwqTiNjogpyz8SJudOQ,5598
11
+ unittest/lockss/turtles/test_plugin_registry.py,sha256=PIBeAKoDudInJOGhwTSug_b4AugGtK5yFmapUtQYTNw,18372
12
+ unittest/lockss/turtles/test_plugin_set.py,sha256=I1GNKlFcG_b906vsk12xOq0Y2Jer-vdF7VH9qszlw2w,10916
13
+ lockss_turtles-0.6.0.dev25.dist-info/METADATA,sha256=jDXEkuRovEbrWYawuPRSrqc6igOYJ9l2hJHTFfCuqy4,2362
14
+ lockss_turtles-0.6.0.dev25.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
15
+ lockss_turtles-0.6.0.dev25.dist-info/entry_points.txt,sha256=25BAVFSBRKWAWiXIGZgcr1ypt2mV7nj31Jl8WcNZZOk,51
16
+ lockss_turtles-0.6.0.dev25.dist-info/licenses/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
17
+ lockss_turtles-0.6.0.dev25.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -29,37 +29,78 @@
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
31
  from collections.abc import Callable
32
+ from pathlib import Path
32
33
  from pydantic import ValidationError
33
34
  from pydantic_core import ErrorDetails
34
35
  from typing import Any, Optional, Tuple, Union
35
36
  from unittest import TestCase
36
37
 
37
38
 
39
+ ROOT: Path = Path('.').absolute()
40
+
41
+
38
42
  Loc = Union[Tuple[str], str]
39
43
 
40
44
 
41
45
  class PydanticTestCase(TestCase):
42
46
 
47
+ def assertPydanticListType(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
48
+ self._assertPydanticValidationError(func,
49
+ lambda e: e.get('type') == 'list_type' \
50
+ and e.get('loc') == self._loc(loc_tuple_or_str),
51
+ msg=msg)
52
+
53
+ def assertPydanticLiteralError(self, func: Callable[[], Any], loc_tuple_or_str: Loc, expected: Union[Tuple[str], str], msg=None) -> None:
54
+ if isinstance(expected, str):
55
+ exp = f"'{expected}'"
56
+ elif len(expected) == 0:
57
+ raise RuntimeError(f"'expected' cannot be an empty tuple")
58
+ elif len(expected) == 1:
59
+ exp = f"'{expected[0]}'"
60
+ else:
61
+ exp = f'''{", ".join(f"'{e}'" for e in expected[:-1])} or '{expected[-1]}\''''
62
+ self._assertPydanticValidationError(func,
63
+ lambda e: e.get('type') == 'literal_error' \
64
+ and e.get('loc') == self._loc(loc_tuple_or_str) \
65
+ and (ctx := e.get('ctx')) \
66
+ and ctx.get('expected') == exp,
67
+ msg=msg)
68
+
69
+ def assertPydanticModelAttributesType(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
70
+ self._assertPydanticValidationError(func,
71
+ lambda e: e.get('type') == 'model_attributes_type' \
72
+ and e.get('loc') == self._loc(loc_tuple_or_str),
73
+ msg=msg)
74
+
75
+ def assertPydanticMissing(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
76
+ self._assertPydanticValidationError(func,
77
+ lambda e: e.get('type') == 'missing' \
78
+ and e.get('loc') == self._loc(loc_tuple_or_str),
79
+ msg=msg)
80
+
81
+ def assertPydanticStringType(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
82
+ self._assertPydanticValidationError(func,
83
+ lambda e: e.get('type') == 'string_type' \
84
+ and e.get('loc') == self._loc(loc_tuple_or_str),
85
+ msg=msg)
86
+
87
+ def assertPydanticTooShort(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
88
+ self._assertPydanticValidationError(func,
89
+ lambda e: e.get('type') == 'too_short' \
90
+ and e.get('loc') == self._loc(loc_tuple_or_str),
91
+ msg=msg)
92
+
43
93
  def _assertPydanticValidationError(self,
44
94
  func: Callable[[], Any],
45
95
  matcher: Callable[[ErrorDetails], bool],
46
- msg: Optional[str]=None):
96
+ msg: Optional[str]=None) -> None:
47
97
  with self.assertRaises(ValidationError) as cm:
48
98
  func()
49
- self.fail('Expected ValidationError but did not get one')
50
- self.assertIsInstance(cm.exception, ValidationError)
51
99
  ve: ValidationError = cm.exception
52
100
  for e in ve.errors():
53
101
  if matcher(e):
54
102
  return
55
103
  self.fail(msg or f'Did not get a matching ValidationError; got:\n{"\n".join([str(e) for e in ve.errors()])}\n{ve!s}')
56
104
 
57
- def assertPydanticMissing(self, func: Callable[[], Any], loc: Loc, msg=None) -> None:
58
- if isinstance(loc, str):
59
- loc = (loc,)
60
- self._assertPydanticValidationError(func, lambda e: e.get('type') == 'missing' and e.get('loc') == loc)
61
-
62
- def assertPydanticLiteralError(self, func: Callable[[], Any], loc: Loc, expected: str, msg=None) -> None:
63
- if isinstance(loc, str):
64
- loc = (loc,)
65
- self._assertPydanticValidationError(func, lambda e: e.get('type') == 'literal_error' and e.get('loc') == loc and (ctx := e.get('ctx')) and ctx.get('expected') == repr(expected))
105
+ def _loc(self, loc_tuple_or_str: Loc) -> tuple[str]:
106
+ return (loc_tuple_or_str,) if isinstance(loc_tuple_or_str, str) else loc_tuple_or_str