Skip to content

Commit ce2d299

Browse files
committed
feat: add class ProficencyScore
1 parent 5a4a0ce commit ce2d299

3 files changed

Lines changed: 318 additions & 1 deletion

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""ProficiencyScore module for OpenProficiency library."""
2+
3+
from enum import Enum
4+
from typing import Union
5+
6+
7+
class ProficiencyScoreName(Enum):
8+
"""Enum for proficiency score names."""
9+
UNAWARE = 0.0
10+
AWARE = 0.1
11+
FAMILIAR = 0.5
12+
PROFICIENT = 0.8
13+
PROFICIENT_WITH_EVIDENCE = 1.0
14+
15+
16+
class ProficiencyScore:
17+
"""Class representing a proficiency score for a topic."""
18+
19+
# Initializers
20+
def __init__(
21+
self,
22+
# Required
23+
topic_id: str,
24+
score: Union[float, ProficiencyScoreName]
25+
):
26+
# Required
27+
self.topic_id = topic_id
28+
self._set_score(score)
29+
30+
# Properties - Score
31+
@property
32+
def score(self) -> float:
33+
"""Get the score as a numeric value between 0.0 and 1.0."""
34+
return self._score
35+
36+
@score.setter
37+
def score(self, value: Union[float, ProficiencyScoreName]) -> None:
38+
"""Set the score numerically or using a ProficiencyScoreName enum."""
39+
self._set_score(value)
40+
41+
# Properties - Score
42+
@property
43+
def score_name(self) -> ProficiencyScoreName:
44+
"""Get the proficiency name as an enum value."""
45+
return self._get_name_from_score(self._score)
46+
47+
@score_name.setter
48+
def score_name(self, value: ProficiencyScoreName) -> None:
49+
"""Set the proficiency name using a ProficiencyScoreName enum."""
50+
if not isinstance(value, ProficiencyScoreName):
51+
raise ValueError(
52+
f"Name must be a ProficiencyScoreName enum, got {type(value)}")
53+
self._score = value.value
54+
55+
# Methods
56+
def _set_score(self, value: Union[float, ProficiencyScoreName]) -> None:
57+
"""Internal method to set score from numeric or enum value."""
58+
if isinstance(value, ProficiencyScoreName):
59+
self._score = value.value
60+
61+
elif isinstance(value, (int, float)):
62+
# Validate score is between 0.0 and 1.0
63+
if not (0.0 <= value <= 1.0):
64+
raise ValueError(
65+
f"Score must be between 0.0 and 1.0, got {value}")
66+
self._score = float(value)
67+
68+
else:
69+
raise ValueError(
70+
f"Score must be numeric or ProficiencyScoreName enum. Got type: '{type(value)}'")
71+
72+
def _get_name_from_score(self, score: float) -> ProficiencyScoreName:
73+
"""Internal method to determine proficiency name from numeric score."""
74+
if score <= 0.0:
75+
return ProficiencyScoreName.UNAWARE
76+
elif score <= 0.1:
77+
return ProficiencyScoreName.AWARE
78+
elif score <= 0.5:
79+
return ProficiencyScoreName.FAMILIAR
80+
elif score <= 0.8:
81+
return ProficiencyScoreName.PROFICIENT
82+
elif score <= 1.0:
83+
return ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE
84+
else:
85+
raise ValueError(f"Invalid score value: {score}")
86+
87+
def to_json(self) -> dict:
88+
"""Convert to a JSON-serializable dictionary."""
89+
return {
90+
"topic_id": self.topic_id,
91+
"score": self._score
92+
}
93+
94+
# Debugging
95+
96+
def __repr__(self) -> str:
97+
"""String representation of ProficiencyScore."""
98+
return f"ProficiencyScore(topic_id='{self.topic_id}', score={self._score}, name={self.score_name.name})"

openproficiency/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""OpenProficiency - Library to manage proficiency scores using topics and topic lists."""
22

33
from .Topic import Topic
4-
from .TopicList import TopicList
4+
from .TopicList import TopicList
5+
from .ProficiencyScore import ProficiencyScore, ProficiencyScoreName

tests/ProficiencyScore_test.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"""Tests for the ProficiencyScore class."""
2+
3+
from openproficiency import ProficiencyScore, ProficiencyScoreName
4+
5+
6+
class TestProficiencyScore:
7+
8+
# Initializers
9+
def test_init_with_numeric_score(self):
10+
"""Create a proficiency score with numeric value."""
11+
12+
# Arrange
13+
topic_id = "git-commit"
14+
score = 0.8
15+
16+
# Act
17+
ps = ProficiencyScore(topic_id=topic_id, score=score)
18+
19+
# Assert
20+
assert ps.topic_id == topic_id
21+
assert ps.score == score
22+
23+
def test_init_with_enum_score(self):
24+
"""Create a proficiency score with ProficiencyScoreName enum."""
25+
26+
# Arrange
27+
topic_id = "git-commit"
28+
score_name = ProficiencyScoreName.PROFICIENT
29+
30+
# Act
31+
ps = ProficiencyScore(topic_id=topic_id, score=score_name)
32+
33+
# Assert
34+
assert ps.topic_id == topic_id
35+
assert ps.score == 0.8
36+
37+
def test_init_invalid_score_too_low(self):
38+
"""Test that score below 0.0 raises ValueError."""
39+
40+
# Act & Assert
41+
try:
42+
ProficiencyScore(topic_id="test", score=-0.1)
43+
assert False, "Should have raised ValueError"
44+
except ValueError as e:
45+
assert "between 0.0 and 1.0" in str(e)
46+
47+
def test_init_invalid_score_too_high(self):
48+
"""Test that score above 1.0 raises ValueError."""
49+
50+
# Act & Assert
51+
try:
52+
ProficiencyScore(topic_id="test", score=1.1)
53+
assert False, "Should have raised ValueError"
54+
except ValueError as e:
55+
assert "between 0.0 and 1.0" in str(e)
56+
57+
def test_init_invalid_score_type(self):
58+
"""Test that invalid score type raises ValueError."""
59+
60+
# Act & Assert
61+
try:
62+
ProficiencyScore(topic_id="test", score="invalid")
63+
assert False, "Should have raised ValueError"
64+
except ValueError as e:
65+
assert "numeric or ProficiencyScoreName" in str(e)
66+
67+
# Properties - Score
68+
def test_score_getter(self):
69+
"""Test getting score property."""
70+
71+
# Arrange
72+
ps = ProficiencyScore(topic_id="test", score=0.5)
73+
74+
# Act & Assert
75+
assert ps.score == 0.5
76+
77+
def test_score_setter_numeric(self):
78+
"""Test setting score with numeric value."""
79+
80+
# Arrange
81+
ps = ProficiencyScore(topic_id="test", score=0.1)
82+
83+
# Act
84+
ps.score = 0.9
85+
86+
# Assert
87+
assert ps.score == 0.9
88+
89+
def test_score_setter_enum(self):
90+
"""Test setting score with enum value."""
91+
92+
# Arrange
93+
ps = ProficiencyScore(topic_id="test", score=0.1)
94+
95+
# Act
96+
ps.score = ProficiencyScoreName.FAMILIAR
97+
98+
# Assert
99+
assert ps.score == 0.5
100+
101+
def test_score_setter_invalid(self):
102+
"""Test setting invalid score raises ValueError."""
103+
104+
# Arrange
105+
ps = ProficiencyScore(topic_id="test", score=0.1)
106+
107+
# Act & Assert
108+
try:
109+
ps.score = 1.5
110+
assert False, "Should have raised ValueError"
111+
except ValueError:
112+
pass
113+
114+
# Properties - Score Name
115+
def test_score_name_unaware(self):
116+
"""Test score_name property for UNAWARE level."""
117+
118+
# Arrange
119+
ps = ProficiencyScore(topic_id="test", score=0.0)
120+
121+
# Act & Assert
122+
assert ps.score_name == ProficiencyScoreName.UNAWARE
123+
124+
def test_score_name_aware(self):
125+
"""Test score_name property for AWARE level."""
126+
127+
# Arrange
128+
ps = ProficiencyScore(topic_id="test", score=0.05)
129+
130+
# Act & Assert
131+
assert ps.score_name == ProficiencyScoreName.AWARE
132+
133+
def test_score_name_familiar(self):
134+
"""Test score_name property for FAMILIAR level."""
135+
136+
# Arrange
137+
ps = ProficiencyScore(topic_id="test", score=0.3)
138+
139+
# Act & Assert
140+
assert ps.score_name == ProficiencyScoreName.FAMILIAR
141+
142+
def test_score_name_proficient(self):
143+
"""Test score_name property for PROFICIENT level."""
144+
145+
# Arrange
146+
ps = ProficiencyScore(topic_id="test", score=0.8)
147+
148+
# Act & Assert
149+
assert ps.score_name == ProficiencyScoreName.PROFICIENT
150+
151+
def test_score_name_proficient_with_evidence(self):
152+
"""Test score_name property for PROFICIENT_WITH_EVIDENCE level."""
153+
154+
# Arrange
155+
ps = ProficiencyScore(topic_id="test", score=1.0)
156+
157+
# Act & Assert
158+
assert ps.score_name == ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE
159+
160+
def test_score_name_setter(self):
161+
"""Test setting score_name property."""
162+
163+
# Arrange
164+
ps = ProficiencyScore(topic_id="test", score=0.1)
165+
166+
# Act
167+
ps.score_name = ProficiencyScoreName.PROFICIENT
168+
169+
# Assert
170+
assert ps.score == 0.8
171+
172+
def test_score_name_setter_invalid_type(self):
173+
"""Test setting score_name with invalid type raises ValueError."""
174+
175+
# Arrange
176+
ps = ProficiencyScore(topic_id="test", score=0.1)
177+
178+
# Act & Assert
179+
try:
180+
ps.score_name = 0.5
181+
assert False, "Should have raised ValueError"
182+
except ValueError as e:
183+
assert "ProficiencyScoreName enum" in str(e)
184+
185+
# Methods
186+
187+
def test_to_json(self):
188+
"""Test conversion to JSON-serializable dictionary."""
189+
190+
# Arrange
191+
topic_id = "git-commit"
192+
score = 0.8
193+
ps = ProficiencyScore(topic_id=topic_id, score=score)
194+
195+
# Act
196+
json_dict = ps.to_json()
197+
198+
# Assert
199+
assert json_dict == {
200+
"topic_id": topic_id,
201+
"score": score
202+
}
203+
204+
# Debugging
205+
def test_repr(self):
206+
"""Test string representation of ProficiencyScore."""
207+
208+
# Arrange
209+
ps = ProficiencyScore(topic_id="git-commit", score=0.8)
210+
211+
# Act
212+
repr_str = repr(ps)
213+
214+
# Assert
215+
assert "ProficiencyScore" in repr_str
216+
assert "git-commit" in repr_str
217+
assert "0.8" in repr_str
218+
assert "PROFICIENT" in repr_str

0 commit comments

Comments
 (0)