Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve instantiate error message in case where _target_ cannot be loaded #1864

Merged
merged 11 commits into from
Nov 1, 2021

Conversation

Jasha10
Copy link
Collaborator

@Jasha10 Jasha10 commented Oct 22, 2021

This PR improves the error handling of hydra.utils.instantiate for the case
where _target_ cannot be properly resolved.

Closes #1863

Below is a summary of the changed error handling (with tracebacks condensed).

The case where a module cannot be found:

Before:

>>> instantiate({"_target_": "foo"})
Traceback (most recent call last):
  ...
ValueError: Empty module name
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Error loading module 'foo'

Above, note the confusing ValueError: Empty module name.

After:

>>> instantiate({"_target_": "foo"})
Traceback (most recent call last):
  ...
ModuleNotFoundError: No module named 'foo'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Error loading module 'foo'

The case where an attribute cannot be found on an object:

Before:

>>> instantiate({"_target_": "pickle.dumps.missing_attribute"})
  ...
ModuleNotFoundError: No module named 'pickle.dumps'; 'pickle' is not a package
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Encountered error: `No module named 'pickle.dumps'; 'pickle' is not a package` when loading module 'pickle.dumps.missing_attribute'

Note above the misleading ModuleNotFoundError: No module named 'pickle.dumps'; 'pickle' is not a package.

After:

>>> instantiate({"_target_": "pickle.dumps.missing_attribute"})
Traceback (most recent call last):
  ...
AttributeError: 'builtin_function_or_method' object has no attribute 'missing_attribute'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Encountered AttributeError while loading 'pickle.dumps.missing_attribute': 'builtin_function_or_method' object has no attribute 'missing_attribute'

The case where an attribute cannot be found on a module (and no submodule can be found with the correct name):

Before:

>>> instantiate({"_target_": "pickle.typo"})
Traceback (most recent call last):
  ...
ModuleNotFoundError: No module named 'pickle.typo'; 'pickle' is not a package
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Encountered error: `No module named 'pickle.typo'; 'pickle' is not a package` when loading module 'pickle.typo'

After:

>>> instantiate({"_target_": "pickle.typo"})
Traceback (most recent call last):
  ...
AttributeError: module 'pickle' has no attribute 'typo'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Encountered AttributeError while loading 'pickle.typo': module 'pickle' has no attribute 'typo'

The case where a module contains a syntax error or other error (as in #1863)

# my_module.py
class Foo  # syntax error
    print("Hi")

Before:

>>> instantiate({"_target_": "my_module.Foo"})
Traceback (most recent call last):
  ...
ValueError: Empty module name
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Error loading module 'my_module.Foo'

After:

>>> instantiate({"_target_": "my_module.Foo"})
Traceback (most recent call last):
  ...
  File "/home/jasha10/hydra.git/my_module.py", line 2
    class Foo  # syntax error
             ^
SyntaxError: invalid syntax
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  ...
ImportError: Error loading module 'my_module.Foo'

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 22, 2021
except Exception as e:
if n == 0:
if n == 1:
Copy link

@robogast robogast Oct 22, 2021

Choose a reason for hiding this comment

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

What do you think of:

for n in range(len(parts), 0, -1):
    mod = ".".join(parts[:n])
    try:
        obj = import_module(mod)
    except Exception as e:
        continue
    break
else:
    raise ImportError(f"Error loading module '{path}'") from e
  1. The reversed statement can be incorporated in range
  2. The real try statement is around import_module; it's not intended for ".".join(parts[:n]) to raise an exception (that would be a bug). So you can move the mod = ... before the try.
  3. You can replace if n == 1: with the use of the for/else statement https://book.pythontips.com/en/latest/for_-_else.html#else-clause
  4. If you replace the if n == 1: with for/else you can even replace n in ... mod = ... with:
for mod in (
  ".".join(parts[:n]) for n in range(len(parts), 0, -1)
):

Just commenting because I like the code puzzle :), feel free to ignore it if you want.

Edit:
Ah apparently the exception e gets cleared when exiting the except: https://docs.python.org/3.3/reference/compound_stmts.html#the-try-statement, so then in the else you have to do something like:

    except Exception as e:
      _e = e
...
else:
    raise ImportError(f"Error loading module '{path}'") from _e

But I think that's a bit ugly 😅

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for your review @robogast!

Edit: Ah apparently the exception e gets cleared when exiting the except

I ran into this same issue when I was working on the PR 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

1 & 4. Yes, but I think that reversed is slightly easier to understand for future readers -- There is already a lot going on with the indices here :)
2. Agreed! Updated in 5de3754.
3. I wonder why python clears the exception e after the except clause... Too bad :P

@Jasha10 Jasha10 marked this pull request as draft October 22, 2021 11:21
@Jasha10 Jasha10 marked this pull request as ready for review October 22, 2021 11:49
Previously (before this PR), an ImportError was raised if a submodule
mod_a.mod_b could not be found and module and also mod_a has no
attribute named `mod_b`. An earlier commit in this PR changed this
ImportError behavior to raise an AttributeError instead. This commit
reverts that behavior -- now once again an ImportError is raised.
if n == 0:
raise ImportError(f"Error loading module '{path}'") from e
obj = import_module(mod)
except Exception as exc_import:

Choose a reason for hiding this comment

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

Also; as far as I can find, import_module should only throw an ImportError, so maybe change the generic Exception to ImportError?

Copy link
Collaborator Author

@Jasha10 Jasha10 Oct 25, 2021

Choose a reason for hiding this comment

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

This except clause also catches errors raised by the module that's being imported (e.g. if the imported module contains a line that says assert False).

Another option here would be to just catch ImportError and let the exceptions from mod bubble up to the user, but that would be potentially breaking change to Hydra's API.

Choose a reason for hiding this comment

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

A change for Hydra 2.0 then :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea :)

import_module(mod)
except ModuleNotFoundError:
pass
except Exception as exc_import:

Choose a reason for hiding this comment

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

Same comment as above, and as ModuleNotFoundError is already handled above, catching the more generic ImportError here should be OK, as import_module should only throw an ImportError.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same comment as above -- exceptions raised by mod itself (rather than by import_module) are caught here.

Copy link
Contributor

@jieru-hu jieru-hu left a comment

Choose a reason for hiding this comment

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

lgtm! thanks for improving this. only one small nit.

Comment on lines +41 to +48
param(
"datetime",
raises(
ValueError,
match=re.escape("Invalid type (<class 'module'>) found for datetime"),
),
id="top_level_module",
),
Copy link
Contributor

Choose a reason for hiding this comment

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

nice - we can probably remove/update the comment in line 600 in hydra/_internal/utils.py

(github won't let me comment on that line)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! Updated in e716b17.

@Jasha10 Jasha10 merged commit 043ec9b into facebookresearch:main Nov 1, 2021
@Jasha10 Jasha10 deleted the closes1863 branch November 1, 2021 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Opaque error message when attempting to instantiate object from a module that has errors
4 participants