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
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@
---

**Akaidoo** extends the [manifestoo](https://github.com/acsone/manifestoo) CLI to list
and copy all relevant source files (Python models, XML views, wizards, data, reports, and
even OpenUpgrade migration scripts) from a specific Odoo addon and its _entire_ dependency
tree. It's designed to feed AI LLMs.
and copy all relevant source files (Python models, XML views, wizards, data, reports,
and even OpenUpgrade migration scripts) from a specific Odoo addon and its _entire_
dependency tree. It's designed to feed AI LLMs.

Akaidoo bridges the AI gap for Odoo by helping you:

- 🤖 **Boost AI Tools:** Feed precisely the right context to AI LLMs. Works best with Gemini and
its 1 million tokens context.
- 🤖 **Boost AI Tools:** Feed precisely the right context to AI LLMs. Works best with
Gemini and its 1 million tokens context.
- 📝 **Streamline Editing:** Open all pertinent files in your editor with a single
command.
- 🧩 **Understand Scope:** Quickly grasp the breadth of an addon's interactions.
- 🔍 **Perform searches:** (`akaidoo sale_stock -c ~/DEV/odoo16/odoo.cfg | xargs grep "def _compute_price_unit"`)
- 🔍 **Perform searches:**
(`akaidoo sale_stock -c ~/DEV/odoo16/odoo.cfg | xargs grep "def _compute_price_unit"`)
- 🚀 **Accelerate Migrations:** Gather module code, dependencies, and their
corresponding OpenUpgrade migration scripts in one go.

Expand Down Expand Up @@ -104,8 +105,8 @@ Imagine you're working on the `sale_timesheet` addon in an Odoo project.
akaidoo sale_timesheet -c ~/path/to/your/odoo.conf
```

2. **Copy all Python model code for `sale_timesheet` (without its deps -l) to your clipboard
for an AI prompt:**
2. **Copy all Python model code for `sale_timesheet` (without its deps -l) to your
clipboard for an AI prompt:**

```console
akaidoo sale_timesheet -c odoo.conf --only-models -l -x
Expand Down
13 changes: 7 additions & 6 deletions src/akaidoo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def akaidoo_command_entrypoint(
help=f"Exclude {FRAMEWORK_ADDONS} framework addons.",
),
separator: str = typer.Option(
"\n", "--separator", "-S", help="Separator character between filenames."
"\n", "--separator", help="Separator character between filenames."
),
shrink: bool = typer.Option(
False,
Expand All @@ -352,11 +352,12 @@ def akaidoo_command_entrypoint(
),
shrink_aggressive: bool = typer.Option(
False,
"--shrink_aggressive",
"--shrink-aggressive",
"-S",
help="Shrink dependency Python files to essentials (classes, methods, fields).",
help="Enable aggressive shrinking, removing method bodies entirely.",
),
output_file: Optional[Path] = typer.Option(
# Path("akaidoo.out"),
None,
"--output-file",
"-o",
Expand Down Expand Up @@ -411,7 +412,7 @@ def akaidoo_command_entrypoint(
if item.is_file():
if (
"__pycache__" in str(item)
or "/." in str(item)
or str(item).replace(str(potential_path), "").startswith("/")
or ".png" in str(item)
):
continue
Expand Down Expand Up @@ -721,8 +722,8 @@ def akaidoo_command_entrypoint(
or addon_to_scan_name != addon_name
):
shrunken_content = shrink_python_file(
found_file.parts[0]
+ "/".join(found_file.parts[1:])
str(found_file),
aggressive=shrink_aggressive,
)
shrunken_files_content[abs_file_path] = (
shrunken_content
Expand Down
26 changes: 20 additions & 6 deletions src/akaidoo/shrinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
parser.language = Language(python_language())


def shrink_python_file(path: str) -> str:
def shrink_python_file(path: str, aggressive: bool = False) -> str:
"""
Shrinks Python code from a file to keep only class/function definitions
(with decorators), class attributes, and field assignments.
Expand Down Expand Up @@ -52,8 +52,8 @@ def process_function(node, indent=""):
stripped_line = line.strip()
if stripped_line:
shrunken_parts.append(f"{indent}{stripped_line}")

shrunken_parts.append(f"{indent} pass # shrunk")
if not aggressive:
shrunken_parts.append(f"{indent} pass # shrunk")

# --- Main Processing Loop ---
for node in root_node.children:
Expand All @@ -78,12 +78,18 @@ def process_function(node, indent=""):
line_bytes = code_bytes[child.start_byte : child.end_byte]
line_text = line_bytes.decode("utf8").strip()
shrunken_parts.append(f" {line_text}")
elif child.type in ("function_definition", "decorated_definition"):
elif (
child.type in ("function_definition", "decorated_definition")
and not aggressive
):
shrunken_parts.append("")
process_function(child, indent=" ")
shrunken_parts.append("")

elif node.type in ("function_definition", "decorated_definition"):
elif (
node.type in ("function_definition", "decorated_definition")
and not aggressive
):
process_function(node, indent="")
shrunken_parts.append("")

Expand Down Expand Up @@ -112,6 +118,12 @@ def main():
cli_parser.add_argument(
"input_file", type=str, help="The path to the Python file you want to shrink."
)
cli_parser.add_argument(
"-S",
"--shrink-aggressive",
action="store_true",
help="Enable aggressive shrinking, removing method bodies entirely.",
)
cli_parser.add_argument(
"-o",
"--output",
Expand All @@ -121,7 +133,9 @@ def main():
args = cli_parser.parse_args()

try:
shrunken_content = shrink_python_file(args.input_file)
shrunken_content = shrink_python_file(
args.input_file, aggressive=args.shrink_aggressive
)

if args.output:
# Write to the specified output file
Expand Down
49 changes: 49 additions & 0 deletions tests/test_shrinker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from pathlib import Path
import pytest
from akaidoo.shrinker import shrink_python_file


@pytest.fixture
def sample_python_file(tmp_path: Path) -> Path:
"""Create a sample Python file for testing."""
file_path = tmp_path / "sample.py"
file_path.write_text(
"""
class MyClass:
field = "value"

def my_method(self):
print("Hello")

@decorator
def my_function():
return 1
"""
)
return file_path


def test_shrink_python_file_default(sample_python_file: Path):
"""Test the default shrinking behavior."""
shrunken_content = shrink_python_file(str(sample_python_file))
assert "class MyClass:" in shrunken_content
assert 'field = "value"' in shrunken_content
assert "def my_method(self):" in shrunken_content
assert "pass # shrunk" in shrunken_content
assert "@decorator" in shrunken_content
assert "def my_function():" in shrunken_content
assert 'print("Hello")' not in shrunken_content
assert "return 1" not in shrunken_content


def test_shrink_python_file_aggressive(sample_python_file: Path):
"""Test the aggressive shrinking behavior."""
shrunken_content = shrink_python_file(str(sample_python_file), aggressive=True)
assert "class MyClass:" in shrunken_content
assert 'field = "value"' in shrunken_content
assert "def my_method(self):" not in shrunken_content
assert "pass # shrunk" not in shrunken_content
assert "@decorator" not in shrunken_content
assert "def my_function():" not in shrunken_content
assert 'print("Hello")' not in shrunken_content
assert "return 1" not in shrunken_content
Loading