├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── README.txt ├── camel ├── __init__.py └── tests │ ├── __init__.py │ ├── test_docs.py │ └── test_general.py ├── docs ├── Makefile ├── api.rst ├── camel.rst ├── conf.py ├── index.rst ├── make.bat └── yamlref.rst └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .cache/ 4 | *.egg-info/ 5 | __pycache__/ 6 | build/ 7 | dist/ 8 | docs/_build/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is licensed under the ISC license, reproduced below. 2 | 3 | Copyright (c) 2012, Lexy "eevee" Munroe 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README.txt -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Camel 2 | ===== 3 | 4 | Camel is a library that lets you describe how to serialize your objects to 5 | YAML — and refuses to serialize them if you don't. 6 | 7 | Quick example: 8 | 9 | .. code-block:: python 10 | 11 | from camel import Camel, CamelRegistry 12 | 13 | class DieRoll(tuple): 14 | def __new__(cls, a, b): 15 | return tuple.__new__(cls, [a, b]) 16 | 17 | def __repr__(self): 18 | return "DieRoll(%s,%s)" % self 19 | 20 | reg = CamelRegistry() 21 | 22 | @reg.dumper(DieRoll, u'roll', version=None) 23 | def _dump_dice(data): 24 | return u"{}d{}".format(*data) 25 | 26 | @reg.loader(u'roll', version=None) 27 | def _load_dice(data, version): 28 | a, _, b = data.partition(u'd') 29 | return DieRoll(int(a), int(b)) 30 | 31 | value = DieRoll(3, 6) 32 | camel = Camel([reg]) 33 | print(camel.dump(value)) 34 | 35 | # !roll 3d6 36 | # ... 37 | 38 | Docs: http://camel.readthedocs.org/en/latest/ 39 | 40 | GitHub: https://github.com/eevee/camel 41 | -------------------------------------------------------------------------------- /camel/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | # TODO 3 | # i kinda need for pokedex purposes: 4 | # - consider creating an @inherited_dumper? would need a way to pattern-match on the tag or something though, yikes. can this be done in a more controlled way, like you only get to pick a suffix to add to the tag? or is it useful in some other way to be able to pattern match on tags? 5 | # - easy way to explicitly set the representer style (maybe a decorator arg?) 6 | # - document tag_prefix! or was i thinking about the design of it? should you be able to override the tag prefix when adding to the camel? 7 | # - tag_prefix=YAML_TAG_PREFIX is different from passing it as the actual tag name and i don't get why -- maybe because the default tag prefix is ! 8 | # - test and document inherit arg to @dumper 9 | # - comment, test, document add_registry and tag_shorthand 10 | # 11 | # - expose the plain scalar parser things 12 | # 13 | # - support %TAG directives some nice reasonable way 14 | # - support the attrs module, if installed, somehow 15 | # - consider using (optionally?) ruamel.yaml, which roundtrips comments, merges, anchors, ... 16 | # - DWIM formatting: block style except for very short sequences (if at all?), quotey style for long text... 17 | # - make dumper/loader work on methods? ehh 18 | # - "raw" loaders and dumpers that get to deal with raw yaml nodes? 19 | 20 | 21 | from __future__ import absolute_import 22 | from __future__ import division 23 | from __future__ import print_function 24 | from __future__ import unicode_literals 25 | import base64 26 | import collections 27 | import functools 28 | from io import StringIO 29 | import types 30 | 31 | import yaml 32 | 33 | try: 34 | from yaml import CSafeDumper as SafeDumper 35 | from yaml import CSafeLoader as SafeLoader 36 | except ImportError: 37 | from yaml import SafeDumper 38 | from yaml import SafeLoader 39 | 40 | 41 | YAML_TAG_PREFIX = 'tag:yaml.org,2002:' 42 | 43 | _str = type('') 44 | _bytes = type(b'') 45 | _long = type(18446744073709551617) # 2**64 + 1 46 | 47 | 48 | class CamelDumper(SafeDumper): 49 | """Subclass of yaml's `SafeDumper` that scopes representers to the 50 | instance, rather than to the particular class, because damn. 51 | """ 52 | def __init__(self, *args, **kwargs): 53 | # TODO this isn't quite good enough; pyyaml still escapes anything 54 | # outside the BMP 55 | kwargs.setdefault('allow_unicode', True) 56 | super(CamelDumper, self).__init__(*args, **kwargs) 57 | self.yaml_representers = SafeDumper.yaml_representers.copy() 58 | self.yaml_multi_representers = SafeDumper.yaml_multi_representers.copy() 59 | 60 | # Always dump bytes as binary, even on Python 2 61 | self.add_representer(bytes, CamelDumper.represent_binary) 62 | 63 | def represent_binary(self, data): 64 | # This is copy-pasted, because it only exists in pyyaml in python 3 (?!) 65 | if hasattr(base64, 'encodebytes'): 66 | data = base64.encodebytes(data).decode('ascii') 67 | else: 68 | data = base64.encodestring(data).decode('ascii') 69 | return self.represent_scalar( 70 | YAML_TAG_PREFIX + 'binary', data, style='|') 71 | 72 | def add_representer(self, data_type, representer): 73 | self.yaml_representers[data_type] = representer 74 | 75 | def add_multi_representer(self, data_type, representer): 76 | self.yaml_multi_representers[data_type] = representer 77 | 78 | 79 | class CamelLoader(SafeLoader): 80 | """Subclass of yaml's `SafeLoader` that scopes constructors to the 81 | instance, rather than to the particular class, because damn. 82 | """ 83 | def __init__(self, *args, **kwargs): 84 | super(CamelLoader, self).__init__(*args, **kwargs) 85 | self.yaml_constructors = SafeLoader.yaml_constructors.copy() 86 | self.yaml_multi_constructors = SafeLoader.yaml_multi_constructors.copy() 87 | self.yaml_implicit_resolvers = SafeLoader.yaml_implicit_resolvers.copy() 88 | 89 | def add_constructor(self, data_type, constructor): 90 | self.yaml_constructors[data_type] = constructor 91 | 92 | def add_multi_constructor(self, data_type, constructor): 93 | self.yaml_multi_constructors[data_type] = constructor 94 | 95 | def add_implicit_resolver(self, tag, regexp, first): 96 | if first is None: 97 | first = [None] 98 | for ch in first: 99 | self.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) 100 | 101 | def add_path_resolver(self, *args, **kwargs): 102 | # This API is non-trivial and claims to be experimental and unstable 103 | raise NotImplementedError 104 | 105 | 106 | class Camel(object): 107 | """Class responsible for doing the actual dumping to and loading from YAML. 108 | """ 109 | def __init__(self, registries=()): 110 | self.registries = collections.OrderedDict() 111 | self.version_locks = {} # class => version 112 | 113 | self.add_registry(STANDARD_TYPES) 114 | for registry in registries: 115 | self.add_registry(registry) 116 | 117 | def add_registry(self, registry, tag_prefix=None, tag_shorthand=None): 118 | self.registries[registry] = ( 119 | tag_prefix or registry.tag_prefix, 120 | tag_shorthand or registry.tag_shorthand, 121 | ) 122 | 123 | def lock_version(self, cls, version): 124 | self.version_locks[cls] = version 125 | 126 | def make_dumper(self, stream): 127 | tag_shorthands = {} 128 | for registry, (prefix, shorthand) in self.registries.items(): 129 | if shorthand is None: 130 | continue 131 | if shorthand in tag_shorthands: 132 | raise ValueError( 133 | "Conflicting tag shorthands: {!r} is short for both {!r} and {!r}" 134 | .format(shorthand, tag_shorthands[shorthand], prefix)) 135 | tag_shorthands[shorthand] = prefix 136 | 137 | dumper = CamelDumper(stream, default_flow_style=False, tags=tag_shorthands) 138 | for registry in self.registries: 139 | registry.inject_dumpers(dumper, version_locks=self.version_locks) 140 | return dumper 141 | 142 | def dump(self, data): 143 | stream = StringIO() 144 | dumper = self.make_dumper(stream) 145 | dumper.open() 146 | dumper.represent(data) 147 | dumper.close() 148 | return stream.getvalue() 149 | 150 | def make_loader(self, stream): 151 | loader = CamelLoader(stream) 152 | for registry in self.registries: 153 | registry.inject_loaders(loader) 154 | return loader 155 | 156 | def load(self, data): 157 | stream = StringIO(data) 158 | loader = self.make_loader(stream) 159 | obj = loader.get_data() 160 | if loader.check_node(): 161 | raise RuntimeError("Multiple documents found in stream; use load_all") 162 | return obj 163 | 164 | def load_first(self, data): 165 | stream = StringIO(data) 166 | loader = self.make_loader(stream) 167 | return loader.get_data() 168 | 169 | def load_all(self, data): 170 | stream = StringIO(data) 171 | loader = self.make_loader(stream) 172 | while loader.check_node(): 173 | yield loader.get_data() 174 | 175 | 176 | class DuplicateVersion(ValueError): 177 | pass 178 | 179 | 180 | class CamelRegistry(object): 181 | frozen = False 182 | 183 | def __init__(self, tag_prefix='!', tag_shorthand=None): 184 | self.tag_prefix = tag_prefix 185 | self.tag_shorthand = tag_shorthand 186 | 187 | # type => {version => function) 188 | self.dumpers = collections.defaultdict(dict) 189 | self.multi_dumpers = collections.defaultdict(dict) 190 | # base tag => {version => function} 191 | self.loaders = collections.defaultdict(dict) 192 | 193 | def freeze(self): 194 | self.frozen = True 195 | 196 | # Dumping 197 | 198 | def _check_tag(self, tag): 199 | # Good a place as any, I suppose 200 | if self.frozen: 201 | raise RuntimeError("Can't add to a frozen registry") 202 | 203 | if ';' in tag: 204 | raise ValueError( 205 | "Tags may not contain semicolons: {0!r}".format(tag)) 206 | 207 | def dumper(self, cls, tag, version, inherit=False): 208 | self._check_tag(tag) 209 | 210 | if inherit: 211 | store_in = self.multi_dumpers 212 | else: 213 | store_in = self.dumpers 214 | 215 | if version in store_in[cls]: 216 | raise DuplicateVersion 217 | 218 | tag = self.tag_prefix + tag 219 | 220 | if version is None: 221 | full_tag = tag 222 | elif isinstance(version, (int, _long)) and version > 0: 223 | full_tag = "{0};{1}".format(tag, version) 224 | else: 225 | raise TypeError( 226 | "Expected None or a positive integer version; " 227 | "got {0!r} instead".format(version)) 228 | 229 | def decorator(f): 230 | store_in[cls][version] = functools.partial( 231 | self.run_representer, f, full_tag) 232 | return f 233 | 234 | return decorator 235 | 236 | def run_representer(self, representer, tag, dumper, data): 237 | canon_value = representer(data) 238 | # Note that we /do not/ support subclasses of the built-in types here, 239 | # to avoid complications from returning types that have their own 240 | # custom representers 241 | canon_type = type(canon_value) 242 | # TODO this gives no control over flow_style, style, and implicit. do 243 | # we intend to figure it out ourselves? 244 | if canon_type is dict: 245 | return dumper.represent_mapping(tag, canon_value, flow_style=False) 246 | elif canon_type is collections.OrderedDict: 247 | # pyyaml tries to sort the items of a dict, which defeats the point 248 | # of returning an OrderedDict. Luckily, it only does this if the 249 | # value it gets has an 'items' method; otherwise it skips the 250 | # sorting and iterates the value directly, assuming it'll get 251 | # key/value pairs. So pass in the dict's items iterator. 252 | return dumper.represent_mapping(tag, canon_value.items(), flow_style=False) 253 | elif canon_type in (tuple, list): 254 | return dumper.represent_sequence(tag, canon_value, flow_style=False) 255 | elif canon_type in (int, _long, float, bool, _str, type(None)): 256 | return dumper.represent_scalar(tag, canon_value) 257 | else: 258 | raise TypeError( 259 | "Representers must return native YAML types, but the representer " 260 | "for {!r} returned {!r}, which is of type {!r}" 261 | .format(data, canon_value, canon_type)) 262 | 263 | def inject_dumpers(self, dumper, version_locks=None): 264 | if not version_locks: 265 | version_locks = {} 266 | 267 | for add_method, dumpers in [ 268 | (dumper.add_representer, self.dumpers), 269 | (dumper.add_multi_representer, self.multi_dumpers), 270 | ]: 271 | for cls, versions in dumpers.items(): 272 | version = version_locks.get(cls, max) 273 | if versions and version is max: 274 | if None in versions: 275 | representer = versions[None] 276 | else: 277 | representer = versions[max(versions)] 278 | elif version in versions: 279 | representer = versions[version] 280 | else: 281 | raise KeyError( 282 | "Don't know how to dump version {0!r} of type {1!r}" 283 | .format(version, cls)) 284 | add_method(cls, representer) 285 | 286 | # Loading 287 | # TODO implement "upgrader", which upgrades from one version to another 288 | 289 | def loader(self, tag, version): 290 | self._check_tag(tag) 291 | 292 | if version in self.loaders[tag]: 293 | raise DuplicateVersion 294 | 295 | tag = self.tag_prefix + tag 296 | 297 | def decorator(f): 298 | self.loaders[tag][version] = functools.partial( 299 | self.run_constructor, f, version) 300 | return f 301 | 302 | return decorator 303 | 304 | def run_constructor(self, constructor, version, *yaml_args): 305 | # Two args for add_constructor, three for add_multi_constructor 306 | if len(yaml_args) == 3: 307 | loader, suffix, node = yaml_args 308 | version = int(suffix) 309 | else: 310 | loader, node = yaml_args 311 | 312 | if isinstance(node, yaml.ScalarNode): 313 | data = loader.construct_scalar(node) 314 | elif isinstance(node, yaml.SequenceNode): 315 | data = loader.construct_sequence(node, deep=True) 316 | elif isinstance(node, yaml.MappingNode): 317 | data = loader.construct_mapping(node, deep=True) 318 | else: 319 | raise TypeError("Not a primitive node: {!r}".format(node)) 320 | return constructor(data, version) 321 | 322 | def inject_loaders(self, loader): 323 | for tag, versions in self.loaders.items(): 324 | # "all" loader overrides everything 325 | if all in versions: 326 | if None in versions: 327 | loader.add_constructor(tag, versions[None]) 328 | else: 329 | loader.add_constructor(tag, versions[all]) 330 | loader.add_multi_constructor(tag + ";", versions[all]) 331 | continue 332 | 333 | # Otherwise, add each constructor individually 334 | for version, constructor in versions.items(): 335 | if version is None: 336 | loader.add_constructor(tag, constructor) 337 | elif version is any: 338 | loader.add_multi_constructor(tag + ";", versions[any]) 339 | if None not in versions: 340 | loader.add_constructor(tag, versions[any]) 341 | else: 342 | full_tag = "{0};{1}".format(tag, version) 343 | loader.add_constructor(full_tag, constructor) 344 | 345 | 346 | # YAML's "language-independent types" — not builtins, but supported with 347 | # standard !! tags. Most of them are built into pyyaml, but OrderedDict is 348 | # curiously overlooked. Loaded first by default into every Camel object. 349 | # Ref: http://yaml.org/type/ 350 | # TODO pyyaml supports tags like !!python/list; do we care? 351 | STANDARD_TYPES = CamelRegistry(tag_prefix=YAML_TAG_PREFIX) 352 | 353 | 354 | @STANDARD_TYPES.dumper(frozenset, 'set', version=None) 355 | def _dump_frozenset(data): 356 | return dict.fromkeys(data) 357 | 358 | 359 | @STANDARD_TYPES.dumper(collections.OrderedDict, 'omap', version=None) 360 | def _dump_ordered_dict(data): 361 | pairs = [] 362 | for key, value in data.items(): 363 | pairs.append({key: value}) 364 | return pairs 365 | 366 | 367 | @STANDARD_TYPES.loader('omap', version=None) 368 | def _load_ordered_dict(data, version): 369 | return collections.OrderedDict( 370 | pair for datum in data for (pair,) in [datum.items()] 371 | ) 372 | 373 | 374 | # Extra Python types that don't have native YAML equivalents, but that PyYAML 375 | # supports with !!python/foo tags. Dumping them isn't supported by default, 376 | # but loading them is, since there's no good reason for it not to be. 377 | # A couple of these dumpers override builtin type support. For example, tuples 378 | # are dumped as lists by default, but this registry will dump them as 379 | # !!python/tuple. 380 | PYTHON_TYPES = CamelRegistry(tag_prefix=YAML_TAG_PREFIX) 381 | 382 | 383 | @PYTHON_TYPES.dumper(tuple, 'python/tuple', version=None) 384 | def _dump_tuple(data): 385 | return list(data) 386 | 387 | 388 | @STANDARD_TYPES.loader('python/tuple', version=None) 389 | def _load_tuple(data, version): 390 | return tuple(data) 391 | 392 | 393 | @PYTHON_TYPES.dumper(complex, 'python/complex', version=None) 394 | def _dump_complex(data): 395 | ret = repr(data) 396 | if str is bytes: 397 | ret = ret.decode('ascii') 398 | # Complex numbers become (1+2j), but the parens are superfluous 399 | if ret[0] == '(' and ret[-1] == ')': 400 | return ret[1:-1] 401 | else: 402 | return ret 403 | 404 | 405 | @STANDARD_TYPES.loader('python/complex', version=None) 406 | def _load_complex(data, version): 407 | return complex(data) 408 | 409 | 410 | @PYTHON_TYPES.dumper(frozenset, 'python/frozenset', version=None) 411 | def _dump_frozenset(data): 412 | try: 413 | return list(sorted(data)) 414 | except TypeError: 415 | return list(data) 416 | 417 | 418 | @STANDARD_TYPES.loader('python/frozenset', version=None) 419 | def _load_frozenset(data, version): 420 | return frozenset(data) 421 | 422 | 423 | if hasattr(types, 'SimpleNamespace'): 424 | @PYTHON_TYPES.dumper(types.SimpleNamespace, 'python/namespace', version=None) 425 | def _dump_simple_namespace(data): 426 | return data.__dict__ 427 | 428 | 429 | @STANDARD_TYPES.loader('python/namespace', version=None) 430 | def _load_simple_namespace(data, version): 431 | return types.SimpleNamespace(**data) 432 | 433 | 434 | STANDARD_TYPES.freeze() 435 | PYTHON_TYPES.freeze() 436 | -------------------------------------------------------------------------------- /camel/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eevee/camel/1f9132ce43f6933bd3e91681404aab817876b3e1/camel/tests/__init__.py -------------------------------------------------------------------------------- /camel/tests/test_docs.py: -------------------------------------------------------------------------------- 1 | """Make sure the documentation examples actually, uh, work.""" 2 | from __future__ import unicode_literals 3 | import textwrap 4 | 5 | 6 | def test_docs_table_v1(): 7 | class Table(object): 8 | def __init__(self, size): 9 | self.size = size 10 | 11 | def __repr__(self): 12 | return "".format(self=self) 13 | 14 | from camel import CamelRegistry 15 | my_types = CamelRegistry() 16 | 17 | @my_types.dumper(Table, 'table', version=1) 18 | def _dump_table(table): 19 | return { 20 | 'size': table.size, 21 | } 22 | 23 | @my_types.loader('table', version=1) 24 | def _load_table(data, version): 25 | return Table(data["size"]) 26 | 27 | from camel import Camel 28 | table = Table(25) 29 | assert Camel([my_types]).dump(table) == "!table;1\nsize: 25\n" 30 | 31 | data = {'chairs': [], 'tables': [Table(25), Table(36)]} 32 | assert Camel([my_types]).dump(data) == textwrap.dedent(""" 33 | chairs: [] 34 | tables: 35 | - !table;1 36 | size: 25 37 | - !table;1 38 | size: 36 39 | """).lstrip() 40 | 41 | table, = Camel([my_types]).load("[!table;1 {size: 100}]") 42 | assert isinstance(table, Table) 43 | assert table.size == 100 44 | 45 | 46 | def test_docs_table_v2(): 47 | # Tables can be rectangles now! 48 | class Table(object): 49 | def __init__(self, height, width): 50 | self.height = height 51 | self.width = width 52 | 53 | def __repr__(self): 54 | return "
".format(self=self) 55 | 56 | from camel import Camel, CamelRegistry 57 | my_types = CamelRegistry() 58 | 59 | @my_types.dumper(Table, 'table', version=2) 60 | def _dump_table_v2(table): 61 | return { 62 | 'height': table.height, 63 | 'width': table.width, 64 | } 65 | 66 | @my_types.loader('table', version=2) 67 | def _load_table_v2(data, version): 68 | return Table(data["height"], data["width"]) 69 | 70 | @my_types.loader('table', version=1) 71 | def _load_table_v1(data, version): 72 | edge = data["size"] ** 0.5 73 | return Table(edge, edge) 74 | 75 | table = Table(7, 10) 76 | assert Camel([my_types]).dump(table) == textwrap.dedent(""" 77 | !table;2 78 | height: 7 79 | width: 10 80 | """).lstrip() 81 | 82 | @my_types.dumper(Table, 'table', version=1) 83 | def _dump_table_v1(table): 84 | return { 85 | # not really, but the best we can manage 86 | 'size': table.height * table.width, 87 | } 88 | 89 | camel = Camel([my_types]) 90 | camel.lock_version(Table, 1) 91 | assert camel.dump(Table(5, 7)) == "!table;1\nsize: 35\n" 92 | 93 | 94 | def test_docs_deleted(): 95 | class DummyData(object): 96 | def __init__(self, data): 97 | self.data = data 98 | 99 | from camel import Camel, CamelRegistry 100 | my_types = CamelRegistry() 101 | 102 | @my_types.loader('deleted-type', version=all) 103 | def _load_deleted_type(data, version): 104 | return DummyData(data) 105 | 106 | camel = Camel([my_types]) 107 | assert isinstance(camel.load("""!deleted-type;4 foo"""), DummyData) 108 | 109 | 110 | def test_docs_table_any(): 111 | class Table(object): 112 | def __init__(self, height, width): 113 | self.height = height 114 | self.width = width 115 | 116 | def __repr__(self): 117 | return "
".format(self=self) 118 | 119 | from camel import Camel, CamelRegistry 120 | my_types = CamelRegistry() 121 | 122 | @my_types.loader('table', version=any) 123 | def _load_table(data, version): 124 | if 'size' in data: 125 | # version 1 126 | edge = data['size'] ** 0.5 127 | return Table(edge, edge) 128 | else: 129 | # version 2?) 130 | return Table(data['height'], data['width']) 131 | 132 | camel = Camel([my_types]) 133 | table1, table2 = camel.load( 134 | "[!table;1 {size: 49}, !table;2 {height: 5, width: 9}]") 135 | 136 | assert table1.height == 7 137 | assert table1.width == 7 138 | assert table2.height == 5 139 | assert table2.width == 9 140 | -------------------------------------------------------------------------------- /camel/tests/test_general.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | import collections 7 | import datetime 8 | 9 | import pytest 10 | 11 | from camel import Camel, CamelRegistry, PYTHON_TYPES 12 | 13 | 14 | # Round-trips for simple values of built-in types 15 | @pytest.mark.parametrize(('value', 'expected_serialization'), [ 16 | # TODO the trailing ... for non-container values is kinda weird 17 | (None, "null\n...\n"), 18 | ('ⓤⓝⓘⓒⓞⓓⓔ', "ⓤⓝⓘⓒⓞⓓⓔ\n...\n"), 19 | (b'bytes', "!!binary |\n Ynl0ZXM=\n"), 20 | (True, "true\n...\n"), 21 | (133, "133\n...\n"), 22 | # long, for python 2 23 | (2**133, "10889035741470030830827987437816582766592\n...\n"), 24 | (3.52, "3.52\n...\n"), 25 | ([1, 2, 'three'], "- 1\n- 2\n- three\n"), 26 | ({'x': 7, 'y': 8, 'z': 9}, "x: 7\ny: 8\nz: 9\n"), 27 | # TODO this should use ? notation 28 | (set("qvx"), "!!set\nq: null\nv: null\nx: null\n"), 29 | (datetime.date(2015, 10, 21), "2015-10-21\n...\n"), 30 | (datetime.datetime(2015, 10, 21, 4, 29), "2015-10-21 04:29:00\n...\n"), 31 | # TODO case with timezone... unfortunately can't preserve the whole thing 32 | (collections.OrderedDict([('a', 1), ('b', 2), ('c', 3)]), "!!omap\n- a: 1\n- b: 2\n- c: 3\n"), 33 | ]) 34 | def test_basic_roundtrip(value, expected_serialization): 35 | camel = Camel() 36 | dumped = camel.dump(value) 37 | assert dumped == expected_serialization 38 | assert camel.load(dumped) == value 39 | 40 | 41 | def test_tuple_roundtrip(): 42 | # By default, tuples become lists 43 | value = (4, 3, 2) 44 | camel = Camel() 45 | dumped = camel.dump(value) 46 | # TODO short list like this should be flow style? 47 | assert dumped == "- 4\n- 3\n- 2\n" 48 | assert camel.load(dumped) == list(value) 49 | 50 | 51 | def test_frozenset_roundtrip(): 52 | # By default, frozensets become sets 53 | value = frozenset((4, 3, 2)) 54 | camel = Camel() 55 | dumped = camel.dump(value) 56 | # TODO this should use ? notation 57 | assert dumped == "!!set\n2: null\n3: null\n4: null\n" 58 | assert camel.load(dumped) == set(value) 59 | 60 | 61 | # Round-trips for built-in Python types with custom representations 62 | @pytest.mark.parametrize(('value', 'expected_serialization'), [ 63 | ((4, 3, 2), "!!python/tuple\n- 4\n- 3\n- 2\n"), 64 | (5 + 12j, "!!python/complex 5+12j\n...\n"), 65 | (2j, "!!python/complex 2j\n...\n"), 66 | (frozenset((4, 3, 2)), "!!python/frozenset\n- 2\n- 3\n- 4\n"), 67 | ]) 68 | def test_python_roundtrip(value, expected_serialization): 69 | camel = Camel([PYTHON_TYPES]) 70 | dumped = camel.dump(value) 71 | assert dumped == expected_serialization 72 | 73 | # Should be able to load them without the python types 74 | vanilla_camel = Camel() 75 | assert vanilla_camel.load(dumped) == value 76 | 77 | 78 | # ----------------------------------------------------------------------------- 79 | # Simple custom type 80 | 81 | class DieRoll(tuple): 82 | def __new__(cls, a, b): 83 | return tuple.__new__(cls, [a, b]) 84 | 85 | def __repr__(self): 86 | return "DieRoll(%s,%s)" % self 87 | 88 | 89 | # Dump/load as a compact string 90 | reg = CamelRegistry() 91 | 92 | 93 | @reg.dumper(DieRoll, 'roll', version=None) 94 | def dump_dice(data): 95 | return "{}d{}".format(*data) 96 | 97 | 98 | @reg.loader('roll', version=None) 99 | def load_dice(data, version): 100 | # TODO enforce incoming data is a string? 101 | a, _, b = data.partition('d') 102 | return DieRoll(int(a), int(b)) 103 | 104 | 105 | def test_dieroll(): 106 | value = DieRoll(3, 6) 107 | camel = Camel([reg]) 108 | dumped = camel.dump(value) 109 | assert dumped == '!roll 3d6\n...\n' 110 | assert camel.load(dumped) == value 111 | 112 | 113 | # Dump/load as a dict 114 | reg2 = CamelRegistry() 115 | 116 | 117 | @reg2.dumper(DieRoll, 'roll', version=None) 118 | def dump_dice(data): 119 | return collections.OrderedDict([ 120 | # NOTE: These are deliberately arranged in reverse alphabetical order, 121 | # to ensure that we avoid pyyaml's default behavior of sorting a 122 | # dict-like when dumping a mapping 123 | ('numdice', data[0]), 124 | ('faces', data[1]), 125 | ]) 126 | 127 | 128 | @reg2.loader('roll', version=None) 129 | def load_dice(data, version): 130 | # TODO enforce data is a dict? 131 | # TODO enforce data contains only these keys? 132 | return DieRoll( 133 | int(data['numdice']), 134 | int(data['faces']), 135 | ) 136 | 137 | 138 | def test_dieroll2(): 139 | value = DieRoll(3, 6) 140 | camel = Camel([reg2]) 141 | dumped = camel.dump(value) 142 | assert dumped == '!roll\nnumdice: 3\nfaces: 6\n' 143 | assert camel.load(dumped) == value 144 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build2 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build2 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Camel.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Camel.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Camel" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Camel" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. automodule:: camel 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/camel.rst: -------------------------------------------------------------------------------- 1 | Camel overview 2 | ============== 3 | 4 | Camel is intended as a replacement for libraries like :py:mod:`pickle` or 5 | `PyYAML`_, which automagically serialize any type they come across. That seems 6 | convenient at first, but in any large or long-lived application, the benefits 7 | are soon outweighed by the costs: 8 | 9 | .. _PyYAML: http://pyyaml.org/ 10 | 11 | * You can't move, rename, or delete any types that are encoded in a pickle. 12 | 13 | * Even private implementation details of your class are encoded in a pickle by 14 | default, which means you can't change them either. 15 | 16 | * Because pickle's behavior is recursive, it can be difficult to know which 17 | types are pickled. 18 | 19 | * Because pickle's behavior is recursive, you may inadvertently pickle far more 20 | data than necessary, if your objects have caches or reified properties. In 21 | extreme cases you may pickle configuration that's no longer correct when the 22 | pickle is loaded. 23 | 24 | * Since pickles aren't part of your codebase and are rarely covered by tests, 25 | you may not know you've broken pickles until your code hits production... or 26 | much later. 27 | 28 | * Pickle in particular is very opaque, even when using the ASCII format. It's 29 | easy to end up with a needlessly large pickle by accident and have no 30 | visibility into what's being stored in it, or to break loading a large pickle 31 | and be unable to recover gracefully or even tell where the problem is. 32 | 33 | * Automagically serialized data is hard enough to load back into your *own* 34 | application. Loading it anywhere else is effectively out of the question. 35 | 36 | It's certainly possible to whip pickle or PyYAML into shape manually by writing 37 | ``__reduce__`` or representer functions, but their default behavior is still 38 | automagic, so you can't be sure you didn't miss something. Also, nobody 39 | actually does it, so merely knowing it's possible doesn't help much. 40 | 41 | 42 | Camel's philosophy 43 | ------------------ 44 | 45 | Explicit is better than implicit. 46 | 47 | Complex is better than complicated. 48 | 49 | Readability counts. 50 | 51 | If the implementation is hard to explain, it's a bad idea. 52 | 53 | *In the face of ambiguity, refuse the temptation to guess.* 54 | 55 | — `The Zen of Python`_ 56 | 57 | .. _The Zen of Python: https://www.python.org/dev/peps/pep-0020/ 58 | 59 | Serialization is hard. We can't hide that difficulty, only delay it for a 60 | while. And it *will* catch up with you. 61 | 62 | A few people in the Python community have been rallying against pickle and its 63 | ilk for a while, but when asked for alternatives, all we can do is mumble 64 | something about writing functions. Well, that's not very helpful. 65 | 66 | Camel forces you to write all your own serialization code, then wires it all 67 | together for you. It's backed by YAML, which is ostensibly easy for humans to 68 | read — and has explicit support for custom types. Hopefully, after using 69 | Camel, you'll discover you've been tricked into making a library of every type 70 | you serialize, the YAML name you give it, and exactly how it's formatted. All 71 | of this lives in your codebase, so someone refactoring a class will easily 72 | stumble upon its serialization code. Why, you could even use this knowledge to 73 | load your types into an application written in a different language, or turn 74 | them into a documented format! 75 | 76 | 77 | Let's see some code already 78 | --------------------------- 79 | 80 | Let's! 81 | 82 | Here's the Table example from `a talk Alex Gaynor gave at PyCon US 2014`_. 83 | Initially we have some square tables. 84 | 85 | .. _a talk Alex Gaynor gave at PyCon US 2014: https://www.youtube.com/watch?v=7KnfGDajDQw&t=1292 86 | 87 | .. code-block:: python 88 | 89 | class Table(object): 90 | def __init__(self, size): 91 | self.size = size 92 | 93 | def __repr__(self): 94 | return "
".format(self=self) 95 | 96 | We want to be able to serialize these, so we write a *dumper* and a 97 | corresponding *loader* function. We'll also need a *registry* to store these 98 | functions:: 99 | 100 | from camel import CamelRegistry 101 | my_types = CamelRegistry() 102 | 103 | @my_types.dumper(Table, 'table', version=1) 104 | def _dump_table(table): 105 | return dict( 106 | size=table.size, 107 | ) 108 | 109 | @my_types.loader('table', version=1) 110 | def _load_table(data, version): 111 | return Table(data["size"]) 112 | 113 | .. note:: This example is intended for Python 3. With Python 2, 114 | ``dict(size=...)`` will create a "size" key that's a :py:class:`bytes`, 115 | which will be serialized as ``!!binary``. It will still work, but it'll be 116 | ugly, and won't interop with Python 3. If you're still on Python 2, you 117 | should definitely use dict literals with :py:class:`unicode` keys. 118 | 119 | Now we just give this registry to a :py:class:`Camel` object and ask it to dump 120 | for us:: 121 | 122 | from camel import Camel 123 | table = Table(25) 124 | print(Camel([my_types]).dump(table)) 125 | 126 | .. code-block:: yaml 127 | 128 | !table;1 129 | size: 25 130 | 131 | Unlike the simple example given in the talk, we can also dump arbitrary 132 | structures containing Tables with no extra effort:: 133 | 134 | data = dict(chairs=[], tables=[Table(25), Table(36)]) 135 | print(Camel([my_types]).dump(data)) 136 | 137 | .. code-block:: yaml 138 | 139 | chairs: [] 140 | tables: 141 | - !table;1 142 | size: 25 143 | - !table;1 144 | size: 36 145 | 146 | And load them back in:: 147 | 148 | print(Camel([my_types]).load("[!table;1 {size: 100}]")) 149 | 150 | .. code-block:: python 151 | 152 | [
] 153 | 154 | Versioning 155 | .......... 156 | 157 | As you can see, all serialized Tables are tagged as ``!table;1``. The 158 | ``table`` part is the argument we gave to ``@dumper`` and ``@loader``, and the 159 | ``1`` is the version number. 160 | 161 | Version numbers mean that when the time comes to change your class, you don't 162 | have anything to worry about. Just write a new loader and dumper with a higher 163 | version number, and fix the old loader to work with the new code:: 164 | 165 | # Tables can be rectangles now! 166 | class Table(object): 167 | def __init__(self, height, width): 168 | self.height = height 169 | self.width = width 170 | 171 | def __repr__(self): 172 | return "
".format(self=self) 173 | 174 | @my_types.dumper(Table, 'table', version=2) 175 | def _dump_table_v2(table): 176 | return dict( 177 | height=table.height, 178 | width=table.width, 179 | ) 180 | 181 | @my_types.loader('table', version=2) 182 | def _load_table_v2(data, version): 183 | return Table(data["height"], data["width"]) 184 | 185 | @my_types.loader('table', version=1) 186 | def _load_table_v1(data, version): 187 | edge = data["size"] ** 0.5 188 | return Table(edge, edge) 189 | 190 | table = Table(7, 10) 191 | print(Camel([my_types]).dump(table)) 192 | 193 | .. code-block:: yaml 194 | 195 | !table;2 196 | height: 7 197 | width: 10 198 | 199 | 200 | More on versions 201 | ---------------- 202 | 203 | Versions are expected to be positive integers, presumably starting at 1. 204 | Whenever your class changes, you have two options: 205 | 206 | 1. Fix the dumper and loader to preserve the old format but work with the new 207 | internals. 208 | 2. Failing that, write new dumpers and loaders and bump the version. 209 | 210 | One of the advantages of Camel is that your serialization code is nothing more 211 | than functions returning Python structures, so it's very easily tested. Even 212 | if you end up with dozens of versions, you can write test cases for each 213 | without ever dealing with YAML at all. 214 | 215 | You might be wondering whether there's any point to having more than one 216 | version of a dumper function. By default, only the dumper with the highest 217 | version for a type is used. But it's possible you may want to stay 218 | backwards-compatible with other code — perhaps an older version of your 219 | application or library — and thus retain the ability to write out older 220 | formats. You can do this with :py:meth:`Camel.lock_version`:: 221 | 222 | @my_types.dumper(Table, 'table', version=1) 223 | def _dump_table_v1(table): 224 | return dict( 225 | # not really, but the best we can manage 226 | size=table.height * table.width, 227 | ) 228 | 229 | camel = Camel([my_types]) 230 | camel.lock_version(Table, 1) 231 | print(camel.dump(Table(5, 7))) 232 | 233 | .. code-block:: yaml 234 | 235 | !table;1 236 | size: 35 237 | 238 | Obviously you might lose some information when round-tripping through an old 239 | format, but sometimes it's necessary until you can fix old code. 240 | 241 | Note that version locking only applies to dumping, not to loading. For 242 | loading, there are a couple special versions you can use. 243 | 244 | Let's say you delete an old class whose information is no longer useful. While 245 | cleaning up all references to it, you discover it has Camel dumpers and 246 | loaders. What about all your existing data? No problem! Just use a version 247 | of ``all`` and return a dummy object:: 248 | 249 | class DummyData(object): 250 | def __init__(self, data): 251 | self.data = data 252 | 253 | @my_types.loader('deleted-type', version=all) 254 | def _load_deleted_type(data, version): 255 | return DummyData(data) 256 | 257 | ``all`` overrides *all* other loader versions (hence the name). You might 258 | instead want to use ``any``, which is a fallback for when the version isn't 259 | recognized:: 260 | 261 | @my_types.loader('table', version=any) 262 | def _load_table(data, version): 263 | if 'size' in data: 264 | # version 1 265 | edge = data['size'] ** 0.5 266 | return Table(edge, edge) 267 | else: 268 | # version 2? 269 | return Table(data['height'], data['width']) 270 | 271 | Versions must still be integers; a non-integer version will cause an immediate 272 | parse error. 273 | 274 | Going versionless 275 | ................. 276 | 277 | You might be thinking that the version numbers everywhere are an eyesore, and 278 | your data would be much prettier if it only used ``!table``. 279 | 280 | Well, yes, it would. But you'd lose your ability to bump the version, so you'd 281 | have to be *very very sure* that your chosen format can be adapted to any 282 | possible future changes to your class. 283 | 284 | If you are, in fact, *very very sure*, then you can use a version of ``None``. 285 | This is treated like an *infinite* version number, so it will always be used 286 | when dumping (unless overridden by a version lock). 287 | 288 | Similarly, an unversioned tag will look for a loader with a ``None`` version, 289 | then fall back to ``all`` or ``any``. The order versions are checked for is 290 | thus: 291 | 292 | * ``None``, if appropriate 293 | * ``all`` 294 | * Numeric version, if appropriate 295 | * ``any`` 296 | 297 | There are deliberately no examples of unversioned tags here. Designing an 298 | unversioned format requires some care, and a trivial documentation example 299 | can't do it justice. 300 | 301 | 302 | Supported types 303 | --------------- 304 | 305 | By default, Camel knows how to load and dump all types in the `YAML type 306 | registry`_ to their Python equivalents, which are as follows. 307 | 308 | .. _YAML type registry: http://yaml.org/type/ 309 | 310 | =============== ======================================== 311 | YAML tag Python type 312 | =============== ======================================== 313 | ``!!binary`` :py:class:`bytes` 314 | ``!!bool`` :py:class:`bool` 315 | ``!!float`` :py:class:`float` 316 | ``!!int`` :py:class:`int` (or :py:class:`long` on Python 2) 317 | ``!!map`` :py:class:`dict` 318 | ``!!merge`` — 319 | ``!!null`` :py:class:`NoneType` 320 | ``!!omap`` :py:class:`collections.OrderedDict` 321 | ``!!seq`` :py:class:`list` or :py:class:`tuple` (dump only) 322 | ``!!set`` :py:class:`set` or :py:class:`frozenset` (dump only) 323 | ``!!str`` :py:class:`str` (:py:class:`unicode` on Python 2) 324 | ``!!timestamp`` :py:class:`datetime.date` or :py:class:`datetime.datetime` as appropriate 325 | =============== ======================================== 326 | 327 | .. note:: PyYAML tries to guess whether a bytestring is "really" a string on 328 | Python 2, but Camel does not. Serializing *any* bytestring produces an ugly 329 | base64-encoded ``!!binary`` representation. 330 | 331 | This is a **feature**. 332 | 333 | .. note:: A dumper function must return a value that can be expressed in YAML 334 | without a tag — that is, any of the above Python types *except* 335 | :py:class:`bytes`, :py:class:`set`/:py:class:`frozenset`, and 336 | :py:class:`datetime.date`/:py:class:`datetime.datetime`. (Of course, if the 337 | value is a container, its contents can be anything and will be serialized 338 | recursively.) 339 | 340 | If a dumper returns a :py:class:`collections.OrderedDict`, it will be 341 | serialized like a plain dict, but the order of its keys will be preserved. 342 | 343 | The following additional types are loaded by default, but **not dumped**. If 344 | you want to dump these types, you can use the existing ``camel.PYTHON_TYPES`` 345 | registry. 346 | 347 | ====================== ===================================== 348 | YAML tag Python type 349 | ====================== ===================================== 350 | ``!!python/complex`` :py:class:`complex` 351 | ``!!python/frozenset`` :py:class:`frozenset` 352 | ``!!python/namespace`` :py:class:`types.SimpleNamespace` (Python 3.3+) 353 | ``!!python/tuple`` :py:class:`tuple` 354 | ====================== ===================================== 355 | 356 | 357 | Other design notes 358 | ------------------ 359 | 360 | * Camel will automatically use the C extension if available, and fall back to a 361 | Python implementation otherwise. The PyYAML documentation says it doesn't 362 | have this behavior because there are some slight differences between the 363 | implementations, but fails to explain what they are. 364 | 365 | * :py:meth:`Camel.load` is safe by default. There is no calling of arbitrary 366 | functions or execution of arbitrary code just from loading data. There is no 367 | "dangerous" mode. PyYAML's ``!!python/object`` and similar tags are not 368 | supported. (Unless you write your own loaders for them, of course.) 369 | 370 | * There is no "OO" interface, where dumpers or loaders can be written as 371 | methods with special names. That approach forces a class to have only a 372 | single representation, and more importantly litters your class with junk 373 | unrelated to the class itself. Consider this a cheap implementation of 374 | traits. You can fairly easily build support for this in your application if 375 | you really *really* want it. 376 | 377 | * Yes, you may have to write a lot of boring code like this:: 378 | 379 | @my_types.dumper(SomeType, 'sometype') 380 | def _dump_sometype(data): 381 | return dict( 382 | foo=data.foo, 383 | bar=data.bar, 384 | baz=data.baz, 385 | ... 386 | ) 387 | 388 | I strongly encourage you *not* to do this automatically using introspection, 389 | which would defeat the point of using Camel. If it's painful, step back and 390 | consider whether you really need to be serializing as much as you are, or 391 | whether your classes need to be so large. 392 | 393 | * There's no guarantee that the data you get will actually be in the correct 394 | format for that version. YAML is meant for human beings, after all, and 395 | human beings make mistakes. If you're concerned about this, you could 396 | combine Camel with something like the `Colander`_ library. 397 | 398 | .. _Colander: http://docs.pylonsproject.org/projects/colander/en/latest/ 399 | 400 | 401 | Known issues 402 | ------------ 403 | 404 | Camel is a fairly simple wrapper around `PyYAML`_, and inherits many of its 405 | problems. Only YAML 1.1 is supported, not 1.2, so a handful of syntactic edge 406 | cases may not parse correctly. Loading and dumping are certainly slower and 407 | more memory-intensive than pickle or JSON. Unicode handling is slightly 408 | clumsy. Python-specific types use tags starting with ``!!``, which is supposed 409 | for be for YAML's types only. 410 | 411 | .. _PyYAML: http://pyyaml.org/ 412 | 413 | Formatting and comments are not preserved during a round-trip load and dump. 414 | The `ruamel.yaml`_ library is a fork of PyYAML that solves this problem, but it 415 | only works when using the pure-Python implementation, which would hurt Camel's 416 | performance even more. Opinions welcome. 417 | 418 | .. _ruamel.yaml: https://pypi.python.org/pypi/ruamel.yaml 419 | 420 | PyYAML has several features that aren't exposed in Camel yet: dumpers that work 421 | on subclasses, loaders that work on all tags with a given prefix, and parsers 422 | for plain scalars in custom formats. 423 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Camel documentation build configuration file, created by 4 | # sphinx-quickstart2 on Fri May 8 11:39:04 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.intersphinx', 34 | ] 35 | intersphinx_mapping = { 36 | 'python': ('https://docs.python.org/3', None), 37 | } 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'Camel' 53 | copyright = u'2015, Eevee (Lexy Munroe)' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '0.1' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '0.1.2' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all 79 | # documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | # If true, keep warnings as "system message" paragraphs in the built documents. 100 | #keep_warnings = False 101 | 102 | 103 | # -- Options for HTML output ---------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | html_theme = 'classic' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | #html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # Add any extra paths that contain custom files (such as robots.txt or 139 | # .htaccess) here, relative to this directory. These files are copied 140 | # directly to the root of the documentation. 141 | #html_extra_path = [] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'Cameldoc' 186 | 187 | 188 | # -- Options for LaTeX output --------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, 203 | # author, documentclass [howto, manual, or own class]). 204 | latex_documents = [ 205 | ('index', 'Camel.tex', u'Camel Documentation', 206 | u'Eevee (Alex Munroe)', 'manual'), 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | #latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | #latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | #latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | #latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'camel', u'Camel Documentation', 236 | [u'Eevee (Lexy Munroe)'], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | #man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ('index', 'Camel', u'Camel Documentation', 250 | u'Eevee (Lexy Munroe)', 'Camel', 'One line description of project.', 251 | 'Miscellaneous'), 252 | ] 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #texinfo_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #texinfo_domain_indices = True 259 | 260 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 261 | #texinfo_show_urls = 'footnote' 262 | 263 | # If true, do not generate a @detailmenu in the "Top" node's menu. 264 | #texinfo_no_detailmenu = False 265 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Camel documentation master file, created by 2 | sphinx-quickstart2 on Fri May 8 11:39:04 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Camel's documentation! 7 | ================================= 8 | 9 | Camel is a Python serialization library that forces you to explicitly describe 10 | how to dump or load your types. It's good for you, just like eating your 11 | vegetables. 12 | 13 | Contents: 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | camel 19 | yamlref 20 | api 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build2 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build2' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build2' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Camel.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Camel.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/yamlref.rst: -------------------------------------------------------------------------------- 1 | Brief YAML reference 2 | ==================== 3 | 4 | There is no official YAML reference guide. The YAML website only offers the 5 | `YAML specification`_, which is a dense and thorny tome clearly aimed at 6 | implementers. I suspect this has greatly hampered YAML's popularity. 7 | 8 | .. _YAML specification: http://www.yaml.org/spec/1.2/spec.html 9 | 10 | In the hopes of improving this situation, here is a very quick YAML overview 11 | that should describe the language almost entirely. Hopefully it's useful 12 | whether or not you use Camel. 13 | 14 | 15 | Overall structure and design 16 | ---------------------------- 17 | 18 | As I see it, YAML has two primary goals: to support encoding any arbitrary data 19 | structure; and to be easily read and written by humans. If only the spec 20 | shared that last goal. 21 | 22 | Human-readability means that much of YAML's syntax is optional, wherever it 23 | would be unambiguous and easier on a human. The trade-off is more complexity 24 | in parsers and emitters. 25 | 26 | Here's an example document, configuration for some hypothetical app: 27 | 28 | .. code-block:: yaml 29 | 30 | database: 31 | username: admin 32 | password: foobar # TODO get prod passwords out of config 33 | socket: /var/tmp/database.sock 34 | options: {use_utf8: true} 35 | memcached: 36 | host: 10.0.0.99 37 | workers: 38 | - host: 10.0.0.101 39 | port: 2301 40 | - host: 10.0.0.102 41 | port: 2302 42 | 43 | YAML often has more than one way to express the same data, leaving a human free 44 | to use whichever is most convenient. More convenient syntax tends to be more 45 | contextual or whitespace-sensitive. In the above document, you can see that 46 | indenting is enough to make a nested mapping. Integers and booleans are 47 | automatically distinguished from unquoted strings, as well. 48 | 49 | 50 | General syntax 51 | -------------- 52 | 53 | As of 1.2, YAML is a strict superset of JSON. Any valid JSON can be parsed in 54 | the same structure with a YAML 1.2 parser. 55 | 56 | YAML is designed around Unicode, not bytes, and its syntax assumes Unicode 57 | input. There is no syntactic mechanism for giving a character encoding; the 58 | parser is expected to recognize BOMs for UTF-8, UTF-16, and UTF-32, but 59 | otherwise a byte stream is assumed to be UTF-8. 60 | 61 | The only vertical whitespace characters are U+000A LINE FEED and U+000D 62 | CARRIAGE RETURN. The only horizontal whitespace characters are U+0009 TAB and 63 | U+0020 SPACE. Other control characters are not allowed anywhere. Otherwise, 64 | anything goes. 65 | 66 | YAML operates on *streams*, which can contain multiple distinct structures, 67 | each parsed individually. Each structure is called a *document*. 68 | 69 | A document begins with ``---`` and ends with ``...``. Both are optional, 70 | though a ``...`` can only be followed by directives or ``---``. You don't see 71 | multiple documents very often, but it's a very useful feature for sending 72 | intermittent chunks of data over a single network connection. With JSON you'd 73 | usually put each chunk on its own line and delimit with newlines; YAML has 74 | support built in. 75 | 76 | Documents may be preceded by *directives*, in which case the ``---`` is 77 | required to indicate the end of the directives. Directives are a ``%`` 78 | followed by an identifier and some parameters. (This is how directives are 79 | distinguished from a bare document without ``---``, so the first non-blank 80 | non-comment line of a document can't start with a ``%``.) 81 | 82 | There are only two directives at the moment: ``%YAML`` specifies the YAML 83 | version of the document, and ``%TAG`` is used for tag shorthand, described 84 | in :ref:`yamlref-more-tags`. Use of directives is, again, fairly uncommon. 85 | 86 | *Comments* may appear anywhere. ``#`` begins a comment, and it runs until the 87 | end of the line. In most cases, comments are whitespace: they don't affect 88 | indentation level, they can appear between any two tokens, and a comment on its 89 | own line is the same as a blank line. The few exceptions are not too 90 | surprising; for example, you can't have a comment between the key and colon in 91 | ``key:``. 92 | 93 | A YAML document is a graph of values, called *nodes*. See 94 | :ref:`yamlref-kinds`. 95 | 96 | Nodes may be prefixed with up to two properties: a *tag* and an *anchor*. 97 | Order doesn't matter, and both are optional. Properties can be given to any 98 | value, regardless of kind or style. 99 | 100 | Tags 101 | .... 102 | 103 | Tags are prefixed with ``!`` and describe the *type* of a node. This allows 104 | for adding new types without having to extend the syntax or mingle type 105 | information with data. Omitting the tag leaves the type to the parser's 106 | discretion; usually that means you'll get lists, dicts, strings, numbers, and 107 | other simple types. 108 | 109 | You'll probably only see tags in two forms: 110 | 111 | * ``!foo`` is a "local" tag, used for some custom type that's specific to the 112 | document. 113 | 114 | * ``!!bar`` is a built-in YAML type from the `YAML tag repository`_. Most of 115 | these are inferred from plain data — ``!!seq`` for sequences, ``!!int`` for 116 | numbers, and so on — but a few don't have dedicated syntax and have to be 117 | given explicitly. 118 | 119 | For example, ``!!binary`` is used for representing arbitrary binary data 120 | encoded as base64. So ``!!binary aGVsbG8=`` would parse as the bytestring 121 | ``hello``. 122 | 123 | .. _YAML tag repository: http://yaml.org/type/ 124 | 125 | There's much more to tags, most of which is rarely used in practice. See 126 | :ref:`yamlref-more-tags`. 127 | 128 | Anchors 129 | ....... 130 | 131 | The other node property is the *anchor*, which is how YAML can store recursive 132 | data structures. Anchor names are prefixed with ``&`` and can't contain 133 | whitespace, brackets, braces, or commas. 134 | 135 | An *alias node* is an anchor name prefixed with ``*``, and indicates that the 136 | node with that anchor name should occur in both places. (Alias nodes can't 137 | have properties themselves; the properties of the anchored node are used.) 138 | For example, you might share configuration:: 139 | 140 | host1: 141 | &common-host 142 | os: linux 143 | arch: x86_64 144 | host2: *common-host 145 | 146 | Or serialize a list that contains itself:: 147 | 148 | &me [*me] 149 | 150 | .. note:: This is **not** a copy. The exact same value is reused. 151 | 152 | Anchor names act somewhat like variable assignments: at any point in the 153 | document, the parser only knows about the anchors it's seen so far, and a 154 | second anchor with the same name takes precedence. This means that aliases 155 | cannot refer to anchors that appear later in the document. 156 | 157 | Anchor names aren't intended to carry information, which unfortunately means 158 | that most YAML parsers throw them away, and re-serializing a document will get 159 | you anchor names like ``ANCHOR1``. 160 | 161 | 162 | .. _yamlref-kinds: 163 | 164 | Kinds of value 165 | -------------- 166 | 167 | Values come in one of three *kinds*, which reflect the general "shape" of 168 | the data. Scalars are individual values; sequences are ordered collections; 169 | mappings are unordered associations. Each can be written in either a 170 | whitespace-sensitive *block style* or a more compact and explicit *flow style*. 171 | 172 | Scalars 173 | ....... 174 | 175 | Most values in a YAML document will be *plain scalars*. They're defined by 176 | exclusion: if it's not anything else, it's a plain scalar. Technically, they 177 | can only be flow style, so they're really "plain flow scalar style" scalars. 178 | 179 | Plain scalars are the most flexible kind of value, and may resolve to a variety 180 | of types from the `YAML tag repository`_: 181 | 182 | * Integers become, well, integers (``!!int``). Leading ``0``, ``0b``, and 183 | ``0x`` are recognized as octal, binary, and hexadecimal. ``_`` is allowed, 184 | and ignored. Curiously, ``:`` is allowed and treated as a base 60 delimiter, 185 | so you can write a time as ``1:59`` and it'll be loaded as the number of 186 | seconds, 119. 187 | 188 | * Floats become floats (``!!float``). Scientific notation using ``e`` is also 189 | recognized. As with integers, ``_`` is ignored and ``:`` indicates base 60, 190 | though only the last component can have a fractional part. Positive 191 | infinity, negative infinity, and not-a-number are recognized with a leading 192 | dot: ``.inf``, ``-.inf``, and ``.nan``. 193 | 194 | * ``true`` and ``false`` become booleans (``!!bool``). ``y``, ``n``, ``yes``, ``no``, 195 | ``on``, and ``off`` are allowed as synonyms. Uppercase and title case are 196 | also recognized. 197 | 198 | * ``~`` and ``null`` become nulls (``!!null``), which is ``None`` in Python. A 199 | completely empty value also becomes null. 200 | 201 | * ISO8601 dates are recognized (``!!timestamp``), with whitespace allowed 202 | between the date and time. The time is also optional, and defaults to 203 | midnight UTC. 204 | 205 | * ``=`` is a special value (``!!value``) used as a key in mappings. I've never 206 | seen it actually used, and the thing it does is nonsense in many languages 207 | anyway, so don't worry about it. Just remember you can't use ``=`` as a 208 | plain string. 209 | 210 | * ``<<`` is another special value (``!!merge``) used as a key in mappings. 211 | This one is actually kind of useful; it's described below in 212 | :ref:`yamlref-merge-keys`. 213 | 214 | .. note:: The YAML spec has a notion of *schemas*, sets of types which are 215 | recognized. The recommended schema is "core", which doesn't actually 216 | require ``!!timestamp`` support. I think the idea is to avoid requiring 217 | support for types that may not exist natively — a Perl YAML parser can't 218 | reasonably handle ``!!timestamp`` out of the box, because Perl has no 219 | built-in timestamp type. So while you could technically run into a parser 220 | that doesn't support floats (the "failsafe" schema only does strings!), it 221 | probably won't come as a surprise. 222 | 223 | Otherwise, it's a string. Well. Probably. As part of tag resolution (see 224 | :ref:`yamlref-more-tags`), an application is allowed to parse plain scalars 225 | however it wants; you might add logic that parses ``1..5`` as a range type, or 226 | you might recognize keywords and replace them with special objects. But if 227 | you're doing any of that, you're hopefully aware of it. 228 | 229 | Between the above parsing and conflicts with the rest of YAML's syntax, for a 230 | plain scalar to be a string, it must meet these restrictions: 231 | 232 | * It must not be ``true``, ``false``, ``yes``, ``no``, ``y``, ``n``, ``on``, 233 | ``off``, ``null``, or any of those words in uppercase or title case, which 234 | would all be parsed as booleans or nulls. 235 | 236 | * It must not be ``~``, which is null. If it's a mapping key, it must not be 237 | ``=`` or ``<<``, which are special key values. 238 | 239 | * It must not be something that looks like a number or timestamp. I wouldn't 240 | bet on anything that consists exclusively of digits, dashes, underscores, and 241 | colons. 242 | 243 | * The first character must not be any of: ``[`` ``]`` ``{`` ``}`` ``,`` ``#`` 244 | ``&`` ``*`` ``!`` ``|`` ``>`` ``'`` ``"`` ``%`` ``@`` `````. All of these 245 | are YAML syntax for some other kind of construct. 246 | 247 | * If the first character is ``?``, ``:``, or ``-``, the next character must not 248 | be whitespace. Otherwise it'll be parsed as a block mapping or sequence. 249 | 250 | * It must not contain `` #`` or ``: ``, which would be parsed as a comment or a 251 | key. A hash not preceded by space or a colon not followed by space is fine. 252 | 253 | * If the string is inside a flow collection (i.e., inside ``[...]`` or 254 | ``{...}``), it must not contain any of ``[`` ``]`` ``{`` ``}`` ``,``, which 255 | would all be parsed as part of the collection syntax. 256 | 257 | * Leading and trailing whitespace are ignored. 258 | 259 | * If the string is broken across lines, then the newline and any adjacent 260 | whitespace are collapsed into a single space. 261 | 262 | That actually leaves you fairly wide open; the biggest restriction is on the 263 | first character. You can have spaces, you can wrap across lines, you can 264 | include whatever (non-control) Unicode you want. 265 | 266 | If you need explicit strings, you have some other options. 267 | 268 | 269 | Strings 270 | ``````` 271 | 272 | YAML has lots of ways to write explicit strings. Aside from plain scalars, 273 | there are two other *flow scalar styles*. 274 | 275 | Single-quoted strings are surrounded by ``'``. Single quotes may be escaped as 276 | ``''``, but otherwise no escaping is done at all. You may wrap over multiple 277 | lines, but the newline and any surrounding whitespace becomes a single space. 278 | A line containing only whitespace becomes a newline. 279 | 280 | Double-quoted strings are surrounded by ``"``. Backslash escapes are recognized: 281 | 282 | ============== ====== 283 | Sequence Result 284 | ============== ====== 285 | ``\0`` U+0000 NULL 286 | ``\a`` U+0007 BELL 287 | ``\b`` U+0008 BACKSPACE 288 | ``\t`` U+0009 CHARACTER TABULATION 289 | ``\n`` U+000A LINE FEED 290 | ``\v`` U+000B LINE TABULATION 291 | ``\f`` U+000C FORM FEED 292 | ``\r`` U+000D CARRIAGE RETURN 293 | ``\e`` U+001B ESCAPE 294 | ``\"`` U+0022 QUOTATION MARK 295 | ``\/`` U+002F SOLIDUS 296 | ``\\`` U+005C REVERSE SOLIDUS 297 | ``\N`` U+0085 NEXT LINE 298 | ``\_`` U+00A0 NO-BREAK SPACE 299 | ``\L`` U+2028 LINE SEPARATOR 300 | ``\P`` U+2029 PARAGRAPH SEPARATOR 301 | ``\xNN`` Unicode character ``NN`` 302 | ``\uNNNN`` Unicode character ``NNNN`` 303 | ``\UNNNNNNNN`` Unicode character ``NNNNNNNN`` 304 | ============== ====== 305 | 306 | As usual, you may wrap a double-quoted string across multiple lines, but the 307 | newline and any surrounding whitespace becomes a single space. As with 308 | single-quoted strings, a line containing only whitespace becomes a newline. 309 | You can escape spaces and tabs to protect them from being thrown away. You 310 | can also escape a newline to preserve any trailing whitespace on that line, but 311 | throw away the newline and any leading whitespace on the next line. 312 | 313 | These rules are weird, so here's a contrived example:: 314 | 315 | "line \ 316 | one 317 | 318 | line two\n\ 319 | \ \ line three\nline four\n 320 | line five 321 | " 322 | 323 | Which becomes:: 324 | 325 | line one 326 | line two 327 | line three 328 | line four 329 | line five 330 | 331 | Right, well, I hope that clears that up. 332 | 333 | There are also two *block scalar styles*, both consisting of a header followed by an 334 | indented block. The header is usually just a single character, indicating 335 | which block style to use. 336 | 337 | ``|`` indicates *literal style*, which preserves all newlines in the indented 338 | block. ``>`` indicates *folded style*, which performs the same line folding as 339 | with quoted strings. Escaped characters are not recognized in either style. 340 | Indentation, the initial newline, and any leading blank lines are always 341 | ignored. 342 | 343 | So to represent this string:: 344 | 345 | This is paragraph one. 346 | 347 | This is paragraph two. 348 | 349 | You could use either literal style:: 350 | 351 | | 352 | This is paragraph one. 353 | 354 | This is paragraph two. 355 | 356 | Or folded style:: 357 | 358 | > 359 | This is 360 | paragraph one. 361 | 362 | 363 | This 364 | is paragraph 365 | two. 366 | 367 | Obviously folded style is more useful if you have paragraphs with longer lines. 368 | Note that there are two blank lines between paragraphs in folded style; a 369 | single blank line would be parsed as a single newline. 370 | 371 | The header has some other features, but I've never seen them used. It consists 372 | of up to three parts, with no intervening whitespace. 373 | 374 | 1. The character indicating which block style to use. 375 | 2. Optionally, the indentation level of the indented block, relative to its 376 | parent. You only need this if the first line of the block starts with a 377 | space, because the space would be interpreted as indentation. 378 | 3. Optionally, a "chomping" indicator. The default behavior is to include the 379 | final newline as part of the string, but ignore any subsequent empty lines. 380 | You can use ``-`` here to ignore the final newline as well, or use ``+`` to 381 | preserve all trailing whitespace verbatim. 382 | 383 | You can put a comment on the same line as the header, but a comment on the next 384 | line would be interpreted as part of the indented block. You can also put a 385 | tag or an anchor before the header, as with any other node. 386 | 387 | 388 | Sequences 389 | ......... 390 | 391 | Sequences are ordered collections, with type ``!!seq``. They're pretty simple. 392 | 393 | Flow style is a comma-delimited list in square brackets, just like JSON: 394 | ``[one, two, 3]``. A trailing comma is allowed, and whitespace is generally 395 | ignored. The contents must also be written in flow style. 396 | 397 | Block style is written like a bulleted list:: 398 | 399 | - one 400 | - two 401 | - 3 402 | - a plain scalar that's 403 | wrapped across multiple lines 404 | 405 | Indentation determines where each element ends, and where the entire sequence 406 | ends. 407 | 408 | Other blocks may be nested without intervening newlines:: 409 | 410 | - - one one 411 | - one two 412 | - - two one 413 | - two two 414 | 415 | 416 | Mappings 417 | ........ 418 | 419 | Mappings are unordered, er, mappings, with type ``!!map``. The keys must be 420 | unique, but may be of any type. Also, they're unordered. 421 | 422 | Did I mention that mappings are **unordered**? The order of the keys in the 423 | document is irrelevant and arbitrary. If you need order, you need a sequence. 424 | 425 | Flow style looks unsurprisingly like JSON: ``{x: 1, y: 2}``. Again, a trailing 426 | comma is allowed, and whitespace doesn't matter. 427 | 428 | As a special case, inside a sequence, you can write a single-pair mapping 429 | without the braces. So ``[a: b, c: d, e: f]`` is a sequence containing three 430 | mappings. This is allowed in block sequences too, and is used for the ordered 431 | mapping type ``!!omap``. 432 | 433 | Block style is actually a little funny. The canonical form is a little 434 | surprising:: 435 | 436 | ? x 437 | : 1 438 | ? y 439 | : 2 440 | 441 | ``?`` introduces a key, and ``:`` introduces a value. You very rarely see this 442 | form, because the ``?`` is optional as long as the key and colon are all on one 443 | line (to avoid ambiguity) and the key is no more than 1024 characters long (to 444 | avoid needing infinite lookahead). 445 | 446 | So that's more commonly written like this:: 447 | 448 | x: 1 449 | y: 2 450 | 451 | The explicit ``?`` syntax is more useful for complex keys. For example, it's 452 | the only way to use block styles in the key:: 453 | 454 | ? > 455 | If a train leaves Denver at 5:00 PM traveling at 90 MPH, and another 456 | train leaves New York City at 10:00 PM traveling at 80 MPH, by how many 457 | minutes are you going to miss your connection? 458 | : Depends whether we're on Daylight Saving Time or not. 459 | 460 | Other than the syntactic restrictions, an implicit key isn't special in any way 461 | and can also be of any type:: 462 | 463 | true: false 464 | null: null 465 | up: down 466 | [0, 1]: [1, 0] 467 | 468 | It's fairly uncommon to see anything but strings as keys, though, since 469 | languages often don't support it. Python can't have lists and dicts as dict 470 | keys; Perl 5 and JavaScript only support string keys; and so on. 471 | 472 | Unlike sequences, you may **not** nest another block inside a block mapping on 473 | the same line. This is invalid:: 474 | 475 | one: two: buckle my shoe 476 | 477 | But this is fine:: 478 | 479 | - one: 1 480 | two: 2 481 | - three: 3 482 | four: 4 483 | 484 | You can also nest a block sequence without indenting:: 485 | 486 | foods: 487 | - burger 488 | - fries 489 | drinks: 490 | - soda 491 | - iced tea 492 | 493 | One slight syntactic wrinkle: in either style, the colon must be followed by 494 | whitespace. ``foo:bar`` is a single string, remember. (For JSON's sake, the 495 | whitespace can be omitted if the colon immediately follows a flow sequence, a 496 | flow mapping, or a quoted string.) 497 | 498 | .. _yamlref-merge-keys: 499 | 500 | Merge keys 501 | `````````` 502 | 503 | These are written ``<<`` and have type ``!!merge``. A merge key should have 504 | another mapping (or sequence of mappings) as its value. Each mapping is merged 505 | into the containing mapping, with any existing keys left alone. The actual 506 | ``<<`` key is never shown to the application. 507 | 508 | This is generally used in conjunction with anchors to share default values:: 509 | 510 | defaults: &DEFAULTS 511 | use-tls: true 512 | verify-host: true 513 | host1: 514 | <<: *DEFAULTS 515 | hostname: example.com 516 | host2: 517 | <<: *DEFAULTS 518 | hostname: example2.com 519 | host3: 520 | <<: *DEFAULTS 521 | hostname: example3.com 522 | # we have a really, really good reason for doing this, really 523 | verify-host: false 524 | 525 | 526 | .. _yamlref-more-tags: 527 | 528 | More on tags 529 | ------------ 530 | 531 | ``!!str`` is actually an illusion. 532 | 533 | Tag names are actually URIs, using UTF-8 percent-encoding. YAML suggests using 534 | the ``tag:`` scheme and your domain name to help keep tags globally unique; for 535 | example, the string tag is really ``tag:yaml.org,2002:str``. (Domain names can 536 | change hands over time, hence the inclusion of a year.) 537 | 538 | That's quite a mouthful, and wouldn't be recognized as a tag anyway, because 539 | tags have to start with ``!``. So tags are written in shorthand with a prefix, 540 | like ``!foo!bar``. The ``!foo!`` is a *named tag handle* that expands to a 541 | given prefix, kind of like XML namespacing. Named tag handles must be defined 542 | by a ``%TAG`` directive before the document:: 543 | 544 | %TAG !foo! tag:example.com,2015:app/ 545 | 546 | A tag of ``!foo!bar`` would then resolve to ``tag:example.com,2015:app/bar``. 547 | 548 | I've never seen ``%TAG`` used in practice. Instead, everyone uses the two 549 | special tag handles. 550 | 551 | * The *primary tag handle* is ``!``, which by default expands to ``!``. So 552 | ``!bar`` just resolves to ``!bar``, a *local tag*, specific to the document 553 | and not expected to be unique. 554 | 555 | * The *secondary tag handle* is ``!!``, which by default expands to 556 | ``tag:yaml.org,2002:``, the prefix YAML uses for its own built-in types. So 557 | ``!!bar`` resolves to ``tag:yaml.org,2002:bar``, and the tag for a string 558 | would more commonly be written as ``!!str``. Defining new tags that use 559 | ``!!`` is impolite. 560 | 561 | Both special handles can be reassigned with ``%TAG``, just like any other 562 | handle. An important (and confusing) point here is that the **resolved** name 563 | determines whether or not a tag is local; how it's written is irrelevant. 564 | You're free to do this:: 565 | 566 | %TAG !foo! !foo-types/ 567 | 568 | Now ``!foo!bar`` is shorthand for ``!foo-types/bar``, which is a local tag. 569 | You can also do the reverse:: 570 | 571 | %TAG ! tag:example.com,2015:legacy-types/ 572 | 573 | Which would make ``!bar`` a global tag! This is deliberate, as a quick way to 574 | convert an entire document from local tags to global tags. 575 | 576 | You can reassign ``!!``, too. But let's not. 577 | 578 | Tags can also be written *verbatim* as ``!``, in which case ``foo`` is 579 | taken to be the resolved final name of the tag, ignoring ``%TAG`` and any other 580 | resolution mechanism. This is the only way to write a global tag without using 581 | ``%TAG``, since tags must start with a ``!``. 582 | 583 | Every node has a tag, whether it's given one explicitly or not. Nodes without 584 | explicit tags are given one of two special *non-specific* tags: ``!`` for 585 | quoted and folded scalars; or ``?`` for sequences, mappings, and plain scalars. 586 | 587 | The ``?`` tag tells the application to do *tag resolution*. Technically, this 588 | means the application can do any kind of arbitrary inspection to figure out the 589 | type of the node. In practice, it just means that scalars are inspected to see 590 | whether they're booleans, integers, floats, whatever else, or just strings. 591 | 592 | The ``!`` tag forces a node to be interpreted as a basic built-in type, based 593 | on its kind: ``!!str``, ``!!seq``, or ``!!map``. You can explicitly give the 594 | ``!`` tag to a node if you want, for example writing ``! true`` or ``! 133`` to 595 | force parsing as strings. Or you could use quotes. Just saying. 596 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | from io import open 3 | 4 | 5 | setup( 6 | name='camel', 7 | version='0.1.2', 8 | description="Python serialization for adults", 9 | long_description=open('README.txt', encoding='utf8').read(), 10 | url="https://github.com/eevee/camel", 11 | author="Eevee (Lexy Munroe)", 12 | author_email="eevee.camel@veekun.com", 13 | classifiers=[ 14 | 'Development Status :: 2 - Pre-Alpha', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: ISC License (ISCL)', 17 | 'Programming Language :: Python :: 2', 18 | 'Programming Language :: Python :: 2.7', 19 | 'Programming Language :: Python :: 3', 20 | 'Programming Language :: Python :: 3.2', 21 | 'Programming Language :: Python :: 3.3', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Programming Language :: Python :: 3.5', 24 | ], 25 | packages=find_packages(), 26 | install_requires=['pyyaml'], 27 | tests_require=['pytest'], 28 | ) 29 | --------------------------------------------------------------------------------