rhiza 0.9.0__py3-none-any.whl → 0.9.1__py3-none-any.whl

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.
rhiza/__main__.py CHANGED
@@ -11,7 +11,7 @@ import typer
11
11
  from rhiza.cli import app
12
12
 
13
13
 
14
- def load_plugins(app: typer.Typer):
14
+ def load_plugins(app: typer.Typer) -> None:
15
15
  """Load plugins from entry points."""
16
16
  # 'rhiza.plugins' matches the group we defined in rhiza-tools
17
17
  plugin_entries = entry_points(group="rhiza.plugins")
rhiza/cli.py CHANGED
@@ -23,14 +23,14 @@ app = typer.Typer(
23
23
  """
24
24
  Rhiza - Manage reusable configuration templates for Python projects
25
25
 
26
- \x1b]8;;https://jebel-quant.github.io/rhiza-cli/\x1b\\https://jebel-quant.github.io/rhiza-cli/\x1b]8;;\x1b\\
26
+ https://jebel-quant.github.io/rhiza-cli/
27
27
  """
28
28
  ),
29
29
  add_completion=True,
30
30
  )
31
31
 
32
32
 
33
- def version_callback(value: bool):
33
+ def version_callback(value: bool) -> None:
34
34
  """Print version information and exit.
35
35
 
36
36
  Args:
@@ -54,7 +54,7 @@ def main(
54
54
  callback=version_callback,
55
55
  is_eager=True,
56
56
  ),
57
- ):
57
+ ) -> None:
58
58
  """Rhiza CLI main callback.
59
59
 
60
60
  This callback is executed before any command. It handles global options
@@ -107,7 +107,7 @@ def init(
107
107
  "--template-branch",
108
108
  help="Custom template branch. Defaults to 'main'.",
109
109
  ),
110
- ):
110
+ ) -> None:
111
111
  r"""Initialize or validate .rhiza/template.yml.
112
112
 
113
113
  Creates a default `.rhiza/template.yml` configuration file if one
@@ -157,7 +157,7 @@ def materialize(
157
157
  help="Create and checkout a new branch in the target repository for changes",
158
158
  ),
159
159
  force: bool = typer.Option(False, "--force", "-y", help="Overwrite existing files"),
160
- ):
160
+ ) -> None:
161
161
  r"""Inject Rhiza configuration templates into a target repository.
162
162
 
163
163
  Materializes configuration files from the template repository specified
@@ -190,7 +190,7 @@ def validate(
190
190
  help="Target git repository (defaults to current directory)",
191
191
  ),
192
192
  ] = Path("."),
193
- ):
193
+ ) -> None:
194
194
  r"""Validate Rhiza template configuration.
195
195
 
196
196
  Validates the .rhiza/template.yml file to ensure it is syntactically
@@ -227,7 +227,7 @@ def migrate(
227
227
  help="Target git repository (defaults to current directory)",
228
228
  ),
229
229
  ] = Path("."),
230
- ):
230
+ ) -> None:
231
231
  r"""Migrate project to the new .rhiza folder structure.
232
232
 
233
233
  This command helps transition projects to use the new `.rhiza/` folder
@@ -253,7 +253,7 @@ def migrate(
253
253
 
254
254
 
255
255
  @app.command()
256
- def welcome():
256
+ def welcome() -> None:
257
257
  r"""Display a friendly welcome message and explain what Rhiza is.
258
258
 
259
259
  Shows a welcome message, explains Rhiza's purpose, key features,
@@ -282,7 +282,7 @@ def uninstall(
282
282
  "-y",
283
283
  help="Skip confirmation prompt and proceed with deletion",
284
284
  ),
285
- ):
285
+ ) -> None:
286
286
  r"""Remove all Rhiza-managed files from the repository.
287
287
 
288
288
  Reads the `.rhiza.history` file and removes all files that were
@@ -327,7 +327,7 @@ def summarise(
327
327
  help="Output file path (defaults to stdout)",
328
328
  ),
329
329
  ] = None,
330
- ):
330
+ ) -> None:
331
331
  r"""Generate a summary of staged changes for PR descriptions.
332
332
 
333
333
  Analyzes staged git changes and generates a structured PR description
@@ -50,6 +50,8 @@ For more detailed usage examples and workflows, see the USAGE.md guide
50
50
  or try rhiza <command> --help
51
51
  """
52
52
 
53
- from .init import init # noqa: F401
54
- from .materialize import materialize # noqa: F401
55
- from .validate import validate # noqa: F401
53
+ from .init import init
54
+ from .materialize import materialize
55
+ from .validate import validate
56
+
57
+ __all__ = ["init", "materialize", "validate"]
rhiza/commands/init.py CHANGED
@@ -81,7 +81,7 @@ def _prompt_git_host() -> str:
81
81
  git_host = "github"
82
82
  logger.debug("Non-interactive mode detected, defaulting to github")
83
83
 
84
- return git_host
84
+ return str(git_host)
85
85
 
86
86
 
87
87
  def _get_include_paths_for_host(git_host: str) -> list[str]:
@@ -263,7 +263,7 @@ def init(
263
263
  git_host: str | None = None,
264
264
  template_repository: str | None = None,
265
265
  template_branch: str | None = None,
266
- ):
266
+ ) -> bool:
267
267
  """Initialize or validate .rhiza/template.yml in the target repository.
268
268
 
269
269
  Creates a default .rhiza/template.yml file if it doesn't exist,
@@ -20,6 +20,22 @@ from rhiza.models import RhizaTemplate
20
20
  from rhiza.subprocess_utils import get_git_executable
21
21
 
22
22
 
23
+ def _log_git_stderr_errors(stderr: str | None) -> None:
24
+ """Extract and log only relevant error messages from git stderr.
25
+
26
+ Args:
27
+ stderr: Git command stderr output.
28
+ """
29
+ if stderr:
30
+ # Extract relevant error message from git stderr
31
+ stderr_lines = stderr.strip().split("\n")
32
+ # Show only the most relevant error lines, skip verbose git output
33
+ for line in stderr_lines:
34
+ line = line.strip()
35
+ if line and (line.startswith("fatal:") or line.startswith("error:")):
36
+ logger.error(line)
37
+
38
+
23
39
  def _handle_target_branch(
24
40
  target: Path, target_branch: str | None, git_executable: str, git_env: dict[str, str]
25
41
  ) -> None:
@@ -52,6 +68,8 @@ def _handle_target_branch(
52
68
  [git_executable, "checkout", target_branch],
53
69
  cwd=target,
54
70
  check=True,
71
+ capture_output=True,
72
+ text=True,
55
73
  env=git_env,
56
74
  )
57
75
  else:
@@ -61,10 +79,14 @@ def _handle_target_branch(
61
79
  [git_executable, "checkout", "-b", target_branch],
62
80
  cwd=target,
63
81
  check=True,
82
+ capture_output=True,
83
+ text=True,
64
84
  env=git_env,
65
85
  )
66
86
  except subprocess.CalledProcessError as e:
67
- logger.error(f"Failed to create/checkout branch '{target_branch}': {e}")
87
+ logger.error(f"Failed to create/checkout branch '{target_branch}'")
88
+ _log_git_stderr_errors(e.stderr)
89
+ logger.error("Please ensure you have no uncommitted changes or conflicts")
68
90
  sys.exit(1)
69
91
 
70
92
 
@@ -91,6 +113,9 @@ def _validate_and_load_template(target: Path, branch: str) -> tuple[RhizaTemplat
91
113
 
92
114
  # Extract template configuration settings
93
115
  rhiza_repo = template.template_repository
116
+ if not rhiza_repo:
117
+ logger.error("template-repository is not configured in template.yml")
118
+ raise RuntimeError("template-repository is required") # noqa: TRY003
94
119
  rhiza_branch = template.template_branch or branch
95
120
  include_paths = template.include
96
121
  excluded_paths = template.exclude
@@ -181,11 +206,13 @@ def _clone_template_repository(
181
206
  )
182
207
  logger.debug("Git clone completed successfully")
183
208
  except subprocess.CalledProcessError as e:
184
- logger.error(f"Failed to clone repository: {e}")
185
- if e.stderr:
186
- logger.error(f"Git error: {e.stderr.strip()}")
187
- logger.error(f"Check that the repository exists and branch '{rhiza_branch}' is valid")
188
- raise
209
+ logger.error(f"Failed to clone repository from {git_url}")
210
+ _log_git_stderr_errors(e.stderr)
211
+ logger.error("Please check that:")
212
+ logger.error(" - The repository exists and is accessible")
213
+ logger.error(f" - Branch '{rhiza_branch}' exists in the repository")
214
+ logger.error(" - You have network access to the git hosting service")
215
+ sys.exit(1)
189
216
 
190
217
  # Initialize sparse checkout in cone mode
191
218
  try:
@@ -200,10 +227,9 @@ def _clone_template_repository(
200
227
  )
201
228
  logger.debug("Sparse checkout initialized")
202
229
  except subprocess.CalledProcessError as e:
203
- logger.error(f"Failed to initialize sparse checkout: {e}")
204
- if e.stderr:
205
- logger.error(f"Git error: {e.stderr.strip()}")
206
- raise
230
+ logger.error("Failed to initialize sparse checkout")
231
+ _log_git_stderr_errors(e.stderr)
232
+ sys.exit(1)
207
233
 
208
234
  # Set sparse checkout paths
209
235
  try:
@@ -218,10 +244,9 @@ def _clone_template_repository(
218
244
  )
219
245
  logger.debug("Sparse checkout paths configured")
220
246
  except subprocess.CalledProcessError as e:
221
- logger.error(f"Failed to set sparse checkout paths: {e}")
222
- if e.stderr:
223
- logger.error(f"Git error: {e.stderr.strip()}")
224
- raise
247
+ logger.error("Failed to configure sparse checkout paths")
248
+ _log_git_stderr_errors(e.stderr)
249
+ sys.exit(1)
225
250
 
226
251
 
227
252
  def _copy_files_to_target(
@@ -46,7 +46,7 @@ def get_staged_changes(repo_path: Path) -> dict[str, list[str]]:
46
46
  Returns:
47
47
  Dictionary with keys 'added', 'modified', 'deleted' containing file lists
48
48
  """
49
- changes = {
49
+ changes: dict[str, list[str]] = {
50
50
  "added": [],
51
51
  "modified": [],
52
52
  "deleted": [],
@@ -5,8 +5,9 @@ This module provides functionality to validate template.yml files in the
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
- import yaml
10
+ import yaml # type: ignore[import-untyped]
10
11
  from loguru import logger
11
12
 
12
13
 
@@ -98,7 +99,7 @@ def _check_template_file_exists(target: Path) -> tuple[bool, Path]:
98
99
  return True, template_file
99
100
 
100
101
 
101
- def _parse_yaml_file(template_file: Path) -> tuple[bool, dict | None]:
102
+ def _parse_yaml_file(template_file: Path) -> tuple[bool, dict[str, Any] | None]:
102
103
  """Parse YAML file and return configuration.
103
104
 
104
105
  Args:
@@ -125,7 +126,7 @@ def _parse_yaml_file(template_file: Path) -> tuple[bool, dict | None]:
125
126
  return True, config
126
127
 
127
128
 
128
- def _validate_required_fields(config: dict) -> bool:
129
+ def _validate_required_fields(config: dict[str, Any]) -> bool:
129
130
  """Validate required fields exist and have correct types.
130
131
 
131
132
  Args:
@@ -159,7 +160,7 @@ def _validate_required_fields(config: dict) -> bool:
159
160
  return validation_passed
160
161
 
161
162
 
162
- def _validate_repository_format(config: dict) -> bool:
163
+ def _validate_repository_format(config: dict[str, Any]) -> bool:
163
164
  """Validate template-repository format.
164
165
 
165
166
  Args:
@@ -186,7 +187,7 @@ def _validate_repository_format(config: dict) -> bool:
186
187
  return True
187
188
 
188
189
 
189
- def _validate_include_paths(config: dict) -> bool:
190
+ def _validate_include_paths(config: dict[str, Any]) -> bool:
190
191
  """Validate include paths.
191
192
 
192
193
  Args:
@@ -218,7 +219,7 @@ def _validate_include_paths(config: dict) -> bool:
218
219
  return True
219
220
 
220
221
 
221
- def _validate_optional_fields(config: dict) -> None:
222
+ def _validate_optional_fields(config: dict[str, Any]) -> None:
222
223
  """Validate optional fields if present.
223
224
 
224
225
  Args:
rhiza/commands/welcome.py CHANGED
@@ -10,7 +10,7 @@ and explains what Rhiza is and how it can help manage configuration templates.
10
10
  from rhiza import __version__
11
11
 
12
12
 
13
- def welcome():
13
+ def welcome() -> None:
14
14
  """Display a welcome message and explain what Rhiza is.
15
15
 
16
16
  Shows a friendly greeting, explains Rhiza's purpose, and provides
rhiza/models.py CHANGED
@@ -7,12 +7,13 @@ YAML parsing.
7
7
 
8
8
  from dataclasses import dataclass, field
9
9
  from pathlib import Path
10
+ from typing import Any
10
11
 
11
- import yaml
12
+ import yaml # type: ignore[import-untyped]
12
13
 
13
14
 
14
15
  def _normalize_to_list(value: str | list[str] | None) -> list[str]:
15
- """Convert a value to a list of strings.
16
+ r"""Convert a value to a list of strings.
16
17
 
17
18
  Handles the case where YAML multi-line strings (using |) are parsed as
18
19
  a single string instead of a list. Splits the string by newlines and
@@ -23,6 +24,20 @@ def _normalize_to_list(value: str | list[str] | None) -> list[str]:
23
24
 
24
25
  Returns:
25
26
  A list of strings. Empty list if value is None or empty.
27
+
28
+ Examples:
29
+ >>> _normalize_to_list(None)
30
+ []
31
+ >>> _normalize_to_list([])
32
+ []
33
+ >>> _normalize_to_list(['a', 'b', 'c'])
34
+ ['a', 'b', 'c']
35
+ >>> _normalize_to_list('single line')
36
+ ['single line']
37
+ >>> _normalize_to_list('line1\\n' + 'line2\\n' + 'line3')
38
+ ['line1', 'line2', 'line3']
39
+ >>> _normalize_to_list(' item1 \\n' + ' item2 ')
40
+ ['item1', 'item2']
26
41
  """
27
42
  if value is None:
28
43
  return []
@@ -30,7 +45,14 @@ def _normalize_to_list(value: str | list[str] | None) -> list[str]:
30
45
  return value
31
46
  if isinstance(value, str):
32
47
  # Split by newlines and filter out empty strings
33
- return [item.strip() for item in value.strip().split("\n") if item.strip()]
48
+ # Handle both actual newlines (\n) and literal backslash-n (\\n)
49
+ if "\\n" in value and "\n" not in value:
50
+ # Contains literal \n but not actual newlines
51
+ items = value.split("\\n")
52
+ else:
53
+ # Contains actual newlines or neither
54
+ items = value.split("\n")
55
+ return [item.strip() for item in items if item.strip()]
34
56
  return []
35
57
 
36
58
 
@@ -94,7 +116,7 @@ class RhizaTemplate:
94
116
  file_path.parent.mkdir(parents=True, exist_ok=True)
95
117
 
96
118
  # Convert to dictionary with YAML-compatible keys
97
- config = {}
119
+ config: dict[str, Any] = {}
98
120
 
99
121
  # Only include template-repository if it's not None
100
122
  if self.template_repository:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhiza
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: Reusable configuration templates for modern Python projects
5
5
  Project-URL: Homepage, https://github.com/jebel-quant/rhiza-cli
6
6
  Project-URL: Repository, https://github.com/jebel-quant/rhiza-cli
@@ -0,0 +1,21 @@
1
+ rhiza/__init__.py,sha256=4-Dy7AKbJneQNBfv30WhSsUin4y-g4Yp4veO6-YdjVg,1926
2
+ rhiza/__main__.py,sha256=GlTahVC8F6fKEpMw_nZyhp4PB-fyC9UQ_RjTZcpD9P4,837
3
+ rhiza/cli.py,sha256=y2qzFbrd29-j5OXslM96hZB9vDLvLv5FBnR9SnDdQWE,10613
4
+ rhiza/models.py,sha256=39oWJM13D_5FupCFUguiB8VMe5Mk4O8CPEpjPftNUjs,5039
5
+ rhiza/subprocess_utils.py,sha256=Pr5TysIKP76hc64fmqhTd6msMGn5DU43hOSR_v_GFb8,745
6
+ rhiza/_templates/basic/__init__.py.jinja2,sha256=gs8qN4LAKcdFd6iO9gZVLuVetODmZP_TGuEjWrbinC0,27
7
+ rhiza/_templates/basic/main.py.jinja2,sha256=uTCahxf9Bftao1IghHue4cSZ9YzBYmBEXeIhEmK9UXQ,362
8
+ rhiza/_templates/basic/pyproject.toml.jinja2,sha256=Mizpnnd_kFQd-pCWOxG-KWhvg4_ZhZaQppTt2pz0WOc,695
9
+ rhiza/commands/__init__.py,sha256=DV1nlcXxeeHXmHobVcfOsGeiZBZ55Xz1mud073AXDGc,1839
10
+ rhiza/commands/init.py,sha256=4pjNblghF8zFxKJUfw_wA5Ne_xKbovHR7iPxOPQ8zHg,9798
11
+ rhiza/commands/materialize.py,sha256=f6B6luANF089Wn4WzXq7V6oGeFdV6UiMY_-JxUyNfwQ,19741
12
+ rhiza/commands/migrate.py,sha256=A5t4nw7CrdtILCwuSoAqtmM0LpMK8KmX87gzlNgi7fQ,7522
13
+ rhiza/commands/summarise.py,sha256=vgc7M3dwuGjUuVs3XKQr2_g3qURnQH_R3eFoSSrnOro,11758
14
+ rhiza/commands/uninstall.py,sha256=6oO7kdv11Bq4JXjrBg9rsFtoRgttQ4m30zGr6NhZrkQ,7479
15
+ rhiza/commands/validate.py,sha256=z3ePMPYm2nYOYSxbVNWVU4cz52MNjCIz8ChAMnCsIes,10906
16
+ rhiza/commands/welcome.py,sha256=VknKaUh-bD6dM-zcD1eP4H-D_npyezpfF3bTSl_0Dio,2383
17
+ rhiza-0.9.1.dist-info/METADATA,sha256=5gMfSF0HgsGGEpE2IlblyWJwXxdzjzAZt6-rzExvkQk,26298
18
+ rhiza-0.9.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ rhiza-0.9.1.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
20
+ rhiza-0.9.1.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
21
+ rhiza-0.9.1.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- rhiza/__init__.py,sha256=4-Dy7AKbJneQNBfv30WhSsUin4y-g4Yp4veO6-YdjVg,1926
2
- rhiza/__main__.py,sha256=Q02upTGaJceknkDABdCwq5_vdMdGY8Cg3ej6WZIHs_s,829
3
- rhiza/cli.py,sha256=uU0tenEgTe0Wz5lIj0SZIeXy5n_9-8xtUMMP_i71CWA,10609
4
- rhiza/models.py,sha256=f4TT3XPMEE7ciyrpsXllje4eGJRkYOgpZOoFeRC-rVI,4225
5
- rhiza/subprocess_utils.py,sha256=Pr5TysIKP76hc64fmqhTd6msMGn5DU43hOSR_v_GFb8,745
6
- rhiza/_templates/basic/__init__.py.jinja2,sha256=gs8qN4LAKcdFd6iO9gZVLuVetODmZP_TGuEjWrbinC0,27
7
- rhiza/_templates/basic/main.py.jinja2,sha256=uTCahxf9Bftao1IghHue4cSZ9YzBYmBEXeIhEmK9UXQ,362
8
- rhiza/_templates/basic/pyproject.toml.jinja2,sha256=Mizpnnd_kFQd-pCWOxG-KWhvg4_ZhZaQppTt2pz0WOc,695
9
- rhiza/commands/__init__.py,sha256=QWEEVvdW3gKV-FpKgHRJL_H8FpQqvfck9JvnFMDz3gY,1834
10
- rhiza/commands/init.py,sha256=dpLwFbURyndkgw-v4O6gD0gg6ov6q023uOmaWGup2oA,9785
11
- rhiza/commands/materialize.py,sha256=1YmzlPz9-IxnV4znDZQijZ3cjfbBXdepmps963z4Hsg,18668
12
- rhiza/commands/migrate.py,sha256=A5t4nw7CrdtILCwuSoAqtmM0LpMK8KmX87gzlNgi7fQ,7522
13
- rhiza/commands/summarise.py,sha256=KJ8v4lHSbn8AIt3tJM_lSCizzZ2Bv2mqTfI0z7-kyxI,11736
14
- rhiza/commands/uninstall.py,sha256=6oO7kdv11Bq4JXjrBg9rsFtoRgttQ4m30zGr6NhZrkQ,7479
15
- rhiza/commands/validate.py,sha256=VriXJxyuhvGJajw0qiAUmXYKRMbpgKiZ5_MMtE6WlSo,10801
16
- rhiza/commands/welcome.py,sha256=u197cIlY1tXm-CN6YpyUX4Eq06pLeV0hGyNvT35tE8U,2375
17
- rhiza-0.9.0.dist-info/METADATA,sha256=6AIK6bVC54vxBE0NB9jdLkBuTUcn9W0jod6ixm1-EB0,26298
18
- rhiza-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
- rhiza-0.9.0.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
20
- rhiza-0.9.0.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
21
- rhiza-0.9.0.dist-info/RECORD,,
File without changes