pyglove 0.5.0.dev202508250811__py3-none-any.whl → 0.5.0.dev202511300809__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.
Files changed (44) hide show
  1. pyglove/core/__init__.py +8 -1
  2. pyglove/core/geno/base.py +7 -3
  3. pyglove/core/io/file_system.py +295 -2
  4. pyglove/core/io/file_system_test.py +291 -0
  5. pyglove/core/logging.py +45 -1
  6. pyglove/core/logging_test.py +12 -21
  7. pyglove/core/monitoring.py +657 -0
  8. pyglove/core/monitoring_test.py +289 -0
  9. pyglove/core/symbolic/__init__.py +7 -0
  10. pyglove/core/symbolic/base.py +89 -35
  11. pyglove/core/symbolic/base_test.py +3 -3
  12. pyglove/core/symbolic/dict.py +31 -12
  13. pyglove/core/symbolic/dict_test.py +49 -0
  14. pyglove/core/symbolic/list.py +17 -3
  15. pyglove/core/symbolic/list_test.py +24 -2
  16. pyglove/core/symbolic/object.py +3 -1
  17. pyglove/core/symbolic/object_test.py +13 -10
  18. pyglove/core/symbolic/ref.py +19 -7
  19. pyglove/core/symbolic/ref_test.py +94 -7
  20. pyglove/core/symbolic/unknown_symbols.py +147 -0
  21. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  22. pyglove/core/typing/annotation_conversion.py +8 -1
  23. pyglove/core/typing/annotation_conversion_test.py +14 -19
  24. pyglove/core/typing/class_schema.py +24 -1
  25. pyglove/core/typing/json_schema.py +221 -8
  26. pyglove/core/typing/json_schema_test.py +508 -12
  27. pyglove/core/typing/type_conversion.py +17 -3
  28. pyglove/core/typing/type_conversion_test.py +7 -2
  29. pyglove/core/typing/value_specs.py +5 -1
  30. pyglove/core/typing/value_specs_test.py +5 -0
  31. pyglove/core/utils/__init__.py +2 -0
  32. pyglove/core/utils/contextual.py +9 -4
  33. pyglove/core/utils/contextual_test.py +10 -0
  34. pyglove/core/utils/error_utils.py +59 -25
  35. pyglove/core/utils/json_conversion.py +360 -63
  36. pyglove/core/utils/json_conversion_test.py +146 -13
  37. pyglove/core/views/html/controls/tab.py +33 -0
  38. pyglove/core/views/html/controls/tab_test.py +37 -0
  39. pyglove/ext/evolution/base_test.py +1 -1
  40. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA +8 -1
  41. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/RECORD +44 -40
  42. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/WHEEL +0 -0
  43. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE +0 -0
  44. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,8 @@ import os
16
16
  import pathlib
17
17
  import tempfile
18
18
  import unittest
19
+ from unittest import mock
20
+ import fsspec
19
21
  from pyglove.core.io import file_system
20
22
 
21
23
 
@@ -82,6 +84,75 @@ class StdFileSystemTest(unittest.TestCase):
82
84
  fs.rmdirs(os.path.join(dir_a, 'b/c'))
83
85
  self.assertEqual(sorted(fs.listdir(dir_a)), ['file1']) # pylint: disable=g-generic-assert
84
86
 
