Skip to content
This repository was archived by the owner on Jun 6, 2025. It is now read-only.

Commit 8ec9b8d

Browse files
move monkeypatching from app's ready() method to function (#3)
1 parent 31d96c2 commit 8ec9b8d

File tree

8 files changed

+183
-170
lines changed

8 files changed

+183
-170
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1717

1818
## [Unreleased]
1919

20+
### Changed
21+
22+
- **Breaking**: Move monkeypatching from Django app's `ready()` method to dedicated function. Instead of just adding the app to `INSTALLED_APPS`, you now need to explicitly call `django_lazy_gdal.monkeypatch()` at the top of your settings file.
23+
2024
## [0.1.0]
2125

2226
### Added

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,22 @@ A Django app that patches `django.contrib.gis` to lazily load the GDAL library,
2424
uv sync
2525
```
2626

27-
2. Add the app to your Django project's `INSTALLED_APPS` **after** `django.contrib.gis`:
27+
2. Import and call the `monkeypatch` function at the top of your Django project's settings module:
2828
2929
```python
30-
INSTALLED_APPS = [
31-
# Add these at the top
32-
'django.contrib.gis',
33-
'django_lazy_gdal',
34-
# ...
35-
]
30+
# settings.py - add these lines at the TOP of the file
31+
import django_lazy_gdal
32+
33+
34+
django_lazy_gdal.monkeypatch()
35+
36+
# ... rest of your settings file
3637
```
3738
38-
> [!NOTE]
39-
> **Order matters!** It's crucial to place `'django_lazy_gdal'` immediately after `'django.contrib.gis'` and before any other apps that might access the GeoDjango models in `django.contrib.gis.models` or access `django.contrib.gis.gdal.libgdal` directly. This ensures that the patching occurs before other apps access the module, making the lazy loading effective throughout your project.
39+
> [!IMPORTANT]
40+
> **Timing matters!** It's crucial to call `django_lazy_gdal.monkeypatch()` before any GeoDjango modules are imported. This is because Django imports models, which in turn imports the existing eager GDAL loading via imports in `django.contrib.gis.models`, *before* running app `ready()` methods. Calling the monkeypatch function at the top of your settings ensures the patching occurs before any other imports that might access `django.contrib.gis.gdal.libgdal`.
4041
41-
3. That's it! The library will automatically patch Django's `django.contrib.gis.gdal.libgdal` module to use lazy loading.
42+
3. That's it! The library will patch Django's `django.contrib.gis.gdal.libgdal` module to use lazy loading.
4243
4344
## Why?
4445

RELEASING.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,3 @@ When it comes time to cut a new release, follow these steps:
6565
We try our best to adhere to [Semantic Versioning](https://semver.org/), but we do not promise to follow it perfectly (and let's be honest, this is the case with a lot of projects using SemVer).
6666

6767
In general, use your best judgement when choosing the next version number. If you are unsure, you can always ask for a second opinion from another contributor.
68-

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ exclude_lines = [
7676
]
7777
# since we are not testing the actual functionality of libgdal
7878
# we don't need to cover all code paths
79-
fail_under = 60
79+
fail_under = 50
8080

8181
[tool.coverage.run]
8282
omit = [
@@ -194,4 +194,3 @@ keep-runtime-typing = true
194194

195195
[tool.uv]
196196
required-version = "<0.8"
197-

src/django_lazy_gdal/__init__.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import logging
5+
import sys
6+
from typing import Any
7+
8+
logger = logging.getLogger(__name__)
9+
10+
_patching_done = False
11+
12+
13+
def monkeypatch() -> None:
14+
"""
15+
Monkeypatches Django's GDAL library loader to use our lazy loader instead.
16+
17+
This function must be called before Django's model importing phase to be effective.
18+
Typically, it should be called at the top of your settings module, before any
19+
apps or models are imported.
20+
"""
21+
global _patching_done
22+
23+
if _patching_done:
24+
logger.debug("Patching already attempted. Skipping.")
25+
return
26+
_patching_done = True
27+
28+
from django_lazy_gdal import lazy_libgdal
29+
30+
django_libgdal_mod = "django.contrib.gis.gdal.libgdal"
31+
lazy_libgdal_mod = "django_lazy_gdal.lazy_libgdal"
32+
33+
original_libgdal = None
34+
original_module_dict: dict[str, Any] = {}
35+
try:
36+
if (
37+
django_libgdal_mod in sys.modules
38+
and sys.modules[django_libgdal_mod] is not lazy_libgdal
39+
):
40+
logger.warning(
41+
f"{django_libgdal_mod} was imported before django_lazy_gdal could monkeypatch it. "
42+
"Call django_lazy_gdal.monkeypatch_libgdal() early in your settings module."
43+
)
44+
original_libgdal = sys.modules[django_libgdal_mod]
45+
elif django_libgdal_mod not in sys.modules:
46+
try:
47+
original_libgdal = importlib.import_module(django_libgdal_mod)
48+
except ImportError:
49+
# This might happen if django.contrib.gis is partially available
50+
# but libgdal itself fails to import. Patching might still be desired
51+
# but attribute copying won't work.
52+
logger.warning(
53+
f"Could not import original {django_libgdal_mod} for attribute copying.",
54+
exc_info=True,
55+
)
56+
pass
57+
58+
if original_libgdal:
59+
original_module_dict = original_libgdal.__dict__.copy()
60+
61+
except Exception:
62+
logger.exception(
63+
f"Error trying to access original module {django_libgdal_mod} for attribute copying."
64+
)
65+
# Decide whether to proceed without attribute copying or bail out.
66+
# Let's proceed but without attributes.
67+
pass
68+
69+
sys.modules[django_libgdal_mod] = lazy_libgdal
70+
logger.debug(f"Monkeypatched {django_libgdal_mod} to use {lazy_libgdal_mod}")
71+
72+
# Transfer attributes from the original module dict if we have it
73+
if original_module_dict:
74+
copied_attrs_count = 0
75+
for key, value in original_module_dict.items():
76+
# Only copy if the attribute doesn't already exist on the lazy module
77+
if not hasattr(lazy_libgdal, key):
78+
try:
79+
setattr(lazy_libgdal, key, value)
80+
copied_attrs_count += 1
81+
except Exception:
82+
logger.warning(
83+
f"Failed to copy attribute '{key}' from original {django_libgdal_mod} to lazy module.",
84+
exc_info=True,
85+
)
86+
pass
87+
logger.debug(
88+
f"Transferred {copied_attrs_count} missing attributes from {django_libgdal_mod}."
89+
)
90+
else:
91+
logger.debug(
92+
f"Skipping attribute transfer as original module dict for {django_libgdal_mod} wasn't available."
93+
)

src/django_lazy_gdal/apps.py

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,11 @@
11
from __future__ import annotations
22

3-
import importlib
4-
import logging
5-
import sys
63
from typing import final
74

85
from django.apps import AppConfig
96

10-
from ._typing import override
11-
12-
logger = logging.getLogger(__name__)
13-
147

158
@final
169
class DjangoLazyGDALConfig(AppConfig):
1710
name = "django_lazy_gdal"
1811
verbose_name = "Django Lazy GDAL"
19-
_patching_done = False # Sentinel flag
20-
21-
@override
22-
def ready(self):
23-
# Check if patching has already been done or attempted
24-
if DjangoLazyGDALConfig._patching_done:
25-
logger.debug("Patching already attempted. Skipping.")
26-
return
27-
DjangoLazyGDALConfig._patching_done = True
28-
29-
from django_lazy_gdal import lazy_libgdal
30-
31-
django_libgdal_mod = "django.contrib.gis.gdal.libgdal"
32-
lazy_libgdal_mod = "django_lazy_gdal.lazy_libgdal"
33-
34-
original_libgdal = None
35-
original_module_dict = {}
36-
try:
37-
if (
38-
django_libgdal_mod in sys.modules
39-
and sys.modules[django_libgdal_mod] is not lazy_libgdal
40-
):
41-
logger.warning(
42-
f"{django_libgdal_mod} was imported before django_lazy_gdal could monkeypatch it. Ensure 'django_lazy_gdal' is placed early in INSTALLED_APPS."
43-
)
44-
original_libgdal = sys.modules[django_libgdal_mod]
45-
elif django_libgdal_mod not in sys.modules:
46-
try:
47-
original_libgdal = importlib.import_module(django_libgdal_mod)
48-
except ImportError:
49-
# This might happen if django.contrib.gis is partially available
50-
# but libgdal itself fails to import. Patching might still be desired
51-
# but attribute copying won't work.
52-
logger.warning(
53-
f"Could not import original {django_libgdal_mod} for attribute copying.",
54-
exc_info=True,
55-
)
56-
pass
57-
58-
if original_libgdal:
59-
original_module_dict = original_libgdal.__dict__.copy()
60-
61-
except Exception:
62-
logger.exception(
63-
f"Error trying to access original module {django_libgdal_mod} for attribute copying."
64-
)
65-
# Decide whether to proceed without attribute copying or bail out.
66-
# Let's proceed but without attributes.
67-
pass
68-
69-
sys.modules[django_libgdal_mod] = lazy_libgdal
70-
logger.info(f"Monkeypatched {django_libgdal_mod} to use {lazy_libgdal_mod}")
71-
72-
# Transfer attributes from the original module dict if we have it
73-
if original_module_dict:
74-
copied_attrs_count = 0
75-
for key, value in original_module_dict.items():
76-
# Only copy if the attribute doesn't already exist on the lazy module
77-
if not hasattr(lazy_libgdal, key):
78-
try:
79-
setattr(lazy_libgdal, key, value)
80-
copied_attrs_count += 1
81-
except Exception:
82-
logger.warning(
83-
f"Failed to copy attribute '{key}' from original {django_libgdal_mod} to lazy module.",
84-
exc_info=True,
85-
)
86-
pass
87-
logger.debug(
88-
f"Transferred {copied_attrs_count} missing attributes from {django_libgdal_mod}."
89-
)
90-
# else:
91-
logger.debug(
92-
f"Skipping attribute transfer as original module dict for {django_libgdal_mod} wasn't available."
93-
)

tests/test_apps.py

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)