From ee18ebb71b53ce98d9c28579c113c53f1b37a653 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sat, 19 Jun 2021 23:03:44 +0900 Subject: [PATCH 01/13] initial prototype --- pypict/builder/__init__.py | 3 + pypict/builder/_constraint.py | 79 +++++++++++++++++++ pypict/builder/_export.py | 14 ++++ pypict/builder/_model.py | 60 +++++++++++++++ pypict/builder/_parameter.py | 113 ++++++++++++++++++++++++++++ tests/builder_tests/__init__.py | 0 tests/builder_tests/test_builder.py | 18 +++++ 7 files changed, 287 insertions(+) create mode 100644 pypict/builder/__init__.py create mode 100644 pypict/builder/_constraint.py create mode 100644 pypict/builder/_export.py create mode 100644 pypict/builder/_model.py create mode 100644 pypict/builder/_parameter.py create mode 100644 tests/builder_tests/__init__.py create mode 100644 tests/builder_tests/test_builder.py diff --git a/pypict/builder/__init__.py b/pypict/builder/__init__.py new file mode 100644 index 0000000..c9f335c --- /dev/null +++ b/pypict/builder/__init__.py @@ -0,0 +1,3 @@ +from ._model import Model +from ._parameter import Parameter +from ._constraint import AND, OR, NOT, IF \ No newline at end of file diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py new file mode 100644 index 0000000..02b594b --- /dev/null +++ b/pypict/builder/_constraint.py @@ -0,0 +1,79 @@ + +from pypict.builder._export import _literal + + +class _Constraint: + def to_string(self): + raise NotImplemented + + def __str__(self): + return self.to_string() + + def __repr__(self): + return f'' + + +class _Relation(_Constraint): + def __init__(self, param, op, value_or_param): + self._param = param + self._op = op + self._value_or_param = value_or_param + + def to_string(self): + return f'{_literal(self._param)} {self._op} {_literal(self._value_or_param)}' + + +class _LogicalOp(_Constraint): + _op = None + + def __init__(self, consts): + self._consts = consts + + def to_string(self): + return '(' + f' {self._op} '.join([str(x) for x in self._consts]) + ')' + + +class AND(_LogicalOp): + _op = 'AND' + + +class OR(_LogicalOp): + _op = 'OR' + + +class NOT(_Constraint): + def __init__(self, const): + self._const = const + + def to_string(self): + return f'NOT {self._const}' + + +class IF(_Constraint): + def __init__(self, const): + self._const = const + self._then = None + self._else = None + + def THEN(self, const): + if self._then is not None: + raise ValueError + self._then = const + return self + + def ELSE(self, const): + if self._then is None: + raise ValueError + if self._else is None: + raise ValueError + self._else = const + + def to_string(self): + if self._then is None: + raise ValueError + return ( + f'IF {self._const}' + + f'\nTHEN {self._then}' + + (f'\nELSE {self._else}' if self._else else '') + + ';' + ) diff --git a/pypict/builder/_export.py b/pypict/builder/_export.py new file mode 100644 index 0000000..81117cc --- /dev/null +++ b/pypict/builder/_export.py @@ -0,0 +1,14 @@ +from pypict import builder + + +def _literal(v): + if isinstance(v, builder.Parameter): + return f'[{v._name}]' + elif isinstance(v, str): + return f'"{v}"' # TODO check? + else: + return str(v) + + +def _ValueSet(values): + return '{ ' + ','.join([_literal(x) for x in values]) + ' }' diff --git a/pypict/builder/_model.py b/pypict/builder/_model.py new file mode 100644 index 0000000..98c76c0 --- /dev/null +++ b/pypict/builder/_model.py @@ -0,0 +1,60 @@ +from typing import Iterable, Iterator, List, Optional, Tuple, Type, Union + +from pypict.builder._parameter import Parameter +from pypict.builder._constraint import _Constraint + + +class Model: + def __init__(self): + self._params = [] + self._submodels = [] + self._constraints = [] + + def parameters(self, *params: Parameter) -> 'Model': + self._params += params + return self + + def submodel(self, params: Tuple[Parameter], order: Optional[int] = None) -> 'Model': + self._submodels.append(_SubModel(params, order)) + return self + + def constraints(self, *constraints: _Constraint) -> 'Model': + self._constraints += constraints + return self + + def to_string(self): + lines = [] + + # Parameter definitions + if len(self._params) == 0: + raise ValueError('no parameters are added to the model') + for p in self._params: + lines.append(p.to_string()) + lines.append('') + + # Sub-model definitions + if len(self._submodels) != 0: + for s in self._submodels: + lines.append(s.to_string()) + lines.append('') + + # Constraint definitions + if len(self._constraints) != 0: + for c in self._constraints: + lines.append(c.to_string()) + + return '\n'.join(lines) + + +class _SubModel: + def __init__(self, params, order=None): + self._params = params + self._order = order + + def to_string(self): + return ( + '{ ' + + ', '.join([param._name for param in self._params]) + + ' }' + + '' if self._order is None else f' @ {self._order}' + ) \ No newline at end of file diff --git a/pypict/builder/_parameter.py b/pypict/builder/_parameter.py new file mode 100644 index 0000000..81ca3b0 --- /dev/null +++ b/pypict/builder/_parameter.py @@ -0,0 +1,113 @@ +import numbers +from typing import Iterable, Iterator, List, Optional, Tuple, Type, Union + +from pypict.builder._constraint import _Relation +from pypict.builder._export import _literal, _ValueSet + + +NumericType = numbers.Real +StringType = str +DataTypes = Union[NumericType, StringType] + + +class Parameter: + def __init__( + self, + name: str, + values: Iterable[Union[DataTypes, Tuple[DataTypes, int]]], + *, + aliases: Optional[Iterable[str]] = None): + if aliases is None: + aliases = [] + numeric = self._check_values(values) + + self._check_valid_name(name) + for x in aliases: + self._check_valid_name(x) + + self._name = name + self._values = values + self._aliases = aliases + self._numeric = numeric + + @staticmethod + def _check_valid_name(name): + if '[' in name or ']' in name: + raise ValueError(f'invalid parameter name: {name}') + + @staticmethod + def _check_values( + values: Iterable[Union[DataTypes, Tuple[DataTypes, int]]]) -> bool: + numeric = True + for i, x in enumerate(values): + if isinstance(x, tuple): + value, weight = x + else: + value, weight = x, 1 + if isinstance(value, NumericType): + pass + elif isinstance(value, StringType): + numeric = False + else: + raise ValueError( + f'unsupported data type at index {i}: {value} ({type(value)})') + if not isinstance(weight, int): + raise ValueError + return numeric + + def __str__(self): + return self.to_string() + + def __repr__(self): + return self.to_string() # TODO + + def to_string(self, separator=','): + values = f'{separator} '.join([str(x) for x in self._values]) + return f'{self._name}: {values}' + + def _check_operand(self, other, *, allow_string=False): + if isinstance(other, Parameter): + if self._numeric != other._numeric: + raise ValueError + elif isinstance(other, NumericType): + if not self._numeric: + raise ValueError + elif isinstance(other, StringType): + if not allow_string: + raise ValueError + if self._numeric: + raise ValueError + else: + raise ValueError + + def __eq__(self, other: Union[DataTypes, 'Parameter']): + self._check_operand(other, allow_string=True) + return _Relation(self, '=', other) + + def __ne__(self, other: Union[DataTypes, 'Parameter']): + self._check_operand(other, allow_string=True) + return _Relation(self, '<>', other) + + def __gt__(self, other: Union[DataTypes, 'Parameter']): + self._check_operand(other) + return _Relation(self, '>', other) + + def __ge__(self, other: Union[DataTypes, 'Parameter']): + self._check_operand(other) + return _Relation(self, '>=', other) + + def __lt__(self, other: Union[DataTypes, 'Parameter']): + self._check_operand(other) + return _Relation(self, '<', other) + + def __le__(self, other: Union[DataTypes, 'Parameter']): + self._check_operand(other) + return _Relation(self, '<=', other) + + def __in__(self, values: Iterable[DataTypes]): + return _Relation(self, 'IN', _ValueSet(values)) + + def like(self, value): + if self._dtype != str: + raise ValueError + return _Relation(self, 'LIKE', value) diff --git a/tests/builder_tests/__init__.py b/tests/builder_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/builder_tests/test_builder.py b/tests/builder_tests/test_builder.py new file mode 100644 index 0000000..c839ead --- /dev/null +++ b/tests/builder_tests/test_builder.py @@ -0,0 +1,18 @@ +import unittest + +from pypict.builder import Model, Parameter +from pypict.builder import IF + +class TestBuilder(unittest.TestCase): + def test_basic_usecase(self): + type = Parameter('Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) + size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) + filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) + + m = Model() + m.parameters(type, size, filesys) + m.constraints( + IF(filesys == 'FAT').THEN(size <= 4096), + IF(filesys == 'FAT32').THEN(size <= 32000), + ) + print(m.to_string()) \ No newline at end of file From 1969f53ba8c871e237789823f75382020ed9b350 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sat, 19 Jun 2021 23:45:20 +0900 Subject: [PATCH 02/13] wip --- example/example_builder.py | 30 ++++++++++++++++++ pypict/builder/_constraint.py | 57 +++++++++++++++++++++++++---------- pypict/builder/_export.py | 14 --------- pypict/builder/_model.py | 2 +- pypict/builder/_parameter.py | 11 ++++--- 5 files changed, 79 insertions(+), 35 deletions(-) create mode 100644 example/example_builder.py diff --git a/example/example_builder.py b/example/example_builder.py new file mode 100644 index 0000000..34e453a --- /dev/null +++ b/example/example_builder.py @@ -0,0 +1,30 @@ +from ast import Param +from pypict.builder import Model, Parameter, IF, AND, OR, NOT + + +type = Parameter('Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) +size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) +method = Parameter('Format method', ['Quick', 'Slow']) +filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) +cluster = Parameter('Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) +compression = Parameter('Compression', ['On', 'Off']) + +model = Model().parameters( + type, size, method, filesys, cluster, compression +).constraints( + IF(filesys == 'FAT').THEN(size <= 4096), + IF(filesys == 'FAT32').THEN(size <= 32000), + size < 10000, + compression == 'OFF', + filesys.like('FAT*'), + IF(cluster.in_(512, 1024, 2048)).THEN(compression == 'off'), + IF(filesys.in_('FAT', 'FAT32')).THEN(compression == 'off'), + + IF( + OR( + filesys != 'NTFS', + AND(filesys == 'NTFS', cluster > 4096), + ) + ).THEN(compression == 'Off') +) +print(model.to_string()) \ No newline at end of file diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py index 02b594b..dcc1d2b 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/builder/_constraint.py @@ -1,15 +1,15 @@ -from pypict.builder._export import _literal +import pypict.builder class _Constraint: - def to_string(self): + def to_string(self) -> str: raise NotImplemented - def __str__(self): + def __str__(self) -> str: return self.to_string() - def __repr__(self): + def __repr__(self) -> str: return f'' @@ -19,17 +19,20 @@ def __init__(self, param, op, value_or_param): self._op = op self._value_or_param = value_or_param - def to_string(self): + def to_string(self) -> str: return f'{_literal(self._param)} {self._op} {_literal(self._value_or_param)}' class _LogicalOp(_Constraint): - _op = None + _op: str = None # to be overridden - def __init__(self, consts): + def __init__(self, *consts: _Constraint): + for const in consts: + if not isinstance(const, _Constraint): + raise ValueError self._consts = consts - def to_string(self): + def to_string(self) -> str: return '(' + f' {self._op} '.join([str(x) for x in self._consts]) + ')' @@ -42,38 +45,60 @@ class OR(_LogicalOp): class NOT(_Constraint): - def __init__(self, const): + def __init__(self, const: _Constraint): + if not isinstance(const, _Constraint): + raise ValueError self._const = const - def to_string(self): + def to_string(self) -> str: return f'NOT {self._const}' class IF(_Constraint): - def __init__(self, const): + def __init__(self, const: _Constraint): + if not isinstance(const, _Constraint): + raise ValueError self._const = const self._then = None self._else = None - def THEN(self, const): + def THEN(self, const: _Constraint) -> 'IF': if self._then is not None: raise ValueError self._then = const return self - def ELSE(self, const): + def ELSE(self, const: _Constraint) -> 'IF': if self._then is None: raise ValueError if self._else is None: raise ValueError self._else = const - def to_string(self): + def to_string(self) -> str: if self._then is None: raise ValueError return ( f'IF {self._const}' + f'\nTHEN {self._then}' + - (f'\nELSE {self._else}' if self._else else '') + - ';' + (f'\nELSE {self._else}' if self._else else '') ) + + +class _ValueSet: + def __init__(self, values): + self._values = values + + def to_string(self) -> str: + return '{ ' + ', '.join([_literal(x) for x in self._values]) + ' }' + + +def _literal(v): + if isinstance(v, pypict.builder.Parameter): + return f'[{v._name}]' + elif isinstance(v, _ValueSet): + return v.to_string() + elif isinstance(v, str): + return f'"{v}"' # TODO check? + else: + return str(v) \ No newline at end of file diff --git a/pypict/builder/_export.py b/pypict/builder/_export.py index 81117cc..e69de29 100644 --- a/pypict/builder/_export.py +++ b/pypict/builder/_export.py @@ -1,14 +0,0 @@ -from pypict import builder - - -def _literal(v): - if isinstance(v, builder.Parameter): - return f'[{v._name}]' - elif isinstance(v, str): - return f'"{v}"' # TODO check? - else: - return str(v) - - -def _ValueSet(values): - return '{ ' + ','.join([_literal(x) for x in values]) + ' }' diff --git a/pypict/builder/_model.py b/pypict/builder/_model.py index 98c76c0..374d656 100644 --- a/pypict/builder/_model.py +++ b/pypict/builder/_model.py @@ -41,7 +41,7 @@ def to_string(self): # Constraint definitions if len(self._constraints) != 0: for c in self._constraints: - lines.append(c.to_string()) + lines.append(c.to_string() + ';') return '\n'.join(lines) diff --git a/pypict/builder/_parameter.py b/pypict/builder/_parameter.py index 81ca3b0..905da10 100644 --- a/pypict/builder/_parameter.py +++ b/pypict/builder/_parameter.py @@ -1,8 +1,7 @@ import numbers from typing import Iterable, Iterator, List, Optional, Tuple, Type, Union -from pypict.builder._constraint import _Relation -from pypict.builder._export import _literal, _ValueSet +from pypict.builder._constraint import _Relation, _ValueSet NumericType = numbers.Real @@ -104,10 +103,14 @@ def __le__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) return _Relation(self, '<=', other) - def __in__(self, values: Iterable[DataTypes]): + def in_(self, *values: DataTypes): + for x in values: + self._check_operand(x, allow_string=True) return _Relation(self, 'IN', _ValueSet(values)) def like(self, value): - if self._dtype != str: + if self._numeric: + raise ValueError + if not isinstance(value, StringType): raise ValueError return _Relation(self, 'LIKE', value) From 22a2c7d7b36487549a39bdbd1972f0376cfaefc1 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sat, 19 Jun 2021 23:50:07 +0900 Subject: [PATCH 03/13] wip --- example/example_builder.py | 16 +++++++++------- pypict/builder/_constraint.py | 8 +++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/example/example_builder.py b/example/example_builder.py index 34e453a..ebd4dab 100644 --- a/example/example_builder.py +++ b/example/example_builder.py @@ -1,30 +1,32 @@ -from ast import Param from pypict.builder import Model, Parameter, IF, AND, OR, NOT +# https://github.com/microsoft/pict/blob/main/doc/pict.md type = Parameter('Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) -method = Parameter('Format method', ['Quick', 'Slow']) +format_method = Parameter('Format method', ['Quick', 'Slow']) filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) -cluster = Parameter('Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) +cluster_size = Parameter('Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) compression = Parameter('Compression', ['On', 'Off']) model = Model().parameters( - type, size, method, filesys, cluster, compression + type, size, format_method, filesys, cluster_size, compression ).constraints( IF(filesys == 'FAT').THEN(size <= 4096), IF(filesys == 'FAT32').THEN(size <= 32000), + size < 10000, compression == 'OFF', filesys.like('FAT*'), - IF(cluster.in_(512, 1024, 2048)).THEN(compression == 'off'), + + IF(cluster_size.in_(512, 1024, 2048)).THEN(compression == 'off'), IF(filesys.in_('FAT', 'FAT32')).THEN(compression == 'off'), IF( OR( filesys != 'NTFS', - AND(filesys == 'NTFS', cluster > 4096), + AND(filesys == 'NTFS', cluster_size > 4096), ) - ).THEN(compression == 'Off') + ).THEN(compression == 'Off'), ) print(model.to_string()) \ No newline at end of file diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py index dcc1d2b..af65911 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/builder/_constraint.py @@ -2,6 +2,12 @@ import pypict.builder +""" +Implements the constraints grammar as defined in: +https://github.com/microsoft/pict/blob/main/doc/pict.md +""" + + class _Constraint: def to_string(self) -> str: raise NotImplemented @@ -10,7 +16,7 @@ def __str__(self) -> str: return self.to_string() def __repr__(self) -> str: - return f'' + return f'' class _Relation(_Constraint): From a14ff88b3ad0882951f17f9ffb7e01ce91ea634c Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 00:06:54 +0900 Subject: [PATCH 04/13] run builder example in CI --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 209006b..41f72b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,7 @@ jobs: pushd example python example.py python example_capi.py + python example_builder.py popd - name: Run type checking From 8a3ce5a8e466d781f217f462273520b8d12855e7 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 00:11:08 +0900 Subject: [PATCH 05/13] wip --- pypict/builder/_constraint.py | 7 +++---- pypict/builder/_export.py | 0 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 pypict/builder/_export.py diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py index af65911..1f5f65e 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/builder/_constraint.py @@ -1,4 +1,3 @@ - import pypict.builder @@ -67,13 +66,13 @@ def __init__(self, const: _Constraint): self._const = const self._then = None self._else = None - + def THEN(self, const: _Constraint) -> 'IF': if self._then is not None: raise ValueError self._then = const return self - + def ELSE(self, const: _Constraint) -> 'IF': if self._then is None: raise ValueError @@ -107,4 +106,4 @@ def _literal(v): elif isinstance(v, str): return f'"{v}"' # TODO check? else: - return str(v) \ No newline at end of file + return str(v) diff --git a/pypict/builder/_export.py b/pypict/builder/_export.py deleted file mode 100644 index e69de29..0000000 From 61d4b70677374982e99c41eb1c0c61dc04c445e7 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 01:46:26 +0900 Subject: [PATCH 06/13] wip --- example/example_builder.py | 15 ++-- pypict/builder/__init__.py | 2 +- pypict/builder/_constraint.py | 131 ++++++++++++++++++-------------- pypict/builder/_model.py | 19 ++--- pypict/builder/_parameter.py | 137 ++++++++++++++++------------------ pypict/builder/_types.py | 7 ++ 6 files changed, 165 insertions(+), 146 deletions(-) create mode 100644 pypict/builder/_types.py diff --git a/example/example_builder.py b/example/example_builder.py index ebd4dab..310fca5 100644 --- a/example/example_builder.py +++ b/example/example_builder.py @@ -1,4 +1,4 @@ -from pypict.builder import Model, Parameter, IF, AND, OR, NOT +from pypict.builder import Model, Parameter, IF, ALL, ANY, NOT # https://github.com/microsoft/pict/blob/main/doc/pict.md @@ -17,15 +17,18 @@ size < 10000, compression == 'OFF', - filesys.like('FAT*'), + filesys.LIKE('FAT*'), - IF(cluster_size.in_(512, 1024, 2048)).THEN(compression == 'off'), - IF(filesys.in_('FAT', 'FAT32')).THEN(compression == 'off'), + IF(cluster_size.IN(512, 1024, 2048)).THEN(compression == 'off'), + IF(filesys.IN('FAT', 'FAT32')).THEN(compression == 'off'), IF( - OR( + ANY( filesys != 'NTFS', - AND(filesys == 'NTFS', cluster_size > 4096), + ALL( + filesys == 'NTFS', + cluster_size > 4096, + ), ) ).THEN(compression == 'Off'), ) diff --git a/pypict/builder/__init__.py b/pypict/builder/__init__.py index c9f335c..965249d 100644 --- a/pypict/builder/__init__.py +++ b/pypict/builder/__init__.py @@ -1,3 +1,3 @@ from ._model import Model from ._parameter import Parameter -from ._constraint import AND, OR, NOT, IF \ No newline at end of file +from ._constraint import ALL, ANY, NOT, IF \ No newline at end of file diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py index 1f5f65e..05ca9be 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/builder/_constraint.py @@ -1,4 +1,8 @@ -import pypict.builder +import json +from typing import Iterable, Optional, Union + +from pypict.builder import _parameter +from pypict.builder._types import NumericType, StringType, DataTypes """ @@ -18,92 +22,105 @@ def __repr__(self) -> str: return f'' -class _Relation(_Constraint): - def __init__(self, param, op, value_or_param): +class _Predicate(_Constraint): + pass + + +class _Relation(_Predicate): + def __init__(self, + param: '_parameter.Parameter', + op: str, + operand: Union[DataTypes, '_parameter.Parameter', '_ValueSet']): self._param = param self._op = op - self._value_or_param = value_or_param + self._operand = operand def to_string(self) -> str: - return f'{_literal(self._param)} {self._op} {_literal(self._value_or_param)}' + return f'{_as_str(self._param)} {self._op} {_as_str(self._operand)}' + + +class _ValueSet: + def __init__(self, values: Iterable[DataTypes]): + self._values = values + + def to_string(self) -> str: + return '{ ' + ', '.join([_as_str(x) for x in self._values]) + ' }' + + +def _as_str(v: Union[DataTypes, '_parameter.Parameter', '_ValueSet']) -> str: + if isinstance(v, _parameter.Parameter): + return f'[{v._name}]' + elif isinstance(v, _ValueSet): + return v.to_string() + elif isinstance(v, NumericType): + return str(v) + elif isinstance(v, StringType): + # Escape double-quotes in the string then quote the entire string. + return json.dumps(v) + raise ValueError(v) -class _LogicalOp(_Constraint): - _op: str = None # to be overridden +def _check_predicates(*preds: _Predicate): + for pred in preds: + if not isinstance(pred, _Predicate): + raise ValueError(f'expected predicate but got {pred} of {type(pred)}') - def __init__(self, *consts: _Constraint): - for const in consts: - if not isinstance(const, _Constraint): - raise ValueError - self._consts = consts + +class _LogicalOp(_Predicate): + _op: Optional[str] = None # to be overridden + + def __init__(self, *preds: _Predicate): + _check_predicates(*preds) + self._preds = preds def to_string(self) -> str: - return '(' + f' {self._op} '.join([str(x) for x in self._consts]) + ')' + return '(' + f' {self._op} '.join([str(x) for x in self._preds]) + ')' -class AND(_LogicalOp): +class ALL(_LogicalOp): _op = 'AND' -class OR(_LogicalOp): +class ANY(_LogicalOp): _op = 'OR' -class NOT(_Constraint): - def __init__(self, const: _Constraint): - if not isinstance(const, _Constraint): - raise ValueError - self._const = const +class NOT(_Predicate): + def __init__(self, pred: _Predicate): + _check_predicates(pred) + self._pred = pred def to_string(self) -> str: - return f'NOT {self._const}' + return f'NOT {self._pred}' class IF(_Constraint): - def __init__(self, const: _Constraint): - if not isinstance(const, _Constraint): - raise ValueError - self._const = const - self._then = None - self._else = None - - def THEN(self, const: _Constraint) -> 'IF': + def __init__(self, pred: _Predicate): + _check_predicates(pred) + self._if = pred + self._then: Optional[_Predicate] = None + self._else: Optional[_Predicate] = None + + def THEN(self, pred: _Predicate) -> 'IF': + _check_predicates(pred) if self._then is not None: - raise ValueError - self._then = const + raise ValueError('THEN cannot be repeated') + self._then = pred return self - def ELSE(self, const: _Constraint) -> 'IF': + def ELSE(self, pred: _Predicate) -> 'IF': + _check_predicates(pred) if self._then is None: - raise ValueError - if self._else is None: - raise ValueError - self._else = const + raise ValueError('THEN must be given before ELSE') + if self._else is not None: + raise ValueError('ELSE cannot be repeated') + self._else = pred def to_string(self) -> str: if self._then is None: - raise ValueError + raise ValueError('THEN must be given') return ( - f'IF {self._const}' + + f'IF {self._if}' + f'\nTHEN {self._then}' + (f'\nELSE {self._else}' if self._else else '') ) - - -class _ValueSet: - def __init__(self, values): - self._values = values - - def to_string(self) -> str: - return '{ ' + ', '.join([_literal(x) for x in self._values]) + ' }' - - -def _literal(v): - if isinstance(v, pypict.builder.Parameter): - return f'[{v._name}]' - elif isinstance(v, _ValueSet): - return v.to_string() - elif isinstance(v, str): - return f'"{v}"' # TODO check? - else: - return str(v) diff --git a/pypict/builder/_model.py b/pypict/builder/_model.py index 374d656..2e01ddf 100644 --- a/pypict/builder/_model.py +++ b/pypict/builder/_model.py @@ -6,9 +6,9 @@ class Model: def __init__(self): - self._params = [] - self._submodels = [] - self._constraints = [] + self._params: List[Parameter] = [] + self._submodels: List[_SubModel] = [] + self._constraints: List[_Constraint] = [] def parameters(self, *params: Parameter) -> 'Model': self._params += params @@ -22,8 +22,8 @@ def constraints(self, *constraints: _Constraint) -> 'Model': self._constraints += constraints return self - def to_string(self): - lines = [] + def to_string(self) -> str: + lines: List[str] = [] # Parameter definitions if len(self._params) == 0: @@ -42,19 +42,20 @@ def to_string(self): if len(self._constraints) != 0: for c in self._constraints: lines.append(c.to_string() + ';') - + lines.append('') + return '\n'.join(lines) class _SubModel: - def __init__(self, params, order=None): + def __init__(self, params: Tuple[Parameter], order: Optional[int] = None): self._params = params self._order = order - def to_string(self): + def to_string(self) -> str: return ( '{ ' + ', '.join([param._name for param in self._params]) + ' }' + '' if self._order is None else f' @ {self._order}' - ) \ No newline at end of file + ) diff --git a/pypict/builder/_parameter.py b/pypict/builder/_parameter.py index 905da10..81450d4 100644 --- a/pypict/builder/_parameter.py +++ b/pypict/builder/_parameter.py @@ -1,44 +1,28 @@ import numbers -from typing import Iterable, Iterator, List, Optional, Tuple, Type, Union +from typing import Iterable, List, Optional, Tuple, Union -from pypict.builder._constraint import _Relation, _ValueSet +from pypict.builder import _constraint +from pypict.builder._types import NumericType, StringType, DataTypes -NumericType = numbers.Real -StringType = str -DataTypes = Union[NumericType, StringType] +# Values can be a list of (literals or tuple of (literal, weight)). +ValuesType = Iterable[Union[DataTypes, Tuple[DataTypes, int]]] class Parameter: - def __init__( - self, - name: str, - values: Iterable[Union[DataTypes, Tuple[DataTypes, int]]], - *, - aliases: Optional[Iterable[str]] = None): - if aliases is None: - aliases = [] - numeric = self._check_values(values) - - self._check_valid_name(name) - for x in aliases: - self._check_valid_name(x) - + def __init__(self, name: str, values: ValuesType): + # TODO support aliases + if ':' in name: + raise ValueError(f'invalid parameter name: {name}') + is_numeric = self._check_values(values) self._name = name self._values = values - self._aliases = aliases - self._numeric = numeric + self._is_numeric = is_numeric @staticmethod - def _check_valid_name(name): - if '[' in name or ']' in name: - raise ValueError(f'invalid parameter name: {name}') - - @staticmethod - def _check_values( - values: Iterable[Union[DataTypes, Tuple[DataTypes, int]]]) -> bool: - numeric = True - for i, x in enumerate(values): + def _check_values(values: ValuesType) -> bool: + is_numeric = True + for x in values: if isinstance(x, tuple): value, weight = x else: @@ -46,71 +30,78 @@ def _check_values( if isinstance(value, NumericType): pass elif isinstance(value, StringType): - numeric = False + is_numeric = False else: raise ValueError( - f'unsupported data type at index {i}: {value} ({type(value)})') + f'expected numeric or string but got {value} of {type(value)}') if not isinstance(weight, int): - raise ValueError - return numeric + raise ValueError(f'weight must be int, but got {weight} of {type(weight)}') + return is_numeric - def __str__(self): + def __str__(self) -> str: return self.to_string() - def __repr__(self): - return self.to_string() # TODO + def __repr__(self) -> str: + return f'' - def to_string(self, separator=','): - values = f'{separator} '.join([str(x) for x in self._values]) + def to_string(self, separator: str = ',') -> str: + values = f'{separator} '.join([ + f'{x[0]} ({x[1]})' if isinstance(x, tuple) else str(x) + for x in self._values + ]) return f'{self._name}: {values}' - def _check_operand(self, other, *, allow_string=False): + def _check_operand( + self, + other: Union[DataTypes, 'Parameter'], + *, + no_string: bool = False): if isinstance(other, Parameter): - if self._numeric != other._numeric: - raise ValueError + if self._is_numeric != other._is_numeric: + raise ValueError('cannot compare numeric and non-numeric parameters') elif isinstance(other, NumericType): - if not self._numeric: - raise ValueError + if not self._is_numeric: + raise ValueError('cannot compare string-typed parameter with numeric constant') elif isinstance(other, StringType): - if not allow_string: - raise ValueError - if self._numeric: - raise ValueError + if no_string: + raise ValueError('strings cannot be compared') + if self._is_numeric: + raise ValueError('cannot compare numeric-typed parameter with string constant') else: - raise ValueError + raise ValueError(f'cannot compare with {other} of {type(other)}') - def __eq__(self, other: Union[DataTypes, 'Parameter']): - self._check_operand(other, allow_string=True) - return _Relation(self, '=', other) + def __gt__(self, other: Union[NumericType, 'Parameter']): + self._check_operand(other, no_string=True) + return _constraint._Relation(self, '>', other) - def __ne__(self, other: Union[DataTypes, 'Parameter']): - self._check_operand(other, allow_string=True) - return _Relation(self, '<>', other) + def __ge__(self, other: Union[NumericType, 'Parameter']): + self._check_operand(other, no_string=True) + return _constraint._Relation(self, '>=', other) - def __gt__(self, other: Union[DataTypes, 'Parameter']): - self._check_operand(other) - return _Relation(self, '>', other) + def __lt__(self, other: Union[NumericType, 'Parameter']): + self._check_operand(other, no_string=True) + return _constraint._Relation(self, '<', other) - def __ge__(self, other: Union[DataTypes, 'Parameter']): - self._check_operand(other) - return _Relation(self, '>=', other) + def __le__(self, other: Union[NumericType, 'Parameter']): + self._check_operand(other, no_string=True) + return _constraint._Relation(self, '<=', other) - def __lt__(self, other: Union[DataTypes, 'Parameter']): + def __eq__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) - return _Relation(self, '<', other) + return _constraint._Relation(self, '=', other) - def __le__(self, other: Union[DataTypes, 'Parameter']): + def __ne__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) - return _Relation(self, '<=', other) + return _constraint._Relation(self, '<>', other) - def in_(self, *values: DataTypes): + def IN(self, *values: DataTypes): for x in values: - self._check_operand(x, allow_string=True) - return _Relation(self, 'IN', _ValueSet(values)) + self._check_operand(x) + return _constraint._Relation(self, 'IN', _constraint._ValueSet(values)) - def like(self, value): - if self._numeric: - raise ValueError + def LIKE(self, value: StringType): + if self._is_numeric: + raise ValueError('LIKE operator is only for strings') if not isinstance(value, StringType): - raise ValueError - return _Relation(self, 'LIKE', value) + raise ValueError(f'expected wildcard pattern string but got {value} of {type(value)}') + return _constraint._Relation(self, 'LIKE', value) diff --git a/pypict/builder/_types.py b/pypict/builder/_types.py new file mode 100644 index 0000000..dc23544 --- /dev/null +++ b/pypict/builder/_types.py @@ -0,0 +1,7 @@ +import numbers +from typing import Union + + +NumericType = numbers.Real +StringType = str +DataTypes = Union[NumericType, StringType] \ No newline at end of file From 4e46418e1bb49e0c660f15142e6694ea61a6ec27 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 12:53:32 +0900 Subject: [PATCH 07/13] wip --- example/example_builder.py | 89 +++++++++++++++++++++-------------- pypict/builder/_constraint.py | 2 +- pypict/builder/_model.py | 17 +++---- pypict/builder/_parameter.py | 14 +++--- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/example/example_builder.py b/example/example_builder.py index 310fca5..60baf73 100644 --- a/example/example_builder.py +++ b/example/example_builder.py @@ -1,35 +1,54 @@ -from pypict.builder import Model, Parameter, IF, ALL, ANY, NOT - - -# https://github.com/microsoft/pict/blob/main/doc/pict.md -type = Parameter('Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) -size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) -format_method = Parameter('Format method', ['Quick', 'Slow']) -filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) -cluster_size = Parameter('Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) -compression = Parameter('Compression', ['On', 'Off']) - -model = Model().parameters( - type, size, format_method, filesys, cluster_size, compression -).constraints( - IF(filesys == 'FAT').THEN(size <= 4096), - IF(filesys == 'FAT32').THEN(size <= 32000), - - size < 10000, - compression == 'OFF', - filesys.LIKE('FAT*'), - - IF(cluster_size.IN(512, 1024, 2048)).THEN(compression == 'off'), - IF(filesys.IN('FAT', 'FAT32')).THEN(compression == 'off'), - - IF( - ANY( - filesys != 'NTFS', - ALL( - filesys == 'NTFS', - cluster_size > 4096, - ), - ) - ).THEN(compression == 'Off'), -) -print(model.to_string()) \ No newline at end of file +from pypict.cmd import from_model + + +def build_model(): + from pypict.builder import Model, Parameter, IF, ALL, ANY, NOT + + # https://github.com/microsoft/pict/blob/main/doc/pict.md + type = Parameter('Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) + size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) + format_method = Parameter('Format method', ['Quick', 'Slow']) + filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) + cluster_size = Parameter('Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) + compression = Parameter('Compression', ['On', 'Off']) + + model = Model() + model.parameters( + type, size, format_method, filesys, cluster_size, compression + ) + model.constraints( + IF(filesys == 'FAT').THEN(size <= 4096), + + IF(filesys == 'FAT32').THEN(size <= 32000), + + size < 10000, + + compression == 'OFF', + + filesys.LIKE('FAT*'), + + IF(cluster_size.IN(512, 1024, 2048)).THEN(compression == 'off'), + + IF(filesys.IN('FAT', 'FAT32')).THEN(compression == 'off'), + + IF( + ANY( + filesys != 'NTFS', + ALL( + filesys == 'NTFS', + cluster_size > 4096, + ), + ) + ).THEN(compression == 'Off'), + ) + return model + + +model = build_model().to_string() +print('# --- PICT model --- #') +print(model) +print() +print('# --- Generated cases --- #') +params, rows = from_model(model) +for row in rows: + print(dict(zip(params, row))) diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py index 05ca9be..934c6b1 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/builder/_constraint.py @@ -49,7 +49,7 @@ def to_string(self) -> str: def _as_str(v: Union[DataTypes, '_parameter.Parameter', '_ValueSet']) -> str: if isinstance(v, _parameter.Parameter): - return f'[{v._name}]' + return f'[{v.name}]' elif isinstance(v, _ValueSet): return v.to_string() elif isinstance(v, NumericType): diff --git a/pypict/builder/_model.py b/pypict/builder/_model.py index 2e01ddf..0b45eab 100644 --- a/pypict/builder/_model.py +++ b/pypict/builder/_model.py @@ -6,12 +6,13 @@ class Model: def __init__(self): - self._params: List[Parameter] = [] + self._parameters: List[Parameter] = [] self._submodels: List[_SubModel] = [] self._constraints: List[_Constraint] = [] def parameters(self, *params: Parameter) -> 'Model': - self._params += params + # TODO check uniqueness of names + self._parameters += params return self def submodel(self, params: Tuple[Parameter], order: Optional[int] = None) -> 'Model': @@ -26,9 +27,9 @@ def to_string(self) -> str: lines: List[str] = [] # Parameter definitions - if len(self._params) == 0: + if len(self._parameters) == 0: raise ValueError('no parameters are added to the model') - for p in self._params: + for p in self._parameters: lines.append(p.to_string()) lines.append('') @@ -49,13 +50,13 @@ def to_string(self) -> str: class _SubModel: def __init__(self, params: Tuple[Parameter], order: Optional[int] = None): - self._params = params - self._order = order + self.params = params + self.order = order def to_string(self) -> str: return ( '{ ' + - ', '.join([param._name for param in self._params]) + + ', '.join([param.name for param in self.params]) + ' }' + - '' if self._order is None else f' @ {self._order}' + '' if self.order is None else f' @ {self.order}' ) diff --git a/pypict/builder/_parameter.py b/pypict/builder/_parameter.py index 81450d4..974f9e0 100644 --- a/pypict/builder/_parameter.py +++ b/pypict/builder/_parameter.py @@ -14,10 +14,9 @@ def __init__(self, name: str, values: ValuesType): # TODO support aliases if ':' in name: raise ValueError(f'invalid parameter name: {name}') - is_numeric = self._check_values(values) - self._name = name - self._values = values - self._is_numeric = is_numeric + self.name = name + self.values = values + self._is_numeric = self._check_values(values) @staticmethod def _check_values(values: ValuesType) -> bool: @@ -42,14 +41,13 @@ def __str__(self) -> str: return self.to_string() def __repr__(self) -> str: - return f'' + return f'' def to_string(self, separator: str = ',') -> str: - values = f'{separator} '.join([ + return self.name + ': ' + f'{separator} '.join([ f'{x[0]} ({x[1]})' if isinstance(x, tuple) else str(x) - for x in self._values + for x in self.values ]) - return f'{self._name}: {values}' def _check_operand( self, From c28b7e03d852c8d811237eac6d59b46d93542723 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 14:22:49 +0900 Subject: [PATCH 08/13] wip seed generator --- pypict/builder/_seed.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pypict/builder/_seed.py diff --git a/pypict/builder/_seed.py b/pypict/builder/_seed.py new file mode 100644 index 0000000..bf307bf --- /dev/null +++ b/pypict/builder/_seed.py @@ -0,0 +1,14 @@ +from typing import Dict + + +class SeedGenerator: + def __init__(self, model): + self._model = model + self._seeds = [] + + def seed(self, case: Dict): + self._seeds.append( + (case[x] if x in case else None for x in self._model._parameters)) + + def to_string(self): + pass From 67d4f90924e999bf1dc95247fb4cb87b4fe1795c Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 14:33:54 +0900 Subject: [PATCH 09/13] wip --- pypict/builder/_constraint.py | 5 ++++- pypict/builder/_model.py | 2 +- pypict/builder/_parameter.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pypict/builder/_constraint.py b/pypict/builder/_constraint.py index 934c6b1..ea2ca61 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/builder/_constraint.py @@ -21,6 +21,9 @@ def __str__(self) -> str: def __repr__(self) -> str: return f'' + def __bool__(self) -> bool: + raise ValueError('cannot apply Python logical operators on constraints') + class _Predicate(_Constraint): pass @@ -67,7 +70,7 @@ def _check_predicates(*preds: _Predicate): class _LogicalOp(_Predicate): - _op: Optional[str] = None # to be overridden + _op: str = '' # to be overridden def __init__(self, *preds: _Predicate): _check_predicates(*preds) diff --git a/pypict/builder/_model.py b/pypict/builder/_model.py index 0b45eab..ad8b4c9 100644 --- a/pypict/builder/_model.py +++ b/pypict/builder/_model.py @@ -14,7 +14,7 @@ def parameters(self, *params: Parameter) -> 'Model': # TODO check uniqueness of names self._parameters += params return self - + def submodel(self, params: Tuple[Parameter], order: Optional[int] = None) -> 'Model': self._submodels.append(_SubModel(params, order)) return self diff --git a/pypict/builder/_parameter.py b/pypict/builder/_parameter.py index 974f9e0..73fe68d 100644 --- a/pypict/builder/_parameter.py +++ b/pypict/builder/_parameter.py @@ -99,7 +99,7 @@ def IN(self, *values: DataTypes): def LIKE(self, value: StringType): if self._is_numeric: - raise ValueError('LIKE operator is only for strings') + raise ValueError('LIKE operator is only for string parameter') if not isinstance(value, StringType): raise ValueError(f'expected wildcard pattern string but got {value} of {type(value)}') return _constraint._Relation(self, 'LIKE', value) From 38f4b9a0becded1acda43ea6fde137108b47f349 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 23:24:36 +0900 Subject: [PATCH 10/13] wip --- pypict/__init__.py | 4 +- pypict/_builder/__init__.py | 0 .../_constraint.py => _builder/constraint.py} | 49 +++++++++++++++++-- .../{builder/_model.py => _builder/model.py} | 2 +- .../_parameter.py => _builder/parameter.py} | 16 +++--- pypict/{builder/_seed.py => _builder/seed.py} | 0 .../{builder/_types.py => _builder/types.py} | 0 pypict/builder.py | 3 ++ pypict/builder/__init__.py | 3 -- 9 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 pypict/_builder/__init__.py rename pypict/{builder/_constraint.py => _builder/constraint.py} (69%) rename pypict/{builder/_model.py => _builder/model.py} (100%) rename pypict/{builder/_parameter.py => _builder/parameter.py} (84%) rename pypict/{builder/_seed.py => _builder/seed.py} (100%) rename pypict/{builder/_types.py => _builder/types.py} (100%) create mode 100644 pypict/builder.py delete mode 100644 pypict/builder/__init__.py diff --git a/pypict/__init__.py b/pypict/__init__.py index 54677b5..0edf0d9 100644 --- a/pypict/__init__.py +++ b/pypict/__init__.py @@ -1,5 +1,7 @@ from pypict._version import __version__ +from pypict.capi import PAIRWISE_GENERATION + from pypict.api import Task -from pypict.capi import PAIRWISE_GENERATION +from pypict import builder diff --git a/pypict/_builder/__init__.py b/pypict/_builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pypict/builder/_constraint.py b/pypict/_builder/constraint.py similarity index 69% rename from pypict/builder/_constraint.py rename to pypict/_builder/constraint.py index ea2ca61..0646abc 100644 --- a/pypict/builder/_constraint.py +++ b/pypict/_builder/constraint.py @@ -1,5 +1,6 @@ +import enum import json -from typing import Iterable, Optional, Union +from typing import Dict, Iterable, Optional, Union from pypict.builder import _parameter from pypict.builder._types import NumericType, StringType, DataTypes @@ -11,10 +12,24 @@ """ +class _Operator(enum.Enum): + _GT = '>' + _GE = '>=' + _LT = '<' + _LE = '<=' + _EQ = '=' + _NE = '<>' + _IN = 'IN' + _LIKE = 'LIKE' + + class _Constraint: def to_string(self) -> str: raise NotImplemented + def evaluate(self, combination: Dict[str, DataTypes]) -> bool: + raise NotImplemented + def __str__(self) -> str: return self.to_string() @@ -32,14 +47,22 @@ class _Predicate(_Constraint): class _Relation(_Predicate): def __init__(self, param: '_parameter.Parameter', - op: str, + op: _Operator, operand: Union[DataTypes, '_parameter.Parameter', '_ValueSet']): self._param = param self._op = op self._operand = operand def to_string(self) -> str: - return f'{_as_str(self._param)} {self._op} {_as_str(self._operand)}' + return f'{_as_str(self._param)} {self._op.value} {_as_str(self._operand)}' + + def evaluate(self, combination: Dict[str, DataTypes]) -> bool: + if self._param.name not in combination: + return True + value = combination[self._param.name] + # TODO + if self._op is _Operator._GT: + return value > self._operand class _ValueSet: @@ -64,6 +87,8 @@ def _as_str(v: Union[DataTypes, '_parameter.Parameter', '_ValueSet']) -> str: def _check_predicates(*preds: _Predicate): + if len(preds) == 0: + raise ValueError('at least one predicate must be specified') for pred in preds: if not isinstance(pred, _Predicate): raise ValueError(f'expected predicate but got {pred} of {type(pred)}') @@ -83,10 +108,16 @@ def to_string(self) -> str: class ALL(_LogicalOp): _op = 'AND' + def evaluate(self, combination) -> bool: + return all((p.evaluate(combination) for p in self._preds)) + class ANY(_LogicalOp): _op = 'OR' + def evaluate(self, combination) -> bool: + return any((p.evaluate(combination) for p in self._preds)) + class NOT(_Predicate): def __init__(self, pred: _Predicate): @@ -96,6 +127,9 @@ def __init__(self, pred: _Predicate): def to_string(self) -> str: return f'NOT {self._pred}' + def evaluate(self, combination: Dict[str, DataTypes]) -> bool: + return not self._pred.evaluate(combination) + class IF(_Constraint): def __init__(self, pred: _Predicate): @@ -127,3 +161,12 @@ def to_string(self) -> str: f'\nTHEN {self._then}' + (f'\nELSE {self._else}' if self._else else '') ) + + def evaluate(self, combination) -> bool: + if self._then is None: + raise ValueError('cannot evaluate without THEN') + if self._if.evaluate(combination): + return self._then.evaluate(combination) + elif self._else is not None: + return self._else.evaluate(combination) + return True diff --git a/pypict/builder/_model.py b/pypict/_builder/model.py similarity index 100% rename from pypict/builder/_model.py rename to pypict/_builder/model.py index ad8b4c9..64313d3 100644 --- a/pypict/builder/_model.py +++ b/pypict/_builder/model.py @@ -11,7 +11,6 @@ def __init__(self): self._constraints: List[_Constraint] = [] def parameters(self, *params: Parameter) -> 'Model': - # TODO check uniqueness of names self._parameters += params return self @@ -29,6 +28,7 @@ def to_string(self) -> str: # Parameter definitions if len(self._parameters) == 0: raise ValueError('no parameters are added to the model') + # TODO check uniqueness of names for p in self._parameters: lines.append(p.to_string()) lines.append('') diff --git a/pypict/builder/_parameter.py b/pypict/_builder/parameter.py similarity index 84% rename from pypict/builder/_parameter.py rename to pypict/_builder/parameter.py index 73fe68d..f7b590b 100644 --- a/pypict/builder/_parameter.py +++ b/pypict/_builder/parameter.py @@ -70,36 +70,36 @@ def _check_operand( def __gt__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, '>', other) + return _constraint._Relation(self, _constraint._Operator._GT, other) def __ge__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, '>=', other) + return _constraint._Relation(self, _constraint._Operator._GE, other) def __lt__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, '<', other) + return _constraint._Relation(self, _constraint._Operator._LT, other) def __le__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, '<=', other) + return _constraint._Relation(self, _constraint._Operator._LE, other) def __eq__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) - return _constraint._Relation(self, '=', other) + return _constraint._Relation(self, _constraint._Operator._EQ, other) def __ne__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) - return _constraint._Relation(self, '<>', other) + return _constraint._Relation(self, _constraint._Operator._NE, other) def IN(self, *values: DataTypes): for x in values: self._check_operand(x) - return _constraint._Relation(self, 'IN', _constraint._ValueSet(values)) + return _constraint._Relation(self, _constraint._Operator._IN, _constraint._ValueSet(values)) def LIKE(self, value: StringType): if self._is_numeric: raise ValueError('LIKE operator is only for string parameter') if not isinstance(value, StringType): raise ValueError(f'expected wildcard pattern string but got {value} of {type(value)}') - return _constraint._Relation(self, 'LIKE', value) + return _constraint._Relation(self, _constraint._Operator._LIKE, value) diff --git a/pypict/builder/_seed.py b/pypict/_builder/seed.py similarity index 100% rename from pypict/builder/_seed.py rename to pypict/_builder/seed.py diff --git a/pypict/builder/_types.py b/pypict/_builder/types.py similarity index 100% rename from pypict/builder/_types.py rename to pypict/_builder/types.py diff --git a/pypict/builder.py b/pypict/builder.py new file mode 100644 index 0000000..a14eaf8 --- /dev/null +++ b/pypict/builder.py @@ -0,0 +1,3 @@ +from pypict._builder.model import Model +from pypict._builder.parameter import Parameter +from pypict._builder.constraint import ALL, ANY, NOT, IF diff --git a/pypict/builder/__init__.py b/pypict/builder/__init__.py deleted file mode 100644 index 965249d..0000000 --- a/pypict/builder/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._model import Model -from ._parameter import Parameter -from ._constraint import ALL, ANY, NOT, IF \ No newline at end of file From 1fb3ac8cb9c6e9c80e0cb323fccc5423d13dac7e Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 23:26:33 +0900 Subject: [PATCH 11/13] wip --- pypict/_builder/constraint.py | 12 ++++++------ pypict/_builder/model.py | 4 ++-- pypict/_builder/parameter.py | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pypict/_builder/constraint.py b/pypict/_builder/constraint.py index 0646abc..8a0ae2b 100644 --- a/pypict/_builder/constraint.py +++ b/pypict/_builder/constraint.py @@ -2,8 +2,8 @@ import json from typing import Dict, Iterable, Optional, Union -from pypict.builder import _parameter -from pypict.builder._types import NumericType, StringType, DataTypes +from pypict._builder import parameter +from pypict._builder.types import NumericType, StringType, DataTypes """ @@ -46,9 +46,9 @@ class _Predicate(_Constraint): class _Relation(_Predicate): def __init__(self, - param: '_parameter.Parameter', + param: 'parameter.Parameter', op: _Operator, - operand: Union[DataTypes, '_parameter.Parameter', '_ValueSet']): + operand: Union[DataTypes, 'parameter.Parameter', '_ValueSet']): self._param = param self._op = op self._operand = operand @@ -73,8 +73,8 @@ def to_string(self) -> str: return '{ ' + ', '.join([_as_str(x) for x in self._values]) + ' }' -def _as_str(v: Union[DataTypes, '_parameter.Parameter', '_ValueSet']) -> str: - if isinstance(v, _parameter.Parameter): +def _as_str(v: Union[DataTypes, 'parameter.Parameter', '_ValueSet']) -> str: + if isinstance(v, parameter.Parameter): return f'[{v.name}]' elif isinstance(v, _ValueSet): return v.to_string() diff --git a/pypict/_builder/model.py b/pypict/_builder/model.py index 64313d3..b9337c1 100644 --- a/pypict/_builder/model.py +++ b/pypict/_builder/model.py @@ -1,7 +1,7 @@ from typing import Iterable, Iterator, List, Optional, Tuple, Type, Union -from pypict.builder._parameter import Parameter -from pypict.builder._constraint import _Constraint +from pypict._builder.parameter import Parameter +from pypict._builder.constraint import _Constraint class Model: diff --git a/pypict/_builder/parameter.py b/pypict/_builder/parameter.py index f7b590b..f245ddb 100644 --- a/pypict/_builder/parameter.py +++ b/pypict/_builder/parameter.py @@ -1,8 +1,8 @@ import numbers from typing import Iterable, List, Optional, Tuple, Union -from pypict.builder import _constraint -from pypict.builder._types import NumericType, StringType, DataTypes +from pypict._builder import constraint +from pypict._builder.types import NumericType, StringType, DataTypes # Values can be a list of (literals or tuple of (literal, weight)). @@ -70,36 +70,36 @@ def _check_operand( def __gt__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, _constraint._Operator._GT, other) + return constraint._Relation(self, constraint._Operator._GT, other) def __ge__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, _constraint._Operator._GE, other) + return constraint._Relation(self, constraint._Operator._GE, other) def __lt__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, _constraint._Operator._LT, other) + return constraint._Relation(self, constraint._Operator._LT, other) def __le__(self, other: Union[NumericType, 'Parameter']): self._check_operand(other, no_string=True) - return _constraint._Relation(self, _constraint._Operator._LE, other) + return constraint._Relation(self, constraint._Operator._LE, other) def __eq__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) - return _constraint._Relation(self, _constraint._Operator._EQ, other) + return constraint._Relation(self, constraint._Operator._EQ, other) def __ne__(self, other: Union[DataTypes, 'Parameter']): self._check_operand(other) - return _constraint._Relation(self, _constraint._Operator._NE, other) + return constraint._Relation(self, constraint._Operator._NE, other) def IN(self, *values: DataTypes): for x in values: self._check_operand(x) - return _constraint._Relation(self, _constraint._Operator._IN, _constraint._ValueSet(values)) + return constraint._Relation(self, constraint._Operator._IN, constraint._ValueSet(values)) def LIKE(self, value: StringType): if self._is_numeric: raise ValueError('LIKE operator is only for string parameter') if not isinstance(value, StringType): raise ValueError(f'expected wildcard pattern string but got {value} of {type(value)}') - return _constraint._Relation(self, _constraint._Operator._LIKE, value) + return constraint._Relation(self, constraint._Operator._LIKE, value) From e694f5a3e269db62c22927e67e83095e3692ff89 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 23:45:51 +0900 Subject: [PATCH 12/13] flake8 --- example/example_builder.py | 5 +++-- pypict/_builder/constraint.py | 17 +++++++++++------ pypict/_builder/model.py | 9 ++++++--- pypict/_builder/parameter.py | 28 +++++++++++++++++++--------- pypict/_builder/types.py | 2 +- pypict/builder.py | 6 +++--- tests/builder_tests/test_builder.py | 6 ++++-- 7 files changed, 47 insertions(+), 26 deletions(-) diff --git a/example/example_builder.py b/example/example_builder.py index 60baf73..09557b9 100644 --- a/example/example_builder.py +++ b/example/example_builder.py @@ -9,7 +9,8 @@ def build_model(): size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) format_method = Parameter('Format method', ['Quick', 'Slow']) filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) - cluster_size = Parameter('Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) + cluster_size = Parameter( + 'Cluster size', [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]) compression = Parameter('Compression', ['On', 'Off']) model = Model() @@ -25,7 +26,7 @@ def build_model(): compression == 'OFF', - filesys.LIKE('FAT*'), + NOT(filesys.LIKE('FAT*')), IF(cluster_size.IN(512, 1024, 2048)).THEN(compression == 'off'), diff --git a/pypict/_builder/constraint.py b/pypict/_builder/constraint.py index 8a0ae2b..007ed7e 100644 --- a/pypict/_builder/constraint.py +++ b/pypict/_builder/constraint.py @@ -25,10 +25,10 @@ class _Operator(enum.Enum): class _Constraint: def to_string(self) -> str: - raise NotImplemented + raise NotImplementedError def evaluate(self, combination: Dict[str, DataTypes]) -> bool: - raise NotImplemented + raise NotImplementedError def __str__(self) -> str: return self.to_string() @@ -37,7 +37,8 @@ def __repr__(self) -> str: return f'' def __bool__(self) -> bool: - raise ValueError('cannot apply Python logical operators on constraints') + raise ValueError( + 'cannot apply Python logical operators on constraints') class _Predicate(_Constraint): @@ -45,7 +46,8 @@ class _Predicate(_Constraint): class _Relation(_Predicate): - def __init__(self, + def __init__( + self, param: 'parameter.Parameter', op: _Operator, operand: Union[DataTypes, 'parameter.Parameter', '_ValueSet']): @@ -54,7 +56,8 @@ def __init__(self, self._operand = operand def to_string(self) -> str: - return f'{_as_str(self._param)} {self._op.value} {_as_str(self._operand)}' + return (f'{_as_str(self._param)} ' + f'{self._op.value} {_as_str(self._operand)}') def evaluate(self, combination: Dict[str, DataTypes]) -> bool: if self._param.name not in combination: @@ -91,7 +94,9 @@ def _check_predicates(*preds: _Predicate): raise ValueError('at least one predicate must be specified') for pred in preds: if not isinstance(pred, _Predicate): - raise ValueError(f'expected predicate but got {pred} of {type(pred)}') + raise ValueError( + 'expected predicate, ' + f'but got {pred} of {type(pred)}') class _LogicalOp(_Predicate): diff --git a/pypict/_builder/model.py b/pypict/_builder/model.py index b9337c1..4b3a9c3 100644 --- a/pypict/_builder/model.py +++ b/pypict/_builder/model.py @@ -1,4 +1,4 @@ -from typing import Iterable, Iterator, List, Optional, Tuple, Type, Union +from typing import List, Optional, Tuple from pypict._builder.parameter import Parameter from pypict._builder.constraint import _Constraint @@ -14,7 +14,10 @@ def parameters(self, *params: Parameter) -> 'Model': self._parameters += params return self - def submodel(self, params: Tuple[Parameter], order: Optional[int] = None) -> 'Model': + def submodel( + self, + params: Tuple[Parameter], + order: Optional[int] = None) -> 'Model': self._submodels.append(_SubModel(params, order)) return self @@ -56,7 +59,7 @@ def __init__(self, params: Tuple[Parameter], order: Optional[int] = None): def to_string(self) -> str: return ( '{ ' + - ', '.join([param.name for param in self.params]) + + ', '.join([param.name for param in self.params]) + ' }' + '' if self.order is None else f' @ {self.order}' ) diff --git a/pypict/_builder/parameter.py b/pypict/_builder/parameter.py index f245ddb..01401f2 100644 --- a/pypict/_builder/parameter.py +++ b/pypict/_builder/parameter.py @@ -1,5 +1,4 @@ -import numbers -from typing import Iterable, List, Optional, Tuple, Union +from typing import Iterable, Tuple, Union from pypict._builder import constraint from pypict._builder.types import NumericType, StringType, DataTypes @@ -32,9 +31,12 @@ def _check_values(values: ValuesType) -> bool: is_numeric = False else: raise ValueError( - f'expected numeric or string but got {value} of {type(value)}') + 'expected numeric or string, ' + f'but got {value} of {type(value)}') if not isinstance(weight, int): - raise ValueError(f'weight must be int, but got {weight} of {type(weight)}') + raise ValueError( + 'weight must be int, ' + f'but got {weight} of {type(weight)}') return is_numeric def __str__(self) -> str: @@ -56,15 +58,20 @@ def _check_operand( no_string: bool = False): if isinstance(other, Parameter): if self._is_numeric != other._is_numeric: - raise ValueError('cannot compare numeric and non-numeric parameters') + raise ValueError( + 'cannot compare numeric and non-numeric parameters') elif isinstance(other, NumericType): if not self._is_numeric: - raise ValueError('cannot compare string-typed parameter with numeric constant') + raise ValueError( + 'cannot compare string-typed parameter ' + 'with numeric constant') elif isinstance(other, StringType): if no_string: raise ValueError('strings cannot be compared') if self._is_numeric: - raise ValueError('cannot compare numeric-typed parameter with string constant') + raise ValueError( + 'cannot compare numeric-typed parameter ' + 'with string constant') else: raise ValueError(f'cannot compare with {other} of {type(other)}') @@ -95,11 +102,14 @@ def __ne__(self, other: Union[DataTypes, 'Parameter']): def IN(self, *values: DataTypes): for x in values: self._check_operand(x) - return constraint._Relation(self, constraint._Operator._IN, constraint._ValueSet(values)) + return constraint._Relation( + self, constraint._Operator._IN, constraint._ValueSet(values)) def LIKE(self, value: StringType): if self._is_numeric: raise ValueError('LIKE operator is only for string parameter') if not isinstance(value, StringType): - raise ValueError(f'expected wildcard pattern string but got {value} of {type(value)}') + raise ValueError( + 'expected wildcard pattern string, ' + f'but got {value} of {type(value)}') return constraint._Relation(self, constraint._Operator._LIKE, value) diff --git a/pypict/_builder/types.py b/pypict/_builder/types.py index dc23544..d613405 100644 --- a/pypict/_builder/types.py +++ b/pypict/_builder/types.py @@ -4,4 +4,4 @@ NumericType = numbers.Real StringType = str -DataTypes = Union[NumericType, StringType] \ No newline at end of file +DataTypes = Union[NumericType, StringType] diff --git a/pypict/builder.py b/pypict/builder.py index a14eaf8..e1bbc82 100644 --- a/pypict/builder.py +++ b/pypict/builder.py @@ -1,3 +1,3 @@ -from pypict._builder.model import Model -from pypict._builder.parameter import Parameter -from pypict._builder.constraint import ALL, ANY, NOT, IF +from pypict._builder.model import Model # NOQA +from pypict._builder.parameter import Parameter # NOQA +from pypict._builder.constraint import ALL, ANY, NOT, IF # NOQA diff --git a/tests/builder_tests/test_builder.py b/tests/builder_tests/test_builder.py index c839ead..624be35 100644 --- a/tests/builder_tests/test_builder.py +++ b/tests/builder_tests/test_builder.py @@ -3,9 +3,11 @@ from pypict.builder import Model, Parameter from pypict.builder import IF + class TestBuilder(unittest.TestCase): def test_basic_usecase(self): - type = Parameter('Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) + type = Parameter( + 'Type', ['Single', 'Span', 'Stripe', 'Mirror', 'RAID-5']) size = Parameter('Size', [10, 100, 500, 1000, 5000, 10000, 40000]) filesys = Parameter('File System', ['FAT', 'FAT32', 'NTFS']) @@ -15,4 +17,4 @@ def test_basic_usecase(self): IF(filesys == 'FAT').THEN(size <= 4096), IF(filesys == 'FAT32').THEN(size <= 32000), ) - print(m.to_string()) \ No newline at end of file + print(m.to_string()) From 0e585b8a1cb3245645ef606f382555a93178e578 Mon Sep 17 00:00:00 2001 From: Kenichi Maehashi Date: Sun, 20 Jun 2021 23:49:45 +0900 Subject: [PATCH 13/13] wip --- pypict/_builder/constraint.py | 2 +- pypict/_builder/{types.py => dtype.py} | 0 pypict/_builder/parameter.py | 2 +- pypict/_builder/{seed.py => seeding.py} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename pypict/_builder/{types.py => dtype.py} (100%) rename pypict/_builder/{seed.py => seeding.py} (100%) diff --git a/pypict/_builder/constraint.py b/pypict/_builder/constraint.py index 007ed7e..4c43cc2 100644 --- a/pypict/_builder/constraint.py +++ b/pypict/_builder/constraint.py @@ -3,7 +3,7 @@ from typing import Dict, Iterable, Optional, Union from pypict._builder import parameter -from pypict._builder.types import NumericType, StringType, DataTypes +from pypict._builder.dtype import NumericType, StringType, DataTypes """ diff --git a/pypict/_builder/types.py b/pypict/_builder/dtype.py similarity index 100% rename from pypict/_builder/types.py rename to pypict/_builder/dtype.py diff --git a/pypict/_builder/parameter.py b/pypict/_builder/parameter.py index 01401f2..0bc78dc 100644 --- a/pypict/_builder/parameter.py +++ b/pypict/_builder/parameter.py @@ -1,7 +1,7 @@ from typing import Iterable, Tuple, Union from pypict._builder import constraint -from pypict._builder.types import NumericType, StringType, DataTypes +from pypict._builder.dtype import NumericType, StringType, DataTypes # Values can be a list of (literals or tuple of (literal, weight)). diff --git a/pypict/_builder/seed.py b/pypict/_builder/seeding.py similarity index 100% rename from pypict/_builder/seed.py rename to pypict/_builder/seeding.py