diff --git a/changelog/+24f6a8f8.feature.rst b/changelog/+24f6a8f8.feature.rst new file mode 100644 index 00000000..346bf23d --- /dev/null +++ b/changelog/+24f6a8f8.feature.rst @@ -0,0 +1,5 @@ +Add ``--lenient`` option to allow certain errors to be emitted as warnings +instead of causing Twine to exit. + +Note that use of this option represents acknowledgement that uploads may fail +through no fault of Twine! diff --git a/tests/test_settings.py b/tests/test_settings.py index af720555..2e4d3a4e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -81,7 +81,7 @@ def test_settings_transforms_repository_config_non_pypi(write_config_file): assert s.disable_progress_bar is False -def test_settings_verify_feature_compatibility() -> None: +def test_settings_verify_feature_compatibility(caplog) -> None: s = settings.Settings(skip_existing=True) s.repository_config = {"repository": repository.WAREHOUSE} try: @@ -105,6 +105,18 @@ def test_settings_verify_feature_compatibility() -> None: with pytest.raises(exceptions.UnsupportedConfiguration): s.verify_feature_capability() + s.lenient = True + try: + s.verify_feature_capability() + except exceptions.UnsupportedConfiguration as unexpected_exc: + pytest.fail( + "Expected exception to be logged instead of raised, but" + f" {unexpected_exc!r} was raised" + ) + assert len(caplog.messages) == 1 + assert "Unsupported configuration" in caplog.messages[0] + s.lenient = False + s.skip_existing = False try: s.verify_feature_capability() diff --git a/twine/exceptions.py b/twine/exceptions.py index 1b9b02b6..b7c9a8ae 100644 --- a/twine/exceptions.py +++ b/twine/exceptions.py @@ -111,15 +111,21 @@ def with_feature(self, feature: str) -> "UnsupportedConfiguration.Builder": return self def finalize(self) -> "UnsupportedConfiguration": - return UnsupportedConfiguration( - f"The configured repository {self.repository_url!r} does not " - "have support for the following features: " - f"{', '.join(self.features)} and is an unsupported " - "configuration", + return UnsupportedConfiguration.from_args( self.repository_url, - *self.features, + self.features, ) + @classmethod + def from_args( + cls, repository_url: str, features: t.List[str] + ) -> "UnsupportedConfiguration": + return cls( + f"The configured repository {repository_url!r} may not have" + f" support for the following features: {', '.join(features)}" + " and is an unsupported configuration." + ) + class UnreachableRepositoryURLDetected(TwineException): """An upload attempt was detected to a URL without a protocol prefix. diff --git a/twine/settings.py b/twine/settings.py index ba473524..dba21fdd 100644 --- a/twine/settings.py +++ b/twine/settings.py @@ -23,6 +23,8 @@ from twine import repository from twine import utils +logger = logging.getLogger(__name__) + class Settings: """Object that manages the configuration for Twine. @@ -61,6 +63,7 @@ def __init__( repository_url: Optional[str] = None, verbose: bool = False, disable_progress_bar: bool = False, + lenient: bool = False, **ignored_kwargs: Any, ) -> None: """Initialize our settings instance. @@ -107,11 +110,14 @@ def __init__( Show verbose output. :param disable_progress_bar: Disable the progress bar. + :param lenient: + Emit some errors as warnings and attempt to keep going. """ self.config_file = config_file self.comment = comment self.verbose = verbose self.disable_progress_bar = disable_progress_bar + self.lenient = lenient self.skip_existing = skip_existing self._handle_repository_options( repository_name=repository_name, @@ -278,6 +284,13 @@ def register_argparse_arguments(parser: argparse.ArgumentParser) -> None: action="store_true", help="Disable the progress bar.", ) + parser.add_argument( + "--lenient", + default=False, + required=False, + action="store_true", + help="Emit some errors as warnings and try to keep going.", + ) @classmethod def from_argparse(cls, args: argparse.Namespace) -> "Settings": @@ -325,12 +338,21 @@ def verify_feature_capability(self) -> None: """ repository_url = cast(str, self.repository_config["repository"]) + exc = None if self.skip_existing and not repository_url.startswith( (repository.WAREHOUSE, repository.TEST_WAREHOUSE) ): - raise exceptions.UnsupportedConfiguration.Builder().with_feature( - "--skip-existing" - ).with_repository_url(repository_url).finalize() + exc = ( + exceptions.UnsupportedConfiguration.Builder() + .with_feature("--skip-existing") + .with_repository_url(repository_url) + .finalize() + ) + if exc is not None: + if self.lenient: + logger.warning("Unsupported configuration: %s", exc) + else: + raise exc def check_repository_url(self) -> None: """Verify we are not using legacy PyPI.