-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Summary
Bug Report: The mergepythonclient library (version 2.3.1) contains a critical flaw in its dynamic import system that causes infinite recursion when tools like freezegun attempt to introspect module attributes. This results in a RecursionError: maximum recursion depth exceeded and prevents the library
from being used in testing environments that rely on module introspection.
Environment
- Package: mergepythonclient version 2.3.1
- Python: 3.13.2 (also affects other Python versions)
- Platform: All platforms (tested on macOS Darwin 24.6.0)
- Trigger: Module introspection tools (e.g., freezegun, potentially others)
Root Cause
The issue is located in /merge/resources/init.py where the _dynamic_imports dictionary contains self-referential entries:
_dynamic_imports: typing.Dict[str, str] = {
"accounting": ".", # ← Problematic: "." refers to the same module
"ats": ".",
"crm": ".",
"filestorage": ".",
"hris": ".",
"ticketing": ".",
}When the getattr method is triggered:
- import_module(".", package) imports the same module (merge.resources)
- getattr(module, attr_name) attempts to access the attribute on the same module
- This triggers getattr again → infinite recursion
Reproduction Steps
"""
Reproduces the RecursionError in mergepythonclient 2.3.1
Save as reproduce_bug.py and run: python reproduce_bug.py
"""
import sys
import traceback
def test_recursion_bug():
try:
from freezegun import freeze_time
@freeze_time("2025-01-01T00:00:00")
def dummy_test():
return "success"
result = dummy_test()
print("✅ Test passed:", result)
except RecursionError as e:
print("❌ RecursionError occurred:")
print(f"Error: {str(e)}")
tb = traceback.format_exc()
print(tb)
return False
except Exception as e:
print(f"❌ Other error: {e}")
return False
return True
if __name__ == "__main__":
print("Testing mergepythonclient recursion bug...")
print(f"Python version: {sys.version}")
# Check if merge is installed
try:
import merge
print(f"Merge version: {getattr(merge, '__version__', 'unknown')}")
except ImportError:
print(
"❌ mergepythonclient not installed. Install with: pip install mergepythonclient"
)
sys.exit(1)
success = test_recursion_bug()
if not success:
print("\n🐛 Bug confirmed: RecursionError in mergepythonclient")
sys.exit(1)Stack trace
Traceback (most recent call last):
File "/Users/user/devel/bug/test.py", line 18, in test_recursion_bug
result = dummy_test()
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/freezegun/api.py", line 901, in wrapper
with self as time_factory:
^^^^
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/freezegun/api.py", line 717, in __enter__
return self.start()
~~~~~~~~~~^^
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/freezegun/api.py", line 806, in start
module_attrs = _get_cached_module_attributes(module)
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/freezegun/api.py", line 144, in _get_cached_module_attributes
_setup_module_cache(module)
~~~~~~~~~~~~~~~~~~~^^^^^^^^
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/freezegun/api.py", line 123, in _setup_module_cache
all_module_attributes = _get_module_attributes(module)
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/freezegun/api.py", line 112, in _get_module_attributes
attribute_value = getattr(module, attribute_name)
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/merge/__init__.py", line 33, in __getattr__
result = getattr(module, attr_name)
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/merge/resources/__init__.py", line 26, in __getattr__
result = getattr(module, attr_name)
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/merge/resources/__init__.py", line 26, in __getattr__
result = getattr(module, attr_name)
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/merge/resources/__init__.py", line 26, in __getattr__
result = getattr(module, attr_name)
[Previous line repeated 985 more times]
File "/Users/user/devel/bug/.venv/lib/python3.13/site-packages/merge/resources/__init__.py", line 25, in __getattr__
module = import_module(module_name, __package__)
File "/Users/user/.local/share/mise/installs/python/3.13.2/lib/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1384, in _gcd_import
RecursionError: maximum recursion depth exceeded
Impact
- Critical: Prevents use of mergepythonclient in test environments using freezegun
- Widespread: Affects any tool that performs module introspection
Suggested Fix
Replace the self-referential "." entries in /merge/resources/init.py:
Current (Broken) Code:
_dynamic_imports: typing.Dict[str, str] = {
"accounting": ".",
"ats": ".",
"crm": ".",
"filestorage": ".",
"hris": ".",
"ticketing": ".",
}Suggested Fix:
_dynamic_imports: typing.Dict[str, str] = {
"accounting": ".accounting",
"ats": ".ats",
"crm": ".crm",
"filestorage": ".filestorage",
"hris": ".hris",
"ticketing": ".ticketing",
}Alternative Safer Approach:
Add recursion protection to the getattr method:
_in_getattr = set()
def __getattr__(attr_name: str) -> typing.Any:
if attr_name in _in_getattr:
raise AttributeError(f"Recursive access detected for {attr_name}")
module_name = _dynamic_imports.get(attr_name)
if module_name is None:
raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
try:
_in_getattr.add(attr_name)
module = import_module(module_name, __package__)
result = getattr(module, attr_name)
return result
except ImportError as e:
raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
except AttributeError as e:
raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
finally:
_in_getattr.discard(attr_name)Workarounds for Users
Until fixed, users can work around this issue by configuring freezegun to ignore the merge package:
import freezegun
freezegun.configure(extend_ignore_list=['merge'])Additional Notes
- This bug was introduced in the dynamic import system design
- The same pattern may exist in other auto-generated SDK code
- Testing with module introspection tools should be part of the CI/CD pipeline
- Consider adding unit tests that use freezegun or similar introspection tools
Repository Information
- Repository: https://github.com/merge-api/merge-python-client
- Package: https://pypi.org/project/mergepythonclient/
- Current Version: 2.3.1 (affected)
Thank you for maintaining this library! This fix would greatly improve compatibility with testing frameworks.