Skip to content

Commit 92b835b

Browse files
committed
Merge branch 'release/1.2.0.1' into main
2 parents 72fc3b6 + 54574b7 commit 92b835b

File tree

7 files changed

+155
-44
lines changed

7 files changed

+155
-44
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,5 @@ dmypy.json
129129
.pyre/
130130

131131
# Editor
132-
.idea
132+
.idea
133+
/dev_env/

CHANGELOG.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,81 @@
11
# Pyttman Changelog
22

3+
# V 1.2.0.1
4+
5+
This is a hotfix release, fixing an issue with EntityFields with `as_list`
6+
configured, where it would append an infinite amount of matching strings,
7+
when in fact, it should only add as many as defined in `span`.
8+
9+
10+
### **🐛 Splatted bugs and corrected issues**
11+
* **Fixes [#66](https://github.com/dotchetter/Pyttman/issues/66)**
12+
13+
14+
15+
16+
# V 1.2.0
17+
This is a minor release, containing new improved features, some changes
18+
and bug fixes, where the first point on the News list is the reason for this release being a minor release.
19+
20+
### :star2: News
21+
* **Accessing the `Ability` instance in an `Intent` class**
22+
23+
in Pyttman, the relationship between an Ability and Intent classes have
24+
been parent->child, with no way to access the Ability instance from the Intent.
25+
This is now possible, which allows for accessing methods which Intents may share.
26+
To access the Ability from an Intent, you can do so with `self.ability` in all Intent methods.
27+
This is, however, not a breaking change: backwards compatibility is still supported, meaning
28+
apps with `EntityParser` inner classes will continue to work normally.
29+
30+
31+
* **`BoolEntityField` defaults to False**
32+
33+
Instead of defaulting to `None`, the `BoolEntityField` class now more appropriately
34+
defaults to `False` if the sought pattern isn't found in the message.
35+
36+
37+
* **New mode in Pyttman CLI: `shell`**
38+
39+
The `shell` mode allows you to open your Pyttman app bootstrapped with
40+
dependencies and environment loaded up, in an interactive shell. This is
41+
useful where you want to try your classes using an interactive shell.
42+
This feature is invoked using `pyttman shell <app name>`.
43+
44+
45+
### 👀 Changes
46+
47+
* **Removed pytz dependency from the library**
48+
49+
50+
* **Internal refactoring and code cleanup**
51+
52+
53+
* **The inner class "EntityParser" is no longer used and is unsupported.**
54+
55+
The inner class `EntityParser` inside `Intent` classes was optional, and
56+
added EntityParser functionality to an intent class. This class proved
57+
to be redundant however, as the EntityParser API continues to evolve.
58+
The need for an inner class is thus removed, and `EntityField` classes
59+
are instead directly declared inside the `Intent` class as class fields,
60+
more resembling other declarative API:s.
61+
62+
```python
63+
# Old
64+
class SomeIntent(Intent):
65+
class EntityParser:
66+
name = StringEntityField()
67+
68+
69+
# New
70+
class SomeIntent(Intent):
71+
name = StringEntityField()
72+
```
73+
74+
75+
76+
### **🐛 Splatted bugs and corrected issues**
77+
* **Fixes [#64](https://github.com/dotchetter/Pyttman/issues/63)**
78+
379

480

581
# v 1.1.12

pyttman/core/entity_parsing/fields.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import inspect
22
from abc import ABC
3-
from functools import singledispatchmethod
43
from typing import Any, Sequence, Type
54

65
from pyttman.core.entity_parsing.identifiers import IntegerIdentifier, \
@@ -27,7 +26,6 @@ class EntityFieldBase(EntityFieldValueParser, ABC):
2726

2827
def __init__(self,
2928
identifier: Type[Identifier] | None = None,
30-
as_list: bool = False,
3129
default: Any = None,
3230
**kwargs):
3331
"""
@@ -54,9 +52,7 @@ def __init__(self,
5452
f"a valid value for "
5553
f"'type_cls'.")
5654

57-
self.as_list = as_list
5855
_default_arg = default if default is not None else self.default
59-
6056
super().__init__(identifier=identifier or self.identifier_cls,
6157
default=_default_arg, **kwargs)
6258

pyttman/core/entity_parsing/parsers.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self,
2828
span: int | typing.Callable = 0,
2929
identifier: Type[Identifier] | None = None,
3030
exclude: typing.Iterable[str] = None,
31+
as_list: bool = False,
3132
**kwargs):
3233

3334
if hasattr(self, "value"):
@@ -45,6 +46,7 @@ def __init__(self,
4546
self.default = default if default is not None else None
4647
self.identifier = identifier
4748
self.span = span
49+
self.as_list = as_list
4850
self._properties_for_evaluation = {
4951
"prefixes": self.prefixes,
5052
"suffixes": self.suffixes,
@@ -119,7 +121,9 @@ def parse_message(self, message: MessageMixin,
119121
common_occurrences = tuple(
120122
OrderedSet(casefolded_msg).intersection(self.valid_strings))
121123

122-
for word in common_occurrences:
124+
for i, word in enumerate(common_occurrences):
125+
if i > self.span and not self.as_list:
126+
break
123127
word_index = casefolded_msg.index(word)
124128
output.append(message.content[word_index])
125129

@@ -129,6 +133,14 @@ def parse_message(self, message: MessageMixin,
129133
self.value = Entity(output.pop())
130134
else:
131135
self.value = Entity(self.default, is_fallback_default=True)
136+
137+
if self.value:
138+
entity = self.value
139+
if isinstance(entity.value, list):
140+
[message.content.remove(i) for i in entity.value]
141+
entity.index_in_message += len(entity.value)
142+
elif isinstance(entity.value, str):
143+
message.content.remove(self.value)
132144
return
133145

134146
if self.truncates_message_in_parsing is False:
@@ -137,7 +149,6 @@ def parse_message(self, message: MessageMixin,
137149
for i, _ in enumerate(message.content):
138150
parsed_entity: Entity = self._identify_value(message,
139151
start_index=i)
140-
141152
# An entity has been identified, and it's unique.
142153
if parsed_entity is not None and memoization.get(
143154
parsed_entity.index_in_message) is None:
@@ -292,43 +303,43 @@ def _identify_value(self, message: MessageMixin,
292303
# for each span iteration as the walk in the message progresses.
293304
# If an Identifier is does not comply with a string, the walk is
294305
# cancelled.
295-
if parsed_entity is not None:
296-
while parsed_entity.value.casefold() in self.exclude:
297-
parsed_entity.index_in_message += 1
298-
# Traverse the message for as long as the current found
299-
# entity is in the 'exclude' tuple. If the end of message
300-
# is reached, quietly break the loop.
301-
try:
302-
parsed_entity.value = message.content[
303-
parsed_entity.index_in_message]
304-
except IndexError:
305-
return None
306-
307-
current_index = parsed_entity.index_in_message
306+
if parsed_entity is None:
307+
return parsed_entity
308+
309+
while parsed_entity.value.casefold() in self.exclude:
310+
parsed_entity.index_in_message += 1
311+
# Traverse the message for as long as the current found
312+
# entity is in the 'exclude' tuple. If the end of message
313+
# is reached, quietly break the loop.
314+
try:
315+
parsed_entity.value = message.content[
316+
parsed_entity.index_in_message]
317+
except IndexError:
318+
return None
308319

309-
for i in range(1, self.span):
310-
try:
311-
current_index += 1
312-
if self.identifier:
313-
identifier_object: Identifier = self.identifier(
314-
start_index=current_index)
315-
# Identifier did not find
316-
span_entity = identifier_object.try_identify_entity(
317-
message)
318-
if span_entity is None or span_entity.index_in_message != current_index:
319-
break
320-
span_value = span_entity.value
321-
else:
322-
span_value = message.content[current_index]
323-
324-
# There are not enough elements in message.content to walk
325-
# as far as the span property requests - abort.
326-
except IndexError:
327-
break
320+
# Now, add words for as long as `span` allows us to iterate.
321+
for i in range(1, self.span):
322+
parsed_entity.index_in_message += 1
323+
try:
324+
if self.identifier:
325+
identifier_object: Identifier = self.identifier(
326+
start_index=parsed_entity.index_in_message)
327+
# Identifier did not find
328+
span_entity = identifier_object.try_identify_entity(
329+
message)
330+
if span_entity is None or span_entity.index_in_message != parsed_entity.index_in_message:
331+
break
332+
span_value = span_entity.value
328333
else:
329-
if span_value not in self.exclude:
330-
print(f"{span_value} is not in {self.exclude} for {self}")
331-
parsed_entity.value += f" {span_value}"
334+
span_value = message.content[parsed_entity.index_in_message]
335+
336+
# There are not enough elements in message.content to walk
337+
# as far as the span property requests - abort.
338+
except IndexError:
339+
break
340+
else:
341+
if span_value not in self.exclude:
342+
parsed_entity.value += f" {span_value}"
332343
return parsed_entity
333344

334345

pyttman/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11

2-
__version__ = "1.2.0"
2+
__version__ = "1.2.0.1"

tests/core/entity_parsing/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class ImplementedTestIntent(Intent):
2020
with the intent to be tested.
2121
"""
2222
def respond(self, message: Message) -> Reply | ReplyStream:
23-
return Reply(f"'{self.__class__.__name__}' matched a message")
23+
return Reply(f"'{self.__class__.__name__}' "
24+
f"matched message: {message}")
2425

2526

2627
class PyttmanInternalTestBaseCase(PyttmanInternalBaseTestCase):

tests/core/entity_parsing/test_entity_fields/test_entity_parsing.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,36 @@ class IntentClass(ImplementedTestIntent):
3838
# Test that both SoundCloud and Spotify are found despite being
3939
# misspelled in comparison to the mock message above
4040
platform_all = TextEntityField(as_list=True,
41+
span=2,
4142
valid_strings=("spOtifY",
4243
"soundcloud"))
4344

4445

46+
class PyttmanIntentInternalEntityParserTestTwoTextFields(
47+
PyttmanInternalTestBaseCase
48+
):
49+
process_message = True
50+
mock_message = Message("Adam and Eve were out walking on green grass")
51+
expected_entities = {
52+
"person_one": "Adam",
53+
"person_two": "Eve",
54+
"grass_color": "green"
55+
}
56+
57+
class IntentClass(ImplementedTestIntent):
58+
"""
59+
Test a combination of custom Identifier class for a TextEntityField
60+
with valid_strings, message_contains all in combinations.
61+
"""
62+
valid_strings = ("Adam", "Eve")
63+
lead = ("walking",)
64+
person_one = TextEntityField(identifier=CapitalizedIdentifier,
65+
valid_strings=valid_strings)
66+
person_two = TextEntityField(identifier=CapitalizedIdentifier,
67+
valid_strings=valid_strings)
68+
grass_color = TextEntityField(suffixes=("grass",))
69+
70+
4571
class PyttmanIntentInternalEntityParserTestBookKeeperApp(
4672
PyttmanInternalTestBaseCase
4773
):

0 commit comments

Comments
 (0)