Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 53
FEAT: Add to_dict(), keys(), values(), items(), __contains__ to Row #613
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
base:main
Are you sure you want to change the base?
Changes from all commits
a4852add21846e757a550a4804ae9b9630475998e231f3d4789c3b2fb7cb1a80917d1f93d9f37c2c6927ae22ff8e6142d004111b910292eba4b4ac2fa3c112ac74000File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -16,14 +16,21 @@ class Row: | ||
| A row of data from a cursor fetch operation. Provides both tuple-like indexing | ||
| and attribute access to column values. | ||
| Row supports dict-like access via keys(), values(), items(), and to_dict(). | ||
| However, iteration (for x in row) yields values, not keys — consistent with | ||
| pyodbc.Row and sqlite3.Row. Use row.keys() to iterate column names. | ||
| Column attribute access behavior depends on the global 'lowercase' setting: | ||
| - When enabled: Case-insensitive attribute access | ||
| - When disabled (default): Case-sensitive attribute access matching original column names | ||
| Example: | ||
| row = cursor.fetchone() | ||
| print(row[0]) # Access by index | ||
| print(row.column_name) # Access by column name (case sensitivity varies) | ||
| print(row[0]) # Access by index | ||
| print(row.column_name) # Access by column name | ||
| print(row.to_dict()) # Convert to dict | ||
| for value in row: # Iterates values, not keys | ||
| print(value) | ||
| """ | ||
| def __init__( | ||
| @@ -73,6 +80,7 @@ def __init__( | ||
| # Lowercase map is pre-built once per result set in the cursor and shared | ||
| # across all rows. None when lowercase is off (the default) — zero cost. | ||
| self._column_map_lower = column_map_lower | ||
| self._column_names = None # Lazy-computed on first access via _get_column_names() | ||
| def _stringify_uuids(self, indices): | ||
| """ | ||
| @@ -209,6 +217,41 @@ def __getattr__(self, name: str) -> Any: | ||
| raise AttributeError(f"Row has no attribute '{name}'") | ||
| def _get_column_names(self) -> tuple: | ||
| """Lazy-compute and cache deduplicated column names on first access.""" | ||
| if self._column_names is not None: | ||
| return self._column_names | ||
| if self._cursor and hasattr(self._cursor, "description") and self._cursor.description: | ||
| column_names = tuple(desc[0] for desc in self._cursor.description) | ||
| elif self._column_map: | ||
| idx_to_name: dict = {} | ||
| for name, idx in self._column_map.items(): | ||
| if idx not in idx_to_name: | ||
| idx_to_name[idx] = name | ||
| column_names = tuple(idx_to_name[i] for i in sorted(idx_to_name)) | ||
| else: | ||
| column_names = () | ||
| self._column_names = column_names | ||
| return column_names | ||
| def keys(self) -> tuple: | ||
jahnvi480 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just checked during testing, adding cursor.execute("SELECT 'timeout' AS keys, '30' AS [values]")
row=cursor.fetchone()
row.keys# before PR: 'timeout' (via __getattr__)row.keys# after PR: <bound method Row.keys>row["keys"] # still works: 'timeout'class methods win over | ||
| """Return column names, like dict.keys().""" | ||
| return self._get_column_names() | ||
| def values(self) -> tuple: | ||
| """Return column values as a tuple, like dict.values().""" | ||
| return tuple(self._values) | ||
| def items(self) -> list: | ||
| """Return (column_name, value) pairs, like dict.items().""" | ||
| return list(zip(self._get_column_names(), self._values)) | ||
| def to_dict(self) -> dict: | ||
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
cursor.execute(""" SELECT e.id, e.name, m.id, m.name FROM employees e LEFT JOIN employees m ON e.manager_id = m.id""")
row=cursor.fetchone()
list(row) # [1, 'Alice', 2, 'Bob'] — 4 valuesrow.to_dict() # {'id': 2, 'name': 'Bob'} — 2 entries, Alice gonelets discuss approaching this | ||
| """Return the row as a plain dict mapping column names to values.""" | ||
| return dict(zip(self._get_column_names(), self._values)) | ||
| def __eq__(self, other: Any) -> bool: | ||
| """ | ||
| Support comparison with lists for test compatibility. | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
regression caused from previous suggestion:
_get_column_names()readsself._cursor.descriptionlazily on first call. if the cursor re-executes beforekeys()/to_dict()is called on an old row, the old row picks up the new query's column names. repro:fix: snapshot
cursor.descriptionat construction time instead of reading it live. one pointer assignment, no computation:still lazy, still zero-cost if
keys()/to_dict()is never called, but safe on cursor reuse.