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

Swap pkgutil.ImpImporter for importlib when loading custom modules from add-ons #14481

Merged
merged 14 commits into from
Jan 11, 2023

Conversation

LeonarddeR
Copy link
Collaborator

@LeonarddeR LeonarddeR commented Dec 28, 2022

Link to issue number:

Replaces #14405

Summary of the issue:

Addons have the ability to import custom code from the add-ons directory. This is used to load install tasks, for example.
This mechanism was based on a deprecated solution in pkgutil and allowed importing modules using backslashes, resulting in malformed module identifiers in sys.modules.

Description of user facing changes

  • Importing submodules with loadModule should be done with a dot separated name (i.e. loadModule("lib.example") instead of loadModule("lib\\example")
  • loadModule now raises an exception when a module can't be found

Description of development approach

We no longer rely on pkgutil.ImpImporter.

Testing strategy:

Tested importing a lib module from an example addon as well as modules without init.py and submodules. Also tested modules with malformed content, resulting in tracebacks.

Known issues with pull request:

  1. This would break cases where add-ons would use loadModule with a module name containing backslashes to import submodules. However, this scenario was broken anyway since it added malformed names to sys.modules. Note that this is strictly API breaking but I don't think noone was using it anyway.
  2. As loadModule now raises exceptions, add-on authors need to account for that when using this API.

Change log entries:

For Developers

  • Add-ons relying on addonHandler.getCodeAddon().loadModule to load custom add-on modules should now import submodules using dot instead of backslash as a separator. So if your addons folder contains a lib containing example.py, loadModule should be called with "lib.example" instead of "lib\example"
    API breaking changes:
  • addonHandler.Addon.loadModule now raises an exception when a module can't be loaded or has errors, instead of silently returning None without giving information about the cause.

Code Review Checklist:

  • Pull Request description:
    • description is up to date
    • change log entries
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • API is compatible with existing add-ons.
  • Documentation:
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • Security precautions taken.

@LeonarddeR LeonarddeR changed the title Swap pkgutil.ImpImporter for importlib when loading addon custom modules from add-ons Swap pkgutil.ImpImporter for importlib when loading custom modules from add-ons Dec 28, 2022
@AppVeyorBot
Copy link

See test results for failed build of commit ae6b79c403

@AppVeyorBot
Copy link

See test results for failed build of commit 2b2f3ab89b

@AppVeyorBot
Copy link

See test results for failed build of commit bd51e60a95

@LeonarddeR LeonarddeR marked this pull request as ready for review January 3, 2023 07:09
@LeonarddeR LeonarddeR requested a review from a team as a code owner January 3, 2023 07:09
@LeonarddeR LeonarddeR requested a review from seanbudd January 3, 2023 07:09
@LeonarddeR
Copy link
Collaborator Author

I just marked this ready for review. It replaces #14405. Both implementations have their stronger and weaker sides, but for an IDE, you need to trick the system anyway. Current code in a brailleDisplayDriver I have in concept now looks as follows:

if typing.TYPE_CHECKING:
	from ..lib import driver
	from ..lib import protocol
else:
	addon: addonHandler.Addon = addonHandler.getCodeAddon()
	driver = addon.loadModule("lib.driver")
	protocol = addon.loadModule("lib.protocol")

source/addonHandler/__init__.py Outdated Show resolved Hide resolved
source/addonHandler/__init__.py Show resolved Hide resolved
source/addonHandler/__init__.py Outdated Show resolved Hide resolved
source/addonHandler/__init__.py Show resolved Hide resolved
@seanbudd seanbudd marked this pull request as draft January 3, 2023 23:07
@LeonarddeR
Copy link
Collaborator Author

@seanbudd I decided to go the exception route. This ensures that loadModule behaves very similar to importlib.import_module i nterms of raising exceptions. Note that importlib.import_module is used under the hood anyway. While it is API breaking, I don't think it has major impact

@AppVeyorBot
Copy link

See test results for failed build of commit 4adfbfbfaa

@LeonarddeR LeonarddeR marked this pull request as ready for review January 10, 2023 11:36
@LeonarddeR LeonarddeR requested a review from seanbudd January 10, 2023 11:37
@LeonarddeR LeonarddeR added api-breaking-change Addon/API Changes to NVDA's API for addons to support addon development. labels Jan 10, 2023
@@ -507,7 +510,7 @@ def loadModule(self, name: str) -> ModuleType:
sys.modules[parentModule.__name__] = parentModule
spec = importlib.machinery.PathFinder.find_spec(fullNameTop, [self.path])
if not spec:
return None
raise ModuleNotFoundError(importlib._bootstrap._ERR_MSG.format(name), name=name)
Copy link
Member

Choose a reason for hiding this comment

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

considering this relies on an underscored part of python, can we expect stability here when updating python? It might be safer to use a custom message here.

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 point. I changed this into an f-string.

@seanbudd seanbudd added this to the 2023.1 milestone Jan 11, 2023
@seanbudd
Copy link
Member

Hi @LeonarddeR , I'm going to include both change log items under "API breaking changes". This is because the first item requires add-on authors to change their usage of the code.

@seanbudd seanbudd merged commit a232641 into nvaccess:master Jan 11, 2023
seanbudd pushed a commit that referenced this pull request Dec 27, 2023
…tween add-ons (#15967)

Fixes regression from PR #14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on #15852 and #15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before #14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
Nael-Sayegh pushed a commit to Nael-Sayegh/nvda that referenced this pull request Feb 15, 2024
…tween add-ons (nvaccess#15967)

Fixes regression from PR nvaccess#14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on nvaccess#15852 and nvaccess#15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before nvaccess#14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
SaschaCowley pushed a commit to SaschaCowley/nvda that referenced this pull request Feb 27, 2024
…tween add-ons (nvaccess#15967)

Fixes regression from PR nvaccess#14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on nvaccess#15852 and nvaccess#15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before nvaccess#14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
Adriani90 pushed a commit to Adriani90/nvda that referenced this pull request Mar 13, 2024
…tween add-ons (nvaccess#15967)

Fixes regression from PR nvaccess#14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on nvaccess#15852 and nvaccess#15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before nvaccess#14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Addon/API Changes to NVDA's API for addons to support addon development. api-breaking-change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants