232 lines
7.6 KiB
Python
Executable File
232 lines
7.6 KiB
Python
Executable File
#!/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:])
|