oca-ocb-core/odoo-bringout-oca-ocb-base/odoo/cli/command.py
Ernad Husremovic 991d2234ca 19.0 vanilla
2025-10-03 18:07:25 +02:00

139 lines
4.2 KiB
Python

import argparse
import contextlib
import re
import sys
from inspect import cleandoc
from pathlib import Path
import odoo.init # import first for core setup
import odoo.cli
from odoo.modules import initialize_sys_path, load_script
from odoo.tools import config
COMMAND_NAME_RE = re.compile(r'^[a-z][a-z0-9_]*$', re.I)
PROG_NAME = Path(sys.argv[0]).name
commands = {}
"""All loaded commands"""
class Command:
name = None
description = None
epilog = None
_parser = None
def __init_subclass__(cls):
cls.name = cls.name or cls.__name__.lower()
module = cls.__module__.rpartition('.')[2]
if not cls.is_valid_name(cls.name):
raise ValueError(
f"Command name {cls.name!r} "
f"must match {COMMAND_NAME_RE.pattern!r}")
if cls.name != module:
raise ValueError(
f"Command name {cls.name!r} "
f"must match Module name {module!r}")
commands[cls.name] = cls
@property
def prog(self):
return f"{PROG_NAME} [--addons-path=PATH,...] {self.name}"
@property
def parser(self):
if not self._parser:
self._parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
prog=self.prog,
description=cleandoc(self.description or self.__doc__ or ""),
epilog=cleandoc(self.epilog or ""),
)
return self._parser
@classmethod
def is_valid_name(cls, name):
return re.match(COMMAND_NAME_RE, name)
def load_internal_commands():
""" Load ``commands`` from ``odoo.cli`` """
for path in odoo.cli.__path__:
for module in Path(path).iterdir():
if module.suffix != '.py':
continue
__import__(f'odoo.cli.{module.stem}')
def load_addons_commands(command=None):
"""
Search the addons path for modules with a ``cli/{command}.py`` file.
In case no command is provided, discover and load all the commands.
"""
if command is None:
command = '*'
elif not Command.is_valid_name(command):
return
mapping = {}
initialize_sys_path()
for path in odoo.addons.__path__:
for fullpath in Path(path).glob(f'*/cli/{command}.py'):
if (found_command := fullpath.stem) and Command.is_valid_name(found_command):
# loading as odoo.cli and not odoo.addons.{module}.cli
# so it doesn't load odoo.addons.{module}.__init__
mapping[f'odoo.cli.{found_command}'] = fullpath
for fq_name, fullpath in mapping.items():
with contextlib.suppress(ImportError):
load_script(fullpath, fq_name)
def find_command(name: str) -> Command | None:
""" Get command by name. """
# built-in commands
if command := commands.get(name):
return command
# import from odoo.cli
with contextlib.suppress(ImportError):
__import__(f'odoo.cli.{name}')
return commands[name]
# import from odoo.addons.*.cli
load_addons_commands(command=name)
return commands.get(name)
def main():
args = sys.argv[1:]
# The only shared option is '--addons-path=' needed to discover additional
# commands from modules
if len(args) > 1 and args[0].startswith('--addons-path=') and not args[1].startswith('-'):
# parse only the addons-path, do not setup the logger...
config._parse_config([args[0]])
args = args[1:]
if len(args) and not args[0].startswith('-'):
# Command specified, search for it
command_name = args[0]
args = args[1:]
elif '-h' in args or '--help' in args:
# No command specified, but help is requested
command_name = 'help'
args = [x for x in args if x not in ('-h', '--help')]
else:
# No command specified, default command used
command_name = 'server'
if command := find_command(command_name):
odoo.cli.COMMAND = command_name
command().run(args)
else:
message = (
f"Unknown command {command_name!r}.\n"
f"Use '{PROG_NAME} --help' to see the list of available commands."
)
sys.exit(message)