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

Prevent the occurrence of SyntaxError in friendly modules. #533

Merged
merged 9 commits into from May 5, 2024

Conversation

junkmd
Copy link
Collaborator

@junkmd junkmd commented Apr 25, 2024

#524

See also #524 (comment) and #524 (comment).

I will wait for feedback from the community, and if there are no objections, I will merge this and release it as comtypes==1.4.2 in early May.

@junkmd junkmd added this to the 1.4.2 milestone Apr 25, 2024
junkmd added a commit to junkmd/pywinauto that referenced this pull request Apr 25, 2024
@junkmd
Copy link
Collaborator Author

junkmd commented Apr 26, 2024

@cfarrow
Copy link
Member

cfarrow commented Apr 26, 2024

Redefining the generate method to not generate a module seems dangerous. This could break code and workflows that depend on modules being regenerated every time.

A safer option is to add an argument to generate that triggers the new load functionality, while defaulting to the existing behavior. This greatly reduces the potential for unintended side effects down the road.

When the right time comes to make a backward-incompatible change, I advise to change the method name to reflect that it does more than generate modules.

@junkmd
Copy link
Collaborator Author

junkmd commented Apr 26, 2024

@cfarrow

Thank you for your feedback.

As you pointed out, in reality, modules are not always being generated.

However, this behavior of GetModule has not changed since comtypes==1.1.11, before I contributed to the codebase.

The _CreateWrapper, which once had the responsibility to generate the wrapper module, also did not create a new module instance or file if there was an imported module from the sys.modules cache.

def _CreateWrapper(tlib, pathname):
# helper which creates and imports the real typelib wrapper module.
fullname = _name_module(tlib)
try:
return sys.modules[fullname]
except KeyError:
pass
modname = fullname.split(".")[-1]
try:
return _my_import(fullname)
except Exception as details:
logger.info("Could not import %s: %s", fullname, details)
# generate the module since it doesn't exist or is out of date
from comtypes.tools.tlbparser import generate_module
if comtypes.client.gen_dir is None:
if sys.version_info >= (3, 0):
import io
else:
import cStringIO as io
ofi = io.StringIO()
else:
ofi = open(os.path.join(comtypes.client.gen_dir, modname + ".py"), "w")
# XXX use logging!
logger.info("# Generating comtypes.gen.%s", modname)
generate_module(tlib, ofi, pathname)
if comtypes.client.gen_dir is None:
code = ofi.getvalue()
mod = types.ModuleType(fullname)
mod.__file__ = os.path.join(os.path.abspath(comtypes.gen.__path__[0]),
"<memory>")
exec(code, mod.__dict__)
sys.modules[fullname] = mod
setattr(comtypes.gen, modname, mod)
else:
ofi.close()
# clear the import cache to make sure Python sees newly created modules
if hasattr(importlib, "invalidate_caches"):
importlib.invalidate_caches()
mod = _my_import(fullname)
return mod

And for the friendly modules, if _my_import was called and the loading of the existing module was successful, a new module was not generated.

# create and import the module
mod = _CreateWrapper(tlib, pathname)
try:
modulename = tlib.GetDocumentation(-1)[0]
except comtypes.COMError:
return mod
if modulename is None:
return mod
if sys.version_info < (3, 0):
modulename = modulename.encode("mbcs")
# create and import the friendly-named module
try:
mod = _my_import("comtypes.gen." + modulename)
except Exception as details:
logger.info("Could not import comtypes.gen.%s: %s", modulename, details)
else:
return mod

def _my_import(fullname):
# helper function to import dotted modules
import comtypes.gen
if comtypes.client.gen_dir \
and comtypes.client.gen_dir not in comtypes.gen.__path__:
comtypes.gen.__path__.append(comtypes.client.gen_dir)
return __import__(fullname, globals(), locals(), ['DUMMY'])

When I added refactoring to _generate.py, I made sure to maintain this behavior.

So,

the generate method to not generate a module

has been the case from the beginning.

I made changes to the code under the recognition that it would have a greater impact to make GetModule always generate a new module without using the cache (a change that is appropriate for incrementing the y in version x.y.z).

From 1.3.0 onwards, the namespace of the friendly module is not defined by globals().update, but by explicitly importing symbols.

Since the friendly module uses the side effects of the code generation of the wrapper module to determine which symbols to import, the newly generated friendly module becomes partial if the friendly module file does not exist and the wrapper module is loaded from the cache.

Currently, if such a situation occurs, unlike before, it does not load from the cache, and GetModule recreates the wrapper module file and the friendly module file.
The only situation where the cached wrapper module is not used, even if it exists, is this one, and I think it makes sense to include the responsibility of GetModule to regenerate the wrapper module, which is a dependency of the friendly module, if it does not exist.

@junkmd
Copy link
Collaborator Author

junkmd commented Apr 26, 2024

I have added the test to verify that GetModule regenerates the module.

This is similar to the existing test_findgendir.py in that it patches sys.modules.

This test involves a complex interplay of context-managers, sys.modules, sys.path, importlib, and mock.patch.
Consequently, the codebase has become complex.

To clarify the behavior within the fixtures, I've inserted assert statements and comments at key points.
These aims to facilitate asynchronous communication with future maintainers.

I welcome any feedbacks as well.

…erator`.

Instead, `GetModule` attempts to load the existing modules.
@junkmd
Copy link
Collaborator Author

junkmd commented Apr 27, 2024

the generate method to not generate a module

If you mean that ModuleGenerator.generate is not adhering to the single responsibility principle because it is not only generating new modules but also responsible for loading existing modules, then I agree.

Therefore, I have made changes so that the attempt to load existing modules is done within GetModule, before ModuleGenerator.generate is called.

junkmd added a commit to junkmd/pywinauto that referenced this pull request Apr 27, 2024
junkmd added a commit to junkmd/pywinauto that referenced this pull request Apr 28, 2024
junkmd added a commit to junkmd/pywinauto that referenced this pull request Apr 28, 2024
@cfarrow
Copy link
Member

cfarrow commented Apr 28, 2024

Thank you for explaining this to me. My understanding of the code was not complete. I agree that the new responsibility belongs in GetModule. The test is a good addition with the new behavior.

@junkmd
Copy link
Collaborator Author

junkmd commented Apr 29, 2024

Thank you for your reply.

When I first looked at the codebase for comtypes==1.1.11, it took me some time to understand this behavior because my initial expectations and the actual implementation were different.

Thank you.

junkmd added a commit to junkmd/pywinauto that referenced this pull request Apr 30, 2024
@junkmd junkmd merged commit 6b81d07 into enthought:main May 5, 2024
25 checks passed
@junkmd junkmd deleted the fix_issue_524_syntaxerror_and_more branch May 5, 2024 13:21
@junkmd junkmd added the tests enhance or fix tests label May 8, 2024
@junkmd junkmd added the bug Something isn't working label May 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working tests enhance or fix tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants