Skip to content

Make Mercurial command configurable by an environment variable #1144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions changelog.d/20250612_144312_me_hg_command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND`

5 changes: 5 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
: a ``os.pathsep`` separated list
of directory names to ignore for root finding

`SETUPTOOLS_SCM_HG_COMMAND`
: command used for running Mercurial (defaults to ``hg``)

for example, set this to ``chg`` to reduce start-up overhead of Mercurial




Expand Down
6 changes: 4 additions & 2 deletions src/setuptools_scm/_file_finders/hg.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@

log = logging.getLogger(__name__)

HG_COMMAND = os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg")


def _hg_toplevel(path: str) -> str | None:
try:
return _run(
["hg", "root"],
[HG_COMMAND, "root"],
cwd=(path or "."),
check=True,
).parse_success(norm_real)
Expand All @@ -32,7 +34,7 @@ def _hg_toplevel(path: str) -> str | None:
def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
hg_files: set[str] = set()
hg_dirs = {toplevel}
res = _run(["hg", "files"], cwd=toplevel)
res = _run([HG_COMMAND, "files"], cwd=toplevel)
if res.returncode:
return set(), set()
for name in res.stdout.splitlines():
Expand Down
12 changes: 7 additions & 5 deletions src/setuptools_scm/hg.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@

log = logging.getLogger(__name__)

HG_COMMAND = os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg")


class HgWorkdir(Workdir):
@classmethod
def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None:
res = _run(["hg", "root"], wd)
res = _run([HG_COMMAND, "root"], wd)
if res.returncode:
return None
return cls(Path(res.stdout))
Expand All @@ -45,7 +47,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None:
# the dedicated class GitWorkdirHgClient)

branch, dirty_str, dirty_date = _run(
["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"],
[HG_COMMAND, "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"],
cwd=self.path,
check=True,
).stdout.split("\n")
Expand Down Expand Up @@ -108,7 +110,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None:
return None

def hg_log(self, revset: str, template: str) -> str:
cmd = ["hg", "log", "-r", revset, "-T", template]
cmd = [HG_COMMAND, "log", "-r", revset, "-T", template]

return _run(cmd, cwd=self.path, check=True).stdout

Expand Down Expand Up @@ -144,9 +146,9 @@ def check_changes_since_tag(self, tag: str | None) -> bool:


def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None:
_require_command("hg")
_require_command(HG_COMMAND)
if os.path.exists(os.path.join(root, ".hg/git")):
res = _run(["hg", "path"], root)
res = _run([HG_COMMAND, "path"], root)
if not res.returncode:
for line in res.stdout.split("\n"):
if line.startswith("default ="):
Expand Down
23 changes: 12 additions & 11 deletions src/setuptools_scm/hg_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ._run_cmd import CompletedProcess as _CompletedProcess
from ._run_cmd import run as _run
from .git import GitWorkdir
from .hg import HG_COMMAND
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's turn this into a module import to ease test instrumentation

from .hg import HgWorkdir

log = logging.getLogger(__name__)
Expand All @@ -25,26 +26,26 @@
class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
@classmethod
def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None:
res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path)
res = _run([HG_COMMAND, "root"], cwd=wd).parse_success(parse=Path)
if res is None:
return None
return cls(res)

def is_dirty(self) -> bool:
res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True)
res = _run([HG_COMMAND, "id", "-T", "{dirty}"], cwd=self.path, check=True)
return bool(res.stdout)

def get_branch(self) -> str | None:
res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path)
res = _run([HG_COMMAND, "id", "-T", "{bookmarks}"], cwd=self.path)
if res.returncode:
log.info("branch err %s", res)
return None
return res.stdout

