wishful 0.2.1__py3-none-any.whl

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.
wishful/__init__.py ADDED
@@ -0,0 +1,99 @@
1
+ """wishful - Just-in-Time code generation via import hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ from typing import List
8
+
9
+ from wishful.cache import manager as cache
10
+ from wishful.config import configure, reset_defaults, settings
11
+ from wishful.core.discovery import set_context_radius as _set_context_radius
12
+ from wishful.core.finder import install as install_finder
13
+ from wishful.llm.client import GenerationError
14
+ from wishful.safety.validator import SecurityError
15
+ from wishful.types import type as type_decorator
16
+
17
+ # Install on import so `import magic.xyz` is intercepted immediately.
18
+ install_finder()
19
+
20
+ __all__ = [
21
+ "configure",
22
+ "clear_cache",
23
+ "inspect_cache",
24
+ "regenerate",
25
+ "reimport",
26
+ "set_context_radius",
27
+ "settings",
28
+ "reset_defaults",
29
+ "SecurityError",
30
+ "GenerationError",
31
+ "type",
32
+ ]
33
+
34
+ # Alias for cleaner API
35
+ type = type_decorator
36
+
37
+
38
+ def clear_cache() -> None:
39
+ """Delete all generated files from the cache directory."""
40
+
41
+ cache.clear_cache()
42
+ # Remove generated namespaces so they regenerate on next import.
43
+ for name in list(sys.modules):
44
+ if name.startswith("wishful.static") or name.startswith("wishful.dynamic"):
45
+ sys.modules.pop(name, None)
46
+ # Keep root wishful module to retain settings/logging; re-importer can handle children.
47
+
48
+
49
+ def inspect_cache() -> List[str]:
50
+ """Return a list of cached module file paths as strings."""
51
+
52
+ return [str(p) for p in cache.inspect_cache()]
53
+
54
+
55
+ def regenerate(module_name: str) -> None:
56
+ """Force regeneration of a module on next import.
57
+
58
+ Accepts module names with or without the wishful.static prefix.
59
+ Example: regenerate('users') or regenerate('wishful.static.users')
60
+ """
61
+ # Ensure it has the wishful prefix
62
+ if not module_name.startswith("wishful"):
63
+ # Default to static namespace for backward compatibility
64
+ module_name = f"wishful.static.{module_name}"
65
+
66
+ cache.delete_cached(module_name)
67
+ sys.modules.pop(module_name, None)
68
+ importlib.invalidate_caches()
69
+
70
+
71
+ def set_context_radius(radius: int) -> None:
72
+ """Adjust how many surrounding lines are sent as context to the LLM."""
73
+ _set_context_radius(radius)
74
+
75
+
76
+ def reimport(module_path: str):
77
+ """Force a fresh import by clearing the module from cache.
78
+
79
+ This is especially useful for wishful.dynamic.* imports in loops,
80
+ where you want the LLM to regenerate with fresh context on each iteration.
81
+
82
+ Args:
83
+ module_path: The full module path (e.g., 'wishful.dynamic.story')
84
+
85
+ Returns:
86
+ The freshly imported module
87
+
88
+ Example:
89
+ >>> story = wishful.reimport('wishful.dynamic.story')
90
+ >>> next_line = story.cosmic_horror_next_sentence(current_text)
91
+ """
92
+ # Clear from Python's module cache
93
+ sys.modules.pop(module_path, None)
94
+
95
+ # Import fresh (this triggers wishful's import hook if it's a wishful.* module)
96
+ return importlib.import_module(module_path)
97
+
98
+
99
+ __version__ = "0.1.0"
wishful/__main__.py ADDED
@@ -0,0 +1,72 @@
1
+ """Command-line interface for wishful."""
2
+
3
+ import sys
4
+
5
+ import wishful
6
+
7
+
8
+ def _print_usage() -> None:
9
+ print("wishful - Just-in-time Python module generation")
10
+ print("\nUsage:")
11
+ print(" python -m wishful inspect Show cached modules")
12
+ print(" python -m wishful clear Clear all cache")
13
+ print(" python -m wishful regen <module> Regenerate a module")
14
+ print("\nExamples:")
15
+ print(" python -m wishful inspect")
16
+ print(" python -m wishful regen wishful.text")
17
+ print(" python -m wishful clear")
18
+
19
+
20
+ def _cmd_inspect() -> None:
21
+ cached = wishful.inspect_cache()
22
+ if not cached:
23
+ print("No cached modules found in", wishful.settings.cache_dir)
24
+ return
25
+ print(f"Cached modules in {wishful.settings.cache_dir}:")
26
+ for path in cached:
27
+ print(f" {path}")
28
+
29
+
30
+ def _cmd_clear() -> None:
31
+ wishful.clear_cache()
32
+ print(f"Cleared all cached modules from {wishful.settings.cache_dir}")
33
+
34
+
35
+ def _cmd_regen(args: list[str]) -> None:
36
+ if not args:
37
+ raise ValueError("'regen' requires a module name")
38
+ module_name = args[0]
39
+ wishful.regenerate(module_name)
40
+ print(f"Regenerated {module_name} (will be re-created on next import)")
41
+
42
+
43
+ def main() -> None:
44
+ """Main CLI entry point."""
45
+ args = sys.argv[1:]
46
+ if not args:
47
+ _print_usage()
48
+ sys.exit(0)
49
+
50
+ command, *rest = args
51
+ handlers = {
52
+ "inspect": _cmd_inspect,
53
+ "clear": _cmd_clear,
54
+ "regen": lambda: _cmd_regen(rest),
55
+ }
56
+
57
+ handler = handlers.get(command)
58
+ if handler is None:
59
+ print(f"Unknown command: {command}")
60
+ print("Use 'python -m wishful' for help")
61
+ sys.exit(1)
62
+
63
+ try:
64
+ handler()
65
+ except ValueError as exc:
66
+ print(f"Error: {exc}")
67
+ print("Usage: python -m wishful regen <module>")
68
+ sys.exit(1)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,23 @@
1
+ """Cache utilities for wishful."""
2
+
3
+ from .manager import (
4
+ clear_cache,
5
+ delete_cached,
6
+ ensure_cache_dir,
7
+ has_cached,
8
+ inspect_cache,
9
+ module_path,
10
+ read_cached,
11
+ write_cached,
12
+ )
13
+
14
+ __all__ = [
15
+ "read_cached",
16
+ "write_cached",
17
+ "clear_cache",
18
+ "inspect_cache",
19
+ "module_path",
20
+ "ensure_cache_dir",
21
+ "delete_cached",
22
+ "has_cached",
23
+ ]
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from wishful.config import settings
8
+
9
+
10
+ def module_path(fullname: str) -> Path:
11
+ # Strip leading namespace "wishful" (and static/dynamic) and map dots to directories.
12
+ parts = fullname.split(".")
13
+ if parts[0] == "wishful":
14
+ parts = parts[1:]
15
+ # Also strip 'static' or 'dynamic' if present
16
+ if parts and parts[0] in ("static", "dynamic"):
17
+ parts = parts[1:]
18
+ relative = Path(*parts) if parts else Path("__init__")
19
+ return settings.cache_dir / relative.with_suffix(".py")
20
+
21
+
22
+ def dynamic_snapshot_path(fullname: str) -> Path:
23
+ """Path for storing dynamic-generation snapshots without affecting cache."""
24
+ parts = fullname.split(".")
25
+ if parts[0] == "wishful":
26
+ parts = parts[1:]
27
+ if parts and parts[0] in ("static", "dynamic"):
28
+ parts = parts[1:]
29
+ relative = Path(*parts) if parts else Path("__init__")
30
+ return settings.cache_dir / "_dynamic" / relative.with_suffix(".py")
31
+
32
+
33
+ def ensure_cache_dir() -> Path:
34
+ settings.cache_dir.mkdir(parents=True, exist_ok=True)
35
+ return settings.cache_dir
36
+
37
+
38
+ def read_cached(fullname: str) -> Optional[str]:
39
+ path = module_path(fullname)
40
+ if path.exists():
41
+ return path.read_text()
42
+ return None
43
+
44
+
45
+ def write_cached(fullname: str, source: str) -> Path:
46
+ path = module_path(fullname)
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ path.write_text(source)
49
+ return path
50
+
51
+
52
+ def write_dynamic_snapshot(fullname: str, source: str) -> Path:
53
+ path = dynamic_snapshot_path(fullname)
54
+ path.parent.mkdir(parents=True, exist_ok=True)
55
+ path.write_text(source)
56
+ return path
57
+
58
+
59
+ def delete_cached(fullname: str) -> None:
60
+ path = module_path(fullname)
61
+ if path.exists():
62
+ path.unlink()
63
+
64
+
65
+ def clear_cache() -> None:
66
+ if settings.cache_dir.exists():
67
+ shutil.rmtree(settings.cache_dir)
68
+
69
+
70
+ def inspect_cache() -> List[Path]:
71
+ if not settings.cache_dir.exists():
72
+ return []
73
+ return sorted(settings.cache_dir.rglob("*.py"))
74
+
75
+
76
+ def has_cached(fullname: str) -> bool:
77
+ return module_path(fullname).exists()
wishful/config.py ADDED
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import builtins
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from textwrap import dedent
8
+ from typing import Optional
9
+
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables from a local .env if present so users don't need to
13
+ # export them manually when running examples.
14
+ load_dotenv()
15
+
16
+
17
+ _DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", os.getenv("WISHFUL_MODEL", "azure/gpt-4.1"))
18
+ _DEFAULT_SYSTEM_PROMPT = os.getenv(
19
+ "WISHFUL_SYSTEM_PROMPT",
20
+ dedent(
21
+ """
22
+ You are a Python code generator. Output ONLY executable Python code.
23
+ - Do not wrap code in markdown fences.
24
+ - You may use any Python libraries available in the environment.
25
+ - Prefer simple, readable implementations.
26
+ - Avoid network calls, filesystem writes, subprocess, or shell execution.
27
+ - Include docstrings and type hints where helpful.
28
+ """
29
+ ).strip(),
30
+ )
31
+ _DEFAULT_LOG_LEVEL = os.getenv("WISHFUL_LOG_LEVEL", "WARNING").upper()
32
+ _DEFAULT_LOG_TO_FILE = os.getenv("WISHFUL_LOG_TO_FILE", "1") != "0"
33
+
34
+
35
+ @dataclass
36
+ class Settings:
37
+ """Runtime configuration for wishful.
38
+
39
+ Values are mutable at runtime via :func:`configure` to make tests and user
40
+ code ergonomics-friendly. Defaults are sourced from environment variables.
41
+ """
42
+
43
+ model: str = _DEFAULT_MODEL
44
+ cache_dir: Path = field(default_factory=lambda: Path(os.getenv("WISHFUL_CACHE_DIR", ".wishful")))
45
+ review: bool = os.getenv("WISHFUL_REVIEW", "0") == "1"
46
+ debug: bool = os.getenv("WISHFUL_DEBUG", "0") == "1"
47
+ allow_unsafe: bool = os.getenv("WISHFUL_UNSAFE", "0") == "1"
48
+ spinner: bool = os.getenv("WISHFUL_SPINNER", "1") != "0"
49
+ max_tokens: int = int(os.getenv("WISHFUL_MAX_TOKENS", "4096"))
50
+ temperature: float = float(os.getenv("WISHFUL_TEMPERATURE", "1"))
51
+ system_prompt: str = _DEFAULT_SYSTEM_PROMPT
52
+ log_level: str = _DEFAULT_LOG_LEVEL
53
+ log_to_file: bool = _DEFAULT_LOG_TO_FILE
54
+
55
+ def copy(self) -> "Settings":
56
+ return Settings(
57
+ model=self.model,
58
+ cache_dir=self.cache_dir,
59
+ review=self.review,
60
+ debug=self.debug,
61
+ allow_unsafe=self.allow_unsafe,
62
+ spinner=self.spinner,
63
+ max_tokens=self.max_tokens,
64
+ temperature=self.temperature,
65
+ system_prompt=self.system_prompt,
66
+ log_level=self.log_level,
67
+ log_to_file=self.log_to_file,
68
+ )
69
+
70
+
71
+ # Persist the settings object across module reloads (tests deliberately purge
72
+ # wishful.* modules). Stash it on `builtins` so all imports share the same
73
+ # instance even after sys.modules churn.
74
+ if getattr(builtins, "_wishful_settings", None) is None:
75
+ builtins._wishful_settings = Settings()
76
+ settings = builtins._wishful_settings # type: ignore[attr-defined]
77
+
78
+
79
+ # Internal helper to load logging module robustly (handles altered sys.modules)
80
+ def _load_logging_module():
81
+ try:
82
+ from wishful import logging as logging_mod # type: ignore
83
+ return logging_mod
84
+ except Exception:
85
+ pass
86
+ try:
87
+ import importlib.util
88
+ import sys
89
+ path = Path(__file__).parent / "logging.py"
90
+ spec = importlib.util.spec_from_file_location("wishful.logging", path)
91
+ if spec and spec.loader:
92
+ logging_mod = importlib.util.module_from_spec(spec)
93
+ sys.modules["wishful.logging"] = logging_mod
94
+ spec.loader.exec_module(logging_mod) # type: ignore[arg-type]
95
+ return logging_mod
96
+ except Exception:
97
+ return None
98
+ return None
99
+
100
+
101
+ def configure(
102
+ *,
103
+ model: Optional[str] = None,
104
+ cache_dir: Optional[str | Path] = None,
105
+ review: Optional[bool] = None,
106
+ debug: Optional[bool] = None,
107
+ allow_unsafe: Optional[bool] = None,
108
+ spinner: Optional[bool] = None,
109
+ temperature: Optional[float] = None,
110
+ max_tokens: Optional[int] = None,
111
+ system_prompt: Optional[str] = None,
112
+ log_level: Optional[str] = None,
113
+ log_to_file: Optional[bool] = None,
114
+ ) -> None:
115
+ """Update global settings in-place.
116
+
117
+ All parameters are optional; only provided values overwrite current
118
+ settings. Accepts both strings and :class:`pathlib.Path` for `cache_dir`.
119
+ """
120
+
121
+ updates = {
122
+ "model": model,
123
+ "cache_dir": Path(cache_dir) if cache_dir is not None else None,
124
+ "review": review,
125
+ "debug": debug,
126
+ "allow_unsafe": allow_unsafe,
127
+ "spinner": spinner,
128
+ "temperature": temperature,
129
+ "max_tokens": max_tokens,
130
+ "system_prompt": system_prompt,
131
+ "log_level": log_level.upper() if isinstance(log_level, str) else log_level,
132
+ "log_to_file": log_to_file,
133
+ }
134
+
135
+ # If debug explicitly enabled, default to DEBUG level and file logging unless
136
+ # caller provided overrides.
137
+ if debug is True:
138
+ if updates["log_level"] is None:
139
+ updates["log_level"] = "DEBUG"
140
+ if updates["log_to_file"] is None:
141
+ updates["log_to_file"] = True
142
+ # Spinners and heavy debug output don't mix nicely
143
+ if updates["spinner"] is None:
144
+ updates["spinner"] = False
145
+
146
+ for attr, value in updates.items():
147
+ if value is not None:
148
+ setattr(settings, attr, value)
149
+
150
+ # Reconfigure logging after updates (lazy import to avoid cycles during init)
151
+ logging_mod = _load_logging_module()
152
+ if logging_mod:
153
+ logging_mod.configure_logging(force=True)
154
+
155
+
156
+ def reset_defaults() -> None:
157
+ """Reset settings to environment-driven defaults (useful for tests)."""
158
+ # Create new defaults and copy to existing settings object
159
+ # This ensures all existing references to settings get updated
160
+ defaults = Settings()
161
+ settings.model = defaults.model
162
+ settings.cache_dir = defaults.cache_dir
163
+ settings.review = defaults.review
164
+ settings.debug = defaults.debug
165
+ settings.allow_unsafe = defaults.allow_unsafe
166
+ settings.spinner = defaults.spinner
167
+ settings.max_tokens = defaults.max_tokens
168
+ settings.temperature = defaults.temperature
169
+ settings.system_prompt = defaults.system_prompt
170
+ settings.log_level = defaults.log_level
171
+ settings.log_to_file = defaults.log_to_file
172
+
173
+ logging_mod = _load_logging_module()
174
+ if logging_mod:
175
+ logging_mod.configure_logging(force=True)
@@ -0,0 +1,6 @@
1
+ """Core import machinery for wishful."""
2
+
3
+ from .finder import MagicFinder, install
4
+ from .loader import MagicLoader
5
+
6
+ __all__ = ["MagicFinder", "MagicLoader", "install"]