sltcore 0.2.2__tar.gz → 0.2.4__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.
@@ -0,0 +1,43 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: write
8
+
9
+ jobs:
10
+ build-and-publish:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Extract version from pyproject.toml
17
+ id: version
18
+ run: |
19
+ VERSION=$(grep -m1 '^version = "' pyproject.toml | sed -E 's/^version = "([^"]+)"$/\1/')
20
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
21
+ echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v3
25
+
26
+ - name: Build distribution
27
+ run: uv build
28
+
29
+ - name: Publish to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
31
+ with:
32
+ password: ${{ secrets.PYPI_API_TOKEN }}
33
+ packages-dir: dist
34
+
35
+ - name: Create GitHub release and upload artifacts
36
+ uses: softprops/action-gh-release@v2
37
+ with:
38
+ tag_name: ${{ steps.version.outputs.tag }}
39
+ name: ${{ steps.version.outputs.tag }}
40
+ generate_release_notes: true
41
+ files: |
42
+ dist/*.tar.gz
43
+ dist/*.whl
@@ -2,8 +2,6 @@ name: Publish to TestPyPI
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
- release:
6
- types: [published]
7
5
 
8
6
  jobs:
9
7
  build-and-publish:
@@ -23,4 +21,4 @@ jobs:
23
21
  with:
24
22
  password: ${{ secrets.TEST_PYPI_API_TOKEN }}
25
23
  repository-url: https://test.pypi.org/legacy/
26
- packages_dir: dist
24
+ packages-dir: dist
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sltcore
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A toolkit for handling structured data layouts and bit-level operations.
5
5
  Project-URL: Homepage, https://github.com/fangface-hub/StructLayoutToolkitCore
6
6
  Project-URL: Documentation, https://readthedocs.org
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcore"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  license = "MIT"
5
5
  license-files = ["LICENSE"]
6
6
  description = "A toolkit for handling structured data layouts and bit-level operations."
@@ -1,8 +1,24 @@
1
1
  """Type definitions for structured data layouts and bit-level operations."""
2
2
  import math
3
+ import struct
3
4
  from dataclasses import dataclass, field
5
+ from functools import total_ordering
4
6
 
5
7
 
8
+ def _has_struct_half():
9
+ """Checks if the current Python environment supports
10
+ half-precision (16-bit) floats in the struct module."""
11
+ try:
12
+ struct.pack(">e", 1.0)
13
+ return True
14
+ except struct.error:
15
+ return False
16
+
17
+
18
+ HAS_STRUCT_HALF = _has_struct_half() # ← 一度だけ実行
19
+
20
+
21
+ @total_ordering
6
22
  @dataclass(frozen=True)
7
23
  class InfoSize:
8
24
  """Represents a size in bytes and bits."""
@@ -12,26 +28,43 @@ class InfoSize:
12
28
  def __post_init__(self):
13
29
  """Ensures that the bit count is normalized to be less than 8,
14
30
  and adjusts the byte count accordingly."""
15
- extra_bytes, new_bits = divmod(self.bit, 8)
31
+ extra_bytes = self.bit >> 3
32
+ new_bits = self.bit & 7
16
33
  object.__setattr__(self, "byte", self.byte + extra_bytes)
17
34
  object.__setattr__(self, "bit", new_bits)
18
35
 
36
+ def __str__(self) -> str:
37
+ """Returns a string representation of the size
38
+ in the format 'X bytes, Y bits'."""
39
+ return f"{self.byte} bytes, {self.bit} bits"
40
+
41
+ def __eq__(self, other: object) -> bool:
42
+ """Checks equality between two InfoSize instances
43
+ based on their total size in bits."""
44
+ if not isinstance(other, InfoSize):
45
+ return NotImplemented
46
+ return self.bits == other.bits
47
+
48
+ def __lt__(self, other: "InfoSize") -> bool:
49
+ """Compares two InfoSize instances based on their total size in bits."""
50
+ return self.bits < other.bits
51
+
19
52
  @property
20
53
  def bits(self) -> int:
21
54
  """Returns the total size in bits."""
22
- return self.byte * 8 + self.bit
55
+ return (self.byte << 3) | self.bit
23
56
 
24
57
  @property
25
58
  def bytes(self) -> int:
26
59
  """Returns the total size in bytes,
27
60
  rounding up if there are leftover bits."""
28
- return (self.bits + 7) // 8 # round up
61
+ return (self.bits + 7) >> 3
29
62
 
30
63
  @property
31
64
  def hex_digits(self) -> int:
32
65
  """Returns the number of hexadecimal digits
33
66
  needed to represent the size in bits."""
34
- return self.bits // 4
67
+ return self.bits >> 2
35
68
 
36
69
  @property
37
70
  def mask(self) -> int:
@@ -55,6 +88,7 @@ class InfoSize:
55
88
  return InfoSize(0, total_bits)
56
89
 
57
90
 
91
+ @total_ordering
58
92
  @dataclass(frozen=True)
59
93
  class Info:
60
94
  """Represents a slice of bits extracted from a bytearray,
@@ -63,6 +97,28 @@ class Info:
63
97
  info_size: InfoSize = field(default_factory=InfoSize,
64
98
  metadata={"desc": "size information"})
65
99
 
100
+ def __str__(self) -> str:
101
+ """Returns a string representation of the Info instance,
102
+ showing the raw value and its size."""
103
+ return f"Info(raw_value={self.raw_value}, info_size={self.info_size})"
104
+
105
+ def __eq__(self, other: object) -> bool:
106
+ """Checks equality between two Info instances based on their raw values
107
+ and size information."""
108
+ if not isinstance(other, Info):
109
+ return NotImplemented
110
+ return (self.raw_value == other.raw_value
111
+ and self.info_size == other.info_size)
112
+
113
+ def __lt__(self, other: "Info") -> bool:
114
+ """Compares two Info instances based on their raw values
115
+ and size information."""
116
+ if not isinstance(other, Info):
117
+ return NotImplemented
118
+ if self.info_size != other.info_size:
119
+ return self.info_size < other.info_size
120
+ return self.raw_value < other.raw_value
121
+
66
122
  @classmethod
67
123
  def from_int(cls, value: int, size: InfoSize):
68
124
  """Creates an Info instance from an integer value, ensuring that
@@ -136,65 +192,52 @@ def _float_from_bits(raw: int, bits: int) -> float:
136
192
  to a Python float, based on the specified bit size.
137
193
  Supports 16, 32, and 64 bits.
138
194
  """
139
- if bits == 16:
140
- # IEEE754 half-precision (16bit) → float32
141
- s = (raw >> 15) & 0x0001
142
- e = (raw >> 10) & 0x001F
143
- f = raw & 0x03FF
144
-
145
- if e == 0:
146
- if f == 0:
147
- return (-1)**s * 0.0
148
- return (-1)**s * (f / 2**10) * 2**(-14)
149
- elif e == 31:
150
- if f == 0:
151
- return float('inf') if s == 0 else float('-inf')
152
- return float('nan')
153
- else:
154
- return (-1)**s * (1 + f / 2**10) * 2**(e - 15)
155
- elif bits == 32:
156
- # IEEE754 single precision
157
- # High-speed version without using struct
158
- s = (raw >> 31) & 0x1
159
-
160
- e = (raw >> 23) & 0xFF
161
- f = raw & 0x7FFFFF
162
-
163
- if e == 0:
164
- return (-1)**s * (f / 2**23) * 2**(-126)
165
- elif e == 255:
166
- if f == 0:
167
- return float('-inf') if s else float('inf')
168
- return float('nan')
169
- else:
170
- return (-1)**s * (1 + f / 2**23) * 2**(e - 127)
171
-
172
- elif bits == 64:
173
- # IEEE754 double precision
174
- s = (raw >> 63) & 0x1
175
- e = (raw >> 52) & 0x7FF
176
- f = raw & 0xFFFFFFFFFFFFF
177
-
178
- if e == 0:
179
- return (-1)**s * (f / 2**52) * 2**(-1022)
180
- elif e == 2047:
181
- if f == 0:
182
- return float('-inf') if s else float('inf')
183
- return float('nan')
184
- else:
185
- return (-1)**s * (1 + f / 2**52) * 2**(e - 1023)
186
-
187
- else:
188
- raise ValueError(''.join([
189
- f"Unsupported bit size for float conversion: {bits}. ",
195
+ if bits == 64:
196
+ return struct.unpack('>d', struct.pack('>Q', raw))[0]
197
+ if bits == 32:
198
+ # Use struct to unpack the integer as a float
199
+ return struct.unpack('>f', struct.pack('>I', raw))[0]
200
+ if bits != 16:
201
+ raise ValueError("".join([
202
+ f"Unsupported float bit size: {bits}. ",
190
203
  "Only 16, 32, and 64 are supported."
191
204
  ]))
205
+ if HAS_STRUCT_HALF:
206
+ # Use struct to unpack the integer as a half-precision float
207
+ return struct.unpack('>e', struct.pack('>H', raw))[0]
208
+ # IEEE754 half-precision (16bit) → float32
209
+ s = (raw >> 15) & 0x0001
210
+ e = (raw >> 10) & 0x001F
211
+ f = raw & 0x03FF
212
+
213
+ if e == 0:
214
+ if f == 0:
215
+ return (-1)**s * 0.0
216
+ return (-1)**s * (f / 2**10) * 2**(-14)
217
+ if e == 31:
218
+ if f == 0:
219
+ return float('inf') if s == 0 else float('-inf')
220
+ return float('nan')
221
+ return (-1)**s * (1 + f / 2**10) * 2**(e - 15)
192
222
 
193
223
 
194
224
  def _float_to_bits(value: float, bits: int) -> int:
195
225
  """Converts a Python float to its raw integer representation
196
226
  based on the specified bit size. Supports 16, 32, and 64 bits.
197
227
  """
228
+ if bits == 64:
229
+ return struct.unpack('>Q', struct.pack('>d', value))[0]
230
+ if bits == 32:
231
+ # Use struct to pack the float into bytes and then unpack as an integer
232
+ return struct.unpack('>I', struct.pack('>f', value))[0]
233
+ if bits != 16:
234
+ raise ValueError("".join([
235
+ f"Unsupported float bit size: {bits}. ",
236
+ "Only 16, 32, and 64 are supported."
237
+ ]))
238
+ if HAS_STRUCT_HALF:
239
+ # Use struct to pack the float into bytes and then unpack as an integer
240
+ return struct.unpack('>H', struct.pack('>e', value))[0]
198
241
  # Determine the sign and absolute value
199
242
  # Use math.copysign to correctly detect negative zero
200
243
  sign = 1 if math.copysign(1.0, value) < 0 else 0
@@ -206,40 +249,16 @@ def _float_to_bits(value: float, bits: int) -> int:
206
249
 
207
250
  # NaN / Inf
208
251
  if math.isnan(value):
209
- if bits == 16:
210
- return 0x7E00
211
- if bits == 32:
212
- return 0x7FC00000
213
- if bits == 64:
214
- return 0x7FF8000000000000
252
+ return 0x7E00
215
253
 
216
254
  # Handle infinity
217
255
  if math.isinf(value):
218
- if bits == 16:
219
- return (sign << 15) | 0x7C00
220
- if bits == 32:
221
- return (sign << 31) | 0x7F800000
222
- if bits == 64:
223
- return (sign << 63) | 0x7FF0000000000000
256
+ return (sign << 15) | 0x7C00
224
257
 
225
258
  # Compute exponent and mantissa
226
259
  e = int(math.floor(math.log(v, 2)))
227
260
  mant = v / (2**e) - 1.0
228
261
 
229
- if bits == 16:
230
- exp = e + 15
231
- frac = int(mant * (2**10))
232
- return (sign << 15) | (exp << 10) | frac
233
-
234
- elif bits == 32:
235
- exp = e + 127
236
- frac = int(mant * (2**23))
237
- return (sign << 31) | (exp << 23) | frac
238
-
239
- elif bits == 64:
240
- exp = e + 1023
241
- frac = int(mant * (2**52))
242
- return (sign << 63) | (exp << 52) | frac
243
-
244
- else:
245
- raise ValueError(f"Unsupported float bit size: {bits}")
262
+ exp = e + 15
263
+ frac = int(mant * (2**10))
264
+ return (sign << 15) | (exp << 10) | frac
@@ -65,7 +65,7 @@ wheels = [
65
65
 
66
66
  [[package]]
67
67
  name = "sltcore"
68
- version = "0.1.0"
68
+ version = "0.2.3"
69
69
  source = { editable = "." }
70
70
 
71
71
  [package.dev-dependencies]
@@ -1,25 +0,0 @@
1
- name: Publish to PyPI
2
-
3
- on:
4
- workflow_dispatch:
5
- release:
6
- types: [published]
7
-
8
- jobs:
9
- build-and-publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - uses: actions/checkout@v4
14
-
15
- - name: Install uv
16
- uses: astral-sh/setup-uv@v3
17
-
18
- - name: Build wheel
19
- run: uv build
20
-
21
- - name: Publish to PyPI
22
- uses: pypa/gh-action-pypi-publish@release/v1
23
- with:
24
- password: ${{ secrets.PYPI_API_TOKEN }}
25
- packages_dir: dist
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes