schematics/schematics — pull request #266 by bintoro (2014-12-18 14:41:32+00:00)
Title: Create shared environment across nested import/validation loops via a metadata object

#### Summary

This patch introduces a metadata object that is propagated through every level of a recursive import/validation process. It is used to carry options and other data that need to be available throughout the entire process, not just during the outermost loop.

#### Background

One thing currently blocking the `Undefined` feature is the fact that import/validation parameters (importantly, `partial=True`) only affect the first level of processing. If you have nested models, your settings no longer apply.

Apparently someone has noticed this before and created this construct in `import_loop` to pass the deserialization mapping down to nested models:

```python
try:
mapping_by_model = mapping.get('model_mapping', {})
model_mapping = mapping_by_model.get(field_name, {})
raw_value = field_converter(field, raw_value, mapping=model_mapping)
except Exception:
raw_value = field_converter(field, raw_value)
```

And there was a similar `try—except` inside the `field_converter` in question.

#### The patch

Instead of adding yet another parameter here, I have future-proofed this by turning the third parameter into a multipurpose metadata object. Import converters and validators can now use the `meta` parameter to propagate whatever data they please. In addition to holding various options, a practical application might be bookkeeping during validation; currently it's possible to have a `ModelType` structure that gets in an infinite loop.

The above code now looks like this:

```python
model_mapping = meta.mapping.get('model_mapping', {}).get(field_name, {})
sub_meta = Container(meta) # Make a copy...
sub_meta.mapping = model_mapping # Put the relevant mapping in there...
raw_value = field_converter(field, raw_value, meta=sub_meta) # Pass it on
```

The way this works is any function (including the initial caller) could decide to set up the meta dictionary. Once it enters the processing chain, its contents override any overlapping parameters passed as keyword args. Right now, the first `import_loop` takes the `strict` and `partial` arguments it receives and locks them in for the rest of the import recursion.

As an aside, if all options were collated into a `meta` object from the start, the parameter signature of `import_loop`, `convert`, and friends could look like this:

```python
def ...(cls, instance_or_dict, meta)
```

While incorporating many options into a single object may be considered a bit opaque, it's clearly preferable to always passing a dozen values to every method and their aunts:

```python
model.validate(...)
=> validate(...)
=> import_loop(...)
=> field_converter(...)
=> field.validate(...)
=> submodel.validate(...)
```

That's five calls for each level of recursion.

#### BREAKING CHANGE WARNING

I've gotten rid of the `try–except` constructs that otherwise would have been all over the place.

Considering that the `meta` container is only needed in a recursive process, the method signature dilemma has been solved by requiring *compound types* (`MultiType` descendants) to accept the `meta` argument in their `to_native` and `validate_` methods. Simple types are called without `meta`.

Since custom compound types need a (trivial) change as a result, this would be a great opportunity also to get rid of the `context` parameter that appears in a ton of places. It would be better placed inside `meta` as well.

#### Other changes incorporated here

* `Model.__init__` now accepts the `partial` parameter too (default=`True` because it was the de facto default already, specified in the main `convert` func).

* Fixed a bug where `ListType.validate_items()` was added to the list of validators twice: once explicitly and once via collation by `TypeMeta`.