|
6 | 6 | import re
|
7 | 7 | import fnmatch
|
8 | 8 | import warnings
|
| 9 | +from copy import copy |
9 | 10 | from collections import OrderedDict
|
10 | 11 |
|
11 | 12 | import numpy as np
|
@@ -92,7 +93,7 @@ def __init__(self, *args, **kwargs):
|
92 | 93 |
|
93 | 94 | if len(args) == 1:
|
94 | 95 | a0 = args[0]
|
95 |
| - if isinstance(a0, str): |
| 96 | + if isinstance(a0, basestring): |
96 | 97 | # assume a0 is a filename
|
97 | 98 | self.load(a0)
|
98 | 99 | else:
|
@@ -1392,6 +1393,155 @@ def display(k, v, is_metadata=False):
|
1392 | 1393 | return res
|
1393 | 1394 |
|
1394 | 1395 |
|
| 1396 | +# XXX: I wonder if we shouldn't create an AbstractSession instead of defining the _disabled() |
| 1397 | +# private method below. |
| 1398 | +# Auto-completion on any instance of a class that inherits from FrozenSession |
| 1399 | +# should not propose the add(), update(), filter(), transpose() and compact() methods |
| 1400 | +class FrozenSession(Session): |
| 1401 | + """ |
| 1402 | + The purpose of the present class is to be inherited by user defined classes where parameters |
| 1403 | + and variables of a model are defined (see examples below). These classes will allow users to |
| 1404 | + benefit from the so-called 'autocomplete' feature from development software such as PyCharm |
| 1405 | + (see the Notes section below) plus the main features of the :py:obj:`Session()` objects. |
| 1406 | +
|
| 1407 | + After creating an instance of a user defined 'session', some restrictions will be applied on it: |
| 1408 | +
|
| 1409 | + - **it is not possible to add or remove any parameter or variable,** |
| 1410 | + - **all non array variables (axes, groups, ...) cannot be modified,** |
| 1411 | + - **only values of array variables can be modified, not their axes.** |
| 1412 | +
|
| 1413 | + The reason of the first restriction is to avoid to select a deleted variable or |
| 1414 | + to miss an added one somewhere in the code when using the 'autocomplete' feature. |
| 1415 | + In other words, users can safely rely on the 'autocomplete' to write the model. |
| 1416 | +
|
| 1417 | + The reason of the second and third restrictions is to ensure the definition |
| 1418 | + of any variable or parameter to be constant throughout the whole code. |
| 1419 | + For example, a user don't need to remember that a new label has been added to |
| 1420 | + an axis of a given array somewhere earlier in the code (in a part of the model |
| 1421 | + written by a colleague). Therefore, these restrictions reduces the risk to deal |
| 1422 | + with unexpected error messages (like 'Incompatible Axes') and make it easier to |
| 1423 | + work in team. |
| 1424 | +
|
| 1425 | + Parameters |
| 1426 | + ---------- |
| 1427 | + filepath: str, optional |
| 1428 | + Path where items have been saved. This can be either the path to a single file, a path to |
| 1429 | + a directory containing .csv files or a pattern representing several .csv files. |
| 1430 | + meta : list of pairs or dict or OrderedDict or Metadata, optional |
| 1431 | + Metadata (title, description, author, creation_date, ...) associated with the array. |
| 1432 | + Keys must be strings. Values must be of type string, int, float, date, time or datetime. |
| 1433 | +
|
| 1434 | + Notes |
| 1435 | + ----- |
| 1436 | + The 'autocomplete' is a feature in which a software predicts the rest of a variable or function |
| 1437 | + name after a user typed the first letters. This feature allows users to use longer but meaningful |
| 1438 | + variable or function names (like 'population_be' instead of 'pbe') and to avoid creating an unwanted |
| 1439 | + new variable by misspelling the name of a given variable (e.g. typing 'poplation = something' |
| 1440 | + ('population' without u) will create a new variable instead of modifying the 'population' variable). |
| 1441 | +
|
| 1442 | + Examples |
| 1443 | + -------- |
| 1444 | + >>> class ModelVariables(FrozenSession): |
| 1445 | + ... LAST_AGE = 120 |
| 1446 | + ... FIRST_OBS_YEAR = 1991 |
| 1447 | + ... LAST_PROJ_YEAR = 2070 |
| 1448 | + ... AGE = Axis('age=0..{}'.format(LAST_AGE)) |
| 1449 | + ... GENDER = Axis('gender=male,female') |
| 1450 | + ... TIME = Axis('time={}..{}'.format(FIRST_OBS_YEAR, LAST_PROJ_YEAR)) |
| 1451 | + ... CHILDREN = AGE[0:17] |
| 1452 | + ... ELDERS = AGE[65:] |
| 1453 | + ... population = zeros((AGE, GENDER, TIME)) |
| 1454 | + ... births = zeros((AGE, GENDER, TIME)) |
| 1455 | + ... deaths = zeros((AGE, GENDER, TIME)) |
| 1456 | + >>> m = ModelVariables() |
| 1457 | + >>> m.names # doctest: +NORMALIZE_WHITESPACE |
| 1458 | + ['AGE', 'CHILDREN', 'ELDERS', 'FIRST_OBS_YEAR', 'GENDER', 'LAST_AGE', 'LAST_PROJ_YEAR', 'TIME', 'births', |
| 1459 | + 'deaths', 'population'] |
| 1460 | + """ |
| 1461 | + def __init__(self, filepath=None, meta=None): |
| 1462 | + # feed the kwargs dict with all items declared as class attributes |
| 1463 | + kwargs = {} |
| 1464 | + for key, value in vars(self.__class__).items(): |
| 1465 | + if not key.startswith('_'): |
| 1466 | + kwargs[key] = value |
| 1467 | + |
| 1468 | + if meta: |
| 1469 | + kwargs['meta'] = meta |
| 1470 | + |
| 1471 | + Session.__init__(self, **kwargs) |
| 1472 | + object.__setattr__(self, 'add', self._disabled) |
| 1473 | + |
| 1474 | + if filepath: |
| 1475 | + self.load(filepath) |
| 1476 | + |
| 1477 | + def __setitem__(self, key, value): |
| 1478 | + self._check_key_value(key, value) |
| 1479 | + |
| 1480 | + # we need to keep the attribute in sync (initially to mask the class attribute) |
| 1481 | + object.__setattr__(self, key, value) |
| 1482 | + self._objects[key] = value |
| 1483 | + |
| 1484 | + def __setattr__(self, key, value): |
| 1485 | + if key != 'meta': |
| 1486 | + self._check_key_value(key, value) |
| 1487 | + |
| 1488 | + # update the real attribute |
| 1489 | + object.__setattr__(self, key, value) |
| 1490 | + # update self._objects |
| 1491 | + Session.__setattr__(self, key, value) |
| 1492 | + |
| 1493 | + def _check_key_value(self, key, value): |
| 1494 | + cls = self.__class__ |
| 1495 | + attr_def = getattr(cls, key, None) |
| 1496 | + if attr_def is None: |
| 1497 | + raise ValueError("The '{item}' item has not been found in the '{cls}' class declaration. " |
| 1498 | + "Adding a new item after creating an instance of the '{cls}' class is not permitted." |
| 1499 | + .format(item=key, cls=cls.__name__)) |
| 1500 | + if (isinstance(value, (int, float, basestring, np.generic)) and value != attr_def) \ |
| 1501 | + or (isinstance(value, (Axis, Group)) and not value.equals(attr_def)): |
| 1502 | + raise TypeError("The '{key}' item is of kind '{cls_name}' which cannot by modified." |
| 1503 | + .format(key=key, cls_name=attr_def.__class__.__name__)) |
| 1504 | + if type(value) != type(attr_def): |
| 1505 | + raise TypeError("Expected object of type '{attr_cls}'. Got object of type '{value_cls}'." |
| 1506 | + .format(attr_cls=attr_def.__class__.__name__, value_cls=value.__class__.__name__)) |
| 1507 | + if isinstance(attr_def, Array): |
| 1508 | + try: |
| 1509 | + attr_def.axes.check_compatible(value.axes) |
| 1510 | + except ValueError as e: |
| 1511 | + msg = str(e).replace("incompatible axes:", "Incompatible axes for array '{key}':".format(key=key)) |
| 1512 | + raise ValueError(msg) |
| 1513 | + elif isinstance(value, np.ndarray) and value.shape != attr_def.shape: |
| 1514 | + raise ValueError("Incompatible shape for Numpy array '{key}'. " |
| 1515 | + "Expected shape {attr_shape} but got {value_shape}." |
| 1516 | + .format(key=key, attr_shape=attr_def.shape, value_shape=value.shape)) |
| 1517 | + |
| 1518 | + def copy(self): |
| 1519 | + instance = self.__class__() |
| 1520 | + for key, value in self.items(): |
| 1521 | + instance[key] = copy(value) |
| 1522 | + return instance |
| 1523 | + |
| 1524 | + def apply(self, func, *args, **kwargs): |
| 1525 | + kind = kwargs.pop('kind', Array) |
| 1526 | + instance = self.__class__() |
| 1527 | + for key, value in self.items(): |
| 1528 | + instance[key] = func(value, *args, **kwargs) if isinstance(value, kind) else value |
| 1529 | + return instance |
| 1530 | + |
| 1531 | + def _disabled(self, *args, **kwargs): |
| 1532 | + """This method will not work because adding or removing item and modifying axes of declared arrays |
| 1533 | + is not permitted.""" |
| 1534 | + raise ValueError( |
| 1535 | + "Adding or removing item and modifying axes of declared arrays is not permitted.".format( |
| 1536 | + cls=self.__class__.__name__ |
| 1537 | + ) |
| 1538 | + ) |
| 1539 | + |
| 1540 | + # XXX: not sure we should or not disable 'transpose()'? |
| 1541 | + __delitem__ = __delattr__ = _disabled |
| 1542 | + update = filter = transpose = compact = _disabled |
| 1543 | + |
| 1544 | + |
1395 | 1545 | def _exclude_private_vars(vars_dict):
|
1396 | 1546 | return {k: v for k, v in vars_dict.items() if not k.startswith('_')}
|
1397 | 1547 |
|
|
0 commit comments