Skip to content

Commit

Permalink
Merge pull request #397 from brian-team/unknown_attributes
Browse files Browse the repository at this point in the history
Raise an error when setting a new attribute for a `Group`
  • Loading branch information
mstimberg committed Feb 2, 2015
2 parents 97a2307 + 401d9f5 commit 8f50f17
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 18 deletions.
30 changes: 29 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -618,4 +618,32 @@ subject to the following conditions:
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
OTHER DEALINGS IN THE SOFTWARE.

-------------------------------------------------------------------------------

The spelling corrector code in brian2.utils.stringtools is based on the spelling
corrector by Peter Norvig, available at: http://norvig.com/spell.py
It is published under the MIT license:

The MIT License (MIT)

Copyright (c) 2007 Peter Norvig

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
52 changes: 50 additions & 2 deletions brian2/groups/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
get_unit)
from brian2.units.allunits import second
from brian2.utils.logger import get_logger
from brian2.utils.stringtools import get_identifiers
from brian2.utils.stringtools import get_identifiers, SpellChecker

__all__ = ['Group', 'CodeRunner']

Expand Down Expand Up @@ -371,8 +371,55 @@ def __setattr__(self, name, val, level=0):
var.get_addressable_value(name[:-1], self).set_item(slice(None),
val,
level=level+1)
else:
elif hasattr(self, name) or name.startswith('_'):
object.__setattr__(self, name, val)
else:
# Try to suggest the correct name in case of a typo
checker = SpellChecker([varname for varname, var in self.variables.iteritems()
if not (varname.startswith('_') or var.read_only)])
if name.endswith('_'):
suffix = '_'
name = name[:-1]
else:
suffix = ''
error_msg = 'Could not find a state variable with name "%s".' % name
suggestions = checker.suggest(name)
if len(suggestions) == 1:
suggestion, = suggestions
error_msg += ' Did you mean to write "%s%s"?' % (suggestion,
suffix)
elif len(suggestions) > 1:
error_msg += (' Did you mean to write any of the following: %s ?' %
(', '.join(['"%s%s"' % (suggestion, suffix)
for suggestion in suggestions])))
error_msg += (' Use the add_attribute method if you intend to add '
'a new attribute to the object.')
raise AttributeError(error_msg)

def add_attribute(self, name):
'''
Add a new attribute to this group. Using this method instead of simply
assigning to the new attribute name is necessary because Brian will
raise an error in that case, to avoid bugs passing unnoticed
(misspelled state variable name, un-declared state variable, ...).
Parameters
----------
name : str
The name of the new attribute
Raises
------
AttributeError
If the name already exists as an attribute or a state variable.
'''
if name in self.variables:
raise AttributeError('Cannot add an attribute "%s", it is already '
'a state variable of this group.' % name)
if hasattr(self, name):
raise AttributeError('Cannot add an attribute "%s", it is already '
'an attribute of this group.' % name)
object.__setattr__(self, name, None)

