@@ -55,6 +55,7 @@ def __init__(
5555 comment : Optional [str ] = None ,
5656 config_file : str = utils .DEFAULT_CONFIG_FILE ,
5757 skip_existing : bool = False ,
58+ skip_existing_non_pypi : bool = False ,
5859 cacert : Optional [str ] = None ,
5960 client_cert : Optional [str ] = None ,
6061 repository_name : str = "pypi" ,
@@ -90,6 +91,9 @@ def __init__(
9091 Specify whether twine should continue uploading files if one
9192 of them already exists. This primarily supports PyPI. Other
9293 package indexes may not be supported.
94+ :param skip_existing_non_pypi:
95+ Allow --skip-existing to work with non-PyPI repositories.
96+ Use at your own risk with third-party registries.
9397 :param cacert:
9498 The path to the bundle of certificates used to verify the TLS
9599 connection to the package index.
@@ -113,6 +117,7 @@ def __init__(
113117 self .verbose = verbose
114118 self .disable_progress_bar = disable_progress_bar
115119 self .skip_existing = skip_existing
120+ self .skip_existing_non_pypi = skip_existing_non_pypi
116121 self ._handle_repository_options (
117122 repository_name = repository_name ,
118123 repository_url = repository_url ,
@@ -245,9 +250,18 @@ def register_argparse_arguments(parser: argparse.ArgumentParser) -> None:
245250 default = False ,
246251 action = "store_true" ,
247252 help = "Continue uploading files if one already exists. (Only valid "
248- "when uploading to PyPI. Other implementations may not "
253+ "when uploading to PyPI by default. For other repositories, use "
254+ "--skip-existing-non-pypi flag. Other implementations may not "
249255 "support this.)" ,
250256 )
257+ parser .add_argument (
258+ "--skip-existing-non-pypi" ,
259+ action = utils .EnvironmentFlag ,
260+ env = "TWINE_SKIP_EXISTING_NON_PYPI" ,
261+ help = "Allow --skip-existing to work with non-PyPI repositories. "
262+ "Use at your own risk with third-party registries. (Can also be "
263+ "set via %(env)s environment variable.)" ,
264+ )
251265 parser .add_argument (
252266 "--cert" ,
253267 action = utils .EnvironmentDefault ,
@@ -317,7 +331,10 @@ def verify_feature_capability(self) -> None:
317331 """Verify configured settings are supported for the configured repository.
318332
319333 This presently checks:
320- - ``--skip-existing`` was only provided for PyPI and TestPyPI
334+
335+ - ``--skip-existing`` was only provided for PyPI and TestPyPI, unless
336+ explicitly allowed via --skip-existing-non-pypi flag or
337+ TWINE_SKIP_EXISTING_NON_PYPI environment variable
321338
322339 :raises twine.exceptions.UnsupportedConfiguration:
323340 The configured features are not available with the configured
@@ -328,9 +345,15 @@ def verify_feature_capability(self) -> None:
328345 if self .skip_existing and not repository_url .startswith (
329346 (repository .WAREHOUSE , repository .TEST_WAREHOUSE )
330347 ):
331- raise exceptions .UnsupportedConfiguration .Builder ().with_feature (
332- "--skip-existing"
333- ).with_repository_url (repository_url ).finalize ()
348+ # Allow opt-in for non-PyPI repositories (e.g., for testing or
349+ # third-party registries that support PEP 694's 409 response)
350+ if not self .skip_existing_non_pypi :
351+ raise (
352+ exceptions .UnsupportedConfiguration .Builder ()
353+ .with_feature ("--skip-existing" )
354+ .with_repository_url (repository_url )
355+ .finalize ()
356+ )
334357
335358 def check_repository_url (self ) -> None :
336359 """Verify we are not using legacy PyPI.
0 commit comments