Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add published field to Release #17257

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion tests/common/db/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ class Meta:
lambda o: hashlib.blake2b(o.filename.encode("utf8"), digest_size=32).hexdigest()
)
upload_time = factory.Faker(
"date_time_between_dates", datetime_start=datetime.datetime(2008, 1, 1)
"date_time_between_dates",
datetime_start=datetime.datetime(2008, 1, 1),
)
path = factory.LazyAttribute(
lambda o: "/".join(
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/legacy/api/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,91 @@ def test_all_non_prereleases_yanked(self, monkeypatch, db_request):
db_request.matchdict = {"name": project.normalized_name}
assert json.latest_release_factory(db_request) == release

def test_with_staged(self, db_request):
project = ProjectFactory.create()
release = ReleaseFactory.create(project=project, version="1.0")
ReleaseFactory.create(project=project, version="2.0", published=False)
db_request.matchdict = {"name": project.normalized_name}
assert json.latest_release_factory(db_request) == release

def test_only_staged(self, db_request):
project = ProjectFactory.create()
ReleaseFactory.create(project=project, version="1.0", published=False)
db_request.matchdict = {"name": project.normalized_name}
resp = json.latest_release_factory(db_request)

assert isinstance(resp, HTTPNotFound)
_assert_has_cors_headers(resp.headers)

@pytest.mark.parametrize(
("release0_state", "release1_state", "release2_state", "latest_release"),
[
("published", "published", "published", 2),
("published", "published", "staged", 1),
("published", "published", "yanked", 1),
("published", "staged", "published", 2),
("published", "staged", "staged", 0),
("published", "staged", "yanked", 0),
("published", "yanked", "published", 2),
("published", "yanked", "staged", 0),
("published", "yanked", "yanked", 0),
("staged", "published", "published", 2),
("staged", "published", "staged", 1),
("staged", "published", "yanked", 1),
("staged", "staged", "published", 2),
("staged", "staged", "staged", -1),
("staged", "staged", "yanked", 2), # Same endpoint as none yanked
("staged", "yanked", "published", 2),
("staged", "yanked", "staged", 1),
("staged", "yanked", "yanked", 2),
("yanked", "published", "published", 2),
("yanked", "published", "staged", 1),
("yanked", "published", "yanked", 1),
("yanked", "staged", "published", 2),
("yanked", "staged", "staged", 0),
("yanked", "staged", "yanked", 2),
("yanked", "yanked", "published", 2),
("yanked", "yanked", "staged", 1),
("yanked", "yanked", "yanked", 2),
],
)
def test_with_mixed_states(
self, db_request, release0_state, release1_state, release2_state, latest_release
):
project = ProjectFactory.create()

releases = []
for version, state in [
("1.0", release0_state),
("1.1", release1_state),
("2.0", release2_state),
]:
if state == "published":
releases.append(
ReleaseFactory.create(
project=project, version=version, published=True
)
)
elif state == "staged":
releases.append(
ReleaseFactory.create(
project=project, version=version, published=False
)
)
else:
releases.append(
ReleaseFactory.create(project=project, version=version, yanked=True)
)

db_request.matchdict = {"name": project.normalized_name}

resp = json.latest_release_factory(db_request)
if latest_release >= 0:
assert resp == releases[latest_release]
else:
assert isinstance(resp, HTTPNotFound)
_assert_has_cors_headers(resp.headers)

def test_project_quarantined(self, monkeypatch, db_request):
project = ProjectFactory.create(
lifecycle_status=LifecycleStatus.QuarantineEnter
Expand Down Expand Up @@ -191,6 +276,15 @@ def test_renders(self, pyramid_config, db_request, db_session):
)
]

ReleaseFactory.create(
project=project,
version="3.1",
description=DescriptionFactory.create(
content_type=description_content_type
),
published=False,
)

for urlspec in project_urls:
label, _, purl = urlspec.partition(",")
db_session.add(
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/packaging/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Project,
ProjectFactory,
ProjectMacaroonWarningAssociation,
Release,
ReleaseURL,
)

Expand Down Expand Up @@ -1216,6 +1217,25 @@ def test_description_relationship(self, db_request):
assert description in db_request.db.deleted


@pytest.mark.parametrize(
"published",
[
True,
False,
],
)
def test_filter_staged_releases(db_request, published):
DBReleaseFactory.create(published=published)
assert db_request.db.query(Release).count() == (1 if published else 0)


def test_filter_staged_releases_with_staged(db_request):
DBReleaseFactory.create(published=False)
assert (
db_request.db.query(Release).execution_options(include_staged=True).count() == 1
)


class TestFile:
def test_requires_python(self, db_session):
"""
Expand Down
33 changes: 32 additions & 1 deletion tests/unit/packaging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ def test_only_yanked_release(self, monkeypatch, db_request):
assert resp is response
assert release_detail.calls == [pretend.call(release, db_request)]

def test_with_staged(self, monkeypatch, db_request):
project = ProjectFactory.create()
release = ReleaseFactory.create(project=project, version="1.0")
ReleaseFactory.create(project=project, version="1.1", published=False)

response = pretend.stub()
release_detail = pretend.call_recorder(lambda ctx, request: response)
monkeypatch.setattr(views, "release_detail", release_detail)

resp = views.project_detail(project, db_request)
assert resp is response
assert release_detail.calls == [pretend.call(release, db_request)]


class TestReleaseDetail:
def test_normalizing_name_redirects(self, db_request):
Expand Down Expand Up @@ -202,14 +215,27 @@ def test_detail_rendered(self, db_request):
yanked_reason="plaintext yanked reason",
)
]

# Add a staged version
staged_release = ReleaseFactory.create(
project=project,
version="5.1",
description=DescriptionFactory.create(
raw="unrendered description",
html="rendered description",
content_type="text/html",
),
published=False,
)

files = [
FileFactory.create(
release=r,
filename=f"{project.name}-{r.version}.tar.gz",
python_version="source",
packagetype="sdist",
)
for r in releases
for r in releases + [staged_release]
]

# Create a role for each user
Expand All @@ -226,6 +252,7 @@ def test_detail_rendered(self, db_request):
"bdists": [],
"description": "rendered description",
"latest_version": project.latest_version,
# Non published version are not listed here
"all_versions": [
(r.version, r.created, r.is_prerelease, r.yanked, r.yanked_reason)
for r in reversed(releases)
Expand Down Expand Up @@ -324,6 +351,10 @@ def test_long_singleline_license(self, db_request):
"characters, it's really so lo..."
)

def test_created_with_published(self, db_request):
release = ReleaseFactory.create()
assert release.published is True


class TestPEP740AttestationViewer:

Expand Down
17 changes: 17 additions & 0 deletions warehouse/packaging/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import (
Mapped,
ORMExecuteState,
attribute_keyed_dict,
declared_attr,
mapped_column,
validates,
with_loader_criteria,
)
from urllib3.exceptions import LocationParseError
from urllib3.util import parse_url
Expand Down Expand Up @@ -1058,6 +1060,21 @@ def ensure_monotonic_journals(config, session, flush_context, instances):
return


@db.listens_for(db.Session, "do_orm_execute")
def filter_staged_release(_, state: ORMExecuteState):
if (
state.is_select
and not state.is_column_load
and not state.is_relationship_load
and not state.statement.get_execution_options().get("include_staged", False)
Copy link
Contributor Author

@DarkaMaul DarkaMaul Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FLAG: This is added to allow querying for the published=False query but is not used in this PR (except in the tests).

):
state.statement = state.statement.options(
with_loader_criteria(
Release, lambda cls: cls.published, include_aliases=True
)
)


class ProhibitedProjectName(db.Model):
__tablename__ = "prohibited_project_names"
__table_args__ = (
Expand Down