Skip to content

Commit 293b265

Browse files
committed
refactor(Init): make project_info a module and remove self.project_info
1 parent 1bb40c7 commit 293b265

File tree

4 files changed

+147
-88
lines changed

4 files changed

+147
-88
lines changed

commitizen/commands/init.py

Lines changed: 7 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
from __future__ import annotations
22

3-
import os
4-
import shutil
3+
from pathlib import Path
54
from typing import Any, NamedTuple
65

76
import questionary
87
import yaml
98

10-
from commitizen import cmd, factory, out
9+
from commitizen import cmd, factory, out, project_info
1110
from commitizen.__version__ import __version__
1211
from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig
1312
from commitizen.cz import registry
@@ -65,65 +64,13 @@ def title(self) -> str:
6564
)
6665

6766

68-
class ProjectInfo:
69-
"""Discover information about the current folder."""
70-
71-
@property
72-
def has_pyproject(self) -> bool:
73-
return os.path.isfile("pyproject.toml")
74-
75-
@property
76-
def has_uv_lock(self) -> bool:
77-
return os.path.isfile("uv.lock")
78-
79-
@property
80-
def has_setup(self) -> bool:
81-
return os.path.isfile("setup.py")
82-
83-
@property
84-
def has_pre_commit_config(self) -> bool:
85-
return os.path.isfile(".pre-commit-config.yaml")
86-
87-
@property
88-
def is_python_uv(self) -> bool:
89-
return self.has_pyproject and self.has_uv_lock
90-
91-
@property
92-
def is_python_poetry(self) -> bool:
93-
if not self.has_pyproject:
94-
return False
95-
with open("pyproject.toml") as f:
96-
return "[tool.poetry]" in f.read()
97-
98-
@property
99-
def is_python(self) -> bool:
100-
return self.has_pyproject or self.has_setup
101-
102-
@property
103-
def is_rust_cargo(self) -> bool:
104-
return os.path.isfile("Cargo.toml")
105-
106-
@property
107-
def is_npm_package(self) -> bool:
108-
return os.path.isfile("package.json")
109-
110-
@property
111-
def is_php_composer(self) -> bool:
112-
return os.path.isfile("composer.json")
113-
114-
@property
115-
def is_pre_commit_installed(self) -> bool:
116-
return bool(shutil.which("pre-commit"))
117-
118-
11967
class Init:
12068
_PRE_COMMIT_CONFIG_PATH = ".pre-commit-config.yaml"
12169

12270
def __init__(self, config: BaseConfig, *args: object) -> None:
12371
self.config: BaseConfig = config
12472
self.encoding = config.settings["encoding"]
12573
self.cz = factory.committer_factory(self.config)
126-
self.project_info = ProjectInfo()
12774

12875
def __call__(self) -> None:
12976
if self.config.path:
@@ -195,14 +142,10 @@ def __call__(self) -> None:
195142
out.success("Configuration complete 🚀")
196143

197144
def _ask_config_path(self) -> str:
198-
default_path = (
199-
"pyproject.toml" if self.project_info.has_pyproject else ".cz.toml"
200-
)
201-
202145
name: str = questionary.select(
203146
"Please choose a supported config file: ",
204147
choices=CONFIG_FILES,
205-
default=default_path,
148+
default=project_info.get_default_config_filename(),
206149
style=self.cz.style,
207150
).unsafe_ask()
208151
return name
@@ -267,37 +210,17 @@ def _ask_version_provider(self) -> str:
267210
"Choose the source of the version:",
268211
choices=_VERSION_PROVIDER_CHOICES,
269212
style=self.cz.style,
270-
default=self._default_version_provider,
213+
default=project_info.get_default_version_provider(),
271214
).unsafe_ask()
272215
return version_provider
273216

274-
@property
275-
def _default_version_provider(self) -> str:
276-
if self.project_info.is_python:
277-
if self.project_info.is_python_poetry:
278-
return "poetry"
279-
if self.project_info.is_python_uv:
280-
return "uv"
281-
return "pep621"
282-
283-
if self.project_info.is_rust_cargo:
284-
return "cargo"
285-
if self.project_info.is_npm_package:
286-
return "npm"
287-
if self.project_info.is_php_composer:
288-
return "composer"
289-
290-
return "commitizen"
291-
292217
def _ask_version_scheme(self) -> str:
293218
"""Ask for setting: version_scheme"""
294-
default_scheme = "pep440" if self.project_info.is_python else "semver"
295-
296219
scheme: str = questionary.select(
297220
"Choose version scheme: ",
298221
choices=KNOWN_SCHEMES,
299222
style=self.cz.style,
300-
default=default_scheme,
223+
default=project_info.get_default_version_scheme(),
301224
).unsafe_ask()
302225
return scheme
303226

@@ -351,8 +274,7 @@ def _get_config_data(self) -> dict[str, Any]:
351274
],
352275
}
353276

354-
if not self.project_info.has_pre_commit_config:
355-
# .pre-commit-config.yaml does not exist
277+
if not Path(".pre-commit-config.yaml").is_file():
356278
return {"repos": [CZ_HOOK_CONFIG]}
357279

358280
with open(self._PRE_COMMIT_CONFIG_PATH, encoding=self.encoding) as config_file:
@@ -377,7 +299,7 @@ def _install_pre_commit_hook(self, hook_types: list[str] | None = None) -> None:
377299
) as config_file:
378300
yaml.safe_dump(config_data, stream=config_file)
379301

380-
if not self.project_info.is_pre_commit_installed:
302+
if not project_info.is_pre_commit_installed():
381303
raise InitFailedError("pre-commit is not installed in current environment.")
382304
if hook_types is None:
383305
hook_types = ["commit-msg", "pre-push"]

commitizen/project_info.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Resolves project information about the current working directory."""
2+
3+
import shutil
4+
from pathlib import Path
5+
from typing import Literal
6+
7+
8+
def is_pre_commit_installed() -> bool:
9+
return bool(shutil.which("pre-commit"))
10+
11+
12+
def get_default_version_provider() -> Literal[
13+
"commitizen", "cargo", "composer", "npm", "pep621", "poetry", "uv"
14+
]:
15+
pyproject_path = Path("pyproject.toml")
16+
if pyproject_path.is_file():
17+
if "[tool.poetry]" in pyproject_path.read_text():
18+
return "poetry"
19+
if Path("uv.lock").is_file():
20+
return "uv"
21+
return "pep621"
22+
23+
if Path("setup.py").is_file():
24+
return "pep621"
25+
26+
if Path("Cargo.toml").is_file():
27+
return "cargo"
28+
29+
if Path("package.json").is_file():
30+
return "npm"
31+
32+
if Path("composer.json").is_file():
33+
return "composer"
34+
35+
return "commitizen"
36+
37+
38+
def get_default_config_filename() -> Literal["pyproject.toml", ".cz.toml"]:
39+
return "pyproject.toml" if Path("pyproject.toml").is_file() else ".cz.toml"
40+
41+
42+
def get_default_version_scheme() -> Literal["pep440", "semver"]:
43+
return (
44+
"pep440"
45+
if Path("pyproject.toml").is_file() or Path("setup.py").is_file()
46+
else "semver"
47+
)

tests/commands/test_init_command.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def test_executed_pre_commit_command(config: BaseConfig):
127127
def pre_commit_installed(mocker: MockFixture):
128128
# Assume the `pre-commit` is installed
129129
mocker.patch(
130-
"commitizen.commands.init.ProjectInfo.is_pre_commit_installed",
130+
"commitizen.project_info.is_pre_commit_installed",
131131
return_value=True,
132132
)
133133
# And installation success (i.e. no exception raised)
@@ -237,7 +237,7 @@ def test_pre_commit_not_installed(
237237
):
238238
# Assume `pre-commit` is not installed
239239
mocker.patch(
240-
"commitizen.commands.init.ProjectInfo.is_pre_commit_installed",
240+
"commitizen.project_info.is_pre_commit_installed",
241241
return_value=False,
242242
)
243243
with tmpdir.as_cwd():
@@ -249,7 +249,7 @@ def test_pre_commit_exec_failed(
249249
):
250250
# Assume `pre-commit` is installed
251251
mocker.patch(
252-
"commitizen.commands.init.ProjectInfo.is_pre_commit_installed",
252+
"commitizen.project_info.is_pre_commit_installed",
253253
return_value=True,
254254
)
255255
# But pre-commit installation will fail

tests/test_project_info.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Tests for project_info module."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from commitizen import project_info
10+
11+
12+
def _create_project_files(files: dict[str, str | None]) -> None:
13+
for file_path, content in files.items():
14+
path = Path(file_path)
15+
if content is None:
16+
path.touch()
17+
else:
18+
path.write_text(content)
19+
20+
21+
@pytest.mark.parametrize(
22+
"which_return, expected",
23+
[
24+
("/usr/local/bin/pre-commit", True),
25+
(None, False),
26+
("", False),
27+
],
28+
)
29+
def test_is_pre_commit_installed(mocker, which_return, expected):
30+
mocker.patch("shutil.which", return_value=which_return)
31+
assert project_info.is_pre_commit_installed() is expected
32+
33+
34+
@pytest.mark.parametrize(
35+
"files, expected",
36+
[
37+
(
38+
{"pyproject.toml": '[tool.poetry]\nname = "test"\nversion = "0.1.0"'},
39+
"poetry",
40+
),
41+
({"pyproject.toml": "", "uv.lock": ""}, "uv"),
42+
(
43+
{"pyproject.toml": '[tool.commitizen]\nversion = "0.1.0"'},
44+
"pep621",
45+
),
46+
({"setup.py": ""}, "pep621"),
47+
({"Cargo.toml": ""}, "cargo"),
48+
({"package.json": ""}, "npm"),
49+
({"composer.json": ""}, "composer"),
50+
({}, "commitizen"),
51+
(
52+
{
53+
"pyproject.toml": "",
54+
"Cargo.toml": "",
55+
"package.json": "",
56+
"composer.json": "",
57+
},
58+
"pep621",
59+
),
60+
],
61+
)
62+
def test_get_default_version_provider(chdir, files, expected):
63+
_create_project_files(files)
64+
assert project_info.get_default_version_provider() == expected
65+
66+
67+
@pytest.mark.parametrize(
68+
"files, expected",
69+
[
70+
({"pyproject.toml": ""}, "pyproject.toml"),
71+
({}, ".cz.toml"),
72+
],
73+
)
74+
def test_get_default_config_filename(chdir, files, expected):
75+
_create_project_files(files)
76+
assert project_info.get_default_config_filename() == expected
77+
78+
79+
@pytest.mark.parametrize(
80+
"files, expected",
81+
[
82+
({"pyproject.toml": ""}, "pep440"),
83+
({"setup.py": ""}, "pep440"),
84+
({"package.json": ""}, "semver"),
85+
({}, "semver"),
86+
],
87+
)
88+
def test_get_default_version_scheme(chdir, files, expected):
89+
_create_project_files(files)
90+
assert project_info.get_default_version_scheme() == expected

0 commit comments

Comments
 (0)