hypster 0.0.1a0__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.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.1
2
+ Name: hypster
3
+ Version: 0.0.1a0
4
+ Summary: A flexible configuration system for Python projects
5
+ Home-page: https://github.com/hypster-dev/hypster
6
+ License: MIT
7
+ Keywords: configuration,ai,machine-learning
8
+ Author: Gilad Rubin
9
+ Author-email: gilad.rubin@gmail.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Project-URL: Repository, https://github.com/hypster-dev/hypster
17
+ Description-Content-Type: text/markdown
18
+
19
+ <p align="center">
20
+ <img src="assets/hypster_with_text.png" alt="Hypster Logo" width="600"/>
21
+ </p>
22
+
23
+ Hypster is a lightweight configuration system for AI & Machine Learning projects.
24
+ It offers minimal, intuitive syntax, supporting hierarchical and swappable configurations with lazy instantiation - making it both powerful and easy to integrate with existing projects.
25
+
26
+ ## Installation
27
+
28
+ You can install Hypster using pip:
29
+
30
+ ```bash
31
+ pip install hypster
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ Here's a simple example of how to use Hypster:
37
+
38
+ ```python
39
+ %%writefile configs.py
40
+ from hypster import lazy, Options
41
+
42
+ @dataclass
43
+ class DatabaseConfig:
44
+ host: str
45
+ port: int
46
+
47
+ @dataclass
48
+ class CacheConfig:
49
+ type: str
50
+
51
+ # "lazy" defers instantiation
52
+ lazy([Database, Cache], update_globals=True)
53
+
54
+ # Define configuration options
55
+ db_host = Options({"production": "prod.example.com",
56
+ "staging": "staging.example.com"}, default="staging")
57
+ cache_type = Options(["memory", "redis"], default="memory")
58
+ db_port = Options({"main": 5432, "alt": 5433}, default="main")
59
+
60
+ # Create lazy instances
61
+ db = Database(host=db_host, port=db_port)
62
+ cache = Cache(type=cache_type)
63
+ ```
64
+
65
+ Now, in another cell or module, you can instantiate the configuration:
66
+
67
+ ```python
68
+ from hypster import Composer
69
+ import configs
70
+
71
+ config = Composer().with_modules(configs).compose()
72
+
73
+ result = config.instantiate(
74
+ final_vars=["db", "cache"],
75
+ selections={"db.host": "production", "cache.type": "redis"},
76
+ overrides={"db.port": 8000}
77
+ )
78
+
79
+ db.connect() # Outputs: Connecting to prod.example.com:5434
80
+ cache.initialize() # Outputs: Initializing redis cache
81
+ ```
82
+ ## Inspiration
83
+ Hypster draws inspiration from [Meta's Hydra](https://github.com/facebookresearch/hydra) and [hydra-zen](https://github.com/mit-ll-responsible-ai/hydra-zen) packages, combining their powerful configuration management with a minimalist approach.
84
+
85
+ The API design is also influenced by the elegant simplicity of [Hamilton's API](https://github.com/DAGWorks-Inc/hamilton).
86
+
87
+ ## Contributing
88
+
89
+ Contributions are welcome! Please feel free to submit a Pull Request.
90
+
91
+ ## License
92
+
93
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,75 @@
1
+ <p align="center">
2
+ <img src="assets/hypster_with_text.png" alt="Hypster Logo" width="600"/>
3
+ </p>
4
+
5
+ Hypster is a lightweight configuration system for AI & Machine Learning projects.
6
+ It offers minimal, intuitive syntax, supporting hierarchical and swappable configurations with lazy instantiation - making it both powerful and easy to integrate with existing projects.
7
+
8
+ ## Installation
9
+
10
+ You can install Hypster using pip:
11
+
12
+ ```bash
13
+ pip install hypster
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ Here's a simple example of how to use Hypster:
19
+
20
+ ```python
21
+ %%writefile configs.py
22
+ from hypster import lazy, Options
23
+
24
+ @dataclass
25
+ class DatabaseConfig:
26
+ host: str
27
+ port: int
28
+
29
+ @dataclass
30
+ class CacheConfig:
31
+ type: str
32
+
33
+ # "lazy" defers instantiation
34
+ lazy([Database, Cache], update_globals=True)
35
+
36
+ # Define configuration options
37
+ db_host = Options({"production": "prod.example.com",
38
+ "staging": "staging.example.com"}, default="staging")
39
+ cache_type = Options(["memory", "redis"], default="memory")
40
+ db_port = Options({"main": 5432, "alt": 5433}, default="main")
41
+
42
+ # Create lazy instances
43
+ db = Database(host=db_host, port=db_port)
44
+ cache = Cache(type=cache_type)
45
+ ```
46
+
47
+ Now, in another cell or module, you can instantiate the configuration:
48
+
49
+ ```python
50
+ from hypster import Composer
51
+ import configs
52
+
53
+ config = Composer().with_modules(configs).compose()
54
+
55
+ result = config.instantiate(
56
+ final_vars=["db", "cache"],
57
+ selections={"db.host": "production", "cache.type": "redis"},
58
+ overrides={"db.port": 8000}
59
+ )
60
+
61
+ db.connect() # Outputs: Connecting to prod.example.com:5434
62
+ cache.initialize() # Outputs: Initializing redis cache
63
+ ```
64
+ ## Inspiration
65
+ Hypster draws inspiration from [Meta's Hydra](https://github.com/facebookresearch/hydra) and [hydra-zen](https://github.com/mit-ll-responsible-ai/hydra-zen) packages, combining their powerful configuration management with a minimalist approach.
66
+
67
+ The API design is also influenced by the elegant simplicity of [Hamilton's API](https://github.com/DAGWorks-Inc/hamilton).
68
+
69
+ ## Contributing
70
+
71
+ Contributions are welcome! Please feel free to submit a Pull Request.
72
+
73
+ ## License
74
+
75
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,30 @@
1
+ [tool.poetry]
2
+ name = "hypster"
3
+ version = "0.0.1a0"
4
+ description = "A flexible configuration system for Python projects"
5
+
6
+ authors = ["Gilad Rubin <gilad.rubin@gmail.com>"]
7
+ license = "MIT"
8
+ readme = "README.md"
9
+ repository = "https://github.com/hypster-dev/hypster"
10
+ keywords = ["configuration", "ai", "machine-learning"]
11
+
12
+ [tool.poetry.dependencies]
13
+ python = "^3.10"
14
+
15
+ [tool.poetry.dev-dependencies]
16
+ pytest = "^6.0"
17
+ ruff = "^0.1.0"
18
+ mypy = "^0.950"
19
+
20
+ [build-system]
21
+ requires = ["poetry-core>=1.0.0"]
22
+ build-backend = "poetry.core.masonry.api"
23
+
24
+ [tool.ruff]
25
+ line-length = 120
26
+ target-version = "py310"
27
+
28
+ [tool.mypy]
29
+ ignore_missing_imports = true
30
+ strict_optional = true
@@ -0,0 +1,3 @@
1
+ from .core import config, save, load, HP
2
+
3
+ __all__ = ["config", "save", "load", "HP"]
@@ -0,0 +1,278 @@
1
+ import ast
2
+ from typing import Any, Dict, List, Tuple
3
+
4
+ from .logging_utils import configure_logging
5
+
6
+ logger = configure_logging()
7
+
8
+ class Select:
9
+ def __init__(self, lineno: int, select_index: int, explicit_name: str = None, implicit_name: str = None):
10
+ self.lineno = lineno
11
+ self.select_index = select_index
12
+ self.explicit_name = explicit_name
13
+ self.implicit_name = implicit_name
14
+
15
+ class VariableGraphBuilder(ast.NodeVisitor):
16
+ def __init__(self):
17
+ self.graph = {}
18
+ self.current_path = []
19
+
20
+ def visit_Assign(self, node):
21
+ target = self.get_target_name(node.targets[0])
22
+ logger.debug(f"VariableGraphBuilder: Visiting assignment to {target}")
23
+ self.current_path = [target]
24
+ self.graph[target] = self.build_subgraph(node.value)
25
+
26
+ def build_subgraph(self, node):
27
+ logger.debug(f"VariableGraphBuilder: Building subgraph for node type {type(node).__name__}")
28
+ if isinstance(node, ast.Dict):
29
+ return {self.get_key_name(k): self.build_subgraph(v) for k, v in zip(node.keys, node.values)}
30
+ elif isinstance(node, ast.Call):
31
+ return {
32
+ '__call__': self.get_target_name(node.func),
33
+ 'args': [self.get_node_value(arg) for arg in node.args],
34
+ 'kwargs': {kw.arg: self.get_node_value(kw.value) for kw in node.keywords}
35
+ }
36
+ elif isinstance(node, (ast.Name, ast.Attribute)):
37
+ return self.get_target_name(node)
38
+ elif isinstance(node, ast.Constant):
39
+ return node.value
40
+ else:
41
+ return {'__unknown__': ast.unparse(node)}
42
+
43
+ def get_node_value(self, node):
44
+ if isinstance(node, ast.Constant):
45
+ return node.value
46
+ elif isinstance(node, (ast.Name, ast.Attribute)):
47
+ return self.get_target_name(node)
48
+ else:
49
+ return ast.unparse(node)
50
+
51
+ def get_target_name(self, node):
52
+ if isinstance(node, ast.Name):
53
+ return node.id
54
+ elif isinstance(node, ast.Attribute):
55
+ return f"{self.get_target_name(node.value)}.{node.attr}"
56
+ else:
57
+ return "Unknown"
58
+
59
+ def get_key_name(self, node):
60
+ if isinstance(node, ast.Str):
61
+ return node.s
62
+ return self.get_target_name(node)
63
+
64
+ class HPSelectAnalyzer(ast.NodeVisitor):
65
+ def __init__(self, variable_graph):
66
+ self.variable_graph = variable_graph
67
+ self.results = {}
68
+ self.current_assignment = None
69
+ self.current_complex_assignment = None
70
+ self.selects = []
71
+
72
+ def visit_Assign(self, node):
73
+ logger.debug(f"HPSelectAnalyzer: Visiting assignment on line {node.lineno}")
74
+ self.current_assignment = node
75
+ start_line = node.lineno
76
+ end_line = self.get_last_line(node)
77
+
78
+ if isinstance(node.value, (ast.Dict, ast.Call)):
79
+ self.current_complex_assignment = node
80
+
81
+ self.results[start_line] = {'code': ast.unparse(node), 'selects': [], 'end_line': end_line}
82
+ self.generic_visit(node)
83
+
84
+ self.current_complex_assignment = None
85
+ self.current_assignment = None
86
+
87
+ def visit_Call(self, node):
88
+ if self.is_hp_select(node):
89
+ logger.debug(f"HPSelectAnalyzer: hp.select call detected on line {node.lineno}")
90
+ line_number = node.lineno
91
+ select_index = len([s for s in self.selects if s.lineno == line_number])
92
+
93
+ explicit_name = self.get_hp_select_name(node)
94
+ inferred_name = self.infer_name(node)
95
+
96
+ if not explicit_name and inferred_name and self.is_valid_inference(inferred_name):
97
+ implicit_name = inferred_name
98
+ else:
99
+ implicit_name = None
100
+
101
+ select = Select(line_number, select_index, explicit_name, implicit_name)
102
+ self.selects.append(select)
103
+
104
+ if self.current_complex_assignment and self.current_complex_assignment.lineno <= line_number <= self.get_last_line(self.current_complex_assignment):
105
+ logger.debug(f"HPSelectAnalyzer: hp.select is part of a complex assignment starting on line {self.current_complex_assignment.lineno}")
106
+ line_number = self.current_complex_assignment.lineno
107
+
108
+ if line_number in self.results:
109
+ self.results[line_number]['selects'].append(select)
110
+ else:
111
+ self.results[line_number] = {
112
+ 'code': ast.unparse(self.current_complex_assignment or node),
113
+ 'selects': [select],
114
+ 'end_line': self.get_last_line(self.current_complex_assignment or node)
115
+ }
116
+
117
+ self.generic_visit(node)
118
+
119
+ def is_valid_inference(self, inferred_name):
120
+ logger.debug(f"HPSelectAnalyzer: Checking validity of inferred name: {inferred_name}")
121
+ parts = inferred_name.split('.')
122
+ valid = len(parts) <= 4 and all(not part.startswith('arg') for part in parts)
123
+ logger.debug(f"HPSelectAnalyzer: Inferred name {'is' if valid else 'is not'} valid")
124
+ return valid
125
+
126
+ def get_last_line(self, node):
127
+ return max(getattr(node, 'lineno', 0), getattr(node, 'end_lineno', 0))
128
+
129
+ def is_hp_select(self, node):
130
+ return (isinstance(node.func, ast.Attribute) and
131
+ isinstance(node.func.value, ast.Name) and
132
+ node.func.value.id == 'hp' and
133
+ node.func.attr == 'select')
134
+
135
+ def get_hp_select_name(self, node):
136
+ logger.debug(f"HPSelectAnalyzer: Attempting to get hp.select name for node on line {node.lineno}")
137
+ if len(node.args) >= 2:
138
+ name = self.get_node_value(node.args[1])
139
+ logger.debug(f"HPSelectAnalyzer: Found name in second argument: {name}")
140
+ return name
141
+ for keyword in node.keywords:
142
+ if keyword.arg == 'name':
143
+ name = self.get_node_value(keyword.value)
144
+ logger.debug(f"HPSelectAnalyzer: Found name in keyword argument: {name}")
145
+ return name
146
+ logger.debug("HPSelectAnalyzer: No explicit name found for hp.select")
147
+ return None
148
+
149
+ def get_node_value(self, node):
150
+ if isinstance(node, ast.Constant):
151
+ return node.value
152
+ elif isinstance(node, ast.Name):
153
+ return node.id
154
+ else:
155
+ return ast.unparse(node)
156
+
157
+ def infer_name(self, node):
158
+ logger.debug(f"HPSelectAnalyzer: Attempting to infer name for node on line {node.lineno}")
159
+ if self.current_assignment:
160
+ target = self.get_target_name(self.current_assignment.targets[0])
161
+ if isinstance(self.current_assignment.value, ast.Call) and self.is_hp_select(self.current_assignment.value):
162
+ logger.debug(f"HPSelectAnalyzer: Direct assignment to hp.select detected: {target}")
163
+ return target
164
+ inferred = self.find_node_in_assignment(self.current_assignment.value, node, [target])
165
+ logger.debug(f"HPSelectAnalyzer: Inferred name from current assignment: {inferred}")
166
+ return inferred
167
+
168
+ for var, subgraph in self.variable_graph.items():
169
+ path = self.find_node_in_graph(subgraph, node)
170
+ if path:
171
+ inferred = '.'.join([var] + [p for p in path if p != 'kwargs'])
172
+ logger.debug(f"HPSelectAnalyzer: Inferred name from variable graph: {inferred}")
173
+ return inferred
174
+ logger.debug("HPSelectAnalyzer: Unable to infer name")
175
+ return None
176
+
177
+ def find_node_in_assignment(self, value_node, target_node, path):
178
+ logger.debug(f"HPSelectAnalyzer: Searching for node in assignment, current path: {'.'.join(path)}")
179
+ if isinstance(value_node, ast.Dict):
180
+ for key, value in zip(value_node.keys, value_node.values):
181
+ if value == target_node:
182
+ result = '.'.join(path + [self.get_node_value(key)])
183
+ logger.debug(f"HPSelectAnalyzer: Found node in dictionary: {result}")
184
+ return result
185
+ result = self.find_node_in_assignment(value, target_node, path + [self.get_node_value(key)])
186
+ if result:
187
+ return result
188
+ elif isinstance(value_node, ast.Call):
189
+ for idx, arg in enumerate(value_node.args):
190
+ if arg == target_node:
191
+ result = '.'.join(path + [f'arg{idx}'])
192
+ logger.debug(f"HPSelectAnalyzer: Found node in function argument: {result}")
193
+ return result
194
+ result = self.find_node_in_assignment(arg, target_node, path + [f'arg{idx}'])
195
+ if result:
196
+ return result
197
+ for keyword in value_node.keywords:
198
+ if keyword.value == target_node:
199
+ result = '.'.join(path + [keyword.arg])
200
+ logger.debug(f"HPSelectAnalyzer: Found node in keyword argument: {result}")
201
+ return result
202
+ result = self.find_node_in_assignment(keyword.value, target_node, path + [keyword.arg])
203
+ if result:
204
+ return result
205
+ logger.debug("HPSelectAnalyzer: Node not found in assignment")
206
+ return None
207
+
208
+ def find_node_in_graph(self, graph, node, path=None):
209
+ if path is None:
210
+ path = []
211
+
212
+ logger.debug(f"HPSelectAnalyzer: Searching for node in graph, current path: {'.'.join(path)}")
213
+ if isinstance(graph, dict):
214
+ if graph.get('__call__') == 'hp.select' and graph['args'] and self.get_node_value(node.args[0]) == graph['args'][0]:
215
+ logger.debug(f"HPSelectAnalyzer: Found matching hp.select in graph at path: {'.'.join(path)}")
216
+ return path
217
+ for key, value in graph.items():
218
+ new_path = self.find_node_in_graph(value, node, path + [key])
219
+ if new_path:
220
+ return new_path
221
+ elif isinstance(graph, list):
222
+ for i, item in enumerate(graph):
223
+ new_path = self.find_node_in_graph(item, node, path + [str(i)])
224
+ if new_path:
225
+ return new_path
226
+ logger.debug("HPSelectAnalyzer: Node not found in graph")
227
+ return None
228
+
229
+ def get_target_name(self, node):
230
+ if isinstance(node, ast.Name):
231
+ return node.id
232
+ elif isinstance(node, ast.Attribute):
233
+ return f"{self.get_target_name(node.value)}.{node.attr}"
234
+ else:
235
+ return "Unknown"
236
+
237
+ def analyze_hp_select(code: str) -> Tuple[Dict[int, Dict[str, Any]], List[Select]]:
238
+ logger.info("Starting hp.select analysis")
239
+ tree = ast.parse(code)
240
+
241
+ logger.debug("Building variable graph")
242
+ graph_builder = VariableGraphBuilder()
243
+ graph_builder.visit(tree)
244
+
245
+ logger.debug("Analyzing hp.select calls")
246
+ analyzer = HPSelectAnalyzer(graph_builder.graph)
247
+ analyzer.visit(tree)
248
+
249
+ logger.info("hp.select analysis complete")
250
+ return analyzer.results, analyzer.selects
251
+
252
+ def inject_names(source_code: str, selects: List[Select]) -> str:
253
+ tree = ast.parse(source_code)
254
+
255
+ class NameInjector(ast.NodeTransformer):
256
+ def __init__(self, selects):
257
+ self.selects = selects
258
+ self.select_index = {}
259
+
260
+ def visit_Call(self, node):
261
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == 'hp' and node.func.attr == 'select':
262
+ lineno = node.lineno
263
+ if lineno not in self.select_index:
264
+ self.select_index[lineno] = 0
265
+ else:
266
+ self.select_index[lineno] += 1
267
+
268
+ select = next((s for s in self.selects if s.lineno == lineno and s.select_index == self.select_index[lineno]), None)
269
+
270
+ if select and not select.explicit_name and select.implicit_name:
271
+ # Inject the implicit name as a keyword argument
272
+ node.keywords.append(ast.keyword(arg='name', value=ast.Constant(value=select.implicit_name)))
273
+
274
+ return self.generic_visit(node)
275
+
276
+ injector = NameInjector(selects)
277
+ modified_tree = injector.visit(tree)
278
+ return ast.unparse(modified_tree)
@@ -0,0 +1,399 @@
1
+ # core.py
2
+
3
+ import ast
4
+ import inspect
5
+ import types
6
+ from typing import Any, Callable, Dict, List, Union, Optional
7
+
8
+ from .logging_utils import configure_logging
9
+ from .ast_analyzer import inject_names, analyze_hp_select
10
+
11
+ logger = configure_logging()
12
+
13
+ class HP:
14
+ def __init__(self, final_vars: List[str], selections: Dict[str, Any], overrides: Dict[str, Any]):
15
+ self.final_vars = final_vars
16
+ self.selections = selections
17
+ self.overrides = overrides
18
+ self.config_dict = {}
19
+ logger.info("Initialized HP with final_vars: %s, selections: %s, and overrides: %s",
20
+ self.final_vars, self.selections, self.overrides)
21
+
22
+ def select(self, options: Union[Dict[str, Any], List[Any]], name: str = None, default: Any = None):
23
+ if name is None:
24
+ raise ValueError("Name must be provided explicitly or automatically inferred.")
25
+
26
+ if isinstance(options, dict):
27
+ if not all(isinstance(k, str) for k in options.keys()):
28
+ bad_keys = [key for key in options.keys() if not isinstance(key, str)]
29
+ raise ValueError(f"Dictionary keys must be strings. got {bad_keys} instead.")
30
+ elif isinstance(options, list):
31
+ if not all(isinstance(v, (str, int, bool, float)) for v in options):
32
+ raise ValueError("List values must be one of: str, int, bool, float.")
33
+ options = {v: v for v in options}
34
+ else:
35
+ raise ValueError("Options must be a dictionary or a list.")
36
+
37
+ if default is not None and default not in options:
38
+ raise ValueError("Default value must be one of the options.")
39
+
40
+ logger.debug("Select called with options: %s, name: %s, default: %s", options, name, default)
41
+
42
+ result = None
43
+ if name in self.overrides:
44
+ override_value = self.overrides[name]
45
+ logger.debug("Found override for %s: %s", name, override_value)
46
+ if override_value in options:
47
+ result = options[override_value]
48
+ else:
49
+ result = override_value
50
+ logger.info("Applied override for %s: %s", name, result)
51
+ elif name in self.selections:
52
+ selected_value = self.selections[name]
53
+ logger.debug("Found selection for %s: %s", name, selected_value)
54
+ if selected_value in options:
55
+ result = options[selected_value]
56
+ logger.info("Applied selection for %s: %s", name, result)
57
+ else:
58
+ raise InvalidSelectionError(
59
+ f"Invalid selection '{selected_value}' for '{name}'. Not in options: {list(options.keys())}"
60
+ )
61
+ elif default is not None:
62
+ result = options[default]
63
+ else:
64
+ raise ValueError(f"No selection or override found for {name} and no default provided.")
65
+
66
+ self.config_dict[name] = result
67
+ return result
68
+
69
+ def propagate(self, config_func: Callable, name: str) -> Dict[str, Any]:
70
+ logger.info(f"Propagating configuration for {name}")
71
+
72
+ # Create dictionaries for the nested configuration
73
+ nested_selections = {k[len(name)+1:]: v for k, v in self.selections.items() if k.startswith(f"{name}.")}
74
+ nested_overrides = {k[len(name)+1:]: v for k, v in self.overrides.items() if k.startswith(f"{name}.")}
75
+
76
+ # Automatically propagate final_vars
77
+ nested_final_vars = [var[len(name)+1:] for var in self.final_vars if var.startswith(f"{name}.")]
78
+
79
+ logger.debug(f"Propagated configuration for {name} with Selections:\n{nested_selections}\n& Overrides:\n{nested_overrides}\nAuto-propagated final vars: {nested_final_vars}")
80
+ result = config_func(final_vars=nested_final_vars, selections=nested_selections, overrides=nested_overrides)
81
+ return result
82
+
83
+ class Hypster:
84
+ def __init__(self, func: Callable, source_code: str = None):
85
+ self.func = func
86
+ self.source_code = source_code or inspect.getsource(func)
87
+
88
+ def __call__(self, final_vars: List[str] = [], selections: Dict[str, Any] = {}, overrides: Dict[str, Any] = {}):
89
+ logger.info("Hypster called with final_vars: %s, selections: %s, overrides: %s",
90
+ final_vars, selections, overrides)
91
+ try:
92
+ hp = HP(final_vars, selections, overrides)
93
+
94
+ # Analyze and modify the source code
95
+ results, selects = analyze_hp_select(self.source_code)
96
+ modified_source = inject_names(self.source_code, selects)
97
+
98
+ # Extract the function body
99
+ function_body = self._extract_function_body(modified_source)
100
+
101
+ # Create a new namespace and add the 'hp' object to it
102
+ namespace = {'hp': hp}
103
+
104
+ # Execute the modified function body in this namespace
105
+ exec(function_body, globals(), namespace)
106
+
107
+ # Process and filter the results
108
+ final_result = self._process_results(namespace)
109
+
110
+ if not final_vars:
111
+ return final_result
112
+ else:
113
+ result = {k: final_result.get(k, None) for k in final_vars}
114
+ logger.debug("Final result after filtering by final_vars: %s", result)
115
+ return result
116
+
117
+ except Exception as e:
118
+ logger.error("An error occurred: %s", str(e))
119
+ raise
120
+
121
+ def save(self, path: str):
122
+ save(self, path)
123
+
124
+ def _extract_function_body(self, source: str) -> str:
125
+ lines = source.split('\n')
126
+ body_start = next(i for i, line in enumerate(lines) if line.strip().endswith(':'))
127
+ body_lines = lines[body_start + 1:]
128
+ min_indent = min(len(line) - len(line.lstrip()) for line in body_lines if line.strip())
129
+ return '\n'.join(line[min_indent:] for line in body_lines)
130
+
131
+ def _process_results(self, namespace: Dict[str, Any]) -> Dict[str, Any]:
132
+ filtered_locals = {
133
+ k: v for k, v in namespace.items()
134
+ if k != 'hp' and not k.startswith('__') and not isinstance(v, (types.ModuleType, types.FunctionType, type))
135
+ }
136
+
137
+ final_result = {k: v for k, v in filtered_locals.items() if not k.startswith('_')}
138
+
139
+ logger.debug("Captured locals: %s", filtered_locals)
140
+ logger.debug("Final result after filtering: %s", final_result)
141
+
142
+ return final_result
143
+
144
+ def config(func: Callable) -> Hypster:
145
+ return Hypster(func)
146
+
147
+ def save(hypster_instance: Hypster, path: Optional[str] = None):
148
+ if not isinstance(hypster_instance, Hypster):
149
+ raise ValueError("The provided object is not a Hypster instance")
150
+
151
+ if path is None:
152
+ path = f"{hypster_instance.func.__name__}.py"
153
+
154
+ # Parse the source code into an AST
155
+ tree = ast.parse(hypster_instance.source_code)
156
+
157
+ # Find the function definition and remove decorators
158
+ for node in ast.walk(tree):
159
+ if isinstance(node, ast.FunctionDef):
160
+ node.decorator_list = []
161
+ break
162
+
163
+ # Convert the modified AST back to source code
164
+ modified_source = ast.unparse(tree)
165
+
166
+ with open(path, "w") as f:
167
+ f.write(modified_source)
168
+
169
+ logger.info("Configuration saved to %s", path)
170
+
171
+ def load(path: str) -> Hypster:
172
+ with open(path, "r") as f:
173
+ source = f.read()
174
+
175
+ # Execute the source code to define the function
176
+ namespace = {}
177
+ exec(source, namespace)
178
+
179
+ # Find the function in the namespace
180
+ for name, obj in namespace.items():
181
+ if callable(obj) and not name.startswith("__"):
182
+ # Create and return a Hypster instance with the source code
183
+ return Hypster(obj, source_code=source)
184
+
185
+ raise ValueError("No suitable function found in the source code")
186
+
187
+ class InvalidSelectionError(Exception):
188
+ pass
189
+
190
+ # # core.py
191
+
192
+ # import ast
193
+ # import inspect
194
+ # from typing import Any, Callable, Dict, List, Union, Optional
195
+
196
+ # from .logging_utils import configure_logging
197
+
198
+ # logger = configure_logging()
199
+
200
+
201
+ # class HP:
202
+ # def __init__(self, selections: Dict[str, Any], overrides: Dict[str, Any]):
203
+ # self.selections = selections
204
+ # self.overrides = overrides
205
+ # self.config_dict = {}
206
+ # logger.info("Initialized HP with selections: %s and overrides: %s", self.selections, self.overrides)
207
+
208
+ # def select(self, options: Union[Dict[str, Any], List[Any]], name: str = None, default: Any = None):
209
+ # if name is None:
210
+ # raise ValueError("Name must be provided explicitly or automatically inferred.")
211
+
212
+ # if isinstance(options, dict):
213
+ # if not all(isinstance(k, str) for k in options.keys()):
214
+ # bad_keys = [key for key in options.keys() if not isinstance(key, str)]
215
+ # raise ValueError(f"Dictionary keys must be strings. got {bad_keys} instead.")
216
+ # elif isinstance(options, list):
217
+ # if not all(isinstance(v, (str, int, bool, float)) for v in options):
218
+ # raise ValueError("List values must be one of: str, int, bool, float.")
219
+ # options = {v: v for v in options}
220
+ # else:
221
+ # raise ValueError("Options must be a dictionary or a list.")
222
+
223
+ # if default is not None and default not in options:
224
+ # raise ValueError("Default value must be one of the options.")
225
+
226
+ # logger.debug("Select called with options: %s, name: %s, default: %s", options, name, default)
227
+
228
+ # result = None
229
+ # if name in self.overrides:
230
+ # override_value = self.overrides[name]
231
+ # logger.debug("Found override for %s: %s", name, override_value)
232
+ # if override_value in options:
233
+ # result = options[override_value]
234
+ # else:
235
+ # result = override_value
236
+ # logger.info("Applied override for %s: %s", name, result)
237
+ # elif name in self.selections:
238
+ # selected_value = self.selections[name]
239
+ # logger.debug("Found selection for %s: %s", name, selected_value)
240
+ # if selected_value in options:
241
+ # result = options[selected_value]
242
+ # logger.info("Applied selection for %s: %s", name, result)
243
+ # else:
244
+ # raise InvalidSelectionError(
245
+ # f"Invalid selection '{selected_value}' for '{name}'. Not in options: {list(options.keys())}"
246
+ # )
247
+ # elif default is not None:
248
+ # result = options[default]
249
+ # else:
250
+ # raise ValueError(f"No selection or override found for {name} and no default provided.")
251
+
252
+ # self.config_dict[name] = result
253
+ # return result
254
+
255
+ # def propagate(self, config_func: Callable, name: str) -> Dict[str, Any]:
256
+ # logger.info(f"Propagating configuration for {name}")
257
+
258
+ # # Create a new HP instance for the nested configuration
259
+ # nested_selections = {k[len(name)+1:]: v for k, v in self.selections.items() if k.startswith(f"{name}.")}
260
+ # nested_overrides = {k[len(name)+1:]: v for k, v in self.overrides.items() if k.startswith(f"{name}.")}
261
+ # #nested_hp = HP(nested_selections, nested_overrides)
262
+
263
+ # # Execute the nested configuration function
264
+ # logger.debug(f"Propagated configuration for {name} with Selections:\n{nested_selections}\n& Overrides:\n{nested_overrides}")
265
+ # return config_func(selections=nested_selections, overrides=nested_overrides)
266
+
267
+ # import ast
268
+ # import inspect
269
+ # import types
270
+ # from typing import Any, Callable, Dict, List, Union
271
+ # from .logging_utils import configure_logging
272
+ # from .ast_analyzer import inject_names, analyze_hp_select
273
+
274
+ # logger = configure_logging()
275
+
276
+ # class Hypster:
277
+ # def __init__(self, func: Callable, source_code: str = None):
278
+ # self.func = func
279
+ # self.source_code = source_code or inspect.getsource(func)
280
+
281
+ # def __call__(self, final_vars: List[str] = [], selections: Dict[str, Any] = {}, overrides: Dict[str, Any] = {}):
282
+ # logger.info("Hypster called with final_vars: %s, selections: %s, overrides: %s",
283
+ # final_vars, selections, overrides)
284
+ # try:
285
+ # hp = HP(selections, overrides)
286
+
287
+ # # Analyze and modify the source code
288
+ # results, selects = analyze_hp_select(self.source_code)
289
+ # modified_source = inject_names(self.source_code, selects)
290
+
291
+ # # Extract the function body
292
+ # function_body = self._extract_function_body(modified_source)
293
+
294
+ # # Create a new namespace and add the 'hp' object to it
295
+ # namespace = {'hp': hp}
296
+
297
+ # # Execute the modified function body in this namespace
298
+ # exec(function_body, globals(), namespace)
299
+
300
+ # # Process and filter the results
301
+ # final_result = self._process_results(namespace)
302
+
303
+ # if not final_vars:
304
+ # return final_result
305
+ # else:
306
+ # result = {k: final_result.get(k, None) for k in final_vars}
307
+ # logger.debug("Final result after filtering by final_vars: %s", result)
308
+ # return result
309
+
310
+ # except Exception as e:
311
+ # logger.error("An error occurred: %s", str(e))
312
+ # raise
313
+
314
+ # def save(self, path: str):
315
+ # save(self, path)
316
+
317
+ # def _extract_function_body(self, source: str) -> str:
318
+ # lines = source.split('\n')
319
+ # body_start = next(i for i, line in enumerate(lines) if line.strip().endswith(':'))
320
+ # body_lines = lines[body_start + 1:]
321
+ # min_indent = min(len(line) - len(line.lstrip()) for line in body_lines if line.strip())
322
+ # return '\n'.join(line[min_indent:] for line in body_lines)
323
+
324
+ # def _process_results(self, namespace: Dict[str, Any]) -> Dict[str, Any]:
325
+ # filtered_locals = {
326
+ # k: v for k, v in namespace.items()
327
+ # if k != 'hp' and not k.startswith('__') and not isinstance(v, (types.ModuleType, types.FunctionType, type))
328
+ # }
329
+
330
+ # final_result = {k: v for k, v in filtered_locals.items() if not k.startswith('_')}
331
+
332
+ # logger.debug("Captured locals: %s", filtered_locals)
333
+ # logger.debug("Final result after filtering: %s", final_result)
334
+
335
+ # return final_result
336
+
337
+ # def config(func: Callable) -> Hypster:
338
+ # return Hypster(func)
339
+
340
+ # def save(hypster_instance: Hypster, path: Optional[str] = None):
341
+ # if not isinstance(hypster_instance, Hypster):
342
+ # raise ValueError("The provided object is not a Hypster instance")
343
+
344
+ # if path is None:
345
+ # path = f"{hypster_instance.func.__name__}.py"
346
+
347
+ # # Parse the source code into an AST
348
+ # tree = ast.parse(hypster_instance.source_code)
349
+
350
+ # # Find the function definition and remove decorators
351
+ # for node in ast.walk(tree):
352
+ # if isinstance(node, ast.FunctionDef):
353
+ # node.decorator_list = []
354
+ # break
355
+
356
+ # # Convert the modified AST back to source code
357
+ # modified_source = ast.unparse(tree)
358
+
359
+ # with open(path, "w") as f:
360
+ # f.write(modified_source)
361
+
362
+ # logger.info("Configuration saved to %s", path)
363
+
364
+ # def load(path: str) -> Hypster:
365
+ # with open(path, "r") as f:
366
+ # source = f.read()
367
+
368
+ # # Execute the source code to define the function
369
+ # namespace = {}
370
+ # exec(source, namespace)
371
+
372
+ # # Find the function in the namespace
373
+ # for name, obj in namespace.items():
374
+ # if callable(obj) and not name.startswith("__"):
375
+ # # Create and return a Hypster instance with the source code
376
+ # return Hypster(obj, source_code=source)
377
+
378
+ # raise ValueError("No suitable function found in the source code")
379
+
380
+ # class InvalidSelectionError(Exception):
381
+ # pass
382
+
383
+ # # Example usage (can be commented out in the actual module)
384
+ # """
385
+ # @config
386
+ # def my_config(hp):
387
+ # hp.select(["a", "b", "c"], name="a", default="a")
388
+ # hp.select({"x": 1, "y": 2}, name="b", default="x")
389
+
390
+ # # Save the configuration
391
+ # save(my_config, "my_config.py")
392
+
393
+ # # Load the configuration
394
+ # loaded_config = load("my_config.py")
395
+
396
+ # # Use the loaded configuration
397
+ # result = loaded_config(final_vars=["a"], selections={"b": "y"}, overrides={"a": "c"})
398
+ # print(result)
399
+ # """
@@ -0,0 +1,55 @@
1
+ import logging
2
+
3
+ class CustomFormatter(logging.Formatter):
4
+ BLACK = "\033[0;30m"
5
+ RED = "\033[0;31m"
6
+ GREEN = "\033[0;32m"
7
+ BROWN = "\033[0;33m"
8
+ BLUE = "\033[0;34m"
9
+ PURPLE = "\033[0;35m"
10
+ CYAN = "\033[0;36m"
11
+ LIGHT_GRAY = "\033[0;37m"
12
+ DARK_GRAY = "\033[1;30m"
13
+ LIGHT_RED = "\033[1;31m"
14
+ LIGHT_GREEN = "\033[1;32m"
15
+ YELLOW = "\033[1;33m"
16
+ LIGHT_BLUE = "\033[1;34m"
17
+ LIGHT_PURPLE = "\033[1;35m"
18
+ LIGHT_CYAN = "\033[1;36m"
19
+ LIGHT_WHITE = "\033[1;37m"
20
+ BOLD = "\033[1m"
21
+ FAINT = "\033[2m"
22
+ ITALIC = "\033[3m"
23
+ UNDERLINE = "\033[4m"
24
+ BLINK = "\033[5m"
25
+ NEGATIVE = "\033[7m"
26
+ CROSSED = "\033[9m"
27
+ END = "\033[0m"
28
+ reset = "\x1b[0m"
29
+ format = "%(message)s"
30
+
31
+ FORMATS = {
32
+ logging.DEBUG: CYAN + "%(levelname)s" + reset + " - " + format,
33
+ logging.INFO: GREEN + "%(levelname)s" + reset + " - " + format,
34
+ logging.WARNING: YELLOW + "%(levelname)s" + reset + " - " + format,
35
+ logging.ERROR: RED + "%(levelname)s" + reset + " - " + format,
36
+ }
37
+
38
+ # Logging Configuration Function
39
+ def configure_logging():
40
+ # Get the root logger
41
+ logger = logging.getLogger()
42
+
43
+ # Remove all existing handlers
44
+ if logger.hasHandlers():
45
+ logger.handlers.clear()
46
+
47
+ # Initialize the handler with the custom formatter
48
+ handler = logging.StreamHandler()
49
+ handler.setFormatter(CustomFormatter())
50
+
51
+ # Set the handler for the logger
52
+ logger.addHandler(handler)
53
+ logger.setLevel(logging.WARNING)
54
+
55
+ return logger