bear-utils 0.9.2__py3-none-any.whl → 0.9.3__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.
@@ -1 +1 @@
1
- version = "0.9.2"
1
+ version = "0.9.3"
@@ -54,7 +54,7 @@ def get_version() -> ExitCode:
54
54
  return ExitCode.SUCCESS
55
55
 
56
56
 
57
- def bump_version() -> ExitCode:
57
+ def bump_version(args: list[str] | None = None) -> ExitCode:
58
58
  """CLI command to bump the version of the package."""
59
59
  parser = ArgumentParser(description="Bump the version of the package.")
60
60
  parser.add_argument(
@@ -63,21 +63,24 @@ def bump_version() -> ExitCode:
63
63
  choices=VALID_BUMP_TYPES,
64
64
  help=f"Type of version bump: {', '.join(VALID_BUMP_TYPES)}",
65
65
  )
66
- args: Namespace = parser.parse_args(sys.argv[1:])
67
- bump_args = [args.bump_type, debug.__PACKAGE_NAME__, _version]
68
- return cli_bump(bump_args)
66
+ _args: Namespace = parser.parse_args(args or sys.argv[1:])
67
+ return cli_bump([_args.bump_type, debug.__PACKAGE_NAME__, _version])
69
68
 
70
69
 
71
70
  def get_parser() -> ArgumentParser:
72
71
  name = debug._get_name()
73
72
  parser = ArgumentParser(description=name.capitalize(), prog=name, exit_on_error=False)
74
73
  parser.add_argument("-V", "--version", action=_Version, help="Print the version of the package")
74
+ subparser = parser.add_subparsers(dest="command", required=False, help="Available commands")
75
+ subparser.add_parser("get-version", help="Get the current version of the package")
76
+ bump = subparser.add_parser("bump-version", help="Bump the version of the package")
77
+ bump.add_argument("bump_type", type=str, choices=VALID_BUMP_TYPES, help="major, minor, or patch")
75
78
  parser.add_argument("--about", action=_About, help="Print information about the package")
76
79
  parser.add_argument("--debug_info", action=_DebugInfo, help="Print debug information")
77
80
  return parser
78
81
 
79
82
 
80
- def main(args: list[str] | None = None) -> int:
83
+ def main(args: list[str] | None = None) -> ExitCode:
81
84
  """Main entry point for the CLI.
82
85
 
83
86
  This function is called when the CLI is executed. It can be used to
@@ -94,7 +97,18 @@ def main(args: list[str] | None = None) -> int:
94
97
  try:
95
98
  parser: ArgumentParser = get_parser()
96
99
  opts: Namespace = parser.parse_args(args)
97
- print(opts)
100
+ command = opts.command
101
+ if command is None:
102
+ parser.print_help()
103
+ return ExitCode.SUCCESS
104
+ if command == "get-version":
105
+ return get_version()
106
+ if command == "bump-version":
107
+ if not hasattr(opts, "bump_type"):
108
+ print("Error: 'bump-version' command requires a 'bump_type' argument.", file=sys.stderr)
109
+ return ExitCode.FAILURE
110
+ bump_type = opts.bump_type
111
+ return bump_version([bump_type])
98
112
  except Exception as e:
99
113
  print(f"Error initializing CLI: {e}", file=sys.stderr)
100
114
  return ExitCode.FAILURE
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  import importlib.metadata
5
5
  from importlib.metadata import PackageNotFoundError, metadata, version
6
6
  import os
@@ -8,6 +8,7 @@ import platform
8
8
  import sys
9
9
 
10
10
  from bear_utils._internal._version import version as _version
11
+ from bear_utils.cli._get_version import Version
11
12
 
12
13
  __PACKAGE_NAME__ = "bear-utils"
13
14
 
@@ -20,9 +21,20 @@ class _Package:
20
21
  """Package name."""
21
22
  version: str = _version
22
23
  """Package version."""
24
+ _version: Version = field(default_factory=lambda: Version.from_string(_version))
23
25
  description: str = "No description available."
24
26
  """Package description."""
25
27
 
28
+ def __post_init__(self) -> None:
29
+ """Post-initialization to ensure version is a string."""
30
+ if not isinstance(self.version, str) or "0.0.0" in self.version:
31
+ self.version = version(self.name) if self.name else "0.0.0"
32
+ if not self.description:
33
+ try:
34
+ self.description = metadata(self.name)["Summary"]
35
+ except PackageNotFoundError:
36
+ self.description = "No description available."
37
+
26
38
  def __str__(self) -> str:
27
39
  """String representation of the package information."""
28
40
  return f"{self.name} v{self.version}: {self.description}"
@@ -158,4 +170,5 @@ def _print_debug_info() -> None:
158
170
 
159
171
 
160
172
  if __name__ == "__main__":
161
- _print_debug_info()
173
+ # _print_debug_info()
174
+ print(_get_package_info())
@@ -10,42 +10,40 @@ from typing import Literal, Self
10
10
  from pydantic import BaseModel
11
11
 
12
12
  from bear_utils.constants import ExitCode
13
- from bear_utils.constants._meta import RichStrEnum, StrValue as Value
13
+ from bear_utils.constants._meta import IntValue as Value, RichIntEnum
14
+ from bear_utils.extras import zap_as
14
15
 
15
16
 
16
- class VersionParts(RichStrEnum):
17
+ class VerParts(RichIntEnum):
17
18
  """Enumeration for version parts."""
18
19
 
19
- MAJOR = Value("major", "Major version")
20
- MINOR = Value("minor", "Minor version")
21
- PATCH = Value("patch", "Patch version")
20
+ MAJOR = Value(0, "major")
21
+ MINOR = Value(1, "minor")
22
+ PATCH = Value(2, "patch")
22
23
 
23
24
  @classmethod
24
25
  def choices(cls) -> list[str]:
25
26
  """Return a list of valid version parts."""
26
- return [version_part.value for version_part in cls]
27
+ return [version_part.text for version_part in cls]
27
28
 
28
29
  @classmethod
29
- def num(cls) -> int:
30
- """Return the number of valid version parts."""
30
+ def parts(cls) -> int:
31
+ """Return the total number of version parts."""
31
32
  return len(cls.choices())
32
33
 
33
34
 
34
- VALID_BUMP_TYPES: list[str] = VersionParts.choices()
35
- NUM_PARTS: int = VersionParts.num()
36
- MAJOR = VersionParts.MAJOR.str()
37
- MINOR = VersionParts.MINOR.str()
38
- PATCH = VersionParts.PATCH.str()
35
+ VALID_BUMP_TYPES: list[str] = VerParts.choices()
36
+ ALL_PARTS: int = VerParts.parts()
39
37
 
40
38
 
41
39
  class Version(BaseModel):
42
40
  """Model to represent a version string."""
43
41
 
44
- major: int
42
+ major: int = 0
45
43
  """Major version number."""
46
- minor: int
44
+ minor: int = 0
47
45
  """Minor version number."""
48
- patch: int
46
+ patch: int = 0
49
47
  """Patch version number."""
50
48
 
51
49
  @classmethod
@@ -61,10 +59,17 @@ class Version(BaseModel):
61
59
  Raises:
62
60
  ValueError: If the version string is not in the correct format.
63
61
  """
64
- parts = version_str.split(".")
65
- if len(parts) != VersionParts.num() or not all(part.isdigit() for part in parts):
66
- raise ValueError(f"Invalid version format: {version_str}")
67
- return cls(major=int(parts[0]), minor=int(parts[1]), patch=int(parts[2]))
62
+ try:
63
+ major, minor, patch = zap_as("-+", version_str, 3, replace=".", func=int)
64
+ return cls(major=int(major), minor=int(minor), patch=int(patch))
65
+ except ValueError as e:
66
+ raise ValueError(
67
+ f"Invalid version string format: {version_str}. Expected integers for major, minor, and patch."
68
+ ) from e
69
+
70
+ def increment(self, attr_name: str) -> None:
71
+ """Increment the specified part of the version."""
72
+ setattr(self, attr_name, getattr(self, attr_name) + 1)
68
73
 
69
74
  @property
70
75
  def version_string(self) -> str:
@@ -75,30 +80,22 @@ class Version(BaseModel):
75
80
  """
76
81
  return f"{self.major}.{self.minor}.{self.patch}"
77
82
 
78
- def new_version(self, bump_type: Literal["major", "minor", "patch"]) -> Version:
79
- """Return a new version string based on the bump type.
83
+ def default(self, part: str) -> None:
84
+ """Clear the specified part of the version.
80
85
 
81
86
  Args:
82
- bump_type: The type of bump ("major", "minor", or "patch").
83
-
84
- Returns:
85
- A new version string.
86
-
87
- Raises:
88
- ValueError: If the bump_type is unsupported.
87
+ part: The part of the version to clear.
89
88
  """
90
- match bump_type:
91
- case VersionParts.MAJOR:
92
- self.major += 1
93
- self.minor = 0
94
- self.patch = 0
95
- case VersionParts.MINOR:
96
- self.minor += 1
97
- self.patch = 0
98
- case VersionParts.PATCH:
99
- self.patch += 1
100
- case _:
101
- raise ValueError(f"Unsupported bump type: {bump_type}")
89
+ if hasattr(self, part):
90
+ setattr(self, part, 0)
91
+
92
+ def new_version(self, bump_type: str) -> Version:
93
+ """Return a new version string based on the bump type."""
94
+ bump_part: VerParts = VerParts.get(bump_type, default=VerParts.PATCH)
95
+ self.increment(bump_part.text)
96
+ for part in VerParts:
97
+ if part.value > bump_part.value:
98
+ self.default(part.text)
102
99
  return self
103
100
 
104
101
  @classmethod
@@ -135,6 +132,24 @@ def _bump_version(version: str, bump_type: Literal["major", "minor", "patch"]) -
135
132
  return ver.new_version(bump_type)
136
133
 
137
134
 
135
+ def _get_version(package_name: str) -> str:
136
+ """Get the version of the specified package.
137
+
138
+ Args:
139
+ package_name: The name of the package to get the version for.
140
+
141
+ Returns:
142
+ A Version instance representing the current version of the package.
143
+
144
+ Raises:
145
+ PackageNotFoundError: If the package is not found.
146
+ """
147
+ record = StringIO()
148
+ with redirect_stdout(record):
149
+ cli_get_version([package_name])
150
+ return record.getvalue().strip()
151
+
152
+
138
153
  def cli_get_version(args: list[str] | None = None) -> ExitCode:
139
154
  """Get the version of the current package.
140
155
 
@@ -159,24 +174,6 @@ def cli_get_version(args: list[str] | None = None) -> ExitCode:
159
174
  return ExitCode.SUCCESS
160
175
 
161
176
 
162
- def _get_version(package_name: str) -> str:
163
- """Get the version of the specified package.
164
-
165
- Args:
166
- package_name: The name of the package to get the version for.
167
-
168
- Returns:
169
- A Version instance representing the current version of the package.
170
-
171
- Raises:
172
- PackageNotFoundError: If the package is not found.
173
- """
174
- record = StringIO()
175
- with redirect_stdout(record):
176
- cli_get_version([package_name])
177
- return record.getvalue().strip()
178
-
179
-
180
177
  def cli_bump(args: list[str] | None = None) -> ExitCode:
181
178
  if args is None:
182
179
  args = sys.argv[1:]
@@ -204,3 +201,7 @@ def cli_bump(args: list[str] | None = None) -> ExitCode:
204
201
  except Exception as e:
205
202
  print(f"Unexpected error: {e}")
206
203
  return ExitCode.FAILURE
204
+
205
+
206
+ if __name__ == "__main__":
207
+ cli_bump(["patch", "bear-utils", "0.9.2-fart.build-alpha"])
@@ -1,4 +1,4 @@
1
- from bear_utils.constants._meta import RichIntEnum, IntValue as Value
1
+ from bear_utils.constants._meta import IntValue as Value, RichIntEnum
2
2
 
3
3
 
4
4
  class ExitCode(RichIntEnum):
@@ -1,6 +1,7 @@
1
+ from contextlib import suppress
1
2
  from dataclasses import dataclass
2
3
  from enum import IntEnum, StrEnum
3
- from typing import Any, Self, TextIO
4
+ from typing import Any, Self, TextIO, overload
4
5
 
5
6
 
6
7
  @dataclass(frozen=True)
@@ -9,6 +10,7 @@ class IntValue:
9
10
 
10
11
  value: int
11
12
  text: str
13
+ default: int = 0
12
14
 
13
15
 
14
16
  @dataclass(frozen=True)
@@ -17,23 +19,65 @@ class StrValue:
17
19
 
18
20
  value: str
19
21
  text: str
22
+ default: str = ""
20
23
 
21
24
 
22
25
  class RichStrEnum(StrEnum):
23
26
  """Base class for StrEnums with rich metadata."""
24
27
 
25
28
  text: str
29
+ default: str
26
30
 
27
31
  def __new__(cls, value: StrValue) -> Self:
28
- _value: str = value.value
29
32
  text: str = value.text
30
- obj: Self = str.__new__(cls, _value)
31
- obj._value_ = _value
33
+ obj: Self = str.__new__(cls, value.value)
34
+ obj._value_ = value.value
32
35
  obj.text = text
36
+ obj.default = value.default
33
37
  return obj
34
38
 
39
+ @classmethod
40
+ def keys(cls) -> list[str]:
41
+ """Return a list of all enum member names."""
42
+ return [item.name for item in cls]
43
+
44
+ @overload
45
+ @classmethod
46
+ def get(cls, value: str | Self, default: Self) -> Self: ...
47
+
48
+ @overload
49
+ @classmethod
50
+ def get(cls, value: str | Self, default: None = None) -> None: ...
51
+
52
+ @classmethod
53
+ def get(cls, value: str | Self, default: Self | None = None) -> Self | None:
54
+ """Try to get an enum member by its value or name."""
55
+ if isinstance(value, cls):
56
+ return value
57
+ with suppress(ValueError):
58
+ if isinstance(value, str):
59
+ return cls.from_text(value)
60
+ return default
61
+
62
+ @classmethod
63
+ def from_text(cls, text: str) -> Self:
64
+ """Convert a string text to its corresponding enum member."""
65
+ for item in cls:
66
+ if item.text == text:
67
+ return item
68
+ raise ValueError(f"Text {text} not found in {cls.__name__}")
69
+
70
+ @classmethod
71
+ def from_name(cls, name: str) -> Self:
72
+ """Convert a string name to its corresponding enum member."""
73
+ try:
74
+ return cls[name.upper()]
75
+ except KeyError as e:
76
+ raise ValueError(f"Name {name} not found in {cls.__name__}") from e
77
+
35
78
  def __str__(self) -> str:
36
- return f"{self.name} ({self.value}): {self.text}"
79
+ """Return a string representation of the enum."""
80
+ return self.value
37
81
 
38
82
  def str(self) -> str:
39
83
  """Return the string value of the enum."""
@@ -44,13 +88,14 @@ class RichIntEnum(IntEnum):
44
88
  """Base class for IntEnums with rich metadata."""
45
89
 
46
90
  text: str
91
+ default: int
47
92
 
48
93
  def __new__(cls, value: IntValue) -> Self:
49
- _value: int = value.value
50
94
  text: str = value.text
51
- obj: Self = int.__new__(cls, _value)
52
- obj._value_ = _value
95
+ obj: Self = int.__new__(cls, value.value)
96
+ obj._value_ = value.value
53
97
  obj.text = text
98
+ obj.default = value.default
54
99
  return obj
55
100
 
56
101
  def __int__(self) -> int:
@@ -58,18 +103,33 @@ class RichIntEnum(IntEnum):
58
103
  return self.value
59
104
 
60
105
  def __str__(self) -> str:
106
+ """Return a string representation of the enum."""
61
107
  return f"{self.name} ({self.value}): {self.text}"
62
108
 
63
109
  @classmethod
64
- def get(cls, value: Any) -> Self:
110
+ def keys(cls) -> list[str]:
111
+ """Return a list of all enum member names."""
112
+ return [item.name for item in cls]
113
+
114
+ @overload
115
+ @classmethod
116
+ def get(cls, value: str | int | Self, default: Self) -> Self: ...
117
+
118
+ @overload
119
+ @classmethod
120
+ def get(cls, value: str | int | Self, default: None = None) -> None: ...
121
+
122
+ @classmethod
123
+ def get(cls, value: str | int | Self | Any, default: Self | None = None) -> Self | None:
65
124
  """Try to get an enum member by its value, name, or text."""
66
125
  if isinstance(value, cls):
67
126
  return value
68
- if isinstance(value, int):
69
- return cls.from_int(value)
70
- if isinstance(value, str):
71
- return cls.from_name(value)
72
- raise ValueError(f"Cannot convert {value} to {cls.__name__}")
127
+ with suppress(ValueError):
128
+ if isinstance(value, int):
129
+ return cls.from_int(value)
130
+ if isinstance(value, str):
131
+ return cls.from_name(value)
132
+ return default
73
133
 
74
134
  @classmethod
75
135
  def from_name(cls, name: str) -> Self:
@@ -81,6 +141,7 @@ class RichIntEnum(IntEnum):
81
141
 
82
142
  @classmethod
83
143
  def from_int(cls, code: int) -> Self:
144
+ """Convert an integer to its corresponding enum member."""
84
145
  for item in cls:
85
146
  if item.value == code:
86
147
  return item
@@ -96,11 +157,11 @@ class RichIntEnum(IntEnum):
96
157
 
97
158
 
98
159
  class MockTextIO(TextIO):
99
- """A mock TextIO class that does nothing."""
160
+ """A mock TextIO class that captures written output for testing purposes."""
100
161
 
101
162
  def __init__(self) -> None:
102
163
  """Initialize the mock TextIO."""
103
- self._buffer = []
164
+ self._buffer: list[str] = []
104
165
 
105
166
  def write(self, _s: str, *_) -> None: # type: ignore[override]
106
167
  """Mock write method that appends to the buffer."""
@@ -130,6 +191,7 @@ class NullFile(TextIO):
130
191
  """Flush the null file (no operation)."""
131
192
 
132
193
  def __enter__(self) -> Self:
194
+ """Enter context manager and return self."""
133
195
  return self
134
196
 
135
197
  def __exit__(self, *_: object) -> None:
@@ -3,6 +3,7 @@
3
3
  from singleton_base import SingletonBase
4
4
 
5
5
  from ._tools import ClipboardManager, ascii_header, clear_clipboard, copy_to_clipboard, paste_from_clipboard
6
+ from ._zapper import zap, zap_as, zap_as_multi, zap_get, zap_multi
6
7
  from .platform_utils import OS, get_platform, is_linux, is_macos, is_windows
7
8
  from .wrappers.add_methods import add_comparison_methods
8
9
 
@@ -19,4 +20,9 @@ __all__ = [
19
20
  "is_macos",
20
21
  "is_windows",
21
22
  "paste_from_clipboard",
23
+ "zap",
24
+ "zap_as",
25
+ "zap_as_multi",
26
+ "zap_get",
27
+ "zap_multi",
22
28
  ]
@@ -1,15 +1,13 @@
1
1
  import asyncio
2
2
  from asyncio.subprocess import PIPE
3
3
  from collections import deque
4
- from functools import cached_property
5
4
  import shutil
6
5
  from typing import TYPE_CHECKING
7
6
 
8
- from rich.console import Console
9
-
10
7
  from bear_utils.cli.shell._base_command import BaseShellCommand as ShellCommand
11
8
  from bear_utils.cli.shell._base_shell import AsyncShellSession
12
9
  from bear_utils.extras.platform_utils import OS, get_platform
10
+ from bear_utils.graphics.font._utils import ascii_header
13
11
 
14
12
  if TYPE_CHECKING:
15
13
  from subprocess import CompletedProcess
@@ -175,119 +173,13 @@ async def clear_clipboard_async() -> int:
175
173
  return await clipboard_manager.clear()
176
174
 
177
175
 
178
- class TextHelper:
179
- @cached_property
180
- def local_console(self) -> Console:
181
- return Console()
182
-
183
- def print_header(
184
- self,
185
- title: str,
186
- top_sep: str = "#",
187
- left_sep: str = ">",
188
- right_sep: str = "<",
189
- bottom_sep: str = "#",
190
- length: int = 60,
191
- s1: str = "bold red",
192
- s2: str = "bold blue",
193
- return_txt: bool = False,
194
- ) -> str:
195
- """Generate a header string with customizable separators for each line.
196
-
197
- Args:
198
- title: The title text to display
199
- top_sep: Character(s) for the top separator line
200
- left_sep: Character(s) for the left side of title line
201
- right_sep: Character(s) for the right side of title line
202
- bottom_sep: Character(s) for the bottom separator line
203
- length: Total width of each line
204
- s1: Style for the title text
205
- s2: Style for the entire header block
206
- return_txt: If True, return the text instead of printing
207
- """
208
- # Top line: all top_sep characters
209
- top_line: str = top_sep * length
210
-
211
- # Bottom line: all bottom_sep characters
212
- bottom_line: str = bottom_sep * length
213
-
214
- # Title line: left_sep chars + title + right_sep chars
215
- title_with_spaces = f" {title} "
216
- styled_title = f"[{s1}]{title}[/{s1}]"
217
-
218
- # Calculate padding needed on each side
219
- title_length = len(title_with_spaces)
220
- remaining_space = length - title_length
221
- left_padding = remaining_space // 2
222
- right_padding = remaining_space - left_padding
223
-
224
- # Build the title line with different left and right separators
225
- title_line = (
226
- (left_sep * left_padding) + title_with_spaces.replace(title, styled_title) + (right_sep * right_padding)
227
- )
228
-
229
- # Assemble the complete header
230
- output_text: str = f"\n{top_line}\n{title_line}\n{bottom_line}\n"
231
-
232
- if not return_txt:
233
- self.local_console.print(output_text, style=s2)
234
- return output_text
235
-
236
-
237
- def ascii_header(
238
- title: str,
239
- top_sep: str = "#",
240
- left_sep: str = ">",
241
- right_sep: str = "<",
242
- bottom_sep: str = "#",
243
- length: int = 60,
244
- style1: str = "bold red",
245
- style2: str = "bold blue",
246
- print_out: bool = True,
247
- ) -> str:
248
- """Generate a header string for visual tests.
249
-
250
- Args:
251
- title (str): The title to display in the header.
252
- top_sep (str): The character to use for the top separator line. Defaults to '#'.
253
- left_sep (str): The character to use for the left side of title line. Defaults to '>'.
254
- right_sep (str): The character to use for the right side of title line. Defaults to '<'.
255
- bottom_sep (str): The character to use for the bottom separator line. Defaults to '#'.
256
- length (int): The total length of the header line. Defaults to 60.
257
- style1 (str): The style for the title text. Defaults to 'bold red'.
258
- style2 (str): The style for the separator text. Defaults to 'bold blue'.
259
- print_out (bool): Whether to print the header or just return it. Defaults to True.
260
- """
261
- text_helper = TextHelper()
262
- if print_out:
263
- text_helper.print_header(
264
- title=title,
265
- top_sep=top_sep,
266
- left_sep=left_sep,
267
- right_sep=right_sep,
268
- bottom_sep=bottom_sep,
269
- length=length,
270
- s1=style1,
271
- s2=style2,
272
- return_txt=False,
273
- )
274
- return ""
275
- return text_helper.print_header(
276
- title=title,
277
- top_sep=top_sep,
278
- left_sep=left_sep,
279
- right_sep=right_sep,
280
- bottom_sep=bottom_sep,
281
- length=length,
282
- s1=style1,
283
- s2=style2,
284
- return_txt=True,
285
- )
286
-
287
-
288
- if __name__ == "__main__":
289
- # Example usage of the TextHelper
290
- text_helper = TextHelper()
291
- text_helper.print_header("My Title", top_sep="#", bottom_sep="#")
292
- text_helper.print_header("My Title", top_sep="=", left_sep=">", right_sep="<", bottom_sep="=")
293
- text_helper.print_header("My Title", top_sep="-", left_sep="[", right_sep="]", bottom_sep="-")
176
+ __all__ = [
177
+ "ClipboardManager",
178
+ "ascii_header",
179
+ "clear_clipboard",
180
+ "clear_clipboard_async",
181
+ "copy_to_clipboard",
182
+ "copy_to_clipboard_async",
183
+ "paste_from_clipboard",
184
+ "paste_from_clipboard_async",
185
+ ]