lsst-utils 25.2023.2800__py3-none-any.whl → 29.2025.4800__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.
lsst/utils/tests.py CHANGED
@@ -9,22 +9,22 @@
9
9
  # Use of this source code is governed by a 3-clause BSD-style
10
10
  # license that can be found in the LICENSE file.
11
11
 
12
- """Support code for running unit tests"""
12
+ """Support code for running unit tests."""
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
16
  __all__ = [
17
- "init",
18
- "MemoryTestCase",
19
17
  "ExecutablesTestCase",
20
18
  "ImportTestCase",
21
- "getTempFilePath",
19
+ "MemoryTestCase",
22
20
  "TestCase",
23
21
  "assertFloatsAlmostEqual",
24
- "assertFloatsNotEqual",
25
22
  "assertFloatsEqual",
26
- "debugger",
23
+ "assertFloatsNotEqual",
27
24
  "classParameters",
25
+ "debugger",
26
+ "getTempFilePath",
27
+ "init",
28
28
  "methodParameters",
29
29
  "temporaryDirectory",
30
30
  ]
@@ -32,7 +32,6 @@ __all__ = [
32
32
  import contextlib
33
33
  import functools
34
34
  import gc
35
- import importlib.resources as resources
36
35
  import inspect
37
36
  import itertools
38
37
  import os
@@ -43,7 +42,8 @@ import sys
43
42
  import tempfile
44
43
  import unittest
45
44
  import warnings
46
- from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
45
+ from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence
46
+ from importlib import resources
47
47
  from typing import Any, ClassVar
48
48
 
49
49
  import numpy
@@ -129,12 +129,12 @@ unittest.defaultTestLoader.suiteClass = _suiteClassWrapper
129
129
  class MemoryTestCase(unittest.TestCase):
130
130
  """Check for resource leaks."""
131
131
 
132
- ignore_regexps: list[str] = []
132
+ ignore_regexps: ClassVar[list[str]] = []
133
133
  """List of regexps to ignore when checking for open files."""
134
134
 
135
135
  @classmethod
136
136
  def tearDownClass(cls) -> None:
137
- """Reset the leak counter when the tests have been completed"""
137
+ """Reset the leak counter when the tests have been completed."""
138
138
  init()
139
139
 
140
140
  def testFileDescriptorLeaks(self) -> None:
@@ -153,6 +153,7 @@ class MemoryTestCase(unittest.TestCase):
153
153
  for f in now_open
154
154
  if not f.endswith(".car")
155
155
  and not f.startswith("/proc/")
156
+ and not f.startswith("/sys/")
156
157
  and not f.endswith(".ttf")
157
158
  and not (f.startswith("/var/lib/") and f.endswith("/passwd"))
158
159
  and not f.endswith("astropy.log")
@@ -165,7 +166,7 @@ class MemoryTestCase(unittest.TestCase):
165
166
  if diff:
166
167
  for f in diff:
167
168
  print(f"File open: {f}")
168
- self.fail("Failed to close %d file%s" % (len(diff), "s" if len(diff) != 1 else ""))
169
+ self.fail(f"Failed to close {len(diff)} file{'s' if len(diff) != 1 else ''}")
169
170
 
170
171
 
171
172
  class ExecutablesTestCase(unittest.TestCase):
@@ -346,6 +347,16 @@ class ImportTestCase(unittest.TestCase):
346
347
  PACKAGES: ClassVar[Iterable[str]] = ()
347
348
  """Packages to be imported."""
348
349
 
350
+ SKIP_FILES: ClassVar[Mapping[str, Container[str]]] = {}
351
+ """Files to be skipped importing; specified as key-value pairs.
352
+
353
+ The key is the package name and the value is a set of files names in that
354
+ package to skip.
355
+
356
+ Note: Files with names not ending in .py or beginning with leading double
357
+ underscores are always skipped.
358
+ """
359
+
349
360
  _n_registered = 0
350
361
  """Number of packages registered for testing by this class."""
351
362
 
@@ -367,7 +378,7 @@ class ImportTestCase(unittest.TestCase):
367
378
  for mod in cls.PACKAGES:
368
379
  test_name = "test_import_" + mod.replace(".", "_")
369
380
 
370
- def test_import(*args: Any) -> None:
381
+ def test_import(*args: Any, mod=mod) -> None:
371
382
  self = args[0]
372
383
  self.assertImport(mod)
373
384
 
@@ -378,15 +389,19 @@ class ImportTestCase(unittest.TestCase):
378
389
  # If there are no packages listed that is likely a mistake and
379
390
  # so register a failing test.
380
391
  if cls._n_registered == 0:
381
- setattr(cls, "test_no_packages_registered", cls._test_no_packages_registered_for_import_testing)
392
+ cls.test_no_packages_registered = cls._test_no_packages_registered_for_import_testing
382
393
 
383
394
  def assertImport(self, root_pkg):
384
395
  for file in resources.files(root_pkg).iterdir():
385
396
  file = file.name
397
+ # When support for python 3.9 is dropped, this could be updated to
398
+ # use match case construct.
386
399
  if not file.endswith(".py"):
387
400
  continue
388
401
  if file.startswith("__"):
389
402
  continue
403
+ if file in self.SKIP_FILES.get(root_pkg, ()):
404
+ continue
390
405
  root, _ = os.path.splitext(file)
391
406
  module_name = f"{root_pkg}.{root}"
392
407
  with self.subTest(module=module_name):
@@ -399,7 +414,7 @@ class ImportTestCase(unittest.TestCase):
399
414
  @contextlib.contextmanager
400
415
  def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
401
416
  """Return a path suitable for a temporary file and try to delete the
402
- file on success
417
+ file on success.
403
418
 
404
419
  If the with block completes successfully then the file is deleted,
405
420
  if possible; failure results in a printed warning.
@@ -419,11 +434,11 @@ def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
419
434
  If `False`, a file should not be present when the context manager
420
435
  exits.
421
436
 
422
- Returns
423
- -------
437
+ Yields
438
+ ------
424
439
  path : `str`
425
440
  Path for a temporary file. The path is a combination of the caller's
426
- file path and the name of the top-level function
441
+ file path and the name of the top-level function.
427
442
 
428
443
  Examples
429
444
  --------
@@ -432,6 +447,8 @@ def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
432
447
  # file tests/testFoo.py
433
448
  import unittest
434
449
  import lsst.utils.tests
450
+
451
+
435
452
  class FooTestCase(unittest.TestCase):
436
453
  def testBasics(self):
437
454
  self.runTest()
@@ -445,6 +462,8 @@ def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
445
462
  # at the end of this "with" block the path tmpFile will be
446
463
  # deleted, but only if the file exists and the "with"
447
464
  # block terminated normally (rather than with an exception)
465
+
466
+
448
467
  ...
449
468
  """
450
469
  stack = inspect.stack()
@@ -464,7 +483,11 @@ def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
464
483
  callerFileName = os.path.splitext(callerFileNameWithExt)[0]
465
484
  outDir = os.path.join(callerDir, ".tests")
466
485
  if not os.path.isdir(outDir):
467
- outDir = ""
486
+ # No .tests directory implies we are not running with sconsUtils.
487
+ # Need to use the current working directory, the callerDir, or
488
+ # /tmp equivalent. If cwd is used if must be as an absolute path
489
+ # in case the test code changes cwd.
490
+ outDir = os.path.abspath(os.path.curdir)
468
491
  prefix = f"{callerFileName}_{callerFuncName}-"
469
492
  outPath = tempfile.mktemp(dir=outDir, suffix=ext, prefix=prefix)
470
493
  if os.path.exists(outPath):
@@ -473,10 +496,8 @@ def getTempFilePath(ext: str, expectOutput: bool = True) -> Iterator[str]:
473
496
  # Use stacklevel 3 so that the warning is reported from the end of the
474
497
  # with block
475
498
  warnings.warn(f"Unexpectedly found pre-existing tempfile named {outPath!r}", stacklevel=3)
476
- try:
499
+ with contextlib.suppress(OSError):
477
500
  os.remove(outPath)
478
- except OSError:
479
- pass
480
501
 
481
502
  yield outPath
482
503
 
@@ -506,13 +527,23 @@ class TestCase(unittest.TestCase):
506
527
  def inTestCase(func: Callable) -> Callable:
507
528
  """Add a free function to our custom TestCase class, while
508
529
  also making it available as a free function.
530
+
531
+ Parameters
532
+ ----------
533
+ func : `~collections.abc.Callable`
534
+ Function to be added to `unittest.TestCase` class.
535
+
536
+ Returns
537
+ -------
538
+ func : `~collections.abc.Callable`
539
+ The given function.
509
540
  """
510
541
  setattr(TestCase, func.__name__, func)
511
542
  return func
512
543
 
513
544
 
514
545
  def debugger(*exceptions):
515
- """Enter the debugger when there's an uncaught exception
546
+ """Enter the debugger when there's an uncaught exception.
516
547
 
517
548
  To use, just slap a ``@debugger()`` on your function.
518
549
 
@@ -525,6 +556,12 @@ def debugger(*exceptions):
525
556
  Code provided by "Rosh Oxymoron" on StackOverflow:
526
557
  http://stackoverflow.com/questions/4398967/python-unit-testing-automatically-running-the-debugger-when-a-test-fails
527
558
 
559
+ Parameters
560
+ ----------
561
+ *exceptions : `Exception`
562
+ Specific exception classes to catch. Default is to catch
563
+ `AssertionError`.
564
+
528
565
  Notes
529
566
  -----
530
567
  Consider using ``pytest --pdb`` instead of this decorator.
@@ -560,13 +597,13 @@ def plotImageDiff(
560
597
  Parameters
561
598
  ----------
562
599
  lhs : `numpy.ndarray`
563
- LHS values to compare; a 2-d NumPy array
600
+ LHS values to compare; a 2-d NumPy array.
564
601
  rhs : `numpy.ndarray`
565
- RHS values to compare; a 2-d NumPy array
602
+ RHS values to compare; a 2-d NumPy array.
566
603
  bad : `numpy.ndarray`
567
- A 2-d boolean NumPy array of values to emphasize in the plots
604
+ A 2-d boolean NumPy array of values to emphasize in the plots.
568
605
  diff : `numpy.ndarray`
569
- difference array; a 2-d NumPy array, or None to show lhs-rhs
606
+ Difference array; a 2-d NumPy array, or None to show lhs-rhs.
570
607
  plotFileName : `str`
571
608
  Filename to save the plot to. If None, the plot will be displayed in
572
609
  a window.
@@ -577,11 +614,20 @@ def plotImageDiff(
577
614
  wrapped in a try/except block within packages that do not depend on
578
615
  `matplotlib` (including `~lsst.utils`).
579
616
  """
580
- from matplotlib import pyplot
617
+ if plotFileName is None:
618
+ # We need to create an interactive plot with pyplot.
619
+ from matplotlib import pyplot
620
+
621
+ fig = pyplot.figure()
622
+ else:
623
+ # We can create a non-interactive figure.
624
+ from .plotting import make_figure
625
+
626
+ fig = make_figure()
581
627
 
582
628
  if diff is None:
583
629
  diff = lhs - rhs
584
- pyplot.figure()
630
+
585
631
  if bad is not None:
586
632
  # make an rgba image that's red and transparent where not bad
587
633
  badImage = numpy.zeros(bad.shape + (4,), dtype=numpy.uint8)
@@ -594,29 +640,29 @@ def plotImageDiff(
594
640
  vmin2 = numpy.min(diff)
595
641
  vmax2 = numpy.max(diff)
596
642
  for n, (image, title) in enumerate([(lhs, "lhs"), (rhs, "rhs"), (diff, "diff")]):
597
- pyplot.subplot(2, 3, n + 1)
598
- im1 = pyplot.imshow(
643
+ ax = fig.add_subplot(2, 3, n + 1)
644
+ im1 = ax.imshow(
599
645
  image, cmap=pyplot.cm.gray, interpolation="nearest", origin="lower", vmin=vmin1, vmax=vmax1
600
646
  )
601
647
  if bad is not None:
602
- pyplot.imshow(badImage, alpha=0.2, interpolation="nearest", origin="lower")
603
- pyplot.axis("off")
604
- pyplot.title(title)
605
- pyplot.subplot(2, 3, n + 4)
606
- im2 = pyplot.imshow(
648
+ ax.imshow(badImage, alpha=0.2, interpolation="nearest", origin="lower")
649
+ ax.axis("off")
650
+ ax.set_title(title)
651
+ ax = fig.add_subplot(2, 3, n + 4)
652
+ im2 = ax.imshow(
607
653
  image, cmap=pyplot.cm.gray, interpolation="nearest", origin="lower", vmin=vmin2, vmax=vmax2
608
654
  )
609
655
  if bad is not None:
610
- pyplot.imshow(badImage, alpha=0.2, interpolation="nearest", origin="lower")
611
- pyplot.axis("off")
612
- pyplot.title(title)
613
- pyplot.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
614
- cax1 = pyplot.axes([0.8, 0.55, 0.05, 0.4])
615
- pyplot.colorbar(im1, cax=cax1)
616
- cax2 = pyplot.axes([0.8, 0.05, 0.05, 0.4])
617
- pyplot.colorbar(im2, cax=cax2)
656
+ ax.imshow(badImage, alpha=0.2, interpolation="nearest", origin="lower")
657
+ ax.axis("off")
658
+ ax.set_title(title)
659
+ fig.subplots_adjust(left=0.05, bottom=0.05, top=0.92, right=0.75, wspace=0.05, hspace=0.05)
660
+ cax1 = fig.add_subplot([0.8, 0.55, 0.05, 0.4])
661
+ fig.colorbar(im1, cax=cax1)
662
+ cax2 = fig.add_subplot([0.8, 0.05, 0.05, 0.4])
663
+ fig.colorbar(im2, cax=cax2)
618
664
  if plotFileName:
619
- pyplot.savefig(plotFileName)
665
+ fig.savefig(plotFileName)
620
666
  else:
621
667
  pyplot.show()
622
668
 
@@ -812,6 +858,8 @@ def assertFloatsNotEqual(
812
858
  rhs : scalar or array-like
813
859
  RHS value(s) to compare; may be a scalar or array-like of any
814
860
  dimension.
861
+ **kwds : `~typing.Any`
862
+ Keyword parameters forwarded to `assertFloatsAlmostEqual`.
815
863
 
816
864
  Raises
817
865
  ------
@@ -844,6 +892,8 @@ def assertFloatsEqual(
844
892
  rhs : scalar or array-like
845
893
  RHS value(s) to compare; may be a scalar or array-like of any
846
894
  dimension.
895
+ **kwargs : `~typing.Any`
896
+ Keyword parameters forwarded to `assertFloatsAlmostEqual`.
847
897
 
848
898
  Raises
849
899
  ------
@@ -887,7 +937,7 @@ def _settingsIterator(settings: dict[str, Sequence[Any]]) -> Iterator[dict[str,
887
937
 
888
938
 
889
939
  def classParameters(**settings: Sequence[Any]) -> Callable:
890
- """Class decorator for generating unit tests
940
+ """Class decorator for generating unit tests.
891
941
 
892
942
  This decorator generates classes with class variables according to the
893
943
  supplied ``settings``.
@@ -903,8 +953,7 @@ def classParameters(**settings: Sequence[Any]) -> Callable:
903
953
  ::
904
954
 
905
955
  @classParameters(foo=[1, 2], bar=[3, 4])
906
- class MyTestCase(unittest.TestCase):
907
- ...
956
+ class MyTestCase(unittest.TestCase): ...
908
957
 
909
958
  will generate two classes, as if you wrote::
910
959
 
@@ -913,6 +962,7 @@ def classParameters(**settings: Sequence[Any]) -> Callable:
913
962
  bar = 3
914
963
  ...
915
964
 
965
+
916
966
  class MyTestCase_2_4(unittest.TestCase):
917
967
  foo = 2
918
968
  bar = 4
@@ -949,8 +999,7 @@ def methodParameters(**settings: Sequence[Any]) -> Callable:
949
999
  .. code-block:: python
950
1000
 
951
1001
  @methodParameters(foo=[1, 2], bar=[3, 4])
952
- def testSomething(self, foo, bar):
953
- ...
1002
+ def testSomething(self, foo, bar): ...
954
1003
 
955
1004
  will run:
956
1005
 
@@ -965,7 +1014,7 @@ def methodParameters(**settings: Sequence[Any]) -> Callable:
965
1014
  def wrapper(self: unittest.TestCase, *args: Any, **kwargs: Any) -> None:
966
1015
  for params in _settingsIterator(settings):
967
1016
  kwargs.update(params)
968
- with self.subTest(**params):
1017
+ with self.subTest(**{k: repr(v) for k, v in params.items()}):
969
1018
  func(self, *args, **kwargs)
970
1019
 
971
1020
  return wrapper
@@ -974,7 +1023,7 @@ def methodParameters(**settings: Sequence[Any]) -> Callable:
974
1023
 
975
1024
 
976
1025
  def _cartesianProduct(settings: Mapping[str, Sequence[Any]]) -> Mapping[str, Sequence[Any]]:
977
- """Return the cartesian product of the settings
1026
+ """Return the cartesian product of the settings.
978
1027
 
979
1028
  Parameters
980
1029
  ----------
@@ -1007,7 +1056,7 @@ def _cartesianProduct(settings: Mapping[str, Sequence[Any]]) -> Mapping[str, Seq
1007
1056
 
1008
1057
 
1009
1058
  def classParametersProduct(**settings: Sequence[Any]) -> Callable:
1010
- """Class decorator for generating unit tests
1059
+ """Class decorator for generating unit tests.
1011
1060
 
1012
1061
  This decorator generates classes with class variables according to the
1013
1062
  cartesian product of the supplied ``settings``.
@@ -1023,8 +1072,7 @@ def classParametersProduct(**settings: Sequence[Any]) -> Callable:
1023
1072
  .. code-block:: python
1024
1073
 
1025
1074
  @classParametersProduct(foo=[1, 2], bar=[3, 4])
1026
- class MyTestCase(unittest.TestCase):
1027
- ...
1075
+ class MyTestCase(unittest.TestCase): ...
1028
1076
 
1029
1077
  will generate four classes, as if you wrote::
1030
1078
 
@@ -1035,16 +1083,19 @@ def classParametersProduct(**settings: Sequence[Any]) -> Callable:
1035
1083
  bar = 3
1036
1084
  ...
1037
1085
 
1086
+
1038
1087
  class MyTestCase_1_4(unittest.TestCase):
1039
1088
  foo = 1
1040
1089
  bar = 4
1041
1090
  ...
1042
1091
 
1092
+
1043
1093
  class MyTestCase_2_3(unittest.TestCase):
1044
1094
  foo = 2
1045
1095
  bar = 3
1046
1096
  ...
1047
1097
 
1098
+
1048
1099
  class MyTestCase_2_4(unittest.TestCase):
1049
1100
  foo = 2
1050
1101
  bar = 4
@@ -1067,9 +1118,8 @@ def methodParametersProduct(**settings: Sequence[Any]) -> Callable:
1067
1118
  **settings : `dict` (`str`: iterable)
1068
1119
  The parameter combinations to test. Each should be an iterable.
1069
1120
 
1070
- Example
1071
- -------
1072
-
1121
+ Examples
1122
+ --------
1073
1123
  @methodParametersProduct(foo=[1, 2], bar=["black", "white"])
1074
1124
  def testSomething(self, foo, bar):
1075
1125
  ...
@@ -1090,6 +1140,11 @@ def temporaryDirectory() -> Iterator[str]:
1090
1140
 
1091
1141
  The difference from `tempfile.TemporaryDirectory` is that this ignores
1092
1142
  errors when deleting a directory, which may happen with some filesystems.
1143
+
1144
+ Yields
1145
+ ------
1146
+ `str`
1147
+ Name of the temporary directory.
1093
1148
  """
1094
1149
  tmpdir = tempfile.mkdtemp()
1095
1150
  yield tmpdir
lsst/utils/threads.py CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- __all__ = ["set_thread_envvars", "disable_implicit_threading"]
16
+ __all__ = ["disable_implicit_threading", "set_thread_envvars"]
17
17
 
18
18
  import os
19
19