Skip to content

Commit

Permalink
Remove setuptools-odoo dependency.
Browse files Browse the repository at this point in the history
Implement metadata_from_addon_dir natively.
The test suite for that was already most developed than
setuptools-odoo's.
  • Loading branch information
sbidoul committed Sep 10, 2023
1 parent 1b47a0d commit 8931dea
Show file tree
Hide file tree
Showing 8 changed files with 643 additions and 61 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[run]
source =
manifestoo_core
8 changes: 0 additions & 8 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@

## `manifestoo_core.metadata`

```{note}
For this module to be functional, `manifestoo-core` must currently be installed
with the `metadata` extra (`pip install manifestoo-core[metadata]`) because it pulls
`setuptools-odoo` as a dependency to implement this functionality. In the future,
this feature will be implemented natively as part of `manifestoo-core` and the extra
will become unnecessary.
```

```{eval-rst}
.. automodule:: manifestoo_core.metadata
:members:
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ classifiers = [
]
readme = "README.md"
dependencies = [
"dataclasses ; python_version<'3.7'",
"importlib_resources ; python_version<'3.7'",
"packaging",
"typing-extensions ; python_version < '3.8'",
]
requires-python = ">=3.6"
dynamic = ["version"]

[project.optional-dependencies]
metadata = [
"setuptools-odoo>=3.1",
]
metadata = []
test = [
"pytest",
"coverage[toml]",
Expand Down
8 changes: 8 additions & 0 deletions src/manifestoo_core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ class AddonNotFoundNotADirectory(AddonNotFound):

class AddonNotFoundInvalidManifest(AddonNotFound):
pass


class InvalidDistributionName(ManifestooException):
pass


class UnknownPostVersionStrategy(ManifestooException):
pass
170 changes: 170 additions & 0 deletions src/manifestoo_core/git_postversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import os
import subprocess
import sys
from pathlib import Path
from typing import Iterator, List, Optional, TextIO

from packaging.version import parse as parse_version

if sys.version_info >= (3, 8):
from typing import Final
else:
from typing_extensions import Final

from .addon import Addon
from .exceptions import InvalidManifest, UnknownPostVersionStrategy
from .manifest import MANIFEST_NAMES, Manifest

POST_VERSION_STRATEGY_NONE: Final = "none"
POST_VERSION_STRATEGY_NINETYNINE_DEVN: Final = ".99.devN"
POST_VERSION_STRATEGY_P1_DEVN: Final = "+1.devN"
POST_VERSION_STRATEGY_DOT_N: Final = ".N"


def _run_git_command_exit_code(
args: List[str], cwd: Optional[Path] = None, stderr: Optional[TextIO] = None
) -> int:
return subprocess.call(["git", *args], cwd=cwd, stderr=stderr)


def _run_git_command_bytes(
args: List[str], cwd: Optional[Path] = None, stderr: Optional[TextIO] = None
) -> str:
output = subprocess.check_output(
["git", *args], cwd=cwd, universal_newlines=True, stderr=stderr
)
return output.strip()


def _run_git_command_lines(
args: List[str], cwd: Optional[Path] = None, stderr: Optional[TextIO] = None
) -> List[str]:
output = _run_git_command_bytes(args, cwd=cwd, stderr=stderr)
return output.split("\n")


def _is_git_controlled(path: Path) -> bool:
with open(os.devnull, "w") as devnull:
r = _run_git_command_exit_code(["rev-parse"], cwd=path, stderr=devnull)
return r == 0


def _get_git_uncommitted(path: Path) -> bool:
r = _run_git_command_exit_code(["diff", "--quiet", "--exit-code", "."], cwd=path)
return r != 0


def _get_git_root(path: Path) -> Path:
return Path(_run_git_command_bytes(["rev-parse", "--show-toplevel"], cwd=path))


def _git_log_iterator(path: Path) -> Iterator[str]:
"""yield commits using git log -- <dir>"""
n = 10
count = 0
while True:
lines = _run_git_command_lines(
["log", "--oneline", "-n", str(n), "--skip", str(count), "--", "."],
cwd=path,
)
for line in lines:
sha = line.split(" ", 1)[0]
count += 1
yield sha
if len(lines) < n:
break


def _read_manifest_from_sha(
sha: str, addon_dir: Path, git_root: Path
) -> Optional[Manifest]:
rel_addon_dir = addon_dir.relative_to(git_root)
for manifest_name in MANIFEST_NAMES:
manifest_path = rel_addon_dir / manifest_name
try:
with Path(os.devnull).open("w") as devnull:
s = _run_git_command_bytes(
["show", f"{sha}:{manifest_path}"], cwd=git_root, stderr=devnull
)
except subprocess.CalledProcessError:
continue
try:
return Manifest.from_str(s)
except InvalidManifest:
break
return None


def _bump_last(version: str) -> str:
int_version = [int(i) for i in version.split(".")]
int_version[-1] += 1
return ".".join(str(i) for i in int_version)


def get_git_postversion(addon: Addon, strategy: str) -> str: # noqa: C901 too complex
"""return the addon version number, with a developmental version increment
if there were git commits in the addon_dir after the last version change.
If the last change to the addon correspond to the version number in the
manifest it is used as is for the python package version. Otherwise a
counter is incremented for each commit and resulting version number has
the following form, depending on the strategy (N being the number of git
commits since the version change):
* STRATEGY_NONE: return the version in the manifest as is
* STRATEGY_99_DEVN: [8|9].0.x.y.z.99.devN
* STRATEGY_P1_DEVN: [series].0.x.y.(z+1).devN
* STRATEGY_DOT_N: [series].0.x.y.z.N
Notes:
* pip ignores .postN by design (https://github.com/pypa/pip/issues/2872)
* x.y.z.devN is anterior to x.y.z
Note: we don't put the sha1 of the commit in the version number because
this is not PEP 440 compliant and is therefore misinterpreted by pip.
"""
last_version = addon.manifest.version or "0.0.0"
addon_dir = addon.path.resolve()
if strategy == POST_VERSION_STRATEGY_NONE:
return last_version
last_version_parsed = parse_version(last_version)
if not _is_git_controlled(addon_dir):
return last_version
if _get_git_uncommitted(addon_dir):
uncommitted = True
count = 1
else:
uncommitted = False
count = 0
last_sha = None
git_root = _get_git_root(addon_dir)
for sha in _git_log_iterator(addon_dir):
manifest = _read_manifest_from_sha(sha, addon_dir, git_root)
if manifest is None:
break
version = manifest.version or "0.0.0"
version_parsed = parse_version(version)
if version_parsed != last_version_parsed:
break
if last_sha is None:
last_sha = sha
else:
count += 1
if not count:
return last_version
if last_sha:
if strategy == POST_VERSION_STRATEGY_NINETYNINE_DEVN:
return last_version + ".99.dev%s" % count
if strategy == POST_VERSION_STRATEGY_P1_DEVN:
return _bump_last(last_version) + ".dev%s" % count
if strategy == POST_VERSION_STRATEGY_DOT_N:
return last_version + ".%s" % count
msg = f"Unknown postversion strategy: {strategy}"
raise UnknownPostVersionStrategy(msg)
if uncommitted:
return last_version + ".dev1"
# if everything is committed, the last commit
# must have the same version as current,
# so last_sha must be set and we'll never reach this branch
return last_version
22 changes: 20 additions & 2 deletions src/manifestoo_core/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
T = TypeVar("T")
VT = TypeVar("VT")

__all__ = ["Manifest", "InvalidManifest", "get_manifest_path"]
__all__ = ["Manifest", "InvalidManifest", "get_manifest_path", "MANIFEST_NAMES"]

MANIFEST_NAMES = ("__manifest__.py", "__openerp__.py", "__terp__.py")


def _check_str(value: Any) -> str:
Expand Down Expand Up @@ -61,7 +63,7 @@ def get_manifest_path(addon_dir: Path) -> Optional[Path]:
Returns None if no manifest file is found.
"""
for manifest_name in ("__manifest__.py", "__openerp__.py", "__terp__.py"):
for manifest_name in MANIFEST_NAMES:
manifest_path = addon_dir / manifest_name
if manifest_path.is_file():
return manifest_path
Expand All @@ -88,6 +90,14 @@ def _get(self, key: str, checker: Callable[[Any], T], default: T) -> T:
def name(self) -> Optional[str]:
return self._get("name", _check_optional_str, default=None)

@property
def summary(self) -> Optional[str]:
return self._get("summary", _check_optional_str, default=None)

@property
def description(self) -> Optional[str]:
return self._get("description", _check_optional_str, default=None)

@property
def version(self) -> Optional[str]:
return self._get("version", _check_optional_str, default=None)
Expand All @@ -110,6 +120,14 @@ def external_dependencies(self) -> Dict[str, List[str]]:
def license(self) -> Optional[str]: # noqa: A003
return self._get("license", _check_optional_str, default=None)

@property
def author(self) -> Optional[str]:
return self._get("author", _check_optional_str, default=None)

@property
def website(self) -> Optional[str]:
return self._get("website", _check_optional_str, default=None)

@property
def development_status(self) -> Optional[str]:
return self._get("development_status", _check_optional_str, default=None)
Expand Down
Loading

0 comments on commit 8931dea

Please sign in to comment.