Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5655dc0d5e | ||
|
|
e0e48d12c8 | ||
|
|
950b55df5f | ||
|
|
352ac56295 | ||
|
|
c526ec9353 | ||
|
|
d4ff852491 |
@@ -0,0 +1,160 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report unexpected NRS behavior or a crash
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to report a bug. Please fill out as much detail as you can — NRS's guidance behavior is sensitive to model type and node settings, so precise details help a lot.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform / UI
|
||||||
|
description: Which UI/front-end are you running NRS through?
|
||||||
|
options:
|
||||||
|
- ComfyUI
|
||||||
|
- AUTOMATIC1111
|
||||||
|
- Forge
|
||||||
|
- reForge
|
||||||
|
- Forge Neo
|
||||||
|
- Stability Matrix
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform-other
|
||||||
|
attributes:
|
||||||
|
label: Platform / UI (if "Other")
|
||||||
|
description: If you selected "Other" above, name the platform/UI here.
|
||||||
|
placeholder: e.g. SD.Next, a custom fork, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform-version
|
||||||
|
attributes:
|
||||||
|
label: Platform version
|
||||||
|
description: >
|
||||||
|
The version of whichever platform you selected above. Found in that
|
||||||
|
platform's UI (e.g. Help/About, Settings) or its startup console
|
||||||
|
output.
|
||||||
|
placeholder: e.g. 0.3.30 (ComfyUI) or v1.10.1 (AUTOMATIC1111/Forge)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: model-family
|
||||||
|
attributes:
|
||||||
|
label: Model / sampler family
|
||||||
|
description: Which model or sampler family were you using when the issue occurred?
|
||||||
|
options:
|
||||||
|
- MiniMax H3
|
||||||
|
- Flux
|
||||||
|
- Chroma
|
||||||
|
- WAN
|
||||||
|
- SDXL
|
||||||
|
- SD 1.5
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: model-family-other
|
||||||
|
attributes:
|
||||||
|
label: Model / sampler family (if "Other")
|
||||||
|
description: If you selected "Other" above, name the model/sampler family here.
|
||||||
|
placeholder: e.g. custom checkpoint, HunyuanVideo, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: nrs-version
|
||||||
|
attributes:
|
||||||
|
label: NRS version
|
||||||
|
description: >
|
||||||
|
Look for the NRS log line in your console/terminal:
|
||||||
|
`NRS v<version>: prediction type detected -> <TYPE>` (printed when
|
||||||
|
NRS runs, or check the extension's about/version info, depending on
|
||||||
|
your platform). Copy the version number from that line.
|
||||||
|
placeholder: e.g. 0.7.4
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: pred-type
|
||||||
|
attributes:
|
||||||
|
label: Detected prediction type
|
||||||
|
description: >
|
||||||
|
From the same NRS log line as above
|
||||||
|
(`NRS v<version>: prediction type detected -> <TYPE>`), copy the
|
||||||
|
detected type (e.g. EPS, V, FLOW, UNKNOWN).
|
||||||
|
placeholder: e.g. FLOW
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: skew
|
||||||
|
attributes:
|
||||||
|
label: Skew value
|
||||||
|
placeholder: e.g. 2.00
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: stretch
|
||||||
|
attributes:
|
||||||
|
label: Stretch value
|
||||||
|
placeholder: e.g. 5.00
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: squash
|
||||||
|
attributes:
|
||||||
|
label: Squash value
|
||||||
|
placeholder: e.g. 0.75
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: What actually happened? Include screenshots if relevant.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: repro
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Minimal steps (or an attached workflow JSON) to reproduce the issue.
|
||||||
|
placeholder: |
|
||||||
|
1. Load workflow...
|
||||||
|
2. Set Skew/Stretch/Squash to...
|
||||||
|
3. Queue prompt...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: console-log
|
||||||
|
attributes:
|
||||||
|
label: Console log output
|
||||||
|
description: >
|
||||||
|
Paste the relevant console output, including the
|
||||||
|
`NRS v<version>: prediction type detected -> <TYPE>` line and any
|
||||||
|
errors/warnings/tracebacks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: NRS Discussions
|
||||||
|
url: https://github.com/Reithan/negative_rejection_steering/discussions
|
||||||
|
about: Ask questions or discuss ideas that aren't a bug report.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test suite with branch coverage gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
|
- name: Install real CPU torch
|
||||||
|
run: pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
|
- name: Install test tooling
|
||||||
|
run: pip install pytest pytest-cov diff-cover
|
||||||
|
|
||||||
|
- name: Run tests with branch coverage
|
||||||
|
run: pytest --cov=NRS --cov-branch --cov-report=xml --cov-report=term-missing
|
||||||
|
|
||||||
|
- name: Determine base branch for diff-cover
|
||||||
|
id: base
|
||||||
|
run: |
|
||||||
|
base_ref="origin/main"
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ] && git rev-parse --verify "origin/${{ github.base_ref }}" >/dev/null 2>&1; then
|
||||||
|
base_ref="origin/${{ github.base_ref }}"
|
||||||
|
fi
|
||||||
|
echo "ref=${base_ref}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Enforce 90% branch coverage on changed code
|
||||||
|
run: diff-cover coverage.xml --compare-branch=${{ steps.base.outputs.ref }} --branch-coverage --fail-under=90
|
||||||
@@ -36,8 +36,8 @@ repos:
|
|||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: run-tests
|
- id: run-tests
|
||||||
name: Run pytest tests
|
name: Run pytest with branch-coverage gate
|
||||||
entry: bash -c 'if command -v uv > /dev/null 2>&1; then uv run pytest tests/ || exit 1; else echo "WARNING - uv not found, skipping tests"; fi'
|
entry: bash -c 'if ! command -v uv > /dev/null 2>&1; then echo "WARNING - uv not found, skipping tests and coverage gate"; exit 0; fi; if ! git rev-parse --verify --quiet origin/main > /dev/null 2>&1; then echo "ERROR - origin/main not resolvable locally; fetch origin main and retry"; exit 1; fi; uvx --with torch --with pytest-cov --with diff-cover pytest --cov=NRS --cov-branch --cov-report=xml --cov-report=term-missing tests/; status=$?; if [ $status -ne 0 ]; then rm -f coverage.xml .coverage; exit $status; fi; uvx --with diff-cover diff-cover coverage.xml --compare-branch=origin/main --branch-coverage --fail-under=90; cov_status=$?; rm -f coverage.xml .coverage; exit $cov_status'
|
||||||
language: system
|
language: system
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
always_run: true
|
always_run: true
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-08-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Flow-family (flow-matching) models now use a dedicated FLOW prediction/operation space (#37) so NRS applies the correct guidance geometry to them. Previously these models were misclassified, causing NRS to operate on an incorrect prediction-type assumption and underperform. This is the headline fix in 1.0.0.
|
||||||
|
- Pack-aware per-stream NRS routing with a degeneracy tripwire (#36): NRS now unpacks multi-stream packed latents (e.g. MiniMax H3 audio+video) and applies the geometry per stream on the real channel axis, instead of collapsing to a silent no-op on the flat packed latent.
|
||||||
|
- Prediction-type detection for WAN / RES4LYF samplers (#30).
|
||||||
|
- Removed a mangled guard and dead operation-space code paths; added prediction-type detection tests (#34).
|
||||||
|
- Resolved Node.js 20 deprecation warnings in GitHub Actions.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `__version__` string plus a patch-time log line announcing the version and detected prediction type; platform-agnostic GitHub issue template (#39).
|
||||||
|
- CI: full test suite with a >90% branch-coverage gate on diffs (#38); version-increment check in the publish workflow (#33); git hooks and development infrastructure (#32).
|
||||||
|
- Declared `requires-python` (>=3.10) so dependency locking is deterministic across environments.
|
||||||
|
|
||||||
|
### Changed / Upgrade notes
|
||||||
|
|
||||||
|
- Because flow-family models now use the correct FLOW space, NRS output for these models changes (for the better). Existing users of flow-matching models should retune Skew/Stretch/Squash. The `pre-flow` git tag preserves the prior behavior if a rollback is needed.
|
||||||
@@ -54,6 +54,12 @@ Thank you for your interest in contributing! This document provides guidelines f
|
|||||||
pre-commit install --hook-type pre-push
|
pre-commit install --hook-type pre-push
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The pre-push hook runs the full test suite with branch coverage and blocks
|
||||||
|
the push if changed code drops below 90% branch coverage (via pytest-cov +
|
||||||
|
diff-cover, mirroring CI). It needs `uv` installed — if `uv` isn't found,
|
||||||
|
the check is skipped with a warning — and `origin/main` fetched locally so
|
||||||
|
there's something to diff against.
|
||||||
|
|
||||||
6. **Verify setup**:
|
6. **Verify setup**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""NRS package init.
|
||||||
|
|
||||||
|
Re-exports `__version__` from nodes_NRS.py so `NRS.__version__` is
|
||||||
|
importable. This value must match the `version` field in pyproject.toml —
|
||||||
|
bump both together at release time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .nodes_NRS import __version__
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
+126
-46
@@ -1,25 +1,66 @@
|
|||||||
|
# ruff: noqa: N999 -- filename predates NRS/__init__.py; mixed-case "nodes_NRS"
|
||||||
|
# only became checkable once NRS became a regular (non-namespace) package here.
|
||||||
|
# Renaming it is out of scope (would break existing imports); pyproject.toml's
|
||||||
|
# per-file-ignores already carve out N802/N804 for this same file.
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
try:
|
||||||
|
import comfy.utils as _comfy_utils
|
||||||
|
except Exception:
|
||||||
|
_comfy_utils = None
|
||||||
|
|
||||||
|
# Must be bumped together with the `version` field in pyproject.toml at release time.
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_latents(combined, latent_shapes):
|
||||||
|
"""Split a flat packed latent [B, 1, N] back into its per-stream tensors.
|
||||||
|
|
||||||
|
Mirrors comfy.utils.unpack_latents: for each shape in latent_shapes, take
|
||||||
|
math.prod(shape[1:]) elements off the last dim and reshape that [B, 1, n]
|
||||||
|
slice back to `shape`.
|
||||||
|
"""
|
||||||
|
streams = []
|
||||||
|
offset = 0
|
||||||
|
for shape in latent_shapes:
|
||||||
|
n = math.prod(shape[1:])
|
||||||
|
chunk = combined[:, :, offset : offset + n]
|
||||||
|
streams.append(chunk.reshape(shape))
|
||||||
|
offset += n
|
||||||
|
return streams
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_latents(streams):
|
||||||
|
"""Pack a list of per-stream tensors [B, C, ...] into a flat [B, 1, N] tensor.
|
||||||
|
|
||||||
|
Mirrors comfy.utils.pack_latents: each stream is reshaped to (B, 1, -1)
|
||||||
|
and concatenated on the last dim.
|
||||||
|
"""
|
||||||
|
flat = [s.reshape(s.shape[0], 1, -1) for s in streams]
|
||||||
|
return torch.cat(flat, dim=-1)
|
||||||
|
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
class PredictionType(Enum):
|
class PredictionType(Enum):
|
||||||
EPS = auto() # ε-prediction
|
EPS = auto() # ε-prediction
|
||||||
V = auto() # v-prediction
|
V = auto() # v-prediction
|
||||||
X0 = auto() # x₀-prediction
|
X0 = auto() # x₀-prediction
|
||||||
|
FLOW = auto() # flow-matching / velocity — operated natively, no VP conversion
|
||||||
UNKNOWN = auto() # couldn’t detect / new scheduler
|
UNKNOWN = auto() # couldn’t detect / new scheduler
|
||||||
|
|
||||||
|
|
||||||
_RAW_TO_ENUM = {
|
_RAW_TO_ENUM = {
|
||||||
"eps": PredictionType.EPS,
|
"eps": PredictionType.EPS,
|
||||||
"epsilon": PredictionType.EPS,
|
"epsilon": PredictionType.EPS,
|
||||||
"flux": PredictionType.EPS,
|
"flux": PredictionType.FLOW,
|
||||||
"chroma": PredictionType.EPS,
|
"chroma": PredictionType.FLOW,
|
||||||
"flow": PredictionType.EPS, # FLOW models (WAN, etc.) are EPS-compatible
|
"flow": PredictionType.FLOW, # FLOW models (WAN, etc.) operated natively
|
||||||
"wan": PredictionType.EPS, # WAN21 is FLOW-based
|
"wan": PredictionType.FLOW, # WAN21 is FLOW-based
|
||||||
"const": PredictionType.EPS, # CONST prediction class used in FLOW models
|
"const": PredictionType.FLOW, # CONST prediction class used in FLOW models
|
||||||
"v": PredictionType.V,
|
"v": PredictionType.V,
|
||||||
"v_prediction": PredictionType.V,
|
"v_prediction": PredictionType.V,
|
||||||
"x0": PredictionType.X0,
|
"x0": PredictionType.X0,
|
||||||
@@ -125,8 +166,8 @@ class NRS:
|
|||||||
|
|
||||||
# CONST class is used by FLOW models (WAN21, Flux, etc.)
|
# CONST class is used by FLOW models (WAN21, Flux, etc.)
|
||||||
if "const" in sampling_class_name:
|
if "const" in sampling_class_name:
|
||||||
logging.debug("NRS._get_pred_type: Detected FLOW model via CONST sampling class -> EPS")
|
logging.debug("NRS._get_pred_type: Detected FLOW model via CONST sampling class -> FLOW")
|
||||||
return PredictionType.EPS
|
return PredictionType.FLOW
|
||||||
elif "v_prediction" in sampling_class_name:
|
elif "v_prediction" in sampling_class_name:
|
||||||
logging.debug("NRS._get_pred_type: Detected V-prediction model via sampling class -> V")
|
logging.debug("NRS._get_pred_type: Detected V-prediction model via sampling class -> V")
|
||||||
return PredictionType.V
|
return PredictionType.V
|
||||||
@@ -140,8 +181,8 @@ class NRS:
|
|||||||
logging.debug(f"NRS._get_pred_type: Found model.model.model_type: {model_type_str}")
|
logging.debug(f"NRS._get_pred_type: Found model.model.model_type: {model_type_str}")
|
||||||
|
|
||||||
if "flow" in model_type_str or "flux" in model_type_str:
|
if "flow" in model_type_str or "flux" in model_type_str:
|
||||||
logging.debug("NRS._get_pred_type: Detected FLOW/Flux model via model_type -> EPS")
|
logging.debug("NRS._get_pred_type: Detected FLOW/Flux model via model_type -> FLOW")
|
||||||
return PredictionType.EPS
|
return PredictionType.FLOW
|
||||||
elif "v_prediction" in model_type_str:
|
elif "v_prediction" in model_type_str:
|
||||||
logging.debug("NRS._get_pred_type: Detected V-prediction model via model_type -> V")
|
logging.debug("NRS._get_pred_type: Detected V-prediction model via model_type -> V")
|
||||||
return PredictionType.V
|
return PredictionType.V
|
||||||
@@ -163,9 +204,9 @@ class NRS:
|
|||||||
x_div = None
|
x_div = None
|
||||||
v_cond = cond
|
v_cond = cond
|
||||||
v_uncond = uncond
|
v_uncond = uncond
|
||||||
if pred_type == PredictionType.V:
|
if pred_type in (PredictionType.V, PredictionType.FLOW):
|
||||||
logging.debug("NRS._convert_to_v_space: already in v, no pre-scale needed")
|
logging.debug("NRS._convert_to_v_space: already in v/flow, no pre-scale needed")
|
||||||
pass # already in v space
|
pass # already in v space / flow-matching operated natively
|
||||||
elif pred_type == PredictionType.EPS:
|
elif pred_type == PredictionType.EPS:
|
||||||
# ε → v conversion
|
# ε → v conversion
|
||||||
logging.debug("NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps")
|
logging.debug("NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps")
|
||||||
@@ -189,9 +230,9 @@ class NRS:
|
|||||||
|
|
||||||
def _finalize_from_v_space(self, x_orig, x_div, x_final, sig_root, sigma, pred_type):
|
def _finalize_from_v_space(self, x_orig, x_div, x_final, sig_root, sigma, pred_type):
|
||||||
nrs_result = x_final
|
nrs_result = x_final
|
||||||
if pred_type == PredictionType.V:
|
if pred_type in (PredictionType.V, PredictionType.FLOW):
|
||||||
# already in v space
|
# already in v space / flow-matching operated natively
|
||||||
logging.debug("NRS._finalize_from_v_space: already in v, no post-scale needed")
|
logging.debug("NRS._finalize_from_v_space: already in v/flow, no post-scale needed")
|
||||||
pass
|
pass
|
||||||
elif pred_type == PredictionType.EPS:
|
elif pred_type == PredictionType.EPS:
|
||||||
# v → ε conversion
|
# v → ε conversion
|
||||||
@@ -206,51 +247,90 @@ class NRS:
|
|||||||
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
|
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
|
||||||
return nrs_result
|
return nrs_result
|
||||||
|
|
||||||
|
def _apply_guidance(self, x_orig, cond, uncond, sigma, skew, stretch, squash, pred_type):
|
||||||
|
"""Run the NRS geometry pipeline on a single (already-unpacked, channels-first) stream."""
|
||||||
|
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
|
||||||
|
sig_root = (sigma**2 + 1).sqrt()
|
||||||
|
|
||||||
|
# V and FLOW models are operated natively (identity); EPS models are converted to v-space.
|
||||||
|
x_div, nrs_cond, nrs_uncond = self._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond, pred_type)
|
||||||
|
|
||||||
|
def _dot(a, b):
|
||||||
|
return (a * b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
|
||||||
|
|
||||||
|
def _nrm2(v):
|
||||||
|
return _dot(v, v)
|
||||||
|
|
||||||
|
eps = torch.finfo(nrs_cond.dtype).eps
|
||||||
|
c_dot_c = _nrm2(nrs_cond) + eps # [B,1,W,H]
|
||||||
|
u_dot_c = _dot(nrs_uncond, nrs_cond) # [B,1,W,H]
|
||||||
|
u_on_c = (u_dot_c / c_dot_c) * nrs_cond # [B,1,W,H] * [B,C,H,W]
|
||||||
|
|
||||||
|
# Amplify Cond based on length compared to projection of uncond
|
||||||
|
proj_diff = nrs_cond - u_on_c
|
||||||
|
stretched = nrs_cond + (stretch * proj_diff)
|
||||||
|
|
||||||
|
# Skew/Steer Conf based on rejection of uncond on cond
|
||||||
|
u_rej_c = nrs_uncond - u_on_c
|
||||||
|
skewed = stretched - (skew * u_rej_c)
|
||||||
|
|
||||||
|
# Squash final length back down to original length of cond
|
||||||
|
cond_len = nrs_cond.norm(dim=1, keepdim=True)
|
||||||
|
nrs_len = skewed.norm(dim=1, keepdim=True) + eps
|
||||||
|
|
||||||
|
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
|
||||||
|
x_final = skewed * squash_scale
|
||||||
|
|
||||||
|
return self._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma, pred_type)
|
||||||
|
|
||||||
def patch(self, model, skew, stretch, squash):
|
def patch(self, model, skew, stretch, squash):
|
||||||
pred_type = self._get_pred_type(model)
|
pred_type = self._get_pred_type(model)
|
||||||
|
logging.info(f"NRS v{__version__}: prediction type detected -> {pred_type.name}")
|
||||||
|
warned = {"done": False}
|
||||||
|
|
||||||
def nrs(args):
|
def nrs(args):
|
||||||
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
|
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
|
||||||
cond = args["cond"]
|
cond = args["cond"]
|
||||||
uncond = args["uncond"]
|
uncond = args["uncond"]
|
||||||
x_orig = args["input"]
|
x_orig = args["input"]
|
||||||
|
|
||||||
sigma = args["sigma"]
|
sigma = args["sigma"]
|
||||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
|
|
||||||
sig_root = (sigma**2 + 1).sqrt()
|
|
||||||
|
|
||||||
# Operation space is hardcoded to V for now; FLOW is added in a later PR.
|
shapes = getattr(args["model"], "latent_shapes", None)
|
||||||
x_div, nrs_cond, nrs_uncond = self._convert_to_v_space(
|
if shapes and len(shapes) > 1:
|
||||||
x_orig, sig_root, sigma, cond, uncond, pred_type
|
if _comfy_utils is not None and hasattr(_comfy_utils, "unpack_latents"):
|
||||||
)
|
cond_streams = _comfy_utils.unpack_latents(cond, shapes)
|
||||||
|
uncond_streams = _comfy_utils.unpack_latents(uncond, shapes)
|
||||||
|
x_streams = _comfy_utils.unpack_latents(x_orig, shapes)
|
||||||
|
else:
|
||||||
|
cond_streams = _unpack_latents(cond, shapes)
|
||||||
|
uncond_streams = _unpack_latents(uncond, shapes)
|
||||||
|
x_streams = _unpack_latents(x_orig, shapes)
|
||||||
|
else:
|
||||||
|
cond_streams, uncond_streams, x_streams = [cond], [uncond], [x_orig]
|
||||||
|
|
||||||
def _dot(a, b):
|
if not warned["done"]:
|
||||||
return (a * b).sum(dim=1, keepdim=True) # [B,C,W,H] => [B,1,W,H]
|
for stream in x_streams:
|
||||||
|
if stream.shape[1] == 1:
|
||||||
|
logging.warning(
|
||||||
|
f"NRS.nrs: routed stream has a singleton reduction axis {tuple(stream.shape)}; "
|
||||||
|
"NRS geometry (dot/proj/skew) will degenerate to a no-op on this stream."
|
||||||
|
)
|
||||||
|
warned["done"] = True
|
||||||
|
break
|
||||||
|
|
||||||
def _nrm2(v):
|
results = [
|
||||||
return _dot(v, v)
|
self._apply_guidance(
|
||||||
|
x_streams[i], cond_streams[i], uncond_streams[i], sigma, skew, stretch, squash, pred_type
|
||||||
|
)
|
||||||
|
for i in range(len(cond_streams))
|
||||||
|
]
|
||||||
|
|
||||||
eps = torch.finfo(nrs_cond.dtype).eps
|
if len(results) == 1:
|
||||||
c_dot_c = _nrm2(nrs_cond) + eps # [B,1,W,H]
|
return results[0]
|
||||||
u_dot_c = _dot(nrs_uncond, nrs_cond) # [B,1,W,H]
|
|
||||||
u_on_c = (u_dot_c / c_dot_c) * nrs_cond # [B,1,W,H] * [B,C,H,W]
|
|
||||||
|
|
||||||
# Amplify Cond based on length compared to projection of uncond
|
if _comfy_utils is not None and hasattr(_comfy_utils, "pack_latents"):
|
||||||
proj_diff = nrs_cond - u_on_c
|
return _comfy_utils.pack_latents(results)[0]
|
||||||
stretched = nrs_cond + (stretch * proj_diff)
|
return _pack_latents(results)
|
||||||
|
|
||||||
# Skew/Steer Conf based on rejection of uncond on cond
|
|
||||||
u_rej_c = nrs_uncond - u_on_c
|
|
||||||
skewed = stretched - (skew * u_rej_c)
|
|
||||||
|
|
||||||
# Squash final length back down to original length of cond
|
|
||||||
cond_len = nrs_cond.norm(dim=1, keepdim=True)
|
|
||||||
nrs_len = skewed.norm(dim=1, keepdim=True) + eps
|
|
||||||
|
|
||||||
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
|
|
||||||
x_final = skewed * squash_scale
|
|
||||||
|
|
||||||
return self._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma, pred_type)
|
|
||||||
|
|
||||||
m = model.clone()
|
m = model.clone()
|
||||||
m.set_model_sampler_cfg_function(nrs, True)
|
m.set_model_sampler_cfg_function(nrs, True)
|
||||||
|
|||||||
@@ -119,6 +119,15 @@ Once installed and enabled, the NRS settings panel will appear in your generatio
|
|||||||
### StabilityMatrix Integration
|
### StabilityMatrix Integration
|
||||||
NRS is available as a **natively supported module** in [StabilityMatrix](https://lykos.ai/), providing an easy installation and management option for users of that platform.
|
NRS is available as a **natively supported module** in [StabilityMatrix](https://lykos.ai/), providing an easy installation and management option for users of that platform.
|
||||||
|
|
||||||
|
### NRS for Video
|
||||||
|
When using NRS with **video** models (e.g. MiniMax H3), two things need to be turned off or output quality suffers:
|
||||||
|
- **Caching accelerators** (EasyCache, TeaCache, etc.) — their change-thresholded caching skips model evaluations that NRS relies on. With NRS active, this causes motion stutter and audio artifacts.
|
||||||
|
- **Multistep samplers** (`res_multistep`, `dpmpp_2m`, `dpmpp_3m_sde`, and other history/"m" samplers) — they extrapolate NRS's guidance across steps, compounding instability over the clip. Use a memoryless sampler instead; **`euler_ancestral` is recommended** (`euler` and `heun` also work well).
|
||||||
|
|
||||||
|
NRS adds a second inference pass per step, like CFG, so video generation time increases accordingly. Consider reserving NRS for final generations or prompts that need extra adherence.
|
||||||
|
|
||||||
|
These caveats are video-specific — 2D image generation is unaffected.
|
||||||
|
|
||||||
## Submitted User Examples
|
## Submitted User Examples
|
||||||
| User | CFG | NRS |
|
| User | CFG | NRS |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@ build-backend = "setuptools.build_meta"
|
|||||||
name = "negative_rejection_steering"
|
name = "negative_rejection_steering"
|
||||||
description = "NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidance with a more nuanced and composable steering of the generation process with better mathematical basis."
|
description = "NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidance with a more nuanced and composable steering of the generation process with better mathematical basis."
|
||||||
authors = [{name = "Bryan O'Malley", email = "bo122081@hotmail.com"}]
|
authors = [{name = "Bryan O'Malley", email = "bo122081@hotmail.com"}]
|
||||||
version = "0.7.4"
|
version = "1.0.0"
|
||||||
|
requires-python = ">=3.10"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
"""Tests for pack-aware per-stream routing in NRS.nodes_NRS.
|
||||||
|
|
||||||
|
These tests need real torch (tensor math), but tests/conftest.py installs a
|
||||||
|
MagicMock in sys.modules["torch"] for the whole session so other test modules
|
||||||
|
can import without the heavy dependency. We swap the real torch module in for
|
||||||
|
the duration of this module only, then restore the mock so the rest of the
|
||||||
|
suite is unaffected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_saved_torch = None
|
||||||
|
_saved_nodes_nrs = None
|
||||||
|
torch = None
|
||||||
|
nrs_module = None
|
||||||
|
|
||||||
|
|
||||||
|
def setup_module(module):
|
||||||
|
# NOTE: we deliberately avoid importlib.reload() here. reload() mutates
|
||||||
|
# the *existing* NRS.nodes_NRS module dict in place, and other test
|
||||||
|
# modules (e.g. test_pred_type.py) import PredictionType/NRS at
|
||||||
|
# collection time and keep those references for the whole session. Their
|
||||||
|
# methods' __globals__ point at that same dict, so an in-place reload
|
||||||
|
# would silently swap PredictionType out from under them (new class
|
||||||
|
# object, same name -> broken identity-based Enum equality). Instead we
|
||||||
|
# unregister the module from sys.modules and import it fresh: this
|
||||||
|
# creates an independent module object, leaving the original (still
|
||||||
|
# cached in other modules' namespaces) untouched. We restore the exact
|
||||||
|
# original module object on teardown.
|
||||||
|
global _saved_torch, _saved_nodes_nrs, torch, nrs_module
|
||||||
|
_saved_torch = sys.modules.get("torch")
|
||||||
|
sys.modules.pop("torch", None)
|
||||||
|
try:
|
||||||
|
import torch as real_torch
|
||||||
|
except ImportError:
|
||||||
|
pytest.skip("real torch unavailable", allow_module_level=True)
|
||||||
|
torch = real_torch
|
||||||
|
|
||||||
|
_saved_nodes_nrs = sys.modules.get("NRS.nodes_NRS")
|
||||||
|
sys.modules.pop("NRS.nodes_NRS", None)
|
||||||
|
|
||||||
|
import NRS.nodes_NRS as m
|
||||||
|
|
||||||
|
nrs_module = m
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module(module):
|
||||||
|
if _saved_torch is not None:
|
||||||
|
sys.modules["torch"] = _saved_torch
|
||||||
|
else:
|
||||||
|
sys.modules.pop("torch", None)
|
||||||
|
|
||||||
|
if _saved_nodes_nrs is not None:
|
||||||
|
sys.modules["NRS.nodes_NRS"] = _saved_nodes_nrs
|
||||||
|
else:
|
||||||
|
sys.modules.pop("NRS.nodes_NRS", None)
|
||||||
|
|
||||||
|
|
||||||
|
class _StubModelSampling:
|
||||||
|
"""Minimal stand-in that makes _get_pred_type fall back to EPS quickly."""
|
||||||
|
|
||||||
|
|
||||||
|
class _StubInnerModel:
|
||||||
|
def __init__(self, latent_shapes=None):
|
||||||
|
self.model_sampling = _StubModelSampling()
|
||||||
|
if latent_shapes is not None:
|
||||||
|
self.latent_shapes = latent_shapes
|
||||||
|
|
||||||
|
|
||||||
|
class _StubModel:
|
||||||
|
"""Stub for the outer ComfyUI ModelPatcher passed to NRS.patch()."""
|
||||||
|
|
||||||
|
def __init__(self, latent_shapes=None):
|
||||||
|
self.model = _StubInnerModel(latent_shapes)
|
||||||
|
self._captured_fn = None
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_model_sampler_cfg_function(self, fn, flag):
|
||||||
|
self._captured_fn = fn
|
||||||
|
|
||||||
|
|
||||||
|
def _make_args(model, cond, uncond, x_orig, sigma):
|
||||||
|
return {
|
||||||
|
"model": model.model, # args["model"] is the inner model carrying latent_shapes
|
||||||
|
"cond": cond,
|
||||||
|
"uncond": uncond,
|
||||||
|
"input": x_orig,
|
||||||
|
"sigma": sigma,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 1: round-trip pack/unpack correctness
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_roundtrip_unpack_repack_two_streams():
|
||||||
|
video = torch.randn(1, 4, 3, 2)
|
||||||
|
audio = torch.randn(1, 6, 5)
|
||||||
|
shapes = [video.shape, audio.shape]
|
||||||
|
|
||||||
|
packed = nrs_module._pack_latents([video, audio])
|
||||||
|
assert packed.shape == (1, 1, video.numel() + audio.numel())
|
||||||
|
|
||||||
|
unpacked = nrs_module._unpack_latents(packed, shapes)
|
||||||
|
assert len(unpacked) == 2
|
||||||
|
assert torch.allclose(unpacked[0], video)
|
||||||
|
assert torch.allclose(unpacked[1], audio)
|
||||||
|
|
||||||
|
repacked = nrs_module._pack_latents(unpacked)
|
||||||
|
assert torch.allclose(repacked, packed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_roundtrip_single_stream():
|
||||||
|
x = torch.randn(1, 4, 8, 8)
|
||||||
|
shapes = [x.shape]
|
||||||
|
|
||||||
|
packed = nrs_module._pack_latents([x])
|
||||||
|
unpacked = nrs_module._unpack_latents(packed, shapes)
|
||||||
|
assert len(unpacked) == 1
|
||||||
|
assert torch.allclose(unpacked[0], x)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 2: fallback path (no latent_shapes) is a byte-for-byte regression no-op
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _run_nrs(model, cond, uncond, x_orig, sigma, skew=2.0, stretch=5.0, squash=0.75):
|
||||||
|
node = nrs_module.NRS()
|
||||||
|
(patched_model,) = node.patch(model, skew, stretch, squash)
|
||||||
|
fn = patched_model._captured_fn
|
||||||
|
args = _make_args(model, cond, uncond, x_orig, sigma)
|
||||||
|
return fn(args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fallback_no_latent_shapes_matches_single_stream_shape():
|
||||||
|
model = _StubModel(latent_shapes=None)
|
||||||
|
cond = torch.randn(2, 4, 8, 8)
|
||||||
|
uncond = torch.randn(2, 4, 8, 8)
|
||||||
|
x_orig = torch.randn(2, 4, 8, 8)
|
||||||
|
sigma = torch.rand(2) + 0.1
|
||||||
|
|
||||||
|
result = _run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
assert result.shape == x_orig.shape
|
||||||
|
|
||||||
|
# Regression check: manually compute the single-stream result the same
|
||||||
|
# way the pre-split code path did, and confirm equality.
|
||||||
|
node = nrs_module.NRS()
|
||||||
|
expected = node._apply_guidance(
|
||||||
|
x_orig, cond, uncond, sigma, 2.0, 5.0, 0.75, nrs_module.PredictionType.EPS
|
||||||
|
)
|
||||||
|
assert torch.allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_stream_latent_shapes_also_matches():
|
||||||
|
"""A model.latent_shapes list of length 1 must take the same code path."""
|
||||||
|
cond = torch.randn(1, 4, 5, 5)
|
||||||
|
uncond = torch.randn(1, 4, 5, 5)
|
||||||
|
x_orig = torch.randn(1, 4, 5, 5)
|
||||||
|
sigma = torch.rand(1) + 0.1
|
||||||
|
|
||||||
|
model = _StubModel(latent_shapes=[cond.shape])
|
||||||
|
result = _run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
|
||||||
|
node = nrs_module.NRS()
|
||||||
|
expected = node._apply_guidance(
|
||||||
|
x_orig, cond, uncond, sigma, 2.0, 5.0, 0.75, nrs_module.PredictionType.EPS
|
||||||
|
)
|
||||||
|
assert torch.allclose(result, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 3: degeneracy tripwire
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_tripwire_fires_on_flat_pack_without_latent_shapes(caplog):
|
||||||
|
model = _StubModel(latent_shapes=None)
|
||||||
|
cond = torch.randn(1, 1, 100)
|
||||||
|
uncond = torch.randn(1, 1, 100)
|
||||||
|
x_orig = torch.randn(1, 1, 100)
|
||||||
|
sigma = torch.rand(1) + 0.1
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
_run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
|
||||||
|
assert any("singleton reduction axis" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tripwire_does_not_fire_for_normal_single_stream(caplog):
|
||||||
|
model = _StubModel(latent_shapes=None)
|
||||||
|
cond = torch.randn(1, 4, 8, 8)
|
||||||
|
uncond = torch.randn(1, 4, 8, 8)
|
||||||
|
x_orig = torch.randn(1, 4, 8, 8)
|
||||||
|
sigma = torch.rand(1) + 0.1
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
_run_nrs(model, cond, uncond, x_orig, sigma)
|
||||||
|
|
||||||
|
assert not any("singleton reduction axis" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 4: per-stream reduced shapes after unpack (H3-like video + audio)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_per_stream_reduced_shapes_after_unpack():
|
||||||
|
video = torch.randn(1, 24, 4, 3, 2)
|
||||||
|
audio = torch.randn(1, 32, 2, 5)
|
||||||
|
shapes = [video.shape, audio.shape]
|
||||||
|
|
||||||
|
packed = nrs_module._pack_latents([video, audio])
|
||||||
|
unpacked = nrs_module._unpack_latents(packed, shapes)
|
||||||
|
|
||||||
|
video_u, audio_u = unpacked
|
||||||
|
assert video_u.shape == video.shape
|
||||||
|
assert audio_u.shape == audio.shape
|
||||||
|
|
||||||
|
# Channels sit at dim 1 for both streams.
|
||||||
|
assert video_u.shape[1] == 24
|
||||||
|
assert audio_u.shape[1] == 32
|
||||||
|
|
||||||
|
video_reduced = video_u.sum(dim=1, keepdim=True)
|
||||||
|
audio_reduced = audio_u.sum(dim=1, keepdim=True)
|
||||||
|
assert video_reduced.shape == (1, 1, 4, 3, 2)
|
||||||
|
assert audio_reduced.shape == (1, 1, 2, 5)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 5: split restores non-degenerate rejection (proves Skew is alive)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_restores_nondegenerate_rejection():
|
||||||
|
"""On a real multi-channel stream, uncond's rejection on cond must not
|
||||||
|
collapse to ~0 -- this is the geometry that was silently dead on the flat
|
||||||
|
[B,1,N] pack before the unpack/repack fix.
|
||||||
|
"""
|
||||||
|
torch.manual_seed(0)
|
||||||
|
cond = torch.randn(1, 8, 4, 4)
|
||||||
|
# Make uncond non-parallel to cond so the rejection component is nonzero.
|
||||||
|
uncond = torch.randn(1, 8, 4, 4)
|
||||||
|
|
||||||
|
def _dot(a, b):
|
||||||
|
return (a * b).sum(dim=1, keepdim=True)
|
||||||
|
|
||||||
|
eps = torch.finfo(cond.dtype).eps
|
||||||
|
c_dot_c = _dot(cond, cond) + eps
|
||||||
|
u_dot_c = _dot(uncond, cond)
|
||||||
|
u_on_c = (u_dot_c / c_dot_c) * cond
|
||||||
|
u_rej_c = uncond - u_on_c
|
||||||
|
|
||||||
|
assert u_rej_c.abs().max().item() > 1e-4
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_pack_rejection_is_degenerate_without_split():
|
||||||
|
"""Sanity check for the bug this PR fixes: reducing over the flat pack's
|
||||||
|
singleton dim=1 axis collapses the rejection to exactly zero (up to
|
||||||
|
floating point noise from the eps regularization term).
|
||||||
|
"""
|
||||||
|
# float64 keeps the residual from the eps regularizer near the true
|
||||||
|
# machine epsilon instead of float32 accumulation noise, so the
|
||||||
|
# collapse-to-zero identity is exact enough to assert tightly.
|
||||||
|
packed_cond = torch.randn(1, 1, 100, dtype=torch.float64)
|
||||||
|
packed_uncond = torch.randn(1, 1, 100, dtype=torch.float64)
|
||||||
|
|
||||||
|
def _dot(a, b):
|
||||||
|
return (a * b).sum(dim=1, keepdim=True)
|
||||||
|
|
||||||
|
eps = torch.finfo(packed_cond.dtype).eps
|
||||||
|
c_dot_c = _dot(packed_cond, packed_cond) + eps
|
||||||
|
u_dot_c = _dot(packed_uncond, packed_cond)
|
||||||
|
u_on_c = (u_dot_c / c_dot_c) * packed_cond
|
||||||
|
u_rej_c = packed_uncond - u_on_c
|
||||||
|
|
||||||
|
assert u_rej_c.abs().max().item() < 1e-8
|
||||||
+124
-19
@@ -1,14 +1,18 @@
|
|||||||
"""Regression tests for NRS._get_pred_type and _RAW_TO_ENUM mappings.
|
"""Regression tests for NRS._get_pred_type, _RAW_TO_ENUM mappings, and the
|
||||||
|
V/FLOW/EPS operation-space conversion helpers.
|
||||||
|
|
||||||
These tests pin CURRENT behavior (flow-family names resolve to EPS) as a
|
PR-3 reclassified the flow-matching family (flux, chroma, flow, wan, const)
|
||||||
safety net ahead of the FLOW reclassification planned for a later PR. If
|
from PredictionType.EPS onto a new PredictionType.FLOW, which is operated
|
||||||
this file needs updating because flow-family names now map to
|
natively (identity conversion, no VP ε<->v algebra). These tests pin that
|
||||||
PredictionType.FLOW, that is expected -- it means the reclassification
|
post-reclassification behavior at both detection sites (the _RAW_TO_ENUM
|
||||||
landed and this net did its job.
|
dict and the enhanced-detection fallback in _get_pred_type), and cover the
|
||||||
|
FLOW identity round-trip through _convert_to_v_space / _finalize_from_v_space.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import enum
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -23,6 +27,14 @@ def _make_model_sampling(class_name):
|
|||||||
return type(class_name, (object,), {})()
|
return type(class_name, (object,), {})()
|
||||||
|
|
||||||
|
|
||||||
|
class _ModelType(enum.Enum):
|
||||||
|
"""Mirrors ComfyUI's real model_type.ModelType Enum, as exposed by MiniMax H3's
|
||||||
|
BaseModel.model_type -- a genuine Enum member, not a raw string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLOW = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class _StubModel:
|
class _StubModel:
|
||||||
"""Minimal stand-in for a model object walked by _get_pred_type."""
|
"""Minimal stand-in for a model object walked by _get_pred_type."""
|
||||||
|
|
||||||
@@ -41,11 +53,11 @@ class _StubModel:
|
|||||||
[
|
[
|
||||||
("eps", PredictionType.EPS),
|
("eps", PredictionType.EPS),
|
||||||
("epsilon", PredictionType.EPS),
|
("epsilon", PredictionType.EPS),
|
||||||
("flux", PredictionType.EPS),
|
("flux", PredictionType.FLOW),
|
||||||
("chroma", PredictionType.EPS),
|
("chroma", PredictionType.FLOW),
|
||||||
("flow", PredictionType.EPS),
|
("flow", PredictionType.FLOW),
|
||||||
("wan", PredictionType.EPS),
|
("wan", PredictionType.FLOW),
|
||||||
("const", PredictionType.EPS),
|
("const", PredictionType.FLOW),
|
||||||
("v", PredictionType.V),
|
("v", PredictionType.V),
|
||||||
("v_prediction", PredictionType.V),
|
("v_prediction", PredictionType.V),
|
||||||
("x0", PredictionType.X0),
|
("x0", PredictionType.X0),
|
||||||
@@ -80,22 +92,39 @@ class TestGetPredTypeDirectAttribute:
|
|||||||
model = _StubModel(model_type="x0")
|
model = _StubModel(model_type="x0")
|
||||||
assert node._get_pred_type(model) == PredictionType.X0
|
assert node._get_pred_type(model) == PredictionType.X0
|
||||||
|
|
||||||
def test_model_type_flow_family_is_currently_eps(self):
|
def test_model_type_flow_is_flow(self):
|
||||||
"""Flow-family models currently resolve to EPS (pre-reclassification)."""
|
"""Flow-family models resolve to FLOW (native operation, no VP conversion)."""
|
||||||
model = _StubModel(model_type="flow")
|
model = _StubModel(model_type="flow")
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
def test_model_type_wan_is_currently_eps(self):
|
def test_model_type_wan_is_flow(self):
|
||||||
model = _StubModel(model_type="wan")
|
model = _StubModel(model_type="wan")
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
|
def test_h3_enum_model_type_resolves_to_flow(self):
|
||||||
|
"""MiniMax H3 exposes model.model.model_type as a real Enum member
|
||||||
|
(ModelType.FLOW), not a raw string. _canon's `isinstance(p, Enum)`
|
||||||
|
branch reduces it to `p.name` ("FLOW" -> "flow") before the
|
||||||
|
_RAW_TO_ENUM dict lookup, so this pins that Enum path -- as taken by
|
||||||
|
H3's real model_type attribute -- resolves at the direct-hit site.
|
||||||
|
"""
|
||||||
|
model = _StubModel(inner_model_type=_ModelType.FLOW)
|
||||||
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
|
|
||||||
class TestGetPredTypeEnhancedDetectionFallback:
|
class TestGetPredTypeEnhancedDetectionFallback:
|
||||||
"""The model_sampling class-name and model.model.model_type fallback paths."""
|
"""The model_sampling class-name and model.model.model_type fallback paths.
|
||||||
|
|
||||||
def test_model_sampling_const_class_is_eps(self):
|
Each stub below is deliberately built so the only detectable signal lives
|
||||||
|
in the fallback (section 3) logic -- not an exact _RAW_TO_ENUM key hit
|
||||||
|
during the BFS walk -- so these tests genuinely exercise the fallback
|
||||||
|
branches rather than just re-testing the dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_model_sampling_const_class_is_flow(self):
|
||||||
|
"""A CONST-like model_sampling class name is the only flow signal here."""
|
||||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingContinuousEDMConst"))
|
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingContinuousEDMConst"))
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
def test_model_sampling_v_prediction_class_is_v(self):
|
def test_model_sampling_v_prediction_class_is_v(self):
|
||||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingV_Prediction"))
|
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingV_Prediction"))
|
||||||
@@ -105,7 +134,83 @@ class TestGetPredTypeEnhancedDetectionFallback:
|
|||||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingEps"))
|
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingEps"))
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||||
|
|
||||||
|
def test_inner_model_type_flow_string_is_flow(self):
|
||||||
|
"""A model.model.model_type whose str() merely *contains* 'flow' (e.g. an
|
||||||
|
Enum repr like 'ModelType.FLOW') isn't an exact _RAW_TO_ENUM key, so the
|
||||||
|
BFS direct-hit path can't resolve it -- only the model.model.model_type
|
||||||
|
substring fallback can.
|
||||||
|
"""
|
||||||
|
model = _StubModel(inner_model_type="ModelType.FLOW")
|
||||||
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
|
def test_inner_model_type_flux_string_is_flow(self):
|
||||||
|
model = _StubModel(inner_model_type="ModelType.FLUX")
|
||||||
|
assert NRS()._get_pred_type(model) == PredictionType.FLOW
|
||||||
|
|
||||||
def test_unrecognized_model_defaults_to_eps(self):
|
def test_unrecognized_model_defaults_to_eps(self):
|
||||||
"""Fully-unrecognized models fall back to EPS (documented default)."""
|
"""Fully-unrecognized models fall back to EPS (documented default)."""
|
||||||
model = _StubModel()
|
model = _StubModel()
|
||||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertToVSpaceIdentityBranches:
|
||||||
|
"""FLOW and V are both pure identity conversions -- no ε<->v algebra runs,
|
||||||
|
so plain sentinel objects (no real tensor math) are enough to prove it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_flow_convert_is_identity(self):
|
||||||
|
node = NRS()
|
||||||
|
cond, uncond = object(), object()
|
||||||
|
x_div, v_cond, v_uncond = node._convert_to_v_space(
|
||||||
|
object(), object(), object(), cond, uncond, PredictionType.FLOW
|
||||||
|
)
|
||||||
|
assert x_div is None
|
||||||
|
assert v_cond is cond
|
||||||
|
assert v_uncond is uncond
|
||||||
|
|
||||||
|
def test_flow_finalize_is_identity(self):
|
||||||
|
node = NRS()
|
||||||
|
x_final = object()
|
||||||
|
result = node._finalize_from_v_space(object(), None, x_final, object(), object(), PredictionType.FLOW)
|
||||||
|
assert result is x_final
|
||||||
|
|
||||||
|
def test_v_convert_is_identity(self):
|
||||||
|
"""Regression guard: PR-3 must not disturb the existing V path."""
|
||||||
|
node = NRS()
|
||||||
|
cond, uncond = object(), object()
|
||||||
|
x_div, v_cond, v_uncond = node._convert_to_v_space(
|
||||||
|
object(), object(), object(), cond, uncond, PredictionType.V
|
||||||
|
)
|
||||||
|
assert x_div is None
|
||||||
|
assert v_cond is cond
|
||||||
|
assert v_uncond is uncond
|
||||||
|
|
||||||
|
def test_v_finalize_is_identity(self):
|
||||||
|
node = NRS()
|
||||||
|
x_final = object()
|
||||||
|
result = node._finalize_from_v_space(object(), None, x_final, object(), object(), PredictionType.V)
|
||||||
|
assert result is x_final
|
||||||
|
|
||||||
|
def test_eps_convert_still_performs_algebra(self):
|
||||||
|
"""Regression guard: EPS must still run the ε->v conversion (x_div gets
|
||||||
|
computed, and cond/uncond are transformed rather than passed through).
|
||||||
|
"""
|
||||||
|
node = NRS()
|
||||||
|
x_orig, sig_root, sigma = MagicMock(), MagicMock(), MagicMock()
|
||||||
|
cond, uncond = MagicMock(), MagicMock()
|
||||||
|
x_div, v_cond, v_uncond = node._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond, PredictionType.EPS)
|
||||||
|
assert x_div is not None
|
||||||
|
assert v_cond is not cond
|
||||||
|
assert v_uncond is not uncond
|
||||||
|
|
||||||
|
def test_eps_finalize_still_performs_algebra(self):
|
||||||
|
node = NRS()
|
||||||
|
x_orig, x_div, x_final, sig_root, sigma = (
|
||||||
|
MagicMock(),
|
||||||
|
MagicMock(),
|
||||||
|
MagicMock(),
|
||||||
|
MagicMock(),
|
||||||
|
MagicMock(),
|
||||||
|
)
|
||||||
|
result = node._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma, PredictionType.EPS)
|
||||||
|
assert result is not x_final
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tests for NRS package version metadata and the patch()-time version/pred-type log line."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path for imports
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
import NRS # noqa: E402
|
||||||
|
import NRS.nodes_NRS as nodes_NRS # noqa: E402, N812
|
||||||
|
|
||||||
|
_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_is_nonempty_semver_string():
|
||||||
|
"""NRS.__version__ must be importable and look like a X.Y.Z version."""
|
||||||
|
assert isinstance(NRS.__version__, str)
|
||||||
|
assert NRS.__version__
|
||||||
|
assert _SEMVER_RE.match(NRS.__version__), f"__version__ {NRS.__version__!r} is not X.Y.Z"
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_matches_pyproject():
|
||||||
|
"""__version__ must be kept in lock-step with pyproject.toml's version field."""
|
||||||
|
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
||||||
|
text = pyproject_path.read_text()
|
||||||
|
match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text)
|
||||||
|
assert match, "Could not find version in pyproject.toml"
|
||||||
|
assert NRS.__version__ == match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
class _StubModel:
|
||||||
|
"""Minimal stand-in that resolves to PredictionType.EPS via the direct-hit path."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.model_type = "eps"
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_model_sampler_cfg_function(self, fn, flag):
|
||||||
|
self._captured_fn = fn
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_logs_version_and_pred_type(caplog):
|
||||||
|
"""patch() must announce the NRS version and detected prediction type."""
|
||||||
|
node = nodes_NRS.NRS()
|
||||||
|
model = _StubModel()
|
||||||
|
|
||||||
|
with caplog.at_level("INFO"):
|
||||||
|
node.patch(model, skew=2.0, stretch=5.0, squash=0.75)
|
||||||
|
|
||||||
|
expected = f"NRS v{NRS.__version__}: prediction type detected -> {nodes_NRS.PredictionType.EPS.name}"
|
||||||
|
assert any(expected in rec.message for rec in caplog.records)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfgv"
|
name = "cfgv"
|
||||||
@@ -29,6 +29,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.29.0"
|
version = "3.29.0"
|
||||||
@@ -58,7 +70,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "negative-rejection-steering"
|
name = "negative-rejection-steering"
|
||||||
version = "0.7.4"
|
version = "1.0.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -143,10 +155,12 @@ version = "9.0.3"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "iniconfig" },
|
{ name = "iniconfig" },
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
{ name = "pluggy" },
|
{ name = "pluggy" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -172,6 +186,15 @@ version = "6.0.3"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
@@ -246,6 +269,69 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "21.3.3"
|
version = "21.3.3"
|
||||||
@@ -255,6 +341,7 @@ dependencies = [
|
|||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
{ name = "python-discovery" },
|
{ name = "python-discovery" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|||||||
Reference in New Issue
Block a user