prefect-client 2.16.8__py3-none-any.whl → 2.17.0__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 (89) hide show
  1. prefect/__init__.py +0 -18
  2. prefect/_internal/compatibility/deprecated.py +108 -5
  3. prefect/_internal/compatibility/experimental.py +9 -8
  4. prefect/_internal/concurrency/api.py +23 -42
  5. prefect/_internal/concurrency/waiters.py +25 -22
  6. prefect/_internal/pydantic/__init__.py +16 -3
  7. prefect/_internal/pydantic/_base_model.py +39 -4
  8. prefect/_internal/pydantic/_compat.py +69 -452
  9. prefect/_internal/pydantic/_flags.py +5 -0
  10. prefect/_internal/pydantic/_types.py +8 -0
  11. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  12. prefect/_internal/pydantic/utilities/config_dict.py +72 -0
  13. prefect/_internal/pydantic/utilities/field_validator.py +135 -0
  14. prefect/_internal/pydantic/utilities/model_construct.py +56 -0
  15. prefect/_internal/pydantic/utilities/model_copy.py +55 -0
  16. prefect/_internal/pydantic/utilities/model_dump.py +136 -0
  17. prefect/_internal/pydantic/utilities/model_dump_json.py +112 -0
  18. prefect/_internal/pydantic/utilities/model_fields.py +50 -0
  19. prefect/_internal/pydantic/utilities/model_fields_set.py +29 -0
  20. prefect/_internal/pydantic/utilities/model_json_schema.py +82 -0
  21. prefect/_internal/pydantic/utilities/model_rebuild.py +80 -0
  22. prefect/_internal/pydantic/utilities/model_validate.py +75 -0
  23. prefect/_internal/pydantic/utilities/model_validate_json.py +68 -0
  24. prefect/_internal/pydantic/utilities/model_validator.py +79 -0
  25. prefect/_internal/pydantic/utilities/type_adapter.py +71 -0
  26. prefect/_internal/schemas/bases.py +1 -17
  27. prefect/_internal/schemas/validators.py +425 -4
  28. prefect/agent.py +1 -1
  29. prefect/blocks/kubernetes.py +7 -3
  30. prefect/blocks/notifications.py +18 -18
  31. prefect/blocks/webhook.py +1 -1
  32. prefect/client/base.py +7 -0
  33. prefect/client/cloud.py +1 -1
  34. prefect/client/orchestration.py +51 -11
  35. prefect/client/schemas/actions.py +367 -297
  36. prefect/client/schemas/filters.py +28 -28
  37. prefect/client/schemas/objects.py +78 -147
  38. prefect/client/schemas/responses.py +240 -60
  39. prefect/client/schemas/schedules.py +6 -8
  40. prefect/concurrency/events.py +2 -2
  41. prefect/context.py +4 -2
  42. prefect/deployments/base.py +6 -13
  43. prefect/deployments/deployments.py +34 -9
  44. prefect/deployments/runner.py +9 -27
  45. prefect/deprecated/packaging/base.py +5 -6
  46. prefect/deprecated/packaging/docker.py +19 -25
  47. prefect/deprecated/packaging/file.py +10 -5
  48. prefect/deprecated/packaging/orion.py +9 -4
  49. prefect/deprecated/packaging/serializers.py +8 -58
  50. prefect/engine.py +55 -618
  51. prefect/events/actions.py +16 -1
  52. prefect/events/clients.py +45 -13
  53. prefect/events/filters.py +19 -2
  54. prefect/events/related.py +4 -4
  55. prefect/events/schemas/automations.py +13 -2
  56. prefect/events/schemas/deployment_triggers.py +73 -5
  57. prefect/events/schemas/events.py +1 -1
  58. prefect/events/utilities.py +12 -4
  59. prefect/events/worker.py +26 -8
  60. prefect/exceptions.py +3 -8
  61. prefect/filesystems.py +7 -7
  62. prefect/flows.py +7 -3
  63. prefect/infrastructure/provisioners/ecs.py +1 -0
  64. prefect/logging/configuration.py +2 -2
  65. prefect/manifests.py +1 -8
  66. prefect/profiles.toml +1 -1
  67. prefect/pydantic/__init__.py +74 -2
  68. prefect/pydantic/main.py +26 -2
  69. prefect/serializers.py +6 -31
  70. prefect/settings.py +72 -26
  71. prefect/software/python.py +3 -5
  72. prefect/task_server.py +2 -2
  73. prefect/utilities/callables.py +1 -1
  74. prefect/utilities/collections.py +2 -1
  75. prefect/utilities/dispatch.py +1 -0
  76. prefect/utilities/engine.py +629 -0
  77. prefect/utilities/pydantic.py +1 -1
  78. prefect/utilities/schema_tools/validation.py +2 -2
  79. prefect/utilities/visualization.py +1 -1
  80. prefect/variables.py +88 -12
  81. prefect/workers/base.py +20 -11
  82. prefect/workers/block.py +4 -8
  83. prefect/workers/process.py +2 -5
  84. {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/METADATA +4 -3
  85. {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/RECORD +88 -72
  86. prefect/_internal/schemas/transformations.py +0 -106
  87. {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/LICENSE +0 -0
  88. {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/WHEEL +0 -0
  89. {prefect_client-2.16.8.dist-info → prefect_client-2.17.0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ import string
6
6
  import warnings
7
7
  from functools import partial
8
8
  from pathlib import Path
9
- from typing import Optional
9
+ from typing import Any, Dict, Optional
10
10
 
11
11
  import yaml
12
12
 
@@ -21,7 +21,7 @@ from prefect.utilities.collections import dict_to_flatdict, flatdict_to_dict
21
21
  DEFAULT_LOGGING_SETTINGS_PATH = Path(__file__).parent / "logging.yml"
22
22
 
23
23
  # Stores the configuration used to setup logging in this Python process
24
- PROCESS_LOGGING_CONFIG: dict = None
24
+ PROCESS_LOGGING_CONFIG: Optional[Dict[str, Any]] = None
25
25
 
26
26
  # Regex call to replace non-alphanumeric characters to '_' to create a valid env var
27
27
  to_envvar = partial(re.sub, re.compile(r"[^0-9a-zA-Z]+"), "_")
prefect/manifests.py CHANGED
@@ -4,14 +4,7 @@ Manifests are portable descriptions of one or more workflows within a given dire
4
4
  They are the foundational building blocks for defining Flow Deployments.
5
5
  """
6
6
 
7
-
8
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
9
-
10
- if HAS_PYDANTIC_V2:
11
- from pydantic.v1 import BaseModel, Field
12
- else:
13
- from pydantic import BaseModel, Field
14
-
7
+ from prefect.pydantic import BaseModel, Field
15
8
  from prefect.utilities.callables import ParameterSchema
16
9
 
17
10
 
prefect/profiles.toml CHANGED
@@ -1,3 +1,3 @@
1
1
  active = "default"
2
2
 
3
- [profiles.default]
3
+ [profiles.default]
@@ -1,4 +1,76 @@
1
- from .main import BaseModel, PrefectBaseModel
1
+ """
2
+ This initialization file makes the `BaseModel` and `PrefectBaseModel` classes available for import from the pydantic module within Prefect. This setup allows other parts of the Prefect codebase to use these models without needing to understand the underlying compatibility layer.
3
+ """
4
+ import typing
5
+ from prefect._internal.pydantic._flags import HAS_PYDANTIC_V2, USE_PYDANTIC_V2
2
6
 
7
+ if typing.TYPE_CHECKING:
8
+ # import of virtually everything is supported via `__getattr__` below,
9
+ # but we need them here for type checking and IDE support
10
+ from pydantic import validator, root_validator
11
+ from .main import (
12
+ BaseModel,
13
+ PrefectBaseModel,
14
+ FieldInfo,
15
+ Field,
16
+ PrivateAttr,
17
+ SecretStr,
18
+ field_validator,
19
+ model_validator,
20
+ ConfigDict,
21
+ ValidationError,
22
+ )
3
23
 
4
- __all__ = ["BaseModel", "PrefectBaseModel"]
24
+ __all__ = [
25
+ "BaseModel",
26
+ "PrefectBaseModel",
27
+ "Field",
28
+ "FieldInfo",
29
+ "PrivateAttr",
30
+ "SecretStr",
31
+ "validator",
32
+ "root_validator",
33
+ "field_validator",
34
+ "model_validator",
35
+ "ConfigDict",
36
+ "ValidationError",
37
+ ]
38
+
39
+ _dynamic_imports: "typing.Dict[str, typing.Tuple[str, str]]" = {
40
+ "BaseModel": ("prefect.pydantic", ".main"),
41
+ "PrefectBaseModel": ("prefect.pydantic", ".main"),
42
+ "Field": ("prefect.pydantic", ".main"),
43
+ "FieldInfo": ("prefect.pydantic", ".main"),
44
+ "PrivateAttr": ("prefect.pydantic", ".main"),
45
+ "SecretStr": ("prefect.pydantic", ".main"),
46
+ "field_validator": ("prefect.pydantic", ".main"),
47
+ "model_validator": ("prefect.pydantic", ".main"),
48
+ "ConfigDict": ("prefect.pydantic", ".main"),
49
+ "ValidationError": ("prefect.pydantic", ".main"),
50
+ }
51
+
52
+
53
+ def __getattr__(attr_name: str) -> object:
54
+ from importlib import import_module
55
+
56
+ if attr_name in _dynamic_imports:
57
+ # If the attribute is in the dynamic imports, import it from the specified module
58
+ package, module_name = _dynamic_imports[attr_name]
59
+
60
+ # Prevent recursive import
61
+ if module_name == "__module__":
62
+ return import_module(f".{attr_name}", package=package)
63
+
64
+ # Import the module and return the attribute
65
+ else:
66
+ module = import_module(module_name, package=package)
67
+ return getattr(module, attr_name)
68
+
69
+ elif HAS_PYDANTIC_V2 and not USE_PYDANTIC_V2:
70
+ # In this case, we are using Pydantic v2 but it is not enabled, so we should import from pydantic.v1
71
+ module = import_module("pydantic.v1")
72
+ return getattr(module, attr_name)
73
+ else:
74
+ # In this case, we are using either Pydantic v1 or Pydantic v2 is enabled, so we should import from pydantic
75
+ module = import_module("pydantic")
76
+ return getattr(module, attr_name)
prefect/pydantic/main.py CHANGED
@@ -1,6 +1,19 @@
1
+ """
2
+ This file defines a `PrefectBaseModel` class that extends the `BaseModel` (imported from the internal compatibility layer).
3
+ """
1
4
  import typing
2
5
 
3
- from prefect._internal.pydantic._compat import BaseModel
6
+ from prefect._internal.pydantic._compat import (
7
+ BaseModel,
8
+ ConfigDict,
9
+ Field,
10
+ FieldInfo,
11
+ PrivateAttr,
12
+ SecretStr,
13
+ ValidationError,
14
+ field_validator,
15
+ model_validator,
16
+ )
4
17
 
5
18
 
6
19
  class PrefectBaseModel(BaseModel):
@@ -12,4 +25,15 @@ class PrefectBaseModel(BaseModel):
12
25
  return set()
13
26
 
14
27
 
15
- __all__ = ["BaseModel", "PrefectBaseModel"]
28
+ __all__ = [
29
+ "BaseModel",
30
+ "PrefectBaseModel",
31
+ "Field",
32
+ "FieldInfo",
33
+ "PrivateAttr",
34
+ "SecretStr",
35
+ "field_validator",
36
+ "model_validator",
37
+ "ConfigDict",
38
+ "ValidationError",
39
+ ]
prefect/serializers.py CHANGED
@@ -10,10 +10,10 @@ the instance so the same settings can be used to load saved objects.
10
10
  All serializers must implement `dumps` and `loads` which convert objects to bytes and
11
11
  bytes to an object respectively.
12
12
  """
13
+
13
14
  import abc
14
15
  import base64
15
- import warnings
16
- from typing import Any, Generic, Optional, TypeVar
16
+ from typing import Any, Dict, Generic, Optional, TypeVar
17
17
 
18
18
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
19
19
  from prefect._internal.schemas.validators import (
@@ -22,6 +22,7 @@ from prefect._internal.schemas.validators import (
22
22
  validate_dump_kwargs,
23
23
  validate_load_kwargs,
24
24
  validate_picklelib,
25
+ validate_picklelib_version,
25
26
  )
26
27
 
27
28
  if HAS_PYDANTIC_V2:
@@ -112,33 +113,7 @@ class PickleSerializer(Serializer):
112
113
 
113
114
  @pydantic.root_validator
114
115
  def check_picklelib_version(cls, values):
115
- """
116
- Infers a default value for `picklelib_version` if null or ensures it matches
117
- the version retrieved from the `pickelib`.
118
- """
119
- picklelib = values.get("picklelib")
120
- picklelib_version = values.get("picklelib_version")
121
-
122
- if not picklelib:
123
- raise ValueError("Unable to check version of unrecognized picklelib module")
124
-
125
- pickler = from_qualified_name(picklelib)
126
- pickler_version = getattr(pickler, "__version__", None)
127
-
128
- if not picklelib_version:
129
- values["picklelib_version"] = pickler_version
130
- elif picklelib_version != pickler_version:
131
- warnings.warn(
132
- (
133
- f"Mismatched {picklelib!r} versions. Found {pickler_version} in the"
134
- f" environment but {picklelib_version} was requested. This may"
135
- " cause the serializer to fail."
136
- ),
137
- RuntimeWarning,
138
- stacklevel=3,
139
- )
140
-
141
- return values
116
+ return validate_picklelib_version(values)
142
117
 
143
118
  def dumps(self, obj: Any) -> bytes:
144
119
  pickler = from_qualified_name(self.picklelib)
@@ -178,8 +153,8 @@ class JSONSerializer(Serializer):
178
153
  "by our default `object_encoder`."
179
154
  ),
180
155
  )
181
- dumps_kwargs: dict = pydantic.Field(default_factory=dict)
182
- loads_kwargs: dict = pydantic.Field(default_factory=dict)
156
+ dumps_kwargs: Dict[str, Any] = pydantic.Field(default_factory=dict)
157
+ loads_kwargs: Dict[str, Any] = pydantic.Field(default_factory=dict)
183
158
 
184
159
  @pydantic.validator("dumps_kwargs")
185
160
  def dumps_kwargs_cannot_contain_default(cls, value):
prefect/settings.py CHANGED
@@ -68,6 +68,7 @@ from urllib.parse import urlparse
68
68
  import toml
69
69
 
70
70
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
71
+ from prefect._internal.schemas.validators import validate_settings
71
72
 
72
73
  if HAS_PYDANTIC_V2:
73
74
  from pydantic.v1 import (
@@ -106,6 +107,8 @@ DEFAULT_PROFILES_PATH = Path(__file__).parent.joinpath("profiles.toml")
106
107
  REMOVED_EXPERIMENTAL_FLAGS = {
107
108
  "PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_SCHEDULING_UI",
108
109
  "PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS",
110
+ "PREFECT_EXPERIMENTAL_ENABLE_EVENTS_CLIENT",
111
+ "PREFECT_EXPERIMENTAL_WARN_EVENTS_CLIENT",
109
112
  }
110
113
 
111
114
 
@@ -577,6 +580,14 @@ PREFECT_UNIT_TEST_MODE = Setting(
577
580
  This variable only exists to facilitate unit testing. If `True`,
578
581
  code is executing in a unit test context. Defaults to `False`.
579
582
  """
583
+ PREFECT_UNIT_TEST_LOOP_DEBUG = Setting(
584
+ bool,
585
+ default=True,
586
+ )
587
+ """
588
+ If `True` turns on debug mode for the unit testing event loop.
589
+ Defaults to `False`.
590
+ """
580
591
 
581
592
  PREFECT_TEST_SETTING = Setting(
582
593
  Any,
@@ -1358,16 +1369,6 @@ PREFECT_EXPERIMENTAL_ENABLE_STATES_ON_FLOW_RUN_GRAPH = Setting(bool, default=Tru
1358
1369
  Whether or not to enable flow run states on the flow run graph.
1359
1370
  """
1360
1371
 
1361
- PREFECT_EXPERIMENTAL_ENABLE_EVENTS_CLIENT = Setting(bool, default=True)
1362
- """
1363
- Whether or not to enable experimental Prefect work pools.
1364
- """
1365
-
1366
- PREFECT_EXPERIMENTAL_WARN_EVENTS_CLIENT = Setting(bool, default=False)
1367
- """
1368
- Whether or not to warn when experimental Prefect work pools are used.
1369
- """
1370
-
1371
1372
  PREFECT_EXPERIMENTAL_ENABLE_WORK_POOLS = Setting(bool, default=True)
1372
1373
  """
1373
1374
  Whether or not to enable experimental Prefect work pools.
@@ -1629,8 +1630,26 @@ The directory to serve static files from. This should be used when running into
1629
1630
  when attempting to serve the UI from the default directory (for example when running in a Docker container)
1630
1631
  """
1631
1632
 
1633
+ # Messaging system settings
1634
+
1635
+ PREFECT_MESSAGING_BROKER = Setting(
1636
+ str, default="prefect.server.utilities.messaging.memory"
1637
+ )
1638
+ """
1639
+ Which message broker implementation to use for the messaging system, should point to a
1640
+ module that exports a Publisher and Consumer class.
1641
+ """
1642
+
1643
+ PREFECT_MESSAGING_CACHE = Setting(
1644
+ str, default="prefect.server.utilities.messaging.memory"
1645
+ )
1646
+ """
1647
+ Which cache implementation to use for the events system. Should point to a module that
1648
+ exports a Cache class.
1649
+ """
1632
1650
 
1633
- # Events settings ------------------------------------------------------------------
1651
+
1652
+ # Events settings
1634
1653
 
1635
1654
  PREFECT_EVENTS_MAXIMUM_LABELS_PER_RESOURCE = Setting(int, default=500)
1636
1655
  """
@@ -1642,13 +1661,53 @@ PREFECT_EVENTS_MAXIMUM_RELATED_RESOURCES = Setting(int, default=500)
1642
1661
  The maximum number of related resources an Event may have.
1643
1662
  """
1644
1663
 
1664
+ PREFECT_EVENTS_MAXIMUM_SIZE_BYTES = Setting(int, default=1_500_000)
1665
+ """
1666
+ The maximum size of an Event when serialized to JSON
1667
+ """
1668
+
1669
+ PREFECT_API_SERVICES_EVENT_LOGGER_ENABLED = Setting(bool, default=True)
1670
+ """
1671
+ Whether or not to start the event debug logger service in the server application.
1672
+ """
1673
+
1674
+ PREFECT_API_SERVICES_TRIGGERS_ENABLED = Setting(bool, default=True)
1675
+ """
1676
+ Whether or not to start the triggers service in the server application.
1677
+ """
1678
+
1679
+ PREFECT_EVENTS_EXPIRED_BUCKET_BUFFER = Setting(timedelta, default=timedelta(seconds=60))
1680
+ """
1681
+ The amount of time to retain expired automation buckets
1682
+ """
1683
+
1684
+ PREFECT_EVENTS_PROACTIVE_GRANULARITY = Setting(timedelta, default=timedelta(seconds=5))
1685
+ """
1686
+ How frequently proactive automations are evaluated
1687
+ """
1688
+
1689
+ PREFECT_API_SERVICES_EVENT_PERSISTER_ENABLED = Setting(bool, default=True)
1690
+ """
1691
+ Whether or not to start the event persister service in the server application.
1692
+ """
1693
+
1694
+ PREFECT_API_SERVICES_EVENT_PERSISTER_BATCH_SIZE = Setting(int, default=20, gt=0)
1695
+ """
1696
+ The number of events the event persister will attempt to insert in one batch.
1697
+ """
1698
+
1699
+ PREFECT_API_SERVICES_EVENT_PERSISTER_FLUSH_INTERVAL = Setting(float, default=5, gt=0.0)
1700
+ """
1701
+ The maximum number of seconds between flushes of the event persister.
1702
+ """
1703
+
1645
1704
 
1646
1705
  # Deprecated settings ------------------------------------------------------------------
1647
1706
 
1648
1707
 
1649
1708
  # Collect all defined settings ---------------------------------------------------------
1650
1709
 
1651
- SETTING_VARIABLES = {
1710
+ SETTING_VARIABLES: Dict[str, Any] = {
1652
1711
  name: val for name, val in tuple(globals().items()) if isinstance(val, Setting)
1653
1712
  }
1654
1713
 
@@ -1947,20 +2006,7 @@ class Profile(BaseModel):
1947
2006
 
1948
2007
  @validator("settings", pre=True)
1949
2008
  def map_names_to_settings(cls, value):
1950
- if value is None:
1951
- return value
1952
-
1953
- # Cast string setting names to variables
1954
- validated = {}
1955
- for setting, val in value.items():
1956
- if isinstance(setting, str) and setting in SETTING_VARIABLES:
1957
- validated[SETTING_VARIABLES[setting]] = val
1958
- elif isinstance(setting, Setting):
1959
- validated[setting] = val
1960
- else:
1961
- raise ValueError(f"Unknown setting {setting!r}.")
1962
-
1963
- return validated
2009
+ return validate_settings(value)
1964
2010
 
1965
2011
  def validate_settings(self) -> None:
1966
2012
  """
@@ -1,8 +1,8 @@
1
- import sys
2
1
  from pathlib import Path
3
2
  from typing import List, Type
4
3
 
5
4
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
5
+ from prefect._internal.schemas.validators import infer_python_version
6
6
 
7
7
  if HAS_PYDANTIC_V2:
8
8
  from pydantic.v1 import BaseModel, Field, validate_arguments, validator
@@ -23,10 +23,8 @@ class PythonEnvironment(BaseModel):
23
23
  pip_requirements: List[PipRequirement] = Field(default_factory=list)
24
24
 
25
25
  @validator("python_version", pre=True, always=True)
26
- def infer_python_version(cls, value):
27
- if value is None:
28
- return f"{sys.version_info.major}.{sys.version_info.minor}"
29
- return value
26
+ def validate_python_version(cls, value):
27
+ return infer_python_version(value)
30
28
 
31
29
  @classmethod
32
30
  def from_environment(cls: Type[Self], exclude_nested: bool = False) -> Self:
prefect/task_server.py CHANGED
@@ -53,8 +53,8 @@ def should_try_to_read_parameters(task: Task, task_run: TaskRun) -> bool:
53
53
 
54
54
 
55
55
  class TaskServer:
56
- """This class is responsible for serving tasks that may be executed autonomously by a
57
- task runner in the engine.
56
+ """This class is responsible for serving tasks that may be executed in the background
57
+ by a task runner via the traditional engine machinery.
58
58
 
59
59
  When `start()` is called, the task server will open a websocket connection to a
60
60
  server-side queue of scheduled task runs. When a scheduled task run is found, the
@@ -225,7 +225,7 @@ class ParameterSchema(pydantic.BaseModel):
225
225
  type: Literal["object"] = "object"
226
226
  properties: Dict[str, Any] = pydantic.Field(default_factory=dict)
227
227
  required: List[str] = None
228
- definitions: Dict[str, Any] = None
228
+ definitions: Optional[Dict[str, Any]] = None
229
229
 
230
230
  def dict(self, *args, **kwargs):
231
231
  """Exclude `None` fields by default to comply with
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Utilities for extensions of and operations on Python collections.
3
3
  """
4
+
4
5
  import io
5
6
  import itertools
6
7
  from collections import OrderedDict, defaultdict
@@ -158,7 +159,7 @@ def ensure_iterable(obj: Union[T, Iterable[T]]) -> Iterable[T]:
158
159
  return [obj]
159
160
 
160
161
 
161
- def listrepr(objs: Iterable, sep=" ") -> str:
162
+ def listrepr(objs: Iterable[Any], sep: str = " ") -> str:
162
163
  return sep.join(repr(obj) for obj in objs)
163
164
 
164
165
 
@@ -19,6 +19,7 @@ key = get_dispatch_key(Foo) # 'foo'
19
19
  lookup_type(Base, key) # Foo
20
20
  ```
21
21
  """
22
+
22
23
  import abc
23
24
  import inspect
24
25
  import warnings