fixed testpypi upload
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
"""Tests for PyPI credential setup (~/.pypirc handling).
|
||||
|
||||
The writer does text surgery to preserve the explanatory comments in the file, so
|
||||
these tests check the result of that surgery rather than trusting it. No test
|
||||
touches the real ``~/.pypirc``: every path is injected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD_ROOT = REPO_ROOT / "build"
|
||||
if str(BUILD_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BUILD_ROOT))
|
||||
|
||||
from release import auth # noqa: E402
|
||||
|
||||
TOKEN = "pypi-" + "A" * 30
|
||||
|
||||
COMMENTED_TEMPLATE = """\
|
||||
# keep me: why this file exists
|
||||
# second comment line
|
||||
|
||||
[distutils]
|
||||
index-servers =
|
||||
pypi
|
||||
|
||||
[pypi]
|
||||
repository = https://upload.pypi.org/legacy/
|
||||
username = __token__
|
||||
password = pypi-PASTE-YOUR-PRODUCTION-TOKEN-HERE
|
||||
|
||||
# [testpypi] commented out on purpose
|
||||
# password = pypi-OLD
|
||||
"""
|
||||
|
||||
|
||||
def test_parse_pypirc_reads_sections_and_ignores_comments():
|
||||
sections = auth.parse_pypirc(COMMENTED_TEMPLATE)
|
||||
assert sections["pypi"]["username"] == "__token__"
|
||||
assert "testpypi" not in sections, "commented-out sections must stay inactive"
|
||||
|
||||
|
||||
def test_placeholder_is_detected_as_not_configured(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(COMMENTED_TEMPLATE, encoding="utf-8")
|
||||
state = auth.credential_state("pypi", path=path, env={})
|
||||
assert not state.configured
|
||||
assert "placeholder" in state.problem
|
||||
|
||||
|
||||
def test_missing_file_is_reported(tmp_path):
|
||||
state = auth.credential_state("pypi", path=tmp_path / "nope", env={})
|
||||
assert not state.configured
|
||||
assert "does not exist" in state.problem
|
||||
|
||||
|
||||
def test_env_variables_count_as_configured(tmp_path):
|
||||
state = auth.credential_state(
|
||||
"pypi", path=tmp_path / "nope", env={"TWINE_PASSWORD": "pypi-x"}
|
||||
)
|
||||
assert state.configured
|
||||
assert state.source == "environment variables"
|
||||
|
||||
|
||||
def test_wrong_username_is_rejected(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(
|
||||
"[pypi]\nusername = someone@example.com\npassword = pypi-real-token-value\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
state = auth.credential_state("pypi", path=path, env={})
|
||||
assert not state.configured
|
||||
assert "__token__" in state.problem
|
||||
|
||||
|
||||
def test_write_token_replaces_placeholder_and_keeps_comments(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(COMMENTED_TEMPLATE, encoding="utf-8")
|
||||
|
||||
auth.write_token("pypi", TOKEN, path=path)
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "# keep me: why this file exists" in text
|
||||
assert "# [testpypi] commented out on purpose" in text
|
||||
assert f"password = {TOKEN}" in text
|
||||
assert "PASTE-YOUR-PRODUCTION-TOKEN-HERE" not in text
|
||||
assert text.count("[pypi]") == 1
|
||||
assert auth.credential_state("pypi", path=path, env={}).configured
|
||||
|
||||
|
||||
def test_write_token_adds_missing_section_and_index_servers(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(COMMENTED_TEMPLATE, encoding="utf-8")
|
||||
|
||||
auth.write_token("testpypi", TOKEN, path=path)
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "[testpypi]" in text
|
||||
assert auth.index_servers(text) == {"pypi", "testpypi"}
|
||||
assert auth.credential_state("testpypi", path=path, env={}).configured
|
||||
# The production entry must be untouched.
|
||||
assert "PASTE-YOUR-PRODUCTION-TOKEN-HERE" in text
|
||||
|
||||
|
||||
def test_write_token_creates_file_when_absent(tmp_path):
|
||||
path = tmp_path / "nested" / ".pypirc"
|
||||
auth.write_token("pypi", TOKEN, path=path)
|
||||
|
||||
assert path.is_file()
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "index-servers" in text
|
||||
assert auth.credential_state("pypi", path=path, env={}).configured
|
||||
|
||||
|
||||
def test_write_token_updates_existing_password_in_place(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
auth.write_token("pypi", TOKEN, path=path)
|
||||
replacement = "pypi-" + "B" * 30
|
||||
auth.write_token("pypi", replacement, path=path)
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert f"password = {replacement}" in text
|
||||
assert TOKEN not in text
|
||||
assert text.count("password") == 1
|
||||
|
||||
|
||||
def test_upsert_token_does_not_leak_into_the_next_section():
|
||||
text = "[pypi]\nusername = __token__\n\n[testpypi]\nusername = __token__\n"
|
||||
result = auth.upsert_token(text, "pypi", TOKEN)
|
||||
pypi_block, testpypi_block = result.split("[testpypi]")
|
||||
assert TOKEN in pypi_block
|
||||
assert TOKEN not in testpypi_block
|
||||
|
||||
|
||||
def test_validate_token_rejects_empty_and_whitespace():
|
||||
assert auth.validate_token("")
|
||||
assert auth.validate_token("pypi-abc def")
|
||||
assert not auth.validate_token(TOKEN)
|
||||
|
||||
|
||||
def test_token_warnings_flag_suspicious_pastes():
|
||||
assert any("pypi-" in warning for warning in auth.token_warnings("not-a-token"))
|
||||
assert any("too short" in warning for warning in auth.token_warnings("pypi-abc"))
|
||||
assert auth.token_warnings(TOKEN) == []
|
||||
|
||||
|
||||
def test_prompt_never_echoes_the_token(capsys):
|
||||
token = auth.prompt_for_token("pypi", prompt_fn=lambda label: TOKEN)
|
||||
captured = capsys.readouterr()
|
||||
assert token == TOKEN
|
||||
assert TOKEN not in captured.out
|
||||
assert "hidden" in captured.out
|
||||
|
||||
|
||||
def test_prompt_abort_returns_none():
|
||||
def _raise(label):
|
||||
raise EOFError
|
||||
|
||||
assert auth.prompt_for_token("pypi", prompt_fn=_raise) is None
|
||||
|
||||
|
||||
def test_ensure_credentials_prompts_when_interactive(tmp_path, monkeypatch, capsys):
|
||||
from release import pypi
|
||||
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
monkeypatch.setattr(auth, "prompt_for_token", lambda index, **k: TOKEN)
|
||||
# _ensure_credentials resolves the path through auth.credential_state, so the
|
||||
# monkeypatched pypirc_path applies there too.
|
||||
|
||||
options = pypi.Options(version="1.2.3", apply=True)
|
||||
assert pypi._ensure_credentials(["pypi"], options, interactive=True) is True
|
||||
assert "[ok ] wrote [pypi] credentials" in capsys.readouterr().out
|
||||
assert auth.credential_state("pypi", path=tmp_path / ".pypirc", env={}).configured
|
||||
|
||||
|
||||
def test_ensure_credentials_fails_without_a_terminal(tmp_path, monkeypatch, capsys):
|
||||
from release import pypi
|
||||
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
options = pypi.Options(version="1.2.3", apply=True)
|
||||
|
||||
assert pypi._ensure_credentials(["pypi"], options, interactive=False) is False
|
||||
output = capsys.readouterr().out
|
||||
assert "--set-token pypi" in output
|
||||
assert "prompted" in output
|
||||
assert not (tmp_path / ".pypirc").exists(), "must not write anything when it cannot prompt"
|
||||
|
||||
|
||||
def test_ensure_credentials_dry_run_warns_but_continues(tmp_path, monkeypatch, capsys):
|
||||
from release import pypi
|
||||
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
options = pypi.Options(version="1.2.3", apply=False)
|
||||
|
||||
assert pypi._ensure_credentials(["pypi"], options, interactive=False) is True
|
||||
output = capsys.readouterr().out
|
||||
assert "[warn]" in output
|
||||
assert "dry run continues" in output
|
||||
|
||||
|
||||
def test_ensure_credentials_reports_testpypi_alternative(tmp_path, monkeypatch, capsys):
|
||||
from release import pypi
|
||||
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
options = pypi.Options(version="1.2.3", apply=True)
|
||||
|
||||
assert pypi._ensure_credentials(["testpypi"], options, interactive=False) is False
|
||||
output = capsys.readouterr().out
|
||||
assert "--set-token testpypi" in output
|
||||
assert "--pypi-only" in output
|
||||
|
||||
|
||||
def test_piped_token_is_used_only_once(tmp_path, monkeypatch, capsys):
|
||||
from release import pypi
|
||||
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
options = pypi.Options(version="1.2.3", apply=True, token=TOKEN)
|
||||
|
||||
assert pypi._ensure_credentials(["pypi", "testpypi"], options, interactive=True) is False
|
||||
# Second index had no token left to consume, so it fell back to the prompt,
|
||||
# which is unavailable in tests; nothing may be written for it.
|
||||
assert options.token is None
|
||||
assert auth.credential_state("pypi", path=tmp_path / ".pypirc", env={}).configured
|
||||
assert not auth.credential_state(
|
||||
"testpypi", path=tmp_path / ".pypirc", env={}
|
||||
).configured
|
||||
|
||||
|
||||
def test_section_not_listed_in_index_servers_is_not_configured(tmp_path):
|
||||
"""twine only honours sections named in [distutils] index-servers.
|
||||
|
||||
Regression test for a real failure: a hand-added [testpypi] section looked
|
||||
configured to us while twine answered "Missing 'testpypi' section".
|
||||
"""
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(
|
||||
"[distutils]\n"
|
||||
"index-servers =\n"
|
||||
" pypi\n"
|
||||
"\n"
|
||||
"[pypi]\n"
|
||||
"username = __token__\n"
|
||||
"password = pypi-" + "A" * 30 + "\n"
|
||||
"\n"
|
||||
"[testpypi]\n"
|
||||
"username = __token__\n"
|
||||
"password = pypi-" + "B" * 30 + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
state = auth.credential_state("testpypi", path=path, env={})
|
||||
assert not state.configured
|
||||
assert "index-servers" in state.problem
|
||||
assert "--repair" in state.problem
|
||||
# The production index is unaffected.
|
||||
assert auth.credential_state("pypi", path=path, env={}).configured
|
||||
|
||||
|
||||
def test_credential_state_agrees_with_twine(tmp_path):
|
||||
"""Our verdict must match twine's own resolver for the same file."""
|
||||
twine_utils = pytest.importorskip("twine.utils")
|
||||
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(
|
||||
"[distutils]\n"
|
||||
"index-servers =\n"
|
||||
" pypi\n"
|
||||
"\n"
|
||||
"[pypi]\n"
|
||||
"username = __token__\n"
|
||||
"password = pypi-" + "A" * 30 + "\n"
|
||||
"\n"
|
||||
"[testpypi]\n"
|
||||
"username = __token__\n"
|
||||
"password = pypi-" + "B" * 30 + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
twine_sees = set(twine_utils.get_config(str(path)))
|
||||
for index in ("pypi", "testpypi"):
|
||||
ours = auth.credential_state(index, path=path, env={})
|
||||
# twine is the authority: we may only claim configured when twine agrees.
|
||||
if ours.configured:
|
||||
assert index in twine_sees, f"{index} accepted by us but ignored by twine"
|
||||
|
||||
|
||||
def test_missing_index_servers_key_falls_back_to_both_indexes(tmp_path):
|
||||
"""twine's fallback is 'pypi testpypi' only when the key is absent entirely."""
|
||||
path = tmp_path / ".pypirc"
|
||||
text = "[distutils]\nindex-servers =\n pypi\n\n[ pypi ]\n".replace("[ pypi ]", "[pypi]")
|
||||
path.write_text(text, encoding="utf-8")
|
||||
assert auth.index_servers(path.read_text(encoding="utf-8")) == {"pypi"}
|
||||
|
||||
without_key = path.read_text(encoding="utf-8").replace("index-servers =\n pypi\n", "")
|
||||
path.write_text(without_key, encoding="utf-8", newline="")
|
||||
assert auth.index_servers(path.read_text(encoding="utf-8")) == {"pypi", "testpypi"}
|
||||
|
||||
|
||||
def test_empty_index_servers_honours_nothing(tmp_path):
|
||||
"""A trap: `index-servers =` with no entries means twine honours no sections.
|
||||
|
||||
Unlike a missing key, an empty value does not trigger the pypi+testpypi
|
||||
fallback, so a section that looks configured is ignored.
|
||||
"""
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(
|
||||
"[distutils]\nindex-servers =\n\n"
|
||||
"[pypi]\nusername = __token__\npassword = pypi-" + "A" * 30 + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert auth.index_servers(path.read_text(encoding="utf-8")) == set()
|
||||
state = auth.credential_state("pypi", path=path, env={})
|
||||
assert not state.configured
|
||||
assert "index-servers" in state.problem
|
||||
|
||||
auth.repair_index_servers(path)
|
||||
assert auth.credential_state("pypi", path=path, env={}).configured
|
||||
|
||||
|
||||
def test_index_servers_reads_inline_and_continuation_forms():
|
||||
assert auth.index_servers("[distutils]\nindex-servers = pypi testpypi\n") == {
|
||||
"pypi",
|
||||
"testpypi",
|
||||
}
|
||||
assert auth.index_servers("[distutils]\nindex-servers =\n pypi\n") == {"pypi"}
|
||||
assert auth.index_servers("") == {"pypi", "testpypi"}
|
||||
|
||||
|
||||
def test_unlisted_sections_reports_the_gap(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(
|
||||
"[distutils]\nindex-servers =\n pypi\n\n"
|
||||
"[pypi]\npassword = pypi-x\n\n"
|
||||
"[testpypi]\npassword = pypi-y\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert auth.unlisted_sections(path) == ["testpypi"]
|
||||
|
||||
|
||||
def test_repair_index_servers_fixes_a_hand_edited_file(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text(
|
||||
"[distutils]\nindex-servers =\n pypi\n\n"
|
||||
"[pypi]\nusername = __token__\npassword = pypi-" + "A" * 30 + "\n\n"
|
||||
"[testpypi]\nusername = __token__\npassword = pypi-" + "B" * 30 + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert not auth.credential_state("testpypi", path=path, env={}).configured
|
||||
|
||||
added = auth.repair_index_servers(path)
|
||||
|
||||
assert added == ["testpypi"]
|
||||
assert auth.index_servers(path.read_text(encoding="utf-8")) == {"pypi", "testpypi"}
|
||||
assert auth.credential_state("testpypi", path=path, env={}).configured
|
||||
# Tokens and comments survive the repair.
|
||||
assert "pypi-" + "B" * 30 in path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_repair_index_servers_is_a_no_op_when_correct(tmp_path):
|
||||
path = tmp_path / ".pypirc"
|
||||
auth.write_token("pypi", "pypi-" + "A" * 30, path=path)
|
||||
before = path.read_text(encoding="utf-8")
|
||||
assert auth.repair_index_servers(path) == []
|
||||
assert path.read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_write_token_makes_the_result_visible_to_twine(tmp_path):
|
||||
"""The writer must produce a file twine actually honours, not just one we parse."""
|
||||
path = tmp_path / ".pypirc"
|
||||
path.write_text("[distutils]\nindex-servers =\n pypi\n", encoding="utf-8")
|
||||
|
||||
auth.write_token("testpypi", "pypi-" + "B" * 30, path=path)
|
||||
|
||||
assert auth.credential_state("testpypi", path=path, env={}).configured
|
||||
twine_utils = pytest.importorskip("twine.utils")
|
||||
assert "testpypi" in set(twine_utils.get_config(str(path)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index", ["pypi", "testpypi"])
|
||||
def test_write_then_state_round_trips(tmp_path, index):
|
||||
path = tmp_path / ".pypirc"
|
||||
auth.write_token(index, TOKEN, path=path)
|
||||
state = auth.credential_state(index, path=path, env={})
|
||||
assert state.configured, state.problem
|
||||
@@ -346,6 +346,63 @@ def test_publish_refuses_when_release_already_exists(tmp_path, monkeypatch, caps
|
||||
assert "already exists; nothing to publish" in capsys.readouterr().out
|
||||
|
||||
|
||||
def _synthetic_report_layout(tmp_path: Path, version: str) -> tuple[Path, Path]:
|
||||
"""A fake SDK checkout with wheels and a passing preflight report."""
|
||||
sdk = tmp_path / "sdk"
|
||||
wheel_dir = sdk / "SDK" / version
|
||||
wheel_dir.mkdir(parents=True)
|
||||
wheel = wheel_dir / f"orisys-{version}-cp310-cp310-win_amd64.whl"
|
||||
wheel.write_bytes(b"wheel")
|
||||
|
||||
report_dir = sdk / "build" / "release" / "reports" / version
|
||||
report_dir.mkdir(parents=True)
|
||||
(report_dir / "preflight.json").write_text(
|
||||
json.dumps(_report_payload(version, [wheel])), encoding="utf-8"
|
||||
)
|
||||
return sdk, wheel
|
||||
|
||||
|
||||
def test_upload_phases_default_is_testpypi_only():
|
||||
assert pypi.upload_phases(pypi.Options(version="1.2.3")) == ["testpypi"]
|
||||
|
||||
|
||||
def test_upload_phases_confirm_pypi_adds_the_real_index():
|
||||
options = pypi.Options(version="1.2.3", confirm_pypi=True)
|
||||
assert pypi.upload_phases(options) == ["testpypi", "pypi"]
|
||||
|
||||
|
||||
def test_upload_phases_pypi_only_skips_the_rehearsal():
|
||||
# The relevant case: a production token cannot be used against TestPyPI.
|
||||
assert pypi.upload_phases(pypi.Options(version="1.2.3", pypi_only=True)) == ["pypi"]
|
||||
|
||||
|
||||
def test_pypi_only_dry_run_targets_pypi_only(tmp_path, monkeypatch, capsys):
|
||||
sdk, wheel = _synthetic_report_layout(tmp_path, "1.2.3")
|
||||
monkeypatch.setattr(pypi, "REPO_ROOT", sdk)
|
||||
monkeypatch.setattr(pypi, "index_state", lambda index, **k: pypi.IndexState(versions=[]))
|
||||
from release import auth
|
||||
|
||||
auth.write_token("pypi", "pypi-" + "A" * 30, path=tmp_path / ".pypirc")
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
|
||||
exit_code = pypi.publish(
|
||||
pypi.Options(version="1.2.3", apply=False, pypi_only=True, skip_check=True)
|
||||
)
|
||||
output = capsys.readouterr().out
|
||||
assert exit_code == 0
|
||||
assert "--pypi-only" in output
|
||||
assert "upload 1 wheel(s) to pypi" in output
|
||||
assert "testpypi" not in output
|
||||
|
||||
|
||||
def test_pypi_only_and_confirm_pypi_are_mutually_exclusive():
|
||||
from release.cli import _cmd_pypi
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
_cmd_pypi(["--version", "1.2.3", "--pypi-only", "--confirm-pypi"])
|
||||
assert excinfo.value.code == 2
|
||||
|
||||
|
||||
def test_manifest_checks_use_the_python_placeholder():
|
||||
entry = github.manifest_entry()
|
||||
for command in entry["checks"]:
|
||||
@@ -367,27 +424,35 @@ def test_twine_upload_argv_defaults_to_pypi():
|
||||
assert "--repository" not in argv
|
||||
|
||||
|
||||
def test_credentials_hint_reports_missing(monkeypatch, tmp_path):
|
||||
def test_credentials_hint_points_at_the_setup_command(tmp_path, monkeypatch):
|
||||
from release import auth
|
||||
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
monkeypatch.delenv("TWINE_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("TWINE_USERNAME", raising=False)
|
||||
monkeypatch.setattr(pypi.Path, "home", lambda: tmp_path)
|
||||
hint = pypi.credentials_hint()
|
||||
hint = pypi.credentials_hint("pypi")
|
||||
assert hint is not None
|
||||
assert "TWINE_PASSWORD" in hint
|
||||
assert "release auth --set-token pypi" in hint
|
||||
|
||||
|
||||
def test_credentials_hint_accepts_env(monkeypatch):
|
||||
monkeypatch.setenv("TWINE_USERNAME", "__token__")
|
||||
monkeypatch.setenv("TWINE_PASSWORD", "pypi-secret")
|
||||
assert pypi.credentials_hint() is None
|
||||
def test_credentials_hint_for_testpypi_mentions_pypi_only(tmp_path, monkeypatch):
|
||||
from release import auth
|
||||
|
||||
|
||||
def test_credentials_hint_accepts_pypirc(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: tmp_path / ".pypirc")
|
||||
monkeypatch.delenv("TWINE_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("TWINE_USERNAME", raising=False)
|
||||
(tmp_path / ".pypirc").write_text("[pypi]\n", encoding="utf-8")
|
||||
monkeypatch.setattr(pypi.Path, "home", lambda: tmp_path)
|
||||
assert pypi.credentials_hint() is None
|
||||
hint = pypi.credentials_hint("testpypi")
|
||||
assert hint is not None
|
||||
assert "--set-token testpypi" in hint
|
||||
assert "--pypi-only" in hint
|
||||
|
||||
|
||||
def test_credentials_hint_silent_when_configured(tmp_path, monkeypatch):
|
||||
from release import auth
|
||||
|
||||
path = tmp_path / ".pypirc"
|
||||
auth.write_token("pypi", "pypi-" + "A" * 30, path=path)
|
||||
monkeypatch.setattr(auth, "pypirc_path", lambda: path)
|
||||
monkeypatch.delenv("TWINE_PASSWORD", raising=False)
|
||||
assert pypi.credentials_hint("pypi") is None
|
||||
|
||||
|
||||
def test_index_state_parses_versions(monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user