|
- #!/usr/bin/env python3
- """
- Rewrite the entire source code using the scripts found at
- /odoo/upgrade_code
-
- Each script is named {version}-{name}.py and exposes an upgrade function
- that takes a single argument, the file_manager, and returns nothing.
-
- The file_manager acts as a list of files, files have 3 attributes:
- * path: the pathlib.Path where the file is on the file system;
- * addon: the odoo addon in which the file is;
- * content: the re-writtable content of the file (lazy).
-
- There are additional utilities on the file_manager, such as:
- * print_progress(current, total)
-
- Example:
-
- def upgrade(file_manager):
- files = [f for f in file_manager if f.path.suffix == '.py']
- for fileno, file in enumerate(files, start=1):
- file.content = file.content.replace(..., ...)
- file_manager.print_progress(fileno, len(files))
-
- The command line offers a way to select and run those scripts.
-
- Please note that all the scripts are doing a best-effort a migrating the
- source code, they only help do the heavy-lifting, they are not silver
- bullets.
- """
-
- import argparse
- import sys
-
- from importlib.machinery import SourceFileLoader
- from pathlib import Path
- from types import ModuleType
- from typing import Iterator
-
- ROOT = Path(__file__).parent.parent
-
- try:
- import odoo.addons
- from . import Command
- from odoo import release
- from odoo.modules import initialize_sys_path
- from odoo.tools import config, parse_version
- except ImportError:
- # Assume the script is directy executed (by opposition to be
- # executed via odoo-bin), happily release/parse_version are
- # standalone so we can hack our way there without importing odoo
- sys.path.insert(0, str(ROOT))
- sys.path.insert(0, str(ROOT / 'tools'))
- import release
- from parse_version import parse_version
- class Command:
- pass
- config = {'addons_path': ''}
- initialize_sys_path = None
-
-
- UPGRADE = ROOT / 'upgrade_code'
- AVAILABLE_EXT = ('.py', '.js', '.css', '.scss', '.xml', '.csv')
-
-
- class FileAccessor:
- addon: Path
- path: Path
- content: str
-
- def __init__(self, path: Path, addon_path: Path) -> None:
- self.path = path
- self.addon = addon_path / path.relative_to(addon_path).parts[0]
- self._content = None
- self.dirty = False
-
- @property
- def content(self):
- if self._content is None:
- self._content = self.path.read_text()
- return self._content
-
- @content.setter
- def content(self, value):
- if self._content != value:
- self._content = value
- self.dirty = True
-
-
- class FileManager:
- addons_path: list[str]
- glob: str
-
- def __init__(self, addons_path: list[str], glob: str = '**/*') -> None:
- self.addons_path = addons_path
- self.glob = glob
- self._files = {
- str(path): FileAccessor(path, Path(addon_path))
- for addon_path in addons_path
- for path in Path(addon_path).glob(glob)
- if '__pycache__' not in path.parts
- if path.suffix in AVAILABLE_EXT
- if path.is_file()
- }
-
- def __iter__(self) -> Iterator[FileAccessor]:
- return iter(self._files.values())
-
- def __len__(self):
- return len(self._files)
-
- def get_file(self, path):
- return self._files.get(str(path))
-
- if sys.stdout.isatty():
- def print_progress(self, current, total=None):
- total = total or len(self) or 1
- print(f'{current / total:>4.0%}', end='\r', file=sys.stderr) # noqa: T201
- else:
- def print_progress(self, current, total=None):
- pass
-
-
- def get_upgrade_code_scripts(from_version: tuple[int, ...], to_version: tuple[int, ...]) -> list[tuple[str, ModuleType]]:
- modules: list[tuple[str, ModuleType]] = []
- for script_path in sorted(UPGRADE.glob('*.py')):
- version = parse_version(script_path.name.partition('-')[0])
- if from_version <= version <= to_version:
- module = SourceFileLoader(script_path.name, str(script_path)).load_module()
- modules.append((script_path.name, module))
- return modules
-
-
- def migrate(
- addons_path: list[str],
- glob: str,
- from_version: tuple[int, ...] | None = None,
- to_version: tuple[int, ...] | None = None,
- script: str | None = None,
- dry_run: bool = False,
- ):
- if script:
- script_path = next(UPGRADE.glob(f'*{script.removesuffix(".py")}*.py'), None)
- if not script_path:
- raise FileNotFoundError(script)
- script_path.relative_to(UPGRADE) # safeguard, prevent going up
- module = SourceFileLoader(script_path.name, str(script_path)).load_module()
- modules = [(script_path.name, module)]
- else:
- modules = get_upgrade_code_scripts(from_version, to_version)
-
- file_manager = FileManager(addons_path, glob)
- for (name, module) in modules:
- file_manager.print_progress(0) # 0%
- module.upgrade(file_manager)
- file_manager.print_progress(len(file_manager)) # 100%
-
- for file in file_manager:
- if file.dirty:
- print(file.path) # noqa: T201
- if not dry_run:
- with file.path.open("w") as f:
- f.write(file.content)
-
- return any(file.dirty for file in file_manager)
-
-
- class UpgradeCode(Command):
- """ Rewrite the entire source code using the scripts found at /odoo/upgrade_code """
- name = 'upgrade_code'
- prog_name = Path(sys.argv[0]).name
-
- def __init__(self):
- self.parser = argparse.ArgumentParser(
- prog=(
- f"{self.prog_name} [--addons-path=PATH,...] {self.name}"
- if initialize_sys_path else
- self.prog_name
- ),
- description=__doc__.replace('/odoo/upgrade_code', str(UPGRADE)),
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- group = self.parser.add_mutually_exclusive_group(required=True)
- group.add_argument(
- '--script',
- metavar='NAME',
- help="run this single script")
- group.add_argument(
- '--from',
- dest='from_version',
- type=parse_version,
- metavar='VERSION',
- help="run all scripts starting from this version, inclusive")
- self.parser.add_argument(
- '--to',
- dest='to_version',
- type=parse_version,
- default=parse_version(release.version),
- metavar='VERSION',
- help=f"run all scripts until this version, inclusive (default: {release.version})")
- self.parser.add_argument(
- '--glob',
- default='**/*',
- help="select the files to rewrite (default: %(default)s)")
- self.parser.add_argument(
- '--dry-run',
- action='store_true',
- help="list the files that would be re-written, but rewrite none")
- self.parser.add_argument(
- '--addons-path',
- default=config['addons_path'],
- metavar='PATH,...',
- help="specify additional addons paths (separated by commas)",
- )
-
- def run(self, cmdargs):
- options = self.parser.parse_args(cmdargs)
- if initialize_sys_path:
- config['addons_path'] = options.addons_path
- initialize_sys_path()
- options.addons_path = odoo.addons.__path__
- else:
- options.addons_path = [p for p in options.addons_path.split(',') if p]
- if not options.addons_path:
- self.parser.error("--addons-path is required when used standalone")
- is_dirty = migrate(**vars(options))
- sys.exit(int(is_dirty))
-
-
- if __name__ == '__main__':
- UpgradeCode().run(sys.argv[1:])
|