Skip to content

Commit 95361bd

Browse files
committed
Remove mutating add_outer_namespace and call to it in ModelDesc.from_formula
1 parent 76d4dfd commit 95361bd

File tree

2 files changed

+30
-39
lines changed

2 files changed

+30
-39
lines changed

patsy/desc.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,6 @@ def test_Term():
8383
assert Term([f2, f1]).name() == "b:a"
8484
assert Term([]).name() == "Intercept"
8585

86-
_builtins_dict = {}
87-
six.exec_("from patsy.builtins import *", {}, _builtins_dict)
88-
# This is purely to make the existence of patsy.builtins visible to systems
89-
# like py2app and py2exe. It's basically free, since the above line guarantees
90-
# that patsy.builtins will be present in sys.modules in any case.
91-
import patsy.builtins
92-
9386
class ModelDesc(object):
9487
"""A simple container representing the termlists parsed from a formula.
9588
@@ -167,7 +160,6 @@ def from_formula(cls, tree_or_string, factor_eval_env):
167160
tree = tree_or_string
168161
else:
169162
tree = parse_formula(tree_or_string)
170-
factor_eval_env.add_outer_namespace(_builtins_dict)
171163
value = Evaluator(factor_eval_env).eval(tree, require_evalexpr=False)
172164
assert isinstance(value, cls)
173165
return value

patsy/eval.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -139,20 +139,13 @@ def namespace(self):
139139
from the encapsulated environment."""
140140
return VarLookupDict(self._namespaces)
141141

142-
def add_outer_namespace(self, namespace):
143-
"""Expose the contents of a dict-like object to the encapsulated
144-
environment.
142+
def with_outer_namespace(self, outer_namespace):
143+
"""Return a new EvalEnvironment with an extra namespace added.
145144
146-
The given namespace will be checked last, after all existing namespace
147-
lookups have failed.
148-
"""
149-
# ModelDesc.from_formula unconditionally calls
150-
# eval_env.add_outer_namespace(builtins)
151-
# which means that if someone uses the same environment for a bunch of
152-
# formulas, our namespace chain will grow without bound, which would
153-
# suck.
154-
if id(namespace) not in self._namespace_ids():
155-
self._namespaces.append(namespace)
145+
This namespace will be used only for variables that are not found in
146+
any existing namespace, i.e., it is "outside" them all."""
147+
return self.__class__(self._namespaces + [outer_namespace],
148+
self.flags)
156149

157150
def eval(self, expr, source_name="<string>", inner_namespace={}):
158151
"""Evaluate some Python code in the encapsulated environment.
@@ -350,6 +343,10 @@ def test_EvalEnvironment_eval_namespace():
350343
env2 = EvalEnvironment.capture(0)
351344
assert env2.eval("2 * a") == 6
352345

346+
env3 = env.with_outer_namespace({"a": 10, "b": 3})
347+
assert env3.eval("2 * a") == 2
348+
assert env3.eval("2 * b") == 6
349+
353350
def test_EvalEnvironment_eval_flags():
354351
from nose.tools import assert_raises
355352
if sys.version_info >= (3,):
@@ -362,22 +359,26 @@ def test_EvalEnvironment_eval_flags():
362359
assert env.eval("a != 0") == True
363360
assert_raises(SyntaxError, env.eval, "a <> 0")
364361
assert env.subset(["a"]).flags == 0
362+
assert env.with_outer_namespace({"b": 10}).flags == 0
365363

366364
env2 = EvalEnvironment([{"a": 11}], flags=test_flag)
367365
assert env2.eval("a <> 0") == True
368366
assert_raises(SyntaxError, env2.eval, "a != 0")
369367
assert env2.subset(["a"]).flags == test_flag
368+
assert env2.with_outer_namespace({"b": 10}).flags == test_flag
370369
else:
371370
test_flag = __future__.division.compiler_flag
372371
assert test_flag & _ALL_FUTURE_FLAGS
373372

374373
env = EvalEnvironment([{"a": 11}], flags=0)
375374
assert env.eval("a / 2") == 11 // 2 == 5
376375
assert env.subset(["a"]).flags == 0
376+
assert env.with_outer_namespace({"b": 10}).flags == 0
377377

378378
env2 = EvalEnvironment([{"a": 11}], flags=test_flag)
379379
assert env2.eval("a / 2") == 11 * 1. / 2 != 5
380380
env2.subset(["a"]).flags == test_flag
381+
assert env2.with_outer_namespace({"b": 10}).flags == test_flag
381382

382383
def test_EvalEnvironment_subset():
383384
env = EvalEnvironment([{"a": 1}, {"b": 2}, {"c": 3}])
@@ -404,17 +405,12 @@ def test_EvalEnvironment_eq():
404405
env4 = capture_local_env()
405406
assert env3 != env4
406407

407-
def test_EvalEnvironment_add_outer_namespace():
408-
a = 1
409-
env = EvalEnvironment.capture(0)
410-
env2 = EvalEnvironment.capture(0)
411-
assert env.namespace["a"] == 1
412-
assert "b" not in env.namespace
413-
assert env == env2
414-
env.add_outer_namespace({"a": 10, "b": 2})
415-
assert env.namespace["a"] == 1
416-
assert env.namespace["b"] == 2
417-
assert env != env2
408+
_builtins_dict = {}
409+
six.exec_("from patsy.builtins import *", {}, _builtins_dict)
410+
# This is purely to make the existence of patsy.builtins visible to systems
411+
# like py2app and py2exe. It's basically free, since the above line guarantees
412+
# that patsy.builtins will be present in sys.modules in any case.
413+
import patsy.builtins
418414

419415
class EvalFactor(object):
420416
def __init__(self, code, origin=None):
@@ -464,10 +460,12 @@ def memorize_passes_needed(self, state, eval_env):
464460
# and that will be passed back to later memorize functions
465461
state["transforms"] = {}
466462

463+
eval_env = eval_env.with_outer_namespace(_builtins_dict)
467464
env_namespace = eval_env.namespace
468465
subset_names = [name for name in ast_names(self.code)
469466
if name in env_namespace]
470-
state["eval_env"] = eval_env.subset(subset_names)
467+
eval_env = eval_env.subset(subset_names)
468+
state["eval_env"] = eval_env
471469

472470
# example code: == "2 * center(x)"
473471
i = [0]
@@ -535,17 +533,17 @@ def new_name_maker(token):
535533

536534
return len(pass_bins)
537535

538-
def _eval(self, code, eval_env, memorize_state, data):
536+
def _eval(self, code, memorize_state, data):
539537
inner_namespace = VarLookupDict([data, memorize_state["transforms"]])
540538
return call_and_wrap_exc("Error evaluating factor",
541539
self,
542-
eval_env.eval,
543-
code, inner_namespace=inner_namespace)
540+
memorize_state["eval_env"].eval,
541+
code,
542+
inner_namespace=inner_namespace)
544543

545544
def memorize_chunk(self, state, which_pass, data):
546545
for obj_name in state["pass_bins"][which_pass]:
547546
self._eval(state["memorize_code"][obj_name],
548-
state["eval_env"],
549547
state,
550548
data)
551549

@@ -554,8 +552,9 @@ def memorize_finish(self, state, which_pass):
554552
state["transforms"][obj_name].memorize_finish()
555553

556554
def eval(self, memorize_state, data):
557-
return self._eval(memorize_state["eval_code"], memorize_state["eval_env"],
558-
memorize_state, data)
555+
return self._eval(memorize_state["eval_code"],
556+
memorize_state,
557+
data)
559558

560559
def test_EvalFactor_basics():
561560
e = EvalFactor("a+b")

0 commit comments

Comments
 (0)