langroid 0.16.7__py3-none-any.whl → 0.17.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.
Files changed (91) hide show
  1. langroid/agent/base.py +45 -21
  2. langroid/agent/chat_agent.py +22 -14
  3. langroid/agent/chat_document.py +22 -13
  4. langroid/agent/tool_message.py +11 -11
  5. langroid/agent/tools/file_tools.py +234 -0
  6. langroid/agent/xml_tool_message.py +179 -45
  7. langroid/utils/constants.py +2 -0
  8. langroid/utils/git_utils.py +251 -0
  9. langroid/utils/system.py +78 -0
  10. {langroid-0.16.7.dist-info → langroid-0.17.1.dist-info}/METADATA +6 -3
  11. {langroid-0.16.7.dist-info → langroid-0.17.1.dist-info}/RECORD +14 -89
  12. pyproject.toml +3 -2
  13. langroid/agent/md_tool_message_grammar.py +0 -455
  14. langroid/agent/tools/code_file_tool_parse.py +0 -150
  15. langroid/agent/tools/code_file_tool_pyparsing.py +0 -194
  16. langroid/agent/tools/code_file_tool_pyparsing2.py +0 -199
  17. langroid/agent/tools/extract_tool.py +0 -96
  18. langroid/agent/tools/formatted_model_custom.py +0 -150
  19. langroid/agent/tools/formatted_model_custom2.py +0 -168
  20. langroid/agent/tools/formatted_model_custom3.py +0 -279
  21. langroid/agent/tools/formatted_model_custom4.py +0 -395
  22. langroid/agent/tools/formatted_model_jinja.py +0 -133
  23. langroid/agent/tools/formatted_model_jinja.py-e +0 -122
  24. langroid/agent/tools/formatted_model_jinja2.py +0 -145
  25. langroid/agent/tools/formatted_model_jinja2.py-e +0 -135
  26. langroid/agent/tools/formatted_model_lark.py +0 -0
  27. langroid/agent/tools/formatted_model_lark2.py +0 -168
  28. langroid/agent/tools/formatted_model_parse.py +0 -105
  29. langroid/agent/tools/formatted_model_parse.py-e +0 -98
  30. langroid/agent/tools/formatted_model_parse2.py +0 -113
  31. langroid/agent/tools/formatted_model_parse2.py-e +0 -109
  32. langroid/agent/tools/formatted_model_parse3.py +0 -114
  33. langroid/agent/tools/formatted_model_parse3.py-e +0 -110
  34. langroid/agent/tools/formatted_model_parsimon.py +0 -194
  35. langroid/agent/tools/formatted_model_parsimon.py-e +0 -186
  36. langroid/agent/tools/formatted_model_pyparsing.py +0 -169
  37. langroid/agent/tools/formatted_model_pyparsing.py-e +0 -149
  38. langroid/agent/tools/formatted_model_pyparsing2.py +0 -159
  39. langroid/agent/tools/formatted_model_pyparsing2.py-e +0 -143
  40. langroid/agent/tools/formatted_model_pyparsing3.py +0 -133
  41. langroid/agent/tools/formatted_model_pyparsing3.py-e +0 -121
  42. langroid/agent/tools/formatted_model_pyparsing4.py +0 -213
  43. langroid/agent/tools/formatted_model_pyparsing4.py-e +0 -176
  44. langroid/agent/tools/formatted_model_pyparsing5.py +0 -173
  45. langroid/agent/tools/formatted_model_pyparsing5.py-e +0 -142
  46. langroid/agent/tools/formatted_model_regex.py +0 -246
  47. langroid/agent/tools/formatted_model_regex.py-e +0 -248
  48. langroid/agent/tools/formatted_model_regex2.py +0 -250
  49. langroid/agent/tools/formatted_model_regex2.py-e +0 -253
  50. langroid/agent/tools/formatted_model_tatsu.py +0 -172
  51. langroid/agent/tools/formatted_model_tatsu.py-e +0 -160
  52. langroid/agent/tools/formatted_model_template.py +0 -217
  53. langroid/agent/tools/formatted_model_template.py-e +0 -200
  54. langroid/agent/tools/formatted_model_xml.py +0 -178
  55. langroid/agent/tools/formatted_model_xml2.py +0 -178
  56. langroid/agent/tools/formatted_model_xml3.py +0 -132
  57. langroid/agent/tools/formatted_model_xml4.py +0 -130
  58. langroid/agent/tools/formatted_model_xml5.py +0 -130
  59. langroid/agent/tools/formatted_model_xml6.py +0 -113
  60. langroid/agent/tools/formatted_model_xml7.py +0 -117
  61. langroid/agent/tools/formatted_model_xml8.py +0 -164
  62. langroid/agent/tools/generator_tool.py +0 -20
  63. langroid/agent/tools/generic_tool.py +0 -165
  64. langroid/agent/tools/generic_tool_tatsu.py +0 -275
  65. langroid/agent/tools/grammar_based_model.py +0 -132
  66. langroid/agent/tools/grammar_based_model.py-e +0 -128
  67. langroid/agent/tools/grammar_based_model_lark.py +0 -156
  68. langroid/agent/tools/grammar_based_model_lark.py-e +0 -153
  69. langroid/agent/tools/grammar_based_model_parse.py +0 -86
  70. langroid/agent/tools/grammar_based_model_parse.py-e +0 -80
  71. langroid/agent/tools/grammar_based_model_parsimonious.py +0 -129
  72. langroid/agent/tools/grammar_based_model_parsimonious.py-e +0 -120
  73. langroid/agent/tools/grammar_based_model_pyparsing.py +0 -105
  74. langroid/agent/tools/grammar_based_model_pyparsing.py-e +0 -103
  75. langroid/agent/tools/grammar_based_model_regex.py +0 -139
  76. langroid/agent/tools/grammar_based_model_regex.py-e +0 -130
  77. langroid/agent/tools/grammar_based_model_regex2.py +0 -124
  78. langroid/agent/tools/grammar_based_model_regex2.py-e +0 -116
  79. langroid/agent/tools/grammar_based_model_tatsu.py +0 -80
  80. langroid/agent/tools/grammar_based_model_tatsu.py-e +0 -77
  81. langroid/agent/tools/lark_earley_example.py +0 -135
  82. langroid/agent/tools/lark_earley_example.py-e +0 -117
  83. langroid/agent/tools/lark_example.py +0 -72
  84. langroid/agent/tools/note_tool.py +0 -0
  85. langroid/agent/tools/parse_example.py +0 -76
  86. langroid/agent/tools/parse_example2.py +0 -87
  87. langroid/agent/tools/parse_example3.py +0 -42
  88. langroid/agent/tools/parse_test.py +0 -791
  89. langroid/agent/tools/run_python_code.py +0 -60
  90. {langroid-0.16.7.dist-info → langroid-0.17.1.dist-info}/LICENSE +0 -0
  91. {langroid-0.16.7.dist-info → langroid-0.17.1.dist-info}/WHEEL +0 -0
@@ -1,124 +0,0 @@
1
- import re
2
- from abc import ABC, abstractmethod
3
-
4
- from langroid.pydantic_v1 import BaseModel
5
-
6
-
7
- class FormattingModel(BaseModel, ABC):
8
- @classmethod
9
- @abstractmethod
10
- def format_spec(cls) -> str:
11
- pass
12
-
13
- @classmethod
14
- @abstractmethod
15
- def start_token(cls) -> str:
16
- pass
17
-
18
- @classmethod
19
- @abstractmethod
20
- def end_token(cls) -> str:
21
- pass
22
-
23
- @classmethod
24
- def _create_regex_pattern(cls) -> str:
25
- spec = cls.format_spec()
26
- # Replace {field_name} with (?P<field_name>.*?)
27
- pattern = re.sub(r"\{(\w+)\}", lambda m: f"(?P<{m.group(1)}>.*?)", spec)
28
- # Replace newlines with \s* to allow flexible whitespace
29
- pattern = pattern.replace("\n", r"\s*")
30
- return f"{re.escape(cls.start_token())}\\s*{pattern}\\s*{re.escape(cls.end_token())}"
31
-
32
- @classmethod
33
- def parse(cls, text: str) -> "FormattingModel":
34
- pattern = cls._create_regex_pattern()
35
- match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
36
- if match:
37
- return cls(**{k: v.strip() for k, v in match.groupdict().items()})
38
- raise ValueError(
39
- f"Text does not match the expected format. Pattern: {pattern}, Text: {text}"
40
- )
41
-
42
- def generate(self) -> str:
43
- content = self.format_spec().format(**self.dict())
44
- return f"{self.start_token()}\n{content}\n{self.end_token()}"
45
-
46
-
47
- class PersonModel(FormattingModel):
48
- name: str
49
- age: int
50
- city: str
51
-
52
- @classmethod
53
- def format_spec(cls) -> str:
54
- return "name: {name}\nage is {age}\nlives in {city}"
55
-
56
- @classmethod
57
- def start_token(cls) -> str:
58
- return "<spec>"
59
-
60
- @classmethod
61
- def end_token(cls) -> str:
62
- return "</spec>"
63
-
64
-
65
- def test_round_trip(model_class, input_string):
66
- # Parse the input string
67
- parsed_model = model_class.parse(input_string)
68
- print(f"Parsed model: {parsed_model}")
69
-
70
- # Generate a string from the parsed model
71
- generated_string = parsed_model.generate()
72
- print(f"Generated string:\n{generated_string}")
73
-
74
- # Parse the generated string
75
- reparsed_model = model_class.parse(generated_string)
76
- print(f"Reparsed model: {reparsed_model}")
77
-
78
- # Assert that the original parsed model and the reparsed model are equal
79
- assert (
80
- parsed_model == reparsed_model
81
- ), "Round trip failed: original and reparsed models are not equal"
82
-
83
- # Assert that all fields are present and have the correct types
84
- for field, field_type in model_class.__annotations__.items():
85
- assert hasattr(parsed_model, field), f"Field {field} is missing"
86
- assert isinstance(
87
- getattr(parsed_model, field), field_type
88
- ), f"Field {field} has incorrect type"
89
-
90
- print("Round trip test passed successfully!")
91
-
92
-
93
- if __name__ == "__main__":
94
- # Test case 1: Standard formatting
95
- test_string1 = """
96
- <spec>
97
- name: John Doe
98
- age is 30
99
- lives in New York
100
- </spec>
101
- """
102
- test_round_trip(PersonModel, test_string1)
103
-
104
- print("\n" + "=" * 50 + "\n")
105
-
106
- # Test case 2: Varying whitespace
107
- test_string2 = "<spec>name: Alice \nage is 25 \nlives in Tokyo</spec>"
108
- test_round_trip(PersonModel, test_string2)
109
-
110
- print("\n" + "=" * 50 + "\n")
111
-
112
- # Test case 3: Multiline values
113
- test_string3 = """
114
- <spec>
115
- name: Bob
116
- Smith
117
- age is 40
118
- lives in San
119
- Francisco
120
- </spec>
121
- """
122
- test_round_trip(PersonModel, test_string3)
123
-
124
- print("All tests passed successfully!")
@@ -1,116 +0,0 @@
1
- import re
2
- from abc import ABC, abstractmethod
3
- from typing import Dict, Any
4
- from pydantic import BaseModel
5
-
6
- class FormattingModel(BaseModel, ABC):
7
- @classmethod
8
- @abstractmethod
9
- def format_spec(cls) -> str:
10
- pass
11
-
12
- @classmethod
13
- @abstractmethod
14
- def start_token(cls) -> str:
15
- pass
16
-
17
- @classmethod
18
- @abstractmethod
19
- def end_token(cls) -> str:
20
- pass
21
-
22
- @classmethod
23
- def _create_regex_pattern(cls) -> str:
24
- spec = cls.format_spec()
25
- # Replace {field_name} with (?P<field_name>.*?)
26
- pattern = re.sub(r'\{(\w+)\}', lambda m: f'(?P<{m.group(1)}>.*?)', spec)
27
- # Replace newlines with \s* to allow flexible whitespace
28
- pattern = pattern.replace('\n', r'\s*')
29
- return f"{re.escape(cls.start_token())}\\s*{pattern}\\s*{re.escape(cls.end_token())}"
30
-
31
- @classmethod
32
- def parse(cls, text: str) -> 'FormattingModel':
33
- pattern = cls._create_regex_pattern()
34
- match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
35
- if match:
36
- return cls(**{k: v.strip() for k, v in match.groupdict().items()})
37
- raise ValueError(f"Text does not match the expected format. Pattern: {pattern}, Text: {text}")
38
-
39
- def generate(self) -> str:
40
- content = self.format_spec().format(**self.dict())
41
- return f"{self.start_token()}\n{content}\n{self.end_token()}"
42
-
43
- class PersonModel(FormattingModel):
44
- name: str
45
- age: int
46
- city: str
47
-
48
- @classmethod
49
- def format_spec(cls) -> str:
50
- return "name: {name}\nage is {age}\nlives in {city}"
51
-
52
- @classmethod
53
- def start_token(cls) -> str:
54
- return "<spec>"
55
-
56
- @classmethod
57
- def end_token(cls) -> str:
58
- return "</spec>"
59
-
60
- def test_round_trip(model_class, input_string):
61
- # Parse the input string
62
- parsed_model = model_class.parse(input_string)
63
- print(f"Parsed model: {parsed_model}")
64
-
65
- # Generate a string from the parsed model
66
- generated_string = parsed_model.generate()
67
- print(f"Generated string:\n{generated_string}")
68
-
69
- # Parse the generated string
70
- reparsed_model = model_class.parse(generated_string)
71
- print(f"Reparsed model: {reparsed_model}")
72
-
73
- # Assert that the original parsed model and the reparsed model are equal
74
- assert parsed_model == reparsed_model, "Round trip failed: original and reparsed models are not equal"
75
-
76
- # Assert that all fields are present and have the correct types
77
- for field, field_type in model_class.__annotations__.items():
78
- assert hasattr(parsed_model, field), f"Field {field} is missing"
79
- assert isinstance(getattr(parsed_model, field), field_type), f"Field {field} has incorrect type"
80
-
81
- print("Round trip test passed successfully!")
82
-
83
- if __name__ == "__main__":
84
- # Test case 1: Standard formatting
85
- test_string1 = """
86
- <spec>
87
- name: John Doe
88
- age is 30
89
- lives in New York
90
- </spec>
91
- """
92
- test_round_trip(PersonModel, test_string1)
93
-
94
- print("\n" + "="*50 + "\n")
95
-
96
- # Test case 2: Varying whitespace
97
- test_string2 = "<spec>name: Alice \nage is 25 \nlives in Tokyo</spec>"
98
- test_round_trip(PersonModel, test_string2)
99
-
100
- print("\n" + "="*50 + "\n")
101
-
102
- # Test case 3: Multiline values
103
- test_string3 = """
104
- <spec>
105
- name: Bob
106
- Smith
107
- age is 40
108
- lives in San
109
- Francisco
110
- </spec>
111
- """
112
- test_round_trip(PersonModel, test_string3)
113
-
114
- print("All tests passed successfully!")
115
-
116
-
@@ -1,80 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from typing import ClassVar, Dict
3
-
4
- from tatsu import compile as compile_grammar
5
- from tatsu.model import ModelBuilder
6
-
7
- from langroid.pydantic_v1 import BaseModel
8
-
9
-
10
- class GrammarBasedModel(BaseModel, ABC):
11
- grammar: ClassVar[str]
12
- start_token: ClassVar[str]
13
- end_token: ClassVar[str]
14
- field_mappings: ClassVar[Dict[str, str]]
15
-
16
- @classmethod
17
- @abstractmethod
18
- def get_grammar(cls) -> str:
19
- pass
20
-
21
- @classmethod
22
- def parse(cls, text: str) -> "GrammarBasedModel":
23
- parser = compile_grammar(cls.get_grammar())
24
- ast = parser.parse(text, start="start")
25
- model_dict = {
26
- field: getattr(ast, rule) for field, rule in cls.field_mappings.items()
27
- }
28
- return cls(**model_dict)
29
-
30
- def generate(self) -> str:
31
- grammar = compile_grammar(self.get_grammar())
32
- model_builder = ModelBuilder()
33
- for field, rule in self.field_mappings.items():
34
- setattr(model_builder, rule, getattr(self, field))
35
- ast = model_builder.start()
36
- return f"{self.start_token}\n{grammar.parse(str(ast), start='start')}\n{self.end_token}"
37
-
38
-
39
- class PersonSpec(GrammarBasedModel):
40
- name: str
41
- age: int
42
- city: str
43
-
44
- grammar = """
45
- start = name_line age_line city_line;
46
- name_line = 'name:' /\s*/ name:/.+/ EOL;
47
- age_line = 'age is' /\s*/ age:/\d+/ EOL;
48
- city_line = 'lives in' /\s*/ city:/.+/ EOL;
49
- EOL = /\r?\n/;
50
- """
51
- start_token = "<spec>"
52
- end_token = "</spec>"
53
- field_mappings = {"name": "name", "age": "age", "city": "city"}
54
-
55
- @classmethod
56
- def get_grammar(cls):
57
- return cls.grammar
58
-
59
-
60
- if __name__ == "__main__":
61
- # Test parsing
62
- input_str = """<spec>
63
- name: John Doe
64
- age is 30
65
- lives in New York
66
- </spec>"""
67
- person = PersonSpec.parse(input_str)
68
- print("Parsed person:", person)
69
-
70
- # Test generation
71
- generated_str = person.generate()
72
- print("\nGenerated string:")
73
- print(generated_str)
74
-
75
- # Test round-trip
76
- round_trip_person = PersonSpec.parse(generated_str)
77
- print("\nRound-trip parsed person:", round_trip_person)
78
-
79
- assert person == round_trip_person, "Round-trip parsing failed"
80
- print("\nRound-trip test passed!")
@@ -1,77 +0,0 @@
1
- from typing import Dict, Type, ClassVar
2
- from pydantic import BaseModel
3
- from tatsu import compile as compile_grammar
4
- from tatsu.model import ModelBuilder
5
- from abc import ABC, abstractmethod
6
-
7
- class GrammarBasedModel(BaseModel, ABC):
8
- grammar: ClassVar[str]
9
- start_token: ClassVar[str]
10
- end_token: ClassVar[str]
11
- field_mappings: ClassVar[Dict[str, str]]
12
-
13
- @classmethod
14
- @abstractmethod
15
- def get_grammar(cls) -> str:
16
- pass
17
-
18
- @classmethod
19
- def parse(cls, text: str) -> 'GrammarBasedModel':
20
- parser = compile_grammar(cls.get_grammar())
21
- ast = parser.parse(text, start='start')
22
- model_dict = {field: getattr(ast, rule) for field, rule in cls.field_mappings.items()}
23
- return cls(**model_dict)
24
-
25
- def generate(self) -> str:
26
- grammar = compile_grammar(self.get_grammar())
27
- model_builder = ModelBuilder()
28
- for field, rule in self.field_mappings.items():
29
- setattr(model_builder, rule, getattr(self, field))
30
- ast = model_builder.start()
31
- return f"{self.start_token}\n{grammar.parse(str(ast), start='start')}\n{self.end_token}"
32
-
33
- class PersonSpec(GrammarBasedModel):
34
- name: str
35
- age: int
36
- city: str
37
-
38
- grammar = """
39
- start = name_line age_line city_line;
40
- name_line = 'name:' /\s*/ name:/.+/ EOL;
41
- age_line = 'age is' /\s*/ age:/\d+/ EOL;
42
- city_line = 'lives in' /\s*/ city:/.+/ EOL;
43
- EOL = /\r?\n/;
44
- """
45
- start_token = "<spec>"
46
- end_token = "</spec>"
47
- field_mappings = {
48
- "name": "name",
49
- "age": "age",
50
- "city": "city"
51
- }
52
-
53
- @classmethod
54
- def get_grammar(cls):
55
- return cls.grammar
56
-
57
- if __name__ == "__main__":
58
- # Test parsing
59
- input_str = """<spec>
60
- name: John Doe
61
- age is 30
62
- lives in New York
63
- </spec>"""
64
- person = PersonSpec.parse(input_str)
65
- print("Parsed person:", person)
66
-
67
- # Test generation
68
- generated_str = person.generate()
69
- print("\nGenerated string:")
70
- print(generated_str)
71
-
72
- # Test round-trip
73
- round_trip_person = PersonSpec.parse(generated_str)
74
- print("\nRound-trip parsed person:", round_trip_person)
75
-
76
- assert person == round_trip_person, "Round-trip parsing failed"
77
- print("\nRound-trip test passed!")
@@ -1,135 +0,0 @@
1
- import pytest
2
- from lark import Lark, Transformer, Visitor
3
-
4
- from langroid.pydantic_v1 import BaseModel
5
-
6
-
7
- class Person(BaseModel):
8
- name: str
9
- age: int
10
- city: str
11
-
12
-
13
- grammar = """
14
- person: "Name:" name "Age:" age "City:" city
15
- name: WORD
16
- age: NUMBER
17
- city: WORD
18
- WORD: /\w+/
19
- NUMBER: /\d+/
20
- %import common.WS
21
- %ignore WS
22
- """
23
-
24
- parser = Lark(grammar, start="person", parser="earley")
25
-
26
-
27
- class PersonTransformer(Transformer):
28
- def person(self, items):
29
- return Person(name=items[1], age=items[3], city=items[5])
30
-
31
- def name(self, items):
32
- return items[0].value
33
-
34
- def age(self, items):
35
- return int(items[0].value)
36
-
37
- def city(self, items):
38
- return items[0].value
39
-
40
-
41
- class PersonToStringVisitor(Visitor):
42
- def __init__(self):
43
- self.result = []
44
-
45
- def person(self, tree):
46
- self.visit_children(tree)
47
- return " ".join(self.result)
48
-
49
- def name(self, tree):
50
- self.result.extend(["Name:", str(tree.children[0])])
51
-
52
- def age(self, tree):
53
- self.result.extend(["Age:", str(tree.children[0])])
54
-
55
- def city(self, tree):
56
- self.result.extend(["City:", str(tree.children[0])])
57
-
58
-
59
- transformer = PersonTransformer()
60
- to_string_visitor = PersonToStringVisitor()
61
-
62
-
63
- def from_string(cls, string):
64
- tree = parser.parse(string)
65
- return transformer.transform(tree)
66
-
67
-
68
- def to_string(self):
69
- from lark import Token
70
-
71
- tree = parser.parse(
72
- f"Name: {Token('WORD', self.name)} "
73
- f"Age: {Token('NUMBER', str(self.age))} "
74
- f"City: {Token('WORD', self.city)}"
75
- )
76
- return to_string_visitor.visit(tree)
77
-
78
-
79
- Person.from_string = classmethod(from_string)
80
- Person.to_string = to_string
81
-
82
- # Test functions remain the same as in the previous version
83
-
84
-
85
- # Test functions
86
- def test_from_string():
87
- person_str = "Name: John Age: 30 City: NewYork"
88
- person = Person.from_string(person_str)
89
- assert person.name == "John"
90
- assert person.age == 30
91
- assert person.city == "NewYork"
92
-
93
-
94
- def test_to_string():
95
- person = Person(name="Alice", age=25, city="London")
96
- person_str = person.to_string()
97
- assert person_str == "Name: Alice Age: 25 City: London"
98
-
99
-
100
- def test_roundtrip():
101
- original_str = "Name: Bob Age: 40 City: Paris"
102
- person = Person.from_string(original_str)
103
- regenerated_str = person.to_string()
104
- assert original_str == regenerated_str
105
-
106
-
107
- def test_different_values():
108
- person = Person(name="Charlie", age=35, city="Berlin")
109
- person_str = person.to_string()
110
- assert person_str == "Name: Charlie Age: 35 City: Berlin"
111
-
112
-
113
- def test_edge_cases():
114
- # Test with minimum age
115
- person = Person(name="Young", age=0, city="Baby")
116
- person_str = person.to_string()
117
- assert person_str == "Name: Young Age: 0 City: Baby"
118
-
119
- # Test with very long name and city
120
- long_name = "A" * 100
121
- long_city = "B" * 100
122
- person = Person(name=long_name, age=50, city=long_city)
123
- person_str = person.to_string()
124
- assert person_str == f"Name: {long_name} Age: 50 City: {long_city}"
125
-
126
-
127
- def test_invalid_input():
128
- with pytest.raises(
129
- Exception
130
- ): # The exact exception may vary based on Lark's implementation
131
- Person.from_string("Invalid: Input")
132
-
133
-
134
- if __name__ == "__main__":
135
- pytest.main([__file__])
@@ -1,117 +0,0 @@
1
- import pytest
2
- from pydantic import BaseModel
3
- from lark import Lark, Transformer, Visitor
4
-
5
- class Person(BaseModel):
6
- name: str
7
- age: int
8
- city: str
9
-
10
- grammar = """
11
- person: "Name:" name "Age:" age "City:" city
12
- name: WORD
13
- age: NUMBER
14
- city: WORD
15
- WORD: /\w+/
16
- NUMBER: /\d+/
17
- %import common.WS
18
- %ignore WS
19
- """
20
-
21
- parser = Lark(grammar, start='person', parser='earley')
22
-
23
- class PersonTransformer(Transformer):
24
- def person(self, items):
25
- return Person(name=items[1], age=items[3], city=items[5])
26
-
27
- def name(self, items):
28
- return items[0].value
29
-
30
- def age(self, items):
31
- return int(items[0].value)
32
-
33
- def city(self, items):
34
- return items[0].value
35
-
36
- class PersonToStringVisitor(Visitor):
37
- def __init__(self):
38
- self.result = []
39
-
40
- def person(self, tree):
41
- self.visit_children(tree)
42
- return " ".join(self.result)
43
-
44
- def name(self, tree):
45
- self.result.extend(["Name:", str(tree.children[0])])
46
-
47
- def age(self, tree):
48
- self.result.extend(["Age:", str(tree.children[0])])
49
-
50
- def city(self, tree):
51
- self.result.extend(["City:", str(tree.children[0])])
52
-
53
- transformer = PersonTransformer()
54
- to_string_visitor = PersonToStringVisitor()
55
-
56
- def from_string(cls, string):
57
- tree = parser.parse(string)
58
- return transformer.transform(tree)
59
-
60
- def to_string(self):
61
- from lark import Token
62
- tree = parser.parse(
63
- f"Name: {Token('WORD', self.name)} "
64
- f"Age: {Token('NUMBER', str(self.age))} "
65
- f"City: {Token('WORD', self.city)}"
66
- )
67
- return to_string_visitor.visit(tree)
68
-
69
- Person.from_string = classmethod(from_string)
70
- Person.to_string = to_string
71
-
72
- # Test functions remain the same as in the previous version
73
-
74
- # Test functions
75
- def test_from_string():
76
- person_str = "Name: John Age: 30 City: NewYork"
77
- person = Person.from_string(person_str)
78
- assert person.name == "John"
79
- assert person.age == 30
80
- assert person.city == "NewYork"
81
-
82
- def test_to_string():
83
- person = Person(name="Alice", age=25, city="London")
84
- person_str = person.to_string()
85
- assert person_str == "Name: Alice Age: 25 City: London"
86
-
87
- def test_roundtrip():
88
- original_str = "Name: Bob Age: 40 City: Paris"
89
- person = Person.from_string(original_str)
90
- regenerated_str = person.to_string()
91
- assert original_str == regenerated_str
92
-
93
- def test_different_values():
94
- person = Person(name="Charlie", age=35, city="Berlin")
95
- person_str = person.to_string()
96
- assert person_str == "Name: Charlie Age: 35 City: Berlin"
97
-
98
- def test_edge_cases():
99
- # Test with minimum age
100
- person = Person(name="Young", age=0, city="Baby")
101
- person_str = person.to_string()
102
- assert person_str == "Name: Young Age: 0 City: Baby"
103
-
104
- # Test with very long name and city
105
- long_name = "A" * 100
106
- long_city = "B" * 100
107
- person = Person(name=long_name, age=50, city=long_city)
108
- person_str = person.to_string()
109
- assert person_str == f"Name: {long_name} Age: 50 City: {long_city}"
110
-
111
- def test_invalid_input():
112
- with pytest.raises(Exception): # The exact exception may vary based on Lark's implementation
113
- Person.from_string("Invalid: Input")
114
-
115
- if __name__ == "__main__":
116
- pytest.main([__file__])
117
-