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
pyglove/core/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2019 The PyGlove Authors
1
+ # Copyright 2025 The PyGlove Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -306,6 +306,7 @@ format = utils.format # pylint: disable=redefined-builtin
306
306
  print = utils.print # pylint: disable=redefined-builtin
307
307
  docstr = utils.docstr
308
308
  catch_errors = utils.catch_errors
309
+ match_error = utils.match_error
309
310
  timeit = utils.timeit
310
311
 
311
312
  contextual_override = utils.contextual_override
@@ -352,6 +353,12 @@ from pyglove.core import coding
352
353
 
353
354
  from pyglove.core import logging
354
355
 
356
+ #
357
+ # Symbols from `monitoring.py`.
358
+ #
359
+
360
+ from pyglove.core import monitoring
361
+
355
362
  # pylint: enable=g-import-not-at-top
356
363
  # pylint: enable=reimported
357
364
  # pylint: enable=unused-import
pyglove/core/geno/base.py CHANGED
@@ -1447,6 +1447,7 @@ class DNA(symbolic.Object):
1447
1447
  *,
1448
1448
  allow_partial: bool = False,
1449
1449
  root_path: Optional[utils.KeyPath] = None,
1450
+ **kwargs,
1450
1451
  ) -> 'DNA':
1451
1452
  """Class method that load a DNA from a JSON value.
1452
1453
 
@@ -1454,6 +1455,7 @@ class DNA(symbolic.Object):
1454
1455
  json_value: Input JSON value, only JSON dict is acceptable.
1455
1456
  allow_partial: Whether to allow elements of the list to be partial.
1456
1457
  root_path: KeyPath of loaded object in its object tree.
1458
+ **kwargs: Keyword arguments that will be passed to symbolic.from_json.
1457
1459
 
1458
1460
  Returns:
1459
1461
  A DNA object.
@@ -1463,16 +1465,18 @@ class DNA(symbolic.Object):
1463
1465
  # NOTE(daiyip): DNA.parse will validate the input. Therefore, we can
1464
1466
  # disable runtime type check during constructing the DNA objects.
1465
1467
  with symbolic.enable_type_check(False):
1466
- dna = DNA.parse(symbolic.from_json(json_value.get('value')))
1468
+ dna = DNA.parse(symbolic.from_json(json_value.get('value'), **kwargs))
1467
1469
  if 'metadata' in json_value:
1468
1470
  dna.rebind(
1469
- metadata=symbolic.from_json(json_value.get('metadata')),
1471
+ metadata=symbolic.from_json(json_value.get('metadata'), **kwargs),
1470
1472
  raise_on_no_change=False, skip_notification=True)
1471
1473
  else:
1472
1474
  dna = super(DNA, cls).from_json(
1473
1475
  json_value,
1474
1476
  allow_partial=allow_partial,
1475
- root_path=root_path) # pytype: disable=bad-return-type
1477
+ root_path=root_path,
1478
+ **kwargs,
1479
+ ) # pytype: disable=bad-return-type
1476
1480
  assert isinstance(dna, DNA)
1477
1481
  if cloneable_metadata_keys:
1478
1482
  dna._cloneable_metadata_keys = set(cloneable_metadata_keys) # pylint: disable=protected-access
@@ -14,8 +14,11 @@
14
14
  """Pluggable file system."""
15
15
 
16
16
  import abc
17
+ import fnmatch
18
+ import glob as std_glob
17
19
  import io
18
20
  import os
21
+ import re
19
22
  from typing import Any, Literal, Optional, Union
20
23
 
21
24
 
@@ -84,6 +87,10 @@ class FileSystem(metaclass=abc.ABCMeta):
84
87
  def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
85
88
  """Returns True if a path exists."""
86
89
 
90
+ @abc.abstractmethod
91
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
92
+ """Lists all files or sub-directories."""
93
+
87
94
  @abc.abstractmethod
88
95
  def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
89
96
  """Lists all files or sub-directories."""
@@ -111,6 +118,14 @@ class FileSystem(metaclass=abc.ABCMeta):
111
118
  def rm(self, path: Union[str, os.PathLike[str]]) -> None:
112
119
  """Removes a file based on a path."""
113
120
 
121
+ @abc.abstractmethod
122
+ def rename(
123
+ self,
124
+ oldpath: Union[str, os.PathLike[str]],
125
+ newpath: Union[str, os.PathLike[str]],
126
+ ) -> None:
127
+ """Renames a file based on a path."""
128
+
114
129
  @abc.abstractmethod
115
130
  def rmdir(self, path: Union[str, os.PathLike[str]]) -> bool:
116
131
  """Removes a directory based on a path."""
@@ -137,9 +152,10 @@ def resolve_path(path: Union[str, os.PathLike[str]]) -> str:
137
152
  class StdFile(File):
138
153
  """The standard file."""
139
154
 
140
- def __init__(self, file_object) -> None:
155
+ def __init__(self, file_object, path: Union[str, os.PathLike[str]]) -> None:
141
156
  super().__init__()
142
157
  self._file_object = file_object
158
+ self._path = path
143
159
 
144
160
  def read(self, size: Optional[int] = None) -> Union[str, bytes]:
145
161
  return self._file_object.read(size)
@@ -169,7 +185,7 @@ class StdFileSystem(FileSystem):
169
185
  def open(
170
186
  self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
171
187
  ) -> File:
172
- return StdFile(io.open(path, mode, **kwargs))
188
+ return StdFile(io.open(path, mode, **kwargs), path)
173
189
 
174
190
  def chmod(self, path: Union[str, os.PathLike[str]], mode: int) -> None:
175
191
  os.chmod(path, mode)
@@ -177,6 +193,9 @@ class StdFileSystem(FileSystem):
177
193
  def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
178
194
  return os.path.exists(path)
179
195
 
196
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
197
+ return std_glob.glob(pattern)
198
+
180
199
  def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
181
200
  return os.listdir(path)
182
201
 
@@ -196,6 +215,13 @@ class StdFileSystem(FileSystem):
196
215
  ) -> None:
197
216
  os.makedirs(path, mode, exist_ok)
198
217
 
218
+ def rename(
219
+ self,
220
+ oldpath: Union[str, os.PathLike[str]],
221
+ newpath: Union[str, os.PathLike[str]],
222
+ ) -> None:
223
+ os.rename(oldpath, newpath)
224
+
199
225
  def rm(self, path: Union[str, os.PathLike[str]]) -> None:
200
226
  os.remove(path)
201
227
 
@@ -286,6 +312,21 @@ class MemoryFileSystem(FileSystem):
286
312
  def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
287
313
  return self._locate(path) is not None
288
314
 
315
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
316
+ pattern = resolve_path(pattern)
317
+ regex = re.compile(fnmatch.translate(pattern))
318
+
319
+ results = []
320
+ def _traverse(node, prefix_parts):
321
+ for k, v in node.items():
322
+ path = self._prefix + '/'.join(prefix_parts + [k])
323
+ if regex.match(path):
324
+ results.append(path)
325
+ if isinstance(v, dict):
326
+ _traverse(v, prefix_parts + [k])
327
+ _traverse(self._root, [])
328
+ return results
329
+
289
330
  def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
290
331
  d = self._locate(path)
291
332
  if not isinstance(d, dict):
@@ -338,6 +379,46 @@ class MemoryFileSystem(FileSystem):
338
379
  raise NotADirectoryError(path)
339
380
  current = entry
340
381
 
382
+ def rename(
383
+ self,
384
+ oldpath: Union[str, os.PathLike[str]],
385
+ newpath: Union[str, os.PathLike[str]],
386
+ ) -> None:
387
+ oldpath_str = resolve_path(oldpath)
388
+ newpath_str = resolve_path(newpath)
389
+
390
+ old_parent_dir, old_name = self._parent_and_name(oldpath_str)
391
+ entry = old_parent_dir.get(old_name)
392
+
393
+ if entry is None:
394
+ raise FileNotFoundError(oldpath_str)
395
+
396
+ new_entry = self._locate(newpath_str)
397
+ if new_entry is not None:
398
+ if isinstance(entry, dict): # oldpath is dir
399
+ if not isinstance(new_entry, dict):
400
+ raise NotADirectoryError(
401
+ f'Cannot rename directory {oldpath_str!r} '
402
+ f'to non-directory {newpath_str!r}')
403
+ elif new_entry:
404
+ raise OSError(f'Directory not empty: {newpath_str!r}')
405
+ else:
406
+ self.rmdir(newpath_str)
407
+ else: # oldpath is file
408
+ if isinstance(new_entry, dict):
409
+ raise IsADirectoryError(
410
+ f'Cannot rename non-directory {oldpath_str!r} '
411
+ f'to directory {newpath_str!r}')
412
+ else:
413
+ self.rm(newpath_str)
414
+ elif isinstance(entry, dict) and newpath_str.startswith(oldpath_str + '/'):
415
+ raise OSError(f'Cannot move directory {oldpath_str!r} '
416
+ f'to a subdirectory of itself {newpath_str!r}')
417
+
418
+ new_parent_dir, new_name = self._parent_and_name(newpath_str)
419
+ new_parent_dir[new_name] = entry
420
+ del old_parent_dir[old_name]
421
+
341
422
  def rm(self, path: Union[str, os.PathLike[str]]) -> None:
342
423
  parent_dir, name = self._parent_and_name(path)
343
424
  entry = parent_dir.get(name)
@@ -412,6 +493,205 @@ def add_file_system(prefix: str, fs: FileSystem) -> None:
412
493
  add_file_system('/mem/', MemoryFileSystem('/mem/'))
413
494
 
414
495
 
496
+ try:
497
+ # pylint: disable=g-import-not-at-top
498
+ # pytype: disable=import-error
499
+ import fsspec
500
+ # pytype: enable=import-error
501
+ # pylint: enable=g-import-not-at-top
502
+ except ImportError:
503
+ fsspec = None
504
+
505
+
506
+ class FsspecFile(File):
507
+ """File object based on fsspec."""
508
+
509
+ def __init__(self, f):
510
+ self._f = f
511
+
512
+ def read(self, size: Optional[int] = None) -> Union[str, bytes]:
513
+ return self._f.read(size)
514
+
515
+ def readline(self) -> Union[str, bytes]:
516
+ return self._f.readline()
517
+
518
+ def write(self, content: Union[str, bytes]) -> None:
519
+ self._f.write(content)
520
+
521
+ def seek(self, offset: int, whence: Literal[0, 1, 2] = 0) -> int:
522
+ return self._f.seek(offset, whence)
523
+
524
+ def tell(self) -> int:
525
+ return self._f.tell()
526
+
527
+ def flush(self) -> None:
528
+ self._f.flush()
529
+
530
+ def close(self) -> None:
531
+ self._f.close()
532
+
533
+
534
+ class FsspecFileSystem(FileSystem):
535
+ """File system based on fsspec."""
536
+
537
+ def open(
538
+ self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
539
+ ) -> File:
540
+ assert fsspec is not None, '`fsspec` is not installed.'
541
+ return FsspecFile(fsspec.open(path, mode, **kwargs).open())
542
+
543
+ def chmod(self, path: Union[str, os.PathLike[str]], mode: int) -> None:
544
+ assert fsspec is not None, '`fsspec` is not installed.'
545
+ fs, path = fsspec.core.url_to_fs(path)
546
+ if hasattr(fs, 'chmod'):
547
+ fs.chmod(path, mode)
548
+
549
+ def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
550
+ assert fsspec is not None, '`fsspec` is not installed.'
551
+ fs, path = fsspec.core.url_to_fs(path)
552
+ return fs.exists(path)
553
+
554
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
555
+ assert fsspec is not None, '`fsspec` is not installed.'
556
+ fs, path = fsspec.core.url_to_fs(pattern)
557
+ protocol = fsspec.utils.get_protocol(pattern)
558
+ return [f'{protocol}:///{r.lstrip("/")}' for r in fs.glob(path)]
559
+
560
+ def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
561
+ assert fsspec is not None, '`fsspec` is not installed.'
562
+ fs, path = fsspec.core.url_to_fs(path)
563
+ return [os.path.basename(f) for f in fs.ls(path, detail=False)]
564
+
565
+ def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
566
+ assert fsspec is not None, '`fsspec` is not installed.'
567
+ fs, path = fsspec.core.url_to_fs(path)
568
+ return fs.isdir(path)
569
+
570
+ def mkdir(
571
+ self,
572
+ path: Union[str, os.PathLike[str]], mode: int = 0o777
573
+ ) -> None:
574
+ assert fsspec is not None, '`fsspec` is not installed.'
575
+ fs, path = fsspec.core.url_to_fs(path)
576
+ fs.mkdir(path)
577
+
578
+ def mkdirs(
579
+ self,
580
+ path: Union[str, os.PathLike[str]],
581
+ mode: int = 0o777,
582
+ exist_ok: bool = True,
583
+ ) -> None:
584
+ assert fsspec is not None, '`fsspec` is not installed.'
585
+ fs, path = fsspec.core.url_to_fs(path)
586
+ fs.makedirs(path, exist_ok=exist_ok)
587
+
588
+ def rm(self, path: Union[str, os.PathLike[str]]) -> None:
589
+ assert fsspec is not None, '`fsspec` is not installed.'
590
+ fs, path = fsspec.core.url_to_fs(path)
591
+ fs.rm(path)
592
+
593
+ def rename(
594
+ self,
595
+ oldpath: Union[str, os.PathLike[str]],
596
+ newpath: Union[str, os.PathLike[str]],
597
+ ) -> None:
598
+ assert fsspec is not None, '`fsspec` is not installed.'
599
+ fs, old_path = fsspec.core.url_to_fs(oldpath)
600
+ fs2, new_path = fsspec.core.url_to_fs(newpath)
601
+ if fs.__class__ != fs2.__class__:
602
+ raise ValueError(
603
+ f'Rename across different filesystems is not supported: '
604
+ f'{type(fs)} vs {type(fs2)}'
605
+ )
606
+ fs.rename(old_path, new_path)
607
+
608
+ def rmdir(self, path: Union[str, os.PathLike[str]]) -> None: # pytype: disable=signature-mismatch
609
+ assert fsspec is not None, '`fsspec` is not installed.'
610
+ fs, path = fsspec.core.url_to_fs(path)
611
+ fs.rmdir(path)
612
+
613
+ def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
614
+ assert fsspec is not None, '`fsspec` is not installed.'
615
+ fs, path = fsspec.core.url_to_fs(path)
616
+ fs.rm(path, recursive=True)
617
+
618
+
619
+ class _FsspecUriCatcher(FileSystem):
620
+ """File system to catch URI paths and redirect to FsspecFileSystem."""
621
+
622
+ # Catch all paths that contains '://' but not registered by
623
+ # available_protocols.
624
+ _URI_PATTERN = re.compile(r'^[a-zA-Z][a-zA-Z0-9+-.]*://.*')
625
+
626
+ def __init__(self):
627
+ super().__init__()
628
+ self._std_fs = StdFileSystem()
629
+ self._fsspec_fs = FsspecFileSystem()
630
+
631
+ def get_fs(self, path: Union[str, os.PathLike[str]]) -> FileSystem:
632
+ if self._URI_PATTERN.match(resolve_path(path)):
633
+ return self._fsspec_fs
634
+ return self._std_fs
635
+
636
+ def open(
637
+ self, path: Union[str, os.PathLike[str]], mode: str = 'r', **kwargs
638
+ ) -> File:
639
+ return self.get_fs(path).open(path, mode, **kwargs)
640
+
641
+ def chmod(self, path: Union[str, os.PathLike[str]], mode: int) -> None:
642
+ self.get_fs(path).chmod(path, mode)
643
+
644
+ def exists(self, path: Union[str, os.PathLike[str]]) -> bool:
645
+ return self.get_fs(path).exists(path)
646
+
647
+ def glob(self, pattern: Union[str, os.PathLike[str]]) -> list[str]:
648
+ return self.get_fs(pattern).glob(pattern)
649
+
650
+ def listdir(self, path: Union[str, os.PathLike[str]]) -> list[str]:
651
+ return self.get_fs(path).listdir(path)
652
+
653
+ def isdir(self, path: Union[str, os.PathLike[str]]) -> bool:
654
+ return self.get_fs(path).isdir(path)
655
+
656
+ def mkdir(
657
+ self,
658
+ path: Union[str, os.PathLike[str]],
659
+ mode: int = 0o777
660
+ ) -> None:
661
+ self.get_fs(path).mkdir(path, mode)
662
+
663
+ def mkdirs(
664
+ self,
665
+ path: Union[str, os.PathLike[str]],
666
+ mode: int = 0o777,
667
+ exist_ok: bool = True,
668
+ ) -> None:
669
+ self.get_fs(path).mkdirs(path, mode, exist_ok)
670
+
671
+ def rm(self, path: Union[str, os.PathLike[str]]) -> None:
672
+ self.get_fs(path).rm(path)
673
+
674
+ def rename(
675
+ self,
676
+ oldpath: Union[str, os.PathLike[str]],
677
+ newpath: Union[str, os.PathLike[str]],
678
+ ) -> None:
679
+ self.get_fs(oldpath).rename(oldpath, newpath)
680
+
681
+ def rmdir(self, path: Union[str, os.PathLike[str]]) -> None: # pytype: disable=signature-mismatch
682
+ self.get_fs(path).rmdir(path)
683
+
684
+ def rmdirs(self, path: Union[str, os.PathLike[str]]) -> None:
685
+ self.get_fs(path).rmdirs(path)
686
+
687
+
688
+ if fsspec is not None:
689
+ fsspec_fs = FsspecFileSystem()
690
+ for p in fsspec.available_protocols():
691
+ add_file_system(p + '://', fsspec_fs)
692
+ add_file_system('', _FsspecUriCatcher())
693
+
694
+
415
695
  #
416
696
  # APIs for file IO.
417
697
  #
@@ -458,6 +738,14 @@ def writefile(
458
738
  chmod(path, perms)
459
739
 
460
740
 
741
+ def rename(
742
+ oldpath: Union[str, os.PathLike[str]],
743
+ newpath: Union[str, os.PathLike[str]],
744
+ ) -> None:
745
+ """Renames a file."""
746
+ _fs.get(oldpath).rename(oldpath, newpath)
747
+
748
+
461
749
  def rm(path: Union[str, os.PathLike[str]]) -> None:
462
750
  """Removes a file."""
463
751
  _fs.get(path).rm(path)
@@ -468,6 +756,11 @@ def path_exists(path: Union[str, os.PathLike[str]]) -> bool:
468
756
  return _fs.get(path).exists(path)
469
757
 
470
758
 
759
+ def glob(pattern: Union[str, os.PathLike[str]]) -> list[str]:
760
+ """Lists all files or sub-directories."""
761
+ return _fs.get(pattern).glob(pattern)
762
+
763
+
471
764
  def listdir(
472
765
  path: Union[str, os.PathLike[str]], fullpath: bool = False
473
766
  ) -> list[str]: # pylint: disable=redefined-builtin