Add custom LOADER_CLASS support (#210)

`LOADER_CLASS` on the `WEBPACK_CONFIG` setting is now where the loader class
is defined. To retain backward compatibility and to keep getting started
simple, this defaults to the existing WebpackLoader class.

`WebpackLoader._load_assets` has been renamed to `WebpackLoader.load_assets`.
This keeps the API extendable when creating custom webpack loaders.

Documentation has been updated to include how to extend the WebpackLoader
using the `LOADER_CLASS`.
This commit is contained in:
Rhyne 2020-01-13 15:57:18 -05:00 committed by Owais Lone
parent 8c3c370838
commit 152414cc26
6 changed files with 127 additions and 12 deletions

View file

@ -90,7 +90,8 @@ WEBPACK_LOADER = {
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
'IGNORE': [r'.+\.hot-update.js', r'.+\.map']
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
'LOADER_CLASS': 'webpack_loader.loader.WebpackLoader',
}
}
```
@ -168,6 +169,40 @@ and your webpack config is located at `/home/src/webpack.config.js`, then the va
<br>
#### LOADER_CLASS
`LOADER_CLASS` is the fully qualified name of a python class as a string that holds the custom webpack loader.
This is where behavior can be customized as to how the stats file is loaded. Examples include loading the stats file
from a database, cache, external url, etc. For convenience, `webpack_loader.loader.WebpackLoader` can be extended;
The `load_assets` method is likely where custom behavior will be added. This should return the stats file as an object.
Here's a simple example of loading from an external url:
```py
# in app.module
import requests
from webpack_loader.loader import WebpackLoader
class ExternalWebpackLoader(WebpackLoader):
def load_assets(self):
url = self.config['STATS_URL']
return requests.get(url).json()
# in app.settings
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': False,
'BUNDLE_DIR_NAME': 'bundles/',
'LOADER_CLASS': 'app.module.ExternalWebpackLoader',
# Custom config setting made available in WebpackLoader's self.config
'STATS_URL': 'https://www.test.com/path/to/stats/',
}
}
```
<br>
## Usage
<br>

View file

@ -0,0 +1,63 @@
from imp import reload
from django.test import TestCase
from webpack_loader import utils, config, loader
DEFAULT_CONFIG = 'DEFAULT'
LOADER_PAYLOAD = {'status': 'done', 'chunks': []}
class ValidCustomLoader(loader.WebpackLoader):
def load_assets(self):
return LOADER_PAYLOAD
class CustomLoadersTestCase(TestCase):
def tearDown(self):
self.reload_webpack()
def reload_webpack(self):
'''
Reloads webpack loader modules that have cached values to avoid polluting certain tests
'''
reload(utils)
reload(config)
def test_bad_custom_loader(self):
'''
Tests that a bad custom loader path will raise an error
'''
loader_class = 'app.tests.bad_loader_path.BadCustomLoader'
with self.settings(WEBPACK_LOADER={
'DEFAULT': {
'CACHE': False,
'BUNDLE_DIR_NAME': 'bundles/',
'LOADER_CLASS': loader_class
}
}):
self.reload_webpack()
try:
loader = utils.get_loader(DEFAULT_CONFIG)
self.fail('The loader should fail to load with a bad LOADER_CLASS')
except ImportError as e:
self.assertIn(
'{} doesn\'t look like a valid module path'.format(loader_class),
str(e)
)
def test_good_custom_loader(self):
'''
Tests that a good custom loader will return the correct assets
'''
loader_class = 'app.tests.test_custom_loaders.ValidCustomLoader'
with self.settings(WEBPACK_LOADER={
'DEFAULT': {
'CACHE': False,
'BUNDLE_DIR_NAME': 'bundles/',
'LOADER_CLASS': loader_class,
}
}):
self.reload_webpack()
assets = utils.get_loader(DEFAULT_CONFIG).load_assets()
self.assertEqual(assets, LOADER_PAYLOAD)

View file

@ -1,4 +1,4 @@
__author__ = 'Owais Lone'
__version__ = '0.6.0'
__version__ = '0.7.0'
default_app_config = 'webpack_loader.apps.WebpackLoaderConfig'

View file

@ -14,7 +14,8 @@ DEFAULT_CONFIG = {
# FIXME: Explore usage of fsnotify
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
'IGNORE': [r'.+\.hot-update.js', r'.+\.map']
'IGNORE': ['.+\.hot-update.js', '.+\.map'],
'LOADER_CLASS': 'webpack_loader.loader.WebpackLoader',
}
}

View file

@ -11,17 +11,16 @@ from .exceptions import (
WebpackLoaderTimeoutError,
WebpackBundleLookupError
)
from .config import load_config
class WebpackLoader(object):
_assets = {}
def __init__(self, name='DEFAULT'):
def __init__(self, name, config):
self.name = name
self.config = load_config(self.name)
self.config = config
def _load_assets(self):
def load_assets(self):
try:
with open(self.config['STATS_FILE'], encoding="utf-8") as f:
return json.load(f)
@ -34,9 +33,9 @@ class WebpackLoader(object):
def get_assets(self):
if self.config['CACHE']:
if self.name not in self._assets:
self._assets[self.name] = self._load_assets()
self._assets[self.name] = self.load_assets()
return self._assets[self.name]
return self._load_assets()
return self.load_assets()
def filter_chunks(self, chunks):
for chunk in chunks:

View file

@ -1,14 +1,31 @@
from importlib import import_module
from django.conf import settings
from .loader import WebpackLoader
from .config import load_config
_loaders = {}
def import_string(dotted_path):
'''
This is a rough copy of django's import_string, which wasn't introduced until Django 1.7
Once this package's support for Django 1.6 has been removed, this can be safely replaced with
`from django.utils.module_loading import import_string`
'''
try:
module_path, class_name = dotted_path.rsplit('.', 1)
module = import_module(module_path)
return getattr(module, class_name)
except (ValueError, AttributeError, ImportError):
raise ImportError('%s doesn\'t look like a valid module path' % dotted_path)
def get_loader(config_name):
if config_name not in _loaders:
_loaders[config_name] = WebpackLoader(config_name)
config = load_config(config_name)
loader_class = import_string(config['LOADER_CLASS'])
_loaders[config_name] = loader_class(config_name, config)
return _loaders[config_name]