Skip to content
This repository was archived by the owner on Mar 23, 2023. It is now read-only.
Open
12 changes: 7 additions & 5 deletions compiler/stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,15 @@ def visit_Import(self, node):
self.block.bind_var(self.writer, asname, mod.expr)

def visit_ImportFrom(self, node):
# Wildcard imports are not yet supported.
self._write_py_context(node.lineno)
for alias in node.names:
if alias.name == '*':
msg = 'wildcard member import is not implemented: from %s import %s' % (
node.module, alias.name)
raise util.ParseError(node, msg)
self._write_py_context(node.lineno)
module_name = node.module

with self._import(module_name, module_name.count('.')) as module:
self.writer.write_checked_call1(
'πg.LoadMembers(πF, {})', module.expr)
return
if node.module.startswith(_NATIVE_MODULE_PREFIX):
values = [alias.name for alias in node.names]
with self._import_native(node.module, values) as mod:
Expand Down
14 changes: 6 additions & 8 deletions compiler/stmt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,12 @@ def testImportFromFutureParseError(self):
self.assertRaisesRegexp(util.ParseError, want_regexp,
stmt.import_from_future, node)

def testImportWildcardMemberRaises(self):
regexp = r'wildcard member import is not implemented: from foo import *'
self.assertRaisesRegexp(util.ParseError, regexp, _ParseAndVisit,
'from foo import *')
regexp = (r'wildcard member import is not '
r'implemented: from __go__.foo import *')
self.assertRaisesRegexp(util.ParseError, regexp, _ParseAndVisit,
'from __go__.foo import *')
def testImportWildcard(self):
result = _GrumpRun(textwrap.dedent("""\
from time import *
print sleep"""))
self.assertEqual(0, result[0])
self.assertIn('<function sleep at', result[1])

def testVisitFuture(self):
testcases = [
Expand Down
61 changes: 61 additions & 0 deletions runtime/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,67 @@ func ImportNativeModule(f *Frame, name string, members map[string]*Object) (*Obj
return prev, nil
}

// LoadMembers scans over all the members in module
// and populates globals with them, taking __all__ into
// account.
func LoadMembers(f *Frame, module *Object) *BaseException {
allAttr, raised := GetAttr(f, module, NewStr("__all__"), nil)
if raised != nil && !raised.isInstance(AttributeErrorType) {
return raised
}
f.RestoreExc(nil, nil)

if raised == nil {
raised = loadMembersFromIterable(f, module, allAttr, nil)
if raised != nil {
return raised
}
return nil
}

// Fall back on __dict__
dictAttr, raised := GetAttr(f, module, NewStr("__dict__"), nil)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's simpler to use the dict field of the Object struct: module.dict

if raised != nil && !raised.isInstance(AttributeErrorType) {
return raised
}
raised = loadMembersFromIterable(f, module, dictAttr, func(key *Object) bool {
return strings.HasPrefix(toStrUnsafe(key).value, "_")
})
if raised != nil {
return raised
}
return nil
}

func loadMembersFromIterable(f *Frame, module, iterable *Object, filterF func(*Object) bool) *BaseException {
iter, raised := Iter(f, iterable)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use seqForEach from seq.go here?

if raised != nil {
return raised
}

memberName, raised := Next(f, iter)
for ; raised == nil; memberName, raised = Next(f, iter) {
member, raised := GetAttr(f, module, toStrUnsafe(memberName), nil)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memberName may not be a string so this unsafe conversion is not .. safe :)

It's worth looking at the CPython source to see what happens with non-string members in __all__ or in the globals dict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I read the source code and it seems to raise several errors:

  • lines 48888 and 4893 seem to be unexpected errors, don't think we have to map these
  • line 4895 if the module doesn't contain either one of all and dict. It doesn't seem like it's possible in the Go code
  • line 4909 if all or dict (whichever gets chosen) is not iterable. Throws IndexError
  • line 4924 if the member name is not a string, or the module does not contain the member
  • lines 4926 and 4928 throw errors if locals is malformed, the member name is already validated from above.

Looking over the exceptions, there are already a few covered ones, without changing the code:

  • IndexError when trying to iterate over a non-iterable object
  • AttributeError when the module does not have a requested member

So, what I should focus on is:

  • TypeError is the member name is not actually a string
  • SystemError if the Globals (locals) is malformed. I don't know how to replicate that in real-life though, so I couldn't test in the shell, I just went through the code.

WDYT?

if raised != nil {
return raised
}
if filterF != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if filterF != nil && filterF(memberName) {

if filterF(memberName) {
continue
}
}
raised = f.Globals().SetItem(f, memberName, member)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe stash the globals dict in a local variable at the top of the function.

if raised != nil {
return raised
}
}
if !raised.isInstance(StopIterationType) {
return raised
}
f.RestoreExc(nil, nil)
return nil
}

// newModule creates a new Module object with the given fully qualified name
// (e.g a.b.c) and its corresponding Python filename.
func newModule(name, filename string) *Module {
Expand Down
48 changes: 48 additions & 0 deletions runtime/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,54 @@ func TestImportNativeModule(t *testing.T) {
}
}

func TestLoadMembers(t *testing.T) {
f := NewRootFrame()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend using the the runInvokeTestCase pattern used in other tests if at all possible. It takes care of a number of things like frame creation and handling exceptions. You could return the globals dictionary that gets populated and check that against your expectation. Something like:

fun := wrapFuncForTest(f *Frame, m *Object) (*Dict, *BaseException) {
  raised := LoadMembers(f, m)
  if raised != nil {
    return nil, raised
  }
  return f.Globals(), nil
}
cases := []invokeTestCase{
  {args: fooAllDefinedModule, want: newTestDict(var1, val1, var2, val2)},
}
for _, cas := range cases {
  if err := runInvokeTestCase(fun, &cas); err != "" {
    t.Error(err)
  }
}

var1 := NewStr("var1").ToObject()
var2 := NewStr("_var2").ToObject()
var3 := NewStr("var3").ToObject()
nameAttr := NewStr("__name__").ToObject()
allAttr := NewStr("__all__")
val1 := NewStr("val1").ToObject()
val2 := NewStr("val2").ToObject()
val3 := NewStr("val3").ToObject()
nameValue := NewStr("foo").ToObject()
allValue := newTestList(var1, var2).ToObject()

allDefinedDict := newTestDict(var1, val1, var2, val2, var3, val3, nameAttr, nameValue, allAttr, allValue)
fooAllDefinedModule := &Module{Object: Object{typ: testModuleType, dict: allDefinedDict}}
allUndefinedDict := newTestDict(var1, val1, var2, val2, var3, val3, nameAttr, nameValue)
fooAllUndefinedModule := &Module{Object: Object{typ: testModuleType, dict: allUndefinedDict}}

cases := []struct {
module *Module
wantGlobals *Dict
}{
{
fooAllDefinedModule,
newTestDict(var1, val1, var2, val2),
},
{
fooAllUndefinedModule,
newTestDict(var1, val1, var3, val3),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to test some of the corner cases like when __all__ is not iterable.

},
}
for _, cas := range cases {
f.globals = NewDict()
raised := LoadMembers(f, cas.module.ToObject())
if raised != nil {
t.Errorf("LoadMmembers: raised %v", raised)
}
ne := mustNotRaise(NE(f, f.Globals().ToObject(), cas.wantGlobals.ToObject()))
b, raised := IsTrue(f, ne)
if raised != nil {
panic(raised)
}
if b {
t.Errorf("LoadMembers: Globals() = %v, want %v", f.Globals(), cas.wantGlobals)
}
}
}

func TestModuleGetNameAndFilename(t *testing.T) {
fun := wrapFuncForTest(func(f *Frame, m *Module) (*Tuple, *BaseException) {
name, raised := m.GetName(f)
Expand Down