Commits

Albert Cervera i Areny (NaN·tic) committed 3fbf406

Initial commit.

First version of active record migration script and notes on the supported
changes.

  • Participants

Comments (0)

Files changed (2)

2.6/01-active-record.py

+#!/usr/bin/python
+
+import fnmatch
+import os
+import re
+import sys
+
+def get_module_name(filename):
+    if '.hg' in filename:
+        return None
+    if '/ir/' in filename:
+        return 'ir'
+    if '/webdav/' in filename:
+        return 'webdav'
+    if '/modules/' not in filename:
+        return None
+    c = re.compile('\/modules\/(\w+)')
+    m = c.search(filename)
+    g = m.group(1)
+    return g
+
+def get_modules():
+    modules = {}
+    all_py_files = []
+    all_xml_files = []
+    for root, dirnames, filenames in os.walk('.'):
+        current_module = None
+        for filename in filenames:
+            path = os.path.join(root, filename)
+            module = get_module_name(path)
+            if filename.endswith('.py'):
+                all_py_files.append(path)
+            if filename.endswith('.xml'):
+                all_xml_files.append(path)
+            if module:
+                module_path = path[:path.find(module)+len(module)]
+                if not module in modules:
+                    modules[module] = {
+                        'name': module,
+                        'path': module_path,
+                        'py_files': [],
+                        'xml_files': [],
+                        }
+                if filename.endswith('.py'):
+                    modules[module]['py_files'].append(path)
+                if filename.endswith('.xml'):
+                    modules[module]['xml_files'].append(path)
+    return modules
+
+def get_class_name(line):
+    if not line.startswith('class '):
+        return None
+    c = re.compile('class (\w+)')
+    m = c.search(line)
+    g = m.group(1)
+    return g
+
+def get_class_parents(line):
+    if not line.startswith('class '):
+        return []
+    c = re.compile('class \w+\((\w+)\)')
+    m = c.search(line)
+    if not m:
+        return []
+    g = m.group(1)
+    return [x.strip() for x in g.split(',')]
+
+def get_function_name(line):
+    line = line.strip()
+    if not line.startswith('def '):
+        return None
+    c = re.compile('def (\w+)')
+    m = c.search(line)
+    g = m.group(1)
+    return g
+
+def get_ident(line):
+    return get_ident_count(line) * ' '
+
+def get_ident_count(line):
+    count = 0
+    for x in line:
+        if x != ' ':
+            break
+        count += 1
+    return count
+
+def camelcase(text):
+    return ''.join([x.capitalize() for x in text.split('_')])
+
+def add_classmethod(result, line):
+    ident = get_ident(line)
+    result.append(ident + '@classmethod\n')
+
+def self_to_cls(line):
+    line = line.replace('self.', 'cls.')
+    line = line.replace('self)', 'cls)')
+    line = line.replace('(self,', '(cls,')
+    line = line.replace(' self,', ' cls,')
+    return line
+
+def add_staticmethod(result, line):
+    ident = get_ident(line)
+    result.append(ident + '@staticmethod\n')
+
+def self_to_static(line):
+    line = line.replace('(self)', '()')
+    return line
+
+def ids_to_records(line):
+    line = re.sub(r'self.browse\(ids\)', r'records', line)
+    line = re.sub(r'(\W)ids(\W)', r'\1records\2', line)
+    return line
+
+def split_lines(text):
+    """Converts the given text into a list of lines 
+    with \n appended at the end of each line."""
+    text = text.split('\n')
+    return ['%s\n' % x for x in text] 
+
+def add_before_after_imports(original_content, before=None, after=None):
+    if before is None:
+        before = []
+    if after is None:
+        after = []
+    result = []
+    count = 0
+    imports = 'before'
+    importing = False
+    imported = False
+    in_backslash = False
+    for line in original_content:
+        if not imported:
+            if (line.startswith('import ') or (line.startswith('from ') 
+                    and ' import ' in line)):
+                if not importing:
+                    result += before
+                importing = True
+                if line.strip().endswith('\\'):
+                    in_backslash = True
+            elif in_backslash:
+                if not line.strip().endswith('\\'):
+                    in_backslash = False
+            elif not in_backslash and importing and line.strip():
+                result += after
+                imported = True
+        result.append(line)
+    if not imported:
+        result += after
+    return result
+
+def to_pep8(text):
+    result = ''
+    remaining = text
+    ident = 0
+    while len(remaining) > 80:
+        r = remaining[:80]
+        pos = r.rfind(' ')
+        result += r[:pos] + '\n'
+        ident = (result.count('(') + result.count('[') + result.count('}') 
+            - result.count(')') - result.count(']') - result.count('}'))
+        remaining = (ident * 4 * ' ') + remaining[pos:].lstrip()
+    result += remaining
+    return result
+
+def update_file(module, filename):
+    result = []
+    classes = []
+    current_class = None
+    current_class_info = None
+    current_function = None
+    original_content = open(filename, 'r').readlines()
+    count = 0
+    for line in original_content:
+        if line.strip().startswith('#'):
+            result.append(line)
+            continue
+
+        # Remove _description
+        if line.strip() == '_description = __doc__':
+            continue
+
+        # Remove class instance
+        if line.startswith('%s()' % current_class):
+            continue
+
+        # Replace _name by __name__
+        if line.strip().startswith('_name = '):
+            result.append(re.sub(r'_name = (.*)', r'__name__ = \1', line))
+            continue
+
+        is_class_definition = False
+        is_function_definition = False
+        in_rpc = False
+        c = get_class_name(line)
+        if c:
+            is_class_definition = True
+            current_class = c
+            current_class_info = {
+                    'name': c,
+                    'parents': get_class_parents(line),
+                    }
+            classes.append(current_class_info)
+        f = get_function_name(line)
+        if f:
+            is_function_definition = True
+            current_function = f
+
+        # Replace __init__ by __setup__
+        if is_function_definition and current_function == '__init__':
+            add_classmethod(result, line)
+            line = line.replace('__init__', '__setup__')
+            current_function = '__setup__'
+
+        if current_function == '__setup__':
+            line = self_to_cls(line)
+            # Replace function name in super call
+            if '__init__' in line:
+                line = line.replace('__init__', '__setup__')
+            # Rename _rpc to __rpc__
+            if '._rpc' in line:
+                line = line.replace('._rpc', '.__rpc__')
+            # Change False to RPC() in __rpc__
+            if '__rpc__' in line and 'update(' in line:
+                in_rpc = True
+            # Remove _reset_columns()
+            if 'self._reset_columns()' in line:
+                continue
+
+        # Rename init to __register__
+        if is_function_definition and current_function == 'init':
+            add_classmethod(result, line)
+            line = line.replace('init', '__register__')
+            current_function = '__register__'
+
+        if current_function == '__register__':
+            line = self_to_cls(line)
+            # Replace function name in super call
+            if '.init(' in line:
+                line = line.replace('.init(', '.__register__(')
+
+        # Rename xxx_obj into Xxx
+        if '_obj' in line:
+            c = re.compile('(\w+)_obj')
+            m = c.search(line)
+            g = m.group(1)
+            line = line.replace('%s_obj' % g, camelcase(g))
+
+        # Convert search_rec_name to classmethod
+        if is_function_definition and current_function == 'search_rec_name':
+            add_classmethod(result, line)
+        if current_function == 'search_rec_name':
+            line = self_to_cls(line)
+
+        if is_function_definition and current_function.startswith('default_'):
+            add_staticmethod(result, line)
+        if current_function and current_function.startswith('default_'):
+            line = self_to_static(line)
+
+        if current_class and 'Wizard' in current_class_info['parents']:
+            if is_function_definition:
+                line = line.replace(', session', '')
+            else:
+                line = line.replace('session', 'self')
+
+        if is_function_definition and current_function.startswith('check_'):
+            add_classmethod(result, line)
+        if current_function and current_function.startswith('check_'):
+            line = ids_to_records(line)
+
+        result.append(line)
+
+    if classes and not '/tests/' in filename:
+        _all = (to_pep8('__all__ = %s' % [x['name'] for x in classes]) + '\n\n')
+        _all = _all.splitlines(True)
+        result = add_before_after_imports(result, after=_all)
+        if not 'classes' in module:
+            module['classes'] = []
+        module['classes'] += classes
+    if result != original_content:
+        f = open(filename, 'w')
+        f.write(''.join(result))
+        f.close()
+    return result
+
+def update_init(module, filename):
+    result = []
+    original_content = open(filename, 'r').readlines()
+    before = ['from trytond.pool import Pool\n']
+    classes = module['classes']
+    model_classes = [x['name'] for x in classes if 'Wizard' not in x['parents']]
+    wizard_classes = [x['name'] for x in classes if 'Wizard' in x['parents']]
+    after = ''
+    if model_classes:
+        after += (
+            '    Pool.register(\n'
+            '        ' + 
+            ',\n        '.join(model_classes) +
+            ',\n'
+            "        module='%s', type_='model')\n" % module['name']
+            )
+    if wizard_classes:
+        after += (
+            '    Pool.register(\n'
+            '        ' + 
+            ',\n        '.join(wizard_classes) +
+            ',\n'
+            "        module='%s', type_='wizard')\n" % module['name']
+            )
+    if after:
+        after = '\n\ndef register():\n' + after
+    after = after.splitlines(True)
+    result = add_before_after_imports(original_content, before, after)
+    if result != original_content:
+        f = open(filename, 'w')
+        f.write(''.join(result))
+        f.close()
+
+
+if __name__ == '__main__':
+    to_process = sys.argv[1:]
+
+    if not to_process:
+        print "Help: %s modules" % sys.argv[0]
+        sys.exit(1)
+
+    for module in get_modules().values():
+        if module['name'] not in to_process:
+            continue
+        print "Processing module '%s'" % module['name']
+        for filename in module['py_files']:
+            if filename == '__init__.py':
+                continue
+            result = update_file(module, filename)
+        update_init(module, os.path.join(module['path'], '__init__.py'))
+

