Repository: OpenNMT/OpenNMT-tf

Issue:
AttributeError: 'str' object has no attribute 'update'
```
Traceback (most recent call last):
  File "/home/soul/anaconda2/lib/python2.7/runpy.py", line 174, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/home/soul/anaconda2/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/home/soul/projects/opennmt-tf/OpenNMT-tf/bin/main.py", line 275, in <module>
    main()
  File "/home/soul/projects/opennmt-tf/OpenNMT-tf/bin/main.py", line 225, in main
    config = load_config(args.config)
  File "opennmt/config.py", line 48, in load_config
    config[section].update(subconfig[section])
AttributeError: 'str' object has no attribute 'update'
```
The attribute that caused it was "model_dir", where its value was a string.

The config file that I used:
```
# The directory where models and summaries will be saved. It is created if it does not exist.
model_dir: enfr

data:
  train_features_file: data/enfr/src-train.txt
  train_labels_file: data/enfr/tgt-train.txt
  eval_features_file: data/enfr/src-val.txt
  eval_labels_file: data/enfr/tgt-val.txt

  # (optional) Models may require additional resource files (e.g. vocabularies).
  source_words_vocabulary: data/enfr/src-vocab.txt
  target_words_vocabulary: data/enfr/tgt-vocab.txt

# Model and optimization parameters.
params:
  # The optimizer class name in tf.train or tf.contrib.opt.
  optimizer: AdamOptimizer
  learning_rate: 0.1

  # (optional) Maximum gradients norm (default: None).
  clip_gradients: 5.0
  # (optional) The type of learning rate decay (default: None). See:
  #  * https://www.tensorflow.org/versions/master/api_guides/python/train#Decaying_the_learning_rate
  #  * opennmt/utils/decay.py
  # This value may change the semantics of other decay options. See the documentation or the code.
  decay_type: exponential_decay
  # (optional unless decay_type is set) The learning rate decay rate.
  decay_rate: 0.9
  # (optional unless decay_type is set) Decay every this many steps.
  decay_steps: 10000
  # (optional) If true, the learning rate is decayed in a staircase fashion (default: True).
  staircase: true
  # (optional) After how many steps to start the decay (default: 0).
  start_decay_steps: 50000
  # (optional) Stop decay when this learning rate value is reached (default: 0).
  minimum_learning_rate: 0.0001
  # (optional) Width of the beam search (default: 1).
  beam_width: 5
  # (optional) Length penaly weight to apply on hypotheses (default: 0).
  length_penalty: 0.2
  # (optional) Maximum decoding iterations before stopping (default: 250).
  maximum_iterations: 200

# Training options.
train:
  batch_size: 64

  # (optional) Save a checkpoint every this many steps.
  save_checkpoints_steps: 5000
  # (optional) How many checkpoints to keep on disk.
  keep_checkpoint_max: 3
  # (optional) Save summaries every this many steps.
  save_summary_steps: 100
  # (optional) Train for this many steps. If not set, train forever.
  train_steps: 1000000
  # (optional) Evaluate every this many seconds (default: 3600).
  eval_delay: 7200
  # (optional) Save evaluation predictions in model_dir/eval/.
  save_eval_predictions: false
  # (optional) The maximum length of feature sequences during training (default: None).
  maximum_features_length: 70
  # (optional) The maximum length of label sequences during training (default: None).
  maximum_labels_length: 70
  # (optional) The number of buckets by sequence length to improve training efficiency (default: 5).
  num_buckets: 5
  # (optional) The number of threads to use for processing data in parallel (default: number of logical cores).
  num_parallel_process_calls: 4
  # (optional) The data pre-fetch buffer size, e.g. for shuffling examples (default: batch_size * 1000).
  buffer_size: 10000

# (optional) Inference options.
infer:
  # (optional) The batch size to use (default: 1).
  batch_size: 10
  # (optional) The number of threads to use for processing data in parallel (default: number of logical cores).
  num_parallel_process_calls: 8
  # (optional) The data pre-fetch buffer size when processing data in parallel (default: batch_size * 10).
  buffer_size: 100
  # (optional) For compatible models, the number of hypotheses to output (default: 1).
  n_best: 1
```


Discussion:

Resolving patch:
diff --git a/opennmt/config.py b/opennmt/config.py
--- a/opennmt/config.py
+++ b/opennmt/config.py
@@ -43,7 +43,10 @@ def load_config(config_paths, config=None):
       # Add or update section in main configuration.
       for section in subconfig:
         if section in config:
-          config[section].update(subconfig[section])
+          if isinstance(config[section], dict):
+            config[section].update(subconfig[section])
+          else:
+            config[section] = subconfig[section]
         else:
           config[section] = subconfig[section]
 


Test patch:
diff --git a/opennmt/tests/config_test.py b/opennmt/tests/config_test.py
new file mode 100644
--- /dev/null
+++ b/opennmt/tests/config_test.py
@@ -0,0 +1,38 @@
+import os
+import yaml
+
+import tensorflow as tf
+
+from opennmt import config
+
+config_file_1 = "config_test_1.tmp"
+config_file_2 = "config_test_2.tmp"
+
+
+class ConfigTest(tf.test.TestCase):
+
+  def tearDown(self):
+    if os.path.isfile(config_file_1):
+      os.remove(config_file_1)
+    if os.path.isfile(config_file_2):
+      os.remove(config_file_2)
+
+
+  def testConfigOverride(self):
+    config1 = {"model_dir": "foo", "train": {"batch_size": 32, "steps": 42}}
+    config2 = {"model_dir": "bar", "train": {"batch_size": 64}}
+
+    with open(config_file_1, "w") as config_file:
+      config_file.write(yaml.dump(config1))
+    with open(config_file_2, "w") as config_file:
+      config_file.write(yaml.dump(config2))
+
+    loaded_config = config.load_config([config_file_1, config_file_2])
+
+    self.assertDictEqual(
+        {"model_dir": "bar", "train": {"batch_size": 64, "steps": 42}},
+        loaded_config)
+
+
+if __name__ == "__main__":
+  tf.test.main()