def get_states(self, vars=None, units=True, format='dict', level=0):
'''
Expand Down Expand Up @@ -856,6 +903,7 @@ def __init__(self, group, template, code='', user_code=None,
if codeobj_class is None:
codeobj_class = group.codeobj_class
self.codeobj_class = codeobj_class
self.codeobj = None

def update_abstract_code(self, run_namespace=None, level=0):
'''
Expand Down
4 changes: 2 additions & 2 deletions brian2/groups/poissongroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ def __init__(self, N, rates, dt=None, clock=None, when='thresholds',

self._refractory = False

#
self._enable_group_attributes()
# To avoid a warning about the local variable rates, we set the real
# threshold condition only after creating the object
self.threshold = 'False'
self.thresholder = Thresholder(self)
self.threshold = 'rand() < rates * dt'
self.contained_objects.append(self.thresholder)

self._enable_group_attributes()

# Here we want to use the local namespace, but at the level where the
# constructor was called
self.rates.set_item(slice(None), rates, level=2)
Expand Down
6 changes: 3 additions & 3 deletions brian2/groups/spikegeneratorgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,6 @@ def __init__(self, N, indices, times, dt=None, clock=None,
dtype=np.int32, read_only=True)
self.variables.create_clock_variables(self._clock)

# Activate name attribute access
self._enable_group_attributes()

#: Remember the dt we used the last time when we checked the spike bins
#: to not repeat the work for multiple runs with the same dt
self._previous_dt = None
Expand All @@ -132,6 +129,9 @@ def __init__(self, N, indices, times, dt=None, clock=None,
order=order,
name=None)

# Activate name attribute access
self._enable_group_attributes()

def before_run(self, run_namespace=None, level=0):
# Do some checks on the period vs. dt
if self.period < np.inf*second:
Expand Down
17 changes: 9 additions & 8 deletions brian2/spatialneuron/spatialneuron.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ def __init__(self, morphology=None, model=None, threshold=None,
""")
# Possibilities for the name: characteristic_length, electrotonic_length, length_constant, space_constant

# Insert morphology
self.morphology = morphology

# Link morphology variables to neuron's state variables
self.morphology_data = MorphologyData(len(morphology))
self.morphology.compress(self.morphology_data)

NeuronGroup.__init__(self, len(morphology), model=model + eqs_constants,
threshold=threshold, refractory=refractory,
reset=reset,
Expand All @@ -220,13 +227,6 @@ def __init__(self, morphology=None, model=None, threshold=None,

self.Cm = Cm
self.Ri = Ri

# Insert morphology
self.morphology = morphology

# Link morphology variables to neuron's state variables
self.morphology_data = MorphologyData(self.N)
self.morphology.compress(self.morphology_data)
# TODO: View instead of copy for runtime?
self.diameter_ = self.morphology_data.diameter
self.distance_ = self.morphology_data.distance
Expand All @@ -237,6 +237,7 @@ def __init__(self, morphology=None, model=None, threshold=None,
self.z_ = self.morphology_data.z

# Performs numerical integration step
self.add_attribute('diffusion_state_updater')
self.diffusion_state_updater = SpatialStateUpdater(self, method,
clock=self.clock,
order=order)
Expand Down Expand Up @@ -323,8 +324,8 @@ class SpatialSubgroup(Subgroup):
'''

def __init__(self, source, start, stop, morphology, name=None):
Subgroup.__init__(self, source, start, stop, name)
self.morphology = morphology
Subgroup.__init__(self, source, start, stop, name)

def __getattr__(self, x):
return SpatialNeuron.spatialneuron_attribute(self, x)
Expand Down
3 changes: 2 additions & 1 deletion brian2/synapses/synapses.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,10 +746,11 @@ def __init__(self, source, target=None, model=None, pre=None, post=None,
# Support 2d indexing
self._indices = SynapticIndexing(self)

self._initial_connect = connect

# Activate name attribute access
self._enable_group_attributes()

self._initial_connect = connect
if not connect is False:
self.connect(connect, level=1)

Expand Down
18 changes: 18 additions & 0 deletions brian2/tests/test_neurongroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,23 @@ def test_state_variable_access_strings():
assert_equal(G.v[:], np.arange(10)*volt)


@attr('codegen-independent')
def test_unknown_state_variables():
# Test how setting attribute names that do not correspond to a state
# variable are handled
G = NeuronGroup(10, 'v : 1')
assert_raises(AttributeError, lambda: setattr(G, 'unknown', 42))

# Creating a new private attribute should be fine
G._unknown = 42
assert G._unknown == 42

# Explicitly create the attribute
G.add_attribute('unknown')
G.unknown = 42
assert G.unknown == 42


def test_subexpression():
G = NeuronGroup(10, '''dv/dt = freq : 1
freq : Hz
Expand Down Expand Up @@ -1123,6 +1140,7 @@ def test_random_vector_values():
test_state_variables()
test_state_variable_access()
test_state_variable_access_strings()
test_unknown_state_variables()
test_subexpression()
test_subexpression_with_constant()
test_scalar_parameter_access()
Expand Down
11 changes: 10 additions & 1 deletion brian2/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from nose.plugins.attrib import attr

from brian2.utils.environment import running_from_ipython

from brian2.utils.stringtools import SpellChecker

@attr('codegen-independent')
def test_environment():
Expand Down Expand Up @@ -29,5 +29,14 @@ def test_environment():
del builtins.__IPYTHON__


def test_spell_check():
checker = SpellChecker(['vm', 'alpha', 'beta'])
assert checker.suggest('Vm') == set(['vm'])
assert checker.suggest('alphas') == set(['alpha'])
assert checker.suggest('bta') == set(['beta'])
assert checker.suggest('gamma') == set()


if __name__ == '__main__':
test_environment()
test_spell_check()
41 changes: 41 additions & 0 deletions brian2/utils/stringtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import re
import string

__all__ = ['indent',
'deindent',
Expand All @@ -13,6 +14,7 @@
'stripped_deindented_lines',
'strip_empty_leading_and_trailing_lines',
'code_representation',
'SpellChecker'
]

def indent(text, numtabs=1, spacespertab=4, tab=None):
Expand Down Expand Up @@ -230,3 +232,42 @@ def code_representation(code):
msg += indent(str(v))
output.append(msg)
return strip_empty_leading_and_trailing_lines('\n'.join(output))


# The below is adapted from Peter Norvig's spelling corrector
# http://norvig.com/spell.py (MIT licensed)
class SpellChecker(object):
'''
A simple spell checker that will be used to suggest the correct name if the
user made a typo (e.g. for state variable names).
Parameters
----------
words : iterable of str
The known words
alphabet : iterable of str, optional
The allowed characters. Defaults to the characters allowed for
identifiers, i.e. ascii characters, digits and the underscore.
'''
def __init__(self, words,
alphabet=string.ascii_lowercase+string.digits+'_'):
self.words = words
self.alphabet = alphabet

def edits1(self, word):
s = [(word[:i], word[i:]) for i in range(len(word) + 1)]
deletes = [a + b[1:] for a, b in s if b]
transposes = [a + b[1] + b[0] + b[2:] for a, b in s if len(b)>1]
replaces = [a + c + b[1:] for a, b in s for c in self.alphabet if b]
inserts = [a + c + b for a, b in s for c in self.alphabet]
return set(deletes + transposes + replaces + inserts)

def known_edits2(self, word):
return set(e2 for e1 in self.edits1(word)
for e2 in self.edits1(e1) if e2 in self.words)

def known(self, words):
return set(w for w in words if w in self.words)

def suggest(self, word):
return self.known(self.edits1(word)) or self.known_edits2(word) or set()

0 comments on commit 8f50f17

Please sign in to comment.