Conversation
Adds installer.minimum-release-age and installer.minimum-release-age-exclude config options. Versions released more recently than the configured threshold are filtered out during dependency resolution, guarding against supply chain attacks on freshly published packages (inline with pnpm's minimumReleaseAge). Upload time is sourced from the PEP 691 Simple API page already fetched for version listing, so no extra HTTP requests are made. HTML-only index pages fail open (no upload_time available). Glob patterns are supported in the exclude list.
Reviewer's GuideImplements configurable minimum release age filtering for package versions during dependency resolution, with optional pattern-based exclusions, using upload timestamps from the PEP 691 Simple API and updating configuration handling and tests accordingly. Sequence diagram for dependency resolution with minimum release age filteringsequenceDiagram
actor Developer
participant PoetryCLI
participant Resolver
participant HttpRepository
participant LinkSource
Developer->>PoetryCLI: run install_or_update
PoetryCLI->>Resolver: resolve_dependencies()
Resolver->>HttpRepository: _find_packages(name, constraint)
HttpRepository->>LinkSource: versions(name)
LinkSource-->>HttpRepository: version_list
loop for each version in version_list
HttpRepository->>HttpRepository: _version_meets_minimum_age(page, name, version)
alt minimum_release_age_disabled
HttpRepository-->>HttpRepository: return true
else package_excluded_by_pattern
HttpRepository-->>HttpRepository: return true
else upload_time_available
HttpRepository->>HttpRepository: _release_age_days(page, name, version)
HttpRepository-->>HttpRepository: age_in_days
alt age_in_days < minimum_release_age
HttpRepository->>HttpRepository: _log(skip_message, level=debug)
HttpRepository-->>HttpRepository: return false
else age_in_days >= minimum_release_age
HttpRepository-->>HttpRepository: return true
end
else no_upload_time_data
HttpRepository-->>HttpRepository: return true
end
HttpRepository-->>Resolver: include_or_exclude_version
end
Resolver-->>PoetryCLI: resolved_package_set
PoetryCLI-->>Developer: install selected_versions
Class diagram for new minimum release age configuration and repository filteringclassDiagram
class Config {
installer_minimum_release_age: int_or_none
installer_minimum_release_age_exclude: list_string
_get_normalizer(name: str) Callable
}
class HttpRepository {
- _lazy_wheel: bool
- _max_retries: int
- _minimum_release_age: int_or_none
- _minimum_release_age_exclude: list_string
_release_age_days(page: LinkSource, name: NormalizedName, version: Any) int_or_none
_is_release_age_excluded(name: NormalizedName) bool
_version_meets_minimum_age(page: LinkSource, name: NormalizedName, version: Any) bool
}
class LegacyRepository {
_find_packages(name: NormalizedName, constraint: Constraint) list_Package
}
class PyPiRepository {
_find_packages(name: NormalizedName, constraint: Constraint) list_Package
}
Config ..> HttpRepository : provides_config
HttpRepository <|-- LegacyRepository
HttpRepository <|-- PyPiRepository
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
_release_age_days,datetime.fromisoformat(link.upload_time_isoformat)will produce a naive datetime for most ISO strings and is then subtracted fromdatetime.now(timezone.utc), which is timezone-aware; make the parsed datetime explicitly timezone-aware (or usedatetime.now()consistently) to avoidTypeErrorand subtle timezone issues. - Consider wrapping the
datetime.fromisoformatcall in_release_age_dayswith a small helper or try/except that treats malformedupload_time_isoformatvalues as missing (returningNone), so a single bad timestamp on a page does not break dependency resolution.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `_release_age_days`, `datetime.fromisoformat(link.upload_time_isoformat)` will produce a naive datetime for most ISO strings and is then subtracted from `datetime.now(timezone.utc)`, which is timezone-aware; make the parsed datetime explicitly timezone-aware (or use `datetime.now()` consistently) to avoid `TypeError` and subtle timezone issues.
- Consider wrapping the `datetime.fromisoformat` call in `_release_age_days` with a small helper or try/except that treats malformed `upload_time_isoformat` values as missing (returning `None`), so a single bad timestamp on a page does not break dependency resolution.
## Individual Comments
### Comment 1
<location path="src/poetry/repositories/http_repository.py" line_range="497-504" />
<code_context>
+ Return the age in days of the oldest distribution file for the given version,
+ or None if no upload_time data is available (e.g. HTML-only index pages).
+ """
+ upload_times = [
+ datetime.fromisoformat(link.upload_time_isoformat)
+ for link in page.links_for_version(name, version)
+ if link.upload_time_isoformat is not None
+ ]
+ if not upload_times:
+ return None
+ return (datetime.now(timezone.utc) - min(upload_times)).days
+
+ def _is_release_age_excluded(self, name: NormalizedName) -> bool:
</code_context>
<issue_to_address>
**issue (bug_risk):** Datetime parsing/timezone handling is likely to raise errors with PyPI-style timestamps and mixed aware/naive datetimes.
PyPI’s `upload_time_isoformat` values are ISO 8601 and may end with `Z` (UTC). On Python <3.11, `datetime.fromisoformat()` rejects `Z`, raising `ValueError`. Also, if `fromisoformat` returns naive datetimes, subtracting them from `datetime.now(timezone.utc)` (aware) will raise `TypeError`. Please normalize to timezone-aware UTC first (e.g., replace trailing `Z` with `+00:00` and use `.fromisoformat(...).astimezone(timezone.utc)`, or a helper that guarantees an aware UTC datetime) before doing the subtraction.
</issue_to_address>
### Comment 2
<location path="src/poetry/config/config.py" line_range="407-412" />
<code_context>
}:
return int_normalizer
+ if name == "installer.minimum-release-age-exclude":
+ return lambda val: (
+ [v.strip() for v in val.split(",")]
+ if isinstance(val, str)
+ else list(val)
+ )
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Normalizer for minimum-release-age-exclude could enforce string elements to avoid surprising values.
Right now strings are split/stripped, but non-strings are passed through as `list(val)`, so a YAML/JSON list like `[1, 2]` would reach `fnmatch`, which expects string patterns. Consider normalizing both branches to a `list[str]` (e.g. `str(x).strip()` for each entry) so the downstream matching logic always receives strings.
```suggestion
if name == "installer.minimum-release-age-exclude":
return lambda val: [
str(v).strip()
for v in (val.split(",") if isinstance(val, str) else val)
]
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if name in { | ||
| "installer.max-workers", | ||
| "requests.max-retries", | ||
| "installer.minimum-release-age", |
There was a problem hiding this comment.
The additions in this file have the same code as console.commands.config which seems like a recipe for unexpected behaviour. For example, this version does not apply the console version's check that the minimum release age is greater than zero.
| if name == "installer.minimum-release-age-exclude": | ||
| return lambda val: [ | ||
| str(v).strip() | ||
| for v in (val.split(",") if isinstance(val, str) else val) |
There was a problem hiding this comment.
Since the definition of this option is an optional list of glob separated strings, it's not clear that type check here is helpful and it could lead to some oddities (e.g. if I give it a list with a NoneType or bool the result will be […, "None", …]. It feels like this would be cleaner broken out into a function which could be used in both locations which returns an empty list when given a false-y value, does the string check to convert a single string to a list (esp. from POETRY_INSTALLER_MINIMUM_RELEASE_AGE_EXCLUDE), and then does filter(None, map(str.strip)) before returning it.
Adds installer.minimum-release-age and installer.minimum-release-age-exclude config options. Versions released more recently than the configured threshold are filtered out during dependency resolution, guarding against supply chain attacks on freshly published packages (inline with pnpm's minimumReleaseAge).
Upload time is sourced from the PEP 691 Simple API page already fetched for version listing, so no extra HTTP requests are made. HTML-only index pages fail open (no upload_time available). Glob patterns are supported in the exclude list.
Pull Request Check List
Resolves: #10646
Also discussed here: https://github.com/orgs/python-poetry/discussions/10555
Summary by Sourcery
Introduce configurable minimum release age filtering for dependency resolution to avoid using very recently published package versions.
New Features:
Tests: