First commit.
authorKim Grasman <kim.grasman@gmail.com>
Mon, 1 Jul 2013 09:08:40 +0000 (11:08 +0200)
committerKim Grasman <kim.grasman@gmail.com>
Mon, 1 Jul 2013 09:08:40 +0000 (11:08 +0200)
.gitignore [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
README.rst [new file with mode: 0644]
__init__.py [new file with mode: 0644]
parser.py [new file with mode: 0644]
pyasn1gen.py [new file with mode: 0644]
sema.py [new file with mode: 0644]
support/__init__.py [new file with mode: 0644]
support/pygen.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..c03cded
--- /dev/null
@@ -0,0 +1,6 @@
+# Python bytecode
+__pycache__/
+*.pyc
+
+# Proprietary test data
+source.asn1
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..365ce36
--- /dev/null
@@ -0,0 +1,27 @@
+Copyright (c) 2013, Schneider Electric Buildings AB
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Schneider Electric Buildings AB nor the
+      names of contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Schneider Electric Buildings AB does not make any claims concerning code
+generated by the asn1ate library.
diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..37b76bf
--- /dev/null
@@ -0,0 +1,86 @@
+asn1ate -- ASN.1 translation library.
+Copyright 2013 Schneider Electric Buildings AB
+
+Introduction
+------------
+
+``asn1ate`` is a Python library for translating ASN.1 into other forms.
+It is intended for code generation from formal ASN.1 definitions, and a
+code generator for ``pyasn1`` is included.
+
+``asn1ate`` is released under a 3-clause BSD license. For details, see
+LICENSE.txt.
+
+
+Caveat
+------
+
+This is very much an alpha-quality prototype. Things that need doing:
+
+* Regression test suite
+* HACK/TODO/BUGs need to be fixed
+* ASN.1 grammar is very incomplete and incorrect in some places
+* Improve parser error handling/reporting
+
+
+Usage
+-----
+
+The immediate use of ``asn1ate`` is to generate ``pyasn1`` definitions from
+ASN.1 definitions. The command to do this is::
+
+  $ python .../asn1ate/pyasn1gen.py source.asn1
+
+It will print the ``pyasn1`` equivalent of ``source.asn1`` to stdout.
+
+
+Dependencies
+------------
+
+The only third-party dependency is ``pyparsing``.
+
+Although ``asn1ate`` was initially developed on Python 3.2, it has been tested
+with Python 2.7.3 and should port to older Python versions easily.
+
+
+Design notes
+------------
+
+The ``asn1ate`` package is designed along the same lines as a compiler with a
+driver, a parser, a semantic model and a convention for code generators.
+
+* ``parser.py`` -- a tokenizing parser for ASN.1 per X.680. It currently
+  recognizes a naive sub-set of X.680
+* ``sema.py`` -- a semantic ASN.1 object model, which can be constructed from
+  the AST generated by ``parser.py``
+* ``support/pygen.py`` -- a support library for generating Python code.
+* ``pyasn1gen.py`` -- a code generator to transform a semantic model into
+  ``pyasn1`` syntax. This can be used as a script in which case it will dump
+  output to stdout.
+
+The ASN.1 parser is very ad-hoc, I've experimented with the grammar until I
+found something that accepted our proprietary ASN.1 definition. It's based on
+``pyparsing`` but sets up parse actions to build an annotated AST. Every node of
+interest is annotated with a string denoting its type, e.g. ``Identifier``,
+``TypeAssignment``, etc. I've tried to stay with token types as named in X.680,
+but added custom ones or suppressed others, as necessary to get the AST in a
+useful shape.
+
+Annotated tokens are represented by a simple class containing the type name and
+a list of children (called ``elements``) which may be annotated tokens, lists or
+simple values. This gives a very discoverable tree structure, but there are
+probably cleaner AST representations we could use. Patches welcome.
+
+``asn1ate.sema`` is an object model that represents ASN.1 constructs. It
+describes everything from type assignments to default values and tags, but still
+only the parts of ASN.1 we happen to use here. Most of the logic revolves around
+transforming the AST produced by ``asn1ate.parser`` into a more semantic model
+with proper Python objects.
+
+Codegen is designed to be extensible. In-house we have a set of code generators
+to build an entire protocol stack based on an ASN.1 source, but ``asn1ate`` only
+includes the generally useful one, ``asn1ate.pyasn1gen``.
+
+The most notable members of ``asn1ate.support`` are probably the
+``PythonWriter`` and ``PythonFragment`` classes, which simplify generation of
+correctly indented Python code.
diff --git a/__init__.py b/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/parser.py b/parser.py
new file mode 100644 (file)
index 0000000..2e11893
--- /dev/null
+++ b/parser.py
@@ -0,0 +1,299 @@
+# Copyright (c) 2013, Schneider Electric Buildings AB
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of Schneider Electric Buildings AB nor the
+#       names of contributors may be used to endorse or promote products
+#       derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import re
+from copy import copy
+from pyparsing import Keyword, Literal, Word, OneOrMore, Combine, Regex, Forward, Optional, Group, Suppress, delimitedList, cStyleComment, nums, alphanums, empty, srange
+
+
+__all__ = ['parse_asn1', 'AnnotatedToken']
+
+
+def parse_asn1(asn1_payload):
+    """ Parse a string containing an ASN.1 module definition
+    and return a syntax tree in the form of a list of
+    AnnotatedToken objects.
+    """
+    grammar = _build_asn1_grammar()
+    parse_result = grammar.parseString(asn1_payload)
+    parse_tree = parse_result.asList()
+    return parse_tree
+
+
+def print_parse_tree(node, indent=1):
+    """ Debugging aid. Dumps a parse tree as returned
+    from parse_asn1 to stdout in indented tree form.
+    """
+    def indented_print(msg):
+        print(' ' * indent + msg)
+
+    if type(node) is AnnotatedToken:
+        # tagged token
+        tag, values = node.ty, node.elements
+        indented_print('%s:' % tag)
+        print_parse_tree(values, indent + 1)
+    elif type(node) is list:
+        # token list
+        for token in node:
+            print_parse_tree(token, indent + 1)
+    else:
+        # token
+        indented_print(str(node))
+
+
+class AnnotatedToken(object):
+    """ A simple data structure to keep track of a token's
+    type, identified by a string, and its children.
+    Children may be other annotated tokens, lists or simple
+    strings.
+    """
+    def __init__(self, token_type, elements):
+        self.ty = token_type
+        self.elements = elements
+
+    def __str__(self):
+        return 'T(%s)%s' % (self.ty, self.elements)
+
+    __repr__ = __str__
+
+
+def _build_asn1_grammar():
+    def build_identifier(prefix_pattern):
+        identifier_suffix = Optional(Word(srange('[-0-9a-zA-Z]')))
+        identifier = Combine(Word(srange(prefix_pattern), exact=1) + identifier_suffix)  # todo: more rigorous? trailing hyphens and -- forbidden
+        return identifier
+
+    def braced_list(element_rule):
+        return Suppress('{') + Group(delimitedList(element_rule)) + Suppress('}')
+
+    def annotate(name):
+        def annotation(t):
+            return AnnotatedToken(name, t.asList())
+
+        return annotation
+
+    # Reserved words
+    DEFINITIONS = Keyword('DEFINITIONS')
+    BEGIN = Keyword('BEGIN')
+    END = Keyword('END')
+    OPTIONAL = Keyword('OPTIONAL')
+    DEFAULT = Keyword('DEFAULT')
+    TRUE = Keyword('TRUE')
+    FALSE = Keyword('FALSE')
+    UNIVERSAL = Keyword('UNIVERSAL')
+    APPLICATION = Keyword('APPLICATION')
+    PRIVATE = Keyword('PRIVATE')
+    MIN = Keyword('MIN')
+    MAX = Keyword('MAX')
+    IMPLICIT = Keyword('IMPLICIT')
+    EXPLICIT = Keyword('EXPLICIT')
+    EXPLICIT_TAGS = Keyword('EXPLICIT TAGS')
+    IMPLICIT_TAGS = Keyword('IMPLICIT TAGS')
+    AUTOMATIC_TAGS = Keyword('AUTOMATIC TAGS')
+    EXTENSIBILITY_IMPLIED = Keyword('EXTENSIBILITY IMPLIED')
+
+    # Built-in types
+    SEQUENCE = Keyword('SEQUENCE')
+    SEQUENCE_OF = Keyword('SEQUENCE OF')
+    CHOICE = Keyword('CHOICE')
+    ENUMERATED = Keyword('ENUMERATED')
+    BIT_STRING = Keyword('BIT STRING')
+    BOOLEAN = Keyword('BOOLEAN')
+    REAL = Keyword('REAL')
+    OCTET_STRING = Keyword('OCTET STRING')
+    CHARACTER_STRING = Keyword('CHARACTER STRING')
+    NULL = Keyword('NULL')
+    INTEGER = Keyword('INTEGER')
+
+    # Restricted string types
+    BMPString = Keyword('BMPString')
+    GeneralString = Keyword('GeneralString')
+    GraphicString = Keyword('GraphicString')
+    IA5String =  Keyword('IA5String')
+    ISO646String = Keyword('ISO646String')
+    NumericString = Keyword('NumericString')
+    PrintableString = Keyword('PrintableString')
+    TeletexString = Keyword('TeletexString')
+    T61String = Keyword('T61String')
+    UniversalString = Keyword('UniversalString')
+    UTF8String = Keyword('UTF8String')
+    VideotexString = Keyword('VideotexString')
+    VisibleString = Keyword('VisibleString')
+
+    # Literals
+    number = Word(nums)
+    signed_number = Combine(Optional('-') + number)  # todo: consider defined values from 18.1
+    bstring = Literal('\'') + Regex('[01]+') + Literal('\'B')
+    hstring = Literal('\'') + Regex('[0-9A-F]+') + Literal('\'H')
+
+    # Comments
+    hyphen_comment = Regex(r"--[\s\S]*?(--|$)", flags=re.MULTILINE)
+    comment = hyphen_comment | cStyleComment
+
+    # identifier
+    identifier = build_identifier('[a-z]')
+
+    # references
+    # these are duplicated to force unique token annotations
+    valuereference = build_identifier('[a-z]')
+    typereference = build_identifier('[A-Z]')
+    module_reference = build_identifier('[A-Z]')
+
+    # values
+    # BUG: These are badly specified and cause the grammar to break if used generally.
+    # todo: consider more literals from 16.9
+    real_value = Regex(r'-?\d+(\.\d*)?') # todo: this doesn't really follow the spec
+    boolean_value = TRUE | FALSE
+    bitstring_value = bstring | hstring     # todo: consider more forms from 21.9
+    integer_value = signed_number
+    null_value = NULL
+
+    builtin_value = boolean_value | bitstring_value | real_value | integer_value | null_value
+    defined_value = valuereference # todo: more options from 13.1
+    value = builtin_value | defined_value
+
+    # tags
+    class_ = UNIVERSAL | APPLICATION | PRIVATE
+    class_number = number # todo: consider defined values from 30.1
+    tag = Suppress('[') + Optional(class_) + class_number + Suppress(']')
+    tag_default = EXPLICIT_TAGS | IMPLICIT_TAGS | AUTOMATIC_TAGS | empty
+
+    # extensions
+    extension_default = EXTENSIBILITY_IMPLIED | empty
+
+    # types
+    defined_type = Unique(typereference)  # todo: consider other defined types from 13.1
+    referenced_type = Unique(defined_type)  # todo: consider other ref:d types from 16.3
+    named_type = Forward() # this can only be full defined once we have all types defined.
+    type_ = Forward() # this can only be full defined once we have all types defined.
+
+    # constraints
+    # todo: consider the full subtype and general constraint syntax described in 45.*
+    # but for now, just implement a simple integer value range.
+    value_range_min = (signed_number | MIN)
+    value_range_max = (signed_number | MAX)
+    value_range_constraint = value_range_min + Suppress('..') + value_range_max
+    constraint = Suppress('(') + value_range_constraint + Suppress(')')  # todo: consider exception spec from 45.6
+
+    # BUG: identifier should not be Optional here,
+    # but our ASN.1 interpreter supports unnamed members,
+    # and we use them.
+    # todo: consider COMPONENTS OF from 24.1
+    component_optional = OPTIONAL
+    component_default = DEFAULT + value
+    component_type = Optional(identifier) + type_ + Optional(component_optional | component_default)
+    tagged_type = tag + Optional(IMPLICIT | EXPLICIT) + type_
+
+    named_number_value = Suppress('(') + signed_number + Suppress(')')
+    named_number = identifier + named_number_value
+    enumeration = named_number | identifier
+
+    # todo: consider extension and exception syntax from 24.1
+    sequence_type = SEQUENCE + braced_list(component_type)
+    sequenceof_type = SEQUENCE_OF + (type_ | named_type)
+    choice_type = CHOICE + braced_list(named_type)
+    enumerated_type = ENUMERATED + braced_list(enumeration)
+    bitstring_type = BIT_STRING + braced_list(named_number)
+    plain_integer_type = INTEGER
+    restricted_integer_type = INTEGER + braced_list(named_number)
+    boolean_type = BOOLEAN
+    real_type = REAL
+    null_type = NULL
+    octetstring_type = OCTET_STRING
+    unrestricted_characterstring_type = CHARACTER_STRING
+    restricted_characterstring_type = BMPString | GeneralString | \
+                                      GraphicString | IA5String | \
+                                      ISO646String | NumericString | \
+                                      PrintableString | TeletexString | \
+                                      T61String | UniversalString | \
+                                      UTF8String | VideotexString | VisibleString
+    characterstring_type = restricted_characterstring_type | unrestricted_characterstring_type
+
+    # todo: consider other builtins from 16.2
+    simple_type = (boolean_type | null_type | octetstring_type | characterstring_type | real_type | plain_integer_type) + Optional(constraint)
+    constructed_type = choice_type | sequence_type
+    value_list_type = restricted_integer_type | enumerated_type
+    builtin_type = tagged_type | simple_type | constructed_type | sequenceof_type | value_list_type | bitstring_type
+
+    type_ << (builtin_type | referenced_type)
+    named_type << (identifier + type_)
+
+    # BUG: Trailing semi-colon is not allowed by standard grammar, but our ASN.1 interpreter accepts it
+    # and we happen to use it.
+    type_assignment = typereference + '::=' + type_ + Suppress(Optional(';'))
+    value_assignment = valuereference + type_ + '::=' + value
+
+    assignment = type_assignment | value_assignment
+    assignment_list = OneOrMore(assignment)
+
+    module_body = (assignment_list | empty)
+    module_defaults = Suppress(tag_default + extension_default)  # we don't want these in the AST
+    module_definition = module_reference + DEFINITIONS + module_defaults + '::=' + BEGIN + module_body + END
+
+    module_definition.ignore(comment)
+
+    # Mark up the parse results with token tags
+    identifier.setParseAction(annotate('Identifier'))
+    named_number_value.setParseAction(annotate('Value'))
+    tag.setParseAction(annotate('Tag'))
+    class_.setParseAction(annotate('TagClass'))
+    class_number.setParseAction(annotate('TagClassNumber'))
+    type_.setParseAction(annotate('Type'))
+    simple_type.setParseAction(annotate('SimpleType'))
+    choice_type.setParseAction(annotate('ChoiceType'))
+    sequence_type.setParseAction(annotate('SequenceType'))
+    value_list_type.setParseAction(annotate('ValueListType'))
+    bitstring_type.setParseAction(annotate('BitStringType'))
+    referenced_type.setParseAction(annotate('ReferencedType'))
+    sequenceof_type.setParseAction(annotate('SequenceOfType'))
+    named_number.setParseAction(annotate('NamedValue'))
+    constraint.setParseAction(annotate('Constraint'))
+    component_type.setParseAction(annotate('ComponentType'))
+    tagged_type.setParseAction(annotate('TaggedType'))
+    named_type.setParseAction(annotate('NamedType'))
+    type_assignment.setParseAction(annotate('TypeAssignment'))
+    value_assignment.setParseAction(annotate('ValueAssignment'))
+    module_reference.setParseAction(annotate('ModuleReference'))
+    module_body.setParseAction(annotate('ModuleBody'))
+    module_definition.setParseAction(annotate('ModuleDefinition'))
+    component_optional.setParseAction(annotate('ComponentOptional'))
+    component_default.setParseAction(annotate('ComponentDefault'))
+
+    return module_definition
+
+
+def Unique(token):
+    """ Use to create a distinct name of a production
+    with the same form as another, e.g.
+      identifier = build_identifier('[a-z]')
+      valuereference = build_identifier('[a-z]')
+    We prefer:
+      identifier = build_identifier('[a-z]')
+      valuereference = Unique(identifier)
+    to avoid duplicating the details of the grammar.
+    This allows unique parse actions for productions
+    with the same underlying rules.
+    """
+    return copy(token)
diff --git a/pyasn1gen.py b/pyasn1gen.py
new file mode 100644 (file)
index 0000000..44f14e3
--- /dev/null
@@ -0,0 +1,311 @@
+# Copyright (c) 2013, Schneider Electric Buildings AB
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of Schneider Electric Buildings AB nor the
+#       names of contributors may be used to endorse or promote products
+#       derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import print_function  # Python 2 compatibility
+
+import sys
+from asn1ate import parser
+from asn1ate.support import pygen
+from asn1ate.sema import *
+
+
+class Pyasn1Backend(object):
+    """ Backend to generate pyasn1 declarations from semantic tree.
+    Generators are divided into declarations and expressions,
+    because types in pyasn1 can be declared either as class
+    definitions or inline, e.g.
+
+    # Foo ::= INTEGER
+    # Foo is a decl
+    class Foo(univ.Integer):
+        pass
+
+    # Seq ::= SEQUENCE {
+    #     foo INTEGER
+    # }
+    # Seq is a decl,
+    # univ.Integer is an expr
+    class Seq(univ.Sequence):
+        componentType = namedtype.NamedTypes(
+            namedtype.NamedType('foo', univ.Integer())
+        )
+
+    Typically, declarations can contain other declarations
+    or expressions, expressions can only contain other expressions.
+    """
+    def __init__(self, sema_module, out_stream):
+        self.sema_module = sema_module
+        self.writer = pygen.PythonWriter(out_stream)
+
+        self.decl_generators = {
+            ChoiceType: self.decl_constructed_type,
+            SequenceType: self.decl_constructed_type,
+            TaggedType: self.decl_tagged_type,
+            SimpleType: self.decl_simple_type,
+            UserDefinedType: self.decl_userdefined_type,
+            ValueListType: self.decl_value_list_type,
+            BitStringType: self.decl_bitstring_type,
+            SequenceOfType: self.decl_sequenceof_type,
+            TypeAssignment: self.decl_type_assignment
+        }
+
+        self.expr_generators = {
+            TaggedType: self.expr_tagged_type,
+            SimpleType: self.expr_simple_type,
+            UserDefinedType: self.expr_userdefined_type,
+            ComponentType: self.expr_component_type,
+            NamedType: self.expr_named_type,
+            SequenceOfType: self.expr_sequenceof_type,
+        }
+
+    def generate_code(self):
+        self.writer.write_line('from pyasn1.type import univ, char, namedtype, namedval, tag, constraint')
+        self.writer.write_blanks(2)
+
+        declarations = topological_sort(self.sema_module.declarations)
+        for declaration in declarations:
+            self.writer.write_block(self.generate_decl(declaration))
+            self.writer.write_blanks(2)
+
+    def generate_expr(self, t):
+        generator = self.expr_generators[type(t)]
+        return generator(t)
+
+    def generate_decl(self, t):
+        generator = self.decl_generators[type(t)]
+        return generator(t)
+
+    def decl_type_assignment(self, assignment):
+        fragment = self.writer.get_fragment()
+
+        assigned_type, type_decl = assignment.type_name, assignment.type_decl
+
+        base_type = _translate_type(type_decl.type_name)
+        fragment.write_line('class %s(%s):' % (assigned_type, base_type))
+
+        fragment.push_indent()
+        fragment.write_block(self.generate_decl(type_decl))
+        fragment.pop_indent()
+
+        return str(fragment)
+
+    def expr_simple_type(self, t):
+        type_expr = _translate_type(t.type_name) + '()'
+        if t.constraint:
+            type_expr += '.subtype(subtypeSpec=constraint.ValueRangeConstraint(%s, %s))' % (t.constraint.min_value, t.constraint.max_value)
+
+        return type_expr
+
+    def decl_simple_type(self, t):
+        return 'pass'
+
+    def expr_userdefined_type(self, t):
+        return t.type_name + '()'
+
+    def decl_userdefined_type(self, t):
+        return 'pass'
+
+    def decl_constructed_type(self, t):
+        fragment = self.writer.get_fragment()
+
+        fragment.write_line('componentType = namedtype.NamedTypes(')
+
+        fragment.push_indent()
+        fragment.write_block(self.expr_component_types(t.components))
+        fragment.pop_indent()
+
+        fragment.write_line(')')
+
+        return str(fragment)
+
+    def expr_component_types(self, components):
+        fragment = self.writer.get_fragment()
+
+        component_exprs = []
+        for c in components:
+            component_exprs.append(self.generate_expr(c))
+
+        fragment.write_enumeration(component_exprs)
+
+        return str(fragment)
+
+    def expr_tagged_type(self, t):
+        tag_type = 'implicitTag' if t.implicit else 'explicitTag'
+        type_expr = self.generate_expr(t.type_decl)
+        type_expr += '.subtype(%s=%s)' % (tag_type, self.build_tag_expr(t))
+
+        return type_expr
+
+    def decl_tagged_type(self, t):
+        fragment = self.writer.get_fragment()
+
+        tag_type = 'tagImplicitly' if t.implicit else 'tagExplicitly'
+        base_type = _translate_type(t.type_decl.type_name)
+        fragment.write_line('tagSet = %s.tagSet.%s(%s)' % (base_type, tag_type, self.build_tag_expr(t)))
+        fragment.write_line(self.generate_decl(t.type_decl))  # possibly 'pass'. but that's OK in a decl
+
+        return str(fragment)
+
+    def build_tag_expr(self, tag_def):
+        context = _translate_tag_class(tag_def.class_name)
+
+        tagged_type_decl = self.sema_module.resolve_type_decl(tag_def.type_decl)
+        if isinstance(tagged_type_decl, ConstructedType):
+            tag_format = 'tag.tagFormatConstructed'
+        else:
+            tag_format = 'tag.tagFormatSimple'
+
+        return 'tag.Tag(%s, %s, %s)' % (context, tag_format, tag_def.class_number)
+
+    def expr_component_type(self, t):
+        if t.optional:
+            return "namedtype.OptionalNamedType('%s', %s)" % (t.identifier, self.generate_expr(t.type_decl))
+        elif t.default_value is not None:
+            type_expr = self.generate_expr(t.type_decl)
+            type_expr += '.subtype(value=%s)' % _translate_value(t.default_value)
+
+            return "namedtype.DefaultedNamedType('%s', %s)" % (t.identifier, type_expr)
+        else:
+            return "namedtype.NamedType('%s', %s)" % (t.identifier, self.generate_expr(t.type_decl))
+
+    def expr_named_type(self, t):
+        return "namedtype.NamedType('%s', %s)" % (t.identifier, self.generate_expr(t.type_decl))
+
+    def decl_value_list_type(self, t):
+        fragment = self.writer.get_fragment()
+
+        if t.named_values:
+            fragment.write_line('namedValues = namedval.NamedValues(')
+            fragment.push_indent()
+
+            named_values = list(map(lambda v: '(\'%s\', %s)' % (v.identifier, v.value), t.named_values))
+            fragment.write_enumeration(named_values)
+
+            fragment.pop_indent()
+            fragment.write_line(')')
+        else:
+            fragment.write_line('pass')
+
+        return str(fragment)
+
+    def decl_bitstring_type(self, t):
+        fragment = self.writer.get_fragment()
+
+        if t.named_bits:
+            fragment.write_line('namedValues = namedval.NamedValues(')
+            fragment.push_indent()
+
+            named_bit_list = list(map(lambda v: '(\'%s\', %s)' % (v.identifier, v.value), t.named_bits))
+            fragment.write_enumeration(named_bit_list)
+
+            fragment.pop_indent()
+            fragment.write_line(')')
+        else:
+            fragment.write_line('pass')
+
+        return str(fragment)
+
+    def expr_sequenceof_type(self, t):
+        return 'univ.SequenceOf(componentType=%s)' % self.generate_expr(t.type_decl)
+
+    def decl_sequenceof_type(self, t):
+        return 'componentType = %s' % self.generate_expr(t.type_decl)
+
+
+def generate_pyasn1(sema_module, out_stream):
+    return Pyasn1Backend(sema_module, out_stream).generate_code()
+
+
+# Translation tables from ASN.1 primitives to pyasn1 primitives
+_ASN1_TAG_CONTEXTS = {
+    'APPLICATION': 'tag.tagClassApplication',
+    'PRIVATE': 'tag.tagClassPrivate',
+    'UNIVERSAL': 'tag.tagClassUniversal'
+}
+
+
+_ASN1_BUILTIN_VALUES = {
+    'FALSE': '0',
+    'TRUE': '1'
+}
+
+
+_ASN1_BUILTIN_TYPES = {
+    'INTEGER': 'univ.Integer',
+    'BOOLEAN': 'univ.Boolean',
+    'NULL': 'univ.Null',
+    'ENUMERATED': 'univ.Enumerated',
+    'REAL': 'univ.Real',
+    'BIT STRING': 'univ.BitString',
+    'OCTET STRING': 'univ.OctetString',
+    'CHOICE': 'univ.Choice',
+    'SEQUENCE': 'univ.Sequence',
+    'SEQUENCE OF': 'univ.SequenceOf',
+    'UTF8String': 'char.UTF8String'
+}
+
+
+def _translate_type(type_name):
+    """ Translate ASN.1 built-in types to pyasn1 equivalents.
+    Non-builtins are not translated.
+    """
+    return _ASN1_BUILTIN_TYPES.get(type_name, type_name)
+
+
+def _translate_tag_class(tag_class):
+    """ Translate ASN.1 tag class names to pyasn1 equivalents.
+    Defaults to tag.tagClassContext if tag_class is not
+    recognized.
+    """
+    return _ASN1_TAG_CONTEXTS.get(tag_class, 'tag.tagClassContext')
+
+
+def _translate_value(value):
+    """ Translate ASN.1 built-in values to Python equivalents.
+    Unrecognized values are not translated.
+    """
+    return _ASN1_BUILTIN_VALUES.get(value, value)
+
+
+# Simplistic command-line driver
+def main(args):
+    with open(args[0]) as f:
+        asn1def = f.read()
+
+    parse_tree = parser.parse_asn1(asn1def)
+
+    modules = build_semantic_model(parse_tree)
+    if len(modules) > 1:
+        print('WARNING: More than one module generated to the same stream.', file=sys.stderr)
+
+    for module in modules:
+        print(pygen.auto_generated_header())
+        generate_pyasn1(module, sys.stdout)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/sema.py b/sema.py
new file mode 100644 (file)
index 0000000..82eeb01
--- /dev/null
+++ b/sema.py
@@ -0,0 +1,429 @@
+# Copyright (c) 2013, Schneider Electric Buildings AB
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of Schneider Electric Buildings AB nor the
+#       names of contributors may be used to endorse or promote products
+#       derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from asn1ate import parser
+
+
+def build_semantic_model(parse_result):
+    """ Build a semantic model of the ASN.1 definition
+    from a syntax tree generated by asn1ate.parser.
+    """
+    root = []
+    for token in parse_result:
+        _assert_annotated_token(token)
+        root.append(_create_sema_node(token))
+
+    return root
+
+
+def topological_sort(decls):
+    """ Algorithm adapted from:
+    http://en.wikipedia.org/wiki/Topological_sorting.
+
+    Assumes decls is an iterable of items with two members:
+    - type_name
+    - imports() method that returns an iterable of type names
+    upon which the decl depends.
+    """
+    graph = dict((d.type_name, set(d.imports())) for d in decls)
+
+    def has_predecessor(node):
+        for predecessor in graph.keys():
+            if node in graph[predecessor]:
+                return True
+
+        return False
+
+    # Build a topological order of type names
+    topological_order = []
+    roots = [type_name for type_name in graph.keys() if not has_predecessor(type_name)]
+
+    while roots:
+        root = roots.pop()
+
+        # Remove the current node from the graph
+        # and collect all new roots (the nodes that
+        # were previously only referenced from n)
+        successors = graph.pop(root, set())
+        roots.extend(successor for successor in successors if not has_predecessor(successor))
+
+        topological_order.insert(0, root)
+
+    if graph:
+        raise Exception('Can\'t sort cyclic dependencies: %s' % graph)
+
+    # Sort the actual decls based on the topological order
+    return sorted(decls, key=lambda d: topological_order.index(d.type_name))
+
+
+class Module(object):
+    def __init__(self, elements):
+        module_reference, _, _, _, module_body, _ = elements
+        _assert_annotated_token(module_reference)
+        _assert_annotated_token(module_body)
+
+        self.name = module_reference.elements[0]
+        self.declarations = [_create_sema_node(token) for token in module_body.elements]
+        self._user_types = {}
+
+    def user_types(self):
+        if not self._user_types:
+            # Index all type assignments by name
+            for user_defined in self.declarations:
+                self._user_types[user_defined.type_name] = user_defined.type_decl
+
+        return self._user_types
+
+    def resolve_type_decl(self, type_decl):
+        """ Recursively resolve user-defined types to their
+        built-in declaration.
+        """
+        user_types = self.user_types()
+
+        if isinstance(type_decl, UserDefinedType):
+            return self.resolve_type_decl(user_types[type_decl.type_name])
+        else:
+            return type_decl
+
+
+    def __str__(self):
+        return '%s DEFINITIONS ::=\n' % self.name \
+            + 'BEGIN\n' \
+            + '\n'.join(map(str, self.declarations)) + '\n' \
+            + 'END\n'
+
+    __repr__ = __str__
+
+
+class TypeAssignment(object):
+    def __init__(self, elements):
+        assert(len(elements) == 3)
+        type_name, _, type_decl = elements
+        self.type_name = type_name
+        self.type_decl = _create_sema_node(type_decl)
+
+    def imports(self):
+        return self.type_decl.imports()
+
+    def __str__(self):
+        return '%s ::= %s' % (self.type_name, self.type_decl)
+
+    __repr__ = __str__
+
+
+class ConstructedType(object):
+    def __init__(self, elements):
+        kind, component_tokens = elements
+        self.type_name = kind
+        self.components = [_create_sema_node(token) for token in component_tokens]
+
+    def imports(self):
+        imports = []
+        for component in self.components:
+            imports.extend(component.imports())
+        return imports
+
+    def __str__(self):
+        component_type_list = ', '.join(map(str, self.components))
+        return '%s { %s }' % (self.type_name, component_type_list)
+
+    __repr__ = __str__
+
+
+class ChoiceType(ConstructedType):
+    def __init__(self, elements):
+        super(ChoiceType, self).__init__(elements)
+
+class SequenceType(ConstructedType):
+    def __init__(self, elements):
+        super(SequenceType, self).__init__(elements)
+
+class SequenceOfType(object):
+    def __init__(self, elements):
+        sequenceof, type_token = elements
+        self.type_name = sequenceof
+        self.type_decl = _create_sema_node(type_token)
+
+    def imports(self):
+        return self.type_decl.imports()
+
+    def __str__(self):
+        return '%s %s' % (self.type_name, self.type_decl)
+
+    __repr__ = __str__
+
+
+class TaggedType(object):
+    def __init__(self, elements):
+        self.class_name = None
+        self.class_number = None
+        self.implicit = False
+
+        tag_token = elements[0]
+        if type(elements[1]) is parser.AnnotatedToken:
+            type_token = elements[1]
+        else:
+            self.implicit = elements[1] == 'IMPLICIT'
+            type_token = elements[2]
+
+        for tag_element in tag_token.elements:
+            if tag_element.ty == 'TagClassNumber':
+                self.class_number = tag_element.elements[0]
+            elif tag_element.ty == 'TagClass':
+                self.class_name = tag_element.elements[0]
+            else:
+                assert False, 'Unknown tag element: %s' % tag_element
+
+        self.type_decl = _create_sema_node(type_token)
+
+    @property
+    def type_name(self):
+        return self.type_decl.type_name
+
+    def imports(self):
+        return self.type_decl.imports()
+
+    def __str__(self):
+        class_spec = []
+        if self.class_name:
+            class_spec.append(self.class_name)
+        class_spec.append(self.class_number)
+
+        result = '[%s] ' % ' '.join(class_spec)
+        if self.implicit:
+            result += 'IMPLICIT '
+
+        result += str(self.type_decl)
+
+        return result
+
+    __repr__ = __str__
+
+
+class SimpleType(object):
+    def __init__(self, elements):
+        self.constraint = None
+        self.type_name = elements[0]
+        if len(elements) > 1 and elements[1].ty == 'Constraint':
+            self.constraint = Constraint(elements[1].elements)
+
+    def imports(self):
+        return [self.type_name]
+
+    def __str__(self):
+        if self.constraint is None:
+            return self.type_name
+
+        return '%s %s' % (self.type_name, self.constraint)
+
+    __repr__ = __str__
+
+
+class UserDefinedType(object):
+    def __init__(self, elements):
+        self.type_name = elements[0]
+
+    def imports(self):
+        return [self.type_name]
+
+    def __str__(self):
+        return self.type_name
+
+    __repr__ = __str__
+
+
+class Constraint(object):
+    def __init__(self, elements):
+        min_value, max_value = elements
+        self.min_value = min_value
+        self.max_value = max_value
+
+    def __str__(self):
+        return '(%s..%s)' % (self.min_value, self.max_value)
+
+    __repr__ = __str__
+
+
+class ComponentType(object):
+    def __init__(self, elements):
+        first_token = elements[0]
+        if first_token.ty == 'Type':
+            # an unnamed member
+            type_token = first_token
+            self.identifier = _get_next_unnamed()
+            elements = elements[1:]
+        elif first_token.ty == 'Identifier':
+            # an identifier
+            self.identifier = first_token.elements[0]
+            type_token = elements[1]
+            elements = elements[2:]
+
+        self.optional = elements and elements[0].ty == 'ComponentOptional'
+
+        if elements and elements[0].ty == 'ComponentDefault':
+            default_spec = elements[0]
+            assert default_spec.elements[0] == 'DEFAULT'
+            self.default_value = default_spec.elements[1]
+        else:
+            self.default_value = None
+
+        self.type_decl = _create_sema_node(type_token)
+
+    def imports(self):
+        return self.type_decl.imports()
+
+    def __str__(self):
+        result = '%s %s' % (self.identifier, self.type_decl)
+        if self.optional:
+            result += ' OPTIONAL'
+
+        if not self.default_value is None:
+            result += ' DEFAULT ' + self.default_value
+        
+        return result
+
+    __repr__ = __str__
+
+
+class NamedType(object):
+    def __init__(self, elements):
+        assert(elements[0].ty == 'Identifier')
+        assert(elements[1].ty == 'Type')
+        self.identifier = elements[0].elements[0]
+        self.type_decl = _create_sema_node(elements[1])
+
+    def imports(self):
+        return self.type_decl.imports()
+
+    def __str__(self):
+        return '%s %s' % (self.identifier, self.type_decl)
+
+    __repr__ = __str__
+
+
+class ValueListType(object):
+    def __init__(self, elements):
+        self.type_name = elements[0]
+        if len(elements) > 1:
+            self.named_values = [_create_sema_node(token) for token in elements[1]]
+        else:
+            self.named_values = None
+
+    def imports(self):
+        return []
+
+    def __str__(self):
+        if self.named_values:
+            named_value_list = ', '.join(map(str, self.named_values))
+            return '%s { %s }' % (self.type_name, named_value_list)
+        else:
+            return '%s' % self.type_name
+
+    __repr__ = __str__
+
+
+class BitStringType(object):
+    def __init__(self, elements):
+        self.type_name = elements[0]
+        if len(elements) > 1:
+            self.named_bits = [_create_sema_node(token) for token in elements[1]]
+        else:
+            self.named_bits = None
+
+    def imports(self):
+        return []
+
+    def __str__(self):
+        if self.named_bits:
+            named_bit_list = ', '.join(map(str, self.named_bits))
+            return '%s { %s }' % (self.type_name, named_bit_list)
+        else:
+            return '%s' % self.type_name
+
+    __repr__ = __str__
+
+
+class NamedValue(object):
+    def __init__(self, elements):
+        identifier_token, value_token = elements
+        self.identifier = identifier_token.elements[0]
+        self.value = value_token.elements[0]
+
+    def imports(self):
+        return []
+
+    def __str__(self):
+        return '%s (%s)' % (self.identifier, self.value)
+
+    __repr__ = __str__
+
+
+def _create_sema_node(token):
+    _assert_annotated_token(token)
+
+    if token.ty == 'ModuleDefinition':
+        return Module(token.elements)
+    elif token.ty == 'TypeAssignment':
+        return TypeAssignment(token.elements)
+    elif token.ty == 'ComponentType':
+        return ComponentType(token.elements)
+    elif token.ty == 'NamedType':
+        return NamedType(token.elements)
+    elif token.ty == 'ValueListType':
+        return ValueListType(token.elements)
+    elif token.ty == 'BitStringType':
+        return BitStringType(token.elements)
+    elif token.ty == 'NamedValue':
+        return NamedValue(token.elements)
+    elif token.ty == 'Type':
+        # Type tokens have a more specific type category
+        # embedded as their first element
+        return _create_sema_node(token.elements[0])
+    elif token.ty == 'SimpleType':
+        return SimpleType(token.elements)
+    elif token.ty == 'ReferencedType':
+        return UserDefinedType(token.elements)
+    elif token.ty == 'TaggedType':
+        return TaggedType(token.elements)
+    elif token.ty == 'SequenceType':
+        return SequenceType(token.elements)
+    elif token.ty == 'ChoiceType':
+        return ChoiceType(token.elements)
+    elif token.ty == 'SequenceOfType':
+        return SequenceOfType(token.elements)
+
+    raise Exception('Unknown token type: %s' % token.ty)
+
+
+def _assert_annotated_token(obj):
+    assert(type(obj) is parser.AnnotatedToken)
+
+
+# HACK: Generate unique names for unnamed members
+_unnamed_counter = 0
+def _get_next_unnamed():
+    global _unnamed_counter
+    _unnamed_counter += 1
+    return 'unnamed%d' % _unnamed_counter
diff --git a/support/__init__.py b/support/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/support/pygen.py b/support/pygen.py
new file mode 100644 (file)
index 0000000..91388a6
--- /dev/null
@@ -0,0 +1,97 @@
+# Copyright (c) 2013, Schneider Electric Buildings AB
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of Schneider Electric Buildings AB nor the
+#       names of contributors may be used to endorse or promote products
+#       derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+try:
+    # Python 2
+    from cStringIO import StringIO
+except ImportError:
+    # Python 3
+    from io import StringIO
+
+from datetime import datetime
+
+
+def auto_generated_header():
+    return '# Auto-generated by asn1ate on %s' % datetime.now()
+
+
+class NullBackend(object):
+    """ Code generator to create an empty file.
+    Used to create __init__.py files.
+    """
+    def __init__(self, *args):
+        pass
+
+    def generate_code(self, *args):
+        pass
+
+
+class PythonWriter(object):
+    """ Indentation-aware text stream. """
+    def __init__(self, out_stream, indent_size=4):
+        self.out = out_stream
+        self.indent_size = indent_size
+        self.current_indent = 0
+
+    def push_indent(self):
+        self.current_indent += self.indent_size
+
+    def pop_indent(self):
+        self.current_indent -= self.indent_size
+
+    def write_line(self, line):
+        if line is not None:
+            line = self._indent(line) if line else line
+            self.out.write('%s\n' % line)
+
+    def write_blanks(self, count=1):
+        for i in range(0, count):
+            self.out.write('\n')
+
+    def write_block(self, block):
+        """ Reindents after every line break. """
+        block = block.rstrip()
+        for line in block.split('\n'):
+            self.write_line(line)
+
+    def write_enumeration(self, items):
+        self.write_block(',\n'.join(items))
+
+    def get_fragment(self):
+        return PythonFragment(self.indent_size)
+
+    def _indent(self, line):
+        return ' ' * self.current_indent + line
+
+
+class PythonFragment(PythonWriter):
+    """ A buffering python writer, useful for nested structures.
+    """
+    def __init__(self, indent_size=4):
+        self.buf = StringIO()
+        super(PythonFragment, self).__init__(self.buf, indent_size)
+
+    def __str__(self):
+        return self.buf.getvalue()