pyglove 0.5.0.dev202510230131__py3-none-any.whl → 0.5.0.dev202601060812__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.
@@ -0,0 +1,147 @@
1
+ # Copyright 2021 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Symbolic types for reprenting unknown types and objects."""
15
+
16
+ from typing import Annotated, Any, ClassVar, Literal
17
+ from pyglove.core import typing as pg_typing
18
+ from pyglove.core import utils
19
+ from pyglove.core.symbolic import list as pg_list # pylint: disable=unused-import
20
+ from pyglove.core.symbolic import object as pg_object
21
+
22
+
23
+ class UnknownSymbol(pg_object.Object, pg_typing.CustomTyping):
24
+ """Interface for symbolic representation of unknown symbols."""
25
+ auto_register = False
26
+
27
+ def custom_apply(self, *args, **kwargs) -> tuple[bool, Any]:
28
+ """Bypass PyGlove type check."""
29
+ return (False, self)
30
+
31
+
32
+ class UnknownType(UnknownSymbol):
33
+ """Symbolic object for representing unknown types."""
34
+
35
+ auto_register = True
36
+ __serialization_key__ = 'unknown_type'
37
+
38
+ # TODO(daiyip): Revisit the design on how `pg.typing.Object()` handles
39
+ # UnknownType. This hacky solution should be removed in the future.
40
+ __no_type_check__ = True
41
+
42
+ name: str
43
+ args: list[Any] = []
44
+
45
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
46
+ json_dict = {'_type': 'type', 'name': self.name}
47
+ if self.args:
48
+ json_dict['args'] = utils.to_json(self.args, **kwargs)
49
+ return json_dict
50
+
51
+ def format(
52
+ self,
53
+ compact: bool = False,
54
+ verbose: bool = True,
55
+ root_indent: int = 0,
56
+ **kwargs
57
+ ) -> str:
58
+ s = f'<unknown-type {self.name}>'
59
+ if self.args:
60
+ s += f'[{", ".join(repr(x) for x in self.args)}]'
61
+ return s
62
+
63
+ def __call__(self, **kwargs):
64
+ return UnknownTypedObject(
65
+ type_name=self.name, **kwargs
66
+ )
67
+
68
+
69
+ class UnknownCallable(UnknownSymbol):
70
+ """Symbolic object for representing unknown callables."""
71
+
72
+ auto_register = False
73
+ name: str
74
+ CALLABLE_TYPE: ClassVar[Literal['function', 'method']]
75
+
76
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
77
+ return {'_type': self.CALLABLE_TYPE, 'name': self.name}
78
+
79
+ def format(
80
+ self,
81
+ compact: bool = False,
82
+ verbose: bool = True,
83
+ root_indent: int = 0,
84
+ **kwargs
85
+ ) -> str:
86
+ return f'<unknown-{self.CALLABLE_TYPE} {self.name}>'
87
+
88
+
89
+ class UnknownFunction(UnknownCallable):
90
+ """Symbolic objject for representing unknown functions."""
91
+
92
+ auto_register = True
93
+ __serialization_key__ = 'unknown_function'
94
+ CALLABLE_TYPE = 'function'
95
+
96
+
97
+ class UnknownMethod(UnknownCallable):
98
+ """Symbolic object for representing unknown methods."""
99
+
100
+ auto_register = True
101
+ __serialization_key__ = 'unknown_method'
102
+ CALLABLE_TYPE = 'method'
103
+
104
+
105
+ class UnknownTypedObject(UnknownSymbol):
106
+ """Symbolic object for representing objects of unknown-type."""
107
+
108
+ auto_register = True
109
+ __serialization_key__ = 'unknown_object'
110
+
111
+ type_name: str
112
+ __kwargs__: Annotated[
113
+ Any,
114
+ (
115
+ 'Fields of the original object will be kept as symbolic attributes '
116
+ 'of this object so they can be accessed through `__getattr__`.'
117
+ )
118
+ ]
119
+
120
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
121
+ """Converts current object to a dict of plain Python objects."""
122
+ json_dict = self._sym_attributes.to_json(
123
+ exclude_keys=set(['type_name']), **kwargs
124
+ )
125
+ assert isinstance(json_dict, dict)
126
+ json_dict[utils.JSONConvertible.TYPE_NAME_KEY] = self.type_name
127
+ return json_dict
128
+
129
+ def format(
130
+ self,
131
+ compact: bool = False,
132
+ verbose: bool = True,
133
+ root_indent: int = 0,
134
+ **kwargs
135
+ ) -> str:
136
+ exclude_keys = kwargs.pop('exclude_keys', set())
137
+ exclude_keys.add('type_name')
138
+ kwargs['exclude_keys'] = exclude_keys
139
+ return self._sym_attributes.format(
140
+ compact,
141
+ verbose,
142
+ root_indent,
143
+ cls_name=f'<unknown-type {self.type_name}>',
144
+ key_as_attribute=True,
145
+ bracket_type=utils.BracketType.ROUND,
146
+ **kwargs,
147
+ )
@@ -0,0 +1,100 @@
1
+ # Copyright 2025 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import unittest
16
+ from pyglove.core import utils
17
+ from pyglove.core.symbolic import unknown_symbols
18
+
19
+
20
+ class UnknownTypeTest(unittest.TestCase):
21
+
22
+ def test_basics(self):
23
+ t = unknown_symbols.UnknownType(name='__main__.ABC', args=[int, str])
24
+ self.assertEqual(t.name, '__main__.ABC')
25
+ self.assertEqual(t.args, [int, str])
26
+ self.assertEqual(
27
+ repr(t),
28
+ '<unknown-type __main__.ABC>[<class \'int\'>, <class \'str\'>]'
29
+ )
30
+ self.assertEqual(
31
+ t.to_json(),
32
+ {
33
+ '_type': 'type',
34
+ 'name': '__main__.ABC',
35
+ 'args': [
36
+ {'_type': 'type', 'name': 'builtins.int'},
37
+ {'_type': 'type', 'name': 'builtins.str'},
38
+ ]
39
+ }
40
+ )
41
+ self.assertEqual(utils.from_json(t.to_json(), convert_unknown=True), t)
42
+ self.assertEqual(
43
+ t(x=1, y=2),
44
+ unknown_symbols.UnknownTypedObject(type_name='__main__.ABC', x=1, y=2)
45
+ )
46
+
47
+
48
+ class UnknownFunctionTest(unittest.TestCase):
49
+
50
+ def test_basics(self):
51
+ t = unknown_symbols.UnknownFunction(name='__main__.foo')
52
+ self.assertEqual(t.name, '__main__.foo')
53
+ self.assertEqual(repr(t), '<unknown-function __main__.foo>')
54
+ self.assertEqual(
55
+ t.to_json(),
56
+ {
57
+ '_type': 'function',
58
+ 'name': '__main__.foo',
59
+ }
60
+ )
61
+ self.assertEqual(utils.from_json(t.to_json(), convert_unknown=True), t)
62
+
63
+
64
+ class UnknownMethodTest(unittest.TestCase):
65
+
66
+ def test_basics(self):
67
+ t = unknown_symbols.UnknownMethod(name='__main__.ABC.bar')
68
+ self.assertEqual(t.name, '__main__.ABC.bar')
69
+ self.assertEqual(repr(t), '<unknown-method __main__.ABC.bar>')
70
+ self.assertEqual(
71
+ t.to_json(),
72
+ {
73
+ '_type': 'method',
74
+ 'name': '__main__.ABC.bar',
75
+ }
76
+ )
77
+ self.assertEqual(utils.from_json(t.to_json(), convert_unknown=True), t)
78
+
79
+
80
+ class UnknownObjectTest(unittest.TestCase):
81
+
82
+ def test_basics(self):
83
+ v = unknown_symbols.UnknownTypedObject(type_name='__main__.ABC', x=1)
84
+ self.assertEqual(v.type_name, '__main__.ABC')
85
+ self.assertEqual(v.x, 1)
86
+ self.assertEqual(repr(v), '<unknown-type __main__.ABC>(x=1)')
87
+ self.assertEqual(
88
+ str(v), '<unknown-type __main__.ABC>(\n x = 1\n)')
89
+ self.assertEqual(
90
+ v.to_json(),
91
+ {
92
+ '_type': '__main__.ABC',
93
+ 'x': 1,
94
+ }
95
+ )
96
+ self.assertEqual(utils.from_json(v.to_json(), convert_unknown=True), v)
97
+
98
+
99
+ if __name__ == '__main__':
100
+ unittest.main()
@@ -16,6 +16,7 @@
16
16
  import builtins
