Repository navigation
Add __repr__ to TableProxy for meaningful print output #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0807b47
4229ea0
52e83d8
b374137
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # TableProxy 表示機能の修正報告書 | ||
|
|
||
| ## 概要 | ||
|
|
||
| 本修正は、`TableProxy` オブジェクトを `print()` で表示した際に、デフォルトのPythonオブジェクト表現(例:`<builtins.TableProxy object at 0x0000018DFFE6DB70>`)ではなく、テーブル名と内容を表示するように改善しました。 | ||
|
|
||
| ## 修正内容 | ||
|
|
||
| ### 1. `TableProxy` クラスの修正(lib.rs) | ||
|
|
||
| 以下のメソッドを追加しました: | ||
|
|
||
| - **`__repr__`**: テーブル名と内容を辞書形式で表示 | ||
| - 例: `TableProxy('users', {"alice": {'name': 'Alice', 'role': 'admin'}})` | ||
| - **`__str__`**: `__repr__` に委譲 | ||
|
|
||
| ### 2. `AsyncTableProxy` クラスの修正(async_ops.rs) | ||
|
|
||
| 同期版と同様に以下のメソッドを追加しました: | ||
|
|
||
| - **`__repr__`**: テーブル名と内容を辞書形式で表示 | ||
| - **`__str__`**: `__repr__` に委譲 | ||
| - **`keys()`**: テーブル内の全キーを取得 | ||
| - **`items()`**: テーブル内の全アイテムを (key, value) タプルで取得 | ||
| - **`__len__()`**: テーブル内のアイテム数を取得 | ||
|
|
||
| ### 3. テストの追加(test_table_proxy_repr.py) | ||
|
|
||
| 以下のテストケースを追加しました: | ||
|
|
||
| - `test_table_proxy_repr_basic`: 基本的な `__repr__` 機能のテスト | ||
| - `test_table_proxy_repr_empty`: 空のテーブルの `__repr__` テスト | ||
| - `test_table_proxy_repr_multiple_items`: 複数アイテムの `__repr__` テスト | ||
| - `test_async_table_proxy_repr`: 非同期版のテスト | ||
| - `test_table_proxy_print`: `print()` 出力のテスト | ||
|
|
||
| ## 使用例 | ||
|
|
||
| ```python | ||
| from dictsqlite import DictSQLiteV4 as DictSQLite | ||
|
|
||
| db = DictSQLite("app.db", table_name="users", storage_mode="jsonb") | ||
|
|
||
| # テーブルプロキシの作成 | ||
| users_table = db.table("users") | ||
| sessions_table = db.table("sessions") | ||
|
|
||
| # データの追加 | ||
| users_table["alice"] = {"name": "Alice", "role": "admin"} | ||
| sessions_table["session_123"] = {"user": "alice", "expires": "2025-10-10"} | ||
|
|
||
| # 修正前の出力 | ||
| # <builtins.TableProxy object at 0x0000018DFFE6DB70> | ||
|
|
||
| # 修正後の出力 | ||
| print(users_table) | ||
| # TableProxy('users', {"alice": {'name': 'Alice', 'role': 'admin'}}) | ||
|
|
||
| print(sessions_table) | ||
| # TableProxy('sessions', {"session_123": {'expires': '2025-10-10', 'user': 'alice'}}) | ||
|
|
||
| # 空のテーブル | ||
| empty_table = db.table("empty") | ||
| print(empty_table) | ||
| # TableProxy('empty', {}) | ||
| ``` | ||
|
|
||
| ## 技術的な詳細 | ||
|
|
||
| ### 実装方針 | ||
|
|
||
| 1. **Method Chaining**: エラー処理には `and_then` を使用したメソッドチェーンを採用し、コードの可読性を向上 | ||
| 2. **安全なフォールバック**: 値の `__repr__` 取得に失敗した場合は `"..."` を表示 | ||
| 3. **辞書形式の出力**: キーは引用符付き文字列、値はPythonのrepr形式で表示 | ||
|
|
||
| ### Bandit警告への対応 | ||
|
|
||
| - `pickle.loads()` 使用箇所に `# nosec B301` コメントを追加 | ||
| - 信頼できないデータには `safe_pickle` モジュールの使用を推奨 | ||
|
|
||
| ## テスト結果 | ||
|
|
||
| 全15件のテストがパスしました: | ||
|
|
||
| - `test_table_proxy_repr.py`: 5件パス | ||
| - `test_jsonb_table_support.py`: 10件パス | ||
|
|
||
| ## 関連ファイル | ||
|
|
||
| - `dictsqlite_v2/dictsqlite/src/lib.rs`: TableProxy 実装 | ||
| - `dictsqlite_v2/dictsqlite/src/async_ops.rs`: AsyncTableProxy 実装 | ||
| - `dictsqlite_v2/dictsqlite/tests/test_table_proxy_repr.py`: テストファイル | ||
|
|
||
| ## 結論 | ||
|
|
||
| 本修正により、`TableProxy` オブジェクトのデバッグや表示が大幅に改善され、開発者が直感的にテーブルの内容を確認できるようになりました。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -888,4 +888,83 @@ impl AsyncTableProxy { | |
| let result = db.get_async(full_key, py)?; | ||
| Ok(result.is_some()) | ||
| } | ||
|
|
||
| /// Get all keys in this table | ||
| fn keys(&self, py: Python) -> PyResult<Vec<String>> { | ||
| let db = self.db.borrow(py); | ||
| let prefix = format!("{}:", self.table_name); | ||
|
|
||
| // Get all keys from cache | ||
| let mut all_keys: Vec<String> = db | ||
| .cache | ||
| .iter() | ||
| .filter(|entry| entry.key().starts_with(&prefix)) | ||
| .map(|entry| entry.key()[prefix.len()..].to_string()) | ||
| .collect(); | ||
|
|
||
| // Also get keys from storage if not in memory mode | ||
| if db.config.persist_mode != PersistMode::Memory { | ||
| let storage_guard = db.storage.lock().unwrap(); | ||
| if let Some(ref storage) = *storage_guard { | ||
| if let Ok(storage_keys) = storage.keys() { | ||
| for key in storage_keys { | ||
| if key.starts_with(&prefix) { | ||
| let short_key = key[prefix.len()..].to_string(); | ||
| if !all_keys.contains(&short_key) { | ||
| all_keys.push(short_key); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(all_keys) | ||
| } | ||
|
|
||
| /// Get all items as (key, value) tuples | ||
| fn items(&self, py: Python) -> PyResult<Vec<(String, PyObject)>> { | ||
| let keys = self.keys(py)?; | ||
| let mut items = Vec::new(); | ||
| for key in keys { | ||
| items.push((key.clone(), self.__getitem__(key, py)?)); | ||
| } | ||
| Ok(items) | ||
| } | ||
|
|
||
| /// Get number of items in this table | ||
| fn __len__(&self, py: Python) -> PyResult<usize> { | ||
| Ok(self.keys(py)?.len()) | ||
| } | ||
|
|
||
| /// String representation: show table name and contents as dict | ||
| fn __repr__(&self, py: Python) -> PyResult<String> { | ||
| let items = self.items(py)?; | ||
| if items.is_empty() { | ||
| return Ok(format!("TableProxy('{}', {{}})", self.table_name)); | ||
| } | ||
|
|
||
| // Format items as dict-like string | ||
| let mut item_strs = Vec::new(); | ||
| for (key, value) in items { | ||
| // Try to get a string representation of the value using method chaining | ||
| let value_repr = value | ||
| .getattr(py, "__repr__") | ||
| .and_then(|repr_method| repr_method.call0(py)) | ||
| .and_then(|repr_result| repr_result.extract::<String>(py)) | ||
| .unwrap_or_else(|_| "...".to_string()); | ||
| item_strs.push(format!("{:?}: {}", key, value_repr)); | ||
| } | ||
|
|
||
| Ok(format!( | ||
| "TableProxy('{}', {{{}}})", | ||
| self.table_name, | ||
| item_strs.join(", ") | ||
| )) | ||
|
Comment on lines
+941
to
+963
|
||
| } | ||
|
|
||
| /// String representation for str() | ||
| fn __str__(&self, py: Python) -> PyResult<String> { | ||
| self.__repr__(py) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -706,7 +706,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// デフォルト設定の取得 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 一般的なユースケースに最適化されたデフォルト値を返します。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 必要に応じてこれらの値を上書きしてカスタマイズできます。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 709 in dictsqlite_v2/dictsqlite/src/lib.rs
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn default() -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Config { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hot_tier_capacity: 1_000_000, // 100万エントリ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1808,6 +1808,37 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn __len__(&self, py: Python) -> PyResult<usize> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(self.keys(py)?.len()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// String representation: show table name and contents as dict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn __repr__(&self, py: Python) -> PyResult<String> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let items = self.items(py)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if items.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(format!("TableProxy('{}', {{}})", self.table_name)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Format items as dict-like string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut item_strs = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (key, value) in items { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try to get a string representation of the value using method chaining | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let value_repr = value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .getattr(py, "__repr__") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .and_then(|repr_method| repr_method.call0(py)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .and_then(|repr_result| repr_result.extract::<String>(py)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "...".to_string()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| item_strs.push(format!("{:?}: {}", key, value_repr)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(format!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "TableProxy('{}', {{{}}})", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.table_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| item_strs.join(", ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1812
to
+1834
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// String representation: show table name and contents as dict | |
| fn __repr__(&self, py: Python) -> PyResult<String> { | |
| let items = self.items(py)?; | |
| if items.is_empty() { | |
| return Ok(format!("TableProxy('{}', {{}})", self.table_name)); | |
| } | |
| // Format items as dict-like string | |
| let mut item_strs = Vec::new(); | |
| for (key, value) in items { | |
| // Try to get a string representation of the value using method chaining | |
| let value_repr = value | |
| .getattr(py, "__repr__") | |
| .and_then(|repr_method| repr_method.call0(py)) | |
| .and_then(|repr_result| repr_result.extract::<String>(py)) | |
| .unwrap_or_else(|_| "...".to_string()); | |
| item_strs.push(format!("{:?}: {}", key, value_repr)); | |
| } | |
| Ok(format!( | |
| "TableProxy('{}', {{{}}})", | |
| self.table_name, | |
| item_strs.join(", ") | |
| /// String representation: show table name and contents as dict (limited to first 10 items) | |
| fn __repr__(&self, py: Python) -> PyResult<String> { | |
| // Limit the number of items displayed | |
| const MAX_ITEMS: usize = 10; | |
| let keys = self.keys(py)?; | |
| let total = keys.len(); | |
| if total == 0 { | |
| return Ok(format!("TableProxy('{}', {{}})", self.table_name)); | |
| } | |
| // Get up to MAX_ITEMS keys | |
| let shown_keys = keys.iter().take(MAX_ITEMS); | |
| let mut item_strs = Vec::new(); | |
| for key in shown_keys { | |
| // Get value for key | |
| let value = self.get(py, key)?; | |
| let value_repr = match value { | |
| Some(val) => val | |
| .getattr(py, "__repr__") | |
| .and_then(|repr_method| repr_method.call0(py)) | |
| .and_then(|repr_result| repr_result.extract::<String>(py)) | |
| .unwrap_or_else(|_| "...".to_string()), | |
| None => "None".to_string(), | |
| }; | |
| item_strs.push(format!("{:?}: {}", key, value_repr)); | |
| } | |
| let more = if total > MAX_ITEMS { | |
| format!(", ... {} more items", total - MAX_ITEMS) | |
| } else { | |
| "".to_string() | |
| }; | |
| Ok(format!( | |
| "TableProxy('{}', {{{}{}}})", | |
| self.table_name, | |
| item_strs.join(", "), | |
| more |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The __repr__ implementation is duplicated identically in AsyncTableProxy. This duplication makes maintenance harder - if a bug is found or improvement is needed in one place, it must be updated in both. Consider extracting the formatting logic into a shared helper function that both implementations can use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop uses
all_keys.contains(&short_key)to check for duplicates, which is O(n) for each key from storage. For tables with many keys, this results in O(n²) complexity. Consider using aHashSetto track seen keys instead, which would reduce this to O(n):