Skip to content

Commit 11bcafd

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

File tree

2 files changed

+253
-1
lines changed

2 files changed

+253
-1
lines changed

khiops/core/dictionary.py

Lines changed: 251 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,131 @@ def remove_variable_block(
880881

881882
return removed_block
882883

884+
def set_variable_rule(self, variable_name, rule):
885+
"""Sets a rule on a specified variable in the dictionary
886+
887+
Parameters
888+
----------
889+
variable_name : str
890+
Name of the variable the rule is set on.
891+
rule : `Rule`
892+
The rule to be set on the variable whose name is ``variable_name``.
893+
894+
Raises
895+
------
896+
`TypeError`
897+
If ``rule`` is not of type `Rule`
898+
If ``variable_name`` is not of type `str`
899+
900+
`ValueError`
901+
If ``variable_name`` is the empty string
902+
903+
`KeyError`
904+
If no variable of name ``variable_name`` exists in the dictionary
905+
"""
906+
if not is_string_like(variable_name):
907+
raise TypeError(
908+
type_error_message("variable_name", variable_name, "string-like")
909+
)
910+
if not variable_name:
911+
raise ValueError("'variable_name' must not be empty")
912+
if not isinstance(rule, Rule):
913+
raise TypeError(type_error_message("rule", rule, Rule))
914+
self.get_variable(variable_name).rule = repr(rule)
915+
916+
def set_variable_block_rule(self, variable_block_name, rule):
917+
"""Sets a rule on a specified variable block in the dictionary
918+
919+
Parameters
920+
----------
921+
variable_block_name : str
922+
Name of the variable block the rule is set on.
923+
rule : `Rule`
924+
The rule to be set on the variable block whose name is
925+
``variable_block_name``.
926+
927+
Raises
928+
------
929+
`TypeError`
930+
If ``rule`` is not of type `Rule`
931+
If ``variable_block_name`` is not of type `str`
932+
933+
`ValueError`
934+
If ``variable_block_name`` is the empty string
935+
936+
`KeyError`
937+
If no variable block of name ``variable_block_name`` exists in the
938+
dictionary
939+
"""
940+
if not is_string_like(variable_block_name):
941+
raise TypeError(
942+
type_error_message(
943+
"variable_block_name", variable_block_name, "string-like"
944+
)
945+
)
946+
if not variable_block_name:
947+
raise ValueError("'variable_block_name' must not be empty")
948+
if not isinstance(rule, Rule):
949+
raise TypeError(type_error_message("rule", rule, Rule))
950+
self.get_variable_block(variable_block_name).rule = repr(rule)
951+
952+
def get_variable_rule(self, variable_name):
953+
"""Gets `Rule` from a specified variable
954+
955+
Parameters
956+
----------
957+
variable_name : str
958+
Name of the variable the rule is set on.
959+
960+
Raises
961+
------
962+
`TypeError`
963+
If ``variable_name`` is not of type `str`
964+
965+
`ValueError`
966+
If ``variable_name`` is the empty string
967+
968+
`KeyError`
969+
If no variable of name ``variable_name`` exists in the dictionary
970+
"""
971+
if not is_string_like(variable_name):
972+
raise TypeError(
973+
type_error_message("variable_name", variable_name, "string-like")
974+
)
975+
if not variable_name:
976+
raise ValueError("'variable_name' must not be empty")
977+
return Rule(name=self.get_variable(variable_name).rule)
978+
979+
def get_variable_block_rule(self, variable_block_name):
980+
"""Gets `Rule` from a specified variable block
981+
982+
Parameters
983+
----------
984+
variable_block_name : str
985+
Name of the variable block_the rule is set on.
986+
987+
Raises
988+
------
989+
`TypeError`
990+
If ``variable_block_name`` is not of type `str`
991+
992+
`ValueError`
993+
If ``variable_block_name`` is the empty string
994+
995+
`KeyError`
996+
If no variable block of name ``variable_block_name`` exists in the
997+
dictionary
998+
"""
999+
if not is_string_like(variable_block_name):
1000+
raise TypeError(
1001+
type_error_message(
1002+
"variable_block_name", variable_block_name, "string-like"
1003+
)
1004+
)
1005+
if not variable_block_name:
1006+
raise ValueError("'variable_block_name' must not be empty")
1007+
return Rule(name=self.get_variable_block(variable_block_name).rule)
1008+
8831009
def is_key_variable(self, variable):
8841010
"""Returns ``True`` if a variable belongs to this dictionary's key
8851011
@@ -989,8 +1115,10 @@ class Variable:
9891115
rule : str
9901116
Derivation rule or external table reference. Set to "" if there is no
9911117
rule associated to this variable. Examples:
1118+
9921119
- standard rule: "Sum(Var1, Var2)"
9931120
- reference rule: "[TableName]"
1121+
9941122
variable_block : `VariableBlock`
9951123
Block to which the variable belongs. Not set if the variable does not belong to
9961124
a block.
@@ -1402,6 +1530,129 @@ def write(self, writer):
14021530
writer.writeln("")
14031531

14041532

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

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)