Repository: opensistemas-hub/osbrain
Pull request #160 (2017-07-10 16:22:45)

Allow attributes to be set when running an agent
It is a common task, so this makes it easier for the end user.

In our walk-forward analysis system it also allows to take advantage of the COW on Unix systems and reduce memory usage (we set big read-only market data history dataframes from the parent process using this new parameter and we avoid complete copies on each backtest worker).

Peque: @Flood1993 I don't think that is necessary right now. Do you like the `init` name for the attribute? (that was the hardest part of this PR... :joy:)
Flood1993: It is hard to come up with a better name, to be honest :P. Should we sacrifice some extra characters and name it `attributes`? 

Just for comparison:
`a0 = run_agent('foo', init=dict(x=1, y=2))`
`a0 = run_agent('foo', attributes=dict(x=1, y=2))`
Peque: @Flood1993 Yeah, that is the problem, I also thought about `attributes`. Maybe it is a better name... I don't know. :sweat_smile:
codecov[bot]: # [Codecov](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=h1) Report
> Merging [#160](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=desc) into [master](https://codecov.io/gh/opensistemas-hub/osbrain/commit/0bb9de77a445ca3057942fb4ecf60e6d8b1b209e?src=pr&el=desc) will **decrease** coverage by `0.35%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160/graphs/tree.svg?token=LfvMoNOrOc&src=pr&height=150&width=650)](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #160      +/-   ##
==========================================
- Coverage   98.39%   98.04%   -0.36%     
==========================================
  Files          25       25              
  Lines        3049     3064      +15     
  Branches      232      235       +3     
==========================================
+ Hits         3000     3004       +4     
- Misses         37       47      +10     
- Partials       12       13       +1
```


| [Impacted Files](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [osbrain/tests/test\_agent.py](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree#diff-b3NicmFpbi90ZXN0cy90ZXN0X2FnZW50LnB5) | `97.32% <100%> (-2.68%)` | :arrow_down: |
| [osbrain/agent.py](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree#diff-b3NicmFpbi9hZ2VudC5weQ==) | `95.91% <100%> (+0.03%)` | :arrow_up: |
| [osbrain/tests/test\_logging.py](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree#diff-b3NicmFpbi90ZXN0cy90ZXN0X2xvZ2dpbmcucHk=) | `95.12% <0%> (-4.88%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=footer). Last update [0bb9de7...a8b6138](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=h1) Report
> Merging [#160](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=desc) into [master](https://codecov.io/gh/opensistemas-hub/osbrain/commit/0bb9de77a445ca3057942fb4ecf60e6d8b1b209e?src=pr&el=desc) will **decrease** coverage by `0.35%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160/graphs/tree.svg?width=650&src=pr&token=LfvMoNOrOc&height=150)](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #160      +/-   ##
==========================================
- Coverage   98.39%   98.04%   -0.36%     
==========================================
  Files          25       25              
  Lines        3049     3064      +15     
  Branches      232      235       +3     
==========================================
+ Hits         3000     3004       +4     
- Misses         37       47      +10     
- Partials       12       13       +1
```


| [Impacted Files](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [osbrain/tests/test\_agent.py](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree#diff-b3NicmFpbi90ZXN0cy90ZXN0X2FnZW50LnB5) | `97.32% <100%> (-2.68%)` | :arrow_down: |
| [osbrain/agent.py](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree#diff-b3NicmFpbi9hZ2VudC5weQ==) | `95.91% <100%> (+0.03%)` | :arrow_up: |
| [osbrain/tests/test\_logging.py](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=tree#diff-b3NicmFpbi90ZXN0cy90ZXN0X2xvZ2dpbmcucHk=) | `95.12% <0%> (-4.88%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=footer). Last update [0bb9de7...a8b6138](https://codecov.io/gh/opensistemas-hub/osbrain/pull/160?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Peque: @Flood1993 changed name to `attributes`. :wink:

Patch:
diff --git a/docs/source/considerations.rst b/docs/source/considerations.rst
index 419a32a..7597afb 100644
--- a/docs/source/considerations.rst
+++ b/docs/source/considerations.rst
@@ -108,3 +108,27 @@ however you may notice some differences:
    to. As the binding was executed on initialization, we need to use the
    ``addr()`` method, which will return the address associated to the alias
    passed as parameter (in the example above it is ``main``).
+
+
+Setting initial attributes
+==========================
+
+Many times, after spawning an agent, we want to set some attributes, which
+may be used to configure the agent before it starts working with the rest of
+the multi-agent system:
+
+.. code-block:: python
+
+   a0 = run_agent('foo')
+   a0.set_attr(x=1, y=2)
+
+It is such a common task that a parameter ``attributes`` can be used when
+running the agent for exactly that:
+
+.. code-block:: python
+
+   a0 = run_agent('foo', attributes=dict(x=1, y=2))
+
+As you can see, this parameter accepts a dictionary in which the keys are the
+name of the attributes to be set in the agent and the values are the actual
+values that this attributes will take.
diff --git a/osbrain/agent.py b/osbrain/agent.py
index 1f2007c..b1c189b 100644
--- a/osbrain/agent.py
+++ b/osbrain/agent.py
@@ -170,6 +170,8 @@ class Agent():
         (localhost) is used.
     transport : str, AgentAddressTransport, default is None
         Transport protocol.
+    attributes : dict, default is None
+        A dictionary that defines initial attributes for the agent.
 
     Attributes
     ----------
@@ -195,7 +197,8 @@ class Agent():
     running : bool
         Set to `True` if the agent is running (executing the main loop).
     """
-    def __init__(self, name=None, host=None, serializer=None, transport=None):
+    def __init__(self, name=None, host=None, serializer=None, transport=None,
+                 attributes=None):
         self.uuid = unique_identifier()
         self.name = name
         self.host = host
@@ -230,6 +233,12 @@ class Agent():
                   handler=self._handle_loopback_safe, transport='inproc',
                   serializer='pickle')
 
+        if attributes:
+            for key, value in attributes.items():
+                if hasattr(self, key):
+                    raise KeyError('Agent already has "%s" attribute!' % key)
+                setattr(self, key, value)
+
         self.on_init()
 
     def on_init(self):
@@ -1523,7 +1532,7 @@ class AgentProcess(multiprocessing.Process):
     can be run independently.
     """
     def __init__(self, name, nsaddr=None, addr=None, serializer=None,
-                 transport=None, base=Agent):
+                 transport=None, base=Agent, attributes=None):
         super().__init__()
         self.name = name
         self._daemon = None
@@ -1537,6 +1546,7 @@ class AgentProcess(multiprocessing.Process):
         self.shutdown_event = multiprocessing.Event()
         self.queue = multiprocessing.Queue()
         self.sigint = False
+        self.attributes = attributes
 
     def run(self):
         # Capture SIGINT
@@ -1545,14 +1555,16 @@ class AgentProcess(multiprocessing.Process):
         try:
             ns = NSProxy(self.nsaddr)
             self._daemon = Pyro4.Daemon(self.host, self.port)
+            self.agent = self.base(name=self.name, host=self.host,
+                                   serializer=self.serializer,
+                                   transport=self.transport,
+                                   attributes=self.attributes)
         except Exception:
             self.queue.put(format_exception())
             return
+
         self.queue.put('STARTED')
 
-        self.agent = self.base(name=self.name, host=self.host,
-                               serializer=self.serializer,
-                               transport=self.transport)
         uri = self._daemon.register(self.agent)
         ns.register(self.name, uri)
         ns.release()
@@ -1599,7 +1611,7 @@ class AgentProcess(multiprocessing.Process):
 
 
 def run_agent(name, nsaddr=None, addr=None, base=Agent, serializer=None,
-              transport=None, safe=None):
+              transport=None, safe=None, attributes=None):
     """
     Ease the agent creation process.
 
@@ -1618,6 +1630,8 @@ def run_agent(name, nsaddr=None, addr=None, base=Agent, serializer=None,
         Transport protocol.
     safe : bool, default is None
         Use safe calls by default from the Proxy.
+    attributes : dict, default is None
+        A dictionary that defines initial attributes for the agent.
 
     Returns
     -------
@@ -1627,7 +1641,8 @@ def run_agent(name, nsaddr=None, addr=None, base=Agent, serializer=None,
     if not nsaddr:
         nsaddr = os.environ.get('OSBRAIN_NAMESERVER_ADDRESS')
     AgentProcess(name, nsaddr=nsaddr, addr=addr, base=base,
-                 serializer=serializer, transport=transport).start()
+                 serializer=serializer, transport=transport,
+                 attributes=attributes).start()
     proxy = Proxy(name, nsaddr, safe=safe)
     proxy.run()
     while not proxy.get_attr('running'):


Test patch:
diff --git a/osbrain/tests/test_agent.py b/osbrain/tests/test_agent.py
index cd89f66..e285763 100644
--- a/osbrain/tests/test_agent.py
+++ b/osbrain/tests/test_agent.py
@@ -102,6 +102,28 @@ def test_agent_shutdown(nsproxy):
     assert 'a0' not in nsproxy.list()
 
 
+def test_run_agent_initial_attributes(nsproxy):
+    """
+    The user can specify a set of attributes to be set in the agent when
+    creating it.
+    """
+    a0 = run_agent('a0', attributes=dict(x=1, y='a'))
+    assert a0.get_attr('x') == 1
+    assert a0.get_attr('y') == 'a'
+
+
+def test_run_agent_initial_attributes_exception(nsproxy):
+    """
+    The user can specify a set of attributes to be set in the agent when
+    creating it. If the attribute specified overrites an existing attribute,
+    an exception will be raised.
+    """
+    with pytest.raises(RuntimeError) as error:
+        run_agent('a0', attributes=dict(name='foo'))
+    assert 'KeyError' in str(error.value)
+    assert 'Agent already has "name" attribute!' in str(error.value)
+
+
 def test_set_method(nsproxy):
     """
     Set new methods for the agent.