Skip to content

Commit

Permalink
Add initialization hook for session creation (#2682)
Browse files Browse the repository at this point in the history
This adds an initialization hook to customize any botocore sessions.

Botocore sessions provide context for creating one or more clients.
It allows you to provide configuration, components, hooks, etc. that
can be shared across all clients created within that session.

There is no similar concept for sessions.  Specifically, there is no way
to provide a common set of configuration, components, hooks, etc. that
can shared across all sessions created within botocore.

This change adds this initialization hook.  You can now register an
initialization callback that's invoked whenever a session is created:

```python
import botocore

def my_initializer(session: botocore.session.Session) -> None:
    session.register_component('data_loader', MyNewCrazyLoader())

botocore.register_initializer(my_initializer)
```

Now every session will automatically use this new loader:

```python

some_session = botocore.session.get_session()
assert isinstance(
    some_session.get_component('data_loader'), MyNewCrazyLoader
)
```
  • Loading branch information
jamesls authored Jun 17, 2022
1 parent 27894a3 commit f1a73ff
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 1 deletion.
40 changes: 40 additions & 0 deletions botocore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def emit(self, record):
log = logging.getLogger('botocore')
log.addHandler(NullHandler())

_INITIALIZERS = []

_first_cap_regex = re.compile('(.)([A-Z][a-z]+)')
_end_cap_regex = re.compile('([a-z0-9])([A-Z])')
Expand Down Expand Up @@ -97,3 +98,42 @@ def xform_name(name, sep='_', _xform_cache=_xform_cache):
transformed = _end_cap_regex.sub(r'\1' + sep + r'\2', s1).lower()
_xform_cache[key] = transformed
return _xform_cache[key]


def register_initializer(callback):
"""Register an initializer function for session creation.
This initializer function will be invoked whenever a new
`botocore.session.Session` is instantiated.
:type callback: callable
:param callback: A callable that accepts a single argument
of type `botocore.session.Session`.
"""
_INITIALIZERS.append(callback)


def unregister_initializer(callback):
"""Unregister an initializer function.
:type callback: callable
:param callback: A callable that was previously registered
with `botocore.register_initializer`.
:raises ValueError: If a callback is provided that is not currently
registered as an initializer.
"""
_INITIALIZERS.remove(callback)


def invoke_initializers(session):
"""Invoke all initializers for a session.
:type session: botocore.session.Session
:param session: The session to initialize.
"""
for initializer in _INITIALIZERS:
initializer(session)
2 changes: 2 additions & 0 deletions botocore/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
UNSIGNED,
__version__,
handlers,
invoke_initializers,
monitoring,
paginate,
retryhandler,
Expand Down Expand Up @@ -148,6 +149,7 @@ def __init__(
self.session_var_map = SessionVarDict(self, self.SESSION_VARIABLES)
if session_vars is not None:
self.session_var_map.update(session_vars)
invoke_initializers(self)

def _register_components(self):
self._register_credential_provider()
Expand Down
36 changes: 35 additions & 1 deletion tests/unit/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
import botocore.exceptions
import botocore.loaders
import botocore.session
from botocore import UNSIGNED, client
from botocore import (
UNSIGNED,
client,
register_initializer,
unregister_initializer,
)
from botocore.configprovider import ConfigChainFactory
from botocore.hooks import HierarchicalEmitter
from botocore.model import ServiceModel
Expand Down Expand Up @@ -993,3 +998,32 @@ def test_new_session_with_none_region(self):
s3_client = self.session.create_client('s3', region_name=None)
self.assertIsInstance(s3_client, client.BaseClient)
self.assertTrue(s3_client.meta.region_name is not None)


class TestInitializationHooks(BaseSessionTest):
def test_can_register_init_hook(self):
call_args = []

def init_hook(session):
call_args.append(session)

register_initializer(init_hook)
self.addCleanup(unregister_initializer, init_hook)
session = create_session()
self.assertEqual(call_args, [session])

def test_can_unregister_hook(self):
call_args = []

def init_hook(session):
call_args.append(session)

register_initializer(init_hook)
unregister_initializer(init_hook)
create_session()
self.assertEqual(call_args, [])

def test_unregister_hook_raises_value_error(self):
not_registered = lambda session: None
with self.assertRaises(ValueError):
self.assertRaises(unregister_initializer(not_registered))

0 comments on commit f1a73ff

Please sign in to comment.