odoo-dev 1.4.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo_dev-1.5.0/.release-please-manifest.json +3 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/CHANGELOG.md +12 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/PKG-INFO +13 -1
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/README.md +12 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/pyproject.toml +1 -1
- odoo_dev-1.5.0/src/odoo_dev/__init__.py +3 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/run.py +20 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/setup.py +123 -2
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/config.py +34 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/materialize.py +20 -3
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/verify.py +49 -4
- odoo_dev-1.5.0/tests/test_drop_filestore.py +49 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_integration.py +9 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_setup.py +8 -1
- odoo_dev-1.5.0/tests/test_setup_data_dir.py +179 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_verify.py +60 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/uv.lock +1 -1
- odoo_dev-1.4.0/.release-please-manifest.json +0 -3
- odoo_dev-1.4.0/src/odoo_dev/__init__.py +0 -3
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/.github/workflows/release-please.yml +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/.gitignore +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/.gitmodules +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/.python-version +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/release-please-config.json +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/cli.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/bump.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/db.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/docker.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/git.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/commands/vendor.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/preflight.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/docker/Dockerfile +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/docker/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/docker/docker-entrypoint.sh +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/vscode/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/vscode/launch.json +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/vscode/settings.json +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/templates/vscode/tasks.json +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/utils/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/utils/console.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/utils/manifest.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/develop.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/edit.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/lock.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/migrate.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/sources.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/sync.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/src/odoo_dev/vendor/update.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/fixtures/__init__.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_bump.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_cli.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_config.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_db.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_db_config.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_drop_database.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_git_hygiene.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_preflight.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_run.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_setup_db.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_develop.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_edit.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_lock.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_materialize.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_migrate.py +0 -0
- {odoo_dev-1.4.0 → odoo_dev-1.5.0}/tests/test_vendor_update.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0](https://github.com/bemade/odoo-dev/compare/v1.4.0...v1.5.0) (2026-10-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **setup:** project-local data_dir, addon manifest deps, ADMIN_PASSWD ([#23](https://github.com/bemade/odoo-dev/issues/23)) ([60c931e](https://github.com/bemade/odoo-dev/commit/60c931e3cf38c025895eb183d1a3a1ee929931e2))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **vendor:** ignore local python bytecode in vendor check, fail on tracked bytecode ([#25](https://github.com/bemade/odoo-dev/issues/25)) ([8d679af](https://github.com/bemade/odoo-dev/commit/8d679af962d96002a1ff978e6229ffeaf063250c))
|
|
14
|
+
|
|
3
15
|
## [1.4.0](https://github.com/bemade/odoo-dev/compare/v1.3.0...v1.4.0) (2026-09-04)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: odoo-dev
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Odoo Development Environment Helper
|
|
5
5
|
Project-URL: Homepage, https://github.com/bemade/odoo-dev
|
|
6
6
|
Project-URL: Repository, https://github.com/bemade/odoo-dev
|
|
@@ -236,8 +236,20 @@ PYTHON_VERSION=3.12
|
|
|
236
236
|
# DB_PORT=5432
|
|
237
237
|
# DB_USER=odoo
|
|
238
238
|
# DB_PASSWORD=odoo
|
|
239
|
+
|
|
240
|
+
# Optional — Odoo master password, written into conf/odoo.conf by `setup`.
|
|
241
|
+
# Defaults to "admin".
|
|
242
|
+
# ADMIN_PASSWD=admin
|
|
239
243
|
```
|
|
240
244
|
|
|
245
|
+
`setup` also points `data_dir` at a project-local `.odoo-data/` (git-ignored)
|
|
246
|
+
rather than the shared `~/.local/share/Odoo`, so filestores, sessions and the
|
|
247
|
+
addons cache stay scoped to the checkout they belong to.
|
|
248
|
+
|
|
249
|
+
Python packages declared in your addons' manifests — `external_dependencies`
|
|
250
|
+
*and* the OCA `test_external_dependencies` convention, across both `addons/`
|
|
251
|
+
and `vendored/` — are installed into the venv by `setup`.
|
|
252
|
+
|
|
241
253
|
## Requirements
|
|
242
254
|
|
|
243
255
|
- Python 3.12+
|
|
@@ -221,8 +221,20 @@ PYTHON_VERSION=3.12
|
|
|
221
221
|
# DB_PORT=5432
|
|
222
222
|
# DB_USER=odoo
|
|
223
223
|
# DB_PASSWORD=odoo
|
|
224
|
+
|
|
225
|
+
# Optional — Odoo master password, written into conf/odoo.conf by `setup`.
|
|
226
|
+
# Defaults to "admin".
|
|
227
|
+
# ADMIN_PASSWD=admin
|
|
224
228
|
```
|
|
225
229
|
|
|
230
|
+
`setup` also points `data_dir` at a project-local `.odoo-data/` (git-ignored)
|
|
231
|
+
rather than the shared `~/.local/share/Odoo`, so filestores, sessions and the
|
|
232
|
+
addons cache stay scoped to the checkout they belong to.
|
|
233
|
+
|
|
234
|
+
Python packages declared in your addons' manifests — `external_dependencies`
|
|
235
|
+
*and* the OCA `test_external_dependencies` convention, across both `addons/`
|
|
236
|
+
and `vendored/` — are installed into the venv by `setup`.
|
|
237
|
+
|
|
226
238
|
## Requirements
|
|
227
239
|
|
|
228
240
|
- Python 3.12+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Local Odoo runtime commands."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import shutil
|
|
4
5
|
import signal
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
@@ -582,6 +583,25 @@ def _drop_database(cfg, db_name: str) -> None:
|
|
|
582
583
|
warning(f"Failed to drop test database {db_name}")
|
|
583
584
|
if result.stderr:
|
|
584
585
|
warning(result.stderr.strip())
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# dropdb only removes the database. odoo-bin's own `db drop` deletes the
|
|
589
|
+
# filestore too, so the fallback has to, or every test run leaks a
|
|
590
|
+
# filestore directory that nothing ever cleans up.
|
|
591
|
+
_drop_filestore(cfg, db_name)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _drop_filestore(cfg, db_name: str) -> None:
|
|
595
|
+
"""Remove the filestore directory belonging to a dropped database."""
|
|
596
|
+
from odoo_dev.config import read_data_dir
|
|
597
|
+
|
|
598
|
+
filestore = read_data_dir(cfg.config_file) / "filestore" / db_name
|
|
599
|
+
if not filestore.is_dir():
|
|
600
|
+
return
|
|
601
|
+
try:
|
|
602
|
+
shutil.rmtree(filestore)
|
|
603
|
+
except OSError as exc:
|
|
604
|
+
warning(f"Could not remove filestore {filestore}: {exc}")
|
|
585
605
|
|
|
586
606
|
|
|
587
607
|
def _terminate_connections(db_name: str, db_config: dict[str, str]) -> None:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Setup commands for initializing Odoo development environment."""
|
|
2
2
|
|
|
3
|
+
import ast
|
|
3
4
|
import importlib.resources
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
@@ -248,10 +249,85 @@ def setup_venv() -> None:
|
|
|
248
249
|
[*venv_pip, "pytest", "pytest-odoo", "debugpy", "manifestoo", "coverage"]
|
|
249
250
|
)
|
|
250
251
|
|
|
252
|
+
# Install the external Python dependencies our addons declare
|
|
253
|
+
_install_manifest_python_deps(cfg, venv_pip)
|
|
254
|
+
|
|
251
255
|
success("\nVirtual environment setup complete!")
|
|
252
256
|
success(f"To activate: source {cfg.venv_path}/bin/activate")
|
|
253
257
|
|
|
254
258
|
|
|
259
|
+
def _manifest_test_deps(addons_dir: Path) -> set[str]:
|
|
260
|
+
"""Collect ``test_external_dependencies`` python packages from manifests.
|
|
261
|
+
|
|
262
|
+
This is an OCA convention rather than an Odoo core manifest key, so
|
|
263
|
+
manifestoo does not report it -- but the packages are needed to *run* the
|
|
264
|
+
tests of addons that declare it (e.g. pdfminer.six). Manifests that are not
|
|
265
|
+
plain literals are skipped rather than failing setup.
|
|
266
|
+
"""
|
|
267
|
+
deps: set[str] = set()
|
|
268
|
+
if not addons_dir.is_dir():
|
|
269
|
+
return deps
|
|
270
|
+
for manifest_path in sorted(addons_dir.glob("*/__manifest__.py")):
|
|
271
|
+
try:
|
|
272
|
+
manifest = ast.literal_eval(manifest_path.read_text())
|
|
273
|
+
except (SyntaxError, ValueError):
|
|
274
|
+
warning(f"Could not parse {manifest_path}, skipping its test deps")
|
|
275
|
+
continue
|
|
276
|
+
if not isinstance(manifest, dict):
|
|
277
|
+
continue
|
|
278
|
+
test_deps = manifest.get("test_external_dependencies") or {}
|
|
279
|
+
deps.update(test_deps.get("python") or [])
|
|
280
|
+
return deps
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _manifest_runtime_deps(addons_dir: Path, venv_python: Path) -> set[str]:
|
|
284
|
+
"""Collect ``external_dependencies`` python packages via manifestoo."""
|
|
285
|
+
if not addons_dir.is_dir():
|
|
286
|
+
return set()
|
|
287
|
+
result = subprocess.run(
|
|
288
|
+
[
|
|
289
|
+
str(venv_python),
|
|
290
|
+
"-m",
|
|
291
|
+
"manifestoo",
|
|
292
|
+
"--select-addons-dir",
|
|
293
|
+
str(addons_dir),
|
|
294
|
+
"list-external-dependencies",
|
|
295
|
+
"python",
|
|
296
|
+
],
|
|
297
|
+
capture_output=True,
|
|
298
|
+
text=True,
|
|
299
|
+
)
|
|
300
|
+
if result.returncode != 0:
|
|
301
|
+
warning(f"manifestoo failed for {addons_dir}: {result.stderr.strip()}")
|
|
302
|
+
return set()
|
|
303
|
+
return {line.strip() for line in result.stdout.splitlines() if line.strip()}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _install_manifest_python_deps(cfg, venv_pip: list[str]) -> None:
|
|
307
|
+
"""Install the Python packages our addons declare in their manifests.
|
|
308
|
+
|
|
309
|
+
Covers both ``addons/`` (client-specific) and ``vendored/`` (shared), and
|
|
310
|
+
both runtime and test dependencies. Without this, a module whose tests need
|
|
311
|
+
an external package installs fine and then fails at test time.
|
|
312
|
+
"""
|
|
313
|
+
venv_python = cfg.venv_path / "bin" / "python"
|
|
314
|
+
deps: set[str] = set()
|
|
315
|
+
for addons_dir in (cfg.addons_dir, cfg.vendored_dir):
|
|
316
|
+
deps |= _manifest_runtime_deps(addons_dir, venv_python)
|
|
317
|
+
deps |= _manifest_test_deps(addons_dir)
|
|
318
|
+
|
|
319
|
+
if not deps:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
success(f"Installing addon manifest dependencies: {', '.join(sorted(deps))}")
|
|
323
|
+
result = subprocess.run([*venv_pip, *sorted(deps)])
|
|
324
|
+
if result.returncode != 0:
|
|
325
|
+
warning(
|
|
326
|
+
"Some addon dependencies failed to install; tests that need them "
|
|
327
|
+
"will fail until they are installed manually."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
255
331
|
def vscode(cfg=None) -> None:
|
|
256
332
|
"""Set up VSCode configuration for debugging."""
|
|
257
333
|
if cfg is None:
|
|
@@ -334,6 +410,7 @@ def _setup_odoo_config(cfg, community_only: bool = False) -> None:
|
|
|
334
410
|
|
|
335
411
|
if conf_file.exists():
|
|
336
412
|
warning(f"Config file already exists at {conf_file}")
|
|
413
|
+
_ensure_data_dir_configured(cfg, conf_file)
|
|
337
414
|
return
|
|
338
415
|
|
|
339
416
|
success("Creating Odoo configuration file...")
|
|
@@ -366,6 +443,8 @@ def _setup_odoo_config(cfg, community_only: bool = False) -> None:
|
|
|
366
443
|
db_host = os.getenv("DB_HOST", "")
|
|
367
444
|
db_port = os.getenv("DB_PORT", "")
|
|
368
445
|
|
|
446
|
+
admin_passwd = os.getenv("ADMIN_PASSWD", "admin")
|
|
447
|
+
|
|
369
448
|
db_lines = []
|
|
370
449
|
if db_host:
|
|
371
450
|
db_lines.append(f"db_host = {db_host}")
|
|
@@ -377,15 +456,55 @@ def _setup_odoo_config(cfg, community_only: bool = False) -> None:
|
|
|
377
456
|
config_content = (
|
|
378
457
|
"[options]\n"
|
|
379
458
|
f"addons_path = {','.join(addons_paths)}\n"
|
|
380
|
-
"
|
|
459
|
+
f"data_dir = {cfg.data_dir}\n"
|
|
460
|
+
f"admin_passwd = {admin_passwd}\n"
|
|
381
461
|
f"{chr(10).join(db_lines)}\n"
|
|
382
462
|
)
|
|
383
463
|
|
|
384
464
|
conf_file.write_text(config_content)
|
|
385
465
|
conf_file.chmod(0o600)
|
|
466
|
+
_gitignore_data_dir(cfg)
|
|
386
467
|
success(f"Config file created at {conf_file}")
|
|
387
468
|
|
|
388
469
|
|
|
470
|
+
def _ensure_data_dir_configured(cfg, conf_file: Path) -> None:
|
|
471
|
+
"""Add ``data_dir`` to an odoo.conf that predates it.
|
|
472
|
+
|
|
473
|
+
Existing projects already have a conf file, so the generator's early return
|
|
474
|
+
would otherwise leave them on the shared ``~/.local/share/Odoo``. Only a
|
|
475
|
+
missing key is filled in — an explicit data_dir is left alone.
|
|
476
|
+
"""
|
|
477
|
+
content = conf_file.read_text()
|
|
478
|
+
if any(
|
|
479
|
+
line.strip().partition("=")[0].strip() == "data_dir"
|
|
480
|
+
for line in content.splitlines()
|
|
481
|
+
):
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
lines = content.splitlines()
|
|
485
|
+
for i, line in enumerate(lines):
|
|
486
|
+
if line.strip() == "[options]":
|
|
487
|
+
lines.insert(i + 1, f"data_dir = {cfg.data_dir}")
|
|
488
|
+
break
|
|
489
|
+
else:
|
|
490
|
+
lines.append(f"data_dir = {cfg.data_dir}")
|
|
491
|
+
conf_file.write_text("\n".join(lines) + "\n")
|
|
492
|
+
_gitignore_data_dir(cfg)
|
|
493
|
+
success(f"Added data_dir = {cfg.data_dir} to {conf_file}")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _gitignore_data_dir(cfg) -> None:
|
|
497
|
+
"""Ensure the project-local data directory is git-ignored."""
|
|
498
|
+
gitignore = cfg.project_dir / ".gitignore"
|
|
499
|
+
entry = f"{cfg.data_dir.name}/"
|
|
500
|
+
lines = gitignore.read_text().splitlines() if gitignore.exists() else []
|
|
501
|
+
if entry in (line.strip() for line in lines):
|
|
502
|
+
return
|
|
503
|
+
lines.append(entry)
|
|
504
|
+
gitignore.write_text("\n".join(lines) + "\n")
|
|
505
|
+
success(f"Added {entry} to .gitignore")
|
|
506
|
+
|
|
507
|
+
|
|
389
508
|
def _setup_docker_files(cfg, community_only: bool = False) -> None:
|
|
390
509
|
"""Copy Docker templates to .odoo-deploy directory."""
|
|
391
510
|
success("Setting up Docker files...")
|
|
@@ -434,9 +553,11 @@ def _generate_docker_odoo_conf(
|
|
|
434
553
|
if has_vendored:
|
|
435
554
|
addons_paths.append("/opt/project/vendored")
|
|
436
555
|
|
|
556
|
+
admin_passwd = os.getenv("ADMIN_PASSWD", "admin")
|
|
557
|
+
|
|
437
558
|
return f"""[options]
|
|
438
559
|
addons_path = {",".join(addons_paths)}
|
|
439
|
-
admin_passwd =
|
|
560
|
+
admin_passwd = {admin_passwd}
|
|
440
561
|
db_host = db
|
|
441
562
|
db_port = 5432
|
|
442
563
|
db_user = odoo
|
|
@@ -59,6 +59,28 @@ def read_db_config(config_file: Path) -> "DbConfig":
|
|
|
59
59
|
return db
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
# Odoo's own fallback when ``data_dir`` is absent from odoo.conf.
|
|
63
|
+
DEFAULT_ODOO_DATA_DIR = Path.home() / ".local" / "share" / "Odoo"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_data_dir(config_file: Path) -> Path:
|
|
67
|
+
"""Read ``data_dir`` from an odoo.conf file.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config_file: Path to odoo.conf.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The configured data directory, or Odoo's default when the file is
|
|
74
|
+
missing or does not set one.
|
|
75
|
+
"""
|
|
76
|
+
if config_file.exists():
|
|
77
|
+
for line in config_file.read_text().splitlines():
|
|
78
|
+
key, sep, value = line.strip().partition("=")
|
|
79
|
+
if sep and key.strip() == "data_dir":
|
|
80
|
+
return Path(value.strip()).expanduser()
|
|
81
|
+
return DEFAULT_ODOO_DATA_DIR
|
|
82
|
+
|
|
83
|
+
|
|
62
84
|
@dataclass
|
|
63
85
|
class ProjectConfig:
|
|
64
86
|
"""Configuration for an Odoo development project."""
|
|
@@ -74,6 +96,18 @@ class ProjectConfig:
|
|
|
74
96
|
"""Path to the Python virtual environment."""
|
|
75
97
|
return self.project_dir / ".venv"
|
|
76
98
|
|
|
99
|
+
@property
|
|
100
|
+
def data_dir(self) -> Path:
|
|
101
|
+
"""Path to the project-local Odoo data directory.
|
|
102
|
+
|
|
103
|
+
Odoo defaults to a single shared ``~/.local/share/Odoo`` for every
|
|
104
|
+
project on the machine, which mixes filestores, sessions and the addons
|
|
105
|
+
cache across unrelated checkouts and leaves orphaned filestores behind
|
|
106
|
+
whenever a database is dropped without odoo-bin. Keeping it in-project
|
|
107
|
+
scopes all of that to the checkout it belongs to.
|
|
108
|
+
"""
|
|
109
|
+
return self.project_dir / ".odoo-data"
|
|
110
|
+
|
|
77
111
|
@property
|
|
78
112
|
def config_file(self) -> Path:
|
|
79
113
|
"""Path to the local odoo.conf file."""
|
|
@@ -9,7 +9,9 @@ latter two and follows symlinks), which is what makes ``vendor check`` trustwort
|
|
|
9
9
|
Python bytecode (``__pycache__/``, ``*.pyc``, ``*.pyo``) is excluded from the
|
|
10
10
|
comparison: running or testing a vendored addon locally compiles it in place, and
|
|
11
11
|
those artifacts are gitignored build output that is neither part of the pinned
|
|
12
|
-
source nor present in a fresh CI checkout.
|
|
12
|
+
source nor present in a fresh CI checkout. A directory holding nothing but
|
|
13
|
+
bytecode (e.g. a ``tests/`` whose sources a pin bump removed) is skipped too: git
|
|
14
|
+
does not track empty directories, so CI never sees it.
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
from __future__ import annotations
|
|
@@ -69,12 +71,20 @@ _BYTECODE_DIRS = {"__pycache__"}
|
|
|
69
71
|
_BYTECODE_SUFFIXES = (".pyc", ".pyo")
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
def is_bytecode(rel: str) -> bool:
|
|
75
|
+
"""True if ``rel`` is python bytecode or lies inside a ``__pycache__`` dir."""
|
|
76
|
+
parts = Path(rel).parts
|
|
77
|
+
return rel.endswith(_BYTECODE_SUFFIXES) or any(
|
|
78
|
+
p in _BYTECODE_DIRS for p in parts
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
72
82
|
def _entries(root: Path) -> dict:
|
|
73
83
|
"""Map each path relative to ``root`` to a comparable fingerprint.
|
|
74
84
|
|
|
75
85
|
- symlink -> ("link", target) (never dereferenced)
|
|
76
86
|
- file -> ("file", exec_bit, sha1)
|
|
77
|
-
- dir -> ("dir",)
|
|
87
|
+
- dir -> ("dir",) (only if it holds a file or link)
|
|
78
88
|
|
|
79
89
|
Python bytecode is skipped entirely (see the module docstring).
|
|
80
90
|
"""
|
|
@@ -104,7 +114,14 @@ def _entries(root: Path) -> dict:
|
|
|
104
114
|
else:
|
|
105
115
|
exec_bit = 1 if os.stat(p).st_mode & 0o100 else 0
|
|
106
116
|
out[rel] = ("file", exec_bit, _sha1(p))
|
|
107
|
-
|
|
117
|
+
# Like git, a directory exists only through its contents.
|
|
118
|
+
occupied = {
|
|
119
|
+
str(parent)
|
|
120
|
+
for rel, v in out.items()
|
|
121
|
+
if v[0] != "dir"
|
|
122
|
+
for parent in Path(rel).parents
|
|
123
|
+
}
|
|
124
|
+
return {rel: v for rel, v in out.items() if v[0] != "dir" or rel in occupied}
|
|
108
125
|
|
|
109
126
|
|
|
110
127
|
def _sha1(path: Path) -> str:
|
|
@@ -6,7 +6,8 @@ Assertions (any failure => non-empty problem list => CI red):
|
|
|
6
6
|
3. moved-tag tripwire (when the entry carries a ``version`` tag);
|
|
7
7
|
4. external python deps of vendored manifests are present in ``requirements.txt``;
|
|
8
8
|
5. no addon name in both ``addons/`` and ``vendored/`` (double-load);
|
|
9
|
-
6. no file under ``vendored/`` is gitignored (it would be dropped at commit)
|
|
9
|
+
6. no file under ``vendored/`` is gitignored (it would be dropped at commit),
|
|
10
|
+
and no python bytecode under it is tracked by git (it is not in the pin);
|
|
10
11
|
7. (``allow_hybrid=False`` only) no ``addons/`` symlink still points into a
|
|
11
12
|
``.repos/`` submodule — i.e. the repo is fully vendored, not a hybrid.
|
|
12
13
|
The gate writes nothing.
|
|
@@ -23,7 +24,7 @@ from pathlib import Path
|
|
|
23
24
|
from typing import Optional
|
|
24
25
|
|
|
25
26
|
from odoo_dev.vendor.lock import Lockfile
|
|
26
|
-
from odoo_dev.vendor.materialize import extract_subtree, tree_diff
|
|
27
|
+
from odoo_dev.vendor.materialize import extract_subtree, is_bytecode, tree_diff
|
|
27
28
|
from odoo_dev.vendor.sources import get_source_at, tag_resolves_to
|
|
28
29
|
|
|
29
30
|
_MANIFEST_NAMES = ("__manifest__.py", "__openerp__.py")
|
|
@@ -124,6 +125,9 @@ def gitignored_under_vendored(project_dir: Path) -> list:
|
|
|
124
125
|
``check-ignore`` (not a tracked-vs-on-disk comparison) is the right probe: it
|
|
125
126
|
reports exactly the paths git would drop, and consults the index, so neither a
|
|
126
127
|
not-yet-staged ``vendor add`` nor a deliberate ``git add -f`` is a false alarm.
|
|
128
|
+
|
|
129
|
+
Python bytecode is not probed: it is never part of a pin, so ignoring it is
|
|
130
|
+
correct (and :func:`tracked_bytecode_under_vendored` catches the opposite).
|
|
127
131
|
"""
|
|
128
132
|
project_dir = Path(project_dir)
|
|
129
133
|
vendored = project_dir / "vendored"
|
|
@@ -132,7 +136,8 @@ def gitignored_under_vendored(project_dir: Path) -> list:
|
|
|
132
136
|
rels = [
|
|
133
137
|
str(p.relative_to(project_dir))
|
|
134
138
|
for p in sorted(vendored.rglob("*"))
|
|
135
|
-
if p.is_symlink() or p.is_file()
|
|
139
|
+
if (p.is_symlink() or p.is_file())
|
|
140
|
+
and not is_bytecode(str(p.relative_to(vendored)))
|
|
136
141
|
]
|
|
137
142
|
if not rels:
|
|
138
143
|
return []
|
|
@@ -149,6 +154,39 @@ def gitignored_under_vendored(project_dir: Path) -> list:
|
|
|
149
154
|
return [line for line in res.stdout.splitlines() if line.strip()]
|
|
150
155
|
|
|
151
156
|
|
|
157
|
+
def tracked_bytecode_under_vendored(project_dir: Path) -> list:
|
|
158
|
+
"""Python bytecode under ``vendored/`` that git tracks (index or HEAD).
|
|
159
|
+
|
|
160
|
+
:func:`tree_diff` skips bytecode so local test runs don't fail the gate, which
|
|
161
|
+
would also hide a ``.pyc`` that was committed by mistake. Untracked or
|
|
162
|
+
gitignored bytecode is fine; tracked bytecode ships a tree the pin does not
|
|
163
|
+
describe.
|
|
164
|
+
"""
|
|
165
|
+
project_dir = Path(project_dir)
|
|
166
|
+
if not (project_dir / "vendored").is_dir() or not (project_dir / ".git").exists():
|
|
167
|
+
return []
|
|
168
|
+
res = subprocess.run(
|
|
169
|
+
["git", "-C", str(project_dir), "ls-files", "-z", "--", "vendored"],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
)
|
|
173
|
+
if res.returncode != 0:
|
|
174
|
+
return []
|
|
175
|
+
return [
|
|
176
|
+
rel
|
|
177
|
+
for rel in res.stdout.split("\0")
|
|
178
|
+
if rel and is_bytecode(rel[len("vendored/"):])
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _has_source(addon_dir: Path) -> bool:
|
|
183
|
+
"""True if ``addon_dir`` holds anything besides python bytecode."""
|
|
184
|
+
return any(
|
|
185
|
+
(p.is_file() or p.is_symlink()) and not is_bytecode(str(p.relative_to(addon_dir)))
|
|
186
|
+
for p in addon_dir.rglob("*")
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
152
190
|
def verify(
|
|
153
191
|
project_dir: Path,
|
|
154
192
|
lock: Lockfile,
|
|
@@ -167,7 +205,7 @@ def verify(
|
|
|
167
205
|
problems: list = []
|
|
168
206
|
|
|
169
207
|
vendored_dirs = (
|
|
170
|
-
{p.name for p in vendored.iterdir() if p.is_dir()}
|
|
208
|
+
{p.name for p in vendored.iterdir() if p.is_dir() and _has_source(p)}
|
|
171
209
|
if vendored.exists()
|
|
172
210
|
else set()
|
|
173
211
|
)
|
|
@@ -227,6 +265,13 @@ def verify(
|
|
|
227
265
|
f"(add a '!vendored/**' negation, keep it last)"
|
|
228
266
|
)
|
|
229
267
|
|
|
268
|
+
for rel in tracked_bytecode_under_vendored(project_dir):
|
|
269
|
+
problems.append(
|
|
270
|
+
f"{rel}: python bytecode under vendored/ is tracked by git — it is not "
|
|
271
|
+
f"part of the pin (git rm --cached it; ignore '**/__pycache__/' after "
|
|
272
|
+
f"the '!vendored/**' negation)"
|
|
273
|
+
)
|
|
274
|
+
|
|
230
275
|
if not allow_hybrid:
|
|
231
276
|
for name in hybrid_submodule_addons(project_dir):
|
|
232
277
|
problems.append(
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tests for filestore cleanup on the dropdb fallback path."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from odoo_dev.commands.run import _drop_filestore
|
|
8
|
+
from odoo_dev.config import ProjectConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def cfg(tmp_path: Path) -> ProjectConfig:
|
|
13
|
+
(tmp_path / "conf").mkdir()
|
|
14
|
+
return ProjectConfig(
|
|
15
|
+
project_dir=tmp_path,
|
|
16
|
+
script_dir=tmp_path / ".odoo-deploy",
|
|
17
|
+
odoo_version="19.0",
|
|
18
|
+
python_version="3.12",
|
|
19
|
+
project_name="proj",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestDropFilestore:
|
|
24
|
+
"""The dropdb fallback must clean up the filestore odoo-bin would have."""
|
|
25
|
+
|
|
26
|
+
def test_removes_filestore_for_dropped_db(self, cfg):
|
|
27
|
+
cfg.config_file.write_text(f"[options]\ndata_dir = {cfg.data_dir}\n")
|
|
28
|
+
filestore = cfg.data_dir / "filestore" / "test_123"
|
|
29
|
+
filestore.mkdir(parents=True)
|
|
30
|
+
(filestore / "blob").write_text("x")
|
|
31
|
+
|
|
32
|
+
_drop_filestore(cfg, "test_123")
|
|
33
|
+
|
|
34
|
+
assert not filestore.exists()
|
|
35
|
+
|
|
36
|
+
def test_leaves_other_filestores_alone(self, cfg):
|
|
37
|
+
cfg.config_file.write_text(f"[options]\ndata_dir = {cfg.data_dir}\n")
|
|
38
|
+
keep = cfg.data_dir / "filestore" / "other_db"
|
|
39
|
+
keep.mkdir(parents=True)
|
|
40
|
+
|
|
41
|
+
_drop_filestore(cfg, "test_123")
|
|
42
|
+
|
|
43
|
+
assert keep.exists()
|
|
44
|
+
|
|
45
|
+
def test_missing_filestore_is_not_an_error(self, cfg):
|
|
46
|
+
cfg.config_file.write_text(f"[options]\ndata_dir = {cfg.data_dir}\n")
|
|
47
|
+
|
|
48
|
+
_drop_filestore(cfg, "test_123")
|
|
49
|
+
|
|
@@ -90,12 +90,21 @@ def cleanup_fixture_env():
|
|
|
90
90
|
"conf",
|
|
91
91
|
".vscode",
|
|
92
92
|
".odoo-deploy",
|
|
93
|
+
".odoo-data",
|
|
93
94
|
]
|
|
94
95
|
for dirname in created_dirs:
|
|
95
96
|
dir_path = FIXTURE_DIR / dirname
|
|
96
97
|
if dir_path.exists():
|
|
97
98
|
shutil.rmtree(dir_path, ignore_errors=True)
|
|
98
99
|
|
|
100
|
+
# setup adds .odoo-data/ to the project's .gitignore; the fixture is a
|
|
101
|
+
# tracked submodule, so restore it rather than leaving the tree dirty.
|
|
102
|
+
subprocess.run(
|
|
103
|
+
["git", "checkout", "--", ".gitignore"],
|
|
104
|
+
cwd=FIXTURE_DIR,
|
|
105
|
+
capture_output=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
99
108
|
|
|
100
109
|
@pytest.fixture(scope="session")
|
|
101
110
|
def setup_fixture_env():
|
|
@@ -179,7 +179,14 @@ class TestLocalOdooConf:
|
|
|
179
179
|
cfg = _make_cfg(tmp_path)
|
|
180
180
|
(tmp_path / "conf").mkdir()
|
|
181
181
|
_setup_odoo_config(cfg)
|
|
182
|
-
|
|
182
|
+
# Scoped to addons_path: data_dir also embeds project_dir, whose tmp
|
|
183
|
+
# path can itself contain "vendored".
|
|
184
|
+
addons_path = next(
|
|
185
|
+
line
|
|
186
|
+
for line in cfg.config_file.read_text().splitlines()
|
|
187
|
+
if line.startswith("addons_path")
|
|
188
|
+
)
|
|
189
|
+
assert "vendored" not in addons_path
|
|
183
190
|
|
|
184
191
|
def test_local_conf_includes_vendored_when_present(self, tmp_path: Path):
|
|
185
192
|
from odoo_dev.commands.setup import _setup_odoo_config
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Tests for the project-local data_dir and manifest dependency install."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from odoo_dev.commands.setup import (
|
|
9
|
+
_ensure_data_dir_configured,
|
|
10
|
+
_gitignore_data_dir,
|
|
11
|
+
_install_manifest_python_deps,
|
|
12
|
+
_manifest_test_deps,
|
|
13
|
+
_setup_odoo_config,
|
|
14
|
+
)
|
|
15
|
+
from odoo_dev.config import DEFAULT_ODOO_DATA_DIR, ProjectConfig, read_data_dir
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def cfg(tmp_path: Path) -> ProjectConfig:
|
|
20
|
+
(tmp_path / "conf").mkdir()
|
|
21
|
+
return ProjectConfig(
|
|
22
|
+
project_dir=tmp_path,
|
|
23
|
+
script_dir=tmp_path / ".odoo-deploy",
|
|
24
|
+
odoo_version="19.0",
|
|
25
|
+
python_version="3.12",
|
|
26
|
+
project_name="proj",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _write_manifest(addons_dir: Path, name: str, body: str) -> None:
|
|
31
|
+
mod = addons_dir / name
|
|
32
|
+
mod.mkdir(parents=True)
|
|
33
|
+
(mod / "__manifest__.py").write_text(body)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestDataDirConfig:
|
|
37
|
+
def test_generated_config_sets_project_local_data_dir(self, cfg):
|
|
38
|
+
_setup_odoo_config(cfg)
|
|
39
|
+
|
|
40
|
+
content = cfg.config_file.read_text()
|
|
41
|
+
assert f"data_dir = {cfg.project_dir / '.odoo-data'}" in content
|
|
42
|
+
|
|
43
|
+
def test_generated_config_gitignores_data_dir(self, cfg):
|
|
44
|
+
_setup_odoo_config(cfg)
|
|
45
|
+
|
|
46
|
+
assert ".odoo-data/" in (cfg.project_dir / ".gitignore").read_text()
|
|
47
|
+
|
|
48
|
+
def test_existing_config_gains_data_dir(self, cfg):
|
|
49
|
+
cfg.config_file.write_text("[options]\nadmin_passwd = admin\n")
|
|
50
|
+
|
|
51
|
+
_setup_odoo_config(cfg)
|
|
52
|
+
|
|
53
|
+
lines = cfg.config_file.read_text().splitlines()
|
|
54
|
+
assert lines[0] == "[options]"
|
|
55
|
+
assert lines[1] == f"data_dir = {cfg.data_dir}"
|
|
56
|
+
assert "admin_passwd = admin" in lines
|
|
57
|
+
|
|
58
|
+
def test_existing_data_dir_is_not_overwritten(self, cfg):
|
|
59
|
+
cfg.config_file.write_text("[options]\ndata_dir = /srv/odoo-data\n")
|
|
60
|
+
|
|
61
|
+
_ensure_data_dir_configured(cfg, cfg.config_file)
|
|
62
|
+
|
|
63
|
+
assert "/srv/odoo-data" in cfg.config_file.read_text()
|
|
64
|
+
assert str(cfg.data_dir) not in cfg.config_file.read_text()
|
|
65
|
+
|
|
66
|
+
def test_gitignore_entry_is_not_duplicated(self, cfg):
|
|
67
|
+
(cfg.project_dir / ".gitignore").write_text(".odoo-data/\n.venv/\n")
|
|
68
|
+
|
|
69
|
+
_gitignore_data_dir(cfg)
|
|
70
|
+
|
|
71
|
+
content = (cfg.project_dir / ".gitignore").read_text()
|
|
72
|
+
assert content.count(".odoo-data/") == 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestReadDataDir:
|
|
76
|
+
def test_reads_configured_value(self, tmp_path: Path):
|
|
77
|
+
conf = tmp_path / "odoo.conf"
|
|
78
|
+
conf.write_text("[options]\ndata_dir = /srv/odoo-data\n")
|
|
79
|
+
|
|
80
|
+
assert read_data_dir(conf) == Path("/srv/odoo-data")
|
|
81
|
+
|
|
82
|
+
def test_falls_back_to_odoo_default(self, tmp_path: Path):
|
|
83
|
+
conf = tmp_path / "odoo.conf"
|
|
84
|
+
conf.write_text("[options]\nadmin_passwd = admin\n")
|
|
85
|
+
|
|
86
|
+
assert read_data_dir(conf) == DEFAULT_ODOO_DATA_DIR
|
|
87
|
+
|
|
88
|
+
def test_missing_file_falls_back_to_odoo_default(self, tmp_path: Path):
|
|
89
|
+
assert read_data_dir(tmp_path / "nope.conf") == DEFAULT_ODOO_DATA_DIR
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestManifestTestDeps:
|
|
93
|
+
def test_collects_test_external_dependencies(self, tmp_path: Path):
|
|
94
|
+
_write_manifest(
|
|
95
|
+
tmp_path,
|
|
96
|
+
"mod_a",
|
|
97
|
+
"{'name': 'A', 'test_external_dependencies': {'python': ['pdfminer.six']}}",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert _manifest_test_deps(tmp_path) == {"pdfminer.six"}
|
|
101
|
+
|
|
102
|
+
def test_ignores_runtime_dependencies(self, tmp_path: Path):
|
|
103
|
+
_write_manifest(
|
|
104
|
+
tmp_path,
|
|
105
|
+
"mod_a",
|
|
106
|
+
"{'name': 'A', 'external_dependencies': {'python': ['openpyxl']}}",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert _manifest_test_deps(tmp_path) == set()
|
|
110
|
+
|
|
111
|
+
def test_skips_unparseable_manifest(self, tmp_path: Path):
|
|
112
|
+
_write_manifest(tmp_path, "mod_a", "{'name': 'A', 'x': undefined_name}")
|
|
113
|
+
_write_manifest(
|
|
114
|
+
tmp_path,
|
|
115
|
+
"mod_b",
|
|
116
|
+
"{'name': 'B', 'test_external_dependencies': {'python': ['freezegun']}}",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert _manifest_test_deps(tmp_path) == {"freezegun"}
|
|
120
|
+
|
|
121
|
+
def test_missing_dir_is_empty(self, tmp_path: Path):
|
|
122
|
+
assert _manifest_test_deps(tmp_path / "nope") == set()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestInstallManifestPythonDeps:
|
|
126
|
+
def test_installs_runtime_and_test_deps_from_both_dirs(self, cfg):
|
|
127
|
+
_write_manifest(
|
|
128
|
+
cfg.addons_dir,
|
|
129
|
+
"mod_a",
|
|
130
|
+
"{'name': 'A', 'test_external_dependencies': {'python': ['freezegun']}}",
|
|
131
|
+
)
|
|
132
|
+
_write_manifest(
|
|
133
|
+
cfg.vendored_dir,
|
|
134
|
+
"mod_b",
|
|
135
|
+
"{'name': 'B', 'test_external_dependencies': {'python': ['pdfminer.six']}}",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
with patch("odoo_dev.commands.setup._manifest_runtime_deps") as runtime:
|
|
139
|
+
runtime.side_effect = [{"openpyxl"}, {"xlsxwriter"}]
|
|
140
|
+
with patch("odoo_dev.commands.setup.subprocess.run") as run:
|
|
141
|
+
run.return_value.returncode = 0
|
|
142
|
+
_install_manifest_python_deps(cfg, ["uv", "pip", "install"])
|
|
143
|
+
|
|
144
|
+
installed = set(run.call_args[0][0][3:])
|
|
145
|
+
assert installed == {"openpyxl", "xlsxwriter", "freezegun", "pdfminer.six"}
|
|
146
|
+
|
|
147
|
+
def test_no_deps_installs_nothing(self, cfg):
|
|
148
|
+
cfg.addons_dir.mkdir()
|
|
149
|
+
|
|
150
|
+
with patch("odoo_dev.commands.setup._manifest_runtime_deps", return_value=set()):
|
|
151
|
+
with patch("odoo_dev.commands.setup.subprocess.run") as run:
|
|
152
|
+
_install_manifest_python_deps(cfg, ["uv", "pip", "install"])
|
|
153
|
+
|
|
154
|
+
run.assert_not_called()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestAdminPasswd:
|
|
158
|
+
"""admin_passwd follows the same .env pattern as the DB settings."""
|
|
159
|
+
|
|
160
|
+
def test_local_conf_defaults_to_admin(self, cfg, monkeypatch):
|
|
161
|
+
monkeypatch.delenv("ADMIN_PASSWD", raising=False)
|
|
162
|
+
|
|
163
|
+
_setup_odoo_config(cfg)
|
|
164
|
+
|
|
165
|
+
assert "admin_passwd = admin\n" in cfg.config_file.read_text()
|
|
166
|
+
|
|
167
|
+
def test_local_conf_honours_env(self, cfg, monkeypatch):
|
|
168
|
+
monkeypatch.setenv("ADMIN_PASSWD", "s3cret")
|
|
169
|
+
|
|
170
|
+
_setup_odoo_config(cfg)
|
|
171
|
+
|
|
172
|
+
assert "admin_passwd = s3cret\n" in cfg.config_file.read_text()
|
|
173
|
+
|
|
174
|
+
def test_docker_conf_honours_env(self, monkeypatch):
|
|
175
|
+
from odoo_dev.commands.setup import _generate_docker_odoo_conf
|
|
176
|
+
|
|
177
|
+
monkeypatch.setenv("ADMIN_PASSWD", "s3cret")
|
|
178
|
+
|
|
179
|
+
assert "admin_passwd = s3cret\n" in _generate_docker_odoo_conf()
|
|
@@ -243,3 +243,63 @@ def test_verify_strict_ignores_non_repos_symlink(tmp_path):
|
|
|
243
243
|
(proj / "addons").mkdir(exist_ok=True)
|
|
244
244
|
(proj / "addons" / "external").symlink_to(Path("../elsewhere"))
|
|
245
245
|
assert verify(proj, lock, cache_dir=tmp_path / "cache", allow_hybrid=False) == []
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# --- python bytecode left behind by running/testing vendored addons -----------
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _compile_in_place(proj: Path) -> list:
|
|
252
|
+
"""Simulate ``odoo-dev test``: drop bytecode beside the vendored sources.
|
|
253
|
+
|
|
254
|
+
Includes the two shapes that outlive a pin bump: a subdir whose sources are
|
|
255
|
+
gone but whose ``__pycache__`` remains, and a whole ex-addon left as nothing
|
|
256
|
+
but bytecode (git does not track the empty dirs, so CI never sees either).
|
|
257
|
+
"""
|
|
258
|
+
pycs = [
|
|
259
|
+
"vendored/shared_addon/__pycache__/models.cpython-312.pyc",
|
|
260
|
+
"vendored/shared_addon/tests/__pycache__/test_x.cpython-312.pyc",
|
|
261
|
+
"vendored/removed_addon/__pycache__/__init__.cpython-312.pyc",
|
|
262
|
+
]
|
|
263
|
+
for rel in pycs:
|
|
264
|
+
p = proj / rel
|
|
265
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
p.write_bytes(b"\x00compiled")
|
|
267
|
+
return pycs
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_verify_green_with_untracked_bytecode_outside_git(tmp_path):
|
|
271
|
+
proj, lock = _sync_clean(tmp_path)
|
|
272
|
+
_compile_in_place(proj)
|
|
273
|
+
assert verify(proj, lock, cache_dir=tmp_path / "cache") == []
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_verify_green_with_gitignored_bytecode(tmp_path):
|
|
277
|
+
"""Regression: ignoring ``__pycache__`` after ``!vendored/**`` is correct
|
|
278
|
+
hygiene, but the gitignore probe reported every local .pyc as a file git
|
|
279
|
+
would drop, failing the gate after any local test run.
|
|
280
|
+
"""
|
|
281
|
+
repo, sha = _source_repo(tmp_path)
|
|
282
|
+
proj, lock = _git_project(
|
|
283
|
+
tmp_path,
|
|
284
|
+
LockEntry("shared_addon", str(repo), sha),
|
|
285
|
+
"!vendored/**\n**/__pycache__/\n",
|
|
286
|
+
)
|
|
287
|
+
sync_addons(proj, lock, cache_dir=tmp_path / "cache")
|
|
288
|
+
_compile_in_place(proj)
|
|
289
|
+
assert verify(proj, lock, cache_dir=tmp_path / "cache") == []
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_verify_red_when_bytecode_is_tracked(tmp_path):
|
|
293
|
+
"""Bytecode committed under vendored/ is not in the pin; it must fail."""
|
|
294
|
+
repo, sha = _source_repo(tmp_path)
|
|
295
|
+
proj, lock = _git_project(
|
|
296
|
+
tmp_path,
|
|
297
|
+
LockEntry("shared_addon", str(repo), sha),
|
|
298
|
+
"!vendored/**\n**/__pycache__/\n",
|
|
299
|
+
)
|
|
300
|
+
sync_addons(proj, lock, cache_dir=tmp_path / "cache")
|
|
301
|
+
pycs = _compile_in_place(proj)
|
|
302
|
+
_git(proj, "add", "-f", pycs[0])
|
|
303
|
+
problems = verify(proj, lock, cache_dir=tmp_path / "cache")
|
|
304
|
+
assert len(problems) == 1
|
|
305
|
+
assert pycs[0] in problems[0] and "tracked" in problems[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|