yd-cli 0.7__tar.gz → 0.8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yd-cli
3
- Version: 0.7
3
+ Version: 0.8
4
4
  Summary: CLI tool to synchronize directories using rsync.
5
5
  Author: Christian Heinze
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.10,<0.12" ]
4
4
 
5
5
  [project]
6
6
  name = "yd-cli"
7
- version = "0.7"
7
+ version = "0.8"
8
8
  description = "CLI tool to synchronize directories using rsync."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -147,6 +147,8 @@ lint.ignore = [
147
147
  "PLR0913",
148
148
  # Comparison with hardcoded literals allowed.
149
149
  "PLR2004",
150
+ # open(Path) is ok
151
+ "PTH123",
150
152
  "Q000",
151
153
  "Q001",
152
154
  "Q002",
@@ -24,7 +24,6 @@ import yd
24
24
  if TYPE_CHECKING:
25
25
  import enum
26
26
  from collections.abc import Callable
27
- from collections.abc import Set as AbstractSet
28
27
 
29
28
  from rich.text import Text
30
29
 
@@ -41,7 +40,6 @@ class StrEnumHighlighter[T: enum.StrEnum](
41
40
  enum_type: type[T]
42
41
 
43
42
  _: dataclasses.KW_ONLY
44
- ignore: AbstractSet[T] = dataclasses.field(default_factory=set)
45
43
  base_style_name: str | None = None
46
44
 
47
45
  @property
@@ -55,38 +53,36 @@ class StrEnumHighlighter[T: enum.StrEnum](
55
53
  def highlight(self, text: Text) -> None:
56
54
  base_style, hl = self.base_style, text.highlight_regex
57
55
  for cat in self.enum_type:
58
- if cat in self.ignore:
59
- continue
60
56
  # Using `__str__` instead of `cat.name.lower()` for more flexibility.
61
57
  hl(rf"(?P<{cat}>{cat})", style_prefix=base_style)
62
58
 
63
59
  def create_theme_addon(self, *, styler: Callable[[T], str]) -> dict[str, str]:
64
60
  base_style = self.base_style
65
- return {
66
- base_style + str(cat): styler(cat)
67
- for cat in self.enum_type
68
- if cat not in self.ignore
69
- }
61
+ return {base_style + str(cat): styler(cat) for cat in self.enum_type}
62
+
63
+
64
+ def _action_name(action: yd.types.Action) -> str:
65
+ if action is yd.types.Action.HARDLINK:
66
+ return str(yd.types.Action.COPY)
67
+ return str(action)
70
68
 
71
69
 
72
70
  def _action_styler(action: yd.types.Action) -> str:
73
71
  match action:
74
72
  case yd.types.Action.CREATE:
75
- return "bold #aaccff on #0050aa"
73
+ return "bold #606060 on #0050aa"
76
74
  case yd.types.Action.COPY:
77
- return "bold #aaffbb on #006020"
75
+ return "bold #606060 on #008020"
78
76
  case yd.types.Action.DELETE:
79
- return "bold #ffaaaa on #aa0000"
77
+ return "bold #606060 on #aa0000"
80
78
  case yd.types.Action.HARDLINK:
81
79
  return "bold #aaaaaa on #606060"
82
80
  case yd.types.Action.ATTRUPDATE:
83
- return "bold #aaaaaa on #606060"
81
+ return "bold #606060 on #ffffaa"
84
82
 
85
83
 
86
84
  def _setup_rich_devices() -> tuple[rich.console.Console, StrEnumHighlighter]:
87
- highlighter = StrEnumHighlighter(
88
- yd.types.Action, ignore={yd.types.Action.ATTRUPDATE}
89
- )
85
+ highlighter = StrEnumHighlighter(yd.types.Action)
90
86
  theme = rich.theme.Theme(
91
87
  highlighter.create_theme_addon(styler=_action_styler)
92
88
  | {
@@ -332,11 +328,9 @@ def run(
332
328
  table_column=rich.table.Column(min_width=7),
333
329
  ),
334
330
  )
335
- for action_name in (
336
- str(cat)
337
- for cat in highlighter.enum_type
338
- if cat not in highlighter.ignore
339
- )
331
+ for action_name in {
332
+ _action_name(cat): None for cat in highlighter.enum_type
333
+ }
340
334
  ),
341
335
  console=console,
342
336
  ) as progress,
@@ -355,19 +349,16 @@ def run(
355
349
  console.print(text, style="info")
356
350
  return
357
351
  case yd.types.Transaction() as transaction:
358
- if transaction.action is yd.types.Action.ATTRUPDATE:
359
- return
360
-
361
352
  console.print(
362
353
  template.format(
363
- action=transaction.action,
354
+ action=_action_name(transaction.action),
364
355
  filename=target.joinpath(transaction.filename).as_posix()
365
356
  if not Path(transaction.filename).is_absolute()
366
357
  else transaction.filename,
367
358
  transfer_bytes=transaction.transfer_bytes,
368
359
  )
369
360
  )
370
- counter[str(transaction.action)] += 1
361
+ counter[_action_name(transaction.action)] += 1
371
362
  progress.update(
372
363
  taskid,
373
364
  **counter, # type: ignore[ty:invalid-argument-type]
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import itertools as it
9
9
  import logging
10
+ import sys
10
11
  import uuid
11
12
  from pathlib import Path
12
13
 
@@ -48,8 +49,10 @@ def _format_path(path: Path, /) -> str:
48
49
 
49
50
  def _create_error_command(
50
51
  enc_msg: str, /, environment: types.Environment
51
- ) -> tuple[str, tuple[str]]:
52
- return environment.echo_bin, (enc_msg,)
52
+ ) -> tuple[str, tuple[str, ...]]:
53
+ if environment.echo_bin is not None:
54
+ return environment.echo_bin, (enc_msg,)
55
+ return sys.executable, ("-m", "yd.echo", enc_msg)
53
56
 
54
57
 
55
58
  def create( # noqa: PLR0915
@@ -0,0 +1,12 @@
1
+ """Echo command replacement.
2
+
3
+ This is called instead of `echo` if the `echo` command is not available.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ if __name__ == "__main__":
11
+ if len(sys.argv) > 1:
12
+ print(sys.argv[1])
@@ -59,10 +59,7 @@ def _find_binary(binary: str, /, accept_failure: bool = False) -> str | None:
59
59
 
60
60
 
61
61
  def capture_environment() -> types.Environment:
62
- """Capture environment.
63
-
64
- Raises `types.ConfigLoadError` in case of failure.
65
- """
62
+ """Capture environment."""
66
63
  home_dir = Path.home()
67
64
  if not home_dir.exists():
68
65
  raise types.EnvCaptureError(f"home dir `{home_dir}` does not exist")
@@ -85,11 +82,12 @@ def capture_environment() -> types.Environment:
85
82
  rsync_bin = _find_binary(_getenv("YD_RSYNC", "rsync"))
86
83
  except FileNotFoundError as err:
87
84
  raise types.EnvCaptureError("rsync binary not found") from err
88
- try:
89
- echo_bin = _find_binary(_getenv("YD_ECHO", "echo"))
90
- except FileNotFoundError as err:
91
- raise types.EnvCaptureError("echo binary not found") from err
85
+ echo_bin = _find_binary(_getenv("YD_ECHO", "echo"), accept_failure=True)
86
+ if echo_bin is None:
87
+ log.warning("Failed to find echo command. Resorting to builtin alternative.")
92
88
  editor_bin = _find_binary(_getenv("EDITOR", "vim"), accept_failure=True)
89
+ if editor_bin is None:
90
+ log.warning("Failed to find editor command.")
93
91
 
94
92
  try:
95
93
  log_level: int | None = int(os.getenv("YD_LOGLEVEL", ""))
@@ -116,7 +114,7 @@ def load_command_group(path: Path, /) -> types.CommandGroup:
116
114
  Path to TOML file specifying the program.
117
115
  """
118
116
  try:
119
- with path.open(mode="rb") as fh:
117
+ with open(path, mode="rb") as fh:
120
118
  return msgspec.toml.decode(fh.read(), type=types.CommandGroup)
121
119
  except FileNotFoundError as err:
122
120
  log.exception("missing configuration file `%s`", path)
@@ -141,7 +139,7 @@ def list_available_configs(config_dir: Path, /) -> Iterable[types.AvailableConfi
141
139
  if not path.is_file():
142
140
  continue
143
141
  try:
144
- with path.open(encoding="utf-8") as fh:
142
+ with open(path, encoding="utf-8") as fh:
145
143
  # Benchmarked variants using one or more of `itertools.takewhile` and
146
144
  # `yield` but this had best runtime.
147
145
  lines = []
@@ -16,26 +16,26 @@ if TYPE_CHECKING:
16
16
  from pathlib import Path
17
17
 
18
18
 
19
- class SyfError(RuntimeError): ...
19
+ class YdError(RuntimeError): ...
20
20
 
21
21
 
22
- class ConfigLoadError(SyfError):
22
+ class ConfigLoadError(YdError):
23
23
  """Exception raised when loading a configuration fails."""
24
24
 
25
25
 
26
- class EnvCaptureError(SyfError):
26
+ class EnvCaptureError(YdError):
27
27
  """Exception raised when capturing the environment fails."""
28
28
 
29
29
 
30
- class CommandBuildError(SyfError):
30
+ class CommandBuildError(YdError):
31
31
  """Exception raised when building the rsync command fails."""
32
32
 
33
33
 
34
- class OutputParseError(SyfError):
34
+ class OutputParseError(YdError):
35
35
  """Exception raised when parsing output of other CLI apps fails."""
36
36
 
37
37
 
38
- class OutputConsumeError(SyfError):
38
+ class OutputConsumeError(YdError):
39
39
  """Exception raised when the consumption of parsed output fails."""
40
40
 
41
41
 
@@ -48,7 +48,7 @@ class Environment:
48
48
  config_dir: Path
49
49
 
50
50
  rsync_bin: str
51
- echo_bin: str
51
+ echo_bin: str | None
52
52
  editor_bin: str | None
53
53
 
54
54
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes