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
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
[run]
# Trace only odev/ so temp plugin files (e.g. test_16 cycle manifests) are not recorded.
source = odev

[report]
omit = */tests/*
fail_under = 60
show_missing = true
exclude_lines =
pragma: no cover
raise NotImplementedError
Expand Down
83 changes: 49 additions & 34 deletions .github/workflows/odev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- opened
- reopened
- synchronize
push:
branches:
- beta
workflow_dispatch:

jobs:

Expand Down Expand Up @@ -36,6 +40,14 @@ jobs:
uses: actions/[email protected]
with:
python-version: '3.10'
cache: pip
cache-dependency-path: .pre-commit-config.yaml

- name: cache-pre-commit
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}

- name: run-pre-commit
uses: pre-commit/[email protected]
Expand All @@ -57,17 +69,30 @@ jobs:
- "3.13"

steps:
- name: checkout-repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

- name: setup-python
id: setup-python
uses: actions/[email protected]
with:
# Matrix Python runs pytest. Extra interpreters are available if tests or
# tooling need them; database command tests mock odoo-bin and do not clone Odoo.
python-version: |
3.10
${{ matrix.python-version }}
3.10
3.12
architecture: x64
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt

- name: setup-system-dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: postgresql postgresql-client python3-pip libldap2-dev libpq-dev libsasl2-dev build-essential python3-dev libffi-dev
version: 1.1
Expand All @@ -76,46 +101,36 @@ jobs:
run: |
sudo service postgresql start
sudo -u postgres createuser -s $USER
for i in {1..10}; do pg_isready -h localhost -p 5432 && exit 0; sleep 1; done
exit 1

- name: checkout-repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

- name: restore-odoo-repositories
id: restore-odoo-repositories
uses: actions/cache/restore@v4
with:
path: ~/odoo/repositories
key: odoo-repositories-${{ matrix.python-version }}

- name: clone-odoo-repositories
if: steps.restore-odoo-repositories.outputs.cache-hit != 'true'
run: |
git clone --depth 1 https://github.com/odoo/odoo ~/odoo/repositories/odoo/odoo --branch master
cd ~/odoo/repositories/odoo/odoo
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --depth 1 origin 18.0

- name: save-odoo-repositories
if: steps.restore-odoo-repositories.outputs.cache-hit != 'true'
id: save-odoo-repositories
uses: actions/cache/save@v4
- name: cache-python-venv
id: cache-venv
uses: actions/cache@v4
with:
path: ~/odoo/repositories
key: odoo-repositories-${{ matrix.python-version }}
path: .venv-ci
key: ${{ runner.os }}-py${{ steps.setup-python.outputs.python-version }}-venv-${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}

- name: setup-python-requirements
run: |
python -m ensurepip --upgrade
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
if [ "${{ steps.cache-venv.outputs.cache-hit }}" != 'true' ]; then
python -m venv .venv-ci
.venv-ci/bin/python -m pip install --upgrade pip
if [ -f requirements.txt ]; then .venv-ci/bin/pip install -r requirements.txt; fi
if [ -f requirements-dev.txt ]; then .venv-ci/bin/pip install -r requirements-dev.txt; fi
fi
echo "${{ github.workspace }}/.venv-ci/bin" >> "$GITHUB_PATH"

- name: run-unit-tests
id: unit-tests
env:
POSTGRES_HOST: postgres
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
PYTHONPATH: ${{ github.workspace }}
run: |
coverage run -m pytest ./tests --exitfirst
if [ "${{ matrix.python-version }}" = "3.12" ]; then
coverage run -m pytest
coverage report
else
pytest
fi
6 changes: 3 additions & 3 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ Start with your changes!
### Test your changes

Happy with your modifications to the odev codebase? Then it's time to test it and make sure everything still works as
expected! Run `coverage run -m pytest tests` in your terminal, if any of the tests fails you will need to correct your
code until it passes.
expected! Run `coverage run -m pytest tests && coverage report` in your terminal, if any of the tests fails you will need
to correct your code until it passes.

You implemented a brand new feature? Then it's probably good to implement new tests for it! Check what's inside the
[tests](./_tests/) directory for examples.
[tests](../tests/) directory for examples.

If you want to check the coverage of your code, you can now run `coverage html` and open the file `./htmlcov/index.html`
in your favorite browser.
Expand Down
2 changes: 1 addition & 1 deletion odev/commands/git/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def grouped_changes(self) -> dict[str, list[tuple[str, int, int]]]:

if not changes:
if self.args.worktree:
raise self.error(f"Worktree with name {self.args.name!r} does not exist")
raise self.error(f"Worktree with name {self.args.worktree!r} does not exist")
raise self.error("No worktrees found")

return changes
5 changes: 4 additions & 1 deletion odev/common/bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ def stream(command: str, env: dict[str, str] | None = None) -> Generator[str, No
tty.setraw(sys.stdin.fileno())
master, slave = pty.openpty()

process: Popen | None = None

try:
process = Popen( # noqa: S602
command,
Expand Down Expand Up @@ -226,5 +228,6 @@ def stream(command: str, env: dict[str, str] | None = None) -> Generator[str, No
os.close(slave)
os.close(master)
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_tty)
if process.returncode:

if process is not None and process.returncode:
raise CalledProcessError(process.returncode, command)
8 changes: 5 additions & 3 deletions odev/common/connectors/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def transaction(self):
except Exception as e:
self.execute("ROLLBACK")
raise e from e
finally:
else:
self.execute("COMMIT")


Expand Down Expand Up @@ -107,8 +107,10 @@ def invalidate_cache(self, database_name: str | None = None):
def nocache(self):
"""Context manager to disable caching of SQL queries."""
self.__class__._nocache = True
yield
self.__class__._nocache = False
try:
yield
finally:
self.__class__._nocache = False

def query(
self,
Expand Down
10 changes: 8 additions & 2 deletions odev/common/connectors/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class RestConnector(Connector, ABC):
_bypass_cache: ClassVar[bool] = False
"""Whether to bypass the cache for the current request."""

_default_timeout: ClassVar[float] = 30.0
"""Default timeout in seconds for outbound HTTP requests."""

def __init__(self, url: str):
"""Initialize the connector.
:param url: The URL of the endpoint.
Expand Down Expand Up @@ -215,8 +218,10 @@ def nocache(self):
"""Context manager to disable caching of HTTP requests."""
bypass_cache = RestConnector._bypass_cache
RestConnector._bypass_cache = True
yield
RestConnector._bypass_cache = bypass_cache
try:
yield
finally:
RestConnector._bypass_cache = bypass_cache

def _request(
self,
Expand Down Expand Up @@ -257,6 +262,7 @@ def _request(
url = self.url + path

kwargs.setdefault("allow_redirects", True)
kwargs.setdefault("timeout", self._default_timeout)
params = kwargs.pop("params", {})
obfuscate_params = obfuscate_params or []
obfuscated = {k: "xxxxx" for k in obfuscate_params if k in params}
Expand Down
11 changes: 10 additions & 1 deletion odev/common/odev.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
)

from git import GitCommandError, NoSuchPathError, Repo
from networkx import DiGraph, NetworkXUnfeasible, topological_sort
from networkx import DiGraph, NetworkXUnfeasible, simple_cycles, topological_sort
from packaging import version

from odev._version import __version__
Expand Down Expand Up @@ -791,6 +791,15 @@ def _plugins_dependency_tree(self) -> list[str]:
resolved_graph: list[str] = list(topological_sort(graph))
logger.debug(f"Resolved plugins dependency tree:\n{join_bullet(resolved_graph)}")
except NetworkXUnfeasible as exception:
cycles = list(simple_cycles(graph))[:20]
if cycles:
parts: list[str] = []
for c in cycles:
if len(c) == 1:
parts.append(f"{c[0]} depends on itself")
else:
parts.append(" → ".join([*c, c[0]]))
raise OdevError("Circular dependency detected in plugins: " + "; ".join(parts)) from exception
raise OdevError("Circular dependency detected in plugins") from exception

return resolved_graph
Expand Down
4 changes: 2 additions & 2 deletions odev/common/odoobin.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,14 +554,14 @@ def run( # noqa: PLR0913
:param subcommand_input: Input to pipe to the subcommand.
:param stream: Whether to stream the output of the process.
:param progress: Callback to call on each line outputted by the process. Ignored if `stream` is False.
:param prepare: Whether to prepare the environment before running the process.
:param prepare: Whether to prepare the environment before running. A missing venv is always prepared.
:return: The return result of the process after completion.
:rtype: subprocess.CompletedProcess
"""
if self.is_running and subcommand is None:
raise OdevError("Odoo is already running on this database")

if prepare:
if prepare or not self.venv.exists:
with spinner(f"Preparing odoo-bin version {str(self.version)!r} for database {self.database.name!r}"):
self.prepare_odoobin()

Expand Down
5 changes: 5 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[pytest]
testpaths = tests
addopts = --durations=25
markers =
integration: tests requiring external services or full non-mocked stacks (unused by default)
1 change: 1 addition & 0 deletions tests/fixtures/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def __patch_odev(cls):
("upgrades_path", cls.odev.tests_path / "resources" / "upgrades"),
("setup_path", cls.odev.tests_path / "resources" / "setup"),
("scripts_path", cls.odev.tests_path / "resources" / "scripts"),
("plugins_path", cls.run_path / "plugins"),
],
)

Expand Down
Loading
Loading