Restore ability to drop hashing in new config (#347)

Thanks @elfjes for pointing out that I'd missed this! I also went ahead
and bumped the version in the README to 2.0.0dev1, so that it's clear
that what's in master shouldn't be what people expect from pypi or in the
docker image.
This commit is contained in:
Matthew Planchard 2020-10-11 14:16:57 -05:00 committed by GitHub
parent 8014fa56fc
commit 47d6efe196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 14 deletions

View File

@ -9,7 +9,7 @@ pypiserver - minimal PyPI server for use with pip/easy_install
==============================================================================
|pypi-ver| |travis-status| |dependencies| |python-ver| |proj-license|
:Version: 1.4.2
:Version: 2.0.0dev1
:Date: 2020-10-10
:Source: https://github.com/pypiserver/pypiserver
:PyPI: https://pypi.org/project/pypiserver/

View File

@ -2,9 +2,9 @@ import os
import re as _re
import sys
version = __version__ = "1.4.2"
version = __version__ = "2.0.0dev1"
__version_info__ = tuple(_re.split("[.-]", __version__))
__updated__ = "2020-10-10 08:15:56"
__updated__ = "2020-10-11 11:23:15"
__title__ = "pypiserver"
__summary__ = "A minimal PyPI server for use with pip/easy_install."

View File

@ -32,16 +32,24 @@ into a dict by the argument parser.
import argparse
import contextlib
import hashlib
import itertools
import io
import pkg_resources
import re
import textwrap
import sys
import typing as t
from distutils.util import strtobool as strtoint
from pypiserver import __version__
# The "strtobool" function in distutils does a nice job at parsing strings,
# but returns an integer. This just wraps it in a boolean call so that we
# get a bool.
strtobool: t.Callable[[str], bool] = lambda val: bool(strtoint(val))
# Specify defaults here so that we can use them in tests &c. and not need
# to update things in multiple places if a default changes.
class DEFAULTS:
@ -74,21 +82,37 @@ def auth_arg(arg: str) -> t.List[str]:
)
# The "no authentication" option must be specified in isolation.
if "." in items and len(items) > 1:
raise ValueError(
raise argparse.ArgumentTypeError(
"Invalid authentication options. `.` (no authentication) "
"must be specified alone."
)
return items
def hash_algo_arg(arg: str) -> t.Callable:
def hash_algo_arg(arg: str) -> t.Optional[str]:
"""Parse a hash algorithm from the string."""
if arg not in hashlib.algorithms_available:
raise ValueError(
f"Hash algorithm {arg} is not available. Please select one "
f"of {hashlib.algorithms_available}"
# The standard hashing algorithms are all made available via fully
# lowercase names, along with (sometimes) variously cased versions
# as well.
arg = arg.lower()
if arg in hashlib.algorithms_available:
return arg
try:
if not strtobool(arg):
return None
except ValueError:
# strtobool raises if the string doesn't seem like a truthiness-
# indicating string. We do want to raise in this case, but we want
# to raise our own custom message rather than raising the ValueError
# raised by strtobool.
pass
# At this point we either had an invalid hash algorithm or a truthy string
# like 'yes' or 'true'. In either case, we want to throw an error.
raise argparse.ArgumentTypeError(
f"Hash algorithm '{arg}' is not available. Please select one "
f"of {hashlib.algorithms_available}, or turn off hashing by "
"setting --hash-algo to 'off', '0', or 'false'"
)
return getattr(hashlib, arg)
def html_file_arg(arg: t.Optional[str]) -> str:
@ -129,7 +153,7 @@ def log_stream_arg(arg: str) -> t.Optional[t.IO]:
return sys.stdout
if val == "stderr":
return _ORIG_STDERR
raise ValueError(
raise argparse.ArgumentTypeError(
"Invalid option for --log-stream. Value must be one of stdout, "
"stderr, or none."
)
@ -315,7 +339,7 @@ def get_parser() -> argparse.ArgumentParser:
run_parser.add_argument(
"--hash-algo",
default=DEFAULTS.HASH_ALGO,
choices=hashlib.algorithms_available,
type=hash_algo_arg,
help=(
"Any `hashlib` available algorithm to use for generating fragments "
"on package links. Can be disabled with one of (0, no, off, false)."
@ -481,7 +505,7 @@ class RunConfig(_ConfigCommon):
self.fallback_url: str = namespace.fallback_url
self.server_method: str = namespace.server
self.overwrite: bool = namespace.overwrite
self.hash_algo: t.Callable = namespace.hash_algo
self.hash_algo: t.Optional[str] = namespace.hash_algo
self.welcome_msg: str = namespace.welcome
self.cache_control: t.Optional[int] = namespace.cache_control
self.log_req_frmt: str = namespace.log_req_frmt

View File

@ -380,6 +380,16 @@ _CONFIG_TEST_PARAMS: t.Tuple[ConfigTestCase, ...] = (
)
for algo in hashlib.algorithms_available
),
*(
ConfigTestCase(
case="Run: hash-algo disabled",
args=["run", "--hash-algo", off_value],
legacy_args=["--hash-algo", off_value],
exp_config_type=RunConfig,
exp_config_values={"hash_algo": None},
)
for off_value in ("0", "off", "false", "no", "NO")
),
# welcome file
ConfigTestCase(
case="Run: welcome file unspecified",
@ -559,6 +569,36 @@ CONFIG_TEST_PARAMS = (i[1:] for i in _CONFIG_TEST_PARAMS)
CONFIG_TEST_IDS = (i.case for i in _CONFIG_TEST_PARAMS)
class ConfigErrorCase(t.NamedTuple):
"""Configuration arguments that should cause errors.
The cases include a case descrpition, a list of arguments,
and, if desired, expected text that should be part of what
is printed out to stderr. If no text is provided, the content
of stderr will not be checked.
"""
case: str
args: t.List[str]
exp_txt: t.Optional[str]
_CONFIG_ERROR_CASES = (
*(
ConfigErrorCase(
case=f"Invalid hash algo: {val}",
args=["run", "--hash-algo", val],
exp_txt=f"Hash algorithm '{val}' is not available",
)
for val in ("true", "foo", "1", "md6")
),
)
# pylint: disable=unsubscriptable-object
CONFIG_ERROR_PARAMS = (i[1:] for i in _CONFIG_ERROR_CASES)
# pylint: enable=unsubscriptable-object
CONFIG_ERROR_IDS = (i.case for i in _CONFIG_ERROR_CASES)
@pytest.mark.parametrize(
"args, legacy_args, exp_config_type, exp_config_values",
CONFIG_TEST_PARAMS,
@ -591,6 +631,27 @@ def test_config(
assert conf == conf_legacy
@pytest.mark.parametrize(
"args, exp_txt",
CONFIG_ERROR_PARAMS,
ids=CONFIG_TEST_IDS,
)
def test_config_error(
args: t.List[str],
exp_txt: t.Optional[str],
capsys,
) -> None:
"""Validate error cases."""
with pytest.raises(SystemExit):
Config.from_args(args)
# Unfortunatley the error text is printed before the SystemExit is
# raised, rather than being raised _with_ the systemexit, so we
# need to capture stderr and check it for our expected text, if
# any was specified in the test case.
if exp_txt is not None:
assert exp_txt in capsys.readouterr().err
def test_argv_conf():
"""Config uses argv if no args are provided."""
orig_args = list(sys.argv)