serif 0.0.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.
- jib/__init__.py +39 -0
- jib/alias_tracker.py +103 -0
- jib/csv.py +115 -0
- jib/display.py +377 -0
- jib/errors.py +23 -0
- jib/naming.py +83 -0
- jib/table.py +1821 -0
- jib/typeutils.py +18 -0
- jib/typing.py +301 -0
- jib/vector.py +1726 -0
- serif-0.0.1.dist-info/METADATA +374 -0
- serif-0.0.1.dist-info/RECORD +15 -0
- serif-0.0.1.dist-info/WHEEL +5 -0
- serif-0.0.1.dist-info/licenses/LICENSE +21 -0
- serif-0.0.1.dist-info/top_level.txt +1 -0
jib/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jib: A Pythonic, zero-dependency vector and table library
|
|
3
|
+
|
|
4
|
+
Designed for Python users who need to work with datasets beyond Excel's limits
|
|
5
|
+
(>1000 rows) but want the ease-of-use and intuitive feel of Excel or SQL.
|
|
6
|
+
|
|
7
|
+
Main classes:
|
|
8
|
+
- Vector: 1D vector with optional type safety
|
|
9
|
+
- Table: 2D table (multiple columns of equal length)
|
|
10
|
+
|
|
11
|
+
Type-specific subclasses (auto-created):
|
|
12
|
+
- _Float: Vector of floats with float method proxying
|
|
13
|
+
- _Int: Vector of integers with int method proxying
|
|
14
|
+
- _String: Vector of strings with string method proxying
|
|
15
|
+
- _Date: Vector of dates with date method proxying
|
|
16
|
+
|
|
17
|
+
Zero external dependencies - pure Python stdlib only.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .alias_tracker import _ALIAS_TRACKER, AliasError
|
|
21
|
+
from .vector import Vector, _Float, _Int, _String, _Date
|
|
22
|
+
from .table import Table
|
|
23
|
+
from .errors import JibError, JibKeyError, JibValueError, JibTypeError, JibIndexError
|
|
24
|
+
from .csv import read_csv
|
|
25
|
+
from .typing import DataType
|
|
26
|
+
|
|
27
|
+
__version__ = "0.0.1"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Vector",
|
|
30
|
+
"Table",
|
|
31
|
+
"read_csv",
|
|
32
|
+
"DataType",
|
|
33
|
+
"AliasError",
|
|
34
|
+
"JibError",
|
|
35
|
+
"JibKeyError",
|
|
36
|
+
"JibValueError",
|
|
37
|
+
"JibTypeError",
|
|
38
|
+
"JibIndexError"
|
|
39
|
+
]
|
jib/alias_tracker.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# py_vector/_alias_tracker.py
|
|
2
|
+
|
|
3
|
+
import weakref
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AliasError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _AliasTracker:
|
|
11
|
+
"""
|
|
12
|
+
Tracks which Vector instances reference the same underlying tuple.
|
|
13
|
+
|
|
14
|
+
Key design points:
|
|
15
|
+
- Uses id(tuple) → list[weakref.ref(Vector)]
|
|
16
|
+
- No hashing of Vector objects (they remain unhashable)
|
|
17
|
+
- No use of WeakSet (because that requires hashing)
|
|
18
|
+
- Pure identity tracking
|
|
19
|
+
- Auto-prunes dead references
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
# tuple_id -> list of weakrefs to Vectors using that tuple
|
|
24
|
+
self._registry = {}
|
|
25
|
+
|
|
26
|
+
def _cleanup_dead_refs(self, refs):
|
|
27
|
+
"""Remove any weakrefs that no longer point to a live object."""
|
|
28
|
+
return [r for r in refs if r() is not None]
|
|
29
|
+
|
|
30
|
+
def register(self, vec, tuple_id):
|
|
31
|
+
"""
|
|
32
|
+
Register a Vector as sharing the tuple with key tuple_id.
|
|
33
|
+
"""
|
|
34
|
+
refs = self._registry.setdefault(tuple_id, [])
|
|
35
|
+
|
|
36
|
+
# clean dead refs before adding
|
|
37
|
+
refs = self._cleanup_dead_refs(refs)
|
|
38
|
+
|
|
39
|
+
# Check if vec is already registered for this tuple_id
|
|
40
|
+
for r in refs:
|
|
41
|
+
if r() is vec:
|
|
42
|
+
# Already registered, don't add again
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# store the cleaned list back
|
|
46
|
+
self._registry[tuple_id] = refs
|
|
47
|
+
|
|
48
|
+
# we always register by weakref, no hashing required
|
|
49
|
+
refs.append(weakref.ref(vec))
|
|
50
|
+
|
|
51
|
+
def unregister(self, vec, tuple_id):
|
|
52
|
+
"""
|
|
53
|
+
Remove a Vector from the registry for a given tuple_id.
|
|
54
|
+
If that tuple_id has no remaining owners, delete the entry.
|
|
55
|
+
"""
|
|
56
|
+
refs = self._registry.get(tuple_id)
|
|
57
|
+
if not refs:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Remove matching weakref
|
|
61
|
+
alive = []
|
|
62
|
+
for r in refs:
|
|
63
|
+
obj = r()
|
|
64
|
+
if obj is None:
|
|
65
|
+
continue
|
|
66
|
+
if obj is vec:
|
|
67
|
+
continue
|
|
68
|
+
alive.append(r)
|
|
69
|
+
|
|
70
|
+
if alive:
|
|
71
|
+
self._registry[tuple_id] = alive
|
|
72
|
+
else:
|
|
73
|
+
del self._registry[tuple_id]
|
|
74
|
+
|
|
75
|
+
def check_writable(self, vec, tuple_id):
|
|
76
|
+
"""
|
|
77
|
+
Returns True if vec is the *only* owner of tuple_id.
|
|
78
|
+
Otherwise raises AliasError.
|
|
79
|
+
"""
|
|
80
|
+
refs = self._registry.get(tuple_id)
|
|
81
|
+
if not refs:
|
|
82
|
+
return True # nothing registered → writable
|
|
83
|
+
|
|
84
|
+
# drop dead weakrefs
|
|
85
|
+
alive = self._cleanup_dead_refs(refs)
|
|
86
|
+
self._registry[tuple_id] = alive
|
|
87
|
+
|
|
88
|
+
# Count how many Vectors still alive share this tuple
|
|
89
|
+
owners = [r() for r in alive if r() is not None]
|
|
90
|
+
|
|
91
|
+
if len(owners) <= 1:
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
# >1 owner → alias detected
|
|
95
|
+
raise AliasError(
|
|
96
|
+
"This Vector shares underlying storage with another.\n"
|
|
97
|
+
"Use .copy() to create an independent, writable vector."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# THE SINGLETON (imported by Vector)
|
|
102
|
+
_ALIAS_TRACKER = _AliasTracker()
|
|
103
|
+
|
jib/csv.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""CSV reading utilities for Vector/Table."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_csv(file, *, delimiter=',', has_header=True, encoding='utf-8'):
|
|
8
|
+
"""
|
|
9
|
+
Read a CSV file and return a Table.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
file : str or file-like
|
|
14
|
+
Path to CSV file or file-like object
|
|
15
|
+
delimiter : str, default ','
|
|
16
|
+
Field delimiter
|
|
17
|
+
has_header : bool, default True
|
|
18
|
+
Whether first row contains column names
|
|
19
|
+
encoding : str, default 'utf-8'
|
|
20
|
+
File encoding (used only if file is a path)
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
Table
|
|
25
|
+
Table with columns from the CSV file
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> t = read_csv("data.csv")
|
|
30
|
+
>>> t = read_csv("data.tsv", delimiter='\\t')
|
|
31
|
+
>>> with open("data.csv") as f:
|
|
32
|
+
... t = read_csv(f)
|
|
33
|
+
"""
|
|
34
|
+
from .table import Table
|
|
35
|
+
from .vector import Vector
|
|
36
|
+
|
|
37
|
+
# Handle file path vs file object
|
|
38
|
+
if isinstance(file, str):
|
|
39
|
+
with open(file, 'r', encoding=encoding, newline='') as f:
|
|
40
|
+
return _read_csv_from_file(f, delimiter=delimiter, has_header=has_header)
|
|
41
|
+
else:
|
|
42
|
+
return _read_csv_from_file(file, delimiter=delimiter, has_header=has_header)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_csv_from_file(file_obj: TextIO, *, delimiter: str, has_header: bool):
|
|
46
|
+
"""Read CSV data from an open file object."""
|
|
47
|
+
from .table import Table
|
|
48
|
+
from .vector import Vector
|
|
49
|
+
|
|
50
|
+
reader = csv.reader(file_obj, delimiter=delimiter)
|
|
51
|
+
|
|
52
|
+
# Read all rows first
|
|
53
|
+
all_rows = list(reader)
|
|
54
|
+
|
|
55
|
+
if not all_rows:
|
|
56
|
+
return Table()
|
|
57
|
+
|
|
58
|
+
# Determine header and data rows
|
|
59
|
+
if has_header:
|
|
60
|
+
header = all_rows[0]
|
|
61
|
+
rows = all_rows[1:]
|
|
62
|
+
else:
|
|
63
|
+
# Generate default column names: col_0, col_1, etc.
|
|
64
|
+
header = [f"col_{i}" for i in range(len(all_rows[0]))]
|
|
65
|
+
rows = all_rows
|
|
66
|
+
|
|
67
|
+
if not rows:
|
|
68
|
+
# Header only, no data
|
|
69
|
+
return Table({col: Vector() for col in header})
|
|
70
|
+
|
|
71
|
+
# Transpose rows into columns
|
|
72
|
+
num_cols = len(header)
|
|
73
|
+
columns = []
|
|
74
|
+
|
|
75
|
+
for col_idx in range(num_cols):
|
|
76
|
+
column_data = []
|
|
77
|
+
for row in rows:
|
|
78
|
+
# Handle jagged rows
|
|
79
|
+
if col_idx < len(row):
|
|
80
|
+
value = row[col_idx]
|
|
81
|
+
# Try to infer type
|
|
82
|
+
column_data.append(_infer_type(value))
|
|
83
|
+
else:
|
|
84
|
+
column_data.append(None)
|
|
85
|
+
columns.append(Vector(column_data, name=header[col_idx]))
|
|
86
|
+
|
|
87
|
+
return Table(columns)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _infer_type(value: str):
|
|
91
|
+
"""
|
|
92
|
+
Attempt to convert string value to int, float, or leave as string.
|
|
93
|
+
|
|
94
|
+
Returns None for empty strings.
|
|
95
|
+
"""
|
|
96
|
+
if not value or value.strip() == '':
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
value = value.strip()
|
|
100
|
+
|
|
101
|
+
# Try int
|
|
102
|
+
try:
|
|
103
|
+
return int(value)
|
|
104
|
+
except ValueError:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# Try float
|
|
108
|
+
try:
|
|
109
|
+
return float(value)
|
|
110
|
+
except ValueError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Keep as string
|
|
114
|
+
return value
|
|
115
|
+
|
jib/display.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Display and repr logic for Vector and Table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from datetime import date
|
|
5
|
+
from typing import List
|
|
6
|
+
from .naming import _get_reserved_names
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# How many rows/columns to show before inserting "..."
|
|
10
|
+
MAX_HEAD_ROWS = 5
|
|
11
|
+
MAX_HEAD_COLS = 5
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _needs_quote(name: str) -> bool:
|
|
15
|
+
"""Determine if a column name needs quoting in repr output.
|
|
16
|
+
|
|
17
|
+
A name needs quoting if:
|
|
18
|
+
- It's empty
|
|
19
|
+
- It's not a valid Python identifier
|
|
20
|
+
- It starts with a digit
|
|
21
|
+
- It parses as a number
|
|
22
|
+
- It collides with Vector/Table reserved method names
|
|
23
|
+
"""
|
|
24
|
+
# Always quote empty names
|
|
25
|
+
if not name:
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
# If it's not a valid identifier (spaces, punctuation, etc.)
|
|
29
|
+
if not name.isidentifier():
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
# If it starts with a digit, even if isidentifier() somehow allowed it
|
|
33
|
+
if name[0].isdigit():
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
# If it parses as a number (int, float, maybe with sign), quote it
|
|
37
|
+
try:
|
|
38
|
+
float(name)
|
|
39
|
+
return True
|
|
40
|
+
except ValueError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# If the name collides with a method/attribute name, quote it
|
|
44
|
+
if name.lower() in _get_reserved_names():
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _format_column(col, max_preview: int = MAX_HEAD_ROWS) -> List[str]:
|
|
51
|
+
"""Returns a list of strings representing that column, truncated for display."""
|
|
52
|
+
# Truncate with symmetric preview
|
|
53
|
+
vals = col._underlying
|
|
54
|
+
if len(vals) > max_preview * 2:
|
|
55
|
+
preview = list(vals[:max_preview]) + ['...'] + list(vals[-max_preview:])
|
|
56
|
+
else:
|
|
57
|
+
preview = list(vals)
|
|
58
|
+
|
|
59
|
+
# Type-sensitive formatting
|
|
60
|
+
out = []
|
|
61
|
+
for v in preview:
|
|
62
|
+
if v == '...':
|
|
63
|
+
out.append('...')
|
|
64
|
+
elif col._dtype and col._dtype.kind is float:
|
|
65
|
+
out.append(f"{v:.1f}" if v == int(v) else f"{v:g}")
|
|
66
|
+
elif col._dtype and col._dtype.kind is int:
|
|
67
|
+
out.append(str(v))
|
|
68
|
+
elif col._dtype and col._dtype.kind is date:
|
|
69
|
+
out.append(v.isoformat())
|
|
70
|
+
elif col._dtype and col._dtype.kind is str:
|
|
71
|
+
# Pure str columns: no quotes (type already known from footer)
|
|
72
|
+
out.append(str(v) if v is not None else 'None')
|
|
73
|
+
else:
|
|
74
|
+
# Object type - quote strings to distinguish from other types
|
|
75
|
+
if isinstance(v, str):
|
|
76
|
+
out.append(repr(v))
|
|
77
|
+
else:
|
|
78
|
+
out.append(str(v))
|
|
79
|
+
|
|
80
|
+
# Align: numeric right, others left
|
|
81
|
+
max_len = max(len(s) for s in out) if out else 0
|
|
82
|
+
if col._dtype and col._dtype.kind in (int, float):
|
|
83
|
+
return [s.rjust(max_len) for s in out]
|
|
84
|
+
return [s.ljust(max_len) for s in out]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _compute_headers(cols, col_indices, sanitize_func, uniquify_func):
|
|
88
|
+
"""Given Table columns and indices, returns display_names, sanitized_names, dtypes."""
|
|
89
|
+
display_names = []
|
|
90
|
+
sanitized_names = []
|
|
91
|
+
dtypes = []
|
|
92
|
+
seen = set()
|
|
93
|
+
|
|
94
|
+
for idx in col_indices:
|
|
95
|
+
col = cols[idx]
|
|
96
|
+
|
|
97
|
+
# Display name
|
|
98
|
+
disp = col._name or ""
|
|
99
|
+
display_names.append(disp)
|
|
100
|
+
|
|
101
|
+
# Sanitized dot name
|
|
102
|
+
if col._name:
|
|
103
|
+
san = sanitize_func(col._name)
|
|
104
|
+
if san is None:
|
|
105
|
+
san = f"col{idx}_"
|
|
106
|
+
else:
|
|
107
|
+
san = uniquify_func(san, seen)
|
|
108
|
+
seen.add(san)
|
|
109
|
+
else:
|
|
110
|
+
san = f"col{idx}_"
|
|
111
|
+
sanitized_names.append(san)
|
|
112
|
+
|
|
113
|
+
# Dtype (with nullable indicator)
|
|
114
|
+
if col._dtype:
|
|
115
|
+
dtype_str = col._dtype.kind.__name__
|
|
116
|
+
if col._dtype.nullable:
|
|
117
|
+
dtype_str += "?"
|
|
118
|
+
dtypes.append(dtype_str)
|
|
119
|
+
else:
|
|
120
|
+
dtypes.append("object")
|
|
121
|
+
|
|
122
|
+
return display_names, sanitized_names, dtypes
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_structural_change(display_name: str, sanitized_name: str) -> bool:
|
|
126
|
+
"""Check if sanitization involved structural changes beyond just case normalization.
|
|
127
|
+
|
|
128
|
+
Returns True if there were meaningful transformations like:
|
|
129
|
+
- Character removal/substitution (spaces, punctuation)
|
|
130
|
+
- Prefix addition (digit handling)
|
|
131
|
+
- Suffix addition (reserved name collision)
|
|
132
|
+
|
|
133
|
+
Returns False if only case changed.
|
|
134
|
+
"""
|
|
135
|
+
if not display_name or not sanitized_name:
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
# If lowercasing the display name equals sanitized, it's just case change
|
|
139
|
+
if display_name.lower() == sanitized_name:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
# Otherwise there was a structural transformation
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _header_rows(display_names, sanitized_names, dtypes):
|
|
147
|
+
"""Decide which header rows to show based on display vs sanitized names.
|
|
148
|
+
|
|
149
|
+
Returns (header_rows, show_types_in_header) where show_types_in_header indicates
|
|
150
|
+
whether types are heterogeneous and should be shown in header instead of footer.
|
|
151
|
+
"""
|
|
152
|
+
any_display = any(n for n in display_names if n != "...")
|
|
153
|
+
|
|
154
|
+
# Only show dot-access row if there's a structural change, not just case
|
|
155
|
+
any_structural_change = any(
|
|
156
|
+
_is_structural_change(disp, san)
|
|
157
|
+
for disp, san in zip(display_names, sanitized_names)
|
|
158
|
+
if disp != "..." and san != "..."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Check if types are homogeneous (excluding "...")
|
|
162
|
+
unique_types = set(dt for dt in dtypes if dt != "...")
|
|
163
|
+
show_types_in_header = len(unique_types) > 1
|
|
164
|
+
|
|
165
|
+
rows = []
|
|
166
|
+
|
|
167
|
+
# Row 1: display names (quoted if needed)
|
|
168
|
+
if any_display:
|
|
169
|
+
row = []
|
|
170
|
+
for name in display_names:
|
|
171
|
+
if name == "...":
|
|
172
|
+
row.append("...")
|
|
173
|
+
elif _needs_quote(name):
|
|
174
|
+
row.append(repr(name))
|
|
175
|
+
else:
|
|
176
|
+
row.append(name if name else "")
|
|
177
|
+
rows.append(row)
|
|
178
|
+
|
|
179
|
+
# Row 2: sanitized names (only if structural change or no display names)
|
|
180
|
+
if any_structural_change or not any_display:
|
|
181
|
+
rows.append([("." + san) if san and san != "..." else san for san in sanitized_names])
|
|
182
|
+
|
|
183
|
+
# Row 3 (or 2): type annotations (only if heterogeneous)
|
|
184
|
+
if show_types_in_header:
|
|
185
|
+
rows.append([f"[{dt}]" if dt != "..." else "..." for dt in dtypes])
|
|
186
|
+
|
|
187
|
+
return rows, show_types_in_header
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _align_columns(formatted_cols, header_rows, col_dtypes):
|
|
191
|
+
"""Pad columns and headers to consistent widths."""
|
|
192
|
+
num_cols = len(formatted_cols)
|
|
193
|
+
col_widths = []
|
|
194
|
+
|
|
195
|
+
# Compute desired width per column
|
|
196
|
+
for c in range(num_cols):
|
|
197
|
+
body_width = max(len(s) for s in formatted_cols[c]) if formatted_cols[c] else 0
|
|
198
|
+
header_width = max(
|
|
199
|
+
len(header_rows[r][c]) for r in range(len(header_rows))
|
|
200
|
+
) if header_rows else 0
|
|
201
|
+
col_widths.append(max(body_width, header_width))
|
|
202
|
+
|
|
203
|
+
# Re-pad columns based on dtype
|
|
204
|
+
aligned_cols = []
|
|
205
|
+
for c in range(num_cols):
|
|
206
|
+
col = formatted_cols[c]
|
|
207
|
+
w = col_widths[c]
|
|
208
|
+
dtype = col_dtypes[c]
|
|
209
|
+
if dtype in ('int', 'float'):
|
|
210
|
+
aligned_cols.append([s.rjust(w) for s in col])
|
|
211
|
+
else:
|
|
212
|
+
aligned_cols.append([s.ljust(w) for s in col])
|
|
213
|
+
|
|
214
|
+
# Re-pad headers
|
|
215
|
+
aligned_headers = []
|
|
216
|
+
for row in header_rows:
|
|
217
|
+
aligned_headers.append([h.rjust(col_widths[c]) if col_dtypes[c] in ('int', 'float') else h.ljust(col_widths[c])
|
|
218
|
+
for c, h in enumerate(row)])
|
|
219
|
+
|
|
220
|
+
return aligned_cols, aligned_headers
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _footer(pv, dtype_list=None, truncated=False, shown=MAX_HEAD_COLS) -> str:
|
|
224
|
+
"""Generate footer line based on shape and dtypes."""
|
|
225
|
+
shape = pv.shape
|
|
226
|
+
if not shape:
|
|
227
|
+
return "# empty"
|
|
228
|
+
|
|
229
|
+
if len(shape) == 1:
|
|
230
|
+
if pv._dtype:
|
|
231
|
+
dt = pv._dtype.kind.__name__
|
|
232
|
+
if pv._dtype.nullable:
|
|
233
|
+
dt += "?"
|
|
234
|
+
else:
|
|
235
|
+
dt = "object"
|
|
236
|
+
return f"# {len(pv)} element vector <{dt}>"
|
|
237
|
+
|
|
238
|
+
if len(shape) == 2:
|
|
239
|
+
if dtype_list:
|
|
240
|
+
if truncated:
|
|
241
|
+
d = ", ".join(dtype_list[:shown]) + ", ..., " + ", ".join(dtype_list[-shown:])
|
|
242
|
+
else:
|
|
243
|
+
d = ", ".join(dtype_list)
|
|
244
|
+
else:
|
|
245
|
+
d = pv._dtype.kind.__name__ if pv._dtype else "object"
|
|
246
|
+
rows, cols = shape
|
|
247
|
+
return f"# {rows}×{cols} table <{d}>"
|
|
248
|
+
|
|
249
|
+
shape_str = "×".join(str(s) for s in shape)
|
|
250
|
+
if pv._dtype:
|
|
251
|
+
dt = pv._dtype.kind.__name__
|
|
252
|
+
if pv._dtype.nullable:
|
|
253
|
+
dt += "?"
|
|
254
|
+
else:
|
|
255
|
+
dt = "object"
|
|
256
|
+
return f"# {shape_str} tensor <{dt}>"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _repr_vector(v) -> str:
|
|
260
|
+
"""Pretty repr for a 1D Vector."""
|
|
261
|
+
formatted = _format_column(v)
|
|
262
|
+
|
|
263
|
+
# Compute width: max of data and header (if present)
|
|
264
|
+
data_width = max(len(s) for s in formatted) if formatted else 0
|
|
265
|
+
header_width = 0
|
|
266
|
+
if v._name:
|
|
267
|
+
header_text = repr(v._name) if _needs_quote(v._name) else v._name
|
|
268
|
+
header_width = len(header_text)
|
|
269
|
+
|
|
270
|
+
width = max(data_width, header_width)
|
|
271
|
+
|
|
272
|
+
# Re-align data to match combined width
|
|
273
|
+
if v._dtype and v._dtype.kind in (int, float):
|
|
274
|
+
formatted = [s.rjust(width) for s in formatted]
|
|
275
|
+
else:
|
|
276
|
+
formatted = [s.ljust(width) for s in formatted]
|
|
277
|
+
|
|
278
|
+
lines = []
|
|
279
|
+
|
|
280
|
+
# Optional vector name
|
|
281
|
+
if v._name:
|
|
282
|
+
lines.append(header_text.ljust(width) if not v._dtype or v._dtype.kind not in (int, float) else header_text.rjust(width))
|
|
283
|
+
|
|
284
|
+
lines.extend(formatted)
|
|
285
|
+
lines.append("")
|
|
286
|
+
lines.append(_footer(v))
|
|
287
|
+
return "\n".join(lines)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _repr_table(tbl) -> str:
|
|
291
|
+
"""Pretty repr for a 2D Table."""
|
|
292
|
+
from .naming import _sanitize_user_name, _uniquify
|
|
293
|
+
|
|
294
|
+
cols = tbl.cols()
|
|
295
|
+
num_cols = len(cols)
|
|
296
|
+
|
|
297
|
+
if num_cols == 0:
|
|
298
|
+
return "# 0×0 table"
|
|
299
|
+
|
|
300
|
+
truncated = num_cols > MAX_HEAD_COLS * 2
|
|
301
|
+
|
|
302
|
+
if truncated:
|
|
303
|
+
col_indices = list(range(MAX_HEAD_COLS)) + list(range(num_cols - MAX_HEAD_COLS, num_cols))
|
|
304
|
+
else:
|
|
305
|
+
col_indices = list(range(num_cols))
|
|
306
|
+
|
|
307
|
+
# Headers + dtypes
|
|
308
|
+
disp, san, dtypes_displayed = _compute_headers(
|
|
309
|
+
cols, col_indices, _sanitize_user_name, _uniquify
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Get all dtypes for footer
|
|
313
|
+
dtypes_all = []
|
|
314
|
+
for col in cols:
|
|
315
|
+
if col._dtype:
|
|
316
|
+
dtype_str = col._dtype.kind.__name__
|
|
317
|
+
if col._dtype.nullable:
|
|
318
|
+
dtype_str += "?"
|
|
319
|
+
dtypes_all.append(dtype_str)
|
|
320
|
+
else:
|
|
321
|
+
dtypes_all.append("object")
|
|
322
|
+
|
|
323
|
+
# Format columns
|
|
324
|
+
formatted_cols = [_format_column(cols[i]) for i in col_indices]
|
|
325
|
+
|
|
326
|
+
# Insert "..." column if truncated
|
|
327
|
+
if truncated:
|
|
328
|
+
ellipsis_col = ["..." for _ in range(len(formatted_cols[0]))]
|
|
329
|
+
formatted_cols.insert(MAX_HEAD_COLS, ellipsis_col)
|
|
330
|
+
disp.insert(MAX_HEAD_COLS, "...")
|
|
331
|
+
san.insert(MAX_HEAD_COLS, "...")
|
|
332
|
+
dtypes_displayed.insert(MAX_HEAD_COLS, "...")
|
|
333
|
+
|
|
334
|
+
# Build header rows
|
|
335
|
+
header_rows, show_types_in_header = _header_rows(disp, san, dtypes_displayed)
|
|
336
|
+
|
|
337
|
+
# Align everything
|
|
338
|
+
aligned_cols, aligned_headers = _align_columns(formatted_cols, header_rows, dtypes_displayed)
|
|
339
|
+
|
|
340
|
+
# Build output
|
|
341
|
+
lines = []
|
|
342
|
+
for hrow in aligned_headers:
|
|
343
|
+
lines.append(" ".join(hrow))
|
|
344
|
+
|
|
345
|
+
# Table body
|
|
346
|
+
nrows = len(aligned_cols[0]) if aligned_cols else 0
|
|
347
|
+
for r in range(nrows):
|
|
348
|
+
row = " ".join(col[r] for col in aligned_cols)
|
|
349
|
+
lines.append(row)
|
|
350
|
+
|
|
351
|
+
lines.append("")
|
|
352
|
+
|
|
353
|
+
# Footer: use <mixed> if types are in header, otherwise show type info
|
|
354
|
+
if show_types_in_header:
|
|
355
|
+
lines.append(_footer(tbl, None, False, MAX_HEAD_COLS).replace(f"<{tbl._dtype.kind.__name__ if tbl._dtype else 'object'}>", "<mixed>"))
|
|
356
|
+
else:
|
|
357
|
+
# Check if all dtypes are the same (homogeneous table)
|
|
358
|
+
unique_dtypes = set(dtypes_all)
|
|
359
|
+
if len(unique_dtypes) == 1:
|
|
360
|
+
# Homogeneous - show single type
|
|
361
|
+
lines.append(_footer(tbl, None, False, MAX_HEAD_COLS).replace(f"<{tbl._dtype.kind.__name__ if tbl._dtype else 'object'}>", f"<{dtypes_all[0]}>"))
|
|
362
|
+
else:
|
|
363
|
+
# Keep showing all types
|
|
364
|
+
lines.append(_footer(tbl, dtypes_all, truncated, MAX_HEAD_COLS))
|
|
365
|
+
|
|
366
|
+
return "\n".join(lines)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _printr(pv) -> str:
|
|
370
|
+
"""Entry point used by Vector.__repr__ and Table.__repr__."""
|
|
371
|
+
nd = len(pv.shape)
|
|
372
|
+
if nd == 1:
|
|
373
|
+
return _repr_vector(pv)
|
|
374
|
+
if nd == 2:
|
|
375
|
+
return _repr_table(pv)
|
|
376
|
+
return _footer(pv) + " (repr not yet implemented)"
|
|
377
|
+
|
jib/errors.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class JibError(Exception):
|
|
2
|
+
"""Base exception for jib library."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JibKeyError(JibError, KeyError):
|
|
7
|
+
"""Raised when a column/key is missing."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JibTypeError(JibError, TypeError):
|
|
12
|
+
"""Raised for invalid types in API calls."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JibValueError(JibError, ValueError):
|
|
17
|
+
"""Raised for invalid values or mismatched lengths."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JibIndexError(JibError, IndexError):
|
|
22
|
+
"""Raised for invalid indexing operations."""
|
|
23
|
+
pass
|