~launchpad-pqm/launchpad/devel

9492.1.1 by Karl Fogel
Add utilities/formatdoctest.py and utilities/migrater/, both brought
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Utilities to move zcml to a new location in the tree."""
5
6
__metaclass__ = type
7
8
__all__ = [
9
    'handle_zcml',
10
    ]
11
12
import os
13
import re
14
15
from find import find_matches
16
from lxml import etree
17
from rename_module import (
18
    bzr_add, bzr_has_filename, bzr_move_file, bzr_remove_file)
19
20
21
EMPTY_ZCML = """\
22
<configure
23
    xmlns="http://namespaces.zope.org/zope"
24
    xmlns:browser="http://namespaces.zope.org/browser"
25
    xmlns:i18n="http://namespaces.zope.org/i18n"
26
    xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
27
    i18n_domain="launchpad"></configure>"""
28
29
namespaces = {
30
    'zope': 'http://namespaces.zope.org/zope',
31
    'browser': 'http://namespaces.zope.org/browser',
32
    }
33
34
35
def handle_zcml(
36
    app_name, old_top, new_top, app_files, app_members, not_moved):
37
    """Migrate the file if it is ZCML."""
38
    new_path = os.path.join(new_top, app_name)
39
    zcml_files = [path for path in app_files if path.endswith('.zcml')]
40
    for lib_path in zcml_files:
41
        old_path = os.path.join(old_top, lib_path)
42
        if not os.path.exists(old_path):
43
            print "Processing  %s" % old_path
44
            continue
45
        migrate_zcml(old_top, old_path, new_path)
46
        dummy, file_name = os.path.split(old_path)
47
        rewrite_zcml_class_paths(
48
            old_top, new_path, file_name, app_members, app_name)
49
        create_browser_zcml(app_name, new_path, file_name, app_members)
50
        not_moved.remove(lib_path)
51
    package_names = consolidate_app_zcml(app_name, new_path)
52
    register_zcml(package_names, old_top)
53
54
55
def migrate_zcml(old_top, old_path, new_path):
56
    """Move the ZCML into the new tree and reorganise it."""
57
    bzr_move_file(old_path, new_path)
58
    print '%s => %s' % (old_path, new_path)
59
    # Remove the old registration.
60
    old_zcml_dir = os.path.join(old_top, 'zcml')
61
    delete_pattern = r'^.*"%s".*\n' % os.path.basename(old_path)
62
    for dummy in find_matches(
63
        old_zcml_dir, 'configure.zcml', delete_pattern, substitution=''):
64
        print "    Removed old configure include."
65
66
67
def rewrite_zcml_class_paths(old_top, new_path,
68
                             file_name, app_members, app_name):
69
    """Rewrite app classes to use relative paths."""
70
    abs_lib = 'canonical\.launchpad'
71
    module_name, dummy = os.path.splitext(file_name)
72
    for package_name in app_members:
73
        try:
74
            members = '|'.join(app_members[package_name][module_name])
75
        except KeyError:
76
            old_module_path = os.path.join(
77
                old_top, package_name, '%s.py' % module_name)
78
            if os.path.isfile(old_module_path):
79
                print '    ** missing %s.%s' % (package_name, module_name)
80
            continue
81
        module_pattern = r'\b%s(\.%s\.)(?:\w*\.)?(%s)\b' % (
82
            abs_lib, package_name, members)
83
        substitution = r'lp.%s\1%s.\2' % (app_name, module_name)
84
        for dummy in find_matches(
85
            new_path, file_name, module_pattern, substitution=substitution):
86
            # This function is an iterator, but we do not care about the
87
            # summary of what is changed.
88
            pass
89
    # Update the menu and navigation directives.
90
    module_pattern = r'module="canonical.launchpad.browser"'
91
    substitution = r'module="lp.%s.browser.%s"' % (app_name, module_name)
92
    for dummy in find_matches(
93
        new_path, file_name, module_pattern, substitution=substitution):
94
        pass
95
96
97
def create_browser_zcml(app_name, new_path, file_name, app_members):
98
    """Extract browser ZCML to the browser/ directory."""
99
    module_name, dummy = os.path.splitext(file_name)
100
    browser_module_name = '.browser.%s' % module_name
101
    new_file_path = os.path.join(new_path, file_name)
102
    source = open(new_file_path)
103
    try:
104
        parser = etree.XMLParser(remove_blank_text=True)
105
        tree = etree.parse(source, parser)
106
    finally:
107
        source.close()
108
    doc = tree.getroot()
109
    browser_doc = etree.fromstring(EMPTY_ZCML)
110
    # Move the facet browser directives first.
111
    for facet_node in doc.xpath('./zope:facet', namespaces=namespaces):
112
        facet = facet_node.get('facet')
113
        browser_facet = etree.Element('facet', facet=facet)
114
        for node in facet_node.xpath('./browser:*', namespaces=namespaces):
115
            facet_node.remove(node)
116
            browser_facet.append(node)
117
        if len(browser_facet) > 0:
118
            browser_doc.append(browser_facet)
119
    # All remaining browser directives can be moved.
120
    for node in doc.xpath('./browser:*', namespaces=namespaces):
121
        doc.remove(node)
122
        browser_doc.append(node)
123
    # Split the menus directive into two nodes, one for the this app, and
124
    # one for the old Launchpad app.
125
    module_class_path = "lp.%s.browser.%s" % (app_name, module_name)
126
    other_class_path = "canonical.launchpad.browser"
127
    for node in browser_doc.xpath('./browser:menus', namespaces=namespaces):
128
        app_menus = []
129
        other_menus = []
130
        module_members = app_members['browser'][module_name]
131
        for menu in node.get('classes').split():
132
            if menu in module_members:
133
                app_menus.append(menu)
134
            else:
135
                other_menus.append(menu)
136
        if len(other_menus) > 0:
137
            browser_doc.append(
138
                create_menu_node(other_class_path, other_menus))
139
        if len(app_menus) > 0:
140
            browser_doc.append(
141
                create_menu_node(module_class_path, app_menus))
142
        browser_doc.remove(node)
143
    # Only save the browser and other doc if information was put into them.
144
    if len(browser_doc) > 0:
145
        file_path = os.path.join(new_path, 'browser', file_name)
146
        write_zcml_file(file_path, browser_doc)
147
        write_zcml_file(new_file_path, doc)
148
149
150
def create_menu_node(module, menus):
151
    """Create a browser:menus node for the module and list of menu classes."""
152
    menus_tag = '{%s}menus' % namespaces['browser']
153
    classes = ' '.join(menus)
154
    return etree.Element(
155
        menus_tag, module=module, classes=classes, nsmap=namespaces)
156
157
158
def write_zcml_file(file_path, doc):
159
    """Write the zcml file to its location."""
160
    xml = format_xml(etree.tostring(doc, pretty_print=True))
161
    browser_file = open(file_path, 'w')
162
    try:
163
        browser_file.write(xml)
164
    finally:
165
        browser_file.close()
166
167
168
def format_xml(xml):
169
    """Format the xml for pretty printing."""
170
    lines = []
171
    leading_pattern = re.compile(r'^( *)<')
172
    attribute_pattern = re.compile(r' ([\w:]+=)')
173
    open_comment_pattern = re.compile(r'(<!--)')
174
    close_comment_pattern = re.compile(r'(-->)')
175
    classes_pattern = re.compile(
176
        r'( +)(classes|attributes)(=")([^"]+)')
177
    trailing_whitespace_pattern = re.compile(r'\s+$')
178
    for line in xml.splitlines():
179
        match = leading_pattern.match(line)
180
        if match is None:
181
            leading = ''
182
        else:
183
            leading = match.group(1)
184
            if len(leading) > 8:
185
                # lxml does not normalise whitespace between closing tags.
186
                leading = '        '
187
                line = leading + line.strip()
188
        line = open_comment_pattern.sub(r'\n%s\1' % leading, line)
189
        line = close_comment_pattern.sub(r'\1\n%s' % leading, line)
190
        line = attribute_pattern.sub(r'\n  %s\1' % leading, line)
191
        classes = classes_pattern.search(line)
192
        if classes is not None:
193
            indent = classes.group(1)
194
            modules = classes.group(4).split()
195
            module_indent = '\n  %s' % indent
196
            markup = r'\1\2\3' + module_indent + module_indent.join(modules)
197
            line = classes_pattern.sub(markup, line)
198
        lines.append(line)
199
    xml = '\n'.join(lines)
200
    xml = trailing_whitespace_pattern.sub('', xml)
201
    xml = xml.replace('  ', '    ')
202
    return xml
203
204
205
def consolidate_app_zcml(app_name, dir_path):
206
    """Consolidate the all the app zcml into configure.zcml."""
207
    consolidate_zcml(dir_path)
208
    consolidate_zcml(os.path.join(dir_path, 'browser'))
209
    # This function should also create the coop/<module>.zcml files and
210
    # build a list of each one so that they can be registered. The registry
211
    # doesn't have coop files, so it just returns a list of its own name.
212
    return [app_name]
213
214
215
def consolidate_zcml(dir_path):
216
    """Consolidate the directory zcml into configure.zcml."""
217
    all_lines = []
218
    if os.path.isdir(os.path.join(dir_path, 'browser')):
219
        # This configure.zcml must include the browser package.
220
        all_lines.append('\n    <include package=".browser" />')
221
    converted_zcml = []
222
    for file_name in os.listdir(dir_path):
223
        if not file_name.endswith('.zcml') or file_name == 'configure.zcml':
224
            # This is not a single zcml file.
225
            continue
226
        file_path = os.path.join(dir_path, file_name)
227
        zcml_file = open(file_path)
228
        in_root = False
229
        after_root = False
230
        try:
231
            for line in zcml_file:
232
                if '</configure>' not in line and after_root:
233
                    all_lines.append(line)
234
                    continue
235
                if not in_root and '<configure' in line:
236
                    in_root = True
237
                if in_root and '>' in line:
238
                    after_root = True
239
        finally:
240
            zcml_file.close()
241
        converted_zcml.append(file_path)
242
    configure_xml = EMPTY_ZCML.replace('><', '>%s<' % ''.join(all_lines))
243
    configure_path = os.path.join(dir_path, 'configure.zcml')
244
    if os.path.isfile(configure_path):
245
        configure_path = configure_path + '.extra'
246
        print '    ** Warning %s must be reconciled with configure.zcml ' % (
247
            configure_path)
248
    parser = etree.XMLParser(remove_blank_text=True)
249
    doc = etree.fromstring(configure_xml, parser=parser)
250
    write_zcml_file(configure_path, doc)
251
    bzr_add([configure_path])
252
    for file_path in converted_zcml:
253
        if bzr_has_filename(file_path):
254
            bzr_remove_file(file_path)
255
        else:
256
            os.remove(file_path)
257
258
259
def register_zcml(package_names, old_top):
260
    """Register the new zcml in Launchpad's config."""
261
    # Package names could be like: ['answers', 'coop.answersbugs']
262
    for package_name in package_names:
263
        include = r'<include package="lp\.%s" />' % package_name
264
        unregistered = False
265
        for dummy in find_matches(old_top, 'configure.zcml', include):
266
            # The module is already registered.
267
            unregistered = True
268
        if unregistered:
269
            continue
270
        insert_after = r'(<include package="canonical.launchpad.xmlrpc" />)'
271
        include = r'\1\n\n  <include package="lp.%s" />' % package_name
272
        for dummy in find_matches(
273
            old_top, 'configure.zcml', insert_after, substitution=include):
274
            pass
275
276
277
# Verify the formatter is sane.
278
if __name__ == '__main__':
279
    source = open('lib/lp/registry/browser/configure.zcml')
280
    try:
281
        parser = etree.XMLParser(remove_blank_text=True)
282
        tree = etree.parse(source, parser)
283
    finally:
284
        source.close()
285
    doc = tree.getroot()
286
    write_zcml_file('./test.xml', doc)