87
+ def test_rename(self):
88
+ tmp_dir = tempfile.mkdtemp()
89
+ fs = file_system.StdFileSystem()
90
+
91
+ _ = fs.mkdirs(os.path.join(tmp_dir, 'a/b'))
92
+ file_foo = os.path.join(tmp_dir, 'a/foo.txt')
93
+ file_bar = os.path.join(tmp_dir, 'a/bar.txt')
94
+
95
+ with fs.open(file_foo, 'w') as f:
96
+ f.write('foo')
97
+ with fs.open(file_bar, 'w') as f:
98
+ f.write('bar')
99
+
100
+ # Rename file to a new name.
101
+ file_foo_new = os.path.join(tmp_dir, 'a/foo-new.txt')
102
+ fs.rename(file_foo, file_foo_new)
103
+ self.assertFalse(fs.exists(file_foo))
104
+ self.assertTrue(fs.exists(file_foo_new))
105
+
106
+ # Rename file to an existing file name.
107
+ fs.rename(file_foo_new, file_bar)
108
+ self.assertFalse(fs.exists(file_foo_new))
109
+ with fs.open(file_bar, 'r') as f:
110
+ self.assertEqual(f.read(), 'foo')
111
+
112
+ # Rename directory to a new name.
113
+ dir_b = os.path.join(tmp_dir, 'a/b')
114
+ dir_c = os.path.join(tmp_dir, 'a/c')
115
+ fs.rename(dir_b, dir_c)
116
+ self.assertFalse(fs.exists(dir_b))
117
+ self.assertTrue(fs.exists(dir_c))
118
+ self.assertTrue(fs.isdir(dir_c))
119
+
120
+ # Rename directory to an existing empty directory.
121
+ dir_d = os.path.join(tmp_dir, 'a/d')
122
+ fs.mkdirs(dir_d)
123
+ fs.rename(dir_c, dir_d)
124
+ self.assertFalse(fs.exists(dir_c))
125
+ self.assertTrue(fs.exists(dir_d))
126
+
127
+ # Rename directory to a non-empty directory.
128
+ dir_x = os.path.join(tmp_dir, 'x')
129
+ dir_a = os.path.join(tmp_dir, 'a')
130
+ fs.mkdirs(os.path.join(dir_x, 'y'))
131
+ with self.assertRaises(OSError):
132
+ fs.rename(dir_a, dir_x)
133
+ self.assertTrue(fs.exists(dir_a))
134
+ self.assertTrue(fs.exists(os.path.join(dir_x, 'y')))
135
+
136
+ # Errors
137
+ dir_u = os.path.join(tmp_dir, 'u')
138
+ dir_u_v = os.path.join(dir_u, 'v')
139
+ file_u_a = os.path.join(dir_u, 'a.txt')
140
+ fs.mkdirs(dir_u_v)
141
+ with fs.open(file_u_a, 'w') as f:
142
+ f.write('a')
143
+
144
+ with self.assertRaises((OSError, NotADirectoryError)):
145
+ fs.rename(dir_u, file_u_a)
146
+
147
+ with self.assertRaises(IsADirectoryError):
148
+ fs.rename(file_u_a, dir_u_v)
149
+
150
+ with self.assertRaises(FileNotFoundError):
151
+ fs.rename(
152
+ os.path.join(tmp_dir, 'non-existent'),
153
+ os.path.join(tmp_dir, 'y')
154
+ )
155
+
85
156
 
86
157
  class MemoryFileSystemTest(unittest.TestCase):
87
158
 
@@ -180,6 +251,94 @@ class MemoryFileSystemTest(unittest.TestCase):
180
251
  fs.rmdirs(os.path.join(dir_a, 'b/c'))
181
252
  self.assertEqual(fs.listdir(dir_a), ['file1']) # pylint: disable=g-generic-assert
182
253
 
254
+ def test_glob(self):
255
+ fs = file_system.MemoryFileSystem()
256
+ fs.mkdirs('/mem/a/b/c')
257
+ with fs.open('/mem/a/foo.txt', 'w') as f:
258
+ f.write('foo')
259
+ with fs.open('/mem/a/bar.json', 'w') as f:
260
+ f.write('bar')
261
+ with fs.open('/mem/a/b/baz.txt', 'w') as f:
262
+ f.write('baz')
263
+
264
+ self.assertEqual(
265
+ sorted(fs.glob('/mem/a/*')),
266
+ ['/mem/a/b', '/mem/a/b/baz.txt', '/mem/a/b/c',
267
+ '/mem/a/bar.json', '/mem/a/foo.txt'])
268
+ self.assertEqual(
269
+ sorted(fs.glob('/mem/a/*.txt')),
270
+ ['/mem/a/b/baz.txt', '/mem/a/foo.txt'])
271
+ self.assertEqual(
272
+ sorted(fs.glob('/mem/a/b/*')),
273
+ ['/mem/a/b/baz.txt', '/mem/a/b/c'])
274
+ self.assertEqual(fs.glob('/mem/a/b/*.txt'), ['/mem/a/b/baz.txt'])
275
+ self.assertEqual(fs.glob('/mem/a/b/c/*'), [])
276
+ self.assertEqual(fs.glob('/mem/a/???.txt'), ['/mem/a/foo.txt'])
277
+ self.assertEqual(fs.glob('/mem/a/bar.*'), ['/mem/a/bar.json'])
278
+ self.assertEqual(
279
+ sorted(fs.glob('/mem/a/*.*')),
280
+ ['/mem/a/b/baz.txt', '/mem/a/bar.json', '/mem/a/foo.txt'])
281
+
282
+ def test_rename(self):
283
+ fs = file_system.MemoryFileSystem()
284
+ fs.mkdirs('/mem/a/b')
285
+ with fs.open('/mem/a/foo.txt', 'w') as f:
286
+ f.write('foo')
287
+ with fs.open('/mem/a/bar.txt', 'w') as f:
288
+ f.write('bar')
289
+
290
+ # Rename file to a new name.
291
+ fs.rename('/mem/a/foo.txt', '/mem/a/foo-new.txt')
292
+ self.assertFalse(fs.exists('/mem/a/foo.txt'))
293
+ self.assertTrue(fs.exists('/mem/a/foo-new.txt'))
294
+
295
+ # Rename file to an existing file name.
296
+ fs.rename('/mem/a/foo-new.txt', '/mem/a/bar.txt')
297
+ self.assertFalse(fs.exists('/mem/a/foo-new.txt'))
298
+ with fs.open('/mem/a/bar.txt', 'r') as f:
299
+ self.assertEqual(f.read(), 'foo')
300
+
301
+ # Rename directory to a new name.
302
+ fs.rename('/mem/a/b', '/mem/a/c')
303
+ self.assertFalse(fs.exists('/mem/a/b'))
304
+ self.assertTrue(fs.exists('/mem/a/c'))
305
+ self.assertTrue(fs.isdir('/mem/a/c'))
306
+
307
+ # Rename directory to an existing empty directory.
308
+ fs.mkdirs('/mem/a/d')
309
+ fs.rename('/mem/a/c', '/mem/a/d')
310
+ self.assertFalse(fs.exists('/mem/a/c'))
311
+ self.assertTrue(fs.exists('/mem/a/d'))
312
+
313
+ # Rename directory to a non-empty directory.
314
+ fs.mkdirs('/mem/x/y')
315
+ with self.assertRaisesRegex(OSError, "Directory not empty: '/mem/x'"):
316
+ fs.rename('/mem/a', '/mem/x')
317
+ self.assertTrue(fs.exists('/mem/a'))
318
+ self.assertTrue(fs.exists('/mem/x/y'))
319
+
320
+ # Errors
321
+ fs.mkdirs('/mem/u/v')
322
+ with fs.open('/mem/u/a.txt', 'w') as f:
323
+ f.write('a')
324
+
325
+ with self.assertRaisesRegex(
326
+ OSError, "Cannot move directory '/mem/u' to a subdirectory of itself"):
327
+ fs.rename('/mem/u', '/mem/u/v/w')
328
+
329
+ with self.assertRaisesRegex(
330
+ NotADirectoryError,
331
+ "Cannot rename directory '/mem/u' to non-directory '/mem/u/a.txt'"):
332
+ fs.rename('/mem/u', '/mem/u/a.txt')
333
+
334
+ with self.assertRaisesRegex(
335
+ IsADirectoryError,
336
+ "Cannot rename non-directory '/mem/u/a.txt' to directory '/mem/u/v'"):
337
+ fs.rename('/mem/u/a.txt', '/mem/u/v')
338
+
339
+ with self.assertRaises(FileNotFoundError):
340
+ fs.rename('/mem/non-existent', '/mem/y')
341
+
183
342
 
184
343
  class FileIoApiTest(unittest.TestCase):
185
344
 
@@ -217,6 +376,14 @@ class FileIoApiTest(unittest.TestCase):
217
376
  file_system.rm(file2)
218
377
  self.assertFalse(file_system.path_exists(file2))
219
378
 
379
+ # Test glob with standard file system.
380
+ glob_dir = os.path.join(tempfile.mkdtemp(), 'glob')
381
+ file_system.mkdirs(os.path.join(glob_dir, 'a/b'))
382
+ file_system.writefile(os.path.join(glob_dir, 'a/foo.txt'), 'foo')
383
+ self.assertEqual(
384
+ sorted(file_system.glob(os.path.join(glob_dir, 'a/*'))),
385
+ [os.path.join(glob_dir, 'a/b'), os.path.join(glob_dir, 'a/foo.txt')])
386
+
220
387
  def test_memory_filesystem(self):
221
388
  file1 = pathlib.Path('/mem/file1')
222
389
  with self.assertRaises(FileNotFoundError):
@@ -248,6 +415,130 @@ class FileIoApiTest(unittest.TestCase):
248
415
  file_system.rm(file2)
249
416
  self.assertFalse(file_system.path_exists(file2))
250
417
 
418
+ # Test glob with memory file system.
419
+ file_system.mkdirs('/mem/g/a/b')
420
+ file_system.writefile('/mem/g/a/foo.txt', 'foo')
421
+ file_system.rename('/mem/g/a/foo.txt', '/mem/g/a/foo2.txt')
422
+ file_system.writefile('/mem/g/a/b/bar.txt', 'bar')
423
+ self.assertEqual(
424
+ sorted(file_system.glob('/mem/g/a/*')),
425
+ ['/mem/g/a/b', '/mem/g/a/b/bar.txt', '/mem/g/a/foo2.txt'])
426
+
427
+
428
+ class FsspecFileSystemTest(unittest.TestCase):
429
+
430
+ def setUp(self):
431
+ super().setUp()
432
+ self.fs = fsspec.filesystem('memory')
433
+ self.fs.pipe('memory:///a/b/c', b'abc')
434
+ self.fs.pipe('memory:///a/b/d', b'abd')
435
+ self.fs.mkdir('memory:///a/e')
436
+
437
+ def tearDown(self):
438
+ super().tearDown()
439
+ fsspec.filesystem('memory').rm('/', recursive=True)
440
+
441
+ def test_read_file(self):
442
+ self.assertEqual(file_system.readfile('memory:///a/b/c', mode='rb'), b'abc')
443
+ with file_system.open('memory:///a/b/d', 'rb') as f:
444
+ self.assertEqual(f.read(), b'abd')
445
+
446
+ def test_fsspec_file_ops(self):
447
+ file_system.writefile('memory:///f', b'hello\nworld\n', mode='wb')
448
+ with file_system.open('memory:///f', 'rb') as f:
449
+ self.assertIsInstance(f, file_system.FsspecFile)
450
+ self.assertEqual(f.readline(), b'hello\n')
451
+ self.assertEqual(f.tell(), 6)
452
+ self.assertEqual(f.seek(8), 8)
453
+ self.assertEqual(f.read(), b'rld\n')
454
+ f.flush()
455
+
456
+ def test_write_file(self):
457
+ file_system.writefile('memory:///a/b/e', b'abe', mode='wb')
458
+ self.assertTrue(self.fs.exists('memory:///a/b/e'))
459
+ self.assertEqual(self.fs.cat('memory:///a/b/e'), b'abe')
460
+
461
+ def test_exists(self):
462
+ self.assertTrue(file_system.path_exists('memory:///a/b/c'))
463
+ self.assertFalse(file_system.path_exists('memory:///a/b/nonexist'))
464
+
465
+ def test_isdir(self):
466
+ self.assertTrue(file_system.isdir('memory:///a/b'))
467
+ self.assertTrue(file_system.isdir('memory:///a/e'))
468
+ self.assertFalse(file_system.isdir('memory:///a/b/c'))
469
+
470
+ def test_listdir(self):
471
+ self.assertCountEqual(file_system.listdir('memory:///a'), ['b', 'e'])
472
+ self.assertCountEqual(file_system.listdir('memory:///a/b'), ['c', 'd'])
473
+
474
+ def test_glob(self):
475
+ self.assertCountEqual(
476
+ file_system.glob('memory:///a/b/*'),
477
+ ['memory:///a/b/c', 'memory:///a/b/d']
478
+ )
479
+
480
+ def test_mkdir(self):
481
+ file_system.mkdir('memory:///a/f')
482
+ self.assertTrue(self.fs.isdir('memory:///a/f'))
483
+
484
+ def test_mkdirs(self):
485
+ file_system.mkdirs('memory:///g/h/i')
486
+ self.assertTrue(self.fs.isdir('memory:///g/h/i'))
487
+
488
+ def test_rm(self):
489
+ file_system.rm('memory:///a/b/c')
490
+ self.assertFalse(self.fs.exists('memory:///a/b/c'))
491
+
492
+ def test_rename(self):
493
+ file_system.rename('memory:///a/b/c', 'memory:///a/b/c_new')
494
+ self.assertFalse(self.fs.exists('memory:///a/b/c'))
495
+ self.assertTrue(self.fs.exists('memory:///a/b/c_new'))
496
+ with self.assertRaisesRegex(ValueError, 'Rename across different'):
497
+ file_system.rename('memory:///a/b/c_new', 'file:///a/b/c_d')
498
+
499
+ def test_chmod(self):
500
+ mock_fs = mock.Mock()
501
+ mock_fs.chmod = mock.Mock()
502
+ with mock.patch('fsspec.core.url_to_fs', return_value=(mock_fs, 'path')):
503
+ file_system.chmod('protocol:///path', 0o777)
504
+ mock_fs.chmod.assert_called_once_with('path', 0o777)
505
+
506
+ def test_rmdir(self):
507
+ file_system.rmdir('memory:///a/e')
508
+ self.assertFalse(self.fs.exists('memory:///a/e'))
509
+
510
+ def test_rmdirs(self):
511
+ file_system.mkdirs('memory:///x/y/z')
512
+ self.assertTrue(file_system.isdir('memory:///x/y/z'))
513
+ file_system.rmdirs('memory:///x')
514
+ self.assertFalse(file_system.path_exists('memory:///x'))
515
+
516
+ def test_fsspec_uri_catcher(self):
517
+ with mock.patch.object(
518
+ file_system.FsspecFileSystem, 'exists', return_value=True
519
+ ) as mock_fsspec_exists:
520
+ # We use a protocol that is not registered in
521
+ # fsspec.available_protocols() to make sure _FsspecUriCatcher is used.
522
+ self.assertTrue(file_system.path_exists('some-proto://foo'))
523
+ mock_fsspec_exists.assert_called_once_with('some-proto://foo')
524
+
525
+ # For full coverage of _FsspecUriCatcher.get_fs returning StdFileSystem,
526
+ # we need to test a non-URI path that doesn't match other prefixes.
527
+ # We mock StdFileSystem.exists to check if it's called.
528
+ with mock.patch.object(
529
+ file_system.StdFileSystem, 'exists', return_value=True
530
+ ) as mock_std_exists:
531
+ self.assertTrue(file_system.path_exists('/foo/bar/baz'))
532
+ mock_std_exists.assert_called_once_with('/foo/bar/baz')
533
+
534
+ with mock.patch.object(
535
+ file_system.FsspecFileSystem, 'rename', return_value=None
536
+ ) as mock_fsspec_rename:
537
+ file_system.rename('some-proto://foo', 'some-proto://bar')
538
+ mock_fsspec_rename.assert_called_once_with(
539
+ 'some-proto://foo', 'some-proto://bar'
540
+ )
541
+
251
542
 
252
543
  if __name__ == '__main__':
253
544
  unittest.main()
pyglove/core/logging.py CHANGED
@@ -17,9 +17,11 @@ This module allows PyGlove to use external created logger for logging PyGlove
17
17
  events without introducing library dependencies in PyGlove.
18
18
  """
19
19
 
20
+ import contextlib
20
21
  import inspect
21
22
  import logging
22
- from typing import Any, Callable, List, Union
23
+ import sys
24
+ from typing import Any, Callable, Iterator, List, Union
23
25
 
24
26
 
25
27
  _DEFAULT_LOGGER = logging.getLogger()
@@ -117,3 +119,45 @@ def critical(msg: str, *args, **kwargs) -> None:
117
119
  **kwargs: Keyword arguments for the logger.
118
120
  """
119
121
  _DEFAULT_LOGGER.critical(msg, *args, **kwargs)
122
+
123
+
124
+ def use_stream(
125
+ stream: Any,
126
+ level: int = logging.INFO,
127
+ name: str = 'custom',
128
+ fmt: str = '{levelname:8} | {asctime} | {message}',
129
+ datefmt: str = '%Y-%m-%d %H:%M:%S') -> logging.Logger:
130
+ """Use stdout for logging."""
131
+ logger = logging.getLogger(name)
132
+ logger.setLevel(level)
133
+ stdout_handler = logging.StreamHandler(stream=stream)
134
+ stdout_handler.setLevel(level)
135
+ stdout_handler.setFormatter(
136
+ logging.Formatter(fmt=fmt, datefmt=datefmt, style='{'))
137
+ logger.addHandler(stdout_handler)
138
+ set_logger(logger)
139
+ return logger
140
+
141
+
142
+ def use_stdout(
143
+ level: int = logging.INFO,
144
+ fmt: str = '{levelname:8} | {asctime} | {message}',
145
+ datefmt: str = '%Y-%m-%d %H:%M:%S') -> logging.Logger:
146
+ """Use stdout for logging."""
147
+ return use_stream(sys.stdout, level, 'stdout', fmt, datefmt)
148
+
149
+
150
+ @contextlib.contextmanager
151
+ def redirect_stream(
152
+ stream: Any,
153
+ level: int = logging.INFO,
154
+ name: str = 'custom',
155
+ fmt: str = '{levelname:8} | {asctime} | {message}',
156
+ datefmt: str = '%Y-%m-%d %H:%M:%S') -> Iterator[logging.Logger]:
157
+ """Redirect the stream to the given logger."""
158
+ previous_logger = get_logger()
159
+ try:
160
+ logger = use_stream(stream, level, name, fmt, datefmt)
161
+ yield logger
162
+ finally:
163
+ set_logger(previous_logger)
@@ -21,15 +21,9 @@ class LoggingTest(unittest.TestCase):
21
21
  """Tests for pg.logging."""
