forked from github.com/pypiserver
Load plugins in configure()
This commit is contained in:
parent
26d35cd9e9
commit
02f3d5af61
@ -8,10 +8,10 @@ from os import environ, path
|
||||
from textwrap import dedent
|
||||
|
||||
import pkg_resources
|
||||
from pkg_resources import iter_entry_points
|
||||
|
||||
from . import __version__
|
||||
from .bottle import server_names
|
||||
from .core import load_plugins
|
||||
from .const import STANDALONE_WELCOME
|
||||
|
||||
|
||||
@ -166,14 +166,7 @@ class Config(object):
|
||||
self.help_formatter = help_formatter
|
||||
self.parser_cls = parser_cls
|
||||
self.parser_type = parser_type
|
||||
self._plugins = {
|
||||
'auth': {},
|
||||
}
|
||||
|
||||
def load_plugins(self):
|
||||
"""Load plugins for later access."""
|
||||
for entrypoint in iter_entry_points('pypiserver.authenticators'):
|
||||
self._plugins['auth'][entrypoint.name] = entrypoint.load()
|
||||
self._plugins = load_plugins()
|
||||
|
||||
def get_default(self, subcommand='run'):
|
||||
"""Return a parsed config with default argument values.
|
||||
@ -219,7 +212,6 @@ class Config(object):
|
||||
|
||||
def _get_parser(self):
|
||||
"""Return a hydrated parser."""
|
||||
self.load_plugins()
|
||||
parser = self.parser_cls(
|
||||
description='PyPI-compatible package server',
|
||||
formatter_class=self.help_formatter
|
||||
@ -446,16 +438,16 @@ class Config(object):
|
||||
default=environ.get('PYPISERVER_PASSWORD_FILE'),
|
||||
help=dedent('''\
|
||||
use apache htpasswd file PASSWORD_FILE to set usernames &
|
||||
passwords when authenticating certain actions (see -a option).
|
||||
If you want to allow unauthorized access, set this option
|
||||
and -a to '.'
|
||||
passwords when authenticating certain actions (see
|
||||
-a option). If you want to allow unauthorized access,
|
||||
set this option and -a to '.'
|
||||
''')
|
||||
)
|
||||
security.add_argument(
|
||||
'--auth-backend',
|
||||
dest='auther',
|
||||
default=environ.get('PYPISERVER_AUTH_BACKEND'),
|
||||
choices=self._plugins['auth'].keys(),
|
||||
choices=self._plugins['authenticators'].keys(),
|
||||
help=(
|
||||
'Specify an authentication backend. By default, will attempt '
|
||||
'to use an htpasswd file if provided. If specified, must '
|
||||
@ -477,7 +469,7 @@ class Config(object):
|
||||
|
||||
:param ArgumentParser parser: the "run" subcommand parser
|
||||
"""
|
||||
for name, plugin in self._plugins['auth'].items():
|
||||
for name, plugin in self._plugins['authenticators'].items():
|
||||
self.add_plugin_group(parser, name, plugin)
|
||||
|
||||
@staticmethod
|
||||
|
@ -3,5 +3,6 @@
|
||||
from sys import version_info
|
||||
|
||||
|
||||
PLUGIN_GROUPS = ('authenticators',)
|
||||
PY2 = version_info < (3,)
|
||||
STANDALONE_WELCOME = 'standalone'
|
||||
|
@ -11,8 +11,9 @@ import re
|
||||
import sys
|
||||
|
||||
import pkg_resources
|
||||
from pkg_resources import iter_entry_points
|
||||
|
||||
from .const import PY2, STANDALONE_WELCOME
|
||||
from .const import PLUGIN_GROUPS, PY2, STANDALONE_WELCOME
|
||||
|
||||
if PY2:
|
||||
from io import open
|
||||
@ -35,6 +36,31 @@ _pkgname_parts_re = re.compile(
|
||||
re.I)
|
||||
|
||||
|
||||
def _validate_roots(roots):
|
||||
"""Validate roots.
|
||||
|
||||
:param List[str] roots: a list of package roots.
|
||||
"""
|
||||
for root in roots:
|
||||
try:
|
||||
os.listdir(root)
|
||||
except OSError as exc:
|
||||
raise ValueError(
|
||||
'Error while trying to list root({}): '
|
||||
'{}'.format(root, repr(exc))
|
||||
)
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
"""Check config arguments.
|
||||
|
||||
:param argparse.Namespace config: a config namespace
|
||||
|
||||
:raises ValueError: if a config value is invalid
|
||||
"""
|
||||
_validate_roots(config.roots)
|
||||
|
||||
|
||||
def configure(config):
|
||||
"""Validate configuration and return with a package list.
|
||||
|
||||
@ -43,13 +69,8 @@ def configure(config):
|
||||
:return: 2-tuple (Configure, package-list)
|
||||
:rtype: tuple
|
||||
"""
|
||||
for r in config.roots:
|
||||
try:
|
||||
os.listdir(r)
|
||||
except OSError:
|
||||
err = sys.exc_info()[1]
|
||||
msg = "Error: while trying to list root(%s): %s"
|
||||
sys.exit(msg % (r, err))
|
||||
validate_config(config)
|
||||
add_plugins_to_config(config)
|
||||
|
||||
def packages():
|
||||
"""Return an iterable over package files in package roots."""
|
||||
@ -87,6 +108,43 @@ def configure(config):
|
||||
return config, packages
|
||||
|
||||
|
||||
def load_plugins(*groups):
|
||||
"""Load pypiserver plugins.
|
||||
|
||||
:param groups: the plugin group(s) names (str) to load. Group names
|
||||
must be one of ``const.PLUGIN_GROUPS``. If no groups are
|
||||
provided, all groups will be loaded.
|
||||
|
||||
:return: a dict whose keys are plugin group names and whose values
|
||||
are nested dicts whose keys are plugin names and whose values
|
||||
are the loaded plugins.
|
||||
:rtype: dict
|
||||
"""
|
||||
if groups and not all(g in PLUGIN_GROUPS for g in groups):
|
||||
raise ValueError(
|
||||
'Invalid group provided. Groups must '
|
||||
'be one of: {}'.format(PLUGIN_GROUPS)
|
||||
)
|
||||
groups = groups if groups else PLUGIN_GROUPS
|
||||
plugins = {}
|
||||
for group in groups:
|
||||
plugins.setdefault(group, {})
|
||||
for plugin in iter_entry_points('pypiserver.{}'.format(group)):
|
||||
plugins[group][plugin.name] = plugin.load()
|
||||
return plugins
|
||||
|
||||
|
||||
def add_plugins_to_config(config, plugins=None):
|
||||
"""Load plugins if necessary and add to a config object.
|
||||
|
||||
:param argparse.Namespace config: a config namespace
|
||||
:param dict plugins: an optional loaded plugin dict. If not
|
||||
provided, plugins will be loaded.
|
||||
"""
|
||||
plugins = load_plugins() if plugins is None else plugins
|
||||
config.plugins = plugins
|
||||
|
||||
|
||||
def auth_by_htpasswd_file(ht_pwd_file, username, password):
|
||||
"""The default ``config.auther``."""
|
||||
if ht_pwd_file is not None:
|
||||
|
10
tests/doubles.py
Normal file
10
tests/doubles.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Utilities for constructing test doubles."""
|
||||
|
||||
|
||||
class GenericNamespace(object):
|
||||
"""A generic namespace constructed from kwargs."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Convert kwargs to attributes on the instantiated object."""
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
@ -9,6 +9,8 @@ import pytest
|
||||
|
||||
from pypiserver import __main__, core
|
||||
|
||||
from .doubles import GenericNamespace
|
||||
|
||||
|
||||
# Enable logging to detect any problems with it
|
||||
__main__.init_logging(level=logging.NOTSET)
|
||||
@ -86,8 +88,8 @@ def test_listdir_bad_name(tmpdir):
|
||||
|
||||
|
||||
hashes = [
|
||||
('sha256', 'e3b0c44298fc1c149afbf4c8996fb924'), # empty-sha256
|
||||
('md5', 'd41d8cd98f00b204e9800998ecf8427e'), # empty-md5
|
||||
('sha256', 'e3b0c44298fc1c149afbf4c8996fb924'), # empty-sha256
|
||||
('md5', 'd41d8cd98f00b204e9800998ecf8427e'), # empty-md5
|
||||
]
|
||||
|
||||
|
||||
@ -96,3 +98,53 @@ def test_hashfile(tmpdir, algo, digest):
|
||||
f = tmpdir.join("empty")
|
||||
f.ensure()
|
||||
assert core.digest_file(f.strpath, algo) == digest
|
||||
|
||||
|
||||
def test_load_plugins():
|
||||
"""Test loading plugins.
|
||||
|
||||
We should at least be able to get the ones included with the full
|
||||
passlib install.
|
||||
"""
|
||||
plugins = core.load_plugins()
|
||||
assert 'htpasswd' in plugins['authenticators']
|
||||
|
||||
|
||||
def test_load_plugin_group():
|
||||
"""Test loading a single plugin group.
|
||||
|
||||
This test is not quite definitive at the time of authorship since
|
||||
there's only one plugin (therefore the output will be the same as
|
||||
for ``load_plugins()`` with no arguments). However, as soon as
|
||||
a second plugin type is added, it'll become more meaningful.
|
||||
"""
|
||||
auth_plugins = core.load_plugins('authenticators')
|
||||
assert 'htpasswd' in auth_plugins['authenticators']
|
||||
|
||||
|
||||
def test_load_plugin_bad_group():
|
||||
"""Test that trying to load a bad group raises an error."""
|
||||
with pytest.raises(ValueError):
|
||||
# hopefully this is never a legit plugin type
|
||||
core.load_plugins('fhgwgad')
|
||||
|
||||
|
||||
def test_load_plugins_bad_and_good_group():
|
||||
"""Test that the bad group is detected even among a good one."""
|
||||
with pytest.raises(ValueError):
|
||||
core.load_plugins('authenticators', 'wheelchair_assassins')
|
||||
|
||||
|
||||
def test_add_plugins_to_config_load(monkeypatch):
|
||||
"""Test that load_plugins() is called for no provided plugins."""
|
||||
monkeypatch.setattr(core, 'load_plugins', lambda *x: 'plugin_stub')
|
||||
config = GenericNamespace()
|
||||
core.add_plugins_to_config(config)
|
||||
assert config.plugins == 'plugin_stub' # pylint: disable=no-member
|
||||
|
||||
|
||||
def test_add_plugins_to_config_no_load():
|
||||
"""Test adding passed plugins to a config."""
|
||||
config = GenericNamespace()
|
||||
core.add_plugins_to_config(config, plugins='plugins!')
|
||||
assert config.plugins == 'plugins!' # pylint: disable=no-member
|
||||
|
Loading…
Reference in New Issue
Block a user