2.6/01-active-record.rst

+- ** Define __all__ in each python file
+- ** Replace class instance by Pool.register in __init__.register() using the
+  same order
+- ** Remove _description
+- ** Rename _name into __name__
+- ** Replace __init__ by __setup__
+    - ** Rename _rpc into __rpc__ and 
+    - use RPC as value
+    - ** Remove _reset_columns
+
+- ** Replace init by __register__
+- ** Replace default_xxx method by classmethod or staticmethod (depending if
+  it uses self/cls)
+- Change constraint methods into classmethod with records as input or
+  instance method
+- Change on_change, on_change_with, autocomplete into instance method
+  without values
+- Change getter into:
+    - instance method that just return value
+    - classmethod with records that return a dict with values
+- Change setter into classmethod with records
+
+- ** Rename xxx_obj into Xxx
+- browse takes only list of ids, for int just instanciate the Model
+- create return a instance
+- write, delete are classmethod with list of instance and return nothing
+- read takes only list of ids, for int use this: value, = cls.read([id])
+- copy is a classmethod that takes only a list of instance and return a
+  list of instance
+- ** Replace session from wizard method by self
+- Use PoolMeta for extending Model
+- Cache method is replaced by a Cache instance on the class
+
+- Methods to simply replace by classmethod:
+    - ** search_rec_name
+
+- Methods to replace by classmethod with records:
+    - ** check_
+
+- search returns list of instance