This is too easy: sharing code in scripts

5 views
Skip to first unread message

Edward K. Ream

unread,
Aug 12, 2010, 6:12:02 PM8/12/10
to leo-editor
Let's say I have common code that I want to include in several unit
test. Say

class Hello():

def __init__(self,name='john'):
self.name=name
print('hello %s' % name)


Within a script, I could do something like:

p2 = g.findNodeAnywhere(c,'Common unit test code')
p3 = g.findNodeInTree(c,p2,'class Hello')
exec(p3.b+'\n')

Now the definition of class Hello is available to my script or unit
test(!)

Hello('Bob')

This is something that I've wanted to do forever. It's almost too
easy. I suppose it's a small security problem, but so is every other
@test node!

Edward

Edward K. Ream

unread,
Aug 12, 2010, 6:52:30 PM8/12/10
to leo-editor
On Aug 12, 5:12 pm, "Edward K. Ream" <edream...@gmail.com> wrote:

> Within a script, I could do something like:
>
> p2 = g.findNodeAnywhere(c,'Common unit test code')
> p3 = g.findNodeInTree(c,p2,'class Hello')
> exec(p3.b+'\n')
>
> Now the definition of class Hello is available to my script or unit
> test(!)

Naturally, I wanted to shorten this :-)

The best I can do at present is:

exec(g.findTestScript(c,'@common leoRst test code'))

where g.findTestScript is::

def findTestScript(c,h,where=None):
if where:
p = g.findNodeAnywhere(c,where)
if p:
p = g.findNodeInTree(c,p,h)
else:
p = g.findNodeAnywhere(c,h)

if p:
return g.getScript(c,p) # Takes care with ending newline.
else:
return None

This is pretty good. In particular, calling g.getScript means that
@others and section references are valid in the found node! That is,
the script can potentially include the entire tree.

I would also like to eliminate the call to exec in the unit test.
That is, I would like to do the exec within findTestScrip. If I can
do so, I would change the name to something like g.addTestCode.

The problem is that exec adds the definitions in the *local* context,
and that context is the context of findTestScript, not the caller.
This problem should be solvable...

Edward

Edward K. Ream

unread,
Aug 12, 2010, 7:12:56 PM8/12/10
to leo-editor


On Aug 12, 5:52 pm, "Edward K. Ream" <edream...@gmail.com> wrote:

> The problem is that exec adds the definitions in the *local* context,
> and that context is the context of findTestScript, not the caller.
> This problem should be solvable...

The following code discovers what symbols have been added by the exec:

_addTestScript_s = g.getScript(c,p)
_addTestScript_d = None # To put it in locals
_addTestScript_d = locals().copy()
exec(_addTestScript_s)
added = [z for z in locals() if not z in _addTestScript_d]
print(added)

I use the strange symbols to ensure the names here won't clash with
the names in the script being executed.

After the exec, the 'added' array contains 'Hello' as expected and
locals().get('Hello') yields <class 'Hello'>.

So the only task left is to add the following list to the locals of
the *caller*:

[locals.get(z) for z in added]

Edward

Edward K. Ream

unread,
Aug 12, 2010, 7:28:10 PM8/12/10
to leo-editor
On Aug 12, 6:12 pm, "Edward K. Ream" <edream...@gmail.com> wrote:

> So the only task left is to add the following list to the locals of
> the *caller*:
>
>     [locals.get(z) for z in added]

Alas, according to the Python docs, it is invalid to alter the dict
return by locals().

In theory, we could alter the context of the exec in
generalTestCase.runTest. This is the code that executes the body
text of @test nodes, so we would have to hack the *headline* to
indicate the added code. This is too ugly to contemplate seriously.

In short, this looks like the exec in the unit test is the best that
can be done::

exec(g.findTestScript(c,'@common leoRst test code'))

I can live with this.

Anyone have any ideas how an exec can modify the context of the
caller?

Edward

Edward K. Ream

unread,
Aug 12, 2010, 7:37:03 PM8/12/10
to leo-editor


On Aug 12, 6:28 pm, "Edward K. Ream" <edream...@gmail.com> wrote:

> [This] is the best that can be done::
>
>     exec(g.findTestScript(c,'@common leoRst test code'))

This is pretty damn good. In other Python scripting environments, one
would presumably get the shared code using import or execfile. This
is a much more convenient solution: I can update the common code
without changing any file at all.

Indeed, I want to do this precisely because I do not want to have to
do something like this:

import leo.core.leoTest as leoTest
leoTest.Hello('Bob')

It's not that this is more typing, it's that it is considerably less
dynamic. Worse, the common code belongs with the unit tests, not in
leoTest.py.

Edward

Terry Brown

unread,
Aug 12, 2010, 9:45:29 PM8/12/10
to leo-e...@googlegroups.com
On Thu, 12 Aug 2010 16:28:10 -0700 (PDT)
"Edward K. Ream" <edre...@gmail.com> wrote:

> Anyone have any ideas how an exec can modify the context of the
> caller?

http://docs.python.org/release/2.6.5/reference/simple_stmts.html#exec

is hard to read.

but I think you can write

exec g.findTestScript(c,'@common leoRst test code')

or

exec code_string in unified_context

or

exec code_string in globals_context, locals_context

you can do

exec code_string in globals(), locals()

although that seems to be offering code_string the opportunity to modify locals() without even knowing it's doing it.

Cheers -Terry

Edward K. Ream

unread,
Aug 12, 2010, 11:18:59 PM8/12/10
to leo-e...@googlegroups.com
On Thu, Aug 12, 2010 at 8:45 PM, Terry Brown <terry_...@yahoo.com> wrote:

> but I think you can write
>
>    exec g.findTestScript(c,'@common leoRst test code')

Sure, but I wanted to get rid of the explicit exec and put it in
g.findTestScript.

No matter. I'm resigned to the exec. It kinda growing on me.

Experimentation shows that it is *really* nice to be able to tweak the
test harness in unitTest.leo rather than leoTest.py. This is a
surprisingly big step forward.

Edward

Reply all
Reply to author
Forward
0 new messages