vex-ast 0.2.2__tar.gz → 0.2.4__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 (83) hide show
  1. {vex_ast-0.2.2/vex_ast.egg-info → vex_ast-0.2.4}/PKG-INFO +1 -1
  2. {vex_ast-0.2.2 → vex_ast-0.2.4}/pyproject.toml +1 -1
  3. {vex_ast-0.2.2 → vex_ast-0.2.4}/setup.py +1 -1
  4. vex_ast-0.2.4/tests/test_comprehensive_integration.py +338 -0
  5. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_parser.py +5 -4
  6. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/expressions.py +44 -1
  7. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/interfaces.py +16 -0
  8. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/vex_nodes.py +15 -2
  9. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/factory.py +13 -2
  10. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/python_parser.py +88 -27
  11. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/signature.py +49 -1
  12. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/json_deserializer.py +13 -2
  13. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/schema.py +3 -4
  14. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/errors.py +2 -2
  15. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/base.py +4 -1
  16. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/printer.py +2 -1
  17. {vex_ast-0.2.2 → vex_ast-0.2.4/vex_ast.egg-info}/PKG-INFO +1 -1
  18. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast.egg-info/SOURCES.txt +1 -0
  19. {vex_ast-0.2.2 → vex_ast-0.2.4}/LICENSE +0 -0
  20. {vex_ast-0.2.2 → vex_ast-0.2.4}/MANIFEST.in +0 -0
  21. {vex_ast-0.2.2 → vex_ast-0.2.4}/README.md +0 -0
  22. {vex_ast-0.2.2 → vex_ast-0.2.4}/pytest.ini +0 -0
  23. {vex_ast-0.2.2 → vex_ast-0.2.4}/requirements.txt +0 -0
  24. {vex_ast-0.2.2 → vex_ast-0.2.4}/setup.cfg +0 -0
  25. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/conftest.py +0 -0
  26. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_core.py +0 -0
  27. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_integration.py +0 -0
  28. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_literals.py +0 -0
  29. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_navigator.py +0 -0
  30. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_registry.py +0 -0
  31. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_serialization.py +0 -0
  32. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_statements.py +0 -0
  33. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_vex_nodes.py +0 -0
  34. {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_visitors.py +0 -0
  35. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/README.md +0 -0
  36. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/READMEAPI.md +0 -0
  37. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/__init__.py +0 -0
  38. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/README.md +0 -0
  39. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/__init__.py +0 -0
  40. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/core.py +0 -0
  41. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/literals.py +0 -0
  42. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/navigator.py +0 -0
  43. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/operators.py +0 -0
  44. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/statements.py +0 -0
  45. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/validators.py +0 -0
  46. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/README.md +0 -0
  47. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/__init__.py +0 -0
  48. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/interfaces.py +0 -0
  49. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/strategies.py +0 -0
  50. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/README.md +0 -0
  51. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/__init__.py +0 -0
  52. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/api.py +0 -0
  53. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/categories.py +0 -0
  54. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/__init__.py +0 -0
  55. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/display.py +0 -0
  56. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/drivetrain.py +0 -0
  57. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/initialize.py +0 -0
  58. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/motor.py +0 -0
  59. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/sensors.py +0 -0
  60. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/timing.py +0 -0
  61. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/language_map.py +0 -0
  62. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/registry.py +0 -0
  63. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/simulation_behavior.py +0 -0
  64. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/validation.py +0 -0
  65. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/__init__.py +0 -0
  66. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/json_serializer.py +0 -0
  67. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/README.md +0 -0
  68. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/__init__.py +0 -0
  69. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/base.py +0 -0
  70. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/enums.py +0 -0
  71. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/objects.py +0 -0
  72. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/primitives.py +0 -0
  73. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/type_checker.py +0 -0
  74. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/README.md +0 -0
  75. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/__init__.py +0 -0
  76. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/source_location.py +0 -0
  77. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/type_definitions.py +0 -0
  78. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/README.md +0 -0
  79. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/__init__.py +0 -0
  80. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/analyzer.py +0 -0
  81. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/transformer.py +0 -0
  82. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast.egg-info/dependency_links.txt +0 -0
  83. {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vex_ast
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A Python package for generating Abstract Syntax Trees for VEX V5 code.
5
5
  Home-page: https://github.com/heartx2/vex_ast
6
6
  Author: Chaze
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vex_ast"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "A Python package for generating Abstract Syntax Trees for VEX V5 code."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='vex_ast',
5
- version='0.2.2',
5
+ version='0.2.4',
6
6
  description='A Python package for generating Abstract Syntax Trees for VEX V5 code.',
7
7
  long_description=open('README.md').read(),
8
8
  long_description_content_type='text/markdown',
@@ -0,0 +1,338 @@
1
+ """
2
+ Comprehensive integration tests for the vex_ast package.
3
+
4
+ These tests ensure that all API endpoints work together correctly in various scenarios.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import tempfile
10
+ import pytest
11
+ from typing import Dict, Any
12
+
13
+ from vex_ast import (
14
+ parse_string,
15
+ parse_file,
16
+ ErrorHandler,
17
+ VexSyntaxError,
18
+ VexAstError,
19
+ PrintVisitor,
20
+ NodeCounter,
21
+ VariableCollector,
22
+ create_navigator,
23
+ serialize_ast_to_dict,
24
+ serialize_ast_to_json,
25
+ deserialize_ast_from_dict,
26
+ deserialize_ast_from_json,
27
+ generate_ast_schema,
28
+ export_schema_to_file,
29
+ registry_api
30
+ )
31
+ from vex_ast.ast.vex_nodes import VexAPICall
32
+ from vex_ast.ast.validators import validate_vex_functions
33
+
34
+
35
+ class TestComprehensiveIntegration:
36
+ """Comprehensive integration tests for the vex_ast package."""
37
+
38
+ def test_parse_file_functionality(self):
39
+ """Test the parse_file functionality with a temporary file."""
40
+ # Create a temporary file with VEX code
41
+ with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_file:
42
+ temp_file.write("""
43
+ # Define a motor
44
+ motor1 = Motor(PORT1)
45
+
46
+ # Define a function
47
+ def move_forward(speed):
48
+ motor1.spin(FORWARD, speed, PERCENT)
49
+ wait(1, SECONDS)
50
+ motor1.stop()
51
+ return True
52
+
53
+ # Call the function
54
+ success = move_forward(50)
55
+ """)
56
+ temp_file_path = temp_file.name
57
+
58
+ try:
59
+ # Parse the file
60
+ ast = parse_file(temp_file_path)
61
+
62
+ # Verify the AST structure
63
+ assert ast is not None
64
+ assert len(ast.body) > 0
65
+
66
+ # Find function definitions
67
+ func_defs = [node for node in ast.body if node.__class__.__name__ == "FunctionDefinition"]
68
+ assert len(func_defs) == 1
69
+ assert func_defs[0].name == "move_forward"
70
+
71
+ # Test that visitors work with the parsed AST
72
+ printer = PrintVisitor()
73
+ result = printer.visit(ast)
74
+ assert isinstance(result, str)
75
+ assert len(result) > 0
76
+
77
+ counter = NodeCounter()
78
+ count = counter.visit(ast)
79
+ assert count > 10
80
+
81
+ collector = VariableCollector()
82
+ variables = collector.visit(ast)
83
+ assert "motor1" in variables
84
+ assert "speed" in variables
85
+
86
+ finally:
87
+ # Clean up the temporary file
88
+ os.unlink(temp_file_path)
89
+
90
+ def test_error_handling_integration(self):
91
+ """Test error handling integration with parsing and visitors."""
92
+ # Create an error handler that doesn't raise exceptions
93
+ error_handler = ErrorHandler(raise_on_error=False)
94
+
95
+ # Parse code with syntax errors
96
+ code_with_errors = """
97
+ def invalid_function(
98
+ # Missing closing parenthesis
99
+ print("This will cause an error")
100
+ """
101
+
102
+ # This should not raise an exception due to raise_on_error=False
103
+ ast = parse_string(code_with_errors, error_handler=error_handler)
104
+
105
+ # Verify that errors were collected
106
+ assert error_handler.has_errors()
107
+ errors = error_handler.get_errors()
108
+ assert len(errors) > 0
109
+
110
+ # Try with a different error handler that raises exceptions
111
+ error_handler_strict = ErrorHandler(raise_on_error=True)
112
+
113
+ # This should raise a VexSyntaxError
114
+ with pytest.raises(VexSyntaxError):
115
+ parse_string(code_with_errors, error_handler=error_handler_strict)
116
+
117
+ # Test error handling with valid code but invalid VEX API calls
118
+ invalid_vex_code = """
119
+ motor1 = Motor(PORT1)
120
+ # Invalid: wrong arguments to spin
121
+ motor1.spin(INVALID_DIRECTION, "not_a_number", "not_a_unit")
122
+ """
123
+
124
+ # Parse the code (should succeed syntactically)
125
+ ast = parse_string(invalid_vex_code)
126
+
127
+ # Validate VEX functions (should find errors)
128
+ validation_errors = validate_vex_functions(ast)
129
+ assert len(validation_errors) > 0
130
+
131
+ def test_end_to_end_workflow(self):
132
+ """Test an end-to-end workflow combining multiple API functions."""
133
+ # 1. Parse a complex program
134
+ code = """
135
+ # Initialize motors
136
+ left_motor = Motor(PORT1)
137
+ right_motor = Motor(PORT2)
138
+
139
+ # Define a function to drive forward
140
+ def drive_forward(speed, time_ms):
141
+ # Set both motors to the specified speed
142
+ left_motor.set_velocity(speed, PERCENT)
143
+ right_motor.set_velocity(speed, PERCENT)
144
+
145
+ # Start the motors
146
+ left_motor.spin(FORWARD)
147
+ right_motor.spin(FORWARD)
148
+
149
+ # Wait for the specified time
150
+ wait(time_ms, MSEC)
151
+
152
+ # Stop the motors
153
+ left_motor.stop()
154
+ right_motor.stop()
155
+
156
+ return True
157
+
158
+ # Define a function to turn
159
+ def turn(direction, speed, angle):
160
+ if direction == LEFT:
161
+ left_motor.set_velocity(-speed, PERCENT)
162
+ right_motor.set_velocity(speed, PERCENT)
163
+ else:
164
+ left_motor.set_velocity(speed, PERCENT)
165
+ right_motor.set_velocity(-speed, PERCENT)
166
+
167
+ left_motor.spin(FORWARD)
168
+ right_motor.spin(FORWARD)
169
+
170
+ # Wait until the robot has turned the specified angle
171
+ wait_until(gyro.rotation() >= angle)
172
+
173
+ left_motor.stop()
174
+ right_motor.stop()
175
+
176
+ return True
177
+
178
+ # Main program
179
+ def main():
180
+ # Display welcome message
181
+ brain.screen.print("Robot starting...")
182
+ wait(1, SECONDS)
183
+
184
+ # Drive forward at 50% speed for 2 seconds
185
+ success = drive_forward(50, 2000)
186
+
187
+ if success:
188
+ # Turn right 90 degrees
189
+ turn(RIGHT, 30, 90)
190
+
191
+ # Drive forward again
192
+ drive_forward(50, 1000)
193
+
194
+ brain.screen.print("Mission completed!")
195
+ else:
196
+ brain.screen.print("Drive failed!")
197
+
198
+ return 0
199
+ """
200
+
201
+ # 2. Parse the code
202
+ ast = parse_string(code)
203
+
204
+ # 3. Use a visitor to analyze the AST
205
+ counter = NodeCounter()
206
+ node_count = counter.visit(ast)
207
+ assert node_count > 50 # Complex program should have many nodes
208
+
209
+ # 4. Use the navigator to find specific nodes
210
+ navigator = create_navigator(ast)
211
+
212
+ # Find all function definitions
213
+ func_defs = navigator.find_function_definitions()
214
+ assert len(func_defs) == 3
215
+ func_names = [func.name for func in func_defs]
216
+ assert "drive_forward" in func_names
217
+ assert "turn" in func_names
218
+ assert "main" in func_names
219
+
220
+ # Find all VEX API calls
221
+ vex_calls = navigator.find_vex_api_calls()
222
+ assert len(vex_calls) > 5
223
+
224
+ # 5. Serialize the AST to JSON
225
+ json_str = serialize_ast_to_json(ast)
226
+ assert isinstance(json_str, str)
227
+
228
+ # 6. Deserialize back to an AST
229
+ deserialized_ast = deserialize_ast_from_json(json_str)
230
+
231
+ # 7. Verify the deserialized AST has the same structure
232
+ deserialized_func_defs = [node for node in deserialized_ast.body
233
+ if node.__class__.__name__ == "FunctionDefinition"]
234
+ assert len(deserialized_func_defs) == 3
235
+ deserialized_func_names = [func.name for func in deserialized_func_defs]
236
+ assert set(deserialized_func_names) == set(func_names)
237
+
238
+ # 8. Use a visitor on the deserialized AST
239
+ collector = VariableCollector()
240
+ variables = collector.visit(deserialized_ast)
241
+ assert "left_motor" in variables
242
+ assert "right_motor" in variables
243
+ assert "speed" in variables
244
+ assert "direction" in variables
245
+
246
+ def test_schema_export_functionality(self):
247
+ """Test the schema export functionality."""
248
+ # Generate the schema
249
+ schema = generate_ast_schema()
250
+ assert isinstance(schema, dict)
251
+
252
+ # Export the schema to a temporary file
253
+ with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as temp_file:
254
+ temp_file_path = temp_file.name
255
+
256
+ try:
257
+ # Export the schema
258
+ export_schema_to_file(schema, temp_file_path)
259
+
260
+ # Verify the file exists and contains valid JSON
261
+ assert os.path.exists(temp_file_path)
262
+
263
+ with open(temp_file_path, 'r') as f:
264
+ loaded_schema = json.load(f)
265
+
266
+ # Check that it's the same schema
267
+ assert loaded_schema["$schema"] == schema["$schema"]
268
+ assert "definitions" in loaded_schema
269
+ assert set(loaded_schema["definitions"].keys()) == set(schema["definitions"].keys())
270
+
271
+ finally:
272
+ # Clean up
273
+ if os.path.exists(temp_file_path):
274
+ os.unlink(temp_file_path)
275
+
276
+ def test_edge_cases(self):
277
+ """Test edge cases in parsing and serialization."""
278
+ # Test parsing an empty program
279
+ empty_ast = parse_string("")
280
+ assert empty_ast is not None
281
+ assert len(empty_ast.body) == 0
282
+
283
+ # Test parsing a program with only comments
284
+ comments_ast = parse_string("# This is a comment\n# Another comment")
285
+ assert comments_ast is not None
286
+ assert len(comments_ast.body) == 0
287
+
288
+ # Test parsing a program with unusual whitespace
289
+ whitespace_code = """
290
+
291
+
292
+ x = 42
293
+
294
+
295
+ y = x + 10
296
+
297
+
298
+ """
299
+ whitespace_ast = parse_string(whitespace_code)
300
+ assert whitespace_ast is not None
301
+ assert len(whitespace_ast.body) == 2
302
+
303
+ # Test serializing and deserializing an empty program
304
+ empty_json = serialize_ast_to_json(empty_ast)
305
+ deserialized_empty = deserialize_ast_from_json(empty_json)
306
+ assert deserialized_empty is not None
307
+ assert len(deserialized_empty.body) == 0
308
+
309
+ # Test with a program containing all types of literals
310
+ literals_code = """
311
+ # Number literals
312
+ int_val = 42
313
+ float_val = 3.14
314
+
315
+ # String literals
316
+ str_val = "Hello, world!"
317
+ str_val2 = 'Single quotes'
318
+
319
+ # Boolean literals
320
+ bool_val1 = True
321
+ bool_val2 = False
322
+
323
+ # None literal
324
+ none_val = None
325
+ """
326
+
327
+ literals_ast = parse_string(literals_code)
328
+ assert literals_ast is not None
329
+ assert len(literals_ast.body) > 0
330
+
331
+ # Serialize and deserialize
332
+ literals_json = serialize_ast_to_json(literals_ast)
333
+ deserialized_literals = deserialize_ast_from_json(literals_json)
334
+
335
+ # Verify the literals were preserved
336
+ collector = VariableCollector()
337
+ variables = collector.visit(deserialized_literals)
338
+ assert len(variables) == 0 # All are assignments, no references
@@ -58,9 +58,10 @@ class TestParser:
58
58
  code = "if x ==" # Incomplete if statement
59
59
  error_handler = ErrorHandler(raise_on_error=False)
60
60
 
61
- with pytest.raises(VexSyntaxError):
62
- parse_string(code, error_handler=error_handler)
63
-
61
+ # Should not raise an exception when raise_on_error=False
62
+ ast = parse_string(code, error_handler=error_handler)
63
+
64
+ # But should collect errors
64
65
  assert error_handler.has_errors()
65
66
  errors = error_handler.get_errors()
66
- assert len(errors) >= 1
67
+ assert len(errors) >= 1
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Dict, List, Optional, Union, cast, Any
4
4
 
5
- from .interfaces import IAstNode, IExpression, IVisitor, T_VisitorResult, IIdentifier, IFunctionCall
5
+ from .interfaces import IAstNode, IExpression, IVisitor, T_VisitorResult, IIdentifier, IFunctionCall, IConditionalExpression
6
6
  from .core import Expression
7
7
  from .operators import Operator
8
8
  from ..utils.source_location import SourceLocation
@@ -173,6 +173,49 @@ class KeywordArgument(Expression):
173
173
  """Get the keyword value."""
174
174
  return self.value
175
175
 
176
+ class ConditionalExpression(Expression, IConditionalExpression):
177
+ """A conditional expression (ternary operator, e.g., a if condition else b)."""
178
+
179
+ _fields = ('condition', 'true_expr', 'false_expr')
180
+
181
+ def __init__(self, condition: IExpression, true_expr: IExpression, false_expr: IExpression,
182
+ location: Optional[SourceLocation] = None):
183
+ super().__init__(location)
184
+ self.condition = condition
185
+ self.true_expr = true_expr
186
+ self.false_expr = false_expr
187
+
188
+ # Set parent references
189
+ if isinstance(condition, Expression):
190
+ condition.set_parent(self)
191
+ if isinstance(true_expr, Expression):
192
+ true_expr.set_parent(self)
193
+ if isinstance(false_expr, Expression):
194
+ false_expr.set_parent(self)
195
+
196
+ def get_children(self) -> List[IAstNode]:
197
+ """Get child nodes."""
198
+ return [
199
+ cast(IAstNode, self.condition),
200
+ cast(IAstNode, self.true_expr),
201
+ cast(IAstNode, self.false_expr)
202
+ ]
203
+
204
+ def accept(self, visitor: IVisitor[T_VisitorResult]) -> T_VisitorResult:
205
+ return visitor.visit_conditionalexpression(self)
206
+
207
+ def get_condition(self) -> IExpression:
208
+ """Get the condition expression."""
209
+ return self.condition
210
+
211
+ def get_true_expression(self) -> IExpression:
212
+ """Get the expression to evaluate if condition is true."""
213
+ return self.true_expr
214
+
215
+ def get_false_expression(self) -> IExpression:
216
+ """Get the expression to evaluate if condition is false."""
217
+ return self.false_expr
218
+
176
219
  class FunctionCall(Expression, IFunctionCall):
177
220
  """A function call."""
178
221
 
@@ -84,6 +84,22 @@ class IIdentifier(IExpression, Protocol):
84
84
  """Get the identifier name."""
85
85
  ...
86
86
 
87
+ @runtime_checkable
88
+ class IConditionalExpression(IExpression, Protocol):
89
+ """Protocol for conditional expression (ternary operator) nodes."""
90
+
91
+ def get_condition(self) -> IExpression:
92
+ """Get the condition expression."""
93
+ ...
94
+
95
+ def get_true_expression(self) -> IExpression:
96
+ """Get the expression to evaluate if condition is true."""
97
+ ...
98
+
99
+ def get_false_expression(self) -> IExpression:
100
+ """Get the expression to evaluate if condition is false."""
101
+ ...
102
+
87
103
  @runtime_checkable
88
104
  class IFunctionCall(IExpression, Protocol):
89
105
  """Protocol for function call nodes."""
@@ -91,8 +91,21 @@ class VexAPICall(FunctionCall):
91
91
  return False, self._validation_error
92
92
 
93
93
  # Convert args and kwargs to appropriate format
94
- arg_values = [arg for arg in self.args]
95
- kwarg_values = {kw.name: kw.value for kw in self.keywords or []}
94
+ arg_values = []
95
+ for arg in self.args:
96
+ # For string literals, use their actual string value for validation
97
+ if hasattr(arg, 'value') and hasattr(arg, '__class__') and arg.__class__.__name__ == 'StringLiteral':
98
+ arg_values.append(arg.value)
99
+ else:
100
+ arg_values.append(arg)
101
+
102
+ kwarg_values = {}
103
+ for kw in (self.keywords or []):
104
+ # For string literals, use their actual string value for validation
105
+ if hasattr(kw.value, 'value') and hasattr(kw.value, '__class__') and kw.value.__class__.__name__ == 'StringLiteral':
106
+ kwarg_values[kw.name] = kw.value.value
107
+ else:
108
+ kwarg_values[kw.name] = kw.value
96
109
 
97
110
  # Validate against the signature
98
111
  valid, error = signature.validate_arguments(arg_values, kwarg_values)
@@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Type, Union, cast, List
4
4
 
5
5
  from ..ast.core import Expression, Program, Statement
6
6
  from ..ast.expressions import (
7
- AttributeAccess, BinaryOperation, FunctionCall, Identifier, KeywordArgument,
7
+ AttributeAccess, BinaryOperation, ConditionalExpression, FunctionCall, Identifier, KeywordArgument,
8
8
  UnaryOperation, VariableReference
9
9
  )
10
10
  from ..ast.literals import (
@@ -77,6 +77,11 @@ class NodeFactory:
77
77
  """Create a unary operation node."""
78
78
  return UnaryOperation(op, operand, location)
79
79
 
80
+ def create_conditional_expression(self, condition: Expression, true_expr: Expression, false_expr: Expression,
81
+ location: Optional[SourceLocation] = None) -> ConditionalExpression:
82
+ """Create a conditional expression (ternary operator) node."""
83
+ return ConditionalExpression(condition, true_expr, false_expr, location)
84
+
80
85
  def create_function_call(self, function: Expression, args: List[Expression] = None,
81
86
  keywords: List[KeywordArgument] = None,
82
87
  location: Optional[SourceLocation] = None) -> FunctionCall:
@@ -123,6 +128,12 @@ class NodeFactory:
123
128
  """Create a function definition node."""
124
129
  return FunctionDefinition(name, args, body, return_annotation, location)
125
130
 
131
+ def create_argument(self, name: str, annotation: Optional[Expression] = None,
132
+ default: Optional[Expression] = None,
133
+ location: Optional[SourceLocation] = None) -> Argument:
134
+ """Create an argument node."""
135
+ return Argument(name, annotation, default, location)
136
+
126
137
  def create_return_statement(self, value: Optional[Expression] = None,
127
138
  location: Optional[SourceLocation] = None) -> ReturnStatement:
128
139
  """Create a return statement node."""
@@ -176,4 +187,4 @@ class NodeFactory:
176
187
  return Program(body, location)
177
188
 
178
189
  # Global factory instance for simple use cases
179
- default_factory = NodeFactory()
190
+ default_factory = NodeFactory()
@@ -294,21 +294,12 @@ class PythonParser(BaseParser):
294
294
  op_name = op_type.__name__
295
295
 
296
296
  op_map = {
297
- 'And': 'and',
298
- 'Or': 'or'
297
+ 'And': Operator.LOGICAL_AND,
298
+ 'Or': Operator.LOGICAL_OR
299
299
  }
300
300
 
301
301
  if op_name in op_map:
302
- op_str = op_map[op_name]
303
- vex_op = PYTHON_COMP_OP_MAP.get(op_str)
304
-
305
- if not vex_op:
306
- self.error_handler.add_error(
307
- ErrorType.PARSER_ERROR,
308
- f"Unsupported boolean operator: {op_name}",
309
- loc
310
- )
311
- vex_op = Operator.LOGICAL_AND # Fallback
302
+ vex_op = op_map[op_name]
312
303
  else:
313
304
  self.error_handler.add_error(
314
305
  ErrorType.PARSER_ERROR,
@@ -329,6 +320,15 @@ class PythonParser(BaseParser):
329
320
 
330
321
  return result
331
322
 
323
+ # Conditional expressions (ternary operators)
324
+ elif isinstance(node, ast.IfExp):
325
+ loc = self._get_location(node)
326
+ test = self._convert_expression(node.test)
327
+ body = self._convert_expression(node.body)
328
+ orelse = self._convert_expression(node.orelse)
329
+
330
+ return self.factory.create_conditional_expression(test, body, orelse, loc)
331
+
332
332
  # List literals
333
333
  elif isinstance(node, ast.List) or isinstance(node, ast.Tuple):
334
334
  # We don't have a dedicated list/tuple node, so use function call
@@ -607,22 +607,77 @@ class PythonParser(BaseParser):
607
607
  self._get_location(node)
608
608
  )
609
609
 
610
- # Import statements - not fully supported yet
611
- elif isinstance(node, (ast.Import, ast.ImportFrom)):
610
+ # Import statements
611
+ elif isinstance(node, ast.Import):
612
612
  loc = self._get_location(node)
613
- self.error_handler.add_error(
614
- ErrorType.PARSER_ERROR,
615
- "Import statements are not fully supported",
616
- loc
617
- )
618
- # Create a placeholder expression statement
619
- return self.factory.create_expression_statement(
620
- self.factory.create_identifier(
621
- f"<import:{getattr(node, 'names', [])}>",
613
+ # Create a list of assignments for each imported name
614
+ statements = []
615
+
616
+ for name in node.names:
617
+ # Create an identifier for the module
618
+ module_name = name.name
619
+ as_name = name.asname or module_name
620
+
621
+ # Create an assignment: as_name = module_name
622
+ target = self.factory.create_identifier(as_name, loc)
623
+ value = self.factory.create_identifier(f"<import:{module_name}>", loc)
624
+
625
+ statements.append(self.factory.create_assignment(target, value, loc))
626
+
627
+ # If there's only one statement, return it
628
+ if len(statements) == 1:
629
+ return statements[0]
630
+
631
+ # Otherwise, return the first one and add a warning
632
+ if len(statements) > 1:
633
+ self.error_handler.add_error(
634
+ ErrorType.PARSER_ERROR,
635
+ "Multiple imports in a single statement are not fully supported",
622
636
  loc
623
- ),
624
- loc
625
- )
637
+ )
638
+
639
+ return statements[0]
640
+
641
+ # Import from statements
642
+ elif isinstance(node, ast.ImportFrom):
643
+ loc = self._get_location(node)
644
+ module_name = node.module or ""
645
+
646
+ # Special case for "from vex import *"
647
+ if module_name == "vex" and any(name.name == "*" for name in node.names):
648
+ # Create a special identifier that represents "from vex import *"
649
+ return self.factory.create_expression_statement(
650
+ self.factory.create_identifier("<import:vex:*>", loc),
651
+ loc
652
+ )
653
+
654
+ # For other import from statements, create assignments
655
+ statements = []
656
+
657
+ for name in node.names:
658
+ # Create an identifier for the imported name
659
+ imported_name = name.name
660
+ as_name = name.asname or imported_name
661
+
662
+ # Create an assignment: as_name = module_name.imported_name
663
+ target = self.factory.create_identifier(as_name, loc)
664
+ value = self.factory.create_identifier(f"<import:{module_name}.{imported_name}>", loc)
665
+
666
+ statements.append(self.factory.create_assignment(target, value, loc))
667
+
668
+ # If there's only one statement, return it
669
+ if len(statements) == 1:
670
+ return statements[0]
671
+
672
+ # Otherwise, return the first one and add a warning
673
+ if len(statements) > 1:
674
+ self.error_handler.add_error(
675
+ ErrorType.PARSER_ERROR,
676
+ "Multiple imports in a single statement are not fully supported",
677
+ loc
678
+ )
679
+
680
+ return statements[0]
626
681
 
627
682
  # Class definitions - not supported yet
628
683
  elif isinstance(node, ast.ClassDef):
@@ -692,7 +747,13 @@ class PythonParser(BaseParser):
692
747
  f"Syntax error: {e.msg}",
693
748
  loc
694
749
  )
695
- raise VexSyntaxError(f"Syntax error: {e.msg}", loc) from e
750
+
751
+ # Only raise if the error handler is configured to do so
752
+ if self.error_handler._raise_on_error:
753
+ raise VexSyntaxError(f"Syntax error: {e.msg}", loc) from e
754
+
755
+ # Return an empty program if we're not raising
756
+ return self.factory.create_program([])
696
757
 
697
758
  except Exception as e:
698
759
  # Handle other parsing errors
@@ -24,6 +24,11 @@ class VexFunctionParameter:
24
24
  self.description = description
25
25
  self.is_optional = default_value is not None
26
26
 
27
+ @property
28
+ def optional(self) -> bool:
29
+ """Alias for is_optional for compatibility."""
30
+ return self.is_optional
31
+
27
32
  def __str__(self) -> str:
28
33
  mode_str = ""
29
34
  if self.mode == ParameterMode.REFERENCE:
@@ -138,6 +143,49 @@ class VexFunctionSignature:
138
143
  if param_name in kwargs:
139
144
  return False, f"Duplicate argument '{param_name}' for {self.name}"
140
145
 
141
- # TODO: Add type checking for arguments
146
+ # Type checking for arguments
147
+ from ..types.type_checker import type_checker
148
+ from ..types.enums import EnumType
149
+
150
+ # Check positional arguments
151
+ for i, arg in enumerate(args):
152
+ if i >= len(self.parameters):
153
+ break
154
+
155
+ param = self.parameters[i]
156
+ expected_type = param.type
157
+
158
+ # Handle string literals for enum types
159
+ if isinstance(expected_type, EnumType) and isinstance(arg, str):
160
+ if arg not in expected_type.values:
161
+ return False, f"Invalid enum value '{arg}' for parameter '{param.name}' in {self.name}"
162
+ continue
163
+
164
+ # Handle other types
165
+ if hasattr(arg, 'get_type'):
166
+ arg_type = arg.get_type()
167
+ if arg_type and not type_checker.is_compatible(arg_type, expected_type):
168
+ return False, f"Type mismatch for parameter '{param.name}' in {self.name}: expected {expected_type}, got {arg_type}"
169
+
170
+ # Check keyword arguments
171
+ for kwarg_name, kwarg_value in kwargs.items():
172
+ # Find the parameter
173
+ param = next((p for p in self.parameters if p.name == kwarg_name), None)
174
+ if not param:
175
+ continue # Already checked for unknown kwargs above
176
+
177
+ expected_type = param.type
178
+
179
+ # Handle string literals for enum types
180
+ if isinstance(expected_type, EnumType) and isinstance(kwarg_value, str):
181
+ if kwarg_value not in expected_type.values:
182
+ return False, f"Invalid enum value '{kwarg_value}' for parameter '{param.name}' in {self.name}"
183
+ continue
184
+
185
+ # Handle other types
186
+ if hasattr(kwarg_value, 'get_type'):
187
+ kwarg_type = kwarg_value.get_type()
188
+ if kwarg_type and not type_checker.is_compatible(kwarg_type, expected_type):
189
+ return False, f"Type mismatch for parameter '{param.name}' in {self.name}: expected {expected_type}, got {kwarg_type}"
142
190
 
143
191
  return True, None
@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Type, Union, cast
10
10
  from ..ast.interfaces import IAstNode
11
11
  from ..parser.factory import NodeFactory
12
12
  from ..utils.source_location import SourceLocation
13
- from ..utils.errors import ErrorHandler
13
+ from ..utils.errors import ErrorHandler, ErrorType
14
14
 
15
15
 
16
16
  class DeserializationFactory:
@@ -153,6 +153,7 @@ class DeserializationFactory:
153
153
  "WhileLoop": self.node_factory.create_while_loop,
154
154
  "ForLoop": self.node_factory.create_for_loop,
155
155
  "FunctionDefinition": self.node_factory.create_function_definition,
156
+ "Argument": self.node_factory.create_argument,
156
157
  "ReturnStatement": self.node_factory.create_return_statement,
157
158
  "BreakStatement": self.node_factory.create_break_statement,
158
159
  "ContinueStatement": self.node_factory.create_continue_statement,
@@ -198,11 +199,21 @@ class DeserializationFactory:
198
199
  except TypeError as e:
199
200
  # If the factory method doesn't accept the kwargs, report an error
200
201
  if self.error_handler:
201
- self.error_handler.report_error(f"Failed to create {node_type}: {str(e)}")
202
+ self.error_handler.add_error(
203
+ error_type=ErrorType.INTERNAL_ERROR,
204
+ message=f"Failed to create {node_type}: {str(e)}"
205
+ )
202
206
  raise ValueError(f"Failed to deserialize {node_type}: {str(e)}")
203
207
 
204
208
  # Specific node creation methods for complex cases
205
209
 
210
+ def _create_attributeaccess(self, data: Dict[str, Any],
211
+ location: Optional[SourceLocation]) -> IAstNode:
212
+ """Create an AttributeAccess node from serialized data."""
213
+ object_expr = self._deserialize_value(data.get("object"))
214
+ attribute = data.get("attribute", "")
215
+ return self.node_factory.create_attribute_access(object_expr, attribute, location)
216
+
206
217
  def _create_program(self, data: Dict[str, Any],
207
218
  location: Optional[SourceLocation]) -> IAstNode:
208
219
  """Create a Program node from serialized data."""
@@ -453,16 +453,15 @@ def _generate_definitions() -> Dict[str, Any]:
453
453
  return definitions
454
454
 
455
455
 
456
- def export_schema_to_file(filepath: str, indent: int = 2) -> None:
456
+ def export_schema_to_file(schema: Dict[str, Any], filepath: str, indent: int = 2) -> None:
457
457
  """
458
- Save the generated schema to a file.
458
+ Save the schema to a file.
459
459
 
460
460
  Args:
461
+ schema: The schema to save
461
462
  filepath: The path to save the schema to
462
463
  indent: The indentation level for pretty-printing
463
464
  """
464
- schema = generate_ast_schema()
465
-
466
465
  # Ensure the directory exists
467
466
  os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
468
467
 
@@ -50,7 +50,7 @@ class ErrorObserver(Protocol[T_Error]):
50
50
  class ErrorHandler:
51
51
  """Manages error collection and notification."""
52
52
 
53
- def __init__(self, raise_on_error: bool = False):
53
+ def __init__(self, raise_on_error: bool = True):
54
54
  self._errors: List[Error] = []
55
55
  self._raise_on_error = raise_on_error
56
56
  self._observers: List[ErrorObserver] = []
@@ -109,4 +109,4 @@ class VexSyntaxError(VexAstError):
109
109
 
110
110
  def __init__(self, message: str, location: Optional[SourceLocation] = None):
111
111
  self.location = location
112
- super().__init__(message)
112
+ super().__init__(message)
@@ -47,6 +47,9 @@ class AstVisitor(Generic[T_VisitorResult], ABC):
47
47
  def visit_unaryoperation(self, node: Any) -> T_VisitorResult:
48
48
  return self.generic_visit(node)
49
49
 
50
+ def visit_conditionalexpression(self, node: Any) -> T_VisitorResult:
51
+ return self.generic_visit(node)
52
+
50
53
  def visit_functioncall(self, node: Any) -> T_VisitorResult:
51
54
  return self.generic_visit(node)
52
55
 
@@ -127,4 +130,4 @@ class TypedVisitorMixin:
127
130
  method_name = self.node_type_to_method_name(type(node))
128
131
  if hasattr(self, method_name):
129
132
  return getattr(self, method_name)(node)
130
- return self.generic_visit(node)
133
+ return self.generic_visit(node)
@@ -126,6 +126,7 @@ class PrintVisitor(AstVisitor[str]):
126
126
  visit_variablereference = generic_visit
127
127
  visit_binaryoperation = generic_visit
128
128
  visit_unaryoperation = generic_visit
129
+ visit_conditionalexpression = generic_visit
129
130
  visit_functioncall = generic_visit
130
131
  visit_keywordargument = generic_visit
131
132
  visit_expressionstatement = generic_visit
@@ -142,4 +143,4 @@ class PrintVisitor(AstVisitor[str]):
142
143
  visit_motorcontrol = generic_visit
143
144
  visit_sensorreading = generic_visit
144
145
  visit_timingcontrol = generic_visit
145
- visit_displayoutput = generic_visit
146
+ visit_displayoutput = generic_visit
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vex_ast
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A Python package for generating Abstract Syntax Trees for VEX V5 code.
5
5
  Home-page: https://github.com/heartx2/vex_ast
6
6
  Author: Chaze
@@ -6,6 +6,7 @@ pytest.ini
6
6
  requirements.txt
7
7
  setup.py
8
8
  tests/conftest.py
9
+ tests/test_comprehensive_integration.py
9
10
  tests/test_core.py
10
11
  tests/test_integration.py
11
12
  tests/test_literals.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes