Skip to content

Commit 226e2c0

Browse files
committed
fix #832 : implemented FrozenSession class
1 parent 8026246 commit 226e2c0

File tree

4 files changed

+850
-123
lines changed

4 files changed

+850
-123
lines changed

doc/source/api.rst

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,6 @@ Modifying
790790

791791
Session.add
792792
Session.update
793-
Session.get
794793
Session.apply
795794
Session.transpose
796795

@@ -816,6 +815,74 @@ Load/Save
816815
Session.to_hdf
817816
Session.to_pickle
818817

818+
.. _api-frozensession:
819+
820+
FrozenSession
821+
=============
822+
823+
.. autosummary::
824+
:toctree: _generated/
825+
826+
FrozenSession
827+
828+
Exploring
829+
---------
830+
831+
.. autosummary::
832+
:toctree: _generated/
833+
834+
FrozenSession.names
835+
FrozenSession.keys
836+
FrozenSession.values
837+
FrozenSession.items
838+
FrozenSession.summary
839+
840+
Copying
841+
-------
842+
843+
.. autosummary::
844+
:toctree: _generated/
845+
846+
FrozenSession.copy
847+
848+
Testing
849+
-------
850+
851+
.. autosummary::
852+
:toctree: _generated/
853+
854+
FrozenSession.element_equals
855+
FrozenSession.equals
856+
857+
Selecting
858+
---------
859+
860+
.. autosummary::
861+
:toctree: _generated/
862+
863+
FrozenSession.get
864+
865+
Modifying
866+
---------
867+
868+
.. autosummary::
869+
:toctree: _generated/
870+
871+
FrozenSession.apply
872+
873+
Load/Save
874+
---------
875+
876+
.. autosummary::
877+
:toctree: _generated/
878+
879+
FrozenSession.load
880+
FrozenSession.save
881+
FrozenSession.to_csv
882+
FrozenSession.to_excel
883+
FrozenSession.to_hdf
884+
FrozenSession.to_pickle
885+
819886
.. _api-editor:
820887

821888
Editor

larray/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
full_like, sequence, labels_array, ndtest, asarray, identity, diag,
1010
eye, all, any, sum, prod, cumsum, cumprod, min, max, mean, ptp, var,
1111
std, median, percentile, stack, zip_array_values, zip_array_items)
12-
from larray.core.session import Session, local_arrays, global_arrays, arrays
12+
from larray.core.session import Session, FrozenSession, local_arrays, global_arrays, arrays
1313
from larray.core.constants import nan, inf, pi, e, euler_gamma
1414
from larray.core.metadata import Metadata
1515
from larray.core.ufuncs import wrap_elementwise_array_func, maximum, minimum, where
@@ -58,7 +58,7 @@
5858
'all', 'any', 'sum', 'prod', 'cumsum', 'cumprod', 'min', 'max', 'mean', 'ptp', 'var', 'std',
5959
'median', 'percentile', 'stack', 'zip_array_values', 'zip_array_items',
6060
# session
61-
'Session', 'local_arrays', 'global_arrays', 'arrays',
61+
'Session', 'FrozenSession', 'local_arrays', 'global_arrays', 'arrays',
6262
# constants
6363
'nan', 'inf', 'pi', 'e', 'euler_gamma',
6464
# metadata

larray/core/session.py

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
import fnmatch
88
import warnings
9+
from copy import copy
910
from collections import OrderedDict
1011

1112
import numpy as np
@@ -92,7 +93,7 @@ def __init__(self, *args, **kwargs):
9293

9394
if len(args) == 1:
9495
a0 = args[0]
95-
if isinstance(a0, str):
96+
if isinstance(a0, basestring):
9697
# assume a0 is a filename
9798
self.load(a0)
9899
else:
@@ -1392,6 +1393,155 @@ def display(k, v, is_metadata=False):
13921393
return res
13931394

13941395

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+
13951545
def _exclude_private_vars(vars_dict):
13961546
return {k: v for k, v in vars_dict.items() if not k.startswith('_')}
13971547

0 commit comments

Comments
 (0)