Skip to content

Commit 7d77202

Browse files
queeliusclaude
andcommitted
Fix 13 code review issues: bugs, security, compatibility, and cleanup
- Fix duplicate save/load/commit/whoami method definitions in fluent API - Fix from_json() mutating input dict via .pop() (now uses .get()) - Fix Node.is_file/is_dir/is_device/is_symlink to use proper POSIX bitmask - Replace os.system('clear') with ANSI escape sequence - Replace bare except: clauses with specific exception types - Fix Python 3.8 compat: tuple[str,str] → Tuple[str,str] - Sync __init__.py version to 0.2.1, fix placeholder author/email - Fix ls -l to show actual permissions via _format_mode() - Remove dead duplicate head/tail handler in CommandExecutor - Remove double history recording in run_interactive() - Move inline imports to module-level, remove unused imports - Add 23-test regression suite for all fixed issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ee81813 commit 7d77202

9 files changed

Lines changed: 334 additions & 188 deletions

CITATION.cff

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
cff-version: 1.2.0
2+
message: "If you use this software, please cite it as below."
3+
title: "dagshell"
4+
version: "0.2.1"
5+
license: MIT
6+
repository-code: "https://github.com/queelius/dagshell"
7+
url: "https://pypi.org/project/dagshell/"
8+
authors:
9+
- family-names: "Towell"
10+
given-names: "Alexander"
11+
orcid: "https://orcid.org/0000-0001-6443-9897"

dagshell/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
interface and an embedded Scheme interpreter.
77
"""
88

9-
__version__ = "0.1.0"
10-
__author__ = "Your Name"
11-
__email__ = "your.email@example.com"
9+
__version__ = "0.2.1"
10+
__author__ = "Alex Towell"
11+
__email__ = "lex@metafunctor.com"
1212

1313
from .dagshell import (
1414
FileSystem,

dagshell/dagshell.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
- Simple, composable operations following Unix philosophy
99
"""
1010

11+
import base64
12+
import builtins
1113
import hashlib
1214
import json
13-
import time
1415
import os
16+
import time
17+
from collections import defaultdict
1518
from dataclasses import dataclass, field, asdict
16-
from typing import Dict, List, Optional, Set, Union, Any
1719
from enum import IntEnum
18-
from collections import defaultdict
19-
import random
20-
import string
20+
from typing import Dict, List, Optional, Set, Tuple, Union, Any
2121

2222

2323
class Mode(IntEnum):
@@ -65,19 +65,19 @@ def to_dict(self) -> dict:
6565

6666
def is_file(self) -> bool:
6767
"""Check if this is a regular file."""
68-
return (self.mode & Mode.IFDIR) == 0 and (self.mode & Mode.IFCHR) == 0
68+
return (self.mode & 0o170000) == Mode.IFREG
6969

7070
def is_dir(self) -> bool:
7171
"""Check if this is a directory."""
72-
return (self.mode & Mode.IFDIR) != 0
72+
return (self.mode & 0o170000) == Mode.IFDIR
7373

7474
def is_device(self) -> bool:
7575
"""Check if this is a device."""
76-
return (self.mode & Mode.IFCHR) != 0
76+
return (self.mode & 0o170000) == Mode.IFCHR
7777

7878
def is_symlink(self) -> bool:
7979
"""Check if this is a symbolic link."""
80-
return (self.mode & Mode.IFLNK) == Mode.IFLNK
80+
return (self.mode & 0o170000) == Mode.IFLNK
8181

8282

8383
@dataclass(frozen=True)
@@ -98,7 +98,6 @@ def __init__(self, content: Union[str, bytes] = b"", mode: int = Mode.FILE_DEFAU
9898
def to_dict(self) -> dict:
9999
d = super().to_dict()
100100
# Store content as base64 for JSON serialization
101-
import base64
102101
d['content'] = base64.b64encode(self.content).decode('ascii')
103102
d['type'] = 'file'
104103
return d
@@ -328,7 +327,7 @@ def _resolve_path(self, path: str, follow_symlinks: bool = True,
328327

329328
return node_hash
330329

331-
def _get_parent_path(self, path: str) -> tuple[str, str]:
330+
def _get_parent_path(self, path: str) -> Tuple[str, str]:
332331
"""Split path into parent directory and basename."""
333332
path = os.path.normpath(path)
334333
if path == '/':
@@ -706,7 +705,7 @@ def _create_file(self, path: str, content: bytes, mode: str, mtime: Optional[flo
706705

707706
# User and permission management
708707

709-
def lookup_user(self, username: str) -> tuple[int, int]:
708+
def lookup_user(self, username: str) -> Tuple[int, int]:
710709
"""Look up user ID and primary group ID from /etc/passwd."""
711710
passwd_hash = self._resolve_path('/etc/passwd')
712711
if not passwd_hash:
@@ -813,8 +812,6 @@ def export_to_real(self, target_path: str, preserve_permissions: bool = True,
813812
Returns:
814813
Number of files/directories exported
815814
"""
816-
import shutil
817-
818815
# Default mappings (virtual -> real)
819816
if uid_map is None:
820817
uid_map = {0: 0, 1000: os.getuid(), 1001: os.getuid(), 1002: os.getuid()}
@@ -851,7 +848,7 @@ def export_to_real(self, target_path: str, preserve_permissions: bool = True,
851848
mode = node.mode & 0o777
852849
try:
853850
os.chmod(real_path, mode)
854-
except:
851+
except Exception:
855852
pass # Ignore permission errors
856853

857854
elif node.is_file():
@@ -860,7 +857,6 @@ def export_to_real(self, target_path: str, preserve_permissions: bool = True,
860857
os.makedirs(parent_dir, exist_ok=True)
861858

862859
# Write file content
863-
import builtins
864860
with builtins.open(real_path, 'wb') as f:
865861
f.write(node.content)
866862
exported += 1
@@ -870,7 +866,7 @@ def export_to_real(self, target_path: str, preserve_permissions: bool = True,
870866
mode = node.mode & 0o777
871867
try:
872868
os.chmod(real_path, mode)
873-
except:
869+
except Exception:
874870
pass # Ignore permission errors
875871

876872
# Try to set ownership if running as root
@@ -879,7 +875,7 @@ def export_to_real(self, target_path: str, preserve_permissions: bool = True,
879875
real_gid = gid_map.get(node.gid, node.gid)
880876
try:
881877
os.chown(real_path, real_uid, real_gid)
882-
except:
878+
except Exception:
883879
pass # Ignore if can't change ownership
884880

885881
return exported
@@ -905,8 +901,6 @@ def import_from_real(self, source_path: str, target_path: str = '/',
905901
FileNotFoundError: If source_path doesn't exist
906902
ValueError: If target_path is invalid
907903
"""
908-
import builtins
909-
910904
if uid is None:
911905
uid = 1000
912906
if gid is None:
@@ -1046,13 +1040,12 @@ def from_json(cls, json_str: str) -> 'FileSystem':
10461040
fs.refs = defaultdict(int)
10471041

10481042
# Reconstruct nodes
1049-
import base64
10501043
for hash, node_data in data['nodes'].items():
1051-
node_type = node_data.pop('type', 'file')
1044+
node_type = node_data.get('type', 'file')
10521045

10531046
if node_type == 'file':
1054-
content = base64.b64decode(node_data.pop('content', ''))
1055-
node = FileNode(content=content, **{k: v for k, v in node_data.items() if k != 'content'})
1047+
content = base64.b64decode(node_data.get('content', ''))
1048+
node = FileNode(content=content, **{k: v for k, v in node_data.items() if k not in ('type', 'content')})
10561049
elif node_type == 'dir':
10571050
node = DirNode(**{k: v for k, v in node_data.items() if k != 'type'})
10581051
elif node_type == 'device':

dagshell/dagshell_fluent.py

Lines changed: 6 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ def whoami(self) -> CommandResult:
318318
Usage:
319319
whoami
320320
321+
Examples:
322+
whoami # Show current username
323+
321324
Returns:
322325
Current username.
323326
"""
@@ -623,15 +626,15 @@ def ls(self, path: Optional[str] = None,
623626
files = [f for f in files if not f.startswith('.')]
624627

625628
if long:
626-
# Detailed listing (simplified for now)
629+
# Detailed listing with actual permissions
627630
detailed = []
628631
for f in files:
629632
full_path = os.path.join(target_path, f)
630633
fstat = self.fs.stat(full_path)
631634
if fstat:
632-
type_char = 'd' if fstat['type'] == 'dir' else '-'
635+
mode_str = self._format_mode(fstat.get('mode', 0o100644))
633636
size = fstat.get('size', 0)
634-
detailed.append(f"{type_char}rw-r--r-- 1 user user {size:8} {f}")
637+
detailed.append(f"{mode_str} 1 user user {size:8} {f}")
635638
result = CommandResult(data=files, text='\n'.join(detailed))
636639
else:
637640
result = CommandResult(data=files, text='\n'.join(files))
@@ -1196,8 +1199,6 @@ def grep(self, pattern: str, *paths: str,
11961199
Returns:
11971200
Lines matching the pattern.
11981201
"""
1199-
import re
1200-
12011202
# Prepare regex
12021203
flags = re.IGNORECASE if ignore_case else 0
12031204
try:
@@ -2033,7 +2034,6 @@ def save(self, filename: str = 'dagshell.json') -> CommandResult:
20332034
Status message.
20342035
"""
20352036
try:
2036-
import json
20372037
# Get JSON representation
20382038
json_data = self.fs.to_json()
20392039

@@ -2065,7 +2065,6 @@ def load(self, filename: str = 'dagshell.json') -> CommandResult:
20652065
Status message.
20662066
"""
20672067
try:
2068-
import json
20692068
# Read from real file
20702069
with open(filename, 'r') as f:
20712070
json_data = f.read()
@@ -2131,26 +2130,6 @@ def export(self, target_path: str, preserve_permissions: bool = True) -> Command
21312130
error = f"Export failed: {e}"
21322131
return CommandResult(data=error, text=error, exit_code=1)
21332132

2134-
def whoami(self) -> CommandResult:
2135-
"""Print effective username.
2136-
2137-
Usage:
2138-
whoami
2139-
2140-
Options:
2141-
None
2142-
2143-
Examples:
2144-
whoami # Show current username
2145-
2146-
Returns:
2147-
Current username.
2148-
"""
2149-
# For now, return a default user
2150-
# This will be overridden by terminal session
2151-
username = "user"
2152-
return CommandResult(data=username, text=username, exit_code=0)
2153-
21542133
def su(self, username: str = 'root') -> CommandResult:
21552134
"""Switch user account.
21562135
@@ -2173,86 +2152,6 @@ def su(self, username: str = 'root') -> CommandResult:
21732152
result = f"Switched to user: {username}"
21742153
return CommandResult(data=username, text=result, exit_code=0)
21752154

2176-
def save(self, filename: str = 'dagshell.json') -> CommandResult:
2177-
"""Save virtual filesystem to JSON file.
2178-
2179-
Usage:
2180-
save [FILENAME]
2181-
2182-
Options:
2183-
FILENAME File to save to (default: dagshell.json)
2184-
2185-
Examples:
2186-
save # Save to dagshell.json
2187-
save backup.json # Save to backup.json
2188-
2189-
Returns:
2190-
Status message.
2191-
"""
2192-
# Get filesystem state as JSON
2193-
json_data = self.fs.to_json()
2194-
2195-
# Write to real filesystem
2196-
with open(filename, 'w') as f:
2197-
f.write(json_data)
2198-
2199-
result = f"Filesystem saved to {filename}"
2200-
return CommandResult(data=json_data, text=result, exit_code=0)
2201-
2202-
def load(self, filename: str = 'dagshell.json') -> CommandResult:
2203-
"""Load virtual filesystem from JSON file.
2204-
2205-
Usage:
2206-
load [FILENAME]
2207-
2208-
Options:
2209-
FILENAME File to load from (default: dagshell.json)
2210-
2211-
Examples:
2212-
load # Load from dagshell.json
2213-
load backup.json # Load from backup.json
2214-
2215-
Returns:
2216-
Status message.
2217-
"""
2218-
try:
2219-
# Read from real filesystem
2220-
with open(filename, 'r') as f:
2221-
json_data = f.read()
2222-
2223-
# Create new filesystem from JSON
2224-
self.fs = dagshell.FileSystem.from_json(json_data)
2225-
2226-
# Reset working directory
2227-
self._cwd = '/'
2228-
2229-
result = f"Filesystem loaded from {filename}"
2230-
return CommandResult(data=json_data, text=result, exit_code=0)
2231-
except FileNotFoundError:
2232-
result = f"load: {filename}: No such file"
2233-
return CommandResult(data=None, text=result, exit_code=1)
2234-
except Exception as e:
2235-
result = f"load: {filename}: Error: {e}"
2236-
return CommandResult(data=None, text=result, exit_code=1)
2237-
2238-
def commit(self, filename: str = 'dagshell.json') -> CommandResult:
2239-
"""Alias for save - commit virtual filesystem to JSON file.
2240-
2241-
Usage:
2242-
commit [FILENAME]
2243-
2244-
Options:
2245-
FILENAME File to save to (default: dagshell.json)
2246-
2247-
Examples:
2248-
commit # Save to dagshell.json
2249-
commit state.json # Save to state.json
2250-
2251-
Returns:
2252-
Status message.
2253-
"""
2254-
return self.save(filename)
2255-
22562155
def import_file(self, real_path: str, virtual_path: Optional[str] = None,
22572156
safe_paths: Optional[List[str]] = None, recursive: bool = True) -> CommandResult:
22582157
"""Import a file from the real filesystem into the virtual filesystem.
@@ -2279,9 +2178,6 @@ def import_file(self, real_path: str, virtual_path: Optional[str] = None,
22792178
Returns:
22802179
Status message or error.
22812180
"""
2282-
import os
2283-
import pathlib
2284-
22852181
# Default safe paths if not specified
22862182
if safe_paths is None:
22872183
safe_paths = [
@@ -2410,8 +2306,6 @@ def export_file(self, virtual_path: str, real_path: Optional[str] = None,
24102306
Returns:
24112307
Status message or error.
24122308
"""
2413-
import os
2414-
24152309
# Default safe paths if not specified
24162310
if safe_paths is None:
24172311
safe_paths = [

0 commit comments

Comments
 (0)