fix: resolve CI type checking issues
Some checks failed
Shellhist CI / test (push) Has been cancelled
Shellhist CI / build (push) Has been cancelled
CI / test (push) Has been cancelled

- Add return type annotations to __hash__ (-> int) and __eq__ (-> bool) in HistoryEntry
- Add TextIO import and type annotations for file parameters
- Add type ignore comment for fuzzywuzzy import
- Add HistoryEntry import and list type annotations in time_analysis
- Add assert statements for Optional[datetime] timestamps
- Add TypedDict classes for type-safe pattern dictionaries
- Add CommandPattern import and list[CommandPattern] type annotation
- Add -> None return types to all test methods
- Remove unused HistoryEntry import (F401)
This commit is contained in:
2026-01-31 14:19:08 +00:00
parent e90654ed10
commit 71f7849892

View File

@@ -1 +1,175 @@
bmFtZTogQ0kKCm9uOgogIHB1c2g6CiAgICBicmFuY2hlczogW21haW5dCiAgcHVsbF9yZXF1ZXN0OgogICAgYnJhbmNoZXM6IFttYWluXQoKam9iczogCiAgdGVzdDoKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIHN0ZXBzOgogICAgICAtIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXQdjNFgKICAgICAgdXNlczogYWN0aW9ucy9zZXR1cC1weXRob25fdjUKICAgICAgd2l0aDoKICAgICAgICBweXRob24tdmVyc2lvbjogJzMuMTEnCiAgICAtIHJ1bjogcGlwIGluc3RhbGwgLWUgIltcImRldlwiXSIKICAgIC0gcnVuOiBweXRlc3QgdGVzdHMvIC12CiAgICAtIHJ1bjogcnVmZiBjaGVjayAu
"""Core module for shell history loading and parsing."""
import os
import re
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, TextIO
@dataclass
class HistoryEntry:
"""Represents a single entry in shell history."""
command: str
timestamp: Optional[datetime] = None
line_number: int = 0
shell_type: str = "unknown"
def __hash__(self) -> int:
return hash(self.command)
def __eq__(self, other: object) -> bool:
if isinstance(other, HistoryEntry):
return self.command == other.command
return False
@dataclass
class HistoryStore:
"""Stores and manages shell history entries."""
entries: list[HistoryEntry] = field(default_factory=list)
command_frequency: dict[str, int] = field(default_factory=dict)
def add_entry(self, entry: HistoryEntry) -> None:
"""Add a history entry to the store."""
self.entries.append(entry)
self.command_frequency[entry.command] = self.command_frequency.get(entry.command, 0) + 1
def get_frequency(self, command: str) -> int:
"""Get the frequency of a command."""
return self.command_frequency.get(command, 0)
def get_most_frequent(self, limit: int = 10) -> list[tuple[str, int]]:
"""Get the most frequently used commands."""
sorted_freq = sorted(self.command_frequency.items(), key=lambda x: x[1], reverse=True)
return sorted_freq[:limit]
def get_unique_commands(self) -> list[str]:
"""Get list of unique commands."""
return list(self.command_frequency.keys())
class HistoryLoader:
"""Loads and parses shell history from various shell formats."""
BASH_TIMESTAMP_PATTERN = re.compile(r'^#(\d{10})$')
ZSH_TIMESTAMP_PATTERN = re.compile(r'^: (\d{10}):(\d+);(.*)$')
def __init__(self, history_path: Optional[str] = None):
"""Initialize the history loader.
Args:
history_path: Optional path to history file. If not provided,
will attempt to detect from environment.
"""
self.history_path = history_path
self.shell_type = self._detect_shell()
def _detect_shell(self) -> str:
"""Detect the shell type from environment."""
shell = os.environ.get("SHELL", "")
if "zsh" in shell:
return "zsh"
elif "bash" in shell:
return "bash"
return "bash"
def _get_default_history_path(self) -> Optional[str]:
"""Get the default history file path based on shell type."""
home = os.path.expanduser("~")
if self.shell_type == "zsh":
return os.environ.get("HISTFILE", f"{home}/.zsh_history")
return os.environ.get("HISTFILE", f"{home}/.bash_history")
def load(self) -> HistoryStore:
"""Load history from the configured path.
Returns:
HistoryStore containing all parsed entries.
"""
path = self.history_path or self._get_default_history_path()
if not path or not os.path.exists(path):
raise FileNotFoundError(
f"History file not found: {path}. "
"Set HISTFILE environment variable or use --history flag."
)
store = HistoryStore()
with open(path, "r", encoding="utf-8", errors="replace") as f:
if self.shell_type == "zsh":
self._parse_zsh_history(f, store)
else:
self._parse_bash_history(f, store)
return store
def _parse_bash_history(self, file: TextIO, store: HistoryStore) -> None:
"""Parse bash history format."""
line_number = 0
pending_timestamp: Optional[datetime] = None
for line in file:
line = line.rstrip("\n")
line_number += 1
timestamp_match = self.BASH_TIMESTAMP_PATTERN.match(line)
if timestamp_match:
ts = int(timestamp_match.group(1))
pending_timestamp = datetime.fromtimestamp(ts)
continue
if line and not line.startswith("#"):
command = line
entry = HistoryEntry(
command=command,
timestamp=pending_timestamp,
line_number=line_number,
shell_type="bash"
)
store.add_entry(entry)
pending_timestamp = None
def _parse_zsh_history(self, file: TextIO, store: HistoryStore) -> None:
"""Parse zsh history format."""
line_number = 0
for line in file:
line = line.rstrip("\n")
line_number += 1
timestamp_match = self.ZSH_TIMESTAMP_PATTERN.match(line)
if timestamp_match:
ts = int(timestamp_match.group(1))
command = timestamp_match.group(3)
timestamp = datetime.fromtimestamp(ts)
entry = HistoryEntry(
command=command,
timestamp=timestamp,
line_number=line_number,
shell_type="zsh"
)
store.add_entry(entry)
elif line:
entry = HistoryEntry(
command=line,
timestamp=None,
line_number=line_number,
shell_type="zsh"
)
store.add_entry(entry)
@staticmethod
def from_file(path: str) -> HistoryStore:
"""Convenience method to load history from a specific file.
Args:
path: Path to history file.
Returns:
HistoryStore containing parsed entries.
"""
loader = HistoryLoader(history_path=path)
return loader.load()