Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement builder API #31

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
pushd example
python example.py
python example_capi.py
python example_builder.py
popd

- name: Run type checking
Expand Down
55 changes: 55 additions & 0 deletions example/example_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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',

NOT(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)))
2 changes: 2 additions & 0 deletions pypict/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

from pypict.capi import PAIRWISE_GENERATION # NOQA
from pypict.api import Task # NOQA

from pypict import builder # NOQA
Empty file added pypict/_builder/__init__.py
Empty file.
177 changes: 177 additions & 0 deletions pypict/_builder/constraint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import enum
import json
from typing import Dict, Iterable, Optional, Union

from pypict._builder import parameter
from pypict._builder.dtype import NumericType, StringType, DataTypes


"""
Implements the constraints grammar as defined in:
https://github.com/microsoft/pict/blob/main/doc/pict.md
"""


class _Operator(enum.Enum):
_GT = '>'
_GE = '>='
_LT = '<'
_LE = '<='
_EQ = '='
_NE = '<>'
_IN = 'IN'
_LIKE = 'LIKE'


class _Constraint:
def to_string(self) -> str:
raise NotImplementedError

def evaluate(self, combination: Dict[str, DataTypes]) -> bool:
raise NotImplementedError

def __str__(self) -> str:
return self.to_string()

def __repr__(self) -> str:
return f'<PICT constraint ("{str(self)}")>'

def __bool__(self) -> bool:
raise ValueError(
'cannot apply Python logical operators on constraints')


class _Predicate(_Constraint):
pass


class _Relation(_Predicate):
def __init__(
self,
param: 'parameter.Parameter',
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)} '
f'{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:
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)


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(
'expected predicate, '
f'but got {pred} of {type(pred)}')


class _LogicalOp(_Predicate):
_op: str = '' # 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._preds]) + ')'


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):
_check_predicates(pred)
self._pred = pred

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):
_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('THEN cannot be repeated')
self._then = pred
return self

def ELSE(self, pred: _Predicate) -> 'IF':
_check_predicates(pred)
if self._then is None:
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('THEN must be given')
return (
f'IF {self._if}' +
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
7 changes: 7 additions & 0 deletions pypict/_builder/dtype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import numbers
from typing import Union


NumericType = numbers.Real
StringType = str
DataTypes = Union[NumericType, StringType]
65 changes: 65 additions & 0 deletions pypict/_builder/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import List, Optional, Tuple

from pypict._builder.parameter import Parameter
from pypict._builder.constraint import _Constraint


class Model:
def __init__(self):
self._parameters: List[Parameter] = []
self._submodels: List[_SubModel] = []
self._constraints: List[_Constraint] = []

def parameters(self, *params: Parameter) -> 'Model':
self._parameters += 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) -> str:
lines: List[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('')

# 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() + ';')
lines.append('')

return '\n'.join(lines)


class _SubModel:
def __init__(self, params: Tuple[Parameter], order: Optional[int] = None):
self.params = params
self.order = order

def to_string(self) -> str:
return (
'{ ' +
', '.join([param.name for param in self.params]) +
' }' +
'' if self.order is None else f' @ {self.order}'
)
Loading