Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions odev/commands/database/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class RunCommand(OdoobinTemplateCommand):
"""Run the odoo-bin process for the selected database locally.

The process is run in a python virtual environment depending on the database's odoo version (as defined
by the installed `base` module). The command takes care of installing and updating python requirements within
the virtual environment and fetching the latest sources in the odoo standard repositories, cloning them
Expand Down
14 changes: 6 additions & 8 deletions odev/commands/utilities/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,16 @@ def single_command_help(self) -> str:
parser = command.prepare_parser()
usage = escape(parser.format_usage().replace("usage:", executable).strip())

message_indent = 12
message = f"""
[bold {Colors.PURPLE}]{executable.upper()} {command._name.upper()}[/bold {Colors.PURPLE}]

{{command._description}}
{string.indent(command._description, message_indent)}

[bold][underline]Usage:[/underline] [{Colors.CYAN}]{usage}[/{Colors.CYAN}][/bold]
"""

message_indent = string.min_indent(message)
message_options_indent = message_indent + 4
description = string.indent(command._description, message_indent)[message_indent:]
message = message.replace("{command._description}", description)

if command._aliases:
aliases = f"""
Expand All @@ -87,7 +85,7 @@ def single_command_help(self) -> str:
positionals = f"""
[bold underline]Positional Arguments:[/bold underline]

{string.format_options_list(positional_arguments, message_options_indent)}
{string.indent(string.format_options_list(positional_arguments), message_options_indent)}
"""
message += string.dedent(positionals, message_options_indent - message_indent)

Expand All @@ -100,7 +98,7 @@ def single_command_help(self) -> str:
optionals = f"""
[bold underline]Optional Arguments:[/bold underline]

{string.format_options_list(optional_arguments, message_options_indent)}
{string.indent(string.format_options_list(optional_arguments), message_options_indent)}
"""
message += string.dedent(optionals, message_options_indent - message_indent)

Expand Down Expand Up @@ -147,14 +145,14 @@ def all_commands_help(self) -> str:
blanks=1,
),
message_indent,
)[message_indent:]
)

return f"""
{message.rstrip()}

[bold underline]The following commands are provided:[/bold underline]

{commands_list}
{commands_list}
"""

def command_names(self) -> str:
Expand Down
31 changes: 23 additions & 8 deletions odev/common/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,29 @@ def convert_arguments(cls) -> None:

cls._arguments[argument_name].update(**argument_dict)

# Re-order the internal dictionary to ensure *... arguments are last.
# This is necessary because Python dictionaries preserve insertion order and
# any argument defined in a subclass would otherwise be registered after
# a greedy catch-all argument defined in a parent class.
sorted_arguments = sorted(
cls._arguments.items(),
key=lambda item: 1 if item[1].get("nargs") == "*..." else 0,
)
# Re-order the internal dictionary to ensure *... arguments are last,
# all other arguments are sorted alphabetically for flags, but
# positional arguments MUST preserve their declaration order.
original_order = {name: i for i, name in enumerate(cls._arguments)}

def argument_sort_key(item):
name, arg_def = item
aliases = arg_def.get("aliases", [name])
is_optional = any(a.startswith("-") for a in aliases)

greedy = 1 if arg_def.get("nargs") == "*..." else 0
positional = 0 if not is_optional else 1

if is_optional:
# Prefer long aliases for sorting
long_aliases = [a.lstrip("-") for a in aliases if a.startswith("--")]
sort_name = min(long_aliases, key=len) if long_aliases else aliases[0].lstrip("-")
return (greedy, positional, sort_name.lower())

# For positional arguments, use the insertion order to preserve declaration sequence
return (greedy, positional, original_order[name])

sorted_arguments = sorted(cls._arguments.items(), key=argument_sort_key)
cls._arguments = defaultdict(dict, sorted_arguments)

@classmethod
Expand Down
18 changes: 9 additions & 9 deletions odev/common/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@
)

if __log_level:
LOG_LEVEL = str(__log_level.group(1)).upper().replace("-", "_")
remove = __log_level.group(0).strip().split()
remove_index = sys.argv.index(remove[0])
del sys.argv[remove_index : remove_index + len(remove)]
potential_log_level = str(__log_level.group(1)).upper().replace("-", "_")

if LOG_LEVEL not in ("CRITICAL", "ERROR", "WARN", "INFO", "DEBUG", "DEBUG_SQL"):
raise ValueError(f"Invalid log level {LOG_LEVEL!r}")
if potential_log_level in ("CRITICAL", "ERROR", "WARN", "INFO", "DEBUG", "DEBUG_SQL"):
LOG_LEVEL = potential_log_level
remove = __log_level.group(0).strip().split()
remove_index = sys.argv.index(remove[0])
del sys.argv[remove_index : remove_index + len(remove)]

if LOG_LEVEL == "DEBUG_SQL":
LOG_LEVEL = "DEBUG"
DEBUG_SQL = True
if LOG_LEVEL == "DEBUG_SQL":
LOG_LEVEL = "DEBUG"
DEBUG_SQL = True

SILENCED_LOGGERS = [
"asyncio",
Expand Down
10 changes: 4 additions & 6 deletions odev/common/string.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Shared methods for working with strings."""

import datetime
import inspect
import random
import re
import string as string_module
Expand Down Expand Up @@ -36,10 +37,7 @@ def normalize_indent(text: str) -> str:
if not text:
return ""

if "\n" in text.strip():
min_indent = min(len(line) - len(line.lstrip()) for line in text.splitlines()[1:] if line.strip())
text = " " * min_indent + text
return textwrap.dedent(text).strip()
return inspect.cleandoc(text).strip()


def short_help(name: str, description: str, indent_len: int = 0) -> str:
Expand All @@ -60,14 +58,14 @@ def format_options_list(elements: list[tuple[str, str]], indent_len: int = 0, bl

:param elements: The list of elements to format.
A list of tuples containing the name of the element and its description.
:param indent: The number of spaces to indent the list.
:param indent_len: The number of spaces to indent the list.
:param blanks: The number of blank lines to add between elements of the list.
:return: The list of elements formatted as a string.
:rtype: str
"""
elements_indent = max(len(element[0]) for element in elements)
elements_list: str = ("\n" * (blanks + 1)).join([short_help(*element, elements_indent) for element in elements])
return indent(elements_list, indent_len + 4)[indent_len:]
return indent(elements_list, indent_len + 4)


def indent(text: str, indent: int = 0) -> str:
Expand Down
Loading