Backwards compatibility, deprecation warnings

This commit is contained in:
Matthew Planchard 2018-07-09 21:21:21 -05:00
parent 0763077124
commit 0da6c03c72
10 changed files with 295 additions and 227 deletions

@ -8,9 +8,10 @@ from __future__ import print_function
import logging
import sys
import warnings
import pypiserver
from pypiserver.config import PypiserverParserFactory
from pypiserver.config import ConfigFactory
from pypiserver import bottle
import functools as ft
@ -44,11 +45,8 @@ def _logwrite(logger, level, msg):
logger.log(level, msg)
def main(argv=None):
config = PypiserverParserFactory(
parser_type='pypi-server'
).get_parser().parse_args(args=argv)
def _run_app_from_config(config):
"""Run a bottle application for the given config."""
if (not config.authenticate and config.password_file != '.' or
config.authenticate and config.password_file == '.'):
auth_err = (
@ -90,5 +88,30 @@ def main(argv=None):
)
def main(argv=None):
"""Run the deprecated pypi-server command."""
PY2 = sys.version_info < (3,)
if PY2:
# I honestly don't know why Python 2 is not raising this warning
# with "default" as the filter.
warnings.filterwarnings('always', category=DeprecationWarning)
warnings.warn(DeprecationWarning(
'The "pypi-server" command has been deprecated and will be removed '
'in the next major release. Please use "pypiserver run" or '
'"pypiserver update" instead.'
))
if PY2:
warnings.filterwarnings('default', category=DeprecationWarning)
config = ConfigFactory(
parser_type='pypi-server'
).get_parser().parse_args(args=argv)
_run_app_from_config(config)
def _new_main():
"""Run the new pypiserver command."""
_run_app_from_config(ConfigFactory().get_parsed())
if __name__ == "__main__":
main()

@ -8,6 +8,7 @@ import re
import xml.dom.minidom
import zipfile
from collections import namedtuple
from warnings import warn
try:
from io import BytesIO
@ -46,15 +47,26 @@ _bottle_upload_filename_re = re.compile(r'^[a-z0-9_.!+-]+$', re.I)
Upload = namedtuple('Upload', 'pkg sig')
def app(config=None, auther=None):
def app(config=None, auther=None, **kwargs):
"""Return a hydrated app using the provided config and ``auther``.
:param argparse.Namespace config: a hydrated config namespace
:param callable auther: a callable authenticator
:param dict kwargs: DEPRECATED. Config keyword arguments.
"""
if config.auther is None and auther is not None:
setattr(config, 'auther', auther)
if kwargs:
warn(DeprecationWarning(
'Passing arbitrary keyword arguments to app() has been '
'deprecated. Please use config.ConfigFactory to generate '
'a config and pass it to this function.'
))
for key, value in kwargs:
if key in config:
setattr(config, key, value)
config, packages = core.configure(config)
return create_app(config, packages)

@ -50,7 +50,7 @@ class _Defaults(object):
server = 'auto'
class PypiserverHelpFormatter(ArgumentDefaultsHelpFormatter):
class _HelpFormatter(ArgumentDefaultsHelpFormatter):
"""A custom formatter to flip our one confusing argument.
``--disable-fallback`` is stored as ``redirect_to_fallback``,
@ -64,12 +64,10 @@ class PypiserverHelpFormatter(ArgumentDefaultsHelpFormatter):
if '--disable-fallback' in action.option_strings:
return action.help + ' (default: False)'
else:
return super(
PypiserverHelpFormatter, self
)._get_help_string(action)
return super(_HelpFormatter, self)._get_help_string(action)
class PypiserverCustomParsers(object):
class _CustomParsers(object):
"""Collect custom parsers."""
@staticmethod
@ -125,7 +123,7 @@ class PypiserverCustomParsers(object):
return verbosities[-1]
class PypiserverParser(ArgumentParser):
class _PypiserverParser(ArgumentParser):
"""Allow extra actions following the final parse.
Actions like "count", and regular "store" actions when "nargs" is
@ -135,15 +133,15 @@ class PypiserverParser(ArgumentParser):
"""
extra_parsers = {
'authenticate': PypiserverCustomParsers.auth,
'hash_algo': PypiserverCustomParsers.hash_algo,
'roots': PypiserverCustomParsers.roots,
'verbosity': PypiserverCustomParsers.verbosity,
'authenticate': _CustomParsers.auth,
'hash_algo': _CustomParsers.hash_algo,
'roots': _CustomParsers.roots,
'verbosity': _CustomParsers.verbosity,
}
def parse_args(self, args=None, namespace=None):
"""Parse arguments."""
parsed = super(PypiserverParser, self).parse_args(
parsed = super(_PypiserverParser, self).parse_args(
args=args, namespace=namespace
)
for attr, parser in self.extra_parsers.items():
@ -152,12 +150,11 @@ class PypiserverParser(ArgumentParser):
return parsed
class PypiserverParserFactory(object):
"""Create a pypiserver argument parser."""
class ConfigFactory(object):
"""Factory for pypiserver configs and parsers."""
def __init__(self, parser_cls=PypiserverParser,
help_formatter=PypiserverHelpFormatter,
parser_type='pypiserver'):
def __init__(self, parser_cls=_PypiserverParser,
help_formatter=_HelpFormatter, parser_type='pypiserver'):
"""Instantiate the factory.
:param argparse.HelpFormatter help_formatter: the HelpForamtter class
@ -169,6 +166,22 @@ class PypiserverParserFactory(object):
self.parser_cls = parser_cls
self.parser_type = parser_type
def get_default(self, subcommand='run'):
"""Return a parsed config with default argument values.
:param str subcommand:
the subcommand for which to return default arguments.
:rtype: argparse.Namespace
"""
return self.get_parser().parse_args([subcommand])
def get_parsed(self):
"""Return arguments parsed from the commandline.
:rtype: argparse.Namespace
"""
return self.get_parser().parse_args()
def get_parser(self):
"""Return an ArgumentParser instance with all arguments populated.

@ -60,10 +60,10 @@ def configure(config):
if not callable(config.auther):
if config.password_file and config.password_file != '.':
from passlib.apache import HtpasswdFile
htPsswdFile = HtpasswdFile(config.password_file)
ht_pwd_file = HtpasswdFile(config.password_file)
else:
config.password_file = htPsswdFile = None
config.auther = functools.partial(auth_by_htpasswd_file, htPsswdFile)
config.password_file = ht_pwd_file = None
config.auther = functools.partial(auth_by_htpasswd_file, ht_pwd_file)
try:
# pkg_resources.resource_filename() is not supported for zipfiles,
@ -87,11 +87,11 @@ def configure(config):
return config, packages
def auth_by_htpasswd_file(htPsswdFile, username, password):
def auth_by_htpasswd_file(ht_pwd_file, username, password):
"""The default ``config.auther``."""
if htPsswdFile is not None:
htPsswdFile.load_if_changed()
return htPsswdFile.check_password(username, password)
if ht_pwd_file is not None:
ht_pwd_file.load_if_changed()
return ht_pwd_file.check_password(username, password)
mimetypes.add_type("application/octet-stream", ".egg")

@ -3,7 +3,7 @@
import os
from ._app import app
from .config import str2bool, PypiserverParserFactory
from .config import str2bool, ConfigFactory
def _str_strip(string):
@ -46,7 +46,7 @@ def paste_app_factory(global_config, **local_conf):
return os.path.expanduser(root)
return root
c = PypiserverParserFactory(
c = ConfigFactory(
parser_type='pypi-server'
).get_parser().parse_args([])

@ -74,7 +74,10 @@ setup(
zip_safe=True,
entry_points={
'paste.app_factory': ['main=pypiserver.paste:paste_app_factory'],
'console_scripts': ['pypi-server=pypiserver.__main__:main']
'console_scripts': [
'pypi-server=pypiserver.__main__:main',
'pypiserver=pypiserver.__main__:_new_main',
]
},
options={
'bdist_wheel': {'universal': True},

@ -30,24 +30,18 @@ __main__.init_logging(level=logging.NOTSET)
hp = HTMLParser()
# @pytest.fixture()
# def _app(app):
# return app.module
@pytest.fixture
def app(tmpdir):
conf = config.PypiserverParserFactory(
conf = config.ConfigFactory(
parser_type='pypi-server'
).get_parser().parse_args(
['-a', '.', tmpdir.strpath]
)
# return app(root=tmpdir.strpath, authenticated=[])
return pypiserver.app(conf)
def app_from_args(args):
conf = config.PypiserverParserFactory(
conf = config.ConfigFactory(
parser_type='pypi-server'
).get_parser().parse_args(args)
return pypiserver.app(conf)

@ -1,5 +1,6 @@
"""Test the ArgumentParser and associated functions."""
import argparse
import logging
from os import getcwd
from os.path import exists, expanduser
@ -39,9 +40,9 @@ def test_argument_formatter(options, expected_help):
case.
"""
action = StubAction(options)
assert config.PypiserverHelpFormatter(
'prog'
)._get_help_string(action) == (expected_help)
assert config._HelpFormatter('prog')._get_help_string(action) == (
expected_help
)
class TestCustomParsers(object):
@ -59,28 +60,28 @@ class TestCustomParsers(object):
))
def test_auth_parse_success(self, arg, exp):
"""Test parsing auth strings from the commandline."""
assert config.PypiserverCustomParsers.auth(arg) == exp
assert config._CustomParsers.auth(arg) == exp
def test_auth_parse_disallowed_item(self):
"""Test that including a non-whitelisted action throws."""
with pytest.raises(ValueError):
config.PypiserverCustomParsers.auth('download update foo')
config._CustomParsers.auth('download update foo')
def test_roots_parse_abspath(self):
"""Test the parsing of root directories returns absolute paths."""
assert config.PypiserverCustomParsers.roots(
assert config._CustomParsers.roots(
['./foo']
) == ['{}/foo'.format(getcwd())]
def test_roots_parse_home(self):
"""Test that parsing of root directories expands the user home."""
assert config.PypiserverCustomParsers.roots(
assert config._CustomParsers.roots(
['~/foo']
) == ([expanduser('~/foo')])
def test_roots_parse_both(self):
"""Test that root directories are both expanded and absolute-ed."""
assert config.PypiserverCustomParsers.roots(
assert config._CustomParsers.roots(
['~/foo/..']
) == [expanduser('~')]
@ -95,7 +96,7 @@ class TestCustomParsers(object):
))
def test_verbosity_parse(self, verbosity, exp):
"""Test converting a number of -v's into a log level."""
assert config.PypiserverCustomParsers.verbosity(verbosity) == exp
assert config._CustomParsers.verbosity(verbosity) == exp
class TestDeprecatedParser:
@ -104,9 +105,7 @@ class TestDeprecatedParser:
@pytest.fixture()
def parser(self):
"""Return a deprecated parser."""
return config.PypiserverParserFactory(
parser_type='pypi-server'
).get_parser()
return config.ConfigFactory(parser_type='pypi-server').get_parser()
def test_version_exits(self, parser):
"""Test that asking for the version exits the program."""
@ -230,7 +229,7 @@ class TestDeprecatedParser:
'resource_filename',
Mock(side_effect=NotImplementedError)
)
assert config.PypiserverParserFactory(
assert config.ConfigFactory(
parser_type='pypi-server'
).get_parser().parse_args([]).welcome_file == const.STANDALONE_WELCOME
@ -327,7 +326,7 @@ class TestParser:
@pytest.fixture()
def parser(self):
"""Return a deprecated parser."""
return config.PypiserverParserFactory().get_parser()
return config.ConfigFactory().get_parser()
# **********************************************************************
# Root Command
@ -510,7 +509,7 @@ class TestParser:
'resource_filename',
Mock(side_effect=NotImplementedError)
)
assert config.PypiserverParserFactory().get_parser().parse_args(
assert config.ConfigFactory().get_parser().parse_args(
['run']
).welcome_file == const.STANDALONE_WELCOME
@ -604,3 +603,45 @@ class TestParser:
"""Test specifying the execute flag."""
args.insert(0, 'update')
assert parser.parse_args(args).download_directory is exp
class TestReadyMades(object):
"""Test generating ready-made configs."""
def test_get_default(self):
"""Test getting the default config."""
conf = config.ConfigFactory().get_default()
assert any(d in conf for d in vars(config._Defaults))
for default, value in vars(config._Defaults).items():
if default in conf:
if default == 'roots':
assert getattr(conf, default) == (
[expanduser(v) for v in value]
)
elif default == 'authenticate':
assert getattr(conf, default) == (
[a for a in value.split()]
)
else:
assert getattr(conf, default) == value
def test_get_default_specify_subcommand(self):
"""Test getting default args for a non-default subcommand."""
conf = config.ConfigFactory().get_default(subcommand='update')
exp_defaults = (
('execute', False),
('pre', False),
('download_directory', None)
)
for default, value in exp_defaults:
assert getattr(conf, default) is value
def test_get_parsed(self, monkeypatch):
"""Test getting a Namespace from commandline args."""
monkeypatch.setattr(
argparse._sys,
'argv',
['pypiserver', 'run', '--interface', '1.2.3.4']
)
conf = config.ConfigFactory().get_parsed()
assert conf.host == '1.2.3.4'

@ -1,47 +1,16 @@
"""
Test module for . . .
"""
# Standard library imports
"""Test module for pypiserver.__init__"""
from __future__ import (absolute_import, division,
print_function, unicode_literals)
import logging
from os.path import abspath, dirname, join, realpath
from sys import path
# Third party imports
import pytest
# Local imports
logger = logging.getLogger(__name__)
test_dir = realpath(dirname(__file__))
src_dir = abspath(join(test_dir, '..'))
path.append(src_dir)
print(path)
import pypiserver
# @pytest.mark.parametrize('conf_options', [
# {},
# {'root': '~/stable_packages'},
# {'root': '~/unstable_packages', 'authenticated': 'upload',
# 'passwords': '~/htpasswd'},
# # Verify that the strip parser works properly.
# {'authenticated': str('upload')},
# ])
# def test_paste_app_factory(conf_options, monkeypatch):
# """Test the paste_app_factory method"""
# monkeypatch.setattr('pypiserver.core.configure',
# lambda **x: (x, [x.keys()]))
# pypiserver.paste_app_factory({}, **conf_options)
logger = logging.getLogger(__name__)
def test_app_factory(monkeypatch, tmpdir):
# monkeypatch.setattr('pypiserver.core.configure',
# lambda **x: (x, [x.keys()]))
conf = pypiserver.config.PypiserverParserFactory(
"""Test creating an app."""
conf = pypiserver.config.ConfigFactory(
parser_type='pypi-server'
).get_parser().parse_args([str(tmpdir)])
assert pypiserver.app(conf) is not pypiserver.app(conf)

@ -1,12 +1,19 @@
#! /usr/bin/env py.test
"""Test the __main__ module."""
import sys, os, pytest, logging
from pypiserver import __main__
import argparse
import logging
import os
import sys
import warnings
try:
from unittest import mock
except ImportError:
import mock
import pytest
from pypiserver import __main__
class main_wrapper(object):
@ -16,7 +23,8 @@ class main_wrapper(object):
def __call__(self, argv):
sys.stdout.write("Running %s\n" % (argv,))
__main__.main(argv)
with warnings.catch_warnings():
__main__.main(argv)
return self.run_kwargs
@ -41,140 +49,145 @@ def main(request, monkeypatch):
return main
def test_default_pkgdir(main):
main([])
assert os.path.normpath(main.pkgdir) == (
os.path.normpath(os.path.expanduser("~/packages"))
)
class TestMain(object):
"""Test the main() method."""
def test_default_pkgdir(self, main):
main([])
assert os.path.normpath(main.pkgdir) == (
os.path.normpath(os.path.expanduser("~/packages"))
)
def test_noargs(self, main):
assert main([]) == {'host': "0.0.0.0", 'port': 8080, 'server': "auto"}
def test_port(self, main):
expected = dict(host="0.0.0.0", port=8081, server="auto")
assert main(["--port=8081"]) == expected
assert main(["--port", "8081"]) == expected
assert main(["-p", "8081"]) == expected
def test_server(self, main):
assert main(["--server=paste"])["server"] == "paste"
assert main(["--server", "cherrypy"])["server"] == "cherrypy"
@pytest.mark.skipif(True, reason='deprecated')
def test_root(self, main):
main(["--root", "."])
assert main.app.module.packages.root == os.path.abspath(".")
assert main.pkgdir == os.path.abspath(".")
@pytest.mark.skipif(True, reason='deprecated')
def test_root_r(self, main):
main(["-r", "."])
assert main.app.module.packages.root == os.path.abspath(".")
assert main.pkgdir == os.path.abspath(".")
def test_fallback_url(self, main):
main(["--fallback-url", "https://pypi.mirror/simple"])
assert main.app.config.fallback_url == "https://pypi.mirror/simple"
def test_fallback_url_default(self, main):
main([])
assert main.app.config.fallback_url == "https://pypi.org/simple"
def test_hash_algo_default(self, main):
main([])
assert main.app.config.hash_algo == 'md5'
def test_hash_algo(self, main):
main(['--hash-algo=sha256'])
assert main.app.config.hash_algo == 'sha256'
def test_hash_algo_off(self, main):
main(['--hash-algo=off'])
assert main.app.config.hash_algo is None
main(['--hash-algo=0'])
assert main.app.config.hash_algo is None
main(['--hash-algo=no'])
assert main.app.config.hash_algo is None
main(['--hash-algo=false'])
assert main.app.config.hash_algo is None
def test_hash_algo_BAD(self, main):
with pytest.raises(SystemExit):
main(['--hash-algo', 'BAD'])
def test_logging(self, main, tmpdir):
logfile = tmpdir.mkdir("logs").join('test.log')
main(["-v", "--log-file", logfile.strpath])
assert logfile.check(), logfile
# @pytest.mark.filterwarnings('ignore::DeprecationWarning')
def test_logging_verbosity(self, main):
main([])
assert logging.getLogger().level == logging.WARN
main(["-v"])
assert logging.getLogger().level == logging.INFO
main(["-v", "-v"])
assert logging.getLogger().level == logging.DEBUG
main(["-v", "-v", "-v"])
assert logging.getLogger().level == logging.NOTSET
def test_welcome_file(self, main):
sample_msg_file = os.path.join(
os.path.dirname(__file__),
"sample_msg.html"
)
main(["--welcome", sample_msg_file])
assert "Hello pypiserver tester!" in main.app.config.welcome_msg
def test_welcome_file_default(self, main):
main([])
assert "Welcome to pypiserver!" in main.app.config.welcome_msg
def test_password_without_auth_list(self, main, monkeypatch):
sysexit = mock.MagicMock(side_effect=ValueError('BINGO'))
monkeypatch.setattr('sys.exit', sysexit)
with pytest.raises(ValueError) as ex:
main(["-P", "pswd-file", "-a", ""])
assert ex.value.args[0] == 'BINGO'
with pytest.raises(ValueError) as ex:
main(["-a", "."])
assert ex.value.args[0] == 'BINGO'
with pytest.raises(ValueError) as ex:
main(["-a", ""])
assert ex.value.args[0] == 'BINGO'
with pytest.raises(ValueError) as ex:
main(["-P", "."])
assert ex.value.args[0] == 'BINGO'
def test_password_alone(self, main, monkeypatch):
monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock())
monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock())
main(["-P", "pswd-file"])
assert main.app.config.authenticate == ['update']
def test_dot_password_without_auth_list(self, main, monkeypatch):
main(["-P", ".", "-a", ""])
assert main.app.config.authenticate == []
main(["-P", ".", "-a", "."])
assert main.app.config.authenticate == []
def test_noargs(main):
assert main([]) == {'host': "0.0.0.0", 'port': 8080, 'server': "auto"}
class TestPypiserverDeprecation(object):
"""Test the deprecation of the old pypi-server command.
Note that these tests should be removed when the pypi-server
command is removed.
"""
def test_port(main):
expected = dict(host="0.0.0.0", port=8081, server="auto")
assert main(["--port=8081"]) == expected
assert main(["--port", "8081"]) == expected
assert main(["-p", "8081"]) == expected
@pytest.fixture(autouse=True)
def patch_run(self, monkeypatch):
"""Monkeypatch argv and the _run_app_from_config method."""
monkeypatch.setattr(argparse._sys, 'argv', ['pypi-server'])
monkeypatch.setattr(__main__, '_run_app_from_config', lambda c: None)
def test_server(main):
assert main(["--server=paste"])["server"] == "paste"
assert main(["--server", "cherrypy"])["server"] == "cherrypy"
@pytest.mark.skipif(True, reason='deprecated')
def test_root(main):
main(["--root", "."])
assert main.app.module.packages.root == os.path.abspath(".")
assert main.pkgdir == os.path.abspath(".")
@pytest.mark.skipif(True, reason='deprecated')
def test_root_r(main):
main(["-r", "."])
assert main.app.module.packages.root == os.path.abspath(".")
assert main.pkgdir == os.path.abspath(".")
def test_fallback_url(main):
main(["--fallback-url", "https://pypi.mirror/simple"])
# assert main.app.module.config.fallback_url == "https://pypi.mirror/simple"
assert main.app.config.fallback_url == "https://pypi.mirror/simple"
def test_fallback_url_default(main):
main([])
# assert main.app.module.config.fallback_url == "https://pypi.org/simple"
assert main.app.config.fallback_url == "https://pypi.org/simple"
def test_hash_algo_default(main):
main([])
assert main.app.config.hash_algo == 'md5'
# assert main.app.module.config.hash_algo == 'md5'
def test_hash_algo(main):
main(['--hash-algo=sha256'])
# assert main.app.module.config.hash_algo == 'sha256'
assert main.app.config.hash_algo == 'sha256'
def test_hash_algo_off(main):
main(['--hash-algo=off'])
assert main.app.config.hash_algo is None
main(['--hash-algo=0'])
assert main.app.config.hash_algo is None
main(['--hash-algo=no'])
assert main.app.config.hash_algo is None
main(['--hash-algo=false'])
assert main.app.config.hash_algo is None
def test_hash_algo_BAD(main):
with pytest.raises(SystemExit) as excinfo:
main(['--hash-algo', 'BAD'])
def test_logging(main, tmpdir):
logfile = tmpdir.mkdir("logs").join('test.log')
main(["-v", "--log-file", logfile.strpath])
assert logfile.check(), logfile
def test_logging_verbosity(main):
main([])
assert logging.getLogger().level == logging.WARN
main(["-v"])
assert logging.getLogger().level == logging.INFO
main(["-v", "-v"])
assert logging.getLogger().level == logging.DEBUG
main(["-v", "-v", "-v"])
assert logging.getLogger().level == logging.NOTSET
def test_welcome_file(main):
sample_msg_file = os.path.join(os.path.dirname(__file__), "sample_msg.html")
main(["--welcome", sample_msg_file])
assert "Hello pypiserver tester!" in main.app.config.welcome_msg
def test_welcome_file_default(main):
main([])
assert "Welcome to pypiserver!" in main.app.config.welcome_msg
def test_password_without_auth_list(main, monkeypatch):
sysexit = mock.MagicMock(side_effect=ValueError('BINGO'))
monkeypatch.setattr('sys.exit', sysexit)
with pytest.raises(ValueError) as ex:
main(["-P", "pswd-file", "-a", ""])
assert ex.value.args[0] == 'BINGO'
with pytest.raises(ValueError) as ex:
main(["-a", "."])
assert ex.value.args[0] == 'BINGO'
with pytest.raises(ValueError) as ex:
main(["-a", ""])
assert ex.value.args[0] == 'BINGO'
with pytest.raises(ValueError) as ex:
main(["-P", "."])
assert ex.value.args[0] == 'BINGO'
def test_password_alone(main, monkeypatch):
monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock())
monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock())
main(["-P", "pswd-file"])
assert main.app.config.authenticate == ['update']
def test_dot_password_without_auth_list(main, monkeypatch):
main(["-P", ".", "-a", ""])
assert main.app.config.authenticate == []
main(["-P", ".", "-a", "."])
assert main.app.config.authenticate == []
def test_warns(self):
"""Test that a deprecation warning is thrown."""
warnings.simplefilter('always', category=DeprecationWarning)
with pytest.warns(DeprecationWarning):
__main__.main()