17
17
  import collections
18
18
  import inspect
19
+ import sys
19
20
  import types
20
21
  import typing
21
22
 
@@ -197,6 +198,8 @@ def annotation_from_str(
197
198
  def _resolve(type_id: str):
198
199
 
199
200
  def _as_forward_ref() -> typing.ForwardRef:
201
+ if sys.version_info >= (3, 14):
202
+ return typing.ForwardRef(type_id) # pytype: disable=not-callable
200
203
  return typing.ForwardRef(type_id, False, parent_module) # pytype: disable=not-callable
201
204
 
202
205
  def _resolve_name(name: str, parent_obj: typing.Any):
@@ -34,6 +34,12 @@ class Foo:
34
34
  _MODULE = sys.modules[__name__]
35
35
 
36
36
 
37
+ def typing_forward_ref(name: str) -> typing.ForwardRef:
38
+ if sys.version_info >= (3, 14):
39
+ return typing.ForwardRef(name)
40
+ return typing.ForwardRef(name, False, _MODULE)
41
+
42
+
37
43
  class AnnotationFromStrTest(unittest.TestCase):
38
44
  """Tests for annotation_from_str."""
39
45
 
@@ -69,7 +75,7 @@ class AnnotationFromStrTest(unittest.TestCase):
69
75
  )
70
76
  self.assertEqual(
71
77
  annotation_conversion.annotation_from_str('list[Foo.Baz]', _MODULE),
72
- list[typing.ForwardRef('Foo.Baz', False, _MODULE)]
78
+ list[typing_forward_ref('Foo.Baz')]
73
79
  )
74
80
 
75
81
  def test_generic_types(self):
@@ -138,18 +144,12 @@ class AnnotationFromStrTest(unittest.TestCase):
138
144
  self.assertEqual(
139
145
  annotation_conversion.annotation_from_str(
140
146
  'AAA', _MODULE),
141
- typing.ForwardRef(
142
- 'AAA', False, _MODULE
143
- )
147
+ typing_forward_ref('AAA')
144
148
  )
145
149
  self.assertEqual(
146
150
  annotation_conversion.annotation_from_str(
147
151
  'typing.List[AAA]', _MODULE),
148
- typing.List[
149
- typing.ForwardRef(
150
- 'AAA', False, _MODULE
151
- )
152
- ]
152
+ typing.List[typing_forward_ref('AAA')]
153
153
  )
154
154
 
155
155
  def test_reloading(self):
@@ -157,20 +157,12 @@ class AnnotationFromStrTest(unittest.TestCase):
157
157
  self.assertEqual(
158
158
  annotation_conversion.annotation_from_str(
159
159
  'typing.List[Foo]', _MODULE),
160
- typing.List[
161
- typing.ForwardRef(
162
- 'Foo', False, _MODULE
163
- )
164
- ]
160
+ typing.List[typing_forward_ref('Foo')]
165
161
  )
166
162
  self.assertEqual(
167
163
  annotation_conversion.annotation_from_str(
168
164
  'typing.List[Foo.Bar]', _MODULE),
169
- typing.List[
170
- typing.ForwardRef(
171
- 'Foo.Bar', False, _MODULE
172
- )
173
- ]
165
+ typing.List[typing_forward_ref('Foo.Bar')]
174
166
  )
175
167
  delattr(_MODULE, '__reloading__')
176
168
 
@@ -15,6 +15,7 @@
15
15
 
16
16
  import calendar
17
17
  import datetime
18
+ import sys
18
19
  from typing import Any, Callable, Optional, Tuple, Type, Union
19
20
 
20
21
  from pyglove.core import utils
@@ -135,9 +136,22 @@ def _register_builtin_converters():
135
136
  register_converter(int, float, float)
136
137
 
137
138
  # int <=> datetime.datetime.
138
- register_converter(int, datetime.datetime, datetime.datetime.utcfromtimestamp)
139
- register_converter(datetime.datetime, int,
140
- lambda x: calendar.timegm(x.timetuple()))
139
+ if sys.version_info >= (3, 11):
140
+ register_converter(
141
+ int,
142
+ datetime.datetime,
143
+ lambda x: datetime.datetime.fromtimestamp(x, datetime.UTC)
144
+ )
145
+ else:
146
+ register_converter(
147
+ int, datetime.datetime, datetime.datetime.utcfromtimestamp
148
+ )
149
+
150
+ register_converter(
151
+ datetime.datetime,
152
+ int,
153
+ lambda x: calendar.timegm(x.timetuple())
154
+ )
141
155
 
142
156
  # string <=> KeyPath.
143
157
  register_converter(str, utils.KeyPath, utils.KeyPath.parse)
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  import calendar
15
15
  import datetime
16
+ import sys
16
17
  import typing
17
18
  import unittest
18
19
 
@@ -130,12 +131,16 @@ class BuiltInConversionsTest(unittest.TestCase):
130
131
  def test_datetime_to_int(self):
131
132
  """Test built-in converter between int and datetime.datetime."""
132
133
  timestamp = calendar.timegm(datetime.datetime.now().timetuple())
133
- now = datetime.datetime.utcfromtimestamp(timestamp)
134
+ if sys.version_info >= (3, 11):
135
+ now = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
136
+ else:
137
+ now = datetime.datetime.utcfromtimestamp(timestamp)
134
138
  self.assertEqual(vs.Object(datetime.datetime).apply(timestamp), now)
135
139
  self.assertEqual(vs.Int().apply(now), timestamp)
136
140
  self.assertEqual(
137
141
  type_conversion.get_json_value_converter(datetime.datetime)(now),
138
- timestamp)
142
+ timestamp
143
+ )
139
144
 
140
145
  def test_keypath_to_str(self):
141
146
  """Test built-in converter between string and KeyPath."""
@@ -1930,8 +1930,12 @@ class Object(Generic, ValueSpecBase):
1930
1930
  elif isinstance(t, type):
1931
1931
  if t is object:
1932
1932
  raise TypeError('<class \'object\'> is too general for Object spec.')
1933
+ elif getattr(t, '__no_type_check__', False):
1934
+ t = object
1933
1935
  elif not pg_inspect.is_generic(t):
1934
- raise TypeError('"cls" for Object spec should be a type or str.')
1936
+ raise TypeError(
1937
+ f'"cls" for Object spec should be a type or str. Encountered: {t!r}.'
1938
+ )
1935
1939
 
1936
1940
  self._forward_ref = forward_ref
1937
1941
  self._type_args = type_args
@@ -2193,6 +2193,11 @@ class ObjectTest(ValueSpecTest):
2193
2193
  self.assertEqual(
2194
2194
  vs.Object(forward_ref('Foo')).forward_refs, set([forward_ref('Foo')]))
2195
2195
 
2196
+ def test_no_type_check_special_handling(self):
2197
+ x = self.A()
2198
+ setattr(x, '__no_type_check__', True)
2199
+ self.assertIs(vs.Object(x).cls, object)
2200
+
2196
2201
  def test_default(self):
2197
2202
  self.assertEqual(vs.Object(self.A).default, typed_missing.MISSING_VALUE)
2198
2203
  a = self.A()
@@ -685,7 +685,7 @@ def from_json(
685
685
  *,
686
686
  context: Optional[JSONConversionContext] = None,
687
687
  auto_import: bool = True,
688
- auto_dict: bool = False,
688
+ convert_unknown: bool = False,
689
689
  **kwargs
690
690
  ) -> Any:
691
691
  """Deserializes a (maybe) JSONConvertible value from JSON value.
@@ -697,8 +697,13 @@ def from_json(
697
697
  identify its parent module and automatically import it. For example,
698
698
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
699
699
  find the class 'A' within the imported module.
700
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
701
- as dict, with '_type' renamed to 'type_name'.
700
+ convert_unknown: If True, when a '_type' is not registered and cannot
701
+ be imported, PyGlove will create objects of:
702
+ - `pg.symbolic.UnknownType` for unknown types;
703
+ - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
704
+ - `pg.symbolic.UnknownFunction` for unknown functions;
705
+ - `pg.symbolic.UnknownMethod` for unknown methods.
706
+ If False, TypeError will be raised.
702
707
  **kwargs: Keyword arguments that will be passed to JSONConvertible.__init__.
703
708
 
704
709
  Returns:
@@ -707,14 +712,21 @@ def from_json(
707
712
  if context is None:
708
713
  if (isinstance(json_value, dict)
709
714
  and (context_node := json_value.get(JSONConvertible.CONTEXT_KEY))):
710
- context = JSONConversionContext.from_json(context_node, **kwargs)
715
+ context = JSONConversionContext.from_json(
716
+ context_node,
717
+ auto_import=auto_import,
718
+ convert_unknown=convert_unknown,
719
+ **kwargs
720
+ )
711
721
  json_value = json_value[JSONConvertible.ROOT_VALUE_KEY]
712
722
  else:
713
723
  context = JSONConversionContext()
714
724
 
715
725
  typename_resolved = kwargs.pop('_typename_resolved', False)
716
726
  if not typename_resolved:
717
- json_value = resolve_typenames(json_value, auto_import, auto_dict)
727
+ json_value = resolve_typenames(
728
+ json_value, auto_import=auto_import, convert_unknown=convert_unknown
729
+ )
718
730
 
719
731
  def child_from(v):
720
732
  return from_json(v, context=context, _typename_resolved=True, **kwargs)
@@ -743,7 +755,7 @@ def from_json(
743
755
  def resolve_typenames(
744
756
  json_value: JSONValueType,
745
757
  auto_import: bool = True,
746
- auto_dict: bool = False,
758
+ convert_unknown: bool = False,
747
759
  ) -> JSONValueType:
748
760
  """Inplace resolves the "_type" keys with their factories in a JSON tree."""
749
761
 
@@ -755,11 +767,11 @@ def resolve_typenames(
755
767
  return False
756
768
  type_name = v[JSONConvertible.TYPE_NAME_KEY]
757
769
  if type_name == 'type':
758
- factory_fn = _type_from_json
770
+ factory_fn = _type_from_json(convert_unknown)
759
771
  elif type_name == 'function':
760
- factory_fn = _function_from_json
772
+ factory_fn = _function_from_json(convert_unknown)
761
773
  elif type_name == 'method':
762
- factory_fn = _method_from_json
774
+ factory_fn = _method_from_json(convert_unknown)
763
775
  else:
764
776
  cls = JSONConvertible.class_from_typename(type_name)
765
777
  if cls is None:
@@ -768,32 +780,38 @@ def resolve_typenames(
768
780
  cls = _load_symbol(type_name)
769
781
  assert inspect.isclass(cls), cls
770
782
  except (ModuleNotFoundError, AttributeError) as e:
771
- if not auto_dict:
783
+ if not convert_unknown:
772
784
  raise TypeError(
773
785
  f'Cannot load class {type_name!r}.\n'
774
- 'Try pass `auto_dict=True` to load the object into a dict '
775
- 'without depending on the type.'
786
+ 'Try pass `convert_unknown=True` to load the object into '
787
+ '`pg.symbolic.UnknownObject` without depending on the type.'
776
788
  ) from e
777
- elif not auto_dict:
789
+ elif not convert_unknown:
778
790
  raise TypeError(
779
791
  f'Type name \'{type_name}\' is not registered '
780
792
  'with a `pg.JSONConvertible` subclass.\n'
781
- 'Try pass `auto_import=True` to load the type from its module, '
782
- 'or pass `auto_dict=True` to load the object into a dict '
783
- 'without depending on the type.'
793
+ 'Try pass `auto_import=True` to load the type from its module.'
784
794
  )
785
795
 
786
796
  factory_fn = getattr(cls, 'from_json', None)
787
- if cls is not None and factory_fn is None and not auto_dict:
797
+ if cls is not None and factory_fn is None and not convert_unknown:
788
798
  raise TypeError(
789
799
  f'{cls} is not a `pg.JSONConvertible` subclass.'
790
- 'Try pass `auto_dict=True` to load the object into a dict '
791
- 'without depending on the type.'
800
+ 'Try pass `convert_unknown=True` to load the object into a '
801
+ '`pg.symbolic.UnknownObject` without depending on the type.'
792
802
  )
793
803
 
794
- if factory_fn is None and auto_dict:
795
- v['type_name'] = type_name
796
- v.pop(JSONConvertible.TYPE_NAME_KEY)
804
+ if factory_fn is None and convert_unknown:
805
+ type_name = v[JSONConvertible.TYPE_NAME_KEY]
806
+ def _factory_fn(json_value: Dict[str, Any], **kwargs):
807
+ del kwargs
808
+ # See `pg.symbolic.UnknownObject` for details.
809
+ unknown_object_cls = JSONConvertible.class_from_typename(
810
+ 'unknown_object'
811
+ )
812
+ return unknown_object_cls(type_name=type_name, **json_value) # pytype: disable=wrong-keyword-args
813
+
814
+ v[JSONConvertible.TYPE_NAME_KEY] = _factory_fn
797
815
  return True
798
816
  assert factory_fn is not None
799
817
 
@@ -992,39 +1010,79 @@ def _load_symbol(type_name: str) -> Any:
992
1010
  return symbol
993
1011
 
994
1012
 
995
- def _type_from_json(json_value: Dict[str, str], **kwargs) -> Type[Any]:
1013
+ def _type_from_json(convert_unknown: bool) -> Callable[..., Any]:
996
1014
  """Loads a type from a JSON dict."""
997
- del kwargs
998
- t = _load_symbol(json_value['name'])
999
- if 'args' in json_value:
1000
- return _bind_type_args(
1001
- t, from_json(json_value['args'], _typename_resolved=True)
1002
- )
1003
- return t
1015
+ def _fn(json_value: Dict[str, str], **kwargs) -> Type[Any]:
1016
+ del kwargs
1017
+ try:
1018
+ t = _load_symbol(json_value['name'])
1019
+ if 'args' in json_value:
1020
+ return _bind_type_args(
1021
+ t, from_json(json_value['args'], _typename_resolved=True)
1022
+ )
1023
+ return t
1024
+ except (ModuleNotFoundError, AttributeError) as e:
1025
+ if not convert_unknown:
1026
+ raise TypeError(
1027
+ f'Cannot load type {json_value["name"]!r}.\n'
1028
+ 'Try pass `convert_unknown=True` to load the object '
1029
+ 'into `pg.UnknownType` without depending on the type.'
1030
+ ) from e
1031
+ # See `pg.symbolic.UnknownType` for details.
1032
+ json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_type'
1033
+ return from_json(json_value)
1034
+ return _fn
1004
1035
 
1005
1036
 
1006
1037
  def _function_from_json(
1007
- json_value: Dict[str, str], **kwargs) -> types.FunctionType:
1038
+ convert_unknown: bool
1039
+ ) -> Callable[..., types.FunctionType]:
1008
1040
  """Loads a function from a JSON dict."""
1009
- del kwargs
1010
- function_name = json_value['name']
1011
- if 'code' in json_value:
1012
- code = marshal.loads(
1013
- base64.decodebytes(json_value['code'].encode('utf-8')))
1014
- defaults = from_json(json_value['defaults'], _typename_resolved=True)
1015
- return types.FunctionType(
1016
- code=code,
1017
- globals=globals(),
1018
- argdefs=defaults,
1019
- )
1020
- else:
1021
- return _load_symbol(function_name)
1022
-
1023
-
1024
- def _method_from_json(json_value: Dict[str, str], **kwargs) -> types.MethodType:
1041
+ def _fn(json_value: Dict[str, str], **kwargs) -> types.FunctionType:
1042
+ del kwargs
1043
+ function_name = json_value['name']
1044
+ if 'code' in json_value:
1045
+ code = marshal.loads(
1046
+ base64.decodebytes(json_value['code'].encode('utf-8')))
1047
+ defaults = from_json(json_value['defaults'], _typename_resolved=True)
1048
+ return types.FunctionType(
1049
+ code=code,
1050
+ globals=globals(),
1051
+ argdefs=defaults,
1052
+ )
1053
+ else:
1054
+ try:
1055
+ return _load_symbol(function_name)
1056
+ except (ModuleNotFoundError, AttributeError) as e:
1057
+ if not convert_unknown:
1058
+ raise TypeError(
1059
+ f'Cannot load function {function_name!r}.\n'
1060
+ 'Try pass `convert_unknown=True` to load the object into '
1061
+ '`pg.UnknownFunction` without depending on the type.'
1062
+ ) from e
1063
+ json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_function'
1064
+ return from_json(json_value)
1065
+ return _fn
1066
+
1067
+
1068
+ def _method_from_json(
1069
+ convert_unknown: bool
1070
+ ) -> Callable[..., types.MethodType]:
1025
1071
  """Loads a class method from a JSON dict."""
1026
- del kwargs
1027
- return _load_symbol(json_value['name'])
1072
+ def _fn(json_value: Dict[str, str], **kwargs) -> types.MethodType:
1073
+ del kwargs
1074
+ try:
1075
+ return _load_symbol(json_value['name'])
1076
+ except (ModuleNotFoundError, AttributeError) as e:
1077
+ if not convert_unknown:
1078
+ raise TypeError(
1079
+ f'Cannot load method {json_value["name"]!r}.\n'
1080
+ 'Try pass `convert_unknown=True` to load the object '
1081
+ 'into `pg.UnknownMethod` without depending on the type.'
1082
+ ) from e
1083
+ json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_method'
1084
+ return from_json(json_value)
1085
+ return _fn
1028
1086
 
1029
1087
 
1030
1088
  def _bind_type_args(t, args):