Skip to content

feat(requirements): add Requirement.__replace__ - #1377

Open
henryiii wants to merge 2 commits into
pypa:mainfrom
henryiii:deprecate-requirement-mutation
Open

henryiii wants to merge 2 commits into
pypa:mainfrom
henryiii:deprecate-requirement-mutation

Conversation

@henryiii

@henryiii henryiii commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Let's start by providing a nice way to do this, then we can (maybe) deprecate in the future. Quite a few projects are mutating this.


Previous (available on a branch requirement-replace-and-deprecate in my fork for the future):

This was proposed in #1252. This assumes we want a deprecation period.

This enables caching __hash__, and __hash__ was always broken if you mutated this class
while it was in a set or similar.

Added support for __replace__ as an alternative if anyone was trying to mutate these.

Mutating the extras set inside doesn't warn. We should swap it for a frozenset when making this immutable.

Assisted-by: ClaudeCode:claude-fable-5

@henryiii

Copy link
Copy Markdown
Contributor Author

Re: deprecation period: Yes, projects are mutating. :)

@henryiii
henryiii force-pushed the deprecate-requirement-mutation branch from de72b7d to ebb72e3 Compare August 10, 2026 05:10
@henryiii henryiii changed the title feat(requirements): deprecate mutating Requirement and add __replace__ feat(requirements): add Requirement.__replace__ Aug 10, 2026
Enables copy.replace() on Python 3.13+ and gives a supported way to
build a modified copy of a requirement. The result is validated and
normalized by re-parsing its string form, matching Version.__replace__.

Assisted-by: ClaudeCode:claude-fable-5
@henryiii
henryiii force-pushed the deprecate-requirement-mutation branch from ebb72e3 to f5bdf9e Compare August 10, 2026 05:22
@henryiii
henryiii marked this pull request as ready for review August 10, 2026 05:40

@shinzoxD shinzoxD left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

copy.replace requires the replacement to have the same dynamic type as the input, but this implementation always constructs Requirement directly. On Python 3.13 at head f5bdf9e:

class CustomRequirement(Requirement):
    pass

result = copy.replace(CustomRequirement(demo>=1), name=renamed)
print(type(result).__name__, isinstance(result, CustomRequirement))
# Requirement False

This contradicts both the copy.replace / object.__replace__ contract and the neighboring Version.__replace__, which preserves self.__class__. Please preserve the subclass type and add coverage for direct __replace__ plus copy.replace on a subclass.

Also, this is a new public API, while the project contribution guide says new features should be documented in the changelog; please add a 26.4 entry.

The existing focused suite is otherwise clean locally: 5,334 passed and 1 Python-version skip; git diff --check is clean and all 62 upstream checks are green.

Annotate __replace__ as returning Self and build the result with
self.__class__, so a subclass gets back its own type.

Assisted-by: ClaudeCode:claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants