Skip to content

Commit 9d7c967

Browse files
committed
Add Khiops dictionary rule API support
- add a `Rule` class - add `Dictionary.set_rule` and `Dictionary.get_rule` methods
1 parent e11dcc3 commit 9d7c967

File tree

2 files changed

+251
-1
lines changed

2 files changed

+251
-1
lines changed

khiops/core/dictionary.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
1414
"""
1515
import io
16+
import math
1617
import os
1718
import re
1819
import warnings
@@ -880,6 +881,124 @@ def remove_variable_block(
880881

881882
return removed_block
882883

884+
def _check_name(self, name, name_label):
885+
if not is_string_like(name):
886+
raise TypeError(type_error_message(name_label, name, str))
887+
if not name:
888+
raise ValueError(f"'{name_label}' must not be empty")
889+
890+
def _set_rule(self, rule, name, label, getter):
891+
# Perform input checks
892+
self._check_name(name, label)
893+
if not isinstance(rule, Rule):
894+
raise TypeError(type_error_message("rule", rule, Rule))
895+
896+
# Get the object of "name" and set the serialized rule on it
897+
getattr(self, getter)(name).rule = repr(rule)
898+
899+
def _get_rule(self, name, label, getter):
900+
# Perform input checks
901+
self._check_name(name, label)
902+
903+
# Get the serialized rule from the object of "name" and pack it into a
904+
# `Rule`
905+
return Rule(name=getattr(self, getter)(name).rule)
906+
907+
def set_variable_rule(self, variable_name, rule):
908+
"""Sets a rule on a specified variable in the dictionary
909+
910+
Parameters
911+
----------
912+
variable_name : str
913+
Name of the variable the rule is set on.
914+
rule : `Rule`
915+
The rule to be set on the variable whose name is ``variable_name``.
916+
917+
Raises
918+
------
919+
`TypeError`
920+
If ``rule`` is not of type `Rule`
921+
If ``variable_name`` is not of type `str`
922+
923+
`ValueError`
924+
If ``variable_name`` is the empty string
925+
926+
`KeyError`
927+
If no variable of name ``variable_name`` exists in the dictionary
928+
"""
929+
self._set_rule(rule, variable_name, "variable_name", "get_variable")
930+
931+
def set_variable_block_rule(self, variable_block_name, rule):
932+
"""Sets a rule on a specified variable block in the dictionary
933+
934+
Parameters
935+
----------
936+
variable_block_name : str
937+
Name of the variable block the rule is set on.
938+
rule : `Rule`
939+
The rule to be set on the variable block whose name is
940+
``variable_block_name``.
941+
942+
Raises
943+
------
944+
`TypeError`
945+
If ``rule`` is not of type `Rule`
946+
If ``variable_block_name`` is not of type `str`
947+
948+
`ValueError`
949+
If ``variable_block_name`` is the empty string
950+
951+
`KeyError`
952+
If no variable block of name ``variable_block_name`` exists in the
953+
dictionary
954+
"""
955+
self._set_rule(
956+
rule, variable_block_name, "variable_block_name", "get_variable_block"
957+
)
958+
959+
def get_variable_rule(self, variable_name):
960+
"""Gets `Rule` from a specified variable
961+
962+
Parameters
963+
----------
964+
variable_name : str
965+
Name of the variable the rule is set on.
966+
967+
Raises
968+
------
969+
`TypeError`
970+
If ``variable_name`` is not of type `str`
971+
972+
`ValueError`
973+
If ``variable_name`` is the empty string
974+
975+
`KeyError`
976+
If no variable of name ``variable_name`` exists in the dictionary
977+
"""
978+
self._get_rule(variable_name, "variable_name", "get_variable")
979+
980+
def get_variable_block_rule(self, variable_block_name):
981+
"""Gets `Rule` from a specified variable block
982+
983+
Parameters
984+
----------
985+
variable_block_name : str
986+
Name of the variable block_the rule is set on.
987+
988+
Raises
989+
------
990+
`TypeError`
991+
If ``variable_block_name`` is not of type `str`
992+
993+
`ValueError`
994+
If ``variable_block_name`` is the empty string
995+
996+
`KeyError`
997+
If no variable block of name ``variable_block_name`` exists in the
998+
dictionary
999+
"""
1000+
self._get_rule(variable_block_name, "variable_block_name", "get_variable_block")
1001+
8831002
def is_key_variable(self, variable):
8841003
"""Returns ``True`` if a variable belongs to this dictionary's key
8851004
@@ -978,8 +1097,10 @@ class Variable:
9781097
rule : str
9791098
Derivation rule or external table reference. Set to "" if there is no
9801099
rule associated to this variable. Examples:
1100+
9811101
- standard rule: "Sum(Var1, Var2)"
9821102
- reference rule: "[TableName]"
1103+
9831104
variable_block : `VariableBlock`
9841105
Block to which the variable belongs. Not set if the variable does not belong to
9851106
a block.
@@ -1386,6 +1507,134 @@ def write(self, writer):
13861507
writer.writeln("")
13871508

13881509

1510+
class Rule:
1511+
"""A rule of a variable in a Khiops dictionary
1512+
1513+
Parameters
1514+
----------
1515+
name : str
1516+
Name of the rule.
1517+
It is intepreted as the verbatim representation of an entire rule if and
1518+
only if:
1519+
1520+
- it starts with an UpperCamelCase string, followed by a
1521+
parenthesized block (...)
1522+
- ``operands`` is empty
1523+
1524+
It is intepreted as a reference rule if and only if:
1525+
1526+
- the first condition above does *not* apply
1527+
- the second condition above applies
1528+
1529+
operands : tuple of operands
1530+
Each operand can have one of the following types:
1531+
1532+
- str
1533+
- int
1534+
- float
1535+
- ``Variable``
1536+
- ``Rule``
1537+
1538+
If no operand is specified, then the rule is:
1539+
1540+
- a standard rule if ``name`` is the verbatim representation of an
1541+
entire rule
1542+
- a reference rule if ``name`` does not satisfy the condition above
1543+
1544+
Attributes
1545+
----------
1546+
name : str
1547+
Name of the rule.
1548+
operands : tuple of operands
1549+
Each operand has one of the following types:
1550+
1551+
- str
1552+
- int
1553+
- float
1554+
- ``Variable``
1555+
- ``Rule``
1556+
"""
1557+
1558+
def __init__(self, name, *operands):
1559+
"""See class docstring"""
1560+
1561+
# Check the types of the parameters
1562+
if not isinstance(name, str):
1563+
raise TypeError(type_error_message("name", name, str))
1564+
for operand in operands:
1565+
if not isinstance(operand, (str, int, float, Variable, Rule)):
1566+
raise TypeError(
1567+
type_error_message(
1568+
f"Operand '{operand}'", operand, str, int, float, Variable, Rule
1569+
)
1570+
)
1571+
1572+
# name must not be empty
1573+
if not name:
1574+
raise ValueError(f"'name' must be a non-empty string")
1575+
1576+
self.name = name
1577+
self.operands = operands
1578+
1579+
def __repr__(self):
1580+
stream = io.BytesIO()
1581+
writer = KhiopsOutputWriter(stream)
1582+
self.write(writer)
1583+
return str(stream.getvalue(), encoding="utf8", errors="replace")
1584+
1585+
def copy(self):
1586+
"""Copies this rule instance
1587+
1588+
Returns
1589+
-------
1590+
`Rule`
1591+
A copy of this instance
1592+
"""
1593+
return Rule(self.name, *self.operands)
1594+
1595+
def write(self, writer):
1596+
"""Writes the rule to a file writer in the ``.kdic`` format
1597+
1598+
Parameters
1599+
----------
1600+
writer : `.KhiopsOutputWriter`
1601+
Output writer.
1602+
"""
1603+
# Check file object type
1604+
if not isinstance(writer, KhiopsOutputWriter):
1605+
raise TypeError(type_error_message("writer", writer, KhiopsOutputWriter))
1606+
1607+
# Write standard rule
1608+
rule_regex = re.compile(r"^[A-Z]([a-zA-Z]*)\(.*\)")
1609+
if self.operands:
1610+
n_operands = len(self.operands)
1611+
writer.write(f"{_format_name(self.name)}(")
1612+
for i, operand in enumerate(self.operands):
1613+
# Write operand, according to its type
1614+
# Variable operands have their name written only
1615+
if isinstance(operand, Rule):
1616+
operand.write(writer)
1617+
elif isinstance(operand, Variable):
1618+
writer.write(_format_name(operand.name))
1619+
elif isinstance(operand, str):
1620+
writer.write(f'"{operand}"')
1621+
elif math.isnan(float(operand)):
1622+
writer.write("#Missing")
1623+
else:
1624+
writer.write(f"{operand}")
1625+
if i < n_operands - 1:
1626+
writer.write(", ")
1627+
writer.write(")")
1628+
1629+
# Write verbatim-given rule
1630+
elif rule_regex.match(self.name):
1631+
writer.write(self.name)
1632+
1633+
# Write rule as a reference rule
1634+
else:
1635+
writer.write(f"[{self.name}]")
1636+
1637+
13891638
class MetaData:
13901639
"""A metadata container for a dictionary, a variable or variable block
13911640

tests/test_core.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1878,8 +1878,9 @@ def test_dictionary_accessors(self):
18781878
dictionary_copy.get_variable_block(block.name)
18791879

18801880
# Set the block as non-native add, and remove it
1881-
block.rule = "SomeBlockCreatingRule()"
18821881
dictionary_copy.add_variable_block(block)
1882+
block_rule = kh.Rule("SomeBlockCreatingRule()")
1883+
dictionary_copy.set_variable_block_rule(block.name, block_rule)
18831884
self.assertEqual(block, dictionary_copy.get_variable_block(block.name))
18841885
removed_block = dictionary_copy.remove_variable_block(
18851886
block.name,

0 commit comments

Comments
 (0)