def get_head_date(self) -> date | None:
return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success(
parse=date.fromisoformat, error_msg="head date err"
)
return _run(
[HG_COMMAND, "log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path
).parse_success(parse=date.fromisoformat, error_msg="head date err")

def is_shallow(self) -> bool:
return False
Expand All @@ -53,7 +54,7 @@ def fetch_shallow(self) -> None:
pass

def get_hg_node(self) -> str | None:
res = _run('hg log -r . -T "{node}"', cwd=self.path)
res = _run([HG_COMMAND, "log", "-r", ".", "-T", "{node}"], cwd=self.path)
if res.returncode:
return None
else:
Expand All @@ -77,7 +78,7 @@ def node(self) -> str | None:

if git_node is None:
# trying again after hg -> git
_run(["hg", "gexport"], cwd=self.path)
_run([HG_COMMAND, "gexport"], cwd=self.path)
git_node = self._hg2git(hg_node)

if git_node is None:
Expand All @@ -92,7 +93,7 @@ def node(self) -> str | None:
return git_node[:7]

def count_all_nodes(self) -> int:
res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path)
res = _run([HG_COMMAND, "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path)
return len(res.stdout)

def default_describe(self) -> _CompletedProcess:
Expand All @@ -104,7 +105,7 @@ def default_describe(self) -> _CompletedProcess:
"""
res = _run(
[
"hg",
HG_COMMAND,
"log",
"-r",
"(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))",
Expand Down Expand Up @@ -132,7 +133,7 @@ def default_describe(self) -> _CompletedProcess:
logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
return _FAKE_GIT_DESCRIBE_ERROR

res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path)
res = _run([HG_COMMAND, "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path)
if res.returncode:
return _FAKE_GIT_DESCRIBE_ERROR
distance = len(res.stdout) - 1
Expand Down
9 changes: 9 additions & 0 deletions testing/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import contextlib
import os
import shutil
import sys

from pathlib import Path
Expand Down Expand Up @@ -86,6 +87,14 @@ def wd(tmp_path: Path) -> WorkDir:
return WorkDir(target_wd)


@pytest.fixture(scope="session")
def hg_exe() -> str:
hg = shutil.which("hg")
if hg is None:
pytest.skip("hg executable not found")
return hg


@pytest.fixture
def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]:
tmp_path = tmp_path.resolve()
Expand Down
33 changes: 33 additions & 0 deletions testing/test_file_finder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import importlib
import os
import sys

Expand All @@ -8,6 +9,7 @@
import pytest

from setuptools_scm._file_finders import find_files
from setuptools_scm._file_finders import hg

from .wd_wrapper import WorkDir

Expand Down Expand Up @@ -245,3 +247,34 @@ def test_archive(
os.link("data/datafile", datalink)

assert set(find_files()) == _sep({archive_file, "data/datafile", "data/datalink"})


@pytest.fixture
def hg_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir:
try:
wd("hg init")
except OSError:
pytest.skip("hg executable not found")
(wd.cwd / "file").touch()
wd("hg add file")
monkeypatch.chdir(wd.cwd)
return wd


def test_hg_gone(hg_wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PATH", str(hg_wd.cwd / "not-existing"))
assert set(find_files()) == set()


def test_hg_command_from_env(
hg_wd: WorkDir,
monkeypatch: pytest.MonkeyPatch,
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
with monkeypatch.context() as m:
m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe)
m.setenv("PATH", str(hg_wd.cwd / "not-existing"))
request.addfinalizer(lambda: importlib.reload(hg))
importlib.reload(hg)
assert set(find_files()) == {"file"}
38 changes: 38 additions & 0 deletions testing/test_hg_git.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from __future__ import annotations

import importlib

import pytest

from setuptools_scm import Configuration
from setuptools_scm import hg
from setuptools_scm import hg_git
from setuptools_scm._run_cmd import CommandNotFoundError
from setuptools_scm._run_cmd import has_command
from setuptools_scm._run_cmd import run
from setuptools_scm.hg import parse
from testing.wd_wrapper import WorkDir


Expand Down Expand Up @@ -81,3 +88,34 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
wd("hg pull -u")
assert wd_git.get_version() == "17.33.0rc0"
assert wd.get_version() == "17.33.0rc0"


def test_hg_gone(
repositories_hg_git: tuple[WorkDir, WorkDir], monkeypatch: pytest.MonkeyPatch
) -> None:
wd = repositories_hg_git[0]
monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
config = Configuration()
wd.write("pyproject.toml", "[tool.setuptools_scm]")
with pytest.raises(CommandNotFoundError, match=r"hg"):
parse(wd.cwd, config=config)

assert wd.get_version(fallback_version="1.0") == "1.0"


def test_hg_command_from_env(
repositories_hg_git: tuple[WorkDir, WorkDir],
monkeypatch: pytest.MonkeyPatch,
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
wd = repositories_hg_git[0]
with monkeypatch.context() as m:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of reloading rhe modules lets turn the logic into a function the test can call

m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe)
m.setenv("PATH", str(wd.cwd / "not-existing"))
request.addfinalizer(lambda: importlib.reload(hg))
request.addfinalizer(lambda: importlib.reload(hg_git))
importlib.reload(hg)
importlib.reload(hg_git)
wd.write("pyproject.toml", "[tool.setuptools_scm]")
assert wd.get_version().startswith("0.1.dev0+")
32 changes: 32 additions & 0 deletions testing/test_mercurial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import importlib
import os

from pathlib import Path
Expand All @@ -9,6 +10,7 @@
import setuptools_scm._file_finders

from setuptools_scm import Configuration
from setuptools_scm import hg
from setuptools_scm._run_cmd import CommandNotFoundError
from setuptools_scm._run_cmd import has_command
from setuptools_scm.hg import archival_to_version
Expand Down Expand Up @@ -67,6 +69,36 @@ def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
assert wd.get_version(fallback_version="1.0") == "1.0"


def test_hg_command_from_env(
wd: WorkDir,
monkeypatch: pytest.MonkeyPatch,
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
with monkeypatch.context() as m:
m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe)
m.setenv("PATH", str(wd.cwd / "not-existing"))
request.addfinalizer(lambda: importlib.reload(hg))
importlib.reload(hg)
wd.write("pyproject.toml", "[tool.setuptools_scm]")
assert wd.get_version() == "0.0"


def test_hg_command_from_env_is_invalid(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest
) -> None:
with monkeypatch.context() as m:
m.setenv("SETUPTOOLS_SCM_HG_COMMAND", str(wd.cwd / "not-existing"))
request.addfinalizer(lambda: importlib.reload(hg))
importlib.reload(hg)
config = Configuration()
wd.write("pyproject.toml", "[tool.setuptools_scm]")
with pytest.raises(CommandNotFoundError, match=r"hg"):
parse(wd.cwd, config=config)

assert wd.get_version(fallback_version="1.0") == "1.0"


def test_find_files_stop_at_root_hg(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
Loading