diff --git a/.gitignore b/.gitignore index b1fd0a2..8f929d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ dist/ .cache +.idea \ No newline at end of file diff --git a/npm/__init__.py b/npm/__init__.py index e69de29..7b1b5a6 100644 --- a/npm/__init__.py +++ b/npm/__init__.py @@ -0,0 +1 @@ +default_app_config = 'npm.apps.NPMConfig' diff --git a/npm/apps.py b/npm/apps.py new file mode 100644 index 0000000..7952d25 --- /dev/null +++ b/npm/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NPMConfig(AppConfig): + name = 'npm' + verbose_name = "NPM package installer" diff --git a/npm/compat.py b/npm/compat.py new file mode 100644 index 0000000..4a0702e --- /dev/null +++ b/npm/compat.py @@ -0,0 +1,4 @@ +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict diff --git a/npm/conf.py b/npm/conf.py new file mode 100644 index 0000000..134b7ec --- /dev/null +++ b/npm/conf.py @@ -0,0 +1,22 @@ +import os +from django.conf import settings +from appconf import AppConf + + +class MyAppConf(AppConf): + # app settings + + # Windows settings + # node_executable = "D:\\Program Files\\nodejs\\node.exe" + # npm_cli = os.path.join(os.path.dirname(node_executable), + # "node_modules\\npm\\bin\\npm-cli.js") + # NPM_EXECUTABLE_PATH = '"%s" "%s"' % (node_executable, npm_cli) + EXECUTABLE_PATH = 'npm' + ROOT_PATH = os.getcwd() + + STATIC_FILES_PREFIX = '' + FINDER_USE_CACHE = True + FILE_PATTERNS = None + + class Meta: + prefix = 'npm' diff --git a/npm/finders.py b/npm/finders.py index 2746bef..be95319 100644 --- a/npm/finders.py +++ b/npm/finders.py @@ -1,37 +1,66 @@ +from __future__ import print_function + import os +import shlex import subprocess +import sys from fnmatch import fnmatch +from logging import getLogger from django.contrib.staticfiles import utils as django_utils from django.contrib.staticfiles.finders import FileSystemFinder from django.core.files.storage import FileSystemStorage -from django.conf import settings -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict +from npm.compat import OrderedDict +from npm.conf import settings +from npm.process import StdinWriter + +logger = getLogger(__name__) + +def npm_install(**config): + """Install nodejs packages""" + npm_executable = config.setdefault('npm_executable', settings.NPM_EXECUTABLE_PATH) + npm_workdir = config.setdefault('npm_workdir', settings.NPM_ROOT_PATH) + npm_command_args = config.setdefault('npm_command_args', ()) + + command = shlex.split(npm_executable) + + if not npm_command_args: + command.extend(['install', '--prefix=' + settings.NPM_ROOT_PATH]) + else: + command.extend(npm_command_args) -def npm_install(): - npm_executable_path = getattr(settings, 'NPM_EXECUTABLE_PATH', 'npm') - command = [npm_executable_path, 'install', '--prefix=' + get_npm_root_path()] proc = subprocess.Popen( command, - env={'PATH': os.environ.get('PATH')}, + env=os.environ, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE, + universal_newlines=True, + cwd=npm_workdir, + bufsize=2048 ) - proc.wait() - + with StdinWriter(proc): + try: + while proc.poll() is None: + data = proc.stdout.read(1) + if not data: + break + print(data, file=sys.stdout, end='') + finally: + proc.stdout.close() -def get_npm_root_path(): - return getattr(settings, 'NPM_ROOT_PATH', '.') + logger.debug("%s %s" % (proc.poll(), command)) + # npm code + return proc.poll() def flatten_patterns(patterns): if patterns is None: return None return [ - os.path.join(module, module_pattern) + os.path.normpath(os.path.join(module, module_pattern)) for module, module_patterns in patterns.items() for module_pattern in module_patterns ] @@ -79,12 +108,12 @@ def get_files(storage, match_patterns='*', ignore_patterns=None, location=''): class NpmFinder(FileSystemFinder): def __init__(self, apps=None, *args, **kwargs): - self.node_modules_path = get_npm_root_path() - self.destination = getattr(settings, 'NPM_STATIC_FILES_PREFIX', '') - self.cache_enabled = getattr(settings, 'NPM_FINDER_USE_CACHE', True) + self.node_modules_path = settings.NPM_ROOT_PATH + self.destination = settings.NPM_STATIC_FILES_PREFIX + self.cache_enabled = settings.NPM_FINDER_USE_CACHE self.cached_list = None - self.match_patterns = flatten_patterns(getattr(settings, 'NPM_FILE_PATTERNS', None)) or ['*'] + self.match_patterns = flatten_patterns(settings.NPM_FILE_PATTERNS) or ['*'] self.locations = [(self.destination, os.path.join(self.node_modules_path, 'node_modules'))] self.storages = OrderedDict() @@ -108,6 +137,8 @@ def list(self, ignore_patterns=None): # TODO should be configurable, add settin def _make_list_generator(self, ignore_patterns=None): for prefix, root in self.locations: + if not os.path.exists(root): + continue storage = self.storages[root] for path in get_files(storage, self.match_patterns, ignore_patterns): yield path, storage diff --git a/npm/management/commands/npm_cli.py b/npm/management/commands/npm_cli.py new file mode 100644 index 0000000..5102afb --- /dev/null +++ b/npm/management/commands/npm_cli.py @@ -0,0 +1,15 @@ +import argparse + +from django.core.management.base import BaseCommand + +from npm.finders import npm_install + + +class Command(BaseCommand): + help = 'Run npm install' + + def add_arguments(self, parser): + parser.add_argument('npm_command_args', nargs=argparse.REMAINDER) + + def handle(self, *args, **options): + npm_install(npm_command_args=options.get('npm_command_args', ())) diff --git a/npm/management/commands/npm_install.py b/npm/management/commands/npm_install.py deleted file mode 100644 index 9550baf..0000000 --- a/npm/management/commands/npm_install.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.core.management.base import BaseCommand -from npm.finders import npm_install - - -class Command(BaseCommand): - help = 'Run npm install' - - def handle(self, *args, **options): - npm_install() diff --git a/npm/process.py b/npm/process.py new file mode 100644 index 0000000..ede2084 --- /dev/null +++ b/npm/process.py @@ -0,0 +1,34 @@ +from __future__ import print_function + +import sys +import threading + + +class StdinWriter(threading.Thread): + """Reads stdin data and passes back to the process""" + def __init__(self, proc): + threading.Thread.__init__(self) + self.proc = proc + self.setDaemon(True) + + def do_input(self): + data = sys.stdin.readline() + self.proc.stdin.write(data) + self.proc.stdin.flush() + + def run(self): + while self.proc.poll() is None: + try: + self.do_input() + except (IOError, ValueError): + break + + def close(self): + self.proc.stdin.close() + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() diff --git a/setup.py b/setup.py index 99f987a..e570653 100644 --- a/setup.py +++ b/setup.py @@ -3,15 +3,16 @@ here = path.abspath(path.dirname(__file__)) +requirements = ['django-appconf'] + try: from collections import OrderedDict - requirements = [] except ImportError: - requirements = ['ordereddict'] + requirements.append('ordereddict') setup( name='django-npm', - version='1.0.0', + version='1.1.0', description='A django staticfiles finder that uses npm', url='https://github.com/kevin1024/django-npm', author='Kevin McCarthy', @@ -29,6 +30,7 @@ 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.9' ], keywords='django npm staticfiles', diff --git a/tests/test_finder.py b/tests/test_finder.py index fcaaf43..3434b81 100644 --- a/tests/test_finder.py +++ b/tests/test_finder.py @@ -1,4 +1,8 @@ -from .util import configure_settings +import os +from os.path import normpath + +from tests.util import configure_settings + configure_settings() import pytest @@ -28,34 +32,41 @@ def test_get_files(npm_dir): files = get_files(storage, match_patterns='*') assert any([True for _ in files]) + def test_finder_list_all(npm_dir): f = NpmFinder() assert any([True for _ in f.list()]) + def test_finder_find(npm_dir): f = NpmFinder() - file = f.find('mocha/mocha.js') + file = f.find(normpath('mocha/mocha.js')) assert file + def test_finder_in_subdirectory(npm_dir): with override_settings(NPM_STATIC_FILES_PREFIX='lib'): f = NpmFinder() - assert f.find('lib/mocha/mocha.js') + assert f.find(normpath('lib/mocha/mocha.js')) + def test_finder_with_patterns_in_subdirectory(npm_dir): with override_settings(NPM_STATIC_FILES_PREFIX='lib', NPM_FILE_PATTERNS={'mocha': ['*']}): f = NpmFinder() - assert f.find('lib/mocha/mocha.js') + assert f.find(normpath('lib/mocha/mocha.js')) + def test_finder_with_patterns_in_directory_component(npm_dir): - with override_settings(NPM_STATIC_FILES_PREFIX='lib', NPM_FILE_PATTERNS={'mocha': ['*/*js']}): + with override_settings(NPM_STATIC_FILES_PREFIX='lib', NPM_FILE_PATTERNS={'mocha': ['*{0.sep}*js'.format(os)]}): f = NpmFinder() - assert f.find('lib/mocha/lib/test.js') + assert f.find(normpath('lib/mocha/lib/test.js')) + def test_no_matching_paths_returns_empty_list(npm_dir): with override_settings(NPM_FILE_PATTERNS={'foo': ['bar']}): f = NpmFinder() - assert f.find('mocha/mocha.js') == [] + assert f.find(normpath('mocha/mocha.js')) == [] + def test_finder_cache(npm_dir): with override_settings(NPM_FINDER_USE_CACHE=True): @@ -64,6 +75,7 @@ def test_finder_cache(npm_dir): assert f.cached_list is not None assert f.list() is f.cached_list + def test_finder_no_cache(npm_dir): with override_settings(NPM_FINDER_USE_CACHE=False): f = NpmFinder() diff --git a/tests/util.py b/tests/util.py index b6b8455..88e6289 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,12 +1,18 @@ +import os + from django.conf import settings +import django def configure_settings(): settings.configure( DEBUG=True, + INSTALLED_APPS=['npm'], + NPM_EXECUTABLE_PATH=os.environ.get('NPM_EXECUTABLE_PATH', 'npm'), CACHES={ 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', } } ) + django.setup()