Odoo18-Base/odoo/cli/upgrade_code.py

232 lines
7.6 KiB
Python
Raw Normal View History

2025-01-06 10:57:38 +07:00
#!/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:])