22
22
 
23
23
  def testLogging(self):
24
- string_io = io.StringIO()
25
- logger = logging.getLogger('logger1')
26
- logger.setLevel(logging.INFO)
27
- console_handler = logging.StreamHandler(stream=string_io)
28
- console_handler.setLevel(logging.INFO)
29
- logger.addHandler(console_handler)
30
-
31
24
  self.assertIs(pg_logging.get_logger(), logging.getLogger())
32
- pg_logging.set_logger(logger)
25
+ string_io = io.StringIO()
26
+ logger = pg_logging.use_stream(string_io, name='logger1', fmt='')
33
27
  self.assertIs(pg_logging.get_logger(), logger)
34
28
 
35
29
  pg_logging.debug('x=%s', 1)
@@ -46,19 +40,16 @@ class LoggingTest(unittest.TestCase):
46
40
  ]) + '\n')
47
41
 
48
42
  string_io = io.StringIO()
49
- logger = logging.getLogger('logger2')
50
- logger.setLevel(logging.DEBUG)
51
- console_handler = logging.StreamHandler(stream=string_io)
52
- console_handler.setLevel(logging.DEBUG)
53
- logger.addHandler(console_handler)
54
-
55
- pg_logging.set_logger(logger)
56
-
57
- pg_logging.debug('x=%s', 6)
58
- self.assertEqual(string_io.getvalue(), '\n'.join([
59
- 'x=6',
60
- ]) + '\n')
61
-
43
+ with pg_logging.redirect_stream(
44
+ string_io, level=logging.DEBUG, name='logger2', fmt=''
45
+ ):
46
+ pg_logging.debug('x=%s', 6)
47
+ self.assertEqual(string_io.getvalue(), '\n'.join([
48
+ 'x=6',
49
+ ]) + '\n')
50
+
51
+ pg_logging.use_stdout()
52
+ pg_logging.info('y=%s', 7)
62
53
 
63
54
  if __name__ == '__main__':
64
55
  unittest.main()