learnstat 0.2.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (34) hide show
  1. {learnstat-0.2.0 → learnstat-0.2.1}/PKG-INFO +29 -1
  2. {learnstat-0.2.0 → learnstat-0.2.1}/README.md +28 -0
  3. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/__init__.py +1 -1
  4. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/cli.py +7 -14
  5. learnstat-0.2.1/learnstat/guide.py +266 -0
  6. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/PKG-INFO +29 -1
  7. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/SOURCES.txt +1 -0
  8. {learnstat-0.2.0 → learnstat-0.2.1}/pyproject.toml +1 -1
  9. {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_ai_config.py +63 -0
  10. {learnstat-0.2.0 → learnstat-0.2.1}/LICENSE +0 -0
  11. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/advisor.py +0 -0
  12. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/commands.py +0 -0
  13. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/config.py +0 -0
  14. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/ipython_ext.py +0 -0
  15. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/knowledge.py +0 -0
  16. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/llm.py +0 -0
  17. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/memory.py +0 -0
  18. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/profile.py +0 -0
  19. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/providers.py +0 -0
  20. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/recovery.py +0 -0
  21. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/safety.py +0 -0
  22. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/session.py +0 -0
  23. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/setup.py +0 -0
  24. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/suggestions.py +0 -0
  25. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/dependency_links.txt +0 -0
  26. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/entry_points.txt +0 -0
  27. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/requires.txt +0 -0
  28. {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/top_level.txt +0 -0
  29. {learnstat-0.2.0 → learnstat-0.2.1}/setup.cfg +0 -0
  30. {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_advisor.py +0 -0
  31. {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_commands.py +0 -0
  32. {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_ipython.py +0 -0
  33. {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_profile_memory.py +0 -0
  34. {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_recovery.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learnstat
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: An AI-assisted learning companion for statistical analysis with pandas, NumPy, SciPy and Matplotlib.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/umorfaruksupto/leanstat
@@ -54,6 +54,33 @@ pip install -e ".[all]" # pandas, numpy, matplotlib, scipy, ipython
54
54
  pip install -e . # core only: profiling, rules, recovery
55
55
  ```
56
56
 
57
+ ### Quick start (no commands to learn)
58
+
59
+ ```bash
60
+ pip install learnstat
61
+ learnstat
62
+ ```
63
+
64
+ That is all. learnstat walks you through everything in plain English:
65
+
66
+ 1. **Pick your AI** (optional): choose the company, paste your key, choose a model. learnstat tests it and says OK or what is wrong.
67
+ 2. **Choose your data**: press Enter and a normal file window opens (CSV or Excel), or drag the file into the terminal.
68
+ 3. **Pick what to do from a menu** by typing a number:
69
+
70
+ ```
71
+ 1) See a summary of my data
72
+ 2) Ask the AI a question about my data
73
+ 3) Get suggestions for a Python script
74
+ 4) Run a Python script with automatic help
75
+ 5) Write Python code here (my data is ready as "df")
76
+ 6) Learn a statistics topic
77
+ 7) Load a different data file
78
+ 8) Change the AI (company, key, model)
79
+ 9) Help
80
+ ```
81
+
82
+ Nothing else needs to be memorised: every option asks for what it needs, and option 9 explains the menu. Skip the AI and everything still works with built-in rules.
83
+
57
84
  ### Add an AI (optional)
58
85
 
59
86
  Just run `learnstat`. If no AI is set up yet it offers a guided setup, and you can repeat it any time with `learnstat setup` (or `/ai` inside a session):
@@ -177,6 +204,7 @@ learnstat/
177
204
  config.py saved AI accounts: provider, api_key, base_url, model
178
205
  providers.py presets for the AI companies
179
206
  setup.py guided terminal setup (company -> key -> model -> check)
207
+ guide.py the numbered home menu students use
180
208
  session.py ties it together; read_csv/track/check/run
181
209
  commands.py slash commands
182
210
  ipython_ext.py %load_ext learnstat
@@ -24,6 +24,33 @@ pip install -e ".[all]" # pandas, numpy, matplotlib, scipy, ipython
24
24
  pip install -e . # core only: profiling, rules, recovery
25
25
  ```
26
26
 
27
+ ### Quick start (no commands to learn)
28
+
29
+ ```bash
30
+ pip install learnstat
31
+ learnstat
32
+ ```
33
+
34
+ That is all. learnstat walks you through everything in plain English:
35
+
36
+ 1. **Pick your AI** (optional): choose the company, paste your key, choose a model. learnstat tests it and says OK or what is wrong.
37
+ 2. **Choose your data**: press Enter and a normal file window opens (CSV or Excel), or drag the file into the terminal.
38
+ 3. **Pick what to do from a menu** by typing a number:
39
+
40
+ ```
41
+ 1) See a summary of my data
42
+ 2) Ask the AI a question about my data
43
+ 3) Get suggestions for a Python script
44
+ 4) Run a Python script with automatic help
45
+ 5) Write Python code here (my data is ready as "df")
46
+ 6) Learn a statistics topic
47
+ 7) Load a different data file
48
+ 8) Change the AI (company, key, model)
49
+ 9) Help
50
+ ```
51
+
52
+ Nothing else needs to be memorised: every option asks for what it needs, and option 9 explains the menu. Skip the AI and everything still works with built-in rules.
53
+
27
54
  ### Add an AI (optional)
28
55
 
29
56
  Just run `learnstat`. If no AI is set up yet it offers a guided setup, and you can repeat it any time with `learnstat setup` (or `/ai` inside a session):
@@ -147,6 +174,7 @@ learnstat/
147
174
  config.py saved AI accounts: provider, api_key, base_url, model
148
175
  providers.py presets for the AI companies
149
176
  setup.py guided terminal setup (company -> key -> model -> check)
177
+ guide.py the numbered home menu students use
150
178
  session.py ties it together; read_csv/track/check/run
151
179
  commands.py slash commands
152
180
  ipython_ext.py %load_ext learnstat
@@ -21,7 +21,7 @@ from .recovery import Executor, RunResult
21
21
  from .session import Session, get_session, set_session
22
22
  from .suggestions import Suggestion
23
23
 
24
- __version__ = "0.2.0"
24
+ __version__ = "0.2.1"
25
25
 
26
26
 
27
27
  def read_csv(path, name: str | None = None, **kwargs):
@@ -118,8 +118,9 @@ def _repl(s: Session, data: str | None) -> int:
118
118
  ready = ["pd", "np"]
119
119
  if data:
120
120
  s.ns["df"] = s.read_csv(data)
121
+ if "df" in s.ns:
121
122
  ready.insert(0, "df")
122
- print(f"learnstat REPL - AI: {s.ai_status}. Ready: {', '.join(ready)}. Type /help for commands, /ai to change the AI, Ctrl+Z/Ctrl+D to exit.")
123
+ print(f"learnstat REPL - AI: {s.ai_status}. Ready: {', '.join(ready)}. Type /help for commands, /ai to change the AI, 'menu' to go back, Ctrl+Z/Ctrl+D to exit.")
123
124
  buffer: list[str] = []
124
125
  while True:
125
126
  try:
@@ -127,6 +128,8 @@ def _repl(s: Session, data: str | None) -> int:
127
128
  except (EOFError, KeyboardInterrupt):
128
129
  print()
129
130
  return 0
131
+ if not buffer and line.strip().lower() in ("menu", "back", "exit", "quit", "exit()", "quit()"):
132
+ return 0
130
133
  if not buffer and is_command(line):
131
134
  print(s.command(line))
132
135
  continue
@@ -205,18 +208,6 @@ def cmd_config(args) -> int:
205
208
  return 0
206
209
 
207
210
 
208
- def _ask_data() -> str | None:
209
- try:
210
- print()
211
- path = input("Dataset to load (CSV/Excel path, Enter to skip): ").strip().strip('"')
212
- except (EOFError, KeyboardInterrupt):
213
- return None
214
- if path and not Path(path).exists():
215
- print(f" '{path}' was not found - starting without a dataset.")
216
- return None
217
- return path or None
218
-
219
-
220
211
  def _start(args, run_setup: bool) -> int:
221
212
  """The guided start: (optional) AI setup, a quick AI check, then the interactive session."""
222
213
  from . import config
@@ -251,8 +242,10 @@ def _start(args, run_setup: bool) -> int:
251
242
  print()
252
243
  print(f"X {a.last_error}")
253
244
  print(" Continuing with offline rules. Type /ai in the session to fix it.")
245
+ from .guide import run_guide
246
+
254
247
  s = _session(args)
255
- return _repl(s, args.data or _ask_data())
248
+ return run_guide(s, repl=lambda: _repl(s, None), data=args.data)
256
249
 
257
250
 
258
251
  def cmd_setup(args) -> int:
@@ -0,0 +1,266 @@
1
+ """The guided home screen: students pick numbers from a menu instead of learning commands.
2
+
3
+ After the AI is set up, ``learnstat`` shows this menu. Every action asks for what it
4
+ needs in plain English (pick a file in a window, type a question) and prints the result.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Callable
12
+
13
+ from .session import Session
14
+ from .setup import Cancelled, IO, run_wizard
15
+
16
+ DATA_TYPES = [("Data files", "*.csv *.tsv *.txt *.xlsx *.xls *.json *.parquet"), ("All files", "*.*")]
17
+ SCRIPT_TYPES = [("Python files", "*.py"), ("All files", "*.*")]
18
+
19
+ MENU = """
20
+ What would you like to do?
21
+
22
+ 1) See a summary of my data
23
+ 2) Ask the AI a question about my data
24
+ 3) Get suggestions for a Python script
25
+ 4) Run a Python script with automatic help
26
+ 5) Write Python code here (my data is ready as "df")
27
+ 6) Learn a statistics topic
28
+ 7) Load a different data file
29
+ 8) Change the AI (company, key, model)
30
+ 9) Help
31
+ q) Quit"""
32
+
33
+ HELP_TEXT = """
34
+ How learnstat works
35
+ -------------------
36
+ 1 Shows what is in your data: columns, types, missing values and possible problems.
37
+ 2 Type any question in your own words, for example "which column has the most missing values?".
38
+ 3 Pick a Python file. learnstat reads it (without running it) and suggests better statistics
39
+ or charts for your data. Enter a suggestion's number to get it explained.
40
+ 4 Runs your Python file. If it crashes on a simple mistake (like a misspelled column) learnstat
41
+ explains it and can fix it for you.
42
+ 5 A Python prompt with your data already loaded as df. Type "menu" to come back here.
43
+ Inside it, lines starting with / are shortcuts, and /help lists them.
44
+ 6 Short lessons, for example "t-test", "correlation" or "missing values".
45
+ 7 Load another file. 8 Switch to another AI company or key.
46
+
47
+ The AI is optional: without it learnstat still gives rule-based suggestions and explanations."""
48
+
49
+ PYTHON_TIPS = (
50
+ 'Python prompt - your data is "df". Try: df.head() or df.describe()\n'
51
+ ' Type /help for shortcuts, "menu" to go back to the menu.'
52
+ )
53
+
54
+
55
+ def pick_file(title: str, types: list[tuple[str, str]]) -> str | None:
56
+ """Open the system file window. Returns None when cancelled or when no window is available."""
57
+ try:
58
+ import tkinter
59
+ from tkinter import filedialog
60
+
61
+ root = tkinter.Tk()
62
+ root.withdraw()
63
+ root.attributes("-topmost", True)
64
+ try:
65
+ path = filedialog.askopenfilename(title=title, filetypes=types)
66
+ finally:
67
+ root.destroy()
68
+ return path or None
69
+ except Exception:
70
+ return None
71
+
72
+
73
+ def _clean(path: str) -> str:
74
+ return path.strip().strip("\"'")
75
+
76
+
77
+ def ask_file(io: IO, pick: Callable, what: str, types: list[tuple[str, str]]) -> Path | None:
78
+ answer = io.line(f"\nChoose your {what}. Press Enter to browse, or paste / drag the file here (b = back): ")
79
+ if answer.lower() in ("b", "back"):
80
+ return None
81
+ if not answer:
82
+ chosen = pick(f"Choose your {what}", types)
83
+ if not chosen:
84
+ io.say("No file chosen. You can also paste the full path to the file.")
85
+ return None
86
+ answer = chosen
87
+ path = Path(_clean(answer)).expanduser()
88
+ if not path.is_file():
89
+ io.say(f"I can't find '{path}'. Check the name, or press Enter next time to browse for it.")
90
+ return None
91
+ return path
92
+
93
+
94
+ def load_dataset(s: Session, io: IO, path: Path):
95
+ suffix = path.suffix.lower()
96
+ try:
97
+ if suffix in (".xlsx", ".xls"):
98
+ df = s.read_excel(path)
99
+ elif suffix == ".json":
100
+ df = s.read_json(path)
101
+ elif suffix == ".parquet":
102
+ df = s.read_parquet(path)
103
+ elif suffix == ".tsv":
104
+ df = s.read_csv(path, sep="\t")
105
+ else:
106
+ df = s.read_csv(path)
107
+ except ImportError as e:
108
+ io.say(f"Reading {suffix} files needs an extra package: {e}")
109
+ return None
110
+ except Exception as e:
111
+ io.say(f"I couldn't read that file as data ({type(e).__name__}: {e}).")
112
+ return None
113
+ s.ns["df"] = df
114
+ io.say('\nYour data is ready, and is available as "df" when you write Python code.')
115
+ return df
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # menu actions
120
+
121
+
122
+ def _summary(s: Session, io: IO) -> None:
123
+ if not s.datasets:
124
+ io.say("No data is loaded yet - choose 7 to load a file.")
125
+ return
126
+ io.say("\n" + s.profile_markdown())
127
+
128
+
129
+ def _ask_ai(s: Session, io: IO) -> None:
130
+ if not s.assistant:
131
+ io.say(f"The AI is {s.ai_status}. Choose 8 to set one up - or use 6 for built-in lessons.")
132
+ return
133
+ io.say('\nAsk anything about your data. Example: "which columns have missing values?"')
134
+ while True:
135
+ question = io.line("\nYour question (Enter to go back): ")
136
+ if not question:
137
+ return
138
+ io.say("\nThinking ...")
139
+ io.say(s.command(f"/ask {question}"))
140
+
141
+
142
+ def _read_script(io: IO, pick: Callable) -> tuple[Path, str] | None:
143
+ path = ask_file(io, pick, "Python file", SCRIPT_TYPES)
144
+ if path is None:
145
+ return None
146
+ code = path.read_text(encoding="utf-8")
147
+ try:
148
+ compile(code, str(path), "exec")
149
+ except SyntaxError as e:
150
+ io.say(f"There is a syntax error on line {e.lineno}: {e.msg}. Fix it and try again.")
151
+ return None
152
+ return path, code
153
+
154
+
155
+ def _check_script(s: Session, io: IO, pick: Callable) -> None:
156
+ got = _read_script(io, pick)
157
+ if got is None:
158
+ return
159
+ path, code = got
160
+ found = s.check(code, show=False, extra_frames=s.profiles_for_code(code, base_dir=path.parent))
161
+ if not found:
162
+ io.say("\nNo suggestions - the rules found nothing to improve for this data.")
163
+ return
164
+ io.say(f"\n{len(found)} suggestion(s):\n")
165
+ for sug in found:
166
+ io.say(sug.render())
167
+ io.say("")
168
+ while True:
169
+ pick_id = io.line("Enter a suggestion number to have it explained (Enter to go back): ")
170
+ if not pick_id:
171
+ return
172
+ io.say("\n" + s.command(f"/describe {pick_id.lstrip('#')}") + "\n")
173
+
174
+
175
+ def _run_script(s: Session, io: IO, pick: Callable) -> None:
176
+ got = _read_script(io, pick)
177
+ if got is None:
178
+ return
179
+ path, code = got
180
+ s.ns.update({"__name__": "__main__", "__file__": str(path.resolve())})
181
+ sys.path.insert(0, str(path.parent.resolve()))
182
+ s.check(code, extra_frames=s.profiles_for_code(code, base_dir=path.parent))
183
+ result = s.run(code, check=False, filename=str(path))
184
+ if result.ok:
185
+ io.say("\nDone - the script finished.")
186
+ if result.changed and io.yes("learnstat corrected something. Save a corrected copy next to your file?"):
187
+ out = path.with_suffix(".fixed.py")
188
+ out.write_text(result.final_code, encoding="utf-8")
189
+ io.say(f"Saved {out} (your original is unchanged).")
190
+ else:
191
+ io.say("\nThe script stopped with an error.")
192
+ if io.yes("Explain what went wrong?"):
193
+ io.say("\n" + s.command("/describe"))
194
+
195
+
196
+ def _learn(s: Session, io: IO) -> None:
197
+ io.say('\nExamples: t-test, correlation, missing values, skewed data, pie charts, histograms')
198
+ topic = io.line("Topic (Enter for a lesson about your latest suggestion): ")
199
+ io.say("\n" + s.command(f"/learn {topic}".strip()))
200
+
201
+
202
+ def _load(s: Session, io: IO, pick: Callable) -> None:
203
+ path = ask_file(io, pick, "data file (CSV or Excel)", DATA_TYPES)
204
+ if path is not None:
205
+ load_dataset(s, io, path)
206
+
207
+
208
+ def _change_ai(s: Session, io: IO) -> None:
209
+ run_wizard(io)
210
+ s.connect_ai()
211
+ io.say(f"\nAI: {s.ai_status}")
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+
216
+
217
+ def run_guide(s: Session, io: IO | None = None, pick: Callable = pick_file,
218
+ repl: Callable[[], object] | None = None, data: str | None = None) -> int:
219
+ """Show the home menu until the user quits."""
220
+ io = io or IO()
221
+ io.say("\n" + "=" * 60 + "\n learnstat - learn statistics with Python\n" + "=" * 60)
222
+ io.say(f"AI: {s.ai_status}")
223
+ try:
224
+ if data:
225
+ path = Path(_clean(data)).expanduser()
226
+ if path.is_file():
227
+ load_dataset(s, io, path)
228
+ else:
229
+ io.say(f"I can't find '{path}'.")
230
+ elif io.yes("\nLoad your data file now?"):
231
+ try:
232
+ _load(s, io, pick)
233
+ except Cancelled:
234
+ pass
235
+ actions: dict[str, Callable[[], None]] = {
236
+ "1": lambda: _summary(s, io),
237
+ "2": lambda: _ask_ai(s, io),
238
+ "3": lambda: _check_script(s, io, pick),
239
+ "4": lambda: _run_script(s, io, pick),
240
+ "6": lambda: _learn(s, io),
241
+ "7": lambda: _load(s, io, pick),
242
+ "8": lambda: _change_ai(s, io),
243
+ "9": lambda: io.say(HELP_TEXT),
244
+ }
245
+ if repl is not None:
246
+ def python_mode() -> None:
247
+ io.say("\n" + PYTHON_TIPS)
248
+ repl()
249
+ actions["5"] = python_mode
250
+ while True:
251
+ io.say(MENU)
252
+ choice = io.line("\nType a number: ").lower()
253
+ if choice in ("q", "quit", "exit"):
254
+ break
255
+ action = actions.get(choice)
256
+ if action is None:
257
+ io.say("Please type one of the numbers in the menu (or q to quit).")
258
+ continue
259
+ try:
260
+ action()
261
+ except Cancelled:
262
+ io.say("\n(back to the menu)")
263
+ except Cancelled:
264
+ pass
265
+ io.say("\nGoodbye - your dataset profiles are saved in .learnstat/ so next time is faster.")
266
+ return 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learnstat
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: An AI-assisted learning companion for statistical analysis with pandas, NumPy, SciPy and Matplotlib.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/umorfaruksupto/leanstat
@@ -54,6 +54,33 @@ pip install -e ".[all]" # pandas, numpy, matplotlib, scipy, ipython
54
54
  pip install -e . # core only: profiling, rules, recovery
55
55
  ```
56
56
 
57
+ ### Quick start (no commands to learn)
58
+
59
+ ```bash
60
+ pip install learnstat
61
+ learnstat
62
+ ```
63
+
64
+ That is all. learnstat walks you through everything in plain English:
65
+
66
+ 1. **Pick your AI** (optional): choose the company, paste your key, choose a model. learnstat tests it and says OK or what is wrong.
67
+ 2. **Choose your data**: press Enter and a normal file window opens (CSV or Excel), or drag the file into the terminal.
68
+ 3. **Pick what to do from a menu** by typing a number:
69
+
70
+ ```
71
+ 1) See a summary of my data
72
+ 2) Ask the AI a question about my data
73
+ 3) Get suggestions for a Python script
74
+ 4) Run a Python script with automatic help
75
+ 5) Write Python code here (my data is ready as "df")
76
+ 6) Learn a statistics topic
77
+ 7) Load a different data file
78
+ 8) Change the AI (company, key, model)
79
+ 9) Help
80
+ ```
81
+
82
+ Nothing else needs to be memorised: every option asks for what it needs, and option 9 explains the menu. Skip the AI and everything still works with built-in rules.
83
+
57
84
  ### Add an AI (optional)
58
85
 
59
86
  Just run `learnstat`. If no AI is set up yet it offers a guided setup, and you can repeat it any time with `learnstat setup` (or `/ai` inside a session):
@@ -177,6 +204,7 @@ learnstat/
177
204
  config.py saved AI accounts: provider, api_key, base_url, model
178
205
  providers.py presets for the AI companies
179
206
  setup.py guided terminal setup (company -> key -> model -> check)
207
+ guide.py the numbered home menu students use
180
208
  session.py ties it together; read_csv/track/check/run
181
209
  commands.py slash commands
182
210
  ipython_ext.py %load_ext learnstat
@@ -6,6 +6,7 @@ learnstat/advisor.py
6
6
  learnstat/cli.py
7
7
  learnstat/commands.py
8
8
  learnstat/config.py
9
+ learnstat/guide.py
9
10
  learnstat/ipython_ext.py
10
11
  learnstat/knowledge.py
11
12
  learnstat/llm.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "learnstat"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "An AI-assisted learning companion for statistical analysis with pandas, NumPy, SciPy and Matplotlib."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -256,3 +256,66 @@ def test_ai_slash_list(server):
256
256
  s = Session(ai=False, out=lambda *_: None)
257
257
  out = dispatch(s, "/ai list")
258
258
  assert "x:" in out and "gpt-a" in out and "k1234567890" not in out
259
+
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # guided menu
263
+
264
+
265
+ def _guide_run(tmp_path, answers, files=None, **kw):
266
+ from learnstat.guide import run_guide
267
+ from learnstat.session import Session
268
+
269
+ s = Session(memory_dir=tmp_path / "mem", ai=False, out=lambda *_: None, confirm=lambda q: True)
270
+ script = Script(answers)
271
+ queue = list(files or [])
272
+ pick = lambda title, types: queue.pop(0) if queue else None
273
+ rc = run_guide(s, script.io(), pick=pick, **kw)
274
+ return s, script, rc
275
+
276
+
277
+ def test_guide_load_data_with_file_window_and_summary(tmp_path):
278
+ csv = tmp_path / "survey.csv"
279
+ csv.write_text("age,income\n20,100\n30,200\n40,\n", encoding="utf-8")
280
+ # load now? y -> Enter opens the file window -> menu: 1 (summary) -> q
281
+ s, script, rc = _guide_run(tmp_path, ["y", "", "1", "q"], files=[str(csv)])
282
+ assert rc == 0 and "df" in s.ns and list(s.datasets) == ["survey"]
283
+ assert 'available as "df"' in script.text and "survey" in script.text
284
+
285
+
286
+ def test_guide_missing_file_and_bad_menu_choice(tmp_path):
287
+ s, script, _ = _guide_run(tmp_path, ["y", str(tmp_path / "nope.csv"), "zzz", "q"])
288
+ assert "I can't find" in script.text and "Please type one of the numbers" in script.text
289
+
290
+
291
+ def test_guide_check_script_and_explain(tmp_path):
292
+ csv = tmp_path / "survey.csv"
293
+ csv.write_text("country,income\n" + "\n".join(f"c{i % 12},{i * 10}" for i in range(60)) + "\n", encoding="utf-8")
294
+ py = tmp_path / "analysis.py"
295
+ py.write_text(f"import pandas as pd\ndf = pd.read_csv(r'{csv}')\ndf['country'].value_counts().plot.pie()\n", encoding="utf-8")
296
+ s, script, _ = _guide_run(tmp_path, ["n", "3", str(py), "1", "", "q"])
297
+ assert "suggestion(s)" in script.text and "Pie chart" in script.text
298
+ assert "In your code:" in script.text # the /describe explanation
299
+
300
+
301
+ def test_guide_run_script_with_automatic_help(tmp_path):
302
+ py = tmp_path / "ok.py"
303
+ py.write_text("total = 1 + 1\n", encoding="utf-8")
304
+ s, script, _ = _guide_run(tmp_path, ["n", "4", str(py), "q"])
305
+ assert "the script finished" in script.text and s.ns["total"] == 2
306
+
307
+
308
+ def test_guide_without_ai_points_to_setup(tmp_path):
309
+ s, script, _ = _guide_run(tmp_path, ["n", "2", "q"])
310
+ assert "Choose 8" in script.text
311
+
312
+
313
+ def test_guide_python_mode_returns_to_menu(tmp_path):
314
+ called = []
315
+ s, script, _ = _guide_run(tmp_path, ["n", "5", "q"], repl=lambda: called.append(1))
316
+ assert called == [1] and "Python prompt" in script.text
317
+
318
+
319
+ def test_guide_ctrl_c_leaves_cleanly(tmp_path):
320
+ s, script, rc = _guide_run(tmp_path, []) # input ends immediately (EOF)
321
+ assert rc == 0 and "Goodbye" in